首页 › tutorials › TS造GitHub检索MCP:REST直封装

概述

本教程带你用官方 @modelcontextprotocol/sdk(TypeScript 版,v1.29.0)把一个真实的公开 REST API(GitHub API)封装成 MCP 服务器,让 Claude Code 能直接调用。你将掌握三步核心模式(建实例→注册工具→接传输)、zod v4 强类型校验与分页、InMemoryTransport 同进程测试、StdioServerTransport 接入 Claude Code、以及MCP Inspector 可视化调试。适合已会用 Node.js/TypeScript、想让 Claude Code 拥有自定义数据能力的开发者,全程可运行代码,约 60 分钟跑通。

前置条件

本教程与已发布的「Python 造 MCP 三原语」「Go 造 MCP」「底层 API 造 MCP」完全互补:本篇聚焦 TypeScript SDK 的高层 McpServer API + zod v4 + REST 封装 + 同进程测试闭环,是上述几篇未覆盖的角度。


第一步:理解 MCP 的「三步核心模式」

在动手前,先记住这张心智模型。官方 TypeScript SDK 把一个 MCP 服务器的生命周期压缩成三步,理解它你就掌握了 90% 的 MCP 开发:

flowchart TD
    A["Step 1<br/>new McpServer(name, version)"] --> B["Step 2<br/>server.tool(name, desc, zodSchema, handler)"]
    B --> C["Step 3<br/>server.connect(transport)"]
    C --> D1["InMemoryTransport<br/>同进程自动化测试"]
    C --> D2["StdioServerTransport<br/>接入 Claude Code / Desktop"]
    C --> D3["Streamable HTTP / SSE<br/>远程团队共享部署"]

MCP 本质是 JSON-RPC 2.0 协议:Host(Claude Code)作为 Client,通过传输层把你的 Server 当作一个子进程拉起,双方按行交换 JSON 消息。SDK 已经把帧解析、握手、能力协商全包了,你只需写业务 handler。

第二步:搭建工程骨架

mkdir gh-mcp && cd gh-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init

预期输出关键行:

added 92 packages, and audited 93 packages in 2s
found 0 vulnerabilities

这里你会拿到 @modelcontextprotocol/[email protected] 与 [email protected]。然后改 package.json,两个坑必须避开:

{
  "name": "gh-mcp",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js",
    "test": "tsx test/in-memory.test.ts"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "tsx": "^4.0.0",
    "typescript": "^5.0.0"
  }
}

坑一(致命):"type": "module" 不可省。SDK 是纯 ESM 包,少了它 Node 无法解析 @modelcontextprotocol/sdk/server/mcp.js 这类子路径导入。

tsconfig.json 关键项:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "outDir": "dist",
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

moduleResolution 用 bundler 或 node16 都行,但不要用老式 node,否则解析不了 ESM 子路径。

第三步:第一个工具——get_repo_info

新建 src/server.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Step 1: 建实例
const server = new McpServer({
  name: "gh-mcp",
  version: "1.0.0",
});

// Step 2: 注册第一个工具
server.tool(
  "get_repo_info",                                    // 工具名
  "Fetch a GitHub repository's basic info (stars, forks, description, language) by owner/repo, e.g. 'facebook/react'.", // 描述:模型靠它决定何时调用
  {
    owner: z.string().describe("GitHub owner, e.g. 'facebook'"),
    repo: z.string().describe("Repository name, e.g. 'react'"),
  },
  async ({ owner, repo }) => {
    const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
      headers: { "Accept": "application/vnd.github+json", "User-Agent": "gh-mcp" },
    });
    if (!res.ok) {
      return {
        content: [{ type: "text", text: `API error: ${res.status} ${res.statusText}` }],
        isError: true,
      };
    }
    const r = await res.json() as any;
    const summary = [
      `Full name: ${r.full_name}`,
      `Description: ${r.description ?? "(none)"}`,
      `Language: ${r.language ?? "unknown"}`,
      `Stars: ${r.stargazers_count}  Forks: ${r.forks_count}  Open issues: ${r.open_issues_count}`,
      `Default branch: ${r.default_branch}`,
      `Homepage: ${r.homepage ?? "(none)"}`,
    ].join("\n");
    return { content: [{ type: "text", text: summary }] };
  }
);

