Skip to content
Conduit

mcp server

mcp-toolkit

TypeScript suite of Model Context Protocol (MCP) servers on a shared typed mini-framework: read-only SQLite explorer, web-research, and a reminder scheduler.

KE-NETIZEN-OOPS/mcp-toolkitcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

Before you start

  • Node.js 20 or newer
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "ke-netizen-oops-mcp-toolkit": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-toolkit"
      ]
    }
  }
}

same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12

Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs

README

synced Oct 23, 2026

mcp-toolkit

A suite of Model Context Protocol (MCP) servers built on a small, shared, typed mini-framework. One core defines how a tool is declared (a Zod input schema + a typed handler) and how tools are registered on an MCP server; three production-shaped servers are built on top of it:

  • sqlite-explorer — open a SQLite database and explore it read-only: list tables/views, describe a table, and run a single SELECT (every write/DDL statement is rejected before it reaches the driver).
  • web-research — fetch a URL and reduce it to readable text, plus a search tool. All network access sits behind a Fetcher / SearchProvider interface.
  • scheduler — create / list / cancel simple in-memory reminders, with time supplied by an injectable Clock.
import { z } from 'zod';
import { ToolkitServer, defineTool, jsonResult } from 'mcp-toolkit/core';

const add = defineTool({
  name: 'add',
  description: 'Add two numbers.',
  schema: z.object({ a: z.number(), b: z.number() }), // input is fully typed
  execute: ({ a, b }) => jsonResult({ sum: a + b }),
});

const server = new ToolkitServer({ name: 'demo', version: '0.1.0', tools: [add] });

// Drive it directly in a test — no transport, no network:
await server.callTool('add', { a: 2, b: 3 }); // -> { content: [{ type:'text', text:'{ "sum": 5 }' }] }
// ...or serve it over stdio to a real MCP client via server.connect(transport).

Why it exists

MCP servers are easy to start and hard to test, because the interesting logic usually ends up entangled with a transport, a database driver, the network, or the wall clock. mcp-toolkit draws those boundaries explicitly:

  • One authoring surface. defineTool ties a Zod schema to a handler whose argument is statically z.infer<typeof schema>. The same schema is converted to JSON Schema for the MCP tools/list response — one source of truth.
  • Handlers are callable without a transport. ToolkitServer implements tools/list and tools/call in terms of its own public listTools() / callTool() methods, so the entire test suite exercises real handler code paths with zero sockets and zero mocking of the SDK.
  • Every external dependency is injected. The DB connection (DatabaseOpener), HTTP (Fetcher), search (SearchProvider), and time (Clock) are all interfaces. Production wiring lives only in the three bin.ts entry points; tests pass deterministic fakes. The whole suite runs offline with no keys.

Architecture

flowchart TD
  subgraph core["core (shared mini-framework)"]
    DT["defineTool(schema, execute)"]
    SC["toInputSchema (Zod to JSON Schema)"]
    TS["ToolkitServer\nregistry + tools/list + tools/call\nZod validation, error wrapping"]
  end

  subgraph servers["MCP servers"]
    SQL["sqlite-explorer\nopen / list / describe / run_query"]
    WEB["web-research\nfetch_url / search"]
    SCH["scheduler\ncreate / list / cancel"]
  end

  subgraph boundaries["injected boundaries (mocked in tests)"]
    DB[("DatabaseOpener\n(better-sqlite3, read-only)")]
    FET["Fetcher / SearchProvider\n(global fetch, DuckDuckGo)"]
    CLK["Clock\n(SystemClock)"]
  end

  DT --> TS
  SC --> TS
  TS --> SQL
  TS --> WEB
  TS --> SCH
  SQL --> DB
  WEB --> FET
  SCH --> CLK

  SQL -->|stdio bin| MCP["MCP client\n(e.g. Claude Desktop)"]
  WEB -->|stdio bin| MCP
  SCH -->|stdio bin| MCP

Quickstart

npm install
npm run typecheck   # tsc --noEmit
npm test            # vitest run  (offline, deterministic)
npm run build       # tsc -> dist/

Run a server over stdio

Each server ships a stdio entry point (the way MCP clients launch local tools):

node dist/servers/scheduler/bin.js
node dist/servers/sqlite-explorer/bin.js
node dist/servers/web-research/bin.js

Example MCP client config (e.g. Claude Desktop mcpServers):

{
  "mcpServers": {
    "scheduler":       { "command": "node", "args": ["/abs/path/dist/servers/scheduler/bin.js"] },
    "sqlite-explorer": { "command": "node", "args": ["/abs/path/dist/servers/sqlite-explorer/bin.js"] },
    "web-research":    { "command": "node", "args": ["/abs/path/dist/servers/web-research/bin.js"] }
  }
}

Tool reference

ServerToolInputNotes
sqlite-exploreropen_database{ path }Opens read-only; replaces any open DB.
sqlite-explorerlist_tables{}User tables + views.
sqlite-explorerdescribe_table{ table }Columns via PRAGMA table_info.
sqlite-explorerrun_query{ sql }Single SELECT/WITH only; capped at 1000 rows.
web-researchfetch_url{ url }HTML reduced to readable text + title.
web-researchsearch{ query, limit? }Via injected SearchProvider.
schedulercreate_reminder{ message, delaySeconds }Returns the reminder + id.
schedulerlist_reminders{ status? }Status computed against the clock.
schedulercancel_reminder{ id }Cannot cancel an already-fired reminder.

Read-only SQL guard

run_query is the security boundary of sqlite-explorer. The connection is opened with better-sqlite3's readonly: true, and the SQL is validated before execution: comments and string/identifier literals are stripped first (so a column literally named drop or a comment containing DELETE can't fool the scanner), then the statement must be a single SELECT/WITH, with no stacked statements and no write/DDL/PRAGMA keywords. See src/servers/sqlite-explorer/sql-guard.ts and its tests.

Scope — what this is and isn't

Is:

  • A working, typed mini-framework for MCP tool servers and three real servers on top of it, with a 45-test offline suite and a Node 18/20/22 CI matrix.
  • A demonstration of clean dependency boundaries: DB, HTTP, search, and clock are all injectable, which is what makes the suite deterministic and key-free.
  • Wired to the official @modelcontextprotocol/sdk; the stdio entry points are real and have been smoke-tested with live JSON-RPC initialize + tools/list.

Isn't:

  • A production data gateway. sqlite-explorer's guard is a deliberately strict allow-list for read-only exploration, not a general SQL sandbox; don't point it at untrusted databases as a substitute for OS-level permissions.
  • A full Readability/boilerplate-removal engine. web-research's HTML→text is a dependency-free reducer (drop scripts/styles, collapse tags, decode common entities); it won't match a browser's reader mode on complex pages.
  • A durable scheduler. Reminders live in memory for the process lifetime and fire by comparison against the clock on query — there is no background timer, persistence, or delivery mechanism.
  • A bundled search backend. web-research ships a DuckDuckGo HTML provider as a realistic default, but search is unconfigured unless you inject a provider; the network path is intentionally never exercised by the tests.

Project layout

src/
  core/                     # defineTool, Zod->JSON Schema, ToolkitServer
  servers/
    sqlite-explorer/        # db.ts (opener iface), sql-guard.ts, server.ts, bin.ts
    web-research/           # fetcher.ts, search.ts, extract.ts, server.ts, bin.ts
    scheduler/              # clock.ts, store.ts, server.ts, bin.ts
tests/                      # core + one suite per server (offline, deterministic)

License

MIT © 2026 Abed Kamau

↑↓ navigate openesc close