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.json on 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 的两种主要运行模式:

  1. Stdio 模式(默认) : Server 作为一个独立的子进程启动,通过标准输入(stdin)和标准输出(stdout)与客户端进行 JSON-RPC 通信。这是最简单、最常见的模式,适用于工具调用和一次性数据查询。
  2. 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 为例:

  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. 编辑该 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 文件路径也必须使用 绝对路径

  1. 保存文件并 完全重启 Claude Desktop
  2. 重启后,在 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 用于生产环境或复杂项目时,遵循以下最佳实践可以提升稳定性、安全性和可维护性。

  1. 清晰的工具定义与文档

    • 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"]
        }
      }
      
  2. 健壮的错误处理

    • 在工具实现内部使用 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,
          };
        }
      });
      
  3. 资源管理与清理

    • 对于流式工具,尤其是那些会开启定时器、数据库连接或文件监听的, 务必 在流结束或客户端断开连接时进行清理。
    • 在异步生成器函数中使用 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.');
        }
      }
      
  4. 安全性考量

    • 权限控制 :不是所有工具都应对所有用户开放。考虑在 Server 启动时读取环境变量或配置文件来启用/禁用特定工具。
    • 输入验证与净化 :永远不要相信来自客户端的输入。对工具参数进行严格的类型和范围检查,防止注入攻击(如 SQL、命令注入)。
    • 敏感信息 :避免在工具描述、日志或错误信息中泄露服务器路径、API密钥、数据库凭据等。
    • HTTP 模式 :如果暴露 HTTP 端点,务必配置 CORS、设置速率限制、考虑添加认证层(如 API Key)以防止未授权访问。
  5. 性能与可观测性

    • 对于计算密集型或 IO 密集型的工具,考虑实现超时机制,防止单个请求阻塞服务器过久。
    • 添加日志记录,记录工具调用、参数(脱敏后)、耗时和结果状态。这对于调试和监控至关重要。
    • 使用 process.on('SIGINT', ...) process.on('SIGTERM', ...) 监听信号,实现优雅关机,完成正在处理的流式请求。
  6. 配置化与可扩展性

    • 将服务器配置(如端口、启用工具列表、外部API地址)抽离到环境变量或配置文件中。
    • 设计工具注册机制,避免在 server.ts 主文件中堆积所有工具的实现,可以按模块划分。

通过深入理解 MCP 协议、掌握 Streamable-HT 这类高级模式,并践行上述工程实践,你构建的就不再是一个简单的脚本,而是一个可靠、强大、可融入现代 AI 应用架构的基础设施组件。从实时数据推送到长任务管理,MCP 为 AI 赋能外部系统打开了新的大门,而扎实的实现是这一切的基石。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