最近把我们的量化信号源接入了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:
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里交流。