本文介绍如何使用 Node.js 和 TypeScript 生成模型上下文协议 (MCP) 服务器。 服务器在无服务器环境中运行工具和服务。 将此结构用作创建自定义 MCP 服务器的起点。
获取代码
浏览 TypeScript 远程模型上下文协议 (MCP) 服务器 示例。 它演示如何使用 Node.js 和 TypeScript 生成远程 MCP 服务器并将其部署到 Azure 容器应用。
跳转到 代码演练部分 ,了解此示例的工作原理。
体系结构概述
MCP 服务器在 Azure 容器应用(ACA)上作为容器化应用运行。 它使用 Node.js/TypeScript 后端通过模型上下文协议向 MCP 客户端提供工具。 所有工具都使用后端 SQLite 数据库。
成本
为了降低成本,此示例对大多数资源使用基本或消耗定价层。 根据需要调整分层,并在完成后删除资源,以避免产生费用。
先决条件
- Visual Studio Code - 支持 MCP 服务器开发的最新版本。
- GitHub Copilot Visual Studio Code 扩展
- GitHub Copilot Chat Visual Studio Code 插件
- Azure 开发工具 CLI (azd)
开发容器包含本文所需的所有依赖项。 可以在 GitHub Codespaces(在浏览器中)或使用 Visual Studio Code 在本地运行它。
若要遵循本文,请确保满足以下先决条件:
- Azure 订阅 - 免费创建一个
- Azure 帐户权限 – Azure 帐户必须具有
Microsoft.Authorization/roleAssignments/write权限,例如 基于角色的访问控制管理员、 用户访问管理员或 所有者。 如果没有订阅级权限,则必须被授予现有资源组的 RBAC 权限,并将其部署到该组。- Azure 帐户还需要
Microsoft.Resources/deployments/write订阅级别的权限。
- Azure 帐户还需要
- GitHub 帐户
开放开发环境
按照以下步骤设置具有所有必需依赖项的预配置开发环境。
GitHub Codespaces 以 Visual Studio Code for Web 作为界面运行 GitHub 托管的开发容器。 使用 GitHub Codespaces 进行最简单的设置,因为它附带了本文预安装的必需工具和依赖项。
重要
所有 GitHub 帐户每月最多可使用 Codespaces 60 小时,其中包含两个核心实例。 有关详细信息,请参阅 GitHub Codespaces 每月所含的存储和计算核心小时数。
使用以下步骤在 main GitHub 存储库的 Azure-Samples/mcp-container-ts 分支上创建新的 GitHub Codespace。
右键单击以下按钮,然后选择 新窗口中的“打开”链接。 此操作使你能够同时打开并查看开发环境和文档。
在 “创建代码空间 ”页上,查看并选择“ 创建新代码空间”。
等待 Codespace 启动。 这可能需要几分钟时间。
在终端的屏幕底部,使用 Azure Developer CLI 登录到 Azure。
azd auth login从终端复制代码,然后将其粘贴到浏览器中。 按照说明使用 Azure 帐户进行身份验证。
在此开发容器中执行其余任务。
注释
在本地运行 MCP 服务器:
- 按照示例存储库的 “本地环境设置 ”部分中所述设置环境。
- 按照示例存储库 中的“在 Visual Studio Code 中配置 MCP 服务器 ”部分中的说明,将 MCP 服务器配置为使用本地环境。
- 跳到“使用 TODO MCP 服务器工具的代理模式”部分继续。
部署和运行
示例存储库包含 MCP 服务器 Azure 部署的所有代码和配置文件。 以下步骤将引导你完成示例 MCP 服务器 Azure 部署过程。
部署到 Azure 云
重要
即使你在命令完成之前停止命令,本节中的 Azure 资源也会立即开始产生费用。
运行以下 Azure Developer CLI 命令,以进行 Azure 资源预配和源代码部署:
azd up使用下表回答提示:
Prompt 答案 环境名称 保持简短和小写。 添加名称或别名。 例如, my-mcp-server。 它用作资源组名称的一部分。订阅 选择要在其中创建资源的订阅。 位置(用于托管) 从列表中选择附近的位置。 Azure OpenAI 模型的位置 从列表中选择附近的位置。 如果与第一个位置相同的位置可用,请选择该位置。 等待应用部署。 部署通常需要 5 到 10 分钟才能完成。
部署完成后,可以使用输出中提供的 URL 访问 MCP 服务器。 URL 如下所示:
https://<env-name>.<container-id>.<region>.azurecontainerapps.io
- 将 URL 复制到剪贴板。 在下一部分中需要用到它。
在 Visual Studio Code 中配置 MCP 服务器
通过将 URL 添加到 mcp.json 文件夹中的文件 .vscode ,在本地 VS Code 环境中配置 MCP 服务器。
在
mcp.json文件夹中打开.vscode文件。找到文件中的
mcp-server-sse-remote节。 它应如下所示:"mcp-server-sse-remote": { "type": "sse", "url": "https://<container-id>.<location>.azurecontainerapps.io/sse" }将现有
url值替换为在上一步中复制的 URL。将
mcp.json文件保存在.vscode文件夹中。
在代理模式下使用 TODO MCP 服务器工具
修改 MCP 服务器后,可以使用这些工具,它在代理模式下提供。 若要在代理模式下使用 MCP 工具,请执行以下作:
打开“聊天”视图(
Ctrl+Alt+I),然后从下拉列表中选择“代理”模式。选择 “工具” 按钮以查看可用工具的列表。 (可选)选择或取消选择要使用的工具。 您可以通过在搜索框中键入来搜索工具。
在聊天输入框中输入提示,例如“我需要在星期三向经理发送电子邮件”,并注意如何根据需要自动调用工具,如以下屏幕截图所示:
注释
默认情况下,调用工具时,您需要在工具运行之前确认操作。 否则,工具可能在计算机上本地运行,并可能执行修改文件或数据的作。
使用“继续”按钮的下拉菜单选项,自动确认针对当前会话、工作区或所有未来调用的特定工具。
探索示例代码
本部分概述了 MCP 服务器示例中的关键文件和代码结构。 代码分为多个主要组件:
-
index.ts:MCP 服务器的主要入口点,用于设置 Express.js HTTP 服务器和路由。 -
server.ts:管理 Server-Sent 事件(SSE)连接和 MCP 协议处理的传输层。 -
tools.ts:包含 MCP 服务器的业务逻辑和实用工具函数。 -
types.ts:定义在 MCP 服务器中使用的 TypeScript 类型和接口。
index.ts - 服务器如何启动和接受 HTTP 连接
该文件 index.ts 是 MCP 服务器的主要入口点。 它初始化服务器,设置 Express.js HTTP 服务器,并定义 Server-Sent 事件(SSE)终结点的路由。
创建 MCP 服务器实例
以下代码片段使用 StreamableHTTPServer 类初始化 MCP 服务器,该类是核心 MCP Server 类的包装器。 此类处理服务器发送的事件 (SSE) 的传输层并管理客户端连接。
const server = new StreamableHTTPServer(
new Server(
{
name: 'todo-http-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
)
);
概念:
-
合成模式:
SSEPServer封装低级别Server类 - 功能声明:服务器报出它支持工具(但不支持资源/提示)
- 命名约定:服务器名称成为 MCP 标识的一部分
设置快速路由
以下代码片段设置 Express.js 服务器来处理 SSE 连接和消息处理的传入 HTTP 请求:
router.post('/messages', async (req: Request, res: Response) => {
await server.handlePostRequest(req, res);
});
router.get('/sse', async (req: Request, res: Response) => {
await server.handleGetRequest(req, res);
});
概念:
- 双终结点模式:用于建立 SSE 连接的 GET、用于发送消息的 POST
-
委派模式:快速路由立即委托给
SSEPServer
进程生命周期管理
以下代码片段处理服务器的生命周期,包括启动服务器并在终止信号上正常关闭它:
process.on('SIGINT', async () => {
log.error('Shutting down server...');
await server.close();
process.exit(0);
});
概念:
- 正常关闭:按 Ctrl+C 进行正常清理
- 异步清理:服务器关闭操作是异步的
- 资源管理:SSE 连接很重要
传输层: server.ts
该文件 server.ts 为 MCP 服务器实现传输层,具体处理 Server-Sent 事件(SSE)连接和路由 MCP 协议消息。
设置 SSE 客户端连接并创建传输
该 SSEPServer 类是处理 MCP 服务器中 Server-Sent 事件(SSE)的主要传输层。 它使用 SSEServerTransport 类管理单个客户端连接。 它管理多个传输及其生命周期。
export class SSEPServer {
server: Server;
transport: SSEServerTransport | null = null;
transports: Record<string, SSEServerTransport> = {};
constructor(server: Server) {
this.server = server;
this.setupServerRequestHandlers();
}
}
概念:
- 状态管理:跟踪当前传输和所有传输
-
会话映射:
transports对象将会话 ID 映射到传输实例 - 构造函数委派:立即设置请求处理程序
SSE 连接建立 (handleGetRequest)
当客户端向handleGetRequest终结点发出 GET 请求时,该方法/sse负责建立新的 SSE 连接。
async handleGetRequest(req: Request, res: Response) {
log.info(`GET ${req.originalUrl} (${req.ip})`);
try {
log.info("Connecting transport to server...");
this.transport = new SSEServerTransport("/messages", res);
TransportsCache.set(this.transport.sessionId, this.transport);
res.on("close", () => {
if (this.transport) {
TransportsCache.delete(this.transport.sessionId);
}
});
await this.server.connect(this.transport);
log.success("Transport connected. Handling request...");
} catch (error) {
// Error handling...
}
}
概念:
-
传输创建:每个 GET 请求的新增
SSEServerTransport功能 - 会话管理:缓存中存储的自动生成会话 ID
- 事件处理程序:在连接关闭时执行清理操作
-
MCP 连接:
server.connect()建立协议连接 - 异步流:连接设置是异步的,具有错误边界
消息处理 (handlePostRequest)
该方法 handlePostRequest 处理传入的 POST 请求以处理客户端发送的 MCP 消息。 它使用查询参数中的会话 ID 来查找正确的传输实例。
async handlePostRequest(req: Request, res: Response) {
log.info(`POST ${req.originalUrl} (${req.ip}) - payload:`, req.body);
const sessionId = req.query.sessionId as string;
const transport = TransportsCache.get(sessionId);
if (transport) {
await transport.handlePostMessage(req, res, req.body);
} else {
log.error("Transport not initialized. Cannot handle POST request.");
res.status(400).json(/* error response */);
}
}
概念:
-
会话查找:使用
sessionId查询参数查找传输 - 会话验证:首先验证 SSE 连接。
- 消息委派:传输负责实际的消息处理
- 错误响应:缺少会话的正确 HTTP 错误代码
MCP 协议处理程序设置 (setupServerRequestHandlers)
该方法 setupServerRequestHandlers 为 MCP 协议请求注册以下处理程序:
- 返回可用 TODO 工具列表的
ListToolsRequestSchema的处理程序。 - 一个用于
CallToolRequestSchema的处理程序,负责查找并执行带有提供参数的请求工具。
此方法使用 Zod 架构 来定义预期的请求和响应格式。
private setupServerRequestHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async (_request) => {
return {
tools: TodoTools,
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = TodoTools.find((tool) => tool.name === name);
if (!tool) {
return this.createJSONErrorResponse(`Tool "${name}" not found.`);
}
const response = await tool.execute(args as any);
return { content: [{ type: "text", text: response }] };
});
}
概念:
- Schema-Based 路由:使用 Zod 架构处理类型安全请求
-
工具发现:
ListToolsRequestSchema返回静态 TodoTools 数组 -
工具执行:
CallToolRequestSchema查找和执行工具 - 错误处理:正常处理未知工具
- 响应格式:MCP 兼容的响应结构
- 类型安全性:TypeScript 类型确保正确的参数传递
业务逻辑: tools.ts
该文件 tools.ts 定义 MCP 客户端可用的实际功能:
- 工具元数据(名称、说明、架构)
- 输入验证架构
- 工具执行逻辑
- 与数据库层集成
此 MCP 服务器定义了四个 TODO 管理工具:
-
add_todo:创建新的 TODO 项 -
complete_todo:将 TODO 项标记为已完成 -
delete_todo:删除待办事项 -
list_todos:列出所有 TODO 项目 -
update_todo_text:更新现有待办事项的文本
工具定义模式
这些工具被定义为一组对象,每个对象代表一个特定的TODO操作。 在以下代码片段中, addTodo 定义了该工具:
{
name: "addTodo",
description: "Add a new TODO item to the list...",
inputSchema: {
type: "object",
properties: {
text: { type: "string" },
},
required: ["text"],
},
outputSchema: { type: "string" },
async execute({ text }: { text: string }) {
const info = await addTodo(text);
return `Added TODO: ${text} (id: ${info.lastInsertRowid})`;
},
}
每个工具定义都有:
-
name:工具的唯一标识符 -
description:工具用途的简要说明 -
inputSchema:定义预期输入格式的 Zod 架构 -
outputSchema:定义预期输出格式的 Zod 架构 -
execute:实现工具逻辑的函数
工具定义在 server.ts 中导入,并通过 ListToolsRequestSchema 处理程序公开。
概念:
- 模块化工具设计:每个工具都是一个独立对象
-
JSON 架构验证:
inputSchema定义预期参数 - 类型安全性:TypeScript 类型与架构定义匹配
- 异步执行:所有工具执行都是异步的
- 数据库集成:调用导入的数据库函数
- Human-Readable 响应:返回格式化字符串,而不是原始数据
工具数组导出
这些工具导出为静态数组,便于在服务器中导入和使用。 每个工具都是一个具有其元数据和执行逻辑的对象。 此结构允许 MCP 服务器根据客户端请求动态发现和执行工具。
export const TodoTools = [
{ /* addTodo */ },
{ /* listTodos */ },
{ /* completeTodo */ },
{ /* deleteTodo */ },
{ /* updateTodoText */ },
];
概念:
- 静态注册:在模块加载时定义的工具
- 数组结构:简单数组使工具易于迭代
- 导入/导出:与服务器逻辑进行清晰分离
工具执行错误处理
每个工具的 execute 函数都能顺利处理错误,并返回明确的消息,而不是引发异常。 此方法可确保 MCP 服务器提供无缝的用户体验。
工具应对各种错误情况:
async execute({ id }: { id: number }) {
const info = await completeTodo(id);
if (info.changes === 0) {
return `TODO with id ${id} not found.`;
}
return `Marked TODO ${id} as completed.`;
}
概念:
-
数据库响应检查:用于
info.changes检测故障 - 正常降级:返回描述性错误消息与异常引发
- User-Friendly 错误:适用于 AI 解析的消息
数据层: db.ts
该 db.ts 文件管理 SQLite 数据库连接,并执行 TODO 应用的 CRUD 操作。 它使用 better-sqlite3 库进行同步数据库访问。
数据库初始化
数据库通过连接到 SQLite 并创建表(如果不存在)进行初始化。 以下代码片段显示了初始化过程:
const db = new Database(":memory:", {
verbose: log.info,
});
try {
db.pragma("journal_mode = WAL");
db.prepare(
`CREATE TABLE IF NOT EXISTS ${DB_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0
)`
).run();
log.success(`Database "${DB_NAME}" initialized.`);
} catch (error) {
log.error(`Error initializing database "${DB_NAME}":`, { error });
}
概念:
-
In-Memory 数据库:
:memory:表示重启时丢失的数据(仅演示/测试) - WAL模式: 使用Write-Ahead日志记录以提高性能
- 模式定义:具有自增 ID 的简单 TODO 表
- 错误处理:正常处理初始化失败
- 日志集成:数据库操作记录用于调试
CRUD操作模式
该文件 db.ts 提供四个主要的 CRUD 操作来管理 TODO 项目。
创建操作:
export async function addTodo(text: string) {
log.info(`Adding TODO: ${text}`);
const stmt = db.prepare(`INSERT INTO todos (text, completed) VALUES (?, 0)`);
return stmt.run(text);
}
读取操作:
export async function listTodos() {
log.info("Listing all TODOs...");
const todos = db.prepare(`SELECT id, text, completed FROM todos`).all() as Array<{
id: number;
text: string;
completed: number;
}>;
return todos.map(todo => ({
...todo,
completed: Boolean(todo.completed),
}));
}
更新操作:
export async function completeTodo(id: number) {
log.info(`Completing TODO with ID: ${id}`);
const stmt = db.prepare(`UPDATE todos SET completed = 1 WHERE id = ?`);
return stmt.run(id);
}
删除操作:
export async function deleteTodo(id: number) {
log.info(`Deleting TODO with ID: ${id}`);
const row = db.prepare(`SELECT text FROM todos WHERE id = ?`).get(id) as
| { text: string }
| undefined;
if (!row) {
log.error(`TODO with ID ${id} not found`);
return null;
}
db.prepare(`DELETE FROM todos WHERE id = ?`).run(id);
log.success(`TODO with ID ${id} deleted`);
return row;
}
概念:
- 准备语句:防范 SQL 注入
- 类型强制转换:查询结果的显式 TypeScript 类型
- 数据转换:将 SQLite 整数转换为布尔值
- 原子操作:每个函数都是一个数据库事务
- 返回值一致性:函数返回作元数据
- 防御性编程:删除前检查模式
架构设计
使用简单的 SQL 语句在 db.ts 文件中定义数据库架构。 该 todos 表有三个字段:
CREATE TABLE todos (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- Unique identifier
text TEXT NOT NULL, -- TODO description
completed INTEGER NOT NULL DEFAULT 0 -- Boolean as integer
);
辅助工具:helpers/ 目录
该 helpers/ 目录为服务器提供实用工具函数和类。
用于调试和监视的结构化日志记录: helpers/logs.ts
该文件 helpers/logs.ts 为 MCP 服务器提供结构化日志记录实用工具。 它使用debug库进行日志记录,并在控制台中进行颜色编码输出。
export const logger = (namespace: string) => {
const dbg = debug('mcp:' + namespace);
const log = (colorize: ChalkInstance, ...args: any[]) => {
const timestamp = new Date().toISOString();
const formattedArgs = [timestamp, ...args].map((arg) => {
if (typeof arg === 'object') {
return JSON.stringify(arg, null, 2);
}
return arg;
});
dbg(colorize(formattedArgs.join(' ')));
};
return {
info(...args: any[]) { log(chalk.cyan, ...args); },
success(...args: any[]) { log(chalk.green, ...args); },
warn(...args: any[]) { log(chalk.yellow, ...args); },
error(...args: any[]) { log(chalk.red, ...args); },
};
};
SSE 传输的会话管理: helpers/cache.ts
helpers/cache.ts 文件使用 Map 按会话 ID 存储 SSE 传输。 此方法允许服务器快速查找和管理活动连接。
import type { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse";
export const TransportsCache = new Map<string, SSEServerTransport>();
注释
TransportsCache 是一个简单的内存缓存。 在生产环境中,请考虑使用更可靠的解决方案(如 Redis 或数据库)进行会话管理。
执行流摘要
下图演示了从客户端到 MCP 服务器和返回的完整请求旅程,包括工具执行和数据库作:
清理 GitHub Codespaces
删除 GitHub Codespaces 环境,以充分利用免费每核心小时数。
重要
有关 GitHub 帐户的免费存储和核心小时数的详细信息,请参阅 GitHub Codespaces 每月包含的存储和核心小时数。
从
Azure-Samples//mcp-container-tsGitHub 存储库查找创建的活动代码空间。打开代码空间的上下文菜单,然后选择“删除”。
