MCP.so
登录
返回博客

Build MCP Server:用 TypeScript SDK v2 从零构建 MCP 服务器

2026年10月15日MCP.so TeamMCP.so Team

从零构建 MCP 服务器(build mcp server):设计好用的工具,用 TypeScript 编写同时支持 stdio 和 Streamable HTTP 的服务器,测试、接入 Claude Code,再发布到 npm 与 MCP Registry。

构建一个 MCP 服务器,只需要选一个官方 SDK,注册几个描述清晰、输入有类型的工具,然后通过 stdio(本地客户端)或 Streamable HTTP(远程客户端)提供服务。用 TypeScript SDK v2,大约 40 行代码就够了。之后用 MCP Inspector 测试,把它添加到 Claude Code 或 Cursor 等客户端,再发布到 npm 和 MCP Registry,让其他人也能安装。

本指南从头到尾构建一个小而真实的服务器。所有代码片段都在 @modelcontextprotocol/server 2.3.1(2026 年 10 月的当前稳定版)上实际运行过。它实现了 2026-07-28 版 MCP 规范,同时仍能响应旧版客户端。如果你对这些概念还不熟悉,建议先读一读 什么是 MCP 服务器。

先了解这些

MCP 服务器向 AI 应用暴露三类东西:

原语由谁决定使用典型例子
Tools(工具)模型调用 API、查询数据库、写文件
Resources(资源)应用文件内容、数据库 schema、文档
Prompts(提示)用户用户从菜单里选择的可复用模板

大多数服务器的主体都是工具。工具和资源的区别见 MCP 资源与工具。同一份服务器代码可以运行在两种传输方式上:

  • stdio:客户端把你的服务器作为子进程启动,通过 stdin 和 stdout 通信。这是本地工具的默认方式。
  • Streamable HTTP:你的服务器是一个 Web 端点(通常是 /mcp),可供多个客户端连接。远程服务器就是这样交付的。旧的 HTTP+SSE 传输已被弃用,详见 MCP SSE 是否已弃用。

第 1 步:写代码之前先设计工具

模型只能看到每个工具的名称、描述和输入 schema。清晰的工具会被正确调用;含糊的工具要么被忽略,要么被传入错误参数。以下几条规则适用于任何语言:

  • 按任务建模,而不是按端点建模。 一个返回有用字段的 get-package,胜过三个对原始 API 路由的薄封装。工具越少,消耗的 token 也越少。参见 MCP 工具发现。
  • 为模型写描述。 说明工具做什么、什么时候用,例如:"推荐或安装依赖之前先调用它。"
  • 为每个输入加类型和约束。 枚举、最小值/最大值以及描述中的示例,能在处理函数运行前拦下大多数错误调用。
  • 返回模型能据此行动的错误。 "找不到该包,请检查拼写"能让模型重试;堆栈跟踪则不能。
  • 标记只读工具。 readOnlyHint 等提示可以帮助客户端判断何时需要先征求用户同意。它们只是提示,不是安全机制。
  • 不要让密钥出现在参数里。 API 密钥应来自环境变量或 OAuth,绝不能来自模型。

示例服务器只有一个工具 get-package:查询 npm 包的最新版本和每周下载量,不需要 API 密钥。

第 2 步:搭建项目

需要 Node.js 20 或更高版本。

mkdir npm-info && cd npm-info
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod
npm install -D typescript tsx @types/node
mkdir src

在 v2 中,SDK 被拆成了多个包:服务器用 @modelcontextprotocol/server,客户端用 @modelcontextprotocol/client,另有 @modelcontextprotocol/node、@modelcontextprotocol/express 等可选适配器。原来的单一包 @modelcontextprotocol/sdk 属于 v1 系列。如果你在升级,请按照 官方迁移指南 操作。

添加 tsconfig.json。SDK 的 README 提到,TypeScript 6 及以上需要显式设置 "types": ["node"]:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "build",
    "rootDir": "src",
    "strict": true,
    "types": ["node"],
    "skipLibCheck": true
  },
  "include": ["src"]
}

第 3 步:编写服务器

