从零搭建 MCP Server:让 AI Agent 调用实盘量化信号

最近把我们的量化信号源接入了MCP协议,做了一个MCP Server,部署到npm、GitHub、远程SSE三种形态上。踩了不少坑,记录一下实战过程。

背景:为什么要做MCP Server

我们团队运行着8个宏观因子量化策略(美股+A股),每天产出交易信号。之前信号只通过微信小程序推送给订阅用户,触达面很窄。

MCP(Model Context Protocol)是Anthropic在2024年底推出的开放协议,让AI大模型可以调用外部工具。接入MCP后,用户对Claude说一句"帮我看看有没有量化策略可以用",AI就能直接调我们的接口,返回策略列表、历史表现、最新信号。

核心价值:AI Agent成了分发渠道。 用户不需要下载App、不需要注册网站,在对话里就完成了从发现到试用的完整闭环。

架构设计

一套Tool定义,三种Transport:

javascript 复制代码
src/index.ts (TypeScript)
    ├─ npm/stdio    → Claude Desktop, Cursor (本地运行)
        ├─ Streamable HTTP → 远程调用 (Seattle服务器)
            └─ Legacy SSE     → 扣子等国内平台 (国内CVM)
            ```
            
            为什么要三种?因为不同MCP Client支持的协议不同:
            - **Claude Desktop / Cursor**:只支持stdio,通过 `npx quanttogo-mcp` 本地启动
            - **远程客户端**:Streamable HTTP是新标准,一个POST endpoint搞定
            - **扣子(Coze)等国内平台**:只认Legacy SSE,需要`/sse` + `/message`两个endpoint
            
            ## Tool设计:8个工具的分层
            
            ```javascript
            // 发现层(免费,无鉴权)
            list_strategies           // 列出所有策略+表现数据
            get_strategy_performance  // 单策略详细数据+NAV历史
            compare_strategies        // 2-8个策略横向对比
            get_index_data           // 自定义指数数据
            get_subscription_info    // 订阅计划+试用引导
            
            // 信号层(需要apiKey)
            register_trial    // 邮箱注册试用 → 返回apiKey
            get_signals       // 获取买卖信号
            check_subscription // 查询试用/订阅状态
            ```
            
            **设计原则:先让Agent"看到",再让Agent"获取"。** 发现层完全免费无鉴权,Agent可以自由浏览所有策略数据。只有在用户明确要获取交易信号时,才需要通过`register_trial`注册获取apiKey。
            
            这个分层很关键------如果所有tool都要鉴权,Agent根本无法向用户展示"我们有什么",漏斗的入口就堵死了。
            
            ## 实战踩坑
            
            ### 坑1:StreamableHTTPServerTransport的session管理
            
            官方SDK的 `StreamableHTTPServerTransport` 用法看起来简单,但坑在于:**sessionId是在 `handleRequest()` 过程中才被设置到transport上的**。
            
            ```javascript
            // ❌ 错误:此时transport.sessionId还是undefined
            sessions.set(transport.sessionId, { transport, server });
            await transport.handleRequest(req, res, req.body);
            
            // ✅ 正确:先处理请求,再存session
            await transport.handleRequest(req, res, req.body);
            sessions.set(transport.sessionId, { transport, server });
            ```
            
            ### 坑2:express.json() 的body传递
            
            `handleRequest` 的第三个参数需要传parsed body。如果你用了 `express.json()` 中间件,必须显式传 `req.body`:
            
            ```javascript
            app.post('/mcp', express.json(), (req, res) => {
              transport.handleRequest(req, res, req.body); // 第三个参数!
              });
              ```
              
              不传第三个参数,SDK会尝试自己parse,但request stream已经被express消费了,结果就是空body。
              
              ### 坑3:扣子不发Accept header
              
              MCP SDK要求请求头包含 `Accept: application/json, text/event-stream`。但扣子(Coze)不带这个header,SDK直接返回406。
              
              解决方案------nginx层面补上:
              
              ```nginx
              location /sse {
                  proxy_set_header Accept "application/json, text/event-stream";
                      proxy_pass http://127.0.0.1:3100;
                          proxy_buffering off;
                              proxy_cache off;
                              }
                              ```
                              
                              ### 坑4:DELETE请求的session清理时序
                              
                              客户端断开时发DELETE请求。SDK在 `handleRequest` 内部的 `onclose` 回调里会清理session。所以要在调handleRequest之前先保存transport/server引用。
                              
                              ### 坑5:Legacy SSE的keepalive
                              
                              Legacy SSE连接容易被代理超时断开。解决方案:每30秒发一个comment:
                              
                              ```javascript
                              setInterval(() => res.write(': keepalive\n\n'), 30000);
                              ```
                              
                              ## 部署:4个平台一次搞定
                              
                              | 平台 | 形态 | 用途 |
                              |------|------|------|
                              | npm | stdio | Claude Desktop, Cursor |
                              | GitHub | source | 开源可审计 |
                              | Seattle | Streamable HTTP | 国际用户 |
                              | China CVM | SSE + Streamable HTTP | 国内平台(扣子) |
                              
                              npm发布后,用户只需要在Claude Desktop配置里加一行:
                              
                              ```json
                              { "mcpServers": { "quanttogo": { "command": "npx", "args": ["-y", "quanttogo-mcp"] } } }
                              ```
                              
                              ## 总结
                              
                              **最大的感受是:MCP真正实现了"一次开发,全平台可用"。** 同一套tool定义,跑在stdio/SSE/Streamable HTTP三种transport上,无需为每个平台写适配代码。这不是替代API,而是让API被AI发现和调用的标准化接口。
                              
                              项目开源在 GitHub: [QuantToGo/quanttogo-mcp](https://github.com/QuantToGo/quanttogo-mcp)
                              
                              npm: `npx quanttogo-mcp`
                              
                              有问题欢迎在GitHub Discussion里交流。
相关推荐
老H科研技术10 小时前
第 04 篇:MCP中SDK 对比与选型 —— 选对工具,事半功倍
人工智能·mcp
木雷坞11 小时前
Playwright MCP Docker 部署:mcr 镜像、浏览器工具和权限配置
运维·docker·容器·mcp
winlife_13 小时前
全程用 AI 做一款商业级手游 · EP10 道具系统:让三个按钮真正改变棋盘
windows·算法·unity·ai编程·游戏开发·mcp·玩法系统
Mr_Morning13 小时前
MCP 通信
mcp
MicrosoftReactor13 小时前
技术速递|以 Token 经济学驱动的架构:混合模型、AI Runway、AKS Kata MicroVM 与 MCP
人工智能·ai·架构·copilot·mcp
不剪发的Tony老师14 小时前
DBHub:一款免费开源的数据库MCP服务器
数据库·mcp
特长腿特长15 小时前
Cherry Studio 通过 MCP 接口操作 Obsidian 完全指南
ai·obsidian·mcp
Python私教15 小时前
AI 代理只会在本地打转?我用 MCP 给它接上手脚,3 步接通第一个外部服务
agent·ai编程·mcp
心之伊始1 天前
Spring AI MCP Client 实战:让 Java 后端通过 stdio 调用本地工具服务
java·spring boot·agent·spring ai·mcp
winlife_1 天前
全程用 AI 做一款商业级手游 · EP9 收尾与复盘:做到了哪,没做到哪,边界在哪
java·开发语言·人工智能·unity·ai编程·游戏开发·mcp