业务故事站
P2 Foundry

MCP 接入与安全标记:把本体安全地开放给 LLM 生态

V5 让 Foundry 长出"对外开放"的接口:MCP Server 把语义检索 / 对象查询 / 指标 / 写路径 Action / 数据集 5 项能力以工具形式暴露给 Claude 等 LLM 客户端,全程强制审计;安全标记(Markings)给属性打上"谁可持有、谁可见"的列级闸门;OWL/SHACL 把本体定义以标准语义格式外送。看完这 4 个故事,你就知道怎么开放得出去、又关得住。

平台管理员 数据工程师 开发工程师 MCP 安全标记 OWL/SHACL 共 4 个故事

能 / 不能速览

✅ 这个主题能做
  • MCP Server:JSON-RPC 2.0 双 transport(stdio --user 绑定服务账号 / HTTP 挂 /api/v1/mcp 复用 JWT)
  • 5 个工具:search_objects / query_objects(limit≤200 强制)/ get_metric / execute_action / get_dataset
  • execute_action 走 writepath 五步流水线不可绕过;越权 403 红线;每次 tools/call 落审计(MCP_TOOL_CALL)
  • Markings 三层模型驱动 CLS 第二道列级闸门(任一来源隐藏即隐藏,NULL AS col)
  • OWL/TTL 整库导出 + 单对象 SHACL 校验形状导出(enum→sh:in、pattern→sh:pattern)
⛔ 这个主题做不了
  • MCP 默认关闭(foundry.mcp.enabled=false → 路由不注册,访问 404),需显式开启
  • 仅 tools 能力面:无 resources / prompts;tools/list 静态(listChanged=false)
  • HTTP SSE GET 路由当前未注册(Handler 支持,gin 只挂了 POST /mcp)
  • object_type / dataset / metric 的标记绑定仅登记、无消费方(只有 property 绑定驱动列级闸门)
  • OWL/SHACL 单向导出,不做 roundtrip 保证;currency/percentage/enum/json/array 映射 xsd:string 并标注

适用角色

本主题面向三个角色:

  • 平台管理员:开启 MCP、配置服务账号、管理安全标记与用户授权矩阵(admin 语义)。
  • 数据工程师:导出 OWL / SHACL 给外部工具,配合标记验证列级隐藏效果。
  • 开发工程师:把 MCP 客户端接上 Foundry,封装工具调用并消费审计。

运营 / 业务人员作为 MCP 工具的最终使用者(如让 LLM 查数、改订单状态),权限受写路径 RBAC 保护。

能力速览(能做什么)

MCP 双 transport

stdio(子进程,--user 显式绑定服务账号,未绑定拒绝启动)+ HTTP(/api/v1/mcp 复用 JWT,无会话 401)。

5 个 MCP 工具

语义检索、对象查询(limit≤200 红线)、指标查询、写路径 Action、数据集;工具输出经语义翻译,口径与 Web 端一致。

强制审计

每次 tools/call 落 MCP_TOOL_CALL:traceID=mcp_ 前缀、参数脱敏(secret/password/token 等键值替换 ***)、失败也留痕。

Markings 打标

打标定义 → 目标绑定 → 用户授权三层模型;属性绑定后与 ApplyCLS 并集叠加成第二道列级闸门。

OWL / SHACL 导出

整库 OWL/Turtle(Class / DatatypeProperty / ObjectProperty / subClassOf)+ 单对象 SHACL(NodeShape / PropertyShape / sh:in / sh:pattern)。

调整指南(怎么调整)

  • 开 MCP:config.yaml 在 foundry 段下加 mcp.enabled=true(可选 service_user);不改配置则 /api/v1/mcp 404。
  • 接客户端:HTTP 用 JWT Bearer 调 POST /api/v1/mcp;stdio 用 foundry mcp stdio --user <服务账号> 子进程方式。
  • 收紧写权限:给服务账号只授必要的 action:<name>,越权即 403;无会话用户 401。
  • 打标隐藏列:先建对象再绑定 property(存在性校验);dataset / metric 绑定可先行,目标未建也能打标。
  • 外送本体:单对象用 GET /ontology/objects/:id/shacl(或 export?format=shacl),整库用 GET /ontology/export/owl。

