UI-TARS 桌面端开源 mcp-http-server:构建 HTTP+SSE 双协议 MCP 服务端的完整指南
【免费下载链接】UI-TARS-desktopThe Open-Source Multimodal AI Agent Stack: Connecting Cutting-Edge AI Models and Agent Infra项目地址: https://gitcode.com/GitHub_Trending/ui/UI-TARS-desktop
mcp-http-server是 UI-TARS-desktop 仓库中packages/agent-infra/mcp-http-server目录下的一个可独立发布的高性能 MCP(Model Context Protocol)HTTP 服务库,它同时支持 Streamable HTTP 与 Server-Sent Events(SSE)两种传输协议,并把「HTTP 请求头透传到 MCP Server 上下文」作为一等特性。本文以该库的官方 README 为骨架,结合其 核心实现源码 与 端到端测试,完整讲解如何从零启动一个带鉴权中间件、可自定义路由前缀、支持无状态高并发与有状态会话两种模式的服务端,读者读完可直接在自己的 MCP Server 项目中落地部署,并理解其底层调用链与边界行为。
功能总览
按 README 的 Features 清单,该库提供五类核心能力:
| 能力 | 说明 |
|---|---|
| HTTP/SSE 双传输 | 同时支持 HTTP POST 与 Server-Sent Events 两种 MCP 传输 |
| 灵活路由 | 端点路径与前缀全部可配置,支持默认/mcp、/message、/sse布局 |
| 请求头透传 | 在createMcpServer工厂函数中直接拿到本次 HTTP 请求的 Headers |
| 双运行模式 | Streamable HTTP 支持有状态(stateful)与无状态(stateless)两种模式 |
| 中间件支持 | 可注入自定义 Express 中间件,用于鉴权、日志、限流等横切逻辑 |
从源码看,该库本质上是基于express(v5)与官方@modelcontextprotocol/sdk(~1.15.1,见 package.json)之上的「轻封装调度层」:它替你管理StreamableHTTPServerTransport与SSEServerTransport的生命周期、路由注册与错误兜底,让开发者只需关注「如何根据请求上下文创建一个业务 MCP Server」。
安装与最小启动
安装依赖:
npm i mcp-http-server -S官方 README 给出的 Quick Start 非常精简:
import { startSseAndStreamableHttpMcpServer } from 'mcp-http-server'; import { createServer } from './server.js'; await startSseAndStreamableHttpMcpServer({ port: 3000, createMcpServer: async (params) => { console.log('Request headers:', params.headers); return createServer(); }, });这里有两个关键点值得展开:
createMcpServer是工厂而非实例。源码中每次新连接到来时都会调用该工厂并传入{ headers: req.headers }(见 startServer.ts),因此每个客户端会话可以拿到彼此隔离、且携带本次 HTTP 请求头信息的 Server 实例,也自然可以做到「每个请求头对应一个独立 MCP 会话 / 鉴权上下文」。这也是 CHANGELOG 中feat: support request headers params、feat: support getRequestContext等迭代引入的能力。- HTTP 传输与 SSE 传输共用同一个工厂。无论是客户端 POST 到
/mcp,还是通过GET /sse建立事件流,底层都会走createMcpServer({ headers: req.headers })。
包入口文件 src/index.ts 只做了一件事:export * from './startServer',即对外暴露的核心 API 就是startSseAndStreamableHttpMcpServer。
一个完整的可运行示例
基于 README 的 Basic HTTP Server 示例,用官方 SDK 直接构造一个带 tools 能力的 Server:
import { startSseAndStreamableHttpMcpServer } from 'mcp-http-server'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; const server = await startSseAndStreamableHttpMcpServer({ port: 3000, createMcpServer: async () => { return new Server( { name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } } ); }, }); console.log(`Server running at ${server.url}`);启动成功后即可在 MCP 客户端中把服务地址指向返回的url(默认是http://127.0.0.1:3000/mcp),客户端既可以是支持 Streamable HTTP 的新版客户端,也可以是支持 SSE 的客户端——服务端对两者同时开放。
API 参考:startSseAndStreamableHttpMcpServer(params)
该函数是库的唯一入口,返回一个Promise<McpServerEndpoint>。其全部参数见 README 的表格,并结合 源码接口定义 可整理出精确的类型语义:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
port | number | 8080或环境变量PORT | 监听端口。源码中取值为Number(port \|\| process.env.PORT \|\| 8080),即三个来源的优先级依次是显式port→process.env.PORT→8080 |
host | string | '127.0.0.1' | 绑定主机。源码const HOST = host \|\| '127.0.0.1' |
stateless | boolean | true | 是否启用 Streamable HTTP 的无状态模式 |
middlewares | MiddlewareFunction[] | 无 | 自定义 Express 中间件数组,按数组顺序挂载 |
routes | RoutesConfig | { prefix: '/', mcp: '/mcp', message: '/message', sse: '/sse' } | 路由配置 |
logger | Logger | ConsoleLogger实例 | 自定义日志器,类型来自@agent-infra/logger |
createMcpServer | (req: RequestContext) => Promise<McpServer \| Server> | 必填 | MCP Server 工厂函数 |
其中RequestContext在源码中被定义为Pick<Request, 'headers'>,即当前 HTTP 请求的完整请求头对象(IncomingHttpHeaders),这是实现「按请求头鉴权 / 多租户隔离」的关键数据来源。
路由配置
服务端默认注册了四条路由(见 README 的 Default Routes 及测试对默认路径的断言):
| 方法与路径 | 用途 |
|---|---|
POST /mcp | MCP HTTP transport 主入口,接收 JSON-RPC 请求 |
GET /mcp | 返回405(Method Not Allowed) |
GET /sse | SSE 连接端点,客户端在此建立事件流 |
POST /message | SSE 消息端点,客户端把请求 POST 到这里 |
SSE 的通信模型是「GET 建流、POST 发消息」:客户端先GET /sse拿到服务端在SSEServerTransport中生成的sessionId,之后所有请求 POST 到/message?sessionId=xxx,服务端通过 sessionId 找到对应 transport 再转发给 MCP Server。源码中对缺失或未知 sessionId 的/message请求会返回400(见 startServer.ts)。
自定义路由与路径规范化
当你的服务需要挂到网关上时,可以用routes整体重命名路径,比如 README 的例子:
await startSseAndStreamableHttpMcpServer({ port: 3000, routes: { prefix: '/api/v1', mcp: '/custom-mcp', message: '/custom-message', sse: '/custom-sse' }, createMcpServer: async () => createServer(), });最终端点会变成:
POST /api/v1/custom-mcp—— MCP HTTP 传输入口GET /api/v1/custom-sse—— SSE 连接端点POST /api/v1/custom-message—— SSE 消息端点
路径拼接有明确的规范化规则(见源码中的buildPath辅助函数,startServer.ts):
prefix末尾的/会被裁掉,根前缀/会被特殊处理为「不拼前缀」;- 各子路径缺省开头的
/会被自动补上; - 因此下面两种写法完全等价,都会得到
/api/v2/mcp-endpoint与/api/v2/sse-endpoint/:
// 写法一 routes: { prefix: '/api/v2/', mcp: 'mcp-endpoint', sse: '/sse-endpoint/' } // 写法二 routes: { prefix: '/api/v2', mcp: '/mcp-endpoint', sse: 'sse-endpoint' }路由配置同样支持部分覆盖:未配置的子项回落默认值。例如只配{ prefix: '/api', mcp: '/custom-mcp' },则实际生效为POST /api/custom-mcp+GET /api/sse,这一点在 routes 测试 中被明确验证。
中间件支持
中间件在框架内部挂载点早于任何 MCP 路由(源码中middlewares.forEach((middleware) => app.use(middleware))位于 SSE / HTTP 路由注册之前),因此可以拦截、改写或终结请求。README 给出了一个典型的 Bearer Token 鉴权示例:
import { startSseAndStreamableHttpMcpServer } from 'mcp-http-server'; const authMiddleware = (req, res, next) => { const token = req.headers.authorization; if (!token) { return res.status(401).json({ error: 'Unauthorized' }); } next(); }; await startSseAndStreamableHttpMcpServer({ port: 3000, host: 'localhost', routes: { prefix: '/api/v1', mcp: '/mcp', sse: '/events' }, middlewares: [authMiddleware], createMcpServer: async (context) => { console.log('User agent:', context.headers['user-agent']); return createServer(); }, });鉴权最佳实践:由于鉴权中间件在 MCP 路由之前执行,未携带Authorization的请求会被直接以401拦截,根本不会走到createMcpServer;而通过鉴权的请求,其 headers 又会经RequestContext传给工厂函数。两者组合即可实现「中间件负责拦、工厂函数负责读」的分层鉴权。仓库内的浏览器 MCP 服务 mcp-servers/browser/src/index.ts 就实际引用了startSseAndStreamableHttpMcpServer,并配合commander解析--host、--port等参数,是生产级调用范本。
传输模式:无状态与有状态
无状态模式(推荐,默认开启)
await startSseAndStreamableHttpMcpServer({ stateless: true, // 默认值 createMcpServer: async () => createServer(), });无状态模式下:
- 服务端不维护任何客户端会话;
- 每次请求互相独立,天然适合横向扩容与负载均衡;
- 每个请求都会创建全新的 transport 与 MCP Server 实例,适合无状态工具类服务。
对应到源码,无状态分支创建 transport 时显式传入sessionIdGenerator: undefined与enableJsonResponse: true,即 SDK 文档规定的无状态 Streamable HTTP Server 配置(见 startServer.ts)。
有状态模式
await startSseAndStreamableHttpMcpServer({ stateless: false, // 关闭无状态,启用有状态 createMcpServer: async () => createServer(), });有状态模式下:
- 每个客户端会获得唯一的 session ID;
- 同一会话的后续请求会复用已建立的 transport,状态(如长连接、订阅、上下文)跨请求保持;
- 会话需要被正确清理。
其底层逻辑较为精巧,见 startServer.ts:
- 请求头带
mcp-session-id且命中内存表transports.streamable→复用已有 transport; - 无 sessionId 且请求体是
initialize(用 SDK 的isInitializeRequest判断)→ 新建 transport,sessionIdGenerator: () => randomUUID()生成 UUID,并在onsessioninitialized回调中把 transport 登记到 Map; - 其余情况(带未知 sessionId、或初始化前直接发非 initialize 请求)→ 返回
400,错误码为ErrorCode.ConnectionClosed。
同时,transport 在onclose时会把自身从 Map 中移除,避免内存泄漏。生产环境建议:若使用有状态模式且需要多实例水平扩展,应把会话存储外置(如 Redis),因为当前实现把会话保存在进程内存中;无状态模式则没有此顾虑。
返回值McpServerEndpoint与优雅关闭
函数返回一个McpServerEndpoint对象,方便外部获取实际监听地址与关闭句柄:
interface McpServerEndpoint { url: string; // 完整的 MCP HTTP 端点 URL sseUrl: string; // 完整的 SSE 端点 URL port: number; // 实际监听端口 close: () => void; // 关闭服务器 }示例(摘自 README,并补充了优雅关闭的惯用法):
const endpoint = await startSseAndStreamableHttpMcpServer({ port: 3000, routes: { prefix: '/api' }, createMcpServer: async () => createServer(), }); console.log('MCP endpoint:', endpoint.url); // http://localhost:3000/api/mcp console.log('SSE endpoint:', endpoint.sseUrl); // http://localhost:3000/api/sse // 优雅关闭:响应 SIGTERM / SIGINT 等信号 process.on('SIGTERM', () => { endpoint.close(); });注意url/sseUrl中的 host 取自实际绑定结果:当监听地址为 IPv6 时会以[addr]形式包裹(见 startServer.ts),返回的 URL 直接可交给 MCP 客户端使用。
命令行集成:同时支持 HTTP 与 Stdio
很多 MCP Server 需要同时支持「远程 HTTP 接入」与「本地 stdio 接入」(后者被桌面客户端 / Agent 作为子进程拉起)。README 给出的commander模式即为此设计——根据是否传入--port/--host自动选择传输:
import { program } from 'commander'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; program .name('mcp-server') .description('MCP server with HTTP and stdio support') .version('1.0.0') .option('--host <host>', 'host to bind server to') .option('--port <port>', 'port to listen on for HTTP transport') .option('--prefix <prefix>', 'route prefix for HTTP endpoints', '/api') .action(async (options) => { try { if (options.port || options.host) { // HTTP/SSE transport await startSseAndStreamableHttpMcpServer({ host: options.host, port: options.port ? parseInt(options.port) : undefined, routes: { prefix: options.prefix, }, createMcpServer: async (params) => { console.log('HTTP request from:', params.headers['user-agent']); return createServer(); }, }); } else { // Stdio transport const server = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.debug('MCP Server running on stdio'); } } catch (error) { console.error('Error:', error); process.exit(1); } }); program.parse();值得注意的一个工程细节:HTTP 分支中日志要使用console.log/ 自定义 logger 而避免在 stdio 分支用console.log打印非调试内容——stdio 通道本身就是 MCP 消息管道,任何多余输出都会污染协议数据;stdio 分支使用console.debug(可被--debug之类开关关闭)正是为了规避这一点。仓库中 mcp-servers/browser/src/index.ts 等已落地的 MCP Server 均采用了类似的 HTTP/stdio 双通道 CLI 结构。
源码视角:错误处理与协议合规
从 核心实现 可以看出,除了路由与传输管理,框架还内置了三层协议合规处理,这部分是 README 未展开但值得写进生产认知的:
- JSON 解析错误兜底:
express.json()解析失败的请求(非法 JSON body)会被错误中间件捕获,统一返回400+ JSON-RPC 格式的ParseError响应,而不是 Node 默认的 HTML 错误页。测试中向/mcpPOST 一段invalid json{会得到400且 body 含jsonrpc: '2.0'与error.code、error.message字段(见 mcpServer 测试)。 GET/DELETE请求/mcp统一返回405:错误码为ErrorCode.ConnectionClosed,消息Method not allowed.,这是 Streamable HTTP 规范要求的「只有 POST 才被允许」的明确拒绝信号。- 内部异常兜底:
transport.handleRequest抛错时,若响应头尚未发送,则回写500+InternalError的 JSON-RPC 响应;否则只能记录日志,避免出现半截响应悬挂。
这套行为在 startServer-server.test.ts 与上述两个测试文件中都有对应的端到端覆盖,可视为框架对外承诺的稳定契约。
请求头透传的典型落地
请求头透传配合createMcpServer工厂,能解锁三类典型场景:
- 多租户 / 多配置隔离:依据
x-tenant-id之类的请求头,为不同租户创建不同配置(模型、权限、插件集合)的 MCP Server 实例,实现单端口多租户; - 用户态识别:读取
authorization或自定义x-user-id,把当前用户信息注入 Server 的 tool 执行上下文; - 设备 / 渠道审计:像 README CLI 示例那样记录
user-agent,或在日志中带上req.ip(源码对 SSE 与 HTTP 请求均有logger.info记录来源 IP)。
测试中对这一特性有直接验证:通过StreamableHTTPClientTransport发送自定义头x-custom-header、x-client-id、authorization,服务端在createMcpServer收到的req.headers能逐一还原(见 mcpServer 测试),证明请求头在任何 MCP 调用(包括工具执行、资源读取)时都会在工厂处可读。
小结
mcp-http-server的核心价值在于:把官方 MCP SDK 中繁琐的 HTTP/SSE transport 接线、session 管理、JSON-RPC 错误规约统一收敛为一个函数startSseAndStreamableHttpMcpServer,同时保留了 Express 中间件生态与按请求头创建 Server 的扩展空间。你可以把它当作独立的 npm 依赖使用,也可以直接参考仓库内 mcp-servers/browser、mcp-servers/filesystem、mcp-servers/commands 等生产级调用方的写法,以及 create-new-mcp 脚手架模板,快速搭建你自己的多协议 MCP Server。
需要进一步深挖时,可直接阅读以下文件:入口类型与默认路由定义 startServer.ts、路由与路径规范化测试 startServer-routes.test.ts、请求头与错误处理测试 startServer-mcpServer.test.ts,以及依赖与版本声明 package.json。
本文档对应源码遵循 MIT License,详见包内 package.json 与仓库根目录 LICENSE。
【免费下载链接】UI-TARS-desktopThe Open-Source Multimodal AI Agent Stack: Connecting Cutting-Edge AI Models and Agent Infra项目地址: https://gitcode.com/GitHub_Trending/ui/UI-TARS-desktop
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考