把服务器放进一个工厂函数,由它创建全新的 McpServer。两种传输方式都复用这一个工厂函数。创建 src/server.ts:

import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

export function createServer(): McpServer {
  const server = new McpServer({ name: 'npm-info', version: '1.0.0' });

  server.registerTool(
    'get-package',
    {
      title: 'Look up an npm package',
      description:
        'Get the latest version, description and weekly downloads of an npm package. Use this before recommending or installing a dependency.',
      inputSchema: z.object({
        name: z
          .string()
          .min(1)
          .describe(
            'Exact npm package name, e.g. "zod" or "@modelcontextprotocol/server"'
          ),
      }),
      annotations: { readOnlyHint: true, openWorldHint: true },
    },
    async ({ name }) => {
      const meta = await fetch(
        `https://registry.npmjs.org/${name.replace('/', '%2F')}/latest`
      );
      if (!meta.ok) {
        return {
          content: [
            {
              type: 'text',
              text: `Package "${name}" not found on npm (HTTP ${meta.status}). Check the spelling.`,
            },
          ],
          isError: true,
        };
      }
      const pkg = (await meta.json()) as {
        version: string;
        description?: string;
      };
      const dl = await fetch(
        `https://api.npmjs.org/downloads/point/last-week/${name}`
      );
      const downloads = dl.ok
        ? ((await dl.json()) as { downloads: number }).downloads
        : undefined;
      return {
        content: [
          {
            type: 'text',
            text: `${name}@${pkg.version}: ${pkg.description ?? 'no description'}. Weekly downloads: ${downloads ?? 'unknown'}.`,
          },
        ],
      };
    }
  );

  return server;
}

Zod schema 是你唯一需要写的 schema。SDK 会把它转换成模型看到的 JSON Schema,用它校验每次调用的参数,并据此推断处理函数的参数类型。如果某次调用未通过校验,模型会收到一个 isError 结果,可以读懂并自行修正。

第 4 步:通过 stdio 提供服务

创建 src/stdio.ts:

#!/usr/bin/env node
import { serveStdio } from '@modelcontextprotocol/server/stdio';

import { createServer } from './server.js';

void serveStdio(createServer);
console.error('npm-info MCP server running on stdio');

在 stdio 模式下,stdout 承载的是协议本身。哪怕一条 console.log 也会破坏 JSON-RPC 数据流,所以日志请用 console.error 输出。

第 5 步:通过 Streamable HTTP 提供服务

要提供远程端点,用 createMcpHandler 包装同一个工厂函数。它返回一个 Web 标准的 fetch 处理器,并为每个请求创建一个新的服务器实例。在 Node 上,先安装适配器(npm install @modelcontextprotocol/node),再创建 src/http.ts:

import { createServer as createHttpServer } from 'node:http';
import {
  localhostHostValidation,
  localhostOriginValidation,
  toNodeHandler,
} from '@modelcontextprotocol/node';
import { createMcpHandler } from '@modelcontextprotocol/server';

import { createServer } from './server.js';

const handler = createMcpHandler(createServer);
const nodeHandler = toNodeHandler(handler);
const validateHost = localhostHostValidation();
const validateOrigin = localhostOriginValidation();

createHttpServer((req, res) => {
  if (!validateHost(req, res) || !validateOrigin(req, res)) return;
  void nodeHandler(req, res);
}).listen(3000, '127.0.0.1', () =>
  console.error('npm-info MCP server on http://127.0.0.1:3000/mcp')
);

处理器本身不检查 Host 或 Origin 请求头,也不校验 token,因此前面的两个 localhost 守卫用来阻止 DNS 重绑定攻击。运行 npx tsx src/http.ts,再用 curl 检查:

curl -s -X POST http://127.0.0.1:3000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

你会收到一个 SSE message 事件,里面包含 get-package 的定义。这个处理器是无状态的,所以放在任何负载均衡器后面都能横向扩展。如何公开托管并在前面加上 OAuth,见 如何部署 MCP 服务器。

第 6 步:用 MCP Inspector 测试

MCP Inspector 是官方测试客户端。针对 stdio 入口打开 Web 界面:

npx @modelcontextprotocol/inspector npx tsx src/stdio.ts