// Step 3: 接入 stdio 传输(本地部署模式)
const transport = new StdioServerTransport();
await server.connect(transport);

server.tool() 的四个参数依次是:工具名、描述、zod schema、async handler。handler 必须返回标准结构:

{ content: [{ type: "text", text: "..." }] }

content 数组里每一项都有 type,除了 text 还可以是 image(带 base64)或 resource(引用资源),但 90% 场景 text 足够。

预期结果:npm run build && node dist/server.js 启动后,进程会阻塞等待,没有任何输出——这是对的。stdio 服务器在等 Client 发 JSON-RPC 消息。Ctrl+C 退出即可。

坑二(最常见):永远不要在 handler 或顶层用 console.log()。stdout 是 JSON-RPC 专用通道,任何杂质输出都会污染协议流、直接破坏通信。要打日志就用 console.error()(走 stderr)或写文件:

import { appendFileSync } from "node:fs";
const log = (msg: string) => appendFileSync("debug.log", `${msg}\n`);

第四步:zod v4 强类型校验与分页

GitHub 搜索接口天然带分页,正好演示 zod 的进阶用法。再加一个 search_repos 工具:

server.tool(
  "search_repos",
  "Search public GitHub repositories by keyword. Returns name, stars, short description and URL.",
  {
    query: z.string().min(1).max(200).describe("Search keywords, e.g. 'state machine typescript'"),
    language: z
      .string()
      .describe("Filter by primary language, e.g. 'typescript'. Omit for any language.")
      .optional(),
    limit: z
      .number()
      .int()
      .min(1)
      .max(10)
      .default(5)
      .describe("Max results to return (1-10)"),
    page: z
      .number()
      .int()
      .min(1)
      .default(1)
      .describe("Page number for pagination, 1-based"),
  },
  async ({ query, language, limit, page }) => {
    const params = new URLSearchParams({
      q: language ? `${query} language:${language}` : query,
      sort: "stars",
      order: "desc",
      per_page: String(limit),
      page: String(page),
    });
    const res = await fetch(`https://api.github.com/search/repositories?${params}`, {
      headers: { "Accept": "application/vnd.github+json", "User-Agent": "gh-mcp" },
    });
    if (!res.ok) {
      return {
        content: [{ type: "text", text: `API error: ${res.status} ${res.statusText}` }],
        isError: true,
      };
    }
    const data = (await res.json()) as any;
    const items = (data.items ?? []).slice(0, limit);
    if (items.length === 0) {
      return { content: [{ type: "text", text: `No results for: ${query}` }] };
    }
    const lines = items.map(
      (it: any, i: number) =>
        `${i + 1}. ${it.full_name}  ⭐${it.stargazers_count}\n   ${it.description ?? "(no description)"}\n   ${it.html_url}`,
    );
    const header = `Found ${data.total_count} repos (showing page ${page}, ${items.length} results):\n\n`;
    return { content: [{ type: "text", text: header + lines.join("\n\n") }] };
  }
);

几个 zod v4 要点:

第五步:InMemoryTransport 同进程测试闭环

stdio 模式不好做自动化测试(要起子进程)。SDK 提供了 InMemoryTransport,让你在同一个进程内完成 Client↔Server 的往返测试,无需任何外部依赖。新建 test/in-memory.test.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { z } from "zod";

// 复用真实 server 构建逻辑(实际项目应抽成 buildServer() 函数导入)
const server = new McpServer({ name: "gh-mcp", version: "1.0.0" });
server.tool(
  "get_repo_info",
  "Fetch repo info",
  { owner: z.string(), repo: z.string() },
  async ({ owner, repo }) => {
    const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
      headers: { "User-Agent": "gh-mcp" },
    });
    const r = (await res.json()) as any;
    return {
      content: [{ type: "text", text: `${r.full_name}: ⭐${r.stargazers_count}` }],
    };
  },
);

