Skip to content

个人 MCP:管理员指南

谁该看这一篇

  • IT / 部署管理员:你负责 Sira 的 sandrpod 集成、license 配置、合规边界
  • 员工:去看 个人 MCP:员工指南

这是什么

让员工在自己电脑上通过 ~/.sandrpod/mcp.json 自助安装 MCP server,Sira AI 在任一渠道(企业微信 / 飞书 / Web)找到该员工时都能调用这些工具——凭据永不进 Sira 服务器

设计意图:把"WorkBuddy 式员工桌面 AI"的差异化体验补齐,员工无需 IT 审批就能让 AI 用上自己的 GitHub / Notion / Jira 账号。

详细架构见 devdocs/SIRA_PERSONAL_MCP_INTEGRATION.md

架构总览

┌────────────── 员工 PC ──────────────┐
│                                      │
│  ~/.sandrpod/mcp.json                │
│   (Claude Desktop 兼容格式 + 个人凭据) │
│         │                            │
│         ▼                            │
│  sandrpod-agent (LaunchAgent)        │
│  ├ fsnotify 监听 mcp.json            │
│  ├ 拉起 N 个 stdio MCP 子进程        │
│  └ 聚合成单一 HTTP MCP endpoint      │
│         │                            │
│         ▼  yamux reverse tunnel      │
└──────────────────────────────────────┘


