☰
基于MCP协议构建流式输出服务器:标准化大模型工具集成实践
2026/10/27 4:50:22 网站建设 项目流程

🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度

最近在尝试将大模型接入本地工具时,发现不同模型、不同框架的集成方式五花八门,配置起来相当繁琐。尤其是在处理流式输出、工具调用和上下文管理时,经常需要为每个项目重复编写大量胶水代码。如果你也遇到过类似问题,那么Model Context Protocol (MCP)可能就是那个你一直在寻找的标准化解决方案。本文将深入解析 MCP 的核心概念,并手把手带你从零开始,基于MCP Server Boot Starters快速构建一个支持流式输出的服务器,让你能轻松地将任何本地工具或数据源暴露给大模型使用。

1. MCP 核心概念:为什么需要它?

在深入代码之前,我们首先要理解 MCP 解决了什么问题。简单来说,MCP 是一个开放协议,旨在为大模型(LLM)与外部工具、数据源之间提供标准化的通信桥梁。

想象一下这个场景:你开发了一个股票查询工具、一个数据库客户端和一个文件搜索工具。现在,你想让 ChatGPT、Claude 或本地部署的 Llama 模型能够使用这些工具。在没有 MCP 之前,你可能需要:

  1. 为每个模型(OpenAI API, Anthropic API, Ollama 等)分别编写适配器。
  2. 处理不同的工具调用格式(如 OpenAI 的function calling, Anthropic 的tools)。
  3. 自己管理上下文、状态和错误处理。
  4. 实现复杂的流式响应机制以提供更好的用户体验。

这个过程不仅重复劳动,而且难以维护和扩展。MCP 的出现,正是为了将工具/数据提供方(Server)与模型/客户端(Client)解耦。它定义了一套标准的 JSON-RPC over STDIO/HTTP 接口,使得:

  • 工具开发者只需实现一次 MCP Server,其工具就能被任何兼容 MCP 的客户端(如 Claude Desktop, Cursor 等)使用。
  • 客户端/模型侧只需实现一次 MCP Client,就能接入无数个 MCP Server 提供的工具和资源。

MCP 的核心组件:

  • MCP Server: 工具或数据的提供方。它向客户端宣告自己提供了哪些“工具”(Tools,即可调用的函数)和“资源”(Resources,即可读取的上下文数据)。
  • MCP Client: 通常是模型运行时环境(如 AI 应用)。它发现并调用 Server 提供的工具和资源,将结果整合到模型上下文中。
  • 传输层: 默认为 JSON-RPC over STDIO(标准输入输出),也支持 HTTP 和 SSE(Server-Sent Events)。这保证了跨语言、跨进程的通用性。

Streamable-HT 是什么?在 MCP 的上下文中,“Streamable” 指的是支持流式传输(Streaming)的能力。这对于需要长时间运行或逐步产生结果的工具至关重要(例如,执行一个长时间脚本、监控日志文件、生成大量文本)。HT可能指代某种特定的处理类型或传输方式(在后续实践中我们会看到其具体表现)。简单理解,一个 Streamable 的 MCP Server 能够将工具执行结果以“数据块”的形式实时推送给客户端,而不是等待全部执行完毕再一次性返回。

接下来,我们将聚焦于如何快速构建一个这样的 MCP Server。

2. 环境准备与项目初始化

我们将使用TypeScript/JavaScript生态来构建 MCP Server,这是目前 MCP 社区最活跃、工具链最成熟的选择。确保你的开发环境满足以下要求:

基础环境:

  • Node.js: 版本 18 或更高(推荐 LTS 版本,如 20.x)。这是运行 JavaScript 代码的引擎。
  • npm或yarn或pnpm: 包管理工具。本文示例使用npm。
  • 代码编辑器: VS Code 或其他你熟悉的 IDE。

初始化项目:首先,创建一个新的项目目录并初始化一个 Node.js 项目。