// 创建一对内存传输(client 端 + server 端,互相直连)
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);

const client = new Client({ name: "test-client", version: "1.0.0" });
await client.connect(clientTransport);

// 1) 列出已注册工具
const { tools } = await client.listTools();
console.log("=== Tools registered ===");
tools.forEach((t) => console.log(` - ${t.name}: ${t.description}`));

// 2) 调用工具
const result = await client.callTool({
  name: "get_repo_info",
  arguments: { owner: "anthropics", repo: "anthropic-sdk-python" },
});
console.log("\n=== Result ===");
console.log(result.content[0].text);

await client.close();

运行 npm test,预期输出:

=== Tools registered ===
 - get_repo_info: Fetch repo info

=== Result ===
anthropics/anthropic-sdk-python: ⭐2987

InMemoryTransport.createLinkedPair() 返回一对直连的传输端,绕开了 stdio/HTTP 的所有基建。这是 CI 里验证 MCP 工具行为最干净的方式——把它接进你现有的测试框架即可。

第六步:错误处理——别 throw,返回 isError

新手最容易犯的错是在 handler 里 throw new Error()。MCP 约定是返回错误,而不是抛异常。不同客户端对抛出的异常处理方式不一致,有的会直接断开连接。正确写法:

async ({ owner, repo }) => {
  try {
    const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
      headers: { "User-Agent": "gh-mcp" },
    });
    if (res.status === 404) {
      return {
        content: [{ type: "text", text: `Repository not found: ${owner}/${repo}` }],
        isError: true, // 显式标记为错误,客户端会按错误处理
      };
    }
    if (res.status === 403) {
      return {
        content: [{ type: "text", text: "GitHub API rate limit hit. Wait a minute and retry." }],
        isError: true,
      };
    }
    if (!res.ok) {
      return {
        content: [{ type: "text", text: `API error: ${res.status} ${res.statusText}` }],
        isError: true,
      };
    }
    const r = await res.json();
    return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
  } catch (err) {
    return {
      content: [
        { type: "text", text: `Network error: ${err instanceof Error ? err.message : String(err)}` },
      ],
      isError: true,
    };
  }
}

isError: true 让客户端(Claude Code)知道这是一次失败调用,从而决定是重试还是向用户报告。区分 404(仓库不存在,模型应换个关键词)与 403(限流,模型应等待)能让模型做出更聪明的决策。

GitHub 公开 API 未认证时每小时只有 60 次配额,开发期很容易撞墙。如果你有自己的 Token,可在请求头加 Authorization: Bearer <token>,配额提升到 5000/小时——但注意绝不要把 Token 硬编码进代码或提交进 git,从环境变量读取:

const token = process.env.GITHUB_TOKEN;
const headers: Record<string, string> = { "User-Agent": "gh-mcp" };
if (token) headers.Authorization = `Bearer ${token}`;

第七步:接入 Claude Code

TypeScript 项目可以跳过编译,直接用 tsx 跑。Claude Code 提供 CLI 一键注册:

# 方式一:CLI(推荐)
claude mcp add gh-mcp -- npx tsx /绝对路径/gh-mcp/src/server.ts

# 验证已注册
claude mcp list

预期 claude mcp list 输出:

gh-mcp ✓ gh-mcp 1.0.0
  - get_repo_info: Fetch a GitHub repository's basic info...
  - search_repos: Search public GitHub repositories by keyword...

方式二:项目根目录 .mcp.json(适合团队共享,提交进仓库):

{
  "mcpServers": {
    "gh-mcp": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/gh-mcp/src/server.ts"]
    }
  }
}

坑四(路径):.mcp.json 里的 args 必须用绝对路径或可靠的相对解析。Claude Code 把 server 当子进程拉起,工作目录不一定是你期望的位置。

注册后,在 Claude Code 里直接提问即可触发工具,例如:

帮我查一下 anthropics/courses 这个仓库的基本信息和星标数,再搜一下"agent eval typescript"语言为 typescript 的热门仓库,给我前 5 个。

Claude Code 会自动调用 get_repo_info 和 search_repos,把结果整理给你。用 /mcp 命令可查看连接状态与工具清单。

第八步:MCP Inspector 可视化调试

开发期,MCP Inspector 是 GUI 调试利器,能在浏览器里直观看到所有工具、它们的 zod schema(渲染成表单),并直接填参调用:

npx @modelcontextprotocol/inspector node dist/server.js

打开 http://localhost:5173:

注意事项:Inspector 要求 Node 18+;首次运行要安装自身依赖,首次启动约需 30–60 秒,别以为卡死了。

sequenceDiagram
    participant Dev as 开发者
    participant Insp as MCP Inspector (5173)
    participant Server as gh-mcp Server (stdio)
    participant GH as GitHub API

    Dev->>Insp: 填参数 owner/repo
    Insp->>Server: JSON-RPC tools/call
    Server->>GH: GET /repos/:owner/:repo
    GH-->>Server: repo JSON
    Server-->>Insp: { content: [{text}] }
    Insp-->>Dev: 渲染响应

实战案例:完整可运行的 GitHub 检索 MCP

把前面所有片段拼成一个完整文件 src/server.ts,含 3 个工具 + 统一错误处理:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "gh-mcp", version: "1.0.0" });

// 统一的请求头构造(可选 Token)
function ghHeaders(): Record<string, string> {
  const h: Record<string, string> = {
    Accept: "application/vnd.github+json",
    "User-Agent": "gh-mcp",
  };
  if (process.env.GITHUB_TOKEN) h.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
  return h;
}

// 通用错误返回
function err(msg: string) {
  return { content: [{ type: "text" as const, text: msg }], isError: true };
}

// 工具 1:仓库详情
server.tool(
  "get_repo_info",
  "Fetch a GitHub repository's basic info (stars, forks, description, language) by owner/repo.",
  { owner: z.string().describe("GitHub owner, e.g. 'facebook'"),
    repo: z.string().describe("Repository name, e.g. 'react'") },
  async ({ owner, repo }) => {
    try {
      const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers: ghHeaders() });
      if (res.status === 404) return err(`Repository not found: ${owner}/${repo}`);
      if (!res.ok) return err(`API error: ${res.status} ${res.statusText}`);
      const r = (await res.json()) as any;
      const text = [
        `Full name: ${r.full_name}`,
        `Description: ${r.description ?? "(none)"}`,
        `Language: ${r.language ?? "unknown"}`,
        `Stars: ${r.stargazers_count}  Forks: ${r.forks_count}  Issues: ${r.open_issues_count}`,
        `URL: ${r.html_url}`,
      ].join("\n");
      return { content: [{ type: "text", text }] };
    } catch (e) {
      return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
    }
  },
);

// 工具 2:搜索仓库(带分页)
server.tool(
  "search_repos",
  "Search public GitHub repositories by keyword with optional language filter and pagination.",
  {
    query: z.string().min(1).max(200).describe("Search keywords"),
    language: z.string().describe("Primary language filter, e.g. 'typescript'. Omit for any.").optional(),
    limit: z.number().int().min(1).max(10).default(5).describe("Max results (1-10)"),
    page: z.number().int().min(1).default(1).describe("1-based page number"),
  },
  async ({ query, language, limit, page }) => {
    const params = new URLSearchParams({
      q: language ? `${query} language:${language}` : query,
      sort: "stars", order: "desc",
      per_page: String(limit), page: String(page),
    });
    const res = await fetch(`https://api.github.com/search/repositories?${params}`, { headers: ghHeaders() });
    if (res.status === 403) return err("Rate limited. Wait and retry, or set GITHUB_TOKEN.");
    if (!res.ok) return err(`API error: ${res.status} ${res.statusText}`);
    const data = (await res.json()) as any;
    const items = (data.items ?? []).slice(0, limit);
    if (!items.length) return { content: [{ type: "text", text: `No results for: ${query}` }] };
    const text = items
      .map((it: any, i: number) => `${i + 1}. ${it.full_name} ⭐${it.stargazers_count}\n   ${it.description ?? ""}\n   ${it.html_url}`)
      .join("\n\n");
    return { content: [{ type: "text", text: `Found ${data.total_count} (page ${page}):\n\n${text}` }] };
  },
);

