linker-report-write-mcp
v0.1.0
Published
MCP server for Linker report AI filling - page skeleton lookup and per-page content writing (weekly / daily reports). Write-only by design; queries live in the read-only MCP servers.
Readme
linker-report-write-mcp
Linker 报告(周报 / 日报)写入型 MCP Server。
面向 AI Agent,提供「读页面骨架 → 逐页填写」两个工具,供 Hermes agent 在报告编辑页按页填充内容。每填完一页即推进完成度,前端据此实时更新进度与占位动画。
读写分离:本 MCP 只写,不含任何行情/持仓/研报查询。查询请用只读 MCP(
linker-position-mcp、linker-trading-data-mcp、linker-report-mcp)。原因见下方「安全模型」——写通道与超管 Key 不能共处一个进程。
快速开始
环境变量
TRADING_API_BASE=http://hub.linker.net/trading-api
REPORT_WRITE_CALLBACK_SECRET=<运维下发,勿复用只读的 TRADING_API_KEY>两者缺一进程直接退出。REPORT_WRITE_CALLBACK_SECRET 是专用回调密钥,不要填只读 MCP 用的 TRADING_API_KEY——后者在后端会命中超管上下文(见「安全模型」)。
配置方式
Hermes(~/.hermes/config.yaml,运维执行):
mcp_servers:
linker-report-write:
command: npx
args: ["-y", "linker-report-write-mcp@latest"]
env:
TRADING_API_BASE: "http://hub.linker.net/trading-api"
REPORT_WRITE_CALLBACK_SECRET: "<secret>"改完需 /reload-mcp 或重启 Hermes——MCP 是启动时全局注册的,无法按请求注入。
本地开发(直接用编译产物):
{
"mcpServers": {
"linker-report-write": {
"command": "node",
"args": ["C:/Users/94503/Documents/GitHub/linker_mcp/packages/linker-report-write-mcp/dist/index.js"],
"env": {
"TRADING_API_BASE": "http://hub.linker.net/trading-api",
"REPORT_WRITE_CALLBACK_SECRET": "<secret>"
}
}
}
}工具列表
| Tool | 说明 | 参数 | 只读 |
|------|------|------|------|
| get_skeleton | 读某份报告实例的页面骨架(每页可填字段 key/标签/是否已填 + 完成度) | report_type, instance_id, job_token | ✅ |
| fill_page | 写入某一页内容,推进完成度 | report_type, instance_id, page_id, fields, job_token, note? | ❌ |
report_type:weekly(周报)/ daily(日报)。日报分支待后端落地后启用,MCP 侧已就绪。
本 MCP 刻意不提供删除、不提供配置修改类工具。 能力面最小化是四道锁之一。
典型调用序列
1. get_skeleton(weekly, 123, token) → 拿到 P1..P16 各页字段 key 与完成度
2. fill_page(weekly, 123, "P4", {...}) → 完成度 1/16,前端进度条动
3. fill_page(weekly, 123, "P5", {...}) → 2/16
...逐页推进agent 必须先 get_skeleton 再 fill_page:字段 key 由骨架决定,凭记忆猜会被后端白名单拒绝。
安全模型
详见 docs/weekly-report-feature.md §14.6。四道锁:
- job_token 绑定:每次 AI 任务由系统签发 token,服务端 Redis 中绑定
(user_id, instance_id),30 分钟 TTL。token 是真正的身份凭证。 - 非超管上下文重建:后端从 token 重建非超管上下文再跑归属校验,所以 A 的 token 打不到 B 的报告。
- 最小能力面:只有
fill_page+get_skeleton,无删除、无配置改动。 - 一次性 + 限流:token 用后即焚并限速。
⚠️ 为什么不能复用 TRADING_API_KEY
后端 X-API-Key 会命中 _api_key_superuser_context()(auth.py:27),产出 is_superuser=True, monitor_scope="all";而 check_resource_access(auth.py:509)首行就是
if is_superuser or monitor_scope == "all": return——归属校验被整体跳过。所以本 MCP 里 X-API-Key 只用于打通通道,不授予任何写权限;授权完全靠 job_token。这也是查询 MCP 必须与写 MCP 分进程的原因:超管 Key 不能和写操作共处一个信任边界。
横向提示:该 bypass 对所有复用此 Key 的写接口成立,不止报告模块,值得单独排查。
富文本
fields 的值支持有限富文本(b/i/u/s/code/span[style=color]、ul/ol/li/p、br),服务端按白名单清洗。script/style/onclick 等一律剥离。
错误语义
后端的 message 会原样带回给 agent,便于它据错自我修正:
| 状态 | 含义 | agent 应做的 |
|------|------|-------------|
| 403 | job_token 无效/过期/越权 | 停止,向用户报告需重新发起任务 |
| 422 | 字段 key 不在该页骨架内 | 重新 get_skeleton 校对 key 后重试 |
| 404 | 实例或页不存在 | 停止 |
fill_page 对同一页重复调用为覆盖,完成度不重复累加(幂等)。
开发
cd packages/linker-report-write-mcp
npm install
npm run build # tsc 编译
npm run watch # 开发热编译
node test_tool.mjs # 工具测试测试
写工具需 job_token 且依赖后端 ai-skeleton/ai-fill-page 端点(backend 阶段 6-3),故测试分两档。
离线契约档(默认,内建 mock server,无需后端与真实 token):
node test_tool.mjs覆盖:越权 token → 403 且 message 透传、骨架正常返回、未知字段 → 422、正常写入推进完成度、重复填同页幂等。
输出:
=== 离线契约档 ===
═══ Test 1: get_skeleton 越权 token 应 403 ═══
✓ 403 且 message 透传: 403 job_token 无效或越权
═══ Test 2: get_skeleton 正常 ═══
✓ 骨架 pages=1, progress=0/1
═══ Test 3: fill_page 未知字段应 422 ═══
✓ 拒绝未知字段: 422 未知字段: nope
═══ Test 4: fill_page 正常写入 ═══
✓ 写入 filled_count=2, progress=1/1
═══ Test 5: fill_page 幂等 ═══
✓ 幂等: progress 仍=1/1
✅ 离线契约档全部通过联调档(6-3 端点落地后,打真实后端):
LIVE=1 TRADING_API_BASE=http://hub.linker.net/trading-api \
REPORT_WRITE_CALLBACK_SECRET=<secret> \
JOB_TOKEN=<token> INSTANCE_ID=123 \
node test_tool.mjs联调档目前只自动验 get_skeleton;fill_page 会真实改数据,需确认目标实例可写后手动补 POST 用例。
QA 联调还需覆盖(阶段 6-8):A 用户 token 打 B 用户实例应 403、幂等、XSS 用例。
技术细节
- 传输:stdio(Hermes 以子进程方式拉起)
- 认证:
X-API-Key建立通道 +job_token授权(后者才是身份) - 回调路径:
weekly→/api/v1/weekly-reports,daily→/api/v1/daily-reports - 日志:只写 stderr(stdout 归 MCP 协议所有)
- Node >= 18(用内建
fetch)
