mcpkit

mcpkit

mcpkit enables rapid creation of MCP servers with Zod-based tool definitions, automatic schema generation, and built-in validation, reducing boilerplate significantly.

Category
访问服务器

README

mcpkit

ci release license node

The TypeScript toolkit for building MCP servers without the boilerplate.

Define a tool with a Zod schema and a handler. Get a working Model Context Protocol server back — schema generation, input validation, error envelopes, transport wiring, all done.

import { defineServer, defineTool } from 'mcpkit';
import { z } from 'zod';

const server = defineServer({
  name: 'demo',
  version: '0.1.0',
  tools: [
    defineTool({
      name: 'add',
      description: 'Add two numbers.',
      input: z.object({ a: z.number(), b: z.number() }),
      handler: ({ a, b }) => `${a + b}`,
    }),
  ],
});

await server.start();

That's a real, functioning MCP server. Run it with mcpkit dev and point any MCP-aware client at it.


why this exists

Writing an MCP server with the official SDK is fine, but you end up doing the same plumbing every time:

  • declaring the tool list in one place
  • declaring a separate JSON Schema for each tool
  • writing a switch over tool names in the call handler
  • coercing handler returns into the protocol's content envelope
  • wiring up a transport
  • catching errors and converting them into the right isError shape

mcpkit collapses all of that into defineTool + defineServer. The schema is generated from your Zod type, validation runs before your handler, errors turn into proper protocol responses, and a string return becomes a text content block. You stay in the layer that actually matters — what the tool does — and skip the layer that doesn't.

with vs without

Same tool, written against the bare SDK and against mcpkit:

<table> <tr> <th>bare sdk</th> <th>mcpkit</th> </tr> <tr> <td>

const server = new Server(
  { name: 'demo', version: '0.1.0' },
  { capabilities: { tools: {} } },
);

server.setRequestHandler(
  ListToolsRequestSchema,
  async () => ({
    tools: [
      {
        name: 'add',
        description: 'Add two numbers.',
        inputSchema: {
          type: 'object',
          properties: {
            a: { type: 'number' },
            b: { type: 'number' },
          },
          required: ['a', 'b'],
        },
      },
    ],
  }),
);

server.setRequestHandler(
  CallToolRequestSchema,
  async (req) => {
    if (req.params.name === 'add') {
      const { a, b } = req.params.arguments as {
        a: number; b: number;
      };
      return {
        content: [{ type: 'text', text: `${a + b}` }],
      };
    }
    throw new Error('unknown tool');
  },
);

await server.connect(new StdioServerTransport());

</td> <td>

const server = defineServer({
  name: 'demo',
  version: '0.1.0',
  tools: [
    defineTool({
      name: 'add',
      description: 'Add two numbers.',
      input: z.object({
        a: z.number(),
        b: z.number(),
      }),
      handler: ({ a, b }) => `${a + b}`,
    }),
  ],
});

await server.start();

</td> </tr> </table>

The right column has the same wire-level behavior, plus input validation, plus typed handler arguments, plus an isError envelope on uncaught throws.

install

npm install mcpkit zod

Or scaffold a fresh project (recommended for a first server):

npx mcpkit create my-server
cd my-server
npm run dev

You'll get a small project with a working stdio server, three example tools, and a tsconfig.json set up for strict mode. Replace the example tools with yours and ship.

the cli

mcpkit create [target]   scaffold a new server from a template
mcpkit dev               run with hot reload (uses tsx under the hood)
mcpkit build             compile to dist/
mcpkit inspect           launch the official inspector against your server

create ships with four templates today:

template what you get
stdio-basic local MCP server over stdio. most clients want this.
http-streaming network-reachable server over the streamable HTTP transport.
with-fetch stdio server with HTTP-fetching tools (timeouts wired in).
with-sqlite stdio server with a SQLite-backed CRUD example (better-sqlite3, WAL).

the api

defineTool

defineTool({
  name: string,            // [a-zA-Z0-9_-]+
  description: string,     // shown to the client / LLM
  input: z.ZodType,        // Zod schema; converted to JSON Schema for you
  handler: (input) => string | ToolContent | ToolContent[] | { content, isError? }
})

The handler input is fully typed via z.infer. Returning a string wraps it as a single text content block — that's the common case. Throwing inside a handler turns into an isError: true response automatically; if you want to shape the error message, pass an onToolError handler to defineServer.

defineServer

defineServer({
  name: string,
  version: string,
  description?: string,
  tools?: ToolDefinition[],
  resources?: ResourceDefinition[],
  prompts?: PromptDefinition[],
  onToolError?: (err, toolName) => ToolResult,
  onEvent?: (event: ServerEvent) => void,
})

