Skip to main content
Glama
yiongq

mcp-foundry

by yiongq

mcp-foundry(工作名)

Guarded MCP gateway for internal business systems — role-scoped tools, row/field-level data guards, human-in-the-loop approvals, and a per-call audit trail.

CI License: MIT Live demo

TL;DR — Put internal systems (HR, CRM) behind an MCP server with a security guard done at the infrastructure layer, independent of any agent framework. Same guard for every entrypoint:

  • Role → tool whitelist — unauthorized tools never appear in tools/list (not "denied after the call").

  • Row filter + field mask — structured rules enforced at the guard's exit, not by prompting; the refusal branch has no data field at the type level, so it structurally cannot smuggle business data.

  • Human-in-the-loop approval — high-risk writes are gated before execute; graded policy-as-code (refund: small auto-passes, > ¥500 or ≥ 2/month → approval + identity check + ownership constraint).

  • Tamper-evident audit — optional Ed25519 signed hash chain; edit one line and verify-audit fails, offline-checkable with just the public key. Logs param hashes, never raw PII.

  • Headline metric — red-team attack-success rate 77.2% (no guard) → 0% (all guards on), quantified by a guard-layer ablation and reproducible with one command (sample report).

Live demo (no signup): crm.yiongspace.com — a self-built LangGraph streaming UI on the production gateway (https://foundry.yiongspace.com/mcp, ES256 tokens), backed by a real Twenty CRM. · Threat model (controls mapped to OWASP Agentic/LLM Top 10 + TC260, with an honest not-covered list): THREAT_MODEL.md · Security policy: SECURITY.md.

flowchart TD
  MCP["MCP client<br/>Claude Code / any Streamable HTTP host"]
  LG["LangGraph client<br/>Python + FastAPI, streaming UI"]
  FS["Feishu bot<br/>long-poll, no public callback"]
  RBAC["config/rbac.*.yaml<br/>roles→tools · approval list · row rules · masked fields"]
  G["packages/core — Guard pipeline (single choke point)<br/>role lookup → tool whitelist → <b>approval gate (pre-write)</b> → row filter → field mask → audit"]
  A["SystemAdapter (hot-swappable, same tools & guard)<br/>orangehrm (HR) · twenty (CRM: fixture/REST) · memory"]
  AUD["audit JSONL + approvals JSONL<br/>Ed25519 signed hash chain (optional)"]
  MCP --> G
  LG --> G
  FS --> G
  RBAC -. config .-> G
  G --> A
  G --> AUD

下面是中文深度稿:架构决策、威胁模型逐条映射、诚实边界。English readers — the TL;DR above plus THREAT_MODEL.md and docs/eval-methodology.md carry the depth.


把内部系统(先做 HR,现已加上 CRM 演示域)接到 MCP 上,并在协议层前面加一道守卫: 谁、能用哪些工具、能看到哪些行哪些字段,高危写操作先过人的点头,问过什么都有账可查。

线上可体验(免注册):CRM 助手 crm.yiongspace.com—— 自建 LangGraph 流式聊天前端,连生产网关https://foundry.yiongspace.com/mcp,ES256 令牌), 后端是真实的 Twenty CRM。用一句话查改 CRM,高危写走人工审批, 全程按身份收窄工具面、行级过滤、字段脱敏、留审计。

为什么做:三个事故 → 三个能力

给 Agent 接内部系统,出的事基本是三类:普通员工顺嘴一问就拿到了同事的工资(没有行级边界); 模型"好心"把带手机号、薪资的整行数据吐进回答(没有字段脱敏);出事之后翻不出谁在什么时候问过什么 (没有审计)。对应地,Foundry 只做三件事:角色 → 工具白名单——不该用的工具连 tools/list 都不出现,而不是调用被拒之后再解释;行级过滤 + 字段脱敏——结构化规则在守卫出口强制执行, 不靠提示词自觉;每次调用一条审计流水——只记参数哈希不记原文,审计日志本身不成为第二个泄漏面, 生产可选开启 Ed25519 签名哈希链(防篡改:改一条断整链、伪造验签不过,校验只需公钥、可离线复核)。 拒答是结构化分支,结构上不可能夹带业务数据。

CRM 域开放写工具后补上第四道:高危写操作人工审批(Human-in-the-Loop)—— 涉审工具的调用在执行前被闸门挂起成审批单(kind: pending,与拒答同款结构化分支、 无 data 字段),审批人在飞书卡片一键批复或走 POST /approvals/{id};批准后同一 调用重试即消票执行(一单一次,防重放),飞书入口还会自动续跑并把结果推回原对话。 闸门在守卫管线内、adapter.execute 之前——行级过滤拦得住泄漏但拦不住已发生的写入, 审批必须在写入发生前。哪些工具涉审由 rbac.*.yamlapproval: 列表按角色声明 (销售代表改/删要审、新建免审、经理免审),审批权由 approver: true 标记。 闸门还支持分级放行(policy-as-code):条目可带 when 条件,按调用参数与适配器 算出的信号分档。退款 issue_refund 即分级——小额自动放行,金额 > ¥500 或本月已退 ≥ 2 笔才挂起等批;并叠加身份核验(订单号 + 邮箱 + 卡尾号对上才受理)与归属约束 (只退自己名下订单,越权退款在审批前就 row_denied)。这一档对应「动钱」类高危动作 的现代口径(EU AI Act Art.14 人工监督、中国 TC260 资金动作二次确认)。

每道控制到 OWASP Agentic Top 10 / OWASP LLM Top 10 / 中国 TC260 的逐条映射、 设计原则与诚实的未覆盖清单THREAT_MODEL.md

Related MCP server: mcp-guardrail-gateway

架构

入口层(多入口,身份都透传给同一套守卫——守卫做在基础设施层,与 agent 框架无关)
  ├─ MCP 客户端(Claude Code / 任意 Streamable HTTP host)
  │    POST /mcp,身份:Bearer 签名令牌(生产)或 x-foundry-* 头(demo)
  │    → packages/server(@yiong/foundry-server,无状态)
  ├─ LangGraph 客户端(clients/langgraph,Python + FastAPI,自建流式前端)
  │    langchain-mcp-adapters 连同一条 /mcp;对话内审批卡片、批准后自动续跑
  └─ 飞书 bot(packages/feishu,长连接,无需公网回调)
       sender open_id → config/identity.feishu.yaml 映射 → principal
       → GLM tool-calling 循环(自然语言进、按身份收窄的工具面出)
        ▼
┌────────────────────────────────────────────┐     config/rbac.*.yaml
│ packages/core  Guard 管线                   │◀────(角色→工具 / 审批名单 / 行规则 / 脱敏字段)
│ 角色查找 → 工具白名单 → 审批闸门(写前挂起)   │
│ → 行级过滤 → 字段脱敏                        │
│ → 审计 JSONL + 审批单 JSONL(audit/)        │
└──────────────────┬─────────────────────────┘
                   ▼
┌────────────────────────────────────────────┐
│ SystemAdapter:orangehrm(HR)              │    ERP …(路线图,今天没有)
│   fixture(内置演示数据 · 6 人)             │
│              :CRM(后端可热切,同工具同守卫)│
│              :twenty(CRM)                 │
│   fixture / REST(真实 Twenty,Bearer API key)│
│              :memory(跨会话记忆,域内 RBAC)│
└────────────────────────────────────────────┘

飞书入口的关键:安全不靠提示词。工具面在 LLM 看到之前就按身份收窄(员工的工具 定义里根本没有 get_salary),越权数据在守卫出口就不存在(refusal 分支无 data 字段), 模型想编也没材料。同一个人在飞书里换身份问同一句「张三的工资是多少」——员工被结构化 拒答、HR 返回真实数字,全程留审计。搭建见 packages/feishu

配套:packages/eval 是越权/泄漏评测——100 题跨三个域(HR 29 + 记忆 20 + CRM 51, 含跨会话记忆泄露、跨域越权、对抗红队题(被提示注入劫持后的 agent 试图伪造归属新建、 越权改删同事的行、导出他人数据),以及退款分级题:小额自动 / 超额或频繁退款挂起等批 / 凭据不符即拒)、硬断言 + LLM-judge 双轨(judge 须先过人工校准)、 3 次重复报方差、守卫逐层开/关 ablation 曲线。头条指标:红队攻击得手率 无守卫 77.2%(47 题 泄漏)→ 生产配置 0%(0 题泄漏)——越权数据在守卫出口不存在,靠执行层拦截而非提示词。 CI 的 eval-gate 在 fixture 上跑并上传报告;方法与实测数据见 docs/eval-methodology.md

Quickstart(fixture 模式,无需密钥;演示头模式需显式开启)

pnpm install
# 未签名的 demo 头模式必须显式开启:安全产品默认 fail-closed——不配签名身份、也不设
# FOUNDRY_DEMO=1 时服务器拒绝启动,而不是偷偷跑成「x-foundry-user 自证任意身份」的形态。
FOUNDRY_DEMO=1 pnpm --filter @yiong/foundry-server serve   # → http://localhost:3100/mcp

换不同身份问同一个问题,行为不同(这正是卖点):

# 小王(employee):tools/list 里根本看不到 get_salary
curl -s http://localhost:3100/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'x-foundry-user: e002' -H 'x-foundry-role: employee' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

# 小王查别人的假期 → 结构化拒答 row_denied(不是模型忍住不说,是数据没进上下文)
curl -s http://localhost:3100/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'x-foundry-user: e002' -H 'x-foundry-role: employee' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_leave_balance","arguments":{"employee_id":"e001"}}}'

# 老李(hr_admin)查薪资 → 正常返回
curl -s http://localhost:3100/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'x-foundry-user: e004' -H 'x-foundry-role: hr_admin' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_salary","arguments":{"employee_id":"e001"}}}'

# 上面每一次调用都留了一条审计
tail -3 audit/foundry.jsonl

同一个网关还挂着第二个业务域(CRM,Twenty 适配器)。角色按域独立: 销售看不到全员业绩盘,HR 的通配权力跨不进 CRM——

# 林哲(sales_rep)想看全员业绩盘 → 白名单拒绝(这工具在他的 tools/list 里根本不出现)
curl -s http://localhost:3100/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'x-foundry-user: e007' -H 'x-foundry-role: sales_rep' \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_pipeline_summary","arguments":{}}}'

# 何静(sales_manager)看业绩盘 → 正常返回
curl -s http://localhost:3100/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'x-foundry-user: e005' -H 'x-foundry-role: sales_manager' \
  -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"get_pipeline_summary","arguments":{}}}'