做得好的场景

MCP + 标记让"对外开放"这件事同时满足即插即用与安全可控,特别适合以下场景:
  • LLM 零适配接入:Claude Desktop 等 MCP 客户端一次握手(initialize → tools/list → tools/call)即能查数。
  • 写数据有红线:execute_action 走 writepath 五步流水线,RBAC 第 2 步 403,模型发起的写操作与 Web 操作地位等价、全程留痕。
  • 敏感列按人可见:PII 打标绑定属性后,未授权用户查询自动 NULL,不用逐列配 CLS 策略。
  • 本体语义外送:OWL/SHACL 让外部推理器 / 校验工具直接消费对象、属性、链接与约束。

限制与不足

以下是明确的边界,使用前先知道:
  • MCP 默认关闭:enabled=false 时路由不注册,访问 /api/v1/mcp 是 gin 404 而非 JSON-RPC 错误。
  • 能力面窄:只有 tools,无 resources / prompts;工具集运行时不可变(listChanged=false)。
  • SSE GET 未注册:Handler 支持 SSE 但 gin 路由层只挂了 POST /mcp,HTTP+SSE 客户端需自行补挂 GET。
  • 标记消费面窄:只有 property 绑定驱动列级隐藏;object_type / dataset / metric 绑定仅登记。
  • 指标聚合不叠加编辑态:get_metric 按源表口径计算,与行级查询可能不一致(已知语义债)。
  • OWL/SHACL 单向导出:不做 roundtrip 保证;部分类型映射 xsd:string 并以 foundry:dataType 标注。

场景故事

故事 1 启用 MCP:从默认 404 到一次握手拿到 5 个工具
背景
张工想把 Foundry 接进 Claude 客户端,让模型直接查订单数据。MCP Server 默认关闭(foundry.mcp.enabled=false 是红线),他先在 config.yaml 显式开启,重启后走一遍 initialize → tools/list 握手。
传统做法对比
以前给 LLM 暴露查询能力要自研一套工具 API,还要自己写鉴权和审计;MCP 客户端也得专门适配。现在 JSON-RPC 2.0 + 双 transport(stdio 绑服务账号 / HTTP 复用 JWT)是开放标准,Claude 等客户端零适配接入,一次握手即得工具清单。
角色
平台管理员(改配置 / 重启);数据工程师(走握手验证)。
操作步骤
  1. 在 config.yaml 的 foundry 段加 mcp.enabled=true(可带 service_user)
  2. 重启 Foundry(默认端口 18081),确认日志出现"Foundry MCP Server 已启用(HTTP /api/v1/mcp)"
  3. POST /api/v1/mcp 发 initialize 握手
  4. 再发 tools/list 查看 5 个工具
系统响应
握手返回:
{"jsonrpc":"2.0","id":1,"result":{
  "protocolVersion":"2025-03-26",
  "capabilities":{"tools":{"listChanged":false}},
  "serverInfo":{"name":"zy-foundry-mcp","version":"v5-stage5-b7-3"},
  "instructions":"ZY LightFoundry MCP Server:语义检索/对象查询/指标/Action 写路径/数据集。"}}
tools/list 返回 5 个工具(字典序):
{"jsonrpc":"2.0","id":2,"result":{
  "tools":[
    {"name":"execute_action","description":"...","inputSchema":{...}},
    {"name":"get_dataset","description":"...","inputSchema":{...}},
    {"name":"get_metric","description":"...","inputSchema":{...}},
    {"name":"query_objects","description":"...","inputSchema":{...}},
    {"name":"search_objects","description":"...","inputSchema":{...}}]}}
