Skip to content

使用 TypeScript SDK 开发 MCP 服务器

MCP 服务器开发——使用 TypeScript SDK

Section titled “MCP 服务器开发——使用 TypeScript SDK”

本教程将引导您使用 TypeScript SDK 构建 MCP (Model Context Protocol) 服务器。MCP 服务器向 MCP 客户端暴露资源 (Resources,数据源)、提示 (Prompts,生成模型的接口) 和工具 (Tools,可调用函数)。

使用 TypeScript SDK 开发 MCP 服务器得益于强类型、高效处理并发请求的异步/等待 (async/await) 模型,以及定义服务器能力的清晰结构。

先决条件:

  • Node.js (推荐 18.x 或更高版本)。
  • 包管理器,如 npm 或 yarn。

安装:

安装 MCP TypeScript SDK:

Terminal window
# 使用 npm
npm install @modelcontext/mcp-sdk
# 或使用 yarn
yarn add @modelcontext/mcp-sdk

初始化服务器: MCP 服务器的核心是一个 MCPServer 实例,它通过 createServer 函数创建。然后,您需要定义其能力 (capabilities) 并启动它以监听客户端连接。

import { createServer, MCPServer, ResourceContext, PromptContext, ToolContext, MCPErrorData } from '@modelcontext/mcp-sdk';
async function initializeAndStartServer() {
console.log('正在初始化 TypeScript MCP 服务器...');
const server: MCPServer = createServer({
serverId: 'my-first-ts-mcp-server',
serverDescription: '一个使用 TypeScript 构建的 MCP 演示服务器。',
// 定义此服务器提供的能力。客户端可以查看这些能力。
capabilities: {
resources: [
{ id: 'serverTime', description: '提供当前服务器时间。' },
// 可以在此处声明更多资源
],
prompts: [
{ id: 'simpleEcho', description: '将提供的文本原样返回 (echo)。' },
// 可以在此处声明更多提示
],
tools: [
{ id: 'stringUtil', description: '执行字符串操作。' },
// 可以在此处声明更多工具
]
}
});
// 资源、提示和工具的处理程序将在后续章节中添加。
// 现在,我们先添加一个占位符。
addServerHandlers(server);
const port = 8080;
const host = '0.0.0.0'; // 监听所有可用的网络接口
try {
await server.listen({ port, host });
console.log(`TypeScript MCP Server listening on ws://${host}:${port}${server.path}`);
console.log('已注册的能力:', JSON.stringify(server.getCapabilities(), null, 2));
} catch (error) {
console.error('启动 MCP 服务器失败:', error);
process.exit(1);
}
// 处理优雅关机(可选但推荐)
process.on('SIGINT', async () => {
console.log('\n正在优雅关闭 MCP 服务器...');
await server.close();
console.log('服务器已关闭。');
process.exit(0);
});
return server;
}
function addServerHandlers(server: MCPServer) {
// 我们将在后续章节中填充此函数。
console.log('添加资源、提示和工具处理程序的占位符。');
}
// 运行服务器(处理程序将在稍后定义):
// initializeAndStartServer();

资源是数据提供者。资源处理程序是一个异步函数,它接收一个 ResourceContext 对象(包含来自客户端的参数、客户端 ID 等)并返回数据。您可以返回单个有效载荷 (payload) 或将数据流式传输回客户端。

使用 server.addResource<ParamsType, PayloadType>(resourceId, handler) 来定义资源。ParamsType 是客户端参数的预期类型,而 PayloadType 是您将在 payload 字段中返回的数据类型。