# 老李(hr_admin,HR 域通配角色)摸 CRM 工具 → unknown_role:业务全权 ≠ 跨域全权
curl -s http://localhost:3100/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'x-foundry-user: e004' -H 'x-foundry-role: hr_admin' \
  -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"list_accounts","arguments":{}}}'

签名身份(生产模式,W3 单网关 / W5 多签发方)

demo 头谁都能自称 hr_admin。设 FOUNDRY_IDENTITY_SECRET(≥16 字符)后服务器只认 HS256 签名令牌,x-foundry-* 头被完全忽略——伪造头不是"被拒绝",是根本不在信任路径上:

export FOUNDRY_IDENTITY_SECRET='replace-with-a-16+char-secret'
pnpm --filter @yiong/foundry-server serve

# 签发令牌(演示里这条 CLI 扮演"网关"这个身份权威;生产由网关/SSO 持同一密钥签发)
pnpm --filter @yiong/foundry-server run mint-token --user e004 --role hr_admin --ttl 3600

# 用令牌访问(mint-token 会打印完整 curl 示例)
curl -s http://localhost:3100/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'authorization: Bearer <token>' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

改令牌任何一个字节(比如把 role 从 employee 改成 hr_admin)→ 签名作废 → 401; 过期 → 401;验签不读令牌自述的 alg,"alg: none" 一类降级攻击在结构上不成立。 飞书入口不走这条:sender open_id 由飞书服务端签发,本身就是可信身份源。