┌──────── sandrpod API Server ────────┐
│  反向隧道转发 + corp 鉴权             │
│  /api/v1/sandboxes/{name}/mcp/*      │
└──────────────────────────────────────┘

              ▼  HTTPS
┌──────── Sira Backend ───────────────┐
│  心跳 (30s) 拉 manifest 写 mcp_tools │
│  Orchestrator 加载工具时 union 进来  │
│  「我的 MCP」UI 展示员工自己的清单    │
└──────────────────────────────────────┘

启用前置条件

1. sandrpod 部署

需要 sandrpod ≥ 含完整 personal MCP 链路的版本(5/28 11:11+ 打包):

  • pkg/mcpbridge 包齐全(commit 4d3d2c6 起)
  • cmd/agent 支持 --mcp-token Bearer(commit 4d3d2c6
  • cmd/serverauthMiddleware X-Sandrpod-Token 修复(commit 5bbeaf5,见 MCP_AUTH_HEADER_CONFLICT_FIX.md
  • cmd/agent + pkg/mcpbridge 容忍 mcp.json 启动时不存在 + rm 软停 + 父目录 watch(commit 7794c8b,体验关键修复)

检查员工 agent 版本

让员工跑 ~/.sira/bin/sandrpod-agent -version,或直接看日志里有没有 no servers loaded yet (hot_reload=true will pick up a later mcp.json create) 字样。出现这行 = 含 7794c8b;出现旧的 MCP bridge: no config at ... bridge disabled = 老版本,让员工重装 installer。

校验 sandrpod 服务端版本:

bash
curl -sS http://your-sandrpod:18089/health
# 期望 {"mode":"control-plane+tunnel", ...}

2. Sira 数据库 migration

确保已应用:

Migration作用
055_mcp_tools_personal_fields.pymcp_tools 表加 provenance / sandbox_id / owner_user_id / last_manifest_at / manifest_state + 索引
056_sandboxes_mcp_token.pysandboxes 表加 mcp_token_encrypted
bash
docker exec wxwork-backend bash -c '
  for f in scripts/migrations/055_*.py scripts/migrations/056_*.py; do
    python "$f"
  done
'

幂等。重跑无副作用。

3. License v2 启用 mcp.personal

mcp.personal 默认 minimum_tier=STANDARD(付费 org 自动享有)。

生产部署:通过正常 license 文件签发(license-server/ 那套)让目标 org 的 feature_set 包含 mcp.personal

dev 环境 临时启用:

python
# 在 backend 容器里执行:
docker exec wxwork-backend python -c "
import json, uuid
from datetime import datetime, timezone, timedelta
from app.database import SessionLocal
from sqlalchemy import text
from app.services.license_v2.features import FEATURES

with SessionLocal() as db:
    db.execute(text('DELETE FROM license_v2_org_grants WHERE organization_id = :o'),
               {'o': 'org_default'})
    db.execute(text('''
        INSERT INTO license_v2_org_grants
            (id, organization_id, license_id, license_type, feature_set,
             limits, deployment_fingerprint, valid_until, created_at, updated_at)
        VALUES (:id, :org, :lic, :ltype, CAST(:fs AS jsonb),
                CAST(:lim AS jsonb), :fp, :vu, NOW(), NOW())
    '''), {
        'id': str(uuid.uuid4()), 'org': 'org_default',
        'lic': 'dev-local', 'ltype': 'enterprise',
        'fs': json.dumps(sorted(FEATURES.keys())),
        'lim': json.dumps({}),
        'fp': 'dev-local-fingerprint',
        'vu': datetime.now(timezone.utc) + timedelta(days=3650),
    })
    db.commit()
"

或者 dev 完全关 license:

yaml
# docker-compose.dev.yml
backend:
  environment:
    LICENSE_V2_MODE: off    # 默认 enforce, dev 临时关

4. Sandrpod corp 配置

/agent-system/sandboxes/config 页面(管理员) 填:

  • API URL:sandrpod API Server 公网地址(员工 PC 拨号到这里)
  • Agent-facing URL:员工 PC 可达的 URL(可与 API URL 同,DMZ 部署时不同)
  • API Token:sandrpod 启动时 -token 设的 corp 级 token
sql
-- 直接看 DB 配置:
SELECT organization_id, api_url, agent_facing_url, enabled
FROM corp_sandrpod_config;

DB schema 速查

mcp_tools (provenance='personal' 新增字段)

字段类型含义
provenanceVARCHAR(16) NOT NULL DEFAULT 'corp''corp''personal'
sandbox_idVARCHAR(36) NULLprovenance='personal' 时必填,指向 sandboxes.id
owner_user_idVARCHAR(50) NULL冗余存一份方便按 user 索引
last_manifest_atTIMESTAMP NULL上次 sandrpod /mcp/manifest 拉到数据时间
manifest_stateVARCHAR(16) NULL'online' / 'partial' / 'offline' / 'no_config'

source_config JSON 内嵌 manifest 全文(schema 与 sandrpod /mcp/manifest 一致)。

sandboxes.mcp_token_encrypted (TEXT NULL)

Fernet 加密的 sandrpod --mcp-token Bearer。每 sandbox 独立

License v2

字段
feature keymcp.personal
minimum_tierSTANDARD
注册位置app/services/license_v2/features.py

索引

sql
ix_mcp_tools_personal   ON mcp_tools(owner_user_id, sandbox_id)
                        WHERE provenance = 'personal'   -- PG 部分索引
ix_mcp_tools_provenance ON mcp_tools(organization_id, provenance)

监控指标 / 日志

当前现状

项目没有暴露 Prometheus /metrics scrape endpoint(依赖里有 prometheus-client,但未 wire 到 HTTP)。Sprint 5 阶段用结构化日志 + 内部 metrics_collector 双轨。生产真要建看板,按下述字段写 ELK / Loki / SigNoz query。

关键 logger 名

app.extensions.agent_system.services.sandbox_heartbeat_service
app.extensions.agent_system.services.sandrpod_client
app.extensions.agent_system.services.personal_mcp_loader
app.extensions.agent_system.api.sandboxes  (refresh endpoint)

关键日志事件

日志 grep含义看板建议
[heartbeat] %d corp / %d sandboxes synced心跳一次的全局统计时序:每 30s 一条;丢失 = 心跳挂了
[heartbeat] personal MCP 新建/更新upsert 成功计数:personal_mcp_upsert_total
[heartbeat] sandbox=%s manifest fetch 失败单 sandbox 拉 manifest 失败按 sandbox / org 分布
[heartbeat] corp=%s license mcp.personal not allowed, skipLicense gate 关显示哪些 org 没买
[personal_mcp] sandbox=%s 加载工具失败session 启动时拉 personal MCP 失败影响真实 AI 调用
mcp.bridge:<tool_name> (caller 字段, sandbox_decision_events 表)personal MCP 工具被调用调用量看板(详见审计章节)

用现有 metrics_collector(可选)

app/core/metrics/metrics_collector.py 提供进程内 deque 储存。Sprint 5 不强制接入,需要时按 metrics.store_counter("personal_mcp_refresh_total", labels={"result": "ok"}) 调用。当前 /metrics HTTP endpoint 不存在;要 scrape 需先 wire prometheus_client.make_asgi_app() 到 FastAPI。

故障排查(按现象)

UI 整体「在线」但 server 列表空

根因:refresh API live fetch 偶发失败,前后端都有回退到 DB cache,但极端情况下还是会出现。

查 DB 看是否有缓存

sql
SELECT name, manifest_state, last_manifest_at,
       source_config->'manifest'->'total_tools' AS total_tools,
       jsonb_array_length(source_config->'manifest'->'servers') AS server_count
FROM mcp_tools
WHERE provenance = 'personal' AND owner_user_id = '<user_id>';

server_count > 0 说明 DB 缓存还在,前端拿到完整数据;server_count = 0 说明心跳从未成功过。

sandbox status=online 但 manifest_state=offline

根因:tunnel 通了(sandrpod 看到 sandbox online),但 /mcp/manifest 调用失败。

典型原因

  1. MCP child 全 failed:mcp.json 里所有 server 都启不起来(npx 找不到 / 凭据缺失 / startup_timeout_sec 太短)。查员工电脑上 ~/.sira/log/sandbox.err 看具体错误,或拉 /admin/manifestlast_error 字段
  2. mcp_token mismatch:员工电脑上 keychain 里的 SANDRPOD_MCP_TOKEN 与 DB 里的不一致(重新 enroll 但 keychain 没更新)。让员工重装
  3. 老版 sandrpod-agent(< 5/28 fix 7794c8b)+ mcp.json 启动时不存在 → bridge disabled让员工重装 installer 把 agent 升上来,新版自动接住后续 mcp.json 创建,无需再重启

手动复现 manifest fetch

bash
# 拿 corp api_token + mcp_token
docker exec wxwork-backend python -c "
import asyncio
from app.extensions.agent_system.services.sandbox_registry import sandbox_registry
async def m():
    cfg = await sandbox_registry.get_corp_config('<org_id>')
    tok = await sandbox_registry.get_mcp_token('<sandbox_id>')
    print('CORP=' + cfg.api_token)
    print('MCP=' + tok)
    print('URL=' + cfg.api_url)
asyncio.run(m())
"

# 直接 curl
curl -sS -H "X-Sandrpod-Token: <CORP>" -H "Authorization: Bearer <MCP>" \
  "<URL>/api/v1/sandboxes/<sandbox_name>/mcp/manifest" | jq

transport closed 频繁出现

根因:员工 PC 上 MCP child 进程不稳定,启动后立刻退出。

最常见原因:

报错解释修复
exec: "npx": file not foundLaunchAgent PATH 缺 Homebrewinstaller 模板已修,重装即可
MCP server 启动需要凭据但缺失env 块没写对让员工补
npm 包还没缓存,下载慢冷启动 30+ 秒sandrpod.startup_timeout_sec: 120

心跳完全没跑

bash
docker exec wxwork-backend python -c "
from app.extensions.agent_system.services.sandbox_heartbeat_service import get_global_heartbeat_service
svc = get_global_heartbeat_service()
print('running:', svc._task is not None and not svc._task.done())
print('interval:', svc.interval)
"

如果 running=False,看 backend 启动日志找 SandboxHeartbeatService 启动错误。

License v2 拒了

bash
docker exec wxwork-backend python -c "
from app.services.license_v2 import get_gate
d = get_gate().check('mcp.personal', organization_id='<org_id>')
print(f'allowed={d.allowed} source={d.source} reason={d.reason}')
"

source='deny' + reason='feature not in v2 grant feature_set' = org 没买这个 feature。生产环境联系销售签发新 license;dev 环境按上面 §3 插 grant。

凭据轮换

单 sandbox 的 mcp_token

python
docker exec wxwork-backend python -c "
import asyncio
from app.extensions.agent_system.services.sandbox_registry import sandbox_registry
async def m():
    new_token = await sandbox_registry.rotate_mcp_token('<sandbox_id>')
    print('new mcp_token:', new_token)
asyncio.run(m())
"

老 token 立即失效

DB 旧值被覆盖。员工 PC 上 sandrpod-agent 还在用旧 token,Sira 后端调用立即 401。员工需要:

  1. 在 tray 里手动把新 token 复制进 keychain(如果 tray 支持),或
  2. 重新跑一遍 installer,新 mcp_token 通过 enrollment JWT 下发到 keychain

sandrpod 当前没有 server-push token 更新的通道,未来如果加,rotate 才能做到无感。

Corp api_token 轮换

/agent-system/sandboxes/config 页面填新 token,DB 自动 Fernet 加密。sandrpod_client 会复用同实例 client 直到 cache 失效(5 分钟),新调用自动用新值。

审计:查 personal MCP 调用流

personal MCP 调用走的是 sandrpod 决策审计管道,存在 sandbox_decision_events 表,与 platform action audit 是两条独立流

sql
SELECT timestamp, decision, source, path, caller, reason
FROM sandbox_decision_events
WHERE source IN ('mcp.spawn', 'mcp.call', 'mcp.restart')
  AND organization_id = '<org_id>'
  AND timestamp > NOW() - INTERVAL '24 hours'
ORDER BY timestamp DESC
LIMIT 100;

字段约定:

sourcepathcaller含义
mcp.spawnmcp:<server_name>mcp.bridge子进程被启动
mcp.callmcp:<server_name>mcp.bridge:<tool_name>AI 调用了某工具
mcp.restartmcp:<server_name>mcp.bridgechild 崩了自动重启

不记录的

绝不记录 args / result / env 值(sandrpod 设计意图)。审计只回答"何时何工具被调用,成功/失败",不回答"调了什么"。

UI 视图:/agent-system/sandboxes → 「AI 决策审计」tab(管理员可见),按 source LIKE 'mcp.%' 过滤。

合规边界

数据留在哪Sira 后端可见IT 管理员可见
mcp.json 内容员工 PC
mcp.json env 块凭据 (token / API key)员工 PC 进程 env
mcp_token (sandrpod /mcp 端点 Bearer)员工 keychain + Sira DB (Fernet)✅ 加密⚠ 可解密但不该看
manifest 摘要(server 名 / 状态 / 工具数)mcp_tools.source_config
工具调用事实sandbox_decision_events
工具调用参数 / 结果任何地方都不存

法务沟通点:sandrpod 的 mcp bridge 设计上不让任何工具参数 / 返回值离开员工 PC(只走 stdio child → 隧道 → orchestrator runtime → 立即 LLM 决策),落盘的仅 audit 元数据。

参考

Apache-2.0 Licensed