# 1. 创建项目目录并进入 mkdir my-mcp-streaming-server cd my-mcp-streaming-server # 2. 初始化 package.json,一路回车或按需填写 npm init -y # 3. 安装 TypeScript 及相关类型定义(我们将用 TypeScript 获得更好的开发体验) npm install typescript @types/node --save-dev # 4. 初始化 TypeScript 配置 npx tsc --init

生成的tsconfig.json文件需要调整以适配现代 Node.js 开发。一个推荐的配置如下:

{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true, "declarationMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }

安装 MCP 核心依赖:MCP 的核心 SDK 由 Anthropic 官方维护。我们还需要安装@modelcontextprotocol/sdk。

npm install @modelcontextprotocol/sdk

现在,项目的基础骨架已经搭建完成。接下来,我们将引入能极大提升开发效率的利器——MCP Server Boot Starters。

3. 认识 MCP Server Boot Starters

手动从零开始实现一个 MCP Server 需要处理 JSON-RPC 通信、生命周期管理、工具注册等底层细节,虽然可行但效率不高。MCP Server Boot Starters 可以理解为 MCP Server 的“脚手架”或“快速启动模板”,它封装了这些通用逻辑,让开发者能专注于实现工具(Tools)和资源(Resources)的业务逻辑。

目前,社区已有一些优秀的 Boot Starters 项目。一个典型的例子是@modelcontextprotocol/server-boot(请注意,这是一个假设的包名,用于说明概念。实际开发中,你可能需要寻找或参考类似mcp-server-starter这样的开源模板)。其核心价值在于:

  1. 标准化入口: 提供了统一的 Server 类,继承后只需重写initialize等方法。
  2. 生命周期管理: 自动处理 Server 的启动、停止、与客户端的握手协议。
  3. 工具/资源注册简化: 提供清晰的 API 来声明工具和资源。
  4. 传输层抽象: 简化了 STDIO、HTTP 等传输方式的配置。
  5. 类型安全: 配合 TypeScript,提供完整的类型提示,减少错误。

为什么使用 Starter?对于新手,它能帮你跳过复杂的协议实现,直接进入业务开发。对于有经验的开发者,它能保证项目结构符合最佳实践,便于维护和协作。在本文中,我们将基于一个类似 Starter 的理念,构建一个结构清晰、支持流式输出的 MCP Server。

4. 构建支持流式输出的 MCP Server

我们将创建一个模拟的“系统监控”服务器,它提供两个工具:

  1. get_cpu_usage: 获取当前 CPU 使用率(一次性调用)。
  2. tail_logs: “尾随”查看一个模拟日志文件的内容(流式输出)。

4.1 项目结构设计

首先,创建以下目录和文件结构:

my-mcp-streaming-server/ ├── package.json ├── tsconfig.json ├── src/ │ ├── index.ts # 服务器主入口 │ ├── server/ # 服务器核心逻辑 │ │ ├── index.ts │ │ ├── StreamingMonitorServer.ts # 我们的 MCP 服务器类 │ │ └── tools/ # 工具实现 │ │ ├── index.ts │ │ ├── CpuTool.ts │ │ └── LogTailTool.ts │ └── types/ # 类型定义 │ └── index.ts └── dist/ # TypeScript 编译输出目录

4.2 定义工具参数类型

在src/types/index.ts中,我们定义工具调用时需要的参数类型,这有助于 TypeScript 进行类型检查。

// src/types/index.ts /** * tail_logs 工具的参数类型 */ export interface TailLogsArgs { /** * 要监控的日志文件路径(模拟) */ filePath: string; /** * 要输出的日志行数 */ lines?: number; } /** * get_cpu_usage 工具的参数类型(此工具无需参数,但保留结构) */ export interface GetCpuUsageArgs { // 目前没有参数,但保持接口定义以备扩展 [key: string]: never; } /** * 所有工具参数的联合类型 */ export type ToolArgs = TailLogsArgs | GetCpuUsageArgs;

4.3 实现流式日志工具

这是本文的核心,我们将展示如何实现一个支持流式输出的工具。关键在于使用 MCP SDK 提供的ToolResult和Content类型,并返回一个Promise,该 Promise 解析为一个包含isDelta标志的结果。

// src/server/tools/LogTailTool.ts import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; import { TailLogsArgs } from '../../types/index.js'; /** * 模拟尾随日志文件的工具 * 这是一个支持流式输出的工具示例。 */ export class LogTailTool { // 工具的名称,客户端通过此名称调用 static toolName = 'tail_logs'; // 工具的描述,帮助模型理解其用途 static toolDescription = 'Tail (follow) a log file and stream its new contents in real time.'; // 工具的参数模式定义(JSON Schema) static inputSchema = { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the log file to tail (simulated).', }, lines: { type: 'number', description: 'Number of historical lines to output first (default: 10).', default: 10, }, }, required: ['filePath'], }; /** * 处理工具调用 * @param server MCP Server 实例 * @param request 工具调用请求 * @returns 返回一个 Promise,解析为流式结果 */ static async handler( server: Server, request: CallToolRequest ) { const args = request.params.arguments as unknown as TailLogsArgs; const filePath = args.filePath; const lines = args.lines || 10; // 模拟读取文件并流式返回 // 在实际应用中,这里可能是 fs.createReadStream, 子进程 stdout 等。 return new Promise(async (resolve) => { // 1. 首先,发送一个初始消息,包含文件信息和开始提示 resolve({ content: [ { type: 'text', text: `开始监控日志文件: ${filePath}\n先显示最后 ${lines} 行:\n${'-'.repeat(50)}\n`, }, ], // `isDelta: true` 表明这是一个流式响应的开始,后续还会有更多数据块。 isDelta: true, }); // 2. 模拟流式发送历史日志行(快速发送) const historicalLogs = this.generateLogs(lines, 'HISTORICAL'); for (const log of historicalLogs) { // 使用 server 的 sendNotification 或其他机制发送增量数据。 // 注意:标准 MCP 工具调用结果返回后,通常通过独立的 `notifications` 或后续调用来发送流数据。 // 这里为了简化演示,我们模拟在 handler 返回后异步发送数据。 // 更精确的实现可能需要使用 Server-Sent Events (SSE) 或 WebSocket,具体取决于传输层。 // 以下代码演示了概念,实际流式传输需要客户端和服务器端共同支持。 setTimeout(() => { // 在实际流式传输中,你会通过一个持久的连接发送这些数据块。 // 例如:response.write(`data: ${JSON.stringify({ delta: log })}\n\n`); console.log('[Stream Delta]', log); // 此处模拟发送数据块 }, Math.random() * 100); // 随机延迟模拟网络波动 } // 3. 模拟持续产生新的日志行(间隔发送) let counter = 0; const intervalId = setInterval(() => { counter++; const newLog = this.generateLogLine('STREAMING', counter); console.log('[Stream Delta]', newLog); // 模拟发送新的数据块 // 模拟流式传输 5 条新日志后停止 if (counter >= 5) { clearInterval(intervalId); // 发送流结束标记(在实际协议中可能有特定消息) console.log('[Stream End]', `\n${'-'.repeat(50)}\n日志流监控结束。`); } }, 1000); // 每秒一条新日志 // 注意:实际的 MCP 流式传输需要更精细的控制,例如使用 `ReadableStream` 或类似的抽象。 // 上述 `setTimeout` 和 `setInterval` 仅用于演示流式数据生成的逻辑。 }); } /** * 生成模拟的历史日志 */ private static generateLogs(count: number, type: string): string[] { const logs: string[] = []; for (let i = 1; i <= count; i++) { logs.push(this.generateLogLine(type, i)); } return logs; } /** * 生成单条模拟日志行 */ private static generateLogLine(type: string, index: number): string { const timestamps = ['2023-10-27 08:00:01', '2023-10-27 08:00:05', '2023-10-27 08:00:12']; const messages = [ 'User login from IP 192.168.1.100', 'Database query executed successfully.', 'Cache miss for key: user_profile_12345', 'Scheduled task `cleanup_temp` started.', 'API `/api/v1/data` responded in 156ms.', ]; const randomTime = timestamps[Math.floor(Math.random() * timestamps.length)]; const randomMsg = messages[Math.floor(Math.random() * messages.length)]; return `[${randomTime}] [INFO] (${type}_${index}) ${randomMsg}`; } }

关键点解析:

  1. isDelta: true: 在返回的ToolResult中设置此标志,是向客户端表明此工具的响应是流式的,初始返回的内容只是第一部分。
  2. 异步数据推送: 真正的流式数据需要在工具调用返回后,通过其他通道(如 Server-Sent Events, WebSocket,或 MCP 协议中定义的特定通知)持续推送给客户端。上面的console.log模拟了这种推送。在实际的 HTTP+SSE 传输层中,你会直接向响应流写入数据。
  3. 模式定义 (inputSchema): 使用 JSON Schema 清晰地定义工具参数,这对客户端(尤其是大模型)理解如何调用工具至关重要。

4.4 实现普通 CPU 工具

作为对比,我们实现一个普通的、一次性返回结果的工具。

// src/server/tools/CpuTool.ts import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; import { GetCpuUsageArgs } from '../../types/index.js'; /** * 获取 CPU 使用率的工具 * 这是一个普通(非流式)工具示例。 */ export class CpuTool { static toolName = 'get_cpu_usage'; static toolDescription = 'Get the current system CPU usage percentage (simulated).'; static inputSchema = { type: 'object', properties: { // 这个工具目前不需要参数,但 schema 仍需是一个对象 }, required: [], }; static async handler( server: Server, request: CallToolRequest ) { // 模拟获取 CPU 使用率 const usage = Math.floor(Math.random() * 30) + 10; // 10% 到 40% const timestamp = new Date().toISOString(); return { content: [ { type: 'text', text: `CPU Usage Report (Simulated):\nTimestamp: ${timestamp}\nUsage: ${usage}%\nStatus: ${usage > 70 ? 'High Load' : 'Normal'}`, }, ], // 对于非流式工具,不设置 isDelta 或设置为 false(默认) }; } }

4.5 创建主服务器类

现在,我们将工具整合到一个 MCP Server 类中,并处理服务器的初始化。

// src/server/StreamingMonitorServer.ts import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { CpuTool } from './tools/CpuTool.js'; import { LogTailTool } from './tools/LogTailTool.js'; /** * 我们的自定义 MCP 服务器,支持流式工具。 */ export class StreamingMonitorServer { private server: Server; constructor() { // 1. 创建 MCP Server 实例 this.server = new Server( { name: 'streaming-monitor-server', version: '0.1.0', }, { capabilities: { // 声明服务器支持的工具 tools: {}, }, } ); // 2. 设置请求处理器 this.setupHandlers(); // 3. 连接传输层(这里使用 STDIO,适用于 Claude Desktop 等客户端) this.setupTransport(); } /** * 设置 JSON-RPC 请求处理器 */ private setupHandlers() { // 处理客户端查询可用工具的请求 this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: CpuTool.toolName, description: CpuTool.toolDescription, inputSchema: CpuTool.inputSchema, }, { name: LogTailTool.toolName, description: LogTailTool.toolDescription, inputSchema: LogTailTool.inputSchema, }, ], }; }); // 处理客户端调用工具的请求 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; console.error(`[Server] Tool called: ${name}`, args); // 日志输出到 stderr,避免干扰 stdio 协议 switch (name) { case CpuTool.toolName: return await CpuTool.handler(this.server, request); case LogTailTool.toolName: return await LogTailTool.handler(this.server, request); default: throw new Error(`Unknown tool: ${name}`); } }); } /** * 设置传输层(使用标准输入输出) */ private setupTransport() { const transport = new StdioServerTransport(); this.server.connect(transport).catch((error) => { console.error('[Server] Failed to connect transport:', error); process.exit(1); }); } /** * 启动服务器 */ async run() { // 服务器在 connect 后会自动开始监听 STDIO console.error('[Server] Streaming Monitor MCP Server is running on STDIO...'); // 保持进程运行 process.on('SIGINT', () => { this.server.close(); process.exit(0); }); } }

4.6 创建程序入口

最后,在src/index.ts中启动我们的服务器。

// src/index.ts import { StreamingMonitorServer } from './server/StreamingMonitorServer.js'; // 创建并运行服务器 const server = new StreamingMonitorServer(); server.run().catch((error) => { console.error('[Server] Fatal error:', error); process.exit(1); });

5. 构建、运行与测试

5.1 构建 TypeScript 项目

首先,我们需要将 TypeScript 代码编译成 JavaScript。

# 在项目根目录执行 npx tsc

如果编译成功,你会在dist/目录下看到生成的.js文件。

5.2 通过 Node.js 直接运行(测试)

为了快速测试服务器逻辑(不涉及完整的 MCP 客户端),我们可以创建一个简单的测试脚本。但请注意,真正的 MCP 交互需要通过 STDIO 协议。

一个更直接的测试方法是使用MCP 客户端调试工具,例如@modelcontextprotocol/sdk包中可能提供的工具,或者第三方 CLI 工具如mcp-cli。这里我们演示如何通过修改入口文件,让服务器在启动时模拟一个工具调用(仅用于开发调试)。

创建src/test.ts(记得在tsconfig.json的include中添加"src/test.ts"):

// src/test.ts - 开发环境下的自检 import { StreamingMonitorServer } from './server/StreamingMonitorServer.js'; const server = new StreamingMonitorServer(); console.log('服务器初始化完成。'); console.log('请使用兼容 MCP 的客户端(如 Claude Desktop)连接到此服务器。'); console.log('确保在客户端配置中指定服务器命令为:node /path/to/dist/index.js'); // 保持进程运行 process.stdin.resume();

编译并运行它,确保没有启动错误:

npx tsc node dist/test.js

你应该看到服务器等待连接的提示。

5.3 与 MCP 客户端集成(以 Claude Desktop 为例)

这是 MCP Server 的真正用途。以 Claude Desktop 为例:

  1. 找到 Claude Desktop 的配置目录:

    • macOS:~/Library/Application Support/Claude/claude_desktop_config.json
    • Windows:%APPDATA%\Claude\claude_desktop_config.json
    • Linux:~/.config/Claude/claude_desktop_config.json
  2. 编辑配置文件,添加你的 MCP Server:

    { "mcpServers": { "my-monitor": { "command": "node", "args": [ "/ABSOLUTE/PATH/TO/YOUR/PROJECT/dist/index.js" ], "env": { "NODE_ENV": "production" } } } }

    重要:将/ABSOLUTE/PATH/TO/YOUR/PROJECT替换为你项目的绝对路径。

  3. 重启 Claude Desktop。

  4. 在 Claude 的聊天界面,你应该能直接使用定义的工具,例如:“请帮我查看一下 CPU 使用率”或“尾随一下/var/log/system.log这个文件”。

6. 常见问题与排查思路

在开发和运行 MCP Server 时,你可能会遇到以下问题:

问题现象可能原因排查思路与解决方案
Claude Desktop 无法连接服务器1. 配置文件路径错误。
2. Node.js 命令路径错误。
3. 服务器脚本存在语法错误,启动即崩溃。
4. 端口或 STDIO 冲突。
1. 检查claude_desktop_config.json格式和路径。
2. 在终端中直接运行node /path/to/dist/index.js,看是否有错误输出。
3. 查看 Claude Desktop 的日志文件(位置因系统而异)。
4. 确保服务器代码正确处理了 STDIO 传输。
工具列表不显示或调用失败1.ListToolsRequest处理器未正确设置或返回格式错误。
2. 工具的名称、描述、参数 Schema 不符合规范。
3.CallToolRequest处理器中未正确匹配工具名。
1. 在setupHandlers中检查ListToolsRequestSchema的处理逻辑。
2. 使用简单的 JSON 验证工具检查inputSchema。
3. 在CallToolRequest的switch语句中添加默认的console.error日志。
流式输出不工作1. 客户端不支持流式传输。
2. 服务器返回的ToolResult中isDelta设置不正确。
3. 流式数据推送的机制未实现或实现有误(如未使用正确的传输通道)。
1. 确认你的客户端(如 Claude Desktop)版本是否支持流式工具。
2. 确保流式工具的 handler 返回{ content: [...], isDelta: true }。
3.深入研究 MCP 协议对于流式传输的具体定义。当前示例中的setInterval是模拟,真正的实现可能需要基于 SSE 或 WebSocket 的传输层。查阅官方 SDK 和示例。
服务器进程意外退出1. 未捕获的异常。
2. 传输层连接断开。
3. 进程收到终止信号。
1. 在server.run()和connect()外包裹try-catch。
2. 监听process的uncaughtException和unhandledRejection事件。
3. 实现server.close()的优雅关闭逻辑。
TypeScript 编译错误1. 类型不匹配。
2.tsconfig.json配置问题。
3. 模块导入路径错误。
1. 仔细阅读错误信息,修正类型。
2. 确保使用NodeNext的模块解析策略。
3. 检查import语句中的文件扩展名(.js或.ts)。

7. 最佳实践与工程建议

构建用于生产环境的 MCP Server 时,请考虑以下建议:

  1. 清晰的工具定义:

    • 名称:使用snake_case,清晰表达动作和对象,如search_web,execute_sql_query。
    • 描述:一句话准确描述功能,让 LLM 能准确判断何时调用。
    • 参数 Schema:务必详细。使用description字段解释每个参数的用途和示例。对于枚举值,使用enum列出所有选项。
  2. 健壮的错误处理:

    • 在工具handler内部使用try-catch,返回结构化的错误信息,而不是让异常冒泡导致整个请求失败。
    • 错误信息应对用户友好,同时包含可供调试的细节(可记录在服务器日志中)。
    try { // ... 业务逻辑 } catch (error: any) { return { content: [{ type: 'text', text: `操作失败: ${error.message}` }], isError: true, // MCP 协议中可能用于标识错误结果 }; }
  3. 资源管理:

    • 对于文件操作、数据库连接、网络请求等,确保在工具执行完毕后正确关闭资源,避免泄漏。
    • 考虑为长时间运行的工具(如tail_logs)实现超时和取消机制。
  4. 安全性:

    • 输入验证:永远不要信任客户端传入的参数。即使有 Schema,也要在 handler 中再次进行边界检查和清理,防止路径遍历(../../../)、命令注入等攻击。
    • 权限控制:如果服务器提供敏感操作,应实现认证和授权机制。MCP 协议本身不强制要求,但可以在 Server 层面或通过传输层(如 HTTPS)添加。
    • 环境隔离:考虑在沙箱或容器中运行工具,尤其是执行任意代码或命令时。
  5. 性能与可观测性:

    • 日志记录:记录工具调用、参数、耗时和结果(注意脱敏)。将日志输出到stderr或文件,避免污染与客户端的协议通信(stdio)。
    • 指标监控:对于高频工具,可以考虑添加简单的指标,如调用次数、平均延迟。
    • 流式传输优化:对于真正的流式工具,注意缓冲区管理,避免内存溢出。考虑使用StreamAPI 或类似的异步迭代器。
  6. 配置化:

    • 将工具列表、服务器元数据(名称、版本)等提取到配置文件(如config.json)中,便于不同环境部署。
  7. 测试:

    • 为每个工具编写单元测试,模拟CallToolRequest。
    • 进行集成测试,使用一个简单的 MCP 客户端脚本实际连接你的服务器并调用工具。

通过遵循以上步骤和最佳实践,你不仅可以构建出演示中的监控服务器,更能将其模式应用到任何你想暴露给大模型的本地能力上,例如代码库搜索、内部 API 调用、数据分析脚本等。MCP 的生态正在快速发展,掌握其 Server 端开发,意味着你能够为自己和团队打造出强大、可复用的 AI 原生工具链。

🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询