点击 Connect,打开 Tools,用 zod 运行 get-package,再试一个无效的包名,看看错误路径。在 CI 中可以使用 CLI 模式:

npx @modelcontextprotocol/inspector --cli npx tsx src/stdio.ts \
  --method tools/call --tool-name get-package --tool-arg name=@modelcontextprotocol/server

第 7 步:接入 Claude Code 和 Cursor

本地客户端只需要一条启动命令。对于 Claude Code,把启动命令全部放在 -- 之后:

claude mcp add npm-info -- npx tsx /absolute/path/to/npm-info/src/stdio.ts

在会话中运行 /mcp,确认已连接。对于 Cursor,把它加到 .cursor/mcp.json:

{
  "mcpServers": {
    "npm-info": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/npm-info/src/stdio.ts"]
    }
  }
}

如果用 HTTP 版本,就让客户端指向 URL,例如 claude mcp add --transport http npm-info http://127.0.0.1:3000/mcp。如果客户端看不到你的工具,先手动运行启动命令:它应该向 stderr 打印一行,然后等待。崩溃,或者有任何内容打印到 stdout,就是问题所在。

第 8 步:发布到 npm 和 MCP Registry

要让任何人都能用 npx 运行你的服务器,先编译,再添加 bin 字段。在 package.json 中:

{
  "name": "@your-scope/npm-info-mcp",
  "version": "1.0.0",
  "type": "module",
  "bin": { "npm-info-mcp": "build/stdio.js" },
  "files": ["build"],
  "mcpName": "io.github.your-username/npm-info",
  "scripts": { "build": "tsc" }
}

运行 npm run build 和 npm publish --access public。之后用户就能用 npx -y @your-scope/npm-info-mcp 启动它。

官方 MCP Registry 只存储元数据,它通过 mcpName 字段验证你拥有该 npm 包。按照 Registry 快速入门:

brew install mcp-publisher        # or download a release binary
mcp-publisher init                # generates server.json
mcp-publisher login github        # namespace io.github.your-username/*
mcp-publisher publish

server.json 中的 name 必须与 mcpName 一致,description 最多 100 个字符。纯远程服务器可以完全跳过 npm,改为列出一个类型为 streamable-http 的 remotes 条目。

第 9 步:在 MCP.so 上架

最后,把你的服务器提交到 MCP.so,让浏览 MCP 服务器目录 的用户能找到它。附上一份清晰的 README,写明安装命令、所需的环境变量和工具列表。用户在 判断一个服务器是否值得信任 时,读的就是这份 README。

其他语言

同样的设计规则和传输方式适用于所有官方 SDK。TypeScript 和 Python 使用最广泛;C#、Go、Rust 和 Ruby 也属于 Tier 1;此外还有 Java、Swift、PHP 和 Kotlin(SDK 列表)。Python 用户请看我们的 Python MCP 服务器指南,其中使用官方 SDK 的 MCPServer 类和 uv。

下一步

常见问题

如何构建自己的 MCP 服务器?

选择 TypeScript 或 Python 等官方 SDK,注册几个描述清晰、输入有类型的工具,然后通过 stdio(本地客户端)或 Streamable HTTP(远程客户端)提供服务。之后用 MCP Inspector 测试,再添加到 Claude Code 或 Cursor 等客户端。

用什么语言构建 MCP 服务器最好?

TypeScript 和 Python 的官方 SDK 使用最广泛,示例也最多。C#、Go、Rust 和 Ruby 也是 Tier 1 官方 SDK,所以通常最好的选择是你现有代码和团队已经在用的语言。

MCP 服务器应该用 stdio 还是 Streamable HTTP?

服务器运行在用户本机、由客户端作为子进程启动时,用 stdio。如果你托管一个供许多用户连接的端点,就用 Streamable HTTP;新服务器不要再使用已弃用的 HTTP+SSE 传输。

如何发布 MCP 服务器供他人使用?

把包发布到 npm 或 PyPI,让用户可以用 npx 或 uvx 运行,再用 mcp-publisher CLI 把元数据登记到官方 MCP Registry。你也可以提交到 MCP.so 等目录,方便用户发现。