本文转自Github开源项目,github.com/modelcontex... ,由dogstar整理翻译成中文分享,方便更多开发者快速掌握开发MCP的技术。已经学会的请跳过。
关于MCP TS SDK
是官方 TypeScript SDK,用于开发 Model Context Protocol(MCP)的服务端与客户端交互。
以下是本文的知识点:
概述
模型上下文协议(MCP)允许应用程序以标准化方式为LLM提供上下文,将上下文提供与实际的LLM交互分离。此TypeScript SDK完整实现了MCP规范,使您可以:
- 构建可连接到任何MCP服务器的客户端
- 创建暴露资源和工具的MCP服务器
- 使用标准传输协议(如标准输入输出和可流式传输的HTTP)
- 处理所有MCP协议消息和生命周期事件
安装
bash
npm install @modelcontextprotocol/sdk
快速入门
让我们创建一个简单的MCP服务器,暴露计算器工具和一些数据:
typescript
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// 创建MCP服务器
const server = new McpServer({
name: "demo-server",
version: "1.0.0"
});
// 添加加法工具
server.registerTool("add",
{
title: "加法工具",
description: "将两个数字相加",
inputSchema: { a: z.number(), b: z.number() }
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
// 添加动态问候资源
server.registerResource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
{
title: "问候资源",
description: "动态问候生成器"
},
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `你好,${name}!`
}]
})
);
// 通过标准输入输出接收和发送消息
const transport = new StdioServerTransport();
await server.connect(transport);
什么是MCP?
模型上下文协议(MCP)是允许您构建以安全、标准化方式向LLM应用程序提供数据和功能的服务器。可以将其视为专门为LLM交互设计的Web API。MCP服务器能够:
- 通过资源暴露数据(类似于REST API的GET端点;用于加载信息到LLM上下文)
- 通过工具提供功能(类似于POST端点;用于执行代码或产生副作用)
- 通过提示定义交互模式(LLM交互的可重用模板)
- 以及更多功能!
核心概念
Server服务端
McpServer 是您与MCP协议交互的核心接口。它处理连接管理、协议合规性和消息路由:
typescript
const server = new McpServer({
name: "my-app",
version: "1.0.0"
});
Resources资源
资源是您向LLMs暴露数据的方式。它们类似于REST API中的GET端点------提供数据但不应执行重大计算或有副作用:
typescript
// 静态资源
server.registerResource(
"config",
"config://app",
{
title: "应用程序配置",
description: "应用程序配置数据",
mimeType: "text/plain"
},
async (uri) => ({
contents: [{
uri: uri.href,
text: "App configuration here"
}]
})
);
// 动态资源与参数
server.registerResource(
"user-profile",
new ResourceTemplate("users://{userId}/profile", { list: undefined }),
{
title: "用户资料",
description: "用户个人信息"
},
async (uri, { userId }) => ({
contents: [{
uri: uri.href,
text: `Profile data for user ${userId}`
}]
})
);
// 带上下文感知补全的资源
server.registerResource(
"repository",
new ResourceTemplate("github://repos/{owner}/{repo}", {
list: undefined,
complete: {
// 基于已解析参数提供智能补全
repo: (value, context) => {
if (context?.arguments?.["owner"] === "org1") {
return ["project1", "project2", "project3"].filter(r => r.startsWith(value));
}
return ["default-repo"].filter(r => r.startsWith(value));
}
}
}),
{
title: "GitHub仓库",
description: "仓库信息"
},
async (uri, { owner, repo }) => ({
contents: [{
uri: uri.href,
text: `Repository: ${owner}/${repo}`
}]
})
);
Tools工具
工具允许LLMs通过您的服务器采取行动。与资源不同,工具预期会执行计算并产生副作用:
typescript
// 带参数的简单工具
server.registerTool(
"calculate-bmi",
{
title: "BMI计算器",
description: "计算身体质量指数",
inputSchema: {
weightKg: z.number(),
heightM: z.number()
}
},
async ({ weightKg, heightM }) => ({
content: [{
type: "text",
text: String(weightKg / (heightM * heightM))
}]
})
);
// 异步工具与外部API调用
server.registerTool(
"fetch-weather",
{
title: "天气查询器",
description: "获取城市的天气数据",
inputSchema: { city: z.string() }
},
async ({ city }) => {
const response = await fetch(`https://api.weather.com/${city}`);
const data = await response.text();
return {
content: [{ type: "text", text: data }]
};
}
);
// 返回ResourceLink的工具
server.registerTool(
"list-files",
{
title: "列出文件",
description: "列出项目文件",
inputSchema: { pattern: z.string() }
},
async ({ pattern }) => ({
content: [
{ type: "text", text: `找到匹配"${pattern}"的文件:` },
// ResourceLink允许工具返回引用而无须嵌入完整内容
{
type: "resource_link",
uri: "file:///project/README.md",
name: "README.md",
mimeType: "text/markdown",
description: '自述文件'
},
{
type: "resource_link",
uri: "file:///project/src/index.ts",
name: "index.ts",
mimeType: "text/typescript",
description: '索引文件'
}
]
})
);
ResourceLink资源链接
工具可以通过返回 ResourceLink 对象引用资源而无需嵌入完整内容。这对于处理大文件或大量资源时的性能至关重要------客户端可以选择性地通过提供的URI读取所需资源。
Prompts提示词
Prompts 是可复用的模板,帮助 LLM 有效与您的服务器交互:
typescript
import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
// 注册代码审查提示
server.registerPrompt(
"review-code",
{
title: "代码审查",
description: "检查代码的最佳实践和潜在问题",
argsSchema: { code: z.string() }
},
({ code }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `请审查以下代码:\n\n${code}`
}
}]
})
);
// 带上下文感知补全的提示
server.registerPrompt(
"team-greeting",
{
title: "团队问候",
description: "为团队成员生成问候语",
argsSchema: {
department: completable(z.string(), (value) => {
// 部门建议
return ["工程部", "销售部", "市场部", "支持部"].filter(d => d.startsWith(value));
}),
name: completable(z.string(), (value, context) => {
// 基于所选部门的姓名建议
const department = context?.arguments?.["department"];
if (department === "工程部") {
return ["爱丽丝", "鲍勃", "查理"].filter(n => n.startsWith(value));
} else if (department === "销售部") {
return ["大卫", "夏娃", "弗兰克"].filter(n => n.startsWith(value));
} else if (department === "市场部") {
return ["格蕾丝", "亨利", "艾里斯"].filter(n => n.startsWith(value));
}
return ["访客"].filter(n => n.startsWith(value));
})
}
},
({ department, name }) => ({
messages: [{
role: "assistant",
content: {
type: "text",
text: `你好 ${name},欢迎加入${department}团队!`
}
}]
})
);
自动补全(Completions)
MCP支持参数补全功能,帮助用户填写提示参数和资源模板参数。具体用法可参考资源补全
和提示补全的示例。
客户端用法
typescript
// 请求任何参数的补全
const result = await client.complete({
ref: {
type: "ref/prompt", // 或 "ref/resource"
name: "example" // 或 uri: "template://..."
},
argument: {
name: "argumentName",
value: "partial" // 用户已输入的内容
},
context: { // 可选:包含已解析的先前参数
arguments: {
previousArg: "value"
}
}
});
显示名称与元数据(Metadata)
所有资源、工具和提示均支持可选的title字段以优化UI展示。title作为显示名称,name仍保持唯一标识符。
注意:新代码推荐使用register*方法(如:registerTool、registerPrompt、registerResource),旧方法(tool、prompt、resource)仍保留以兼容旧版。
工具的标题优先级
对于工具,可通过两种方式指定标题:
- 工具配置中的title字段
- 使用旧版tool()方法时的annotations.title字段
优先级顺序:title → annotations.title → name
typescript
// 使用registerTool(推荐)
server.registerTool("my_tool", {
title: "我的工具", // 优先显示
annotations: {
title: "注解标题" // 若title存在则忽略
}
}, handler);
// 使用带注解的旧版API
server.tool("my_tool", "描述", {
title: "注解标题" // 此处生效
}, handler);
客户端可通过工具函数获取规范显示名称:
typescript
import { getDisplayName } from "@modelcontextprotocol/sdk/shared/metadataUtils.js";
// 自动处理优先级:title → annotations.title → name
const displayName = getDisplayName(tool);
运行你的MCP服务器
stdio标准输入输出
MCP支持通过标准输入输出(STDIO)传输消息,这是最简单的本地开发方式。使用 StdioServerTransport 即可快速启动,适用于命令行工具和直接集成:
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "示例服务器",
version: "1.0.0"
});
// ... 配置服务器资源、工具和提示 ...
const transport = new StdioServerTransport();
await server.connect(transport);
可流式传输的HTTP
适用于远程服务器,需配置支持客户端请求和服务器通知的流式HTTP传输层:
带会话管理(有状态)
通过会话管理实现状态保持:
typescript
import express from "express";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
const app = express();
app.use(express.json());
// 会话ID与传输层映射表
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
// 处理客户端请求
app.post('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// 复用现有会话
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// 新建会话
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
transports[sessionId] = transport;
}
});
// 清理会话
transport.onclose = () => {
if (transport.sessionId) delete transports[transport.sessionId];
};
const server = new McpServer({
name: "示例服务器",
version: "1.0.0"
});
// ... 配置服务器资源、工具和提示 ...
await server.connect(transport);
} else {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: '错误请求:无效的会话ID'
}
});
return;
}
await transport.handleRequest(req, res, req.body);
});
// 处理GET/DELETE请求
const handleSessionRequest = async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('无效或缺失的会话ID');
return;
}
await transports[sessionId].handleRequest(req, res);
};
app.get('/mcp', handleSessionRequest);
app.delete('/mcp', handleSessionRequest);
app.listen(3000);
无会话管理(无状态)
无状态方法适用于以下场景:
简单API包装器
适用于无需状态管理的轻量级服务封装,例如将现有REST API快速转换为MCP服务。
RESTful独立请求场景
每个请求包含完整上下文信息,无需依赖会话状态,符合REST无状态设计原则无共享会话状态的。
水平扩展
通过负载均衡实现横向扩展,每个请求独立处理,避免会话数据同步问题。
适用于简单场景的示例:
typescript
const app = express();
app.use(express.json());
app.post('/mcp', async (req, res) => {
try {
const server = getServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined
});
res.on('close', () => {
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: '内部服务器错误'
}
});
}
});
// 处理不支持的HTTP方法
app.get('/mcp', async (req, res) => {
res.status(405).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "方法不允许"
}
});
});
app.listen(3000, () => {
console.log(`无状态MCP服务器运行在端口3000`);
});
适用场景对比
特性 | 有状态会话模式 | 无状态模式 |
---|---|---|
会话保持 | ✅ 支持跨请求上下文关联 | ❌ 每个请求独立处理 |
资源消耗 | ❌ 较高(需维护会话状态) | ✅ 较低 |
横向扩展能力 | ❌ 受会话存储限制 | ✅ 支持负载均衡 |
典型用例 | 长时交互的复杂任务流程 | 简单API调用/微服务集成 |
测试与调试
使用 @modelcontextprotocol/sdk/testing 包中的工具可以轻松模拟MCP客户端并与服务器交互:
typescript
import { createTestClient } from "@modelcontextprotocol/sdk/testing.js";
const client = createTestClient(server);
const response = await client.sendMessage({
type: "tool_call",
toolName: "add",
args: { a: 2, b: 3 }
});
console.log(response.content); // 输出: [{ type: "text", text: "5" }]
一些MCP示例
简单的Echo输出 服务配置示例
以下是一个简单的MCP服务器示例,它会将客户端发送的消息原样返回:
typescript
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
// 创建MCP服务器实例
const server = new McpServer({
name: "echo-server", // 服务器名称
version: "1.0.0" // 版本号
});
// 注册回声资源
server.registerResource(
"echo", // 资源标识符
new ResourceTemplate("echo://{message}", { list: undefined }), // 资源URI模板
{
title: "回声资源", // 显示名称
description: "通过资源形式回显消息" // 功能描述
},
async (uri, { message }) => ({
contents: [{
uri: uri.href, // 资源URI
text: `资源回显: ${message}` // 响应内容
}]
})
);
// 注册回声工具
server.registerTool(
"echo", // 工具标识符
{
title: "回声工具", // 显示名称
description: "通过工具形式回显消息", // 功能描述
inputSchema: { message: z.string() } // 输入验证规则
},
async ({ message }) => ({
content: [{ type: "text", text: `工具回显: ${message}` }] // 响应内容
})
);
// 注册回声提示模板
server.registerPrompt(
"echo", // 提示标识符
{
title: "回声提示模板", // 显示名称
description: "创建消息处理提示模板", // 功能描述
argsSchema: { message: z.string() } // 参数验证规则
},
({ message }) => ({
messages: [{
role: "user", // 角色类型
content: {
type: "text", // 内容类型
text: `请处理消息: ${message}` // 提示内容
}
}]
})
);
SQLite数据库操作
以下是一个暴露SQLite数据库查询功能的MCP服务器示例:
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import sqlite3 from "sqlite3";
import { promisify } from "util";
import { z } from "zod";
// 创建MCP服务器实例
const server = new McpServer({
name: "sqlite-explorer", // 服务器名称
version: "1.0.0" // 版本号
});
// 数据库连接辅助函数
const getDb = () => {
const db = new sqlite3.Database("database.db");
return {
all: promisify<string, any[]>(db.all.bind(db)), // 将回调转换为Promise
close: promisify(db.close.bind(db))
};
};
// 注册数据库Schema资源
server.registerResource(
"schema", // 资源标识符
"schema://main", // 资源URI模板
{
title: "数据库Schema", // 显示名称
description: "SQLite数据库结构定义", // 功能描述
mimeType: "text/plain" // 内容类型
},
async (uri) => {
const db = getDb();
try {
// 查询所有表结构
const tables = await db.all(
"SELECT sql FROM sqlite_master WHERE type='table'"
);
return {
contents: [{
uri: uri.href, // 资源URI
text: tables.map((t: {sql: string}) => t.sql).join("\n") // 合并表结构
}]
};
} finally {
await db.close(); // 确保连接关闭
}
}
);
// 注册SQL查询工具
server.registerTool(
"query", // 工具标识符
{
title: "SQL查询工具", // 显示名称
description: "执行任意SQL语句并返回结果", // 功能描述
inputSchema: { sql: z.string() } // 输入验证规则
},
async ({ sql }) => {
const db = getDb();
try {
// 执行用户输入的SQL
const results = await db.all(sql);
return {
content: [{
type: "text", // 内容类型
text: JSON.stringify(results, null, 2) // 格式化输出
}]
};
} catch (err: unknown) {
const error = err as Error;
return {
content: [{
type: "text", // 错误信息类型
text: `错误: ${error.message}` // 错误提示
}],
isError: true // 标记错误状态
};
} finally {
await db.close(); // 确保连接关闭
}
}
);
高级用法
动态服务器
动态服务器允许根据运行时配置(如环境变量、数据库状态)动态注册资源和工具。适用于需要灵活扩展的场景:
typescript
const server = new McpServer({ name: "dynamic-server", version: "1.0.0" });
// 根据环境变量动态注册工具
if (process.env.NODE_ENV === "production") {
server.registerTool("prod-analytics",
{ /* 工具配置 */ },
async () => { /* 生产环境逻辑 */ }
);
} else {
server.registerTool("dev-debug",
{ /* 工具配置 */ },
async () => { /* 开发环境逻辑 */ }
);
}
低级服务器
对于需要完全控制协议细节的场景,可以使用低级服务器API直接处理原始消息:
typescript
import { LowLevelServer } from "@modelcontextprotocol/sdk/server/low-level.js";
const server = new LowLevelServer({
onConnect: (connection) => {
console.log("新客户端连接");
},
onMessage: async (connection, message) => {
switch (message.type) {
case "tool_call":
const result = await handleToolCall(message);
connection.send({ type: "tool_response", id: message.id, result });
break;
// 处理其他消息类型...
}
}
});
编写MCP客户端
您可以使用官方提供的 @modelcontextprotocol/sdk/client 包构建自定义客户端,或直接通过HTTP/STDIO与服务器交互:
typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
// 创建传输层实例
const transport = new StdioClientTransport({
command: "node", // 执行命令
args: ["server.js"] // 命令参数
});
// 初始化客户端
const client = new Client(
{
name: "示例客户端", // 客户端名称
version: "1.0.0" // 版本号
}
);
// 建立连接
await client.connect(transport);
// 列出提示模板
const prompts = await client.listPrompts();
// 获取指定提示模板
const prompt = await client.getPrompt({
name: "示例提示模板", // 提示模板名称
arguments: {
arg1: "参数值" // 输入参数
}
});
// 列出资源模板
const resources = await client.listResources();
// 读取指定资源
const resource = await client.readResource({
uri: "file:///示例.txt" // 资源URI
});
// 调用工具
const result = await client.callTool({
name: "示例工具", // 工具名称
arguments: {
arg1: "参数值" // 输入参数
}
});
开发要求
- Node.js ≥ 18.0.0
- npm ≥ 9.0.0
- TypeScript ≥ 5.0.0