从零搭建 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里交流。
相关推荐
sudo_jin10 小时前
别再造轮子了!2026年,用 MCP 给你的 AI 插上“万能 USB 接口”
mcp
zhangshuang-peta1 天前
加密MCP保险库:人工智能系统中安全凭证管理的关键
人工智能·安全·chatgpt·ai agent·mcp·peta
IT 行者1 天前
每天了解几个MCP SERVER:GitHub
github·mcp
阿杆1 天前
五分钟配好向日葵 MCP,让 AI 替你远程安装 OpenClaw!
后端·aigc·mcp
IT 行者1 天前
每天了解几个MCP SERVER:21st.dev Magic
人工智能·ui·mcp
SelectDB技术团队1 天前
易车 × Apache Doris:构建湖仓一体新架构,加速 AI 业务融合实践
数据仓库·人工智能·数据分析·agent·apache doris·mcp·易车
fundroid1 天前
从零构建用于 Android 开发的 MCP 服务:原理、实践与工程思考
android·ai编程·mcp
IT 行者1 天前
每天了解几个MCP SERVER:Atlan 数据目录平台
人工智能·mcp
ETA81 天前
AI 界的"USB-C":解析 MCP 协议如何统一工具调用标准
cursor·mcp