// 工具 3:用户公开资料
server.tool(
  "get_user_profile",
  "Fetch a GitHub user's public profile (name, bio, followers, public repos).",
  { username: z.string().describe("GitHub username") },
  async ({ username }) => {
    const res = await fetch(`https://api.github.com/users/${username}`, { headers: ghHeaders() });
    if (res.status === 404) return err(`User not found: ${username}`);
    if (!res.ok) return err(`API error: ${res.status} ${res.statusText}`);
    const u = (await res.json()) as any;
    const text = [
      `Login: ${u.login}`,
      `Name: ${u.name ?? "(none)"}`,
      `Bio: ${u.bio ?? "(none)"}`,
      `Company: ${u.company ?? "(none)"}`,
      `Location: ${u.location ?? "(none)"}`,
      `Followers: ${u.followers}  Following: ${u.following}`,
      `Public repos: ${u.public_repos}`,
      `URL: ${u.html_url}`,
    ].join("\n");
    return { content: [{ type: "text", text }] };
  },
);

const transport = new StdioServerTransport();
await server.connect(transport);

构建并接入:

npm run build
claude mcp add gh-mcp -- node /绝对路径/gh-mcp/dist/server.js
claude mcp list

之后在 Claude Code 里问「torvalds 这个 GitHub 用户是谁?他有多少粉丝?」模型会自动调用 get_user_profile。

常见问题速查

现象 原因 解决
Cannot find module .../server/mcp.js 缺 "type": "module" 或 moduleResolution 不对 package.json 加 "type":"module",tsconfig 用 bundler
claude mcp list 里 server 标红 ✗ 子进程启动失败或 stdout 被污染 用 Inspector 单独跑确认能起;排查是否有 console.log
调用工具卡住无响应 handler 里 throw 或死循环 改成返回 isError;加超时
GitHub 频繁返回 403 未认证 60 次/小时配额耗尽 设 GITHUB_TOKEN 环境变量
zod schema 报类型错误 用了 v3 写法 .describe() 放 .optional() 之前;升级到 v4 写法
Inspector 首次启动很久 正在装依赖 等 30–60 秒,确保 Node 18+

延伸阅读

训练营适用性评估

参考来源

  1. Build an MCP Server in TypeScript — Official SDK Tutorial (jangwook.net, 2026-05) — 用 @modelcontextprotocol/sdk v1.29.0 + Zod v4 从零到 stdio/HTTP 的完整可运行教程,含 InMemoryTransport 测试与 Inspector 调试
  2. Building Custom MCP Servers for Claude Code: A Developer's Guide (asepalazhari.com) — 真实工程视角的 MCP 开发指南,含内部 API 封装、错误处理与多工具组织实践
  3. Building MCP Servers: Custom Context for Claude Code (SitePoint) — 讲解 MCP 如何让 Claude Code 从通用助手变为上下文感知协作者,含 Claude Code 接入配置
  4. modelcontextprotocol/typescript-sdk (GitHub) — 官方 TypeScript SDK 源码与 README,McpServer/StdioServerTransport/InMemoryTransport 的权威 API 参考
  5. MCP TypeScript SDK 官方文档 (ts.sdk.modelcontextprotocol.io) — SDK 全量 API 文档、传输层选型与工具/资源/Prompts 三原语说明

本文由「51学通信」原创整理,主理人爱卫生(微信:gprshome201101)。MCP 是 Claude Code 生态的核心扩展机制——一次封装,Claude、Cursor、Windsurf 等所有支持 MCP 的客户端通用。下一篇将进入 Round 4:Claude Code Agent 模式与自动化,讲解 -p 无头模式、hooks 与批量任务编排。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入