MCP Server Streamable-HT模式实战:构建AI实时监控工具
1. 背景与核心概念
在当今快速发展的AI应用开发领域,如何让大型语言模型(LLM)安全、高效地访问外部工具和数据,是每个开发者都会遇到的挑战。传统的集成方式往往需要编写大量胶水代码,处理复杂的API调用、数据转换和权限控制,不仅开发效率低下,也带来了安全风险和维护负担。
Model Context Protocol (MCP) 正是为了解决这一痛点而生的开放协议。你可以把它理解为AI应用与外部世界(如数据库、API、文件系统)之间的“通用USB接口”。它定义了一套标准化的通信规范,使得任何遵循MCP协议的服务器(MCP Server)都能被任何兼容MCP的客户端(如Claude Desktop、Cursor等)无缝发现和使用。
核心价值在于解耦与标准化 :应用开发者无需为每个工具重复编写集成逻辑,只需实现或使用现成的MCP Server;而AI客户端通过统一的MCP协议,就能动态加载和使用海量的工具能力,实现了“一次集成,处处可用”。
本文聚焦于MCP生态中一个非常实用的环节: MCP Server Boot Starters ,特别是其 Streamable-HT 特性。简单来说, Streamable-HT 是一种服务器启动模式,它允许MCP Server以HTTP流式响应的方式运行。这对于需要长时间运行、实时推送数据(如监控日志、股票行情)或与需要HTTP接口的外部系统集成的场景至关重要。我们将从零开始,深入探讨其原理、搭建步骤、实战应用以及避坑指南。
2. 环境准备与版本说明
在开始构建一个支持 Streamable-HT 的MCP Server之前,我们需要确保本地开发环境就绪。以下配置是本文示例的基础,你可以根据实际项目需求进行调整。
操作系统 : 支持 macOS, Linux, Windows (WSL2 推荐)。 编程语言 : Node.js (这是目前 MCP 生态最活跃的语言,官方工具链支持完善)。 核心工具与版本 :
- Node.js :
>= 18.0.0(推荐 LTS 版本,如20.x)。 - npm :
>= 9.0.0或 yarn>= 1.22.0。 - @modelcontextprotocol/sdk :
^0.4.0(MCP 官方 JavaScript/TypeScript SDK,版本迭代较快,建议查看最新文档)。 - TypeScript (可选但推荐):
^5.0.0,用于获得更好的类型提示和开发体验。 - 测试客户端 : 我们将使用 Claude Desktop 作为 MCP 客户端进行测试。请确保已安装最新版本,并知晓其配置目录位置(如
~/Library/Application Support/Claude/claude_desktop_config.jsonon macOS)。
项目初始化 : 首先,创建一个新的项目目录并初始化。
mkdir mcp-streaming-server-example
cd mcp-streaming-server-example
npm init -y
接下来,安装必要的依赖。我们将同时安装 MCP SDK 和 TypeScript 相关包。
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx
初始化 TypeScript 配置。
npx tsc --init
生成的 tsconfig.json 需要调整以适应 MCP 开发。一个基础的配置示例如下:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
最后,创建项目基础结构。
mkdir src
touch src/server.ts
至此,一个支持 TypeScript 的 MCP Server 项目骨架就搭建完成了。
3. 核心原理与 Streamable-HT 模式拆解
要理解 Streamable-HT ,首先要了解 MCP Server 的两种主要运行模式:
- Stdio 模式(默认) : Server 作为一个独立的子进程启动,通过标准输入(stdin)和标准输出(stdout)与客户端进行 JSON-RPC 通信。这是最简单、最常见的模式,适用于工具调用和一次性数据查询。
- HTTP 模式 : Server 作为一个 HTTP 服务运行。客户端通过向特定的 HTTP 端点发送 JSON-RPC 请求来与之交互。
Streamable-HT本质上是 HTTP 模式的一个增强特性。
为什么需要 Streamable-HT? 普通的 HTTP 请求-响应模式是同步的:客户端发送一个请求,服务器处理完毕后返回一个响应,连接随即关闭。但对于某些场景,这远远不够:
- 实时数据流 : 例如,一个“服务器日志跟踪”工具,需要持续将新的日志行推送给AI。
- 长时任务进度反馈 : 例如,一个“数据库备份”工具,执行需要几分钟,需要持续汇报“开始”、“进行中 30%”、“完成”等状态。
- 服务化部署 : 你可能希望将 MCP Server 部署为云函数或容器服务,供多个客户端远程调用,而不是每个客户端本地运行一个进程。
Streamable-HT 通过支持 Server-Sent Events (SSE) 或 WebSocket 等机制,实现了从服务器到客户端的单向或双向持续数据流。在 MCP 的语境下,它特指 Server 能够以 HTTP 服务的形式启动,并具备处理流式请求和响应的能力。
关键组件 :
- Transport : MCP SDK 中的传输层抽象。对于
Streamable-HT,我们会使用HttpTransport。 - Server : MCP 服务器实例,注册了各种工具(Tools)和资源(Resources)。
- 工具(Tools) : 定义 AI 可以调用的函数。一个工具可以声明其返回内容是“流式”的。
- 流式响应 : 在工具的实现中,你可以返回一个
AsyncIterable或使用 SDK 提供的流式 API,逐步产生多个结果块(chunks),而不是一次性返回所有数据。
4. 构建一个 Streamable-HT MCP Server 实战
我们将构建一个实用的 MCP Server: “实时系统监控器” 。它将提供一个工具 get_system_stats_stream ,能够以流的形式持续返回 CPU、内存使用率等系统指标,模拟实时监控面板的数据推送。
4.1 创建 Server 主文件
编辑 src/server.ts ,这是服务器的核心入口。
// src/server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { HttpTransport } from '@modelcontextprotocol/sdk/server/http.js'; // 引入 HTTP 传输层
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import os from 'os';
// 1. 创建 MCP 服务器实例
const server = new Server(
{
name: 'system-monitor-mcp-server',
version: '0.1.0',
},
{
capabilities: {
tools: {}, // 启用工具功能
},
}
);
// 2. 定义并注册一个流式工具
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'get_system_stats_stream',
description: '获取实时系统性能指标流(CPU、内存、负载)。每秒推送一次数据,持续10秒。',
inputSchema: {
type: 'object',
properties: {
duration: {
type: 'number',
description: '流式输出的持续时间(秒),默认10秒',
default: 10,
},
},
},
},
],
};
});
// 3. 实现工具的处理逻辑(流式响应核心)
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== 'get_system_stats_stream') {
throw new Error(`Unknown tool: ${request.params.name}`);
}
const duration = (request.params.arguments as any)?.duration || 10;
const intervalSeconds = 1; // 每秒推送一次
// 这是一个异步生成器函数,用于产生流式数据块
async function* generateStats() {
const startTime = Date.now();
const endTime = startTime + duration * 1000;
let count = 0;
while (Date.now() < endTime) {
count++;
const stats = {
timestamp: new Date().toISOString(),
iteration: count,
cpuUsage: os.loadavg()[0].toFixed(2), // 1分钟平均负载
freeMemory: `${(os.freemem() / 1024 / 1024 / 1024).toFixed(2)} GB`,
totalMemory: `${(os.totalmem() / 1024 / 1024 / 1024).toFixed(2)} GB`,
memoryUsagePercent: (
(1 - os.freemem() / os.totalmem()) *
100
).toFixed(1),
};
// 使用 `content` 数组返回结构化数据,AI客户端能更好解析
yield {
content: [
{
type: 'text',
text: `[${stats.timestamp}] 采样 #${stats.iteration}: CPU负载(1min) ${stats.cpuUsage}, 内存使用 ${stats.memoryUsagePercent}% (${stats.freeMemory} free / ${stats.totalMemory} total)`,
},
],
};
// 等待下一次采样
await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));
}
// 流结束
yield {
content: [
{
type: 'text',
text: `系统监控流已结束,共采样 ${count} 次。`,
},
],
isLast: true, // 标记为最后一块
};
}
// 返回流式结果
return {
content: [], // 初始内容可为空,因为数据通过流传递
isStreaming: true, // 关键标志:声明这是一个流式响应
stream: generateStats(), // 传入异步生成器
};
});
// 4. 启动逻辑:支持 Stdio 和 HTTP 两种模式
async function main() {
const mode = process.argv[2];
if (mode === '--stdio') {
// 标准 Stdio 模式,供 Claude Desktop 等客户端子进程调用
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP System Monitor Server running in stdio mode.');
} else if (mode === '--http' || !mode) {
// HTTP 模式 (Streamable-HT 的基础)
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
const transport = new HttpTransport(`http://localhost:${port}`); // 创建 HTTP 传输层
// 将 Server 连接到 HTTP 传输层
// HttpTransport 内部会启动一个 Express.js 服务器来处理 /rpc 等端点
await server.connect(transport);
console.log(`MCP System Monitor Server (Streamable-HT ready) running on http://localhost:${port}`);
console.log(`You can connect via MCP clients that support HTTP transport.`);
} else {
console.error('Usage: node dist/server.js [--stdio | --http]');
process.exit(1);
}
}
// 错误处理
server.onerror = (error) => {
console.error('[MCP Server Error]', error);
};
main().catch((error) => {
console.error('Failed to start server:', error);
process.exit(1);
});
4.2 更新 Package.json 脚本
为了方便启动,修改 package.json 中的 scripts 部分。
{
"name": "mcp-streaming-server-example",
"version": "0.1.0",
"description": "An example MCP server with Streamable-HT support",
"main": "dist/server.js",
"scripts": {
"build": "tsc",
"start:stdio": "node dist/server.js --stdio",
"start:http": "node dist/server.js --http",
"dev:http": "tsx watch src/server.ts -- --http"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^0.4.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
},
"type": "module"
}
4.3 编译与运行
首先,编译 TypeScript 代码。
npm run build
以 HTTP 模式运行(Streamable-HT 基础) :
npm run start:http
# 或直接运行编译后的文件
node dist/server.js --http
如果看到输出 MCP System Monitor Server (Streamable-HT ready) running on http://localhost:3000 ,说明你的 HTTP 模式 MCP Server 已经启动成功。它现在监听在 3000 端口,等待兼容 MCP over HTTP 的客户端连接。
以 Stdio 模式运行(用于本地集成测试) :
npm run start:stdio
此模式会阻塞终端,等待来自标准输入的命令,通常由 Claude Desktop 这样的客户端自动调用。
4.4 配置 Claude Desktop 进行测试
为了验证我们的 Server 是否工作,需要将其配置到 MCP 客户端中。以 Claude Desktop 为例:
- 找到 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
- macOS :
- 编辑该 JSON 文件,在
mcpServers对象中添加我们的服务器配置。
{
"mcpServers": {
"system-monitor": {
"command": "node",
"args": [
"/ABSOLUTE/PATH/TO/YOUR/PROJECT/mcp-streaming-server-example/dist/server.js",
"--stdio"
]
}
}
}
注意 :必须使用 --stdio 参数,并且 command 需要是 node 的绝对路径或确保在系统 PATH 中。 args 中的 JS 文件路径也必须使用 绝对路径 。
- 保存文件并 完全重启 Claude Desktop 。
- 重启后,在 Claude 的聊天界面,你应该能看到一个新的工具图标(或通过
/命令列表),里面出现了get_system_stats_stream工具。尝试调用它,AI 会开始接收持续约10秒的流式系统监控数据。
5. 进阶:实现真正的 Streamable-HT 端点
上面的例子已经是一个功能完整的 MCP HTTP Server。但要充分发挥“流式”潜力,我们通常需要客户端也支持主动发起流式请求。MCP 协议本身支持在工具调用中返回 isStreaming: true 和 stream ,但需要客户端和传输层(Transport)协同处理这些流式数据块。
目前,MCP SDK 的 HttpTransport 主要处理标准的 JSON-RPC over HTTP 请求/响应。对于复杂的双向流(如 WebSocket),可能需要自定义传输层。
一个更贴近“Streamable-HT”概念的实践是: 将 MCP Server 封装为一个既支持普通 MCP 工具调用,又额外提供纯 HTTP SSE 端点的服务 。这样,AI 可以通过 MCP 工具触发一个监控任务,而其他前端应用(如仪表盘)可以直接连接到 SSE 端点观看实时数据流。
以下是 src/server.ts 的增强版,我们添加一个原生的 /stats-stream SSE 端点:
// ... 前面的 Server 创建和工具注册代码保持不变 ...
import express from 'express'; // 需要额外安装: npm install express
import { createServer } from 'http';
// 在 main 函数中,我们手动创建 Express 和 HTTP 服务器,实现更精细的控制
async function main() {
const app = express();
const httpServer = createServer(app);
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
// --- 原生 SSE 端点(独立于 MCP 协议)---
app.get('/stats-stream', (req, res) => {
console.log('Client connected to SSE endpoint /stats-stream');
// 设置 SSE 必需的响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*', // 根据实际情况调整 CORS
});
const sendEvent = (data: any) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
// 模拟每秒发送系统状态
const intervalId = setInterval(() => {
const stats = {
timestamp: new Date().toISOString(),
cpu: os.loadavg()[0],
memory: {
used: os.totalmem() - os.freemem(),
total: os.totalmem(),
},
};
sendEvent(stats);
}, 1000);
// 客户端断开连接时清理
req.on('close', () => {
console.log('Client disconnected from SSE endpoint');
clearInterval(intervalId);
res.end();
});
});
// --- SSE 端点结束 ---
// 创建 MCP HttpTransport,它会自动添加 `/rpc` 等端点
const transport = new HttpTransport(`http://localhost:${port}`);
// 注意:这里我们将 Express app 传递给 transport 的内部适配器(如果SDK支持)
// 当前版本的 SDK HttpTransport 可能内部自建服务器,为了集成SSE,我们需要更底层的操作。
// 以下是一种概念性代码,实际可能需要继承或自定义 Transport。
// await server.connect(transport);
console.log(`Server started.`);
console.log(`- MCP Endpoint: http://localhost:${port}/rpc (for MCP clients)`);
console.log(`- SSE Endpoint : http://localhost:${port}/stats-stream (for browsers/other clients)`);
httpServer.listen(port, () => {
console.log(`HTTP server listening on port ${port}`);
});
}
// ... 错误处理等保持不变 ...
这种架构下,你的服务具备了双重能力:通过 /rpc 端点服务 AI 客户端,通过 /stats-stream 端点服务需要实时数据的 Web 前端,这才是 Streamable-HT 更完整的形态。
6. 常见问题与排查思路
在开发和集成 MCP Server,特别是流式服务器时,会遇到一些典型问题。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| Claude Desktop 不显示工具 | 1. 配置文件路径或格式错误。 2. Server 启动命令失败。 3. Server 未正确实现 ListTools 请求。 |
1. 检查 claude_desktop_config.json 的 JSON 语法,确保路径绝对且正确。 2. 在终端手动运行配置中的 command 和 args ,看 Server 能否独立启动。 3. 查看 Claude Desktop 日志(通常可在应用设置中找到),寻找 MCP 相关的错误信息。 4. 在 Server 代码中添加 console.error 日志,确保 setRequestHandler 被调用。 |
| 调用工具后无响应或立即结束 | 1. 工具处理函数是同步的,没有返回正确的 Promise 或流式结构。 2. isStreaming 和 stream 字段未正确设置。 |
1. 确保工具处理函数是 async 的或返回 Promise 。 2. 对于流式工具,返回值必须包含 { isStreaming: true, stream: asyncGenerator } 。 3. 检查异步生成器函数 function* 是否正确使用了 yield 和 await 。 |
| HTTP 模式启动失败,端口被占用 | 端口 3000 已被其他应用使用。 | 1. 通过 lsof -i :3000 (macOS/Linux) 或 netstat -ano | findstr :3000 (Windows) 查找占用进程。 2. 终止占用进程,或修改 Server 代码中的 port 变量。 |
| SSE 端点连接后收不到数据 | 1. 响应头设置不正确。 2. 客户端未正确解析 SSE 格式。 3. 服务器端循环或发送逻辑有误。 |
1. 使用 curl -N http://localhost:3000/stats-stream 测试,看是否能收到持续的 data: {...} 事件。 2. 确保响应头包含 'Content-Type': 'text/event-stream' 。 3. 检查服务器端 setInterval 或循环逻辑是否正常执行。 |
| 流式输出在客户端显示为乱码或堆积 | AI 客户端对流式数据的渲染处理方式不同。 | 1. 这是正常现象。一些客户端会逐步显示流式内容,一些可能会在流结束后一次性显示。确保你的数据块是完整的句子或段落,便于阅读。 2. 在 yield 返回的 content 中,使用清晰的文本格式。 |
| 生产环境部署后连接不稳定 | 网络超时、防火墙、进程管理等问题。 | 1. 使用 pm2 、 systemd 或容器编排工具管理进程,保证异常退出后重启。 2. 配置合理的 HTTP 超时设置(对于长流连接尤为重要)。 3. 考虑在 Server 端实现心跳机制,定期发送注释行( : )以保持连接活跃。 |
7. 最佳实践与工程建议
将 MCP Server 用于生产环境或复杂项目时,遵循以下最佳实践可以提升稳定性、安全性和可维护性。
-
清晰的工具定义与文档 :
- 在
ListTools返回的description和inputSchema中提供详尽、清晰的描述。这是 AI 理解和使用你工具的唯一依据。 - 为每个参数提供
description和default值。 - 示例:
{ "name": "query_database", "description": "执行安全的SQL查询。仅支持SELECT操作。", "inputSchema": { "type": "object", "properties": { "sql": { "type": "string", "description": "要执行的SELECT SQL语句。" }, "timeoutMs": { "type": "number", "description": "查询超时时间(毫秒)。", "default": 5000 } }, "required": ["sql"] } }
- 在
-
健壮的错误处理 :
- 在工具实现内部使用
try-catch包裹核心逻辑。 - 向客户端返回结构化的错误信息,而不是任由异常抛出导致整个 Server 崩溃。
- MCP 错误响应应遵循协议规范。
- 示例:
server.setRequestHandler(CallToolRequestSchema, async (request) => { try { // ... 业务逻辑 ... return { content: [{ type: 'text', text: 'Success' }] }; } catch (error) { console.error(`Tool ${request.params.name} failed:`, error); // 返回符合 MCP 协议的错误 return { content: [{ type: 'text', text: `操作失败: ${error.message}` }], isError: true, }; } });
- 在工具实现内部使用
-
资源管理与清理 :
- 对于流式工具,尤其是那些会开启定时器、数据库连接或文件监听的, 务必 在流结束或客户端断开连接时进行清理。
- 在异步生成器函数中使用
try...finally块,或在req.on('close')事件中清理资源。 - 示例:
async function* generateData() { const timer = setInterval(() => { /* ... */ }, 1000); try { while (/* condition */) { yield { /* data */ }; } } finally { clearInterval(timer); // 确保定时器被清除 console.log('Stream resources cleaned up.'); } }
-
安全性考量 :
- 权限控制 :不是所有工具都应对所有用户开放。考虑在 Server 启动时读取环境变量或配置文件来启用/禁用特定工具。
- 输入验证与净化 :永远不要相信来自客户端的输入。对工具参数进行严格的类型和范围检查,防止注入攻击(如 SQL、命令注入)。
- 敏感信息 :避免在工具描述、日志或错误信息中泄露服务器路径、API密钥、数据库凭据等。
- HTTP 模式 :如果暴露 HTTP 端点,务必配置 CORS、设置速率限制、考虑添加认证层(如 API Key)以防止未授权访问。
-
性能与可观测性 :
- 对于计算密集型或 IO 密集型的工具,考虑实现超时机制,防止单个请求阻塞服务器过久。
- 添加日志记录,记录工具调用、参数(脱敏后)、耗时和结果状态。这对于调试和监控至关重要。
- 使用
process.on('SIGINT', ...)和process.on('SIGTERM', ...)监听信号,实现优雅关机,完成正在处理的流式请求。
-
配置化与可扩展性 :
- 将服务器配置(如端口、启用工具列表、外部API地址)抽离到环境变量或配置文件中。
- 设计工具注册机制,避免在
server.ts主文件中堆积所有工具的实现,可以按模块划分。
通过深入理解 MCP 协议、掌握 Streamable-HT 这类高级模式,并践行上述工程实践,你构建的就不再是一个简单的脚本,而是一个可靠、强大、可融入现代 AI 应用架构的基础设施组件。从实时数据推送到长任务管理,MCP 为 AI 赋能外部系统打开了新的大门,而扎实的实现是这一切的基石。
更多推荐
所有评论(0)