结果洞察
默认关闭是安全红线:enabled=false 时 NewServer 返回 ErrDisabled,/api/v1/mcp 路由不注册、访问自然 404;stdio 子命令在禁用配置下同样拒绝启动。启用后 5 个工具全部就绪,依赖装配缺失(Ontology/Semantic/Search/Metrics/Executor/Datasets 六项)会拒绝启动,防半装配静默降级。
调整建议
HTTP 场景用 JWT Bearer 调 POST /api/v1/mcp;stdio 场景给专用服务账号(--user 显式绑定,防匿名调用);不启用时 actionctl mcp ping 会收到 404,可作脚本判活信号。
动手试一试
登录:admin / admin1。输入内容:先不启用,POST /api/v1/mcp 发 initialize;再在 config.yaml 开启后重启重发。预期结果:未启用返回 404;启用后 initialize 返回 protocolVersion=2025-03-26,tools/list 列出 5 个工具。
限制提示
仅 tools 能力面(无 resources/prompts);HTTP SSE GET 路由当前未注册(gin 只挂 POST /mcp);MCP enabled 场景未加 HTTP 级冒烟测试(包内单测覆盖)。
故事 2 LLM 调工具查数据:limit 被钳制,每次调用都落审计
背景
MCP 接好后,开发小唐让模型执行 tools/call:先 search_objects 按"订单"做语义检索,再 query_objects 拉订单明细(故意传 limit=500 试探钳制),最后 get_metric 看总销售额口径。全程他去审计日志核对工具调用留痕。
传统做法对比
以前模型访问数据要专门写工具代码,鉴权 / 审计 / 口径全靠自己补,模型看到的数据和业务用户在 Web 端看到的还可能不一致。现在 5 个工具全部复用 semantic / metric / dataset 既有服务,工具输出经过语义翻译,自动带 RLS/CLS/属性级安全/编辑态叠加;每次 tools/call 强制落审计。
角色
开发工程师(封装工具调用 + 核对审计);MCP 客户端(发起调用)。
操作步骤
  1. tools/call search_objects,arguments 传 {"query":"订单","limit":5}
  2. tools/call query_objects,arguments 传 {"object_type":"order","fields":["id","status"],"limit":500}
  3. tools/call get_metric,arguments 传 {"metric_name":"total_gmv","dimensions":["region"]}
  4. 到审计日志查 MCP_TOOL_CALL 记录
系统响应
query_objects 的 limit=500 被钳制为 200(红线):
{"jsonrpc":"2.0","id":3,"result":{
  "content":[{"type":"text",
    "text":"{\"columns\":[\"id\",\"status\"],\"rows\":[[\"A1\",\"open\"]],\"total\":1}"}],
  "isError":false}}
审计日志出现:
{"event":"MCP_TOOL_CALL","ref_type":"query_objects",
  "ref_id":"mcp_<uuid>","user_id":"admin",
  "details":{"tool":"query_objects","trace_id":"mcp_<uuid>","arguments":{"object_type":"order","limit":500}},
  "result":"success"}
结果洞察
query_objects 的 limit 默认 100、强制上限 200(超出钳制);search_objects 上限 50;get_metric 返回 columns/rows/metric 元信息。审计每次调用生成 traceID=mcp_ 前缀,参数里含 secret/password/token/api_key 的键值自动脱敏为 ***;失败(含越权 403)同样留痕 result=error。
调整建议
一次拉不完就分页多次查(limit 上限 200);敏感参数让客户端尽量别传明文凭证;模型发起的操作与 Web 端在审计链上地位等价,可回查"哪个服务账号、哪次调用、带了什么参数"。
动手试一试
登录:admin / admin1。输入内容:tools/call query_objects 传 limit=500。预期结果:返回行数 ≤200(钳制生效);审计日志出现 MCP_TOOL_CALL,ref_id 为 mcp_ 前缀 traceID。
限制提示
get_metric 指标聚合按源表口径计算,不叠加编辑态(与行级查询可能不一致,为已知语义债);工具集运行时不可变;一次性拉全量会被协议层钳制。
故事 3 execute_action 越权红线:未授权 403,授权走五步流水线
背景
运营小陈想让 LLM 客户端帮她改订单状态(order_id=2 从 pending 改为 shipped)。execute_action 是 MCP 侧唯一写能力,她先用自己账号调用——没有授权,看看平台怎么拦;再由管理员授权 action:update_order_status 后重试。
传统做法对比
以前模型写数据要么"根本没门"(只能读),要么给裸 SQL / 直连库权限(危险且无法审计)。现在 execute_action 直接调 writepath.Executor(VALIDATE_AND_EXECUTE),五步流水线(参数 Schema → 逐动作 RBAC → 记录级检查 → 属性级写权限 → 审计 + 幂等 + 乐观锁 + 编辑态)全走、不可绕过,越权就是 403。
角色
运营人员(发起调用);平台管理员(授权 action:update_order_status)。
操作步骤
  1. 用未授权账号 tools/call execute_action(action=update_order_status,params 含 status、record_filter 含 order_id)
  2. 观察 403 拒绝与审计留痕
  3. 管理员给账号授予 action:update_order_status 权限
  4. 再次调用,观察写路径返回与订单状态变化