Returns a DefinedServer with:

  • .start({ transport: 'stdio' }) — connect a transport and serve.
  • .connect(transport) — connect a transport instance you constructed yourself (HTTP, custom, anything that quacks like a Transport).
  • .stop() — close the active transport and the underlying server.
  • .raw — the underlying SDK Server if you need to do something exotic.

resources and prompts

Same declarative shape:

defineResource({
  uri: 'file:///etc/hosts',
  name: 'hosts',
  mimeType: 'text/plain',
  read: async () => ({ text: await fs.readFile('/etc/hosts', 'utf8') }),
});

definePrompt({
  name: 'summarize',
  description: 'Summarize a chunk of text.',
  arguments: z.object({ text: z.string() }),
  build: ({ text }) => ({
    messages: [{ role: 'user', content: { type: 'text', text: `Summarize:\n${text}` } }],
  }),
});

observability

onEvent gets a structured callback for every tool call, resource read, and prompt fetch — start time, end time, latency, error, a per-call requestId to correlate. You can plug it into anything: pino, console, OpenTelemetry, your homemade aggregator. There's also a built-in for the simple case:

import { defineServer, consoleLogger, jsonLogger } from 'mcpkit';

const server = defineServer({
  name: 'demo',
  version: '0.1.0',
  onEvent: consoleLogger(),    // → pretty stderr lines
  // or: onEvent: jsonLogger() // → one JSON object per line, on stderr
  tools: [...]
});

Logging always goes to stderr — stdout is reserved for protocol traffic on stdio transports.

testing

mcpkit/testing exposes an in-process client that talks to your server over an in-memory transport — no subprocess, no stdio piping, no flaky process teardown. Same client a real consumer would use, just routed through RAM.

import { describe, it, expect } from 'vitest';
import { createTestClient, expectToolError, snapshotTools } from 'mcpkit/testing';
import { server } from '../src/index.js';

describe('add', () => {
  it('adds', async () => {
    const client = await createTestClient(server);
    const result = await client.callTool('add', { a: 2, b: 3 });
    expect(result.text).toBe('5');
    expect(result.isError).toBe(false);
    await client.close();
  });

  it('rejects bad input', async () => {
    const client = await createTestClient(server);
    const text = await expectToolError(client, 'add', { a: 'nope', b: 1 });
    expect(text).toMatch(/invalid/i);
    await client.close();
  });

  it("doesn't drift its public surface", () => {
    expect(snapshotTools(server)).toMatchSnapshot();
  });
});

design choices worth knowing

Zod, not raw JSON Schema. You write the type once. Validation, generated JSON Schema for the protocol, and TypeScript inference for the handler all fall out of the same source. Trying to keep three definitions in sync is the boilerplate this project exists to delete.

Errors are values, not exceptions. A handler that throws becomes an isError: true content envelope. The client sees a sensible response instead of a transport-level failure. If you'd rather format the error yourself, override onToolError.

Transport-agnostic core. The same defineServer works over stdio, the streamable HTTP transport, the in-memory test transport, or anything else that implements the SDK's Transport interface. The http-streaming template shows the wiring.

Strict mode by default. Templates ship with strict: true and noUncheckedIndexedAccess. The library itself compiles under the same settings. If you find a hole in the types, that's a bug.

Listener errors are swallowed. If your onEvent handler throws, your tool calls keep working. Observability bugs shouldn't be load-bearing.

faq

Does this lock me into mcpkit forever? No. Every helper has an escape hatch — server.raw gives you the underlying SDK Server, and you can setRequestHandler on it directly if you need something the kit doesn't model yet. The kit is a layer on top, not a replacement.

Why Zod 3 and not 4? Zod 4 is great but the ecosystem (notably zod-to-json-schema) is still catching up. We'll move when it's stable in production. If you're already on Zod 4, the schema interfaces are compatible enough — file an issue if you hit a wall.

Does it support resources and prompts, not just tools? Yes. defineResource and definePrompt are first-class. They're less commonly used than tools, so most examples lead with tools — but the wiring is identical.

Streamable HTTP, SSE, both? Streamable HTTP. The older HTTP+SSE flavor is still in the SDK but is being phased out — if you have a reason to need it, defineServer is transport- agnostic and you can pass any Transport instance via .connect().

Production-ready? The library is small and the surface is intentionally narrow. The official SDK does the heavy lifting underneath. Pin a version, write tests for your tools (the in-process client makes this easy), and you're set.

what this is not

  • not a hosted service. you build, you deploy.
  • not an agent framework. it builds the server side of MCP, not the client.
  • not opinionated about your domain. tools are functions; what they do is your problem.

roadmap

  • more templates (oauth-protected, edge runtime, drizzle/postgres).
  • a mcpkit publish command that lints + packages + tags a release.
  • richer testing helpers (fuzz a tool's input, schema diff against a baseline).
  • optional OpenTelemetry adapter for onEvent.

If something's missing, open an issue with a sketch of the API you'd want.

license

MIT.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选