// 在您的 addServerHandlers 函数或类似设置位置:
// 服务器实例: MCPServer
// 示例 1:一个返回当前服务器时间的简单资源(单次响应)
interface ServerTimeParams {} // 不期望任何参数
interface ServerTimePayload { currentTime: string; timezone: string; }
server.addResource<ServerTimeParams, ServerTimePayload>('serverTime',
async (context: ResourceContext<ServerTimeParams>) => {
console.log(`资源 'serverTime' 被客户端: ${context.clientId} 请求`);
return {
payload: {
currentTime: new Date().toISOString(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
}
};
}
);
// 示例 2:一个从模拟文件流式传输行的资源(流式响应)
interface FileStreamParams { filePath: string; lines?: number }
interface FileLinePayload { line: string; lineNumber: number }
server.addResource<FileStreamParams, FileLinePayload>('fileStream',
async (context: ResourceContext<FileStreamParams>) => {
const { filePath, lines = 5 } = context.params;
console.log(`资源 'fileStream'(针对 '${filePath}')被: ${context.clientId} 请求`);
// 这是一个流式资源,因此我们不直接返回。
// 我们使用 context.stream.write() 和 context.stream.end()。
(async () => { // 立即执行函数表达式 (IIFE),允许在同步处理程序注册中进行异步操作
try {
for (let i = 0; i < lines; i++) {
// 模拟带延迟地读取行
await new Promise(resolve => setTimeout(resolve, 200));
const fileLine = `Line ${i + 1} from ${filePath} (simulated)`;
context.stream.write({ payload: { line: fileLine, lineNumber: i + 1 } });
}
context.stream.end();
console.log(`文件流 'fileStream'(针对 ${filePath})的流式传输已完成`);
} catch (error) {
console.error('fileStream 期间发生错误:', error);
context.stream.error({ code: 500, message: 'Streaming failed' });
}
})();
// 注意:对于流式资源,主处理函数本身不直接返回值。
// 它负责启动流式操作。
}
);

提示通常涉及将输入发送到模型(例如 LLM)并获取生成的响应。提示处理程序接收一个 PromptContext,可以返回单个有效载荷或流式传输响应(例如,逐个 token)。

使用 server.addPrompt<ParamsType, PayloadType>(promptId, handler)。

// 在您的 addServerHandlers 函数或类似设置位置:
// 服务器实例: MCPServer
// 示例 1:一个简单的回显提示(单次响应)
interface EchoParams { message: string; prefix?: string; }
interface EchoPayload { echoedMessage: string; timestamp: string; }
server.addPrompt<EchoParams, EchoPayload>('simpleEcho',
async (context: PromptContext<EchoParams>) => {
const { message, prefix = 'Echo' } = context.params;
console.log(`提示 'simpleEcho'(针对客户端 ${context.clientId},消息:"${message}")`);
if (!message) {
throw { code: 400, message: '参数 "message" 是必需的。' } as MCPErrorData;
}
return {
payload: {
echoedMessage: `${prefix}: ${message}`,
timestamp: new Date().toISOString()
}
};
}
);
// 示例 2:一个模拟 LLM 提示,流式传输单词(流式响应)
interface GenerateTextParams { topic: string; wordCount?: number; }
interface TextTokenPayload { token: string; isFinal?: boolean; }
server.addPrompt<GenerateTextParams, TextTokenPayload>('textGenerator',
async (context: PromptContext<GenerateTextParams>) => {
const { topic, wordCount = 10 } = context.params;
console.log(`提示 'textGenerator'(主题 '${topic}',针对客户端 ${context.clientId})`);
(async () => {
try {
const words = `Generating a short text about ${topic}: `.split(' ');
for (const word of words) {
await new Promise(resolve => setTimeout(resolve, 100)); // 模拟生成延迟
context.stream.write({ payload: { token: word + ' ' } });
}
for (let i = 0; i < wordCount; i++) {
await new Promise(resolve => setTimeout(resolve, 150));
context.stream.write({ payload: { token: `word${i + 1} ` } });
}
context.stream.write({ payload: { token: '.', isFinal: true } });
context.stream.end();
console.log('textGenerator 的流式传输已完成');
} catch (err) {
context.stream.error({ code: 500, message: '文本生成失败' });
}
})();
}
);

工具是客户端可以执行的服务器端函数。工具处理程序接收一个 ToolContext,并应返回一个结果。工具通常不像资源或提示那样流式传输响应,但它们的执行可以是异步的。

使用 server.addTool<ParamsType, PayloadType>(toolId, handler)。

// 在您的 addServerHandlers 函数或类似设置位置:
// 服务器实例: MCPServer
// 示例:一个字符串工具
interface StringUtilParams { operation: 'uppercase' | 'lowercase' | 'length'; text: string; }
interface StringUtilPayload { result: string | number; originalText: string; }
server.addTool<StringUtilParams, StringUtilPayload>('stringUtil',
async (context: ToolContext<StringUtilParams>) => {
const { operation, text } = context.params;
console.log(`工具 'stringUtil'(操作: ${operation})被客户端 ${context.clientId} 请求,文本为:"${text}" `);
if (!text || typeof text !== 'string') {
throw { code: 400, message: '参数 "text" 必须是非空字符串。' } as MCPErrorData;
}
let operationResult: string | number;
switch (operation) {
case 'uppercase':
operationResult = text.toUpperCase();
break;
case 'lowercase':
operationResult = text.toLowerCase();
break;
case 'length':
operationResult = text.length;
break;
default:
throw { code: 400, message: `不支持的操作: ${operation}。有效操作为 'uppercase', 'lowercase', 'length'。` } as MCPErrorData;
}
return {
payload: {
result: operationResult,
originalText: text
}
};
}
);

让我们将这些概念组合到一个可运行的服务器文件中。这个服务器将暴露一个资源、一个提示和一个工具,如上所述。

import { createServer, MCPServer, ResourceContext, PromptContext, ToolContext, MCPErrorData } from '@modelcontext/mcp-sdk';
// 定义处理程序使用的接口
interface ServerTimeParams {}
interface ServerTimePayload { currentTime: string; timezone: string; }
interface EchoParams { message: string; prefix?: string; }
interface EchoPayload { echoedMessage: string; timestamp: string; }
interface StringUtilParams { operation: 'uppercase' | 'lowercase' | 'length'; text: string; }
interface StringUtilPayload { result: string | number; originalText: string; }
function addActualServerHandlers(server: MCPServer) {
// 资源:serverTime
server.addResource<ServerTimeParams, ServerTimePayload>('serverTime',
async (context: ResourceContext<ServerTimeParams>) => {
console.log(`资源 'serverTime' 被客户端: ${context.clientId} 请求`);
return { payload: { currentTime: new Date().toISOString(), timezone: Intl.DateTimeFormat().resolvedOptions().timeZone } };
}
);
// 提示:simpleEcho
server.addPrompt<EchoParams, EchoPayload>('simpleEcho',
async (context: PromptContext<EchoParams>) => {
const { message, prefix = 'Echo' } = context.params;
console.log(`提示 'simpleEcho'(针对客户端 ${context.clientId},消息:"${message}")`);
if (!message) throw { code: 400, message: '参数 "message" 是必需的。' } as MCPErrorData;
return { payload: { echoedMessage: `${prefix}: ${message}`, timestamp: new Date().toISOString() } };
}
);
// 工具:stringUtil
server.addTool<StringUtilParams, StringUtilPayload>('stringUtil',
async (context: ToolContext<StringUtilParams>) => {
const { operation, text } = context.params;
console.log(`工具 'stringUtil'(操作: ${operation})被客户端 ${context.clientId} 请求,文本为:"${text}" `);
if (!text || typeof text !== 'string') throw { code: 400, message: '参数 "text" 必须是非空字符串。' } as MCPErrorData;
let opResult: string | number;
if (operation === 'uppercase') opResult = text.toUpperCase();
else if (operation === 'lowercase') opResult = text.toLowerCase();
else if (operation === 'length') opResult = text.length;
else throw { code: 400, message: `不支持的操作: ${operation}。` } as MCPErrorData;
return { payload: { result: opResult, originalText: text } };
}
);
console.log('所有处理程序已添加。');
}
async function main() {
console.log('正在初始化 TypeScript MCP 服务器 V2...');
const server: MCPServer = createServer({
serverId: 'ts-mcp-server-full-example',
serverDescription: '一个带有资源、提示和工具的 TypeScript MCP 服务器。',
capabilities: {
resources: [{ id: 'serverTime', description: '提供当前服务器时间。' }],
prompts: [{ id: 'simpleEcho', description: '将提供的文本原样返回 (echo)。' }],
tools: [{ id: 'stringUtil', description: '执行字符串操作(大写、小写、长度)。' }]
}
});
addActualServerHandlers(server);
// 可选:监听服务器事件
server.on('clientConnected', (clientId) => console.log(`事件:客户端 ${clientId} 已连接。`));
server.on('clientDisconnected', (clientId) => console.log(`事件:客户端 ${clientId} 已断开连接。`));
server.on('error', (error, clientId) => console.error(`事件:服务器错误(客户端:${clientId || 'N/A'}):`, error));
const port = 8080;
const host = '0.0.0.0';
try {
await server.listen({ port, host });
console.log(`TypeScript MCP Server is live on ws://${host}:${port}${server.path}`);
console.log('按 Ctrl+C 键关闭。');
} catch (error) {
console.error('启动 MCP 服务器失败:', error);
process.exit(1);
}
process.on('SIGINT', async () => {
console.log('\n接收到 SIGINT 信号。正在关闭服务器...');
await server.close();
console.log('服务器已关闭。');
process.exit(0);
});
}
main();
  • 清晰的能力定义: 在初始化时准确定义服务器的 capabilities。这有助于客户端了解您的服务器提供什么。
  • 强类型: 在处理程序中使用 TypeScript 接口来定义 ParamsType 和 PayloadType。这提高了代码清晰度、可维护性,并在编译时捕获错误。
  • 输入验证: 始终验证处理程序中 context.params 对象接收到的参数。如果验证失败,返回有意义的 MCPErrorData(例如,对于错误请求使用代码 400)。
  • 异步处理程序: 确保处理程序中的所有 I/O 操作或长时间运行的任务都是异步的 (async/await),以防止阻塞服务器的事件循环 (event loop)。
  • 错误处理: 在处理程序中实现健壮的错误处理。对于应返回给客户端的错误,抛出 MCPErrorData 对象。对于意外的服务器错误,进行详细的日志记录。
  • 大数据流式传输: 对于处理大量数据或持续更新的资源或提示,使用 context.stream API (write(), end(), error()) 逐步发送数据。
  • 上下文信息: 在处理程序中利用 context 对象(例如 context.clientId, context.requestId)进行日志记录、审计或基于客户端的自定义逻辑。
  • 优雅关机: 实现信号处理程序(例如针对 SIGINT)以优雅地关闭服务器,确保所有客户端连接正确终止并释放资源。
  • 日志记录: 为请求、错误和服务器生命周期事件添加全面的日志记录,以帮助调试和监控。