共享密钥要求签发方与验签方同持一把钥匙,适合单网关。多签发方 / 密钥轮换走 非对称模式(W5):签发方持私钥,Foundry 只见公钥集(JWKS,ES256/RS256)——

# 签发方生成密钥对(私钥 0600 落盘,公钥 JWKS 写 jwks.json)
pnpm --filter @yiong/foundry-server run keygen --kid 2026-07

# 验签方只拿公钥集;设了 FOUNDRY_IDENTITY_JWKS 即切非对称模式(HS256 令牌不再有验签路径)
FOUNDRY_IDENTITY_JWKS=jwks.json pnpm --filter @yiong/foundry-server serve

# 签发走私钥(生产由网关/SSO 持有;Foundry 永远见不到私钥)
pnpm --filter @yiong/foundry-server run mint-token \
  --key identity-es256.key --kid 2026-07 --user e004 --role hr_admin

密钥轮换是运行时动作:新 kid 的 JWK 追加进 jwks.json(mtime 热重载,无需重启), 新旧键并存撑过渡窗口,删旧 JWK 即吊销。算法选择的纪律与 HS256 路径同一条—— 用哪种算法由被 kid 命中的受信密钥自己的 kty 决定(EC→ES256 / RSA→RS256), 令牌头自述的 alg 从头到尾没人读;kid 只是受信集合内的查找提示,未命中与坏签名 同一个 401,不给探测者区分反馈。拿公钥当 HMAC 密钥的经典混淆攻击在这里没有 攻击面:JWKS 模式下不存在 HMAC 验签路径。

公钥集也可以直接指向签发方的 JWKS 端点(W6,签发方轮换后无需再往 Foundry 拷文件):

# 值以 http(s):// 开头即走 URL 模式;启动拉一次,拉不到拒绝启动
FOUNDRY_IDENTITY_JWKS=https://sso.example.com/.well-known/jwks.json \
FOUNDRY_IDENTITY_JWKS_REFRESH=300 \
pnpm --filter @yiong/foundry-server serve

之后按 FOUNDRY_IDENTITY_JWKS_REFRESH(秒,默认 300)固定轮询刷新——带 ETag/If-None-Match,忽略 Cache-Control(这是刷新间隔,不是 HTTP 缓存 TTL); 刷新失败沿用上一份有效集合并打含陈旧时长的警告。信任锚从网络来,运输纪律相应 收紧:只走 https(明文 http 仅放行 127.0.0.1/[::1] 本地调试,localhost 是名字 不是地址、不放行);30x 重定向按失败处理(scheme 检查只管第一跳,所以不跟); 响应限长 1 MiB(按实际读到的字节数数,不信任 Content-Length)、限时 10s;每次 密钥集变更打 kid 差集日志(只记 kid,不记密钥材料)。轮换纪律:新 kid 先发布到 端点、等 ≥2 个刷新间隔(容一次失败轮询)再用它签发;删 kid 即吊销,最迟一个 刷新间隔生效。默认可用性优先——端点故障期间沿用旧集合、时延无上界;要 fail-closed 语义设 FOUNDRY_IDENTITY_JWKS_MAX_STALE(秒,须 ≥ 2×刷新间隔 + 10s 拉取超时——稳态 陈旧度会合法冲到「间隔+在途时延」,还要容一次失败轮询,更紧的截止对健康端点也会误伤): 超过该时长没刷新成功,验签一律 401(304 也算刷新成功)。

签名令牌让任意 MCP 客户端零改造接入网关。线上跑的是自建的 LangGraph 客户端 (Python,langchain-mcp-adapters 直连网关 MCP、FastAPI + SSE 流式前端)—— 同一个网关地址、只有令牌不同,sales_rep 与 sales_manager 的工具面/行数/脱敏在 同一个对话前端里肉眼可见地不同,越权即 row_denied 并如实转述「已记入审计」。 客户端源码见 clients/langgraph/,上线清单见 DEPLOY-CHECKLIST.md

诚实边界

  • CRM 域已上公网连真实 Twenty:线上网关连自托管 Twenty 真库,crm.yiongspace.com 的对话 前端读写的是真实 Twenty 数据。HR 域曾对真实 OrangeHRM 5.7(只读 MySQL、SELECT-only、本地 docker)验证过,该真库后端现已移除——生产与评测一律 fixture。「已验证」是演示数据口径、 不是生产规模。

  • 演示与评测数据都是演示口径(HR 6 人;CRM 5 客户 / 5 联系人 / 5 商机 / 3 个销售身份), 非生产数字。

  • 角色不跨域:CRM 域的角色是 sales_rep/sales_manager,HR 域是 employee/hr_admin, 两套身份各自登记在各自域的 RBAC 里,通配权力跨不进对方域。

  • HTTP 入口两种生产验签:共享密钥 HS256(W3,单网关)与非对称 JWKS(W5 本地文件 / W6 URL 拉取,ES256/RS256,多签发方 + 热轮换)。线上实例现跑 JWKS URL 模式 (W7,2026-07-17 切换):Foundry 从 https://foundry.yiongspace.com/jwks.json 拉公钥集,签发私钥只在管理员本机、服务器永不持有;两个体验链接的令牌已换成 ES256(kid 2026-07-vps,TTL 一年、2027-07 到期),旧 HS256 令牌即时作废。 边界:URL 模式默认不设 max-stale——端点持续故障(宕机或被打瘫)期间吊销不生效、 时延无上界,只有含陈旧时长的告警日志,要 fail-closed 得自己开 FOUNDRY_IDENTITY_JWKS_MAX_STALE;启动时拉不到端点直接起不来(撞上端点故障的 重启=停机,重试靠容器编排的 restart 策略,进程内不重试)。这里 JWKS 端点由 Foundry 自己的 Caddy 静态供给(签发方=部署方同机),不是独立 SSO/IdP—— 「多签发方」是能力,线上是单签发方口径。默认 fail-closed:既不设签名身份、又不显式设 FOUNDRY_DEMO=1拒绝启动;demo 头模式(启动打警告)须显式开启,仅供本地演示与评测。

  • 飞书链路三个域都已在真实飞书应用上端到端验证(HR + 记忆 2026-07-16;CRM 2026-07-17,网页端实测:sales_rep 查自己的单出真数、联系人电话脱敏而邮箱可见、 越权查同事商机被 row_denied 并如实转述「已记入审计」、跨域问工资零泄漏; hr_admin 反向无任何 CRM 能力;审计逐条留痕)。飞书 bot 默认走 fixture 口径; 机器人在飞书里的显示名/简介来自开发者后台的应用配置,与代码无关。

  • 行级/字段规则只在 Guard 出口生效:绕过 Foundry 直连源系统它管不到——部署上要求源系统凭据只发给 Foundry。

  • 线上演示的身份是"配好的",不是"登录来的":CRM 前端持有服务端注入的两枚预置 长效令牌(销售 · 林哲 / 只读经理 · 何静),访客可用侧栏段控在二者间切换、肉眼对比 「同一网关、不同身份、行为不同」(工具面收窄、行级过滤、字段脱敏当场变化)——但只能在 这两枚预置身份间切,不是一套面向访客的登录系统。生产接法是网关/SSO 在用户登录后 按其真实身份签发令牌。经理身份特意用只读角色 sales_manager_ro(无写工具、非 approver),因此把它放到公开页也安全:访客切成经理也改不了数据、批不了单, 不违背「发起人无法自批」与 DEPLOY-CHECKLIST 的「审批人令牌不放前端」。

  • 线上 CRM 域连的是真实 Twenty(REST + Bearer),HR 域一律 fixture——早期对真实 OrangeHRM 5.7 的只读 MySQL 链路本地验证过(SELECT-only 账号),该后端现已移除。

  • 演示令牌 TTL 一年(2027-07 到期),现为 ES256(JWKS 模式)。过期后演示会静默失效—— 届时用 mint-token 拿本机私钥重签、更新 LangGraph 客户端 .envFOUNDRY_MCP_TOKEN (步骤见 DEPLOY-CHECKLIST.md 的「切换到 JWKS / 轮换演示令牌」)。

名称说明

mcp-foundry 是工作名(working title),正式名称待定;@yiong/foundry-* 包名前缀会随定名一起迁移。

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

0Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    A secure MCP gateway for enterprise AI tool execution, enabling governed invocation of business tools with authentication, RBAC, audit logging, PII redaction, and async processing.
    Apache 2.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    A security gateway for MCP servers that enforces policy checks including role-based access, argument constraints, injection scanning, and PII redaction on both tool arguments and results, with tamper-evident audit logging.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    MCP gateway adding per-tool RBAC, tenant isolation, audit export, and PII redaction to any server.
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    A governed MCP server with OAuth 2.1 + PKCE, declarative tool scoping, row-level data filters, per-identity rate limits, and a tamper-evident audit trail.

View all related MCP servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yiongq/mcp-foundry'

If you have feedback or need assistance with the MCP directory API, please join our Discord server