系统响应
未授权返回 JSON-RPC 错误:
{"jsonrpc":"2.0","id":4,"error":{
  "code":-32001,
  "message":"<越权详情>",
  "data":{"http_status":403,"code":"FORBIDDEN","trace_id":"mcp_<uuid>"}}}
授权后成功返回 writepath 结果:
{"jsonrpc":"2.0","id":5,"result":{
  "content":[{"type":"text",
    "text":"{\"mode\":\"VALIDATE_AND_EXECUTE\",\"status\":\"success\",\"validation_result\":{\"result\":\"VALID\"},\"edits\":{\"changed_records\":1},\"rows_affected\":1,\"result\":{\"order_id\":2,\"status\":\"shipped\"},\"operation_id\":\"run_7\"}"}],
  "isError":false}}
结果洞察
RBAC(权限点 action:<name>)在流水线第 2 步强制校验,admin 绕过;未授权 403,无会话用户 401。每次调用自动生成新幂等键 mcp_<uuid>(MCP 侧不重复提交同一键),服务端幂等登记按 action_runs 生效。越权失败同样落审计(result=error),写操作与 Web 端操作在审计链上地位等价。
调整建议
给 MCP 服务账号做最小授权(只授需要的 action:<name>);批量改动先确认记录级检查(RecordScope:无 WHERE 无边界写整批拒绝);乐观锁冲突返回 409 语义,重试前先查最新值。
动手试一试
登录:admin / admin1。输入内容:未授权 tools/call execute_action(action=update_order_status)。预期结果:返回 error.code=-32001、data.http_status=403、data.code=FORBIDDEN;授权后再调返回 rows_affected=1。
限制提示
幂等键自动生成,MCP 侧不提供重放语义;HTTP/API 类回写(对接外部系统)未实现,仅 SQL 类四种 edit_type;demo 数据源重启重建,回写改动会被还原但编辑态与审计保留。
故事 4 安全标记隐藏列 + OWL/SHACL 导出:敏感与开放两手抓
背景
合规要求订单金额按人授权可见,同时数据团队要把 order 对象定义送给外部语义校验工具。管理员在"数据打标"页建 PII 标记并绑定 order.amount,给分析师 alice 授权;数据工程师顺手导出单对象 SHACL 和整库 OWL。
传统做法对比
以前列级隐藏靠手工配 CLS 策略,一个敏感列一份策略,漏配就裸奔;本体外送靠人写文档,语义丢失。现在 Markings 三层模型直接驱动列级闸门,未持有标记的用户查询自动 NULL;OWL/SHACL 把对象 / 属性 / 链接 / 约束以标准格式一次性导出。
角色
平台管理员(打标 / 授权,admin 语义);数据工程师(导出 + 验证列级隐藏)。
操作步骤
  1. POST /api/v1/markings 创建 PII(category=privacy)
  2. POST /api/v1/markings/:id/bindings 绑定 order.amount(target_type=property)
  3. POST /api/v1/markings/user-markings 给 alice 授权 PII
  4. 用未持有用户查询 order,观察 amount 列;授权后复查
  5. GET /ontology/objects/:id/shacl 与 GET /ontology/export/owl 拉取语义导出
系统响应
创建 / 绑定 / 授权返回:
POST /api/v1/markings { "name": "PII", "category": "privacy" }
-> { "code": 0, "data": { "id": "3f2a..." } }
POST /api/v1/markings/3f2a.../bindings { "target_type": "property", "target_id": "amount" }
-> { "code": 0, "data": { "id": "9b1c...", "marking_id": "3f2a..." } }
POST /api/v1/markings/user-markings { "user_id": "alice", "marking_id": "3f2a..." }
-> { "code": 0, "data": { "id": "7d4e..." } }
未持有 PII 的用户查询 order 时 amount 列隐藏:
{ "code": 0, "data": {
  "columns": ["order_id", "amount", "region"],
  "rows": [[1, null, "华东"]] } }
