用 TypeScript SDK 实现 MCP Server
High Contrast
Dark Mode
Light Mode
Sepia
Forest
2 min read317 words

用 TypeScript SDK 实现 MCP Server

TypeScript SDK 是 MCP Server 的另一主流实现方式,特别适合 Node.js 生态项目、需要强类型保障的团队,以及与现有 JavaScript 工具链集成的场景。

环境准备

# 初始化项目
mkdir my-mcp-server && cd my-mcp-server
npm init -y
# 安装依赖
npm install @modelcontextprotocol/sdk
npm install --save-dev typescript @types/node ts-node
# 初始化 TypeScript 配置
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext

tsconfig.json 关键配置

{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}

package.json 添加脚本

{
"scripts": {
"build": "tsc",
"dev": "ts-node src/index.ts",
"start": "node dist/index.js"
},
"type": "module"
}

完整实战:Notion 工作区 MCP Server

以下是一个连接 Notion API 的 MCP Server,展示 TypeScript SDK 的完整用法:

// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
TextContent,
} from "@modelcontextprotocol/sdk/types.js";
// === 类型定义 ===
interface NotionPage {
id: string;
title: string;
url: string;
last_edited: string;
}
interface SearchResult {
pages: NotionPage[];
has_more: boolean;
}
// === API 客户端 ===
class NotionClient {
private baseUrl = "https://api.notion.com/v1";
private token: string;
constructor(token: string) {
this.token = token;
}
private async request<T>(
path: string,
method: "GET" | "POST" = "GET",
body?: object
): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
Authorization: `Bearer ${this.token}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Notion API 错误:${response.status} - ${error.message}`);
}
return response.json() as Promise<T>;
}
async searchPages(query: string, maxResults = 10): Promise<SearchResult> {
const data = await this.request<any>("/search", "POST", {
query,
filter: { property: "object", value: "page" },
page_size: maxResults,
});
const pages: NotionPage[] = data.results.map((item: any) => ({
id: item.id,
title:
item.properties?.title?.title?.[0]?.plain_text ||
item.properties?.Name?.title?.[0]?.plain_text ||
"无标题",
url: item.url,
last_edited: item.last_edited_time,
}));
return { pages, has_more: data.has_more };
}
async getPageContent(pageId: string): Promise<string> {
const blocks = await this.request<any>(`/blocks/${pageId}/children`);
return blocks.results
.filter((b: any) => b.type === "paragraph" || b.type === "heading_1" || b.type === "heading_2")
.map((b: any) => {
const texts = b[b.type]?.rich_text?.map((t: any) => t.plain_text).join("") || "";
return b.type.startsWith("heading") ? `## ${texts}` : texts;
})
.join("\n\n");
}
}
// === MCP Server ===
const server = new Server(
{ name: "notion-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// 延迟初始化 Notion 客户端(避免启动时 API Token 未准备好)
let notionClient: NotionClient;
function getClient(): NotionClient {
if (!notionClient) {
const token = process.env.NOTION_TOKEN;
if (!token) throw new Error("NOTION_TOKEN 环境变量未设置");
notionClient = new NotionClient(token);
}
return notionClient;
}
// === 工具定义 ===
const TOOLS: Tool[] = [
{
name: "search_notion_pages",
description: `
搜索 Notion 工作区中的页面。
适用场景:用户询问"Notion 里有没有关于XXX的文档"或"找一下XX项目的页面"。
返回:匹配的页面标题、链接和最后修改时间。
`.trim(),
inputSchema: {
type: "object" as const,
properties: {
query: {
type: "string",
description: "搜索关键词",
},
max_results: {
type: "number",
default: 10,
minimum: 1,
maximum: 50,
description: "返回结果数量,默认10",
},
},
required: ["query"],
},
},
{
name: "get_notion_page_content",
description: `
获取 Notion 页面的文本内容。
通常在 search_notion_pages 之后调用,读取某个页面的具体内容。
page_id 来自搜索结果的 id 字段。
`.trim(),
inputSchema: {
type: "object" as const,
properties: {
page_id: {
type: "string",
description: "Notion 页面 ID(从搜索结果获取)",
},
},
required: ["page_id"],
},
},
];
// === 工具处理 ===
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const client = getClient();
try {
if (name === "search_notion_pages") {
const { query, max_results = 10 } = args as {
query: string;
max_results?: number;
};
const result = await client.searchPages(query, max_results);
if (result.pages.length === 0) {
return {
content: [
{
type: "text" as const,
text: `在 Notion 工作区中未找到与 "${query}" 相关的页面。`,
},
],
};
}
const lines = [
`找到 ${result.pages.length} 个相关页面${result.has_more ? "(还有更多)" : ""}:\n`,
...result.pages.map(
(page, i) =>
`${i + 1}. **${page.title}**\n   ID: \`${page.id}\`\n   链接: ${page.url}\n   最后修改: ${new Date(page.last_edited).toLocaleDateString("zh-CN")}`
),
];
return {
content: [{ type: "text" as const, text: lines.join("\n") }],
};
}
if (name === "get_notion_page_content") {
const { page_id } = args as { page_id: string };
const content = await client.getPageContent(page_id);
return {
content: [
{
type: "text" as const,
text: content || "页面内容为空或不包含文本块。",
},
],
};
}
throw new Error(`未知工具: ${name}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text" as const, text: `执行出错:${message}` }],
isError: true,
};
}
});
// === 启动 ===
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
// 不要用 console.log(会干扰 stdio 通信),使用 console.error 写日志
console.error("Notion MCP Server 已启动");
}
main().catch(console.error);

构建和配置

# 构建
npm run build
# 配置到 Claude Desktop
{
"mcpServers": {
"notion": {
"command": "node",
"args": ["/path/to/my-mcp-server/dist/index.js"],
"env": {
"NOTION_TOKEN": "secret_xxx"
}
}
}
}

Python vs TypeScript 选型建议

场景 推荐语言 原因
数据处理、科学计算 Python 生态(pandas/numpy/sklearn)
快速原型,小工具 Python + FastMCP 代码量最少
与 Node.js 项目集成 TypeScript 同一生态,共享类型
团队有 TypeScript 规范 TypeScript 强类型降低 bug 率
调用 Web API 两者均可 语法相似,按团队习惯
系统级操作(进程、文件) Python subprocess、pathlib 更成熟

常见 TypeScript 陷阱

// ❌ 错误:不要在 stdio 模式下用 console.log
console.log("Server started")  // 这会污染 stdio 通信流
// ✅ 正确:使用 console.error 写日志(输出到 stderr)
console.error("Server started")
// ❌ 错误:同步执行耗时操作
server.setRequestHandler(CallToolRequestSchema, (request) => {
const data = fs.readFileSync("big-file.json")  // 阻塞!
})
// ✅ 正确:始终使用 async/await
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const data = await fs.promises.readFile("big-file.json")
})

本节执行清单


下一章:第 08 章 — MCP 工作流安全、权限与生产化