SHACL 导出片段:
# LightFoundry Ontology — SHACL 校验形状导出(B7-5)
@prefix sh: <http://www.w3.org/ns/shacl#> .
foundry:orderShape a sh:NodeShape ;
    sh:targetClass foundry:order ;
    sh:property foundry:order_order_idShape , foundry:order_statusShape .
结果洞察
ColumnsHiddenByMarking 与 ApplyCLS 属性级安全经 MergeHiddenColumns 并集叠加——任一来源隐藏即隐藏,最终统一 NULL AS col 隐藏值而非整行拒绝。OWL 整库导出命名空间 http://zyinfo.local/foundry/ontology#:对象→owl:Class、属性→owl:DatatypeProperty(按 DataType→XSD 映射)、链接→owl:ObjectProperty、接口→rdfs:subClassOf。
调整建议
绑定 object_type/property 前先确保本体对象已建(直查主表校验存在性);dataset/metric 绑定可先行(目标未建也能打标);SHACL 看单对象约束、OWL 看整库结构,两者配合覆盖"校验 + 推理"两类消费。
动手试一试
登录:admin / admin1。输入内容:建 PII 标记 → 绑定 amount → 授权 alice。预期结果:未持有用户查询 amount 为 null、region 保持真实值;授权后 amount 恢复;GET /ontology/export/owl 返回含前缀声明的 Turtle 文本。
限制提示
object_type/dataset/metric 的标记绑定仅登记、无消费方(只有 property 绑定驱动列级闸门);OWL/SHACL 单向导出不做 roundtrip 保证;currency/percentage/enum/json/array 映射 xsd:string 并以 foundry:dataType 标注。

常见问题

为什么 /api/v1/mcp 直接 404?

MCP Server 默认关闭(foundry.mcp.enabled=false)。未启用时 NewServer 返回 ErrDisabled,路由不注册,访问 /api/v1/mcp 是 gin 404(不是 JSON-RPC 错误)。在 config.yaml 的 foundry 段加 mcp.enabled=true 并重启即可。

MCP 里的 execute_action 安全吗?

安全。它直接调 writepath.Executor(VALIDATE_AND_EXECUTE),五步流水线全走不可绕过:参数 Schema 校验、逐动作 RBAC(越权 403)、记录级检查、属性级写权限、审计 + 幂等 + 乐观锁 + 编辑态落盘。每次 tools/call 都落 MCP_TOOL_CALL 审计。

打标和 CLS 列级安全是什么关系?

是叠加的两道闸门:ApplyCLS 是原有列级策略;Markings 是 V5 新增的第二道。属性绑定 marking 后,用户未持有该标记即隐藏该列,与 CLS 结果取并集(任一来源隐藏即隐藏),最终统一把命中列替换为 NULL AS col。

OWL 和 SHACL 有什么区别?

OWL 描述"本体是什么":对象是 Class、属性是 DatatypeProperty、链接是 ObjectProperty、接口是 subClassOf,适合推理与语义对齐;SHACL 描述"数据应该长什么样":NodeShape + PropertyShape、基数、枚举(sh:in)、正则(sh:pattern),适合做数据校验。

MCP 工具能接 SSE 吗?

Handler 实现支持 SSE(GET + Accept: text/event-stream 返回 endpoint 事件和 30s 心跳),但 server.go 当前只注册了 POST /api/v1/mcp。HTTP+SSE 客户端接入需自行补挂 GET 路由,或直接用 Handler 挂载。

主题小结

一句话:MCP 把本体能力以标准协议开放给 LLM 生态(默认关闭、写路径 403 红线、强制审计),Markings 用"谁持有谁可见"做列级闸门,OWL/SHACL 把语义外送。记住几个边界:仅 tools 能力面、SSE GET 未注册、只有 property 绑定驱动列级隐藏、OWL/SHACL 单向导出。