技术热点落地:MCP「零接触 OAuth」EMA 进入 Stable——Anthropic / Okta / VSCode 同日接入,一周内把 MCP server 从「每用户手动接」升级到「企业 IdP 一次授权」(2026-06-19)
适用场景与目标
过去 24 小时的最强信号(与 6/19 AI 快报呼应):
- 6 月 18 日 11:30:Model Context Protocol 官方博客「Enterprise-Managed Authorization: Zero-touch OAuth for MCP」 正式发布——EMA extension 进入 stable 状态,作者 Paul Carleton(MCP Core Maintainer)
- 6 月 18 日 21:54:Hacker News「Zero-Touch OAuth for MCP」 95 分登首页——社区把 EMA 视为 MCP 自 2024 年 11 月开源以来最重要的企业级拐点
- 6 月 18 日当日接入阵容:
- IdP 侧:Okta 是首个支持 IdP,基于其主导的 Cross App Access(XAA) 协议
- Client 侧:Anthropic 在 Claude / Claude Code / Cowork 共享 MCP 层实现 EMA;Visual Studio Code IDE 内置 EMA
- Server 侧:Asana / Atlassian / Canva / Figma / Granola / Linear / Supabase 同日支持;Slack 与更多在接入
- 6 月 17 日 23:21:CNBC 报道 Noam Shazeer 从 Google Gemini 转投 OpenAI——AI 头部人才争夺白热化
- 6 月 17 日 19:50:Google DeepMind《From AGI to ASI》——多智能体协调(Multi-Agent Coordination)列 ASI 路径 4
- 6 月 18 日 09:56:新智元经 36 氪转载《清华 + 中山 OpenRath v1.2.1 开源》——清华 Rath Team + 中山大学 SCST Lab 把 Session 提为多 Agent 一等公民
- 6 月 18 日 09:52:新智元经 36 氪转载《OpenAI ChatGPT 弹窗要求 3D 活体自拍 + 实体证件》——OpenAI 实名化 / Anthropic 7 月 8 日跟进,「匿名 AI 时代」终结
6/17 + 6/18 + 6/19 的工程化推论:
| 时间 | 信号 | 工程化产物 |
|---|---|---|
| 6/17 | DeepMind 把多智能体协调列 ASI 路径 4 | 「为什么」:必须搭多 Agent 编排 |
| 6/18 | OpenRath v1.2.1 开源(Session 一等公民) | 「用什么」:Session 跨 Agent 共享状态 |
| 6/18 | OpenAI / Anthropic 同步按下「实名」键 | 「致命约束」:用户实名锁定 + 企业 AI 合规 |
| 6/19 | MCP EMA stable + Okta XAA + VSCode / Anthropic / 7 SaaS 同日接入 | 「今天怎么落地」:MCP server 治理从 per-user OAuth 升级到 enterprise IdP-managed |
这篇不讨论「MCP EMA 是不是比传统 OAuth 更好」。这篇解决「OpenAI / Anthropic 实名化 + ASI 路径 4 多 Agent 协同 + 清华 OpenRath Session 一等公民 + MCP EMA Stable 四股力量在 72 小时内同时按下,我们如何在 1 周内把 MCP server 治理从 per-user OAuth 升级到 enterprise IdP-managed,30 天把 ID-JAG JWT + Okta XAA + 7 个 SaaS 服务端 + Claude / VSCode 双客户端全部跑通,90 天把 ASI 路径 4 工程化基线中的『Agent 工具链 = 企业 IT 一级资产』落地」。
适用场景:
- 你在做 AI 产品 / Agent SaaS / 企业 AI 平台——老板 / 投资人 / CISO 开始问「MCP server 怎么治理?员工每人手接 OAuth 怎么审计?个人账号和工作账号怎么隔离?」
- 你在用 Claude Code / Cursor / Cline / VSCode + MCP 做 IDE 内的 Agent——发现每个 server 都要员工单独 OAuth 一次,安全团队完全无法强制一致策略
- 你在跑 OpenRath v1.2.1 多 Agent + 多 Session 协同(6/18 文章)——发现 Session 跨 server 调用时 MCP OAuth 状态无法共享,每个 server 都要重新授权
- 你在用 LangGraph / AutoGen / CrewAI 做多 Agent 编排(6/17 文章)——需要让企业 IdP 集中管控「哪个 Agent 能调哪些工具」,而不是每个用户各自授权
- 你在跑 SWE-Bench / MCPMark / Claw 24/7 / Claw Bench——需要多 Agent 协同 + 多 MCP server + 企业级审计,但 per-user OAuth 模式完全撑不住
- 你在做 企业 IT / CISO 治理 / AI 工具链合规——OpenAI 实名化后员工转向 Claude / Anthropic / Cursor / Cline,MCP server 的访问治理 是「Agent 进企业」的第一道闸
- 你在做 C 端 AI 聊天 / AI 客服 / AI 心理陪伴 / AI 教育 / AI 医疗 / AI 招聘——MCP server 治理 是个人 / 工作账号隔离 + 数据合规 + 审计追溯 的合规底线
核心目标(一周):
- D+0(今天,2 小时):把 MCP EMA extension spec 读完,定位 4 个核心组件:Client / IdP / MCP server / Authorization Server,画出当前 MCP server 部署的授权架构图
- D+1:注册 Okta 开发者账号,跑通 Okta Cross App Access(XAA) Demo flow——验证 ID-JAG JWT 签发 / 交换 / 验证全链路
- D+2:部署 1 个 MCP server(自托管 Linear / Supabase / Figma 任选 1) 接入 EMA,验证
ID-JAG → access token → MCP request三段链路 - D+3:在 VSCode IDE 内启用 EMA 接入同一 MCP server——验证 IDE 内 Agent 工具调用零接触授权
- D+4:在 Claude Code CLI 内启用 Anthropic 共享 MCP 层 EMA——验证 CLI / IDE / Cowork 三端共享授权
- D+5:扩到 3 个 MCP server(Linear + Supabase + Figma),跑通 企业 IdP group / role 映射 + 条件访问策略
- D+6:跑 回归 + 安全审计 + 合规文档——验证 EMA 不是「安全功能开启后无法 fallback / 审计断裂 / 个人账号和工作账号泄露」
- D+7:产出**「MCP EMA 企业级接入备忘 + ID-JAG JWT 流程图 + Okta XAA 配置清单 + VSCode / Claude Code 客户端启用指南 + 7 个 SaaS 服务端支持矩阵」**,给 VP/CISO/法务 walkthrough
最小可行方案(MVP)步骤
阶段 0:先盘点,再动手(Day 0)
不要直接接 EMA,先回答四个问题——任何治理改造前盘点资产 是底线:
# 1) 当前有哪些 MCP server 在用?每个 server 的授权模式是什么?
ls -la ~/.config/Claude/claude_desktop_config.json 2>/dev/null # Claude Desktop MCP 配置
ls -la ~/.vscode/mcp.json 2>/dev/null # VSCode MCP 配置(1.x+)
find ~/.config -name "mcp*.json" 2>/dev/null # 其他 MCP 客户端配置
cat ~/.config/Claude/claude_desktop_config.json | python3 -m json.tool
# 2) 当前 MCP server 是 self-host 还是 SaaS?
# 推荐写一份 mcp-inventory.md:
cat > mcp-inventory.md <<'EOF'
| Server | 部署模式 | 授权模式 | 用户数 | 访问敏感数据 | 是否需要企业 IdP |
|---|---|---|---|---|---|
| github | SaaS | per-user OAuth | 50 | Yes(代码) | ✅ |
| linear | SaaS | per-user OAuth | 30 | Yes(业务) | ✅ |
| supabase | Self-host | API key | 5 | Yes(DB) | ✅ |
| figma | SaaS | per-user OAuth | 20 | Yes(设计) | ✅ |
| atlassian | SaaS | per-user OAuth | 40 | Yes(Jira) | ✅ |
EOF
# 3) 当前企业 IdP 是什么?支持 OAuth 2.0 + OIDC + SCIM?
# Okta / Entra ID(Azure AD)/ Google Workspace / Ping / Auth0
# 检查 IdP 是否支持 Cross App Access(XAA)协议
# 4) 当前 MCP host 客户端是什么?版本多少?是否支持 EMA?
# VSCode ≥ 1.95 (2025-09 起支持 EMA)
# Claude Desktop ≥ 0.9.0 (2026-06 EMA stable 后支持)
# Claude Code CLI ≥ 1.0.40 (2026-06 支持)
把答案写到 mcp-inventory.md——这是治理的起点,没有这一步,后面所有 EMA 配置都是「无的放矢」。
阶段 1:注册 Okta 开发者账号 + 跑通 Cross App Access Demo(Day 1)
EMA extension 的首个支持 IdP 是 Okta(基于 Cross App Access / XAA 协议)。先跑通 Okta XAA Demo 是最快验证 ID-JAG JWT 流程的方式:
# 1) 注册 Okta 免费开发者账号(https://developer.okta.com/signup/)
# 2) 创建 Org + 第一个 App(OIDC Web App)
okta login # CLI 登录(需先安装 okta-cli)
# 或浏览器登录 Okta Admin Console
# 3) 启用 Cross App Access(XAA)协议
# Admin Console → Security → API → Trust → Cross App Access → Enable
# Okta 默认 2025 年起开启 XAA 支持
# 4) 配置 App 的 XAA Token Exchange
# App → Sign On → OpenID Connect ID Token → "Configure" →
# Grant Type: urn:ietf:params:oauth:grant-type:token-exchange (RFC 8693)
# Subject Token Type: urn:ietf:params:oauth:token-type:id_token
# Actor Token Type: urn:ietf:params:oauth:token-type:id_token
# 5) 跑通 Okta XAA Demo Flow
git clone https://github.com/okta/okta-cross-app-access-demo
cd okta-cross-app-access-demo
npm install
npm run dev # 启动 Demo App
# 浏览器访问 http://localhost:3000 → 点 "Login with Okta" →
# 登录后自动跳到 MCP server 授权页 → 但无 per-server consent 弹窗(XAA 接管)
# 查看 Network 面板 → 应看到 /oauth2/v1/token 请求携带 act (actor) claim
关键产出:验证 ID-JAG(Identity Assertion JWT Authorization Grant) 在 Okta 实际签发出来,client 能用它换 access token,用户全程无 per-server 同意页。
阶段 2:部署 1 个 MCP server(Linear)接入 EMA(Day 2)
Linear 是首批 7 个 EMA 支持 SaaS 之一,官方文档最完整——Day 2 用 Linear 当 target:
# 1) 安装 Linear MCP server(Self-host 模式最稳)
git clone https://github.com/linear/linear-mcp-server
cd linear-mcp-server
npm install
# 2) 配置 EMA-aware 授权服务器
# 关键文件:src/auth/ema.ts
cat > src/auth/ema.ts <<'EOF'
import { Router } from 'express';
import jwt from 'jsonwebtoken';
const router = Router();
// EMA: 验证 ID-JAG(来自 IdP 的 Identity Assertion JWT)
router.post('/oauth2/token', async (req, res) => {
const { grant_type, assertion } = req.body;
if (grant_type !== 'urn:ietf:params:oauth:grant-type:token-exchange') {
return res.status(400).json({ error: 'unsupported_grant_type' });
}
// 1. 验证 ID-JAG 签名(Okta JWKS)
const idJag = await verifyIdJag(assertion, OKTA_JWKS_URL);
// 2. 提取 subject(员工)+ group / role
const subject = idJag.sub; // Okta user id
const groups = idJag.groups; // Okta group memberships
const aud = idJag.aud; // target MCP server client_id
// 3. 检查 IdP 策略(centralized policy)
const allowed = await checkPolicy({
subject,
groups,
resource: 'linear-api',
action: 'mcp:*',
});
if (!allowed) {
return res.status(403).json({ error: 'access_denied' });
}
// 4. 签发 access token(短期 + audience 限定)
const accessToken = jwt.sign(
{ sub: subject, scope: 'mcp:read mcp:write', aud: 'linear-mcp' },
PRIVATE_KEY,
{ expiresIn: '1h', issuer: 'https://linear-mcp.example.com' }
);
res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
scope: 'mcp:read mcp:write',
});
});
async function verifyIdJag(token, jwksUrl) {
// 拉 Okta JWKS → 用 kid 找公钥 → 验签
const jwks = await fetch(jwksUrl).then(r => r.json());
return jwt.verify(token, jwks, {
algorithms: ['RS256'],
audience: 'linear-mcp', // 必须是本 MCP server 的 client_id
issuer: 'https://your-org.okta.com',
});
}
export default router;
EOF
# 3) 启动 MCP server(含 EMA 授权)
npm run build && npm start
# 默认监听 :3001,MCP endpoint 在 /mcp
# 4) 配置 Linear API key(在 Linear Admin → API → Personal API Key)
export LINEAR_API_KEY="lin_api_xxxxxxxxxxxxxx"
# 5) 验证 EMA 流程
curl -X POST http://localhost:3001/oauth2/token \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "assertion=$ID_JAG_FROM_OKTA" \
-d "client_id=linear-mcp-client"
# 期望返回 { access_token, expires_in: 3600, scope: "mcp:read mcp:write" }
关键产出:1 个 MCP server(Linear)+ EMA 授权服务器 + IdP 策略校验 全链路跑通,用户全程无 per-server OAuth 弹窗。
阶段 3:在 VSCode IDE 启用 EMA 接入同一 MCP server(Day 3)
VSCode 是首批 EMA 支持的 IDE client 之一——Day 3 用 VSCode 当 target:
// .vscode/mcp.json(或 ~/.vscode/mcp.json)
{
"servers": {
"linear-ema": {
"type": "http",
"url": "http://localhost:3001/mcp",
"authorization": {
// VSCode 1.95+ 自动从 IdP 拿 ID-JAG,无需 per-server OAuth
"ema": {
"idp": "okta",
"org": "https://your-org.okta.com",
"scope": "mcp:read mcp:write",
"clientId": "vscode-mcp-client"
}
}
}
}
}
# 启动 VSCode + Copilot Chat + MCP
code .
# 在 VSCode Copilot Chat 中输入:
# @linear /mcp.list 列出 Linear MCP 提供的工具
# @linear /mcp.call list_issues {"team": "ENG"} 调用工具
# 关键验证:
# 1. 首次调用时**不应该**弹 Linear OAuth 同意页
# 2. 应该看到 VSCode 顶部 status bar 显示 "Connected to linear-ema via Okta SSO"
# 3. Network 面板:POST /mcp/list_issues → 应携带 Authorization: Bearer <access_token>
# 4. 关闭 VSCode + 重新打开 → 应**仍然保持连接**(token 缓存 + IdP 续签)
关键产出:VSCode IDE 内 通过 EMA 连接 Linear MCP server,用户零接触 OAuth,重启 IDE 仍保持会话。
阶段 4:在 Claude Code CLI 启用 Anthropic 共享 MCP 层 EMA(Day 4)
Anthropic 在 Claude / Claude Code / Cowork 三端共享同一套 MCP 授权层——Day 4 用 Claude Code 当 target 验证「一次授权,三端共享」:
# 1) 更新 Claude Code 到 ≥ 1.0.40(2026-06 EMA 支持版本)
npm install -g @anthropic-ai/claude-code@latest
claude --version # 应 ≥ 1.0.40
# 2) 配置 Anthropic MCP 层 EMA(~/.claude/mcp_settings.json)
cat > ~/.claude/mcp_settings.json <<'EOF'
{
"mcpServers": {
"linear-ema": {
"type": "http",
"url": "http://localhost:3001/mcp",
"auth": {
"type": "ema",
"idp": "okta",
"orgUrl": "https://your-org.okta.com",
"clientId": "claude-code-mcp-client",
"sharedAcrossProducts": ["claude", "claude-code", "cowork"]
}
}
}
}
EOF
# 3) 启动 Claude Code + 调用 Linear MCP 工具
claude chat "/mcp.list linear-ema"
# 期望:列出 linear_create_issue / list_issues / update_issue 等工具
claude chat "用 linear-ema 给我创建 1 个 issue:标题「MCP EMA 验证」,描述「Day 4 测试」"
# 期望:
# 1. 不弹 Linear OAuth 同意页
# 2. 自动从 Okta 拿 ID-JAG → 换 access token → 调 Linear API
# 3. 创建成功,issue id 返回
# 4) 验证「一次授权三端共享」
# 打开 Claude Desktop / Cowork(同 org) → 应**不再弹 OAuth**,
# 因为 Anthropic 共享 MCP 层已经把 Linear-EMA 的 access_token 缓存到 org-level
关键产出:Claude / Claude Code / Cowork 三端共享 EMA 授权——一个员工配置一次,整组织跨产品线继承。
阶段 5:扩展到 3 个 MCP server + 企业 IdP group/role 策略(Day 5)
扩到 Linear + Supabase + Figma 三个 EMA 支持 SaaS,跑通企业级 group / role 映射 + 条件访问:
# 1) Supabase MCP server(Self-host)
git clone https://github.com/supabase/supabase-mcp-server
cd supabase-mcp-server && npm install
# 类似 Linear,在 src/auth/ema.ts 中实现 ID-JAG → access token 流程
npm run build && npm start # 监听 :3002
# 2) Figma MCP server(官方 SaaS)
# Figma 在 2026-06-18 起 EMA 支持,需要在 Figma Admin → API → EMA 启用
# 拿到 client_id + authorization_endpoint
# 3) 配置 Claude Code 接入 3 个 MCP server
cat > ~/.claude/mcp_settings.json <<'EOF'
{
"mcpServers": {
"linear-ema": { ... }, # 同阶段 4
"supabase-ema": {
"type": "http",
"url": "http://localhost:3002/mcp",
"auth": { "type": "ema", "idp": "okta", ... }
},
"figma-ema": {
"type": "http",
"url": "https://api.figma.com/mcp",
"auth": { "type": "ema", "idp": "okta", ... }
}
}
}
EOF
# 4) 在 Okta Admin Console 配置 group / role 映射
# Admin Console → Directory → Groups → 创建 3 个 group:
# mcp-linear-users (10 人)
# mcp-supabase-dba (3 人)
# mcp-figma-designers (15 人)
# Admin Console → Security → API → Trusted Servers →
# linear-mcp: require group=mcp-linear-users
# supabase-mcp: require group=mcp-supabase-dba AND role=db-admin
# figma-mcp: require group=mcp-figma-designers
# 5) 验证「不同 group 只能调对应 MCP server」
# 用 mcp-linear-users 组的员工登录 → 调 figma-mcp → 403 forbidden
# 用 mcp-figma-designers 组的员工登录 → 调 supabase-mcp → 403 forbidden
关键产出:3 个 MCP server × Okta IdP group/role 集中管控——管理员在 IdP 后台改一次策略,所有 client(VSCode / Claude Code / Cowork)实时生效。
阶段 6:跑回归 + 安全审计 + 合规文档(Day 6)
EMA 接好了不能只跑功能测试,安全审计 + 合规文档 才是给 CISO 看的东西:
# 1) 安全审计 checklist
cat > ema-security-audit.md <<'EOF'
## 身份验证
- [ ] ID-JAG JWT 签名验证:RS256 / ES256 + JWKS 轮换
- [ ] ID-JAG audience claim 严格校验 = 本 MCP server client_id
- [ ] ID-JAG issuer claim = Okta org URL(防跨 org 攻击)
- [ ] access token 短期(≤1h)+ refresh via IdP
## 授权
- [ ] IdP group/role 映射在 server 端**再校验一次**(不只信 IdP)
- [ ] MCP tool 级权限(read / write / admin)独立配置
- [ ] 危险操作(delete / drop / execute)需 MFA step-up
## 审计
- [ ] 所有 MCP request 记录:user_id / mcp_server / tool / timestamp / result
- [ ] 审计日志**不可被 MCP server 自己删除**(写到独立 SIEM)
- [ ] 异常检测:同 user 1h 内 100+ 次写操作 → 告警
## 个人/企业账号隔离
- [ ] ID-JAG iss/aud 校验确保**企业 IdP 签发**,拒绝个人 Okta org
- [ ] 客户端启动时**显式选择** enterprise identity(不允许 fallback 到 personal)
- [ ] Token cache key 含 org hash,防止跨 org 串 token
## Fallback
- [ ] EMA 失败 → 自动 fallback 到 per-user OAuth(不阻断用户)
- [ ] Fallback 路径**单独审计**,方便后续切换回 EMA
- [ ] IdP 宕机 → MCP server 走只读 / deny 模式
EOF
# 2) 跑回归测试
npm test # MCP server 单元测试(覆盖 EMA 流程)
npm run test:e2e # 端到端:VSCode + Claude Code + Okta 三方联动
# 3) 合规文档
cat > ema-compliance-doc.md <<'EOF'
## 数据流
员工 SSO (Okta) → ID-JAG → MCP server 授权服务器 → access token → MCP request
全链路经过 IdP 审计;access token 不含 PII(只有 sub + scope)。
## 数据驻留
- ID-JAG 在 Okta 签发,cache 在 client 端(加密)
- access token 在 MCP server 签发,cache 在 client 端(短期)
- MCP request payload 不经过 IdP,直达 MCP server
## 撤销流程
员工离职 / 转岗 → Okta Admin Console 禁用账号 →
所有 client 在 access_token 过期(≤1h)后自动失活 →
无需单独撤销 MCP server 端 session
## 应急
IdP 宕机 → MCP server 降级为 per-user OAuth(用户手动授权)→
SIEM 告警:CISO 收到「IdP 不可用」通知 →
恢复后自动切回 EMA
EOF
关键产出:安全审计 + 合规文档 + 回归测试——给 CISO 看的东西不能只是「功能跑通」,必须有审计 + 有降级 + 有撤销流程。
阶段 7:30 天 / 90 天路线图(Day 7)
一周跑通 MVP 后,30 天扩到 7 个 SaaS + 90 天全组织推广:
| 时段 | 目标 | 关键产出 |
|---|---|---|
| D+0..D+7(已完成) | 1 个 MCP server(Linear)+ Okta XAA + VSCode / Claude Code 双 client | MVP 跑通 |
| D+8..D+14 | 扩到 3 个 server(Linear + Supabase + Figma)+ IdP group/role 策略 | 企业级权限模型 |
| D+15..D+21 | 扩到 7 个 server(+ Asana / Atlassian / Canva / Granola)+ Slack 接入中 | EMA 全支持 SaaS 矩阵 |
| D+22..D+30 | 接入 Anthropic 共享 MCP 层 / Microsoft Entra ID IdP / Visual Studio Code IDE 三 client | 三端共享授权 |
| D+31..D+60 | 全组织 100% MCP server 切到 EMA + 移除 per-user OAuth 选项 | 治理完成 |
| D+61..D+90 | ASI 路径 4 工程化基线:「Agent 工具链 = 企业 IT 一级资产」写进 IT 治理章程 | 长期可持续 |
关键实现细节
1. ID-JAG(Identity Assertion JWT Authorization Grant)JWT 流程
ID-JAG 是 EMA 的核心技术——用户在 IdP SSO 期间签发,client 拿去换 access token:
// 1. Okta 在 SSO 后签发的 ID-JAG payload(结构示意)
interface IdJagPayload {
iss: string; // = "https://your-org.okta.com"
sub: string; // = Okta user id(员工工号)
aud: string; // = 目标 MCP server client_id(如 "linear-mcp")
exp: number; // Unix timestamp(短期,≤5min)
iat: number;
jti: string; // 唯一 ID,防重放
// EMA 关键扩展
act: { // Actor claim:哪个 agent 在代表这个 user
sub: string; // agent id(如 "claude-code-cli" / "vscode-copilot")
};
// IdP group / role
groups: string[]; // ["mcp-linear-users", "engineering"]
email: string; // 仅作为 audit hint,不用于授权决策
name: string;
}
// 2. Client 拿 ID-JAG 去换 access token
async function exchangeIdJagForAccessToken(idJag: string, mcpServer: string) {
const response = await fetch(`${mcpServer}/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
client_id: mcpServer, // = aud in ID-JAG
subject_token: idJag, // ID-JAG 本身
subject_token_type: 'urn:ietf:params:oauth:token-type:id_jag',
requested_token_type: 'urn:ietf:params:oauth:token-type:access_token',
}),
});
const data = await response.json();
return data.access_token; // 短期(≤1h),用于 MCP request
}
// 3. MCP request 携带 access token
async function callMcpTool(server: string, tool: string, args: any) {
const accessToken = await getCachedAccessToken(server);
const response = await fetch(`${server}/mcp/${tool}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(args),
});
if (response.status === 401) {
// access token 过期 → 重新用 ID-JAG 换
await refreshAccessToken(server);
return callMcpTool(server, tool, args); // 重试
}
return response.json();
}
2. Okta Cross App Access(XAA)核心配置
// okta-xaa-config.ts
export const oktaXaaConfig = {
// Okta Org
orgUrl: 'https://your-org.okta.com',
// Cross App Access 协议开关
xaa: {
enabled: true,
// 信任的 MCP server 列表
trustedServers: [
'linear-mcp', // 自托管 Linear MCP
'supabase-mcp', // 自托管 Supabase MCP
'figma-mcp', // Figma 官方 SaaS
// ... EMA stable 后扩到 7+ SaaS
],
// Token Exchange 配置
tokenExchange: {
// 客户端拿 ID-JAG 换 access token 时的 grant type
grantType: 'urn:ietf:params:oauth:grant-type:token-exchange',
// ID-JAG 有效期(短期,防重放)
idJagTtl: 300, // 5 分钟
// access token 有效期
accessTokenTtl: 3600, // 1 小时
// 强制 audience 校验
strictAudience: true, // aud 校验失败 → 拒绝
},
},
// group / role 映射到 MCP server 权限
groupMapping: {
'mcp-linear-users': {
resources: ['linear-mcp'],
scopes: ['mcp:read', 'mcp:write'],
},
'mcp-supabase-dba': {
resources: ['supabase-mcp'],
scopes: ['mcp:read', 'mcp:write', 'mcp:admin'],
mfaRequired: true, // 危险操作需 MFA step-up
},
'mcp-figma-designers': {
resources: ['figma-mcp'],
scopes: ['mcp:read', 'mcp:write'],
},
},
// 审计日志目的地
auditLog: {
destination: 'splunk',
splunkIndex: 'ai-mcp-access',
includeClaims: ['sub', 'groups', 'aud', 'act', 'jti'],
},
};
3. VSCode IDE 内 EMA 客户端配置详解
// VSCode 1.95+ 的 mcp.json 完整 schema
{
"servers": {
"linear-ema": {
"type": "http",
"url": "http://localhost:3001/mcp",
// EMA 授权配置
"authorization": {
"ema": {
"idp": "okta",
"org": "https://your-org.okta.com",
"clientId": "vscode-mcp-client",
// 需要的 scope
"scope": ["mcp:read", "mcp:write"],
// ID-JAG 缓存策略
"cache": {
"type": "memory", // 内存缓存(推荐)
"encryptAtRest": true, // 加密存储
"ttl": 3600, // 1 小时
},
// 用户身份选择策略
"identity": {
"requireEnterprise": true, // 强制企业身份(不允许 personal Okta)
"showIdentityPicker": true, // 启动时显式选择
"rememberChoice": false, // 不记住选择(每次启动重新选)
},
// Fallback 策略(IdP 宕机时)
"fallback": {
"enabled": true,
"type": "per-user-oauth", // 降级到 per-user OAuth
"alertCiso": true, // 触发 SIEM 告警
}
}
},
// MCP server 提供的工具列表(VSCode IDE 内可见)
"tools": [
"linear_create_issue",
"linear_list_issues",
"linear_update_issue",
"linear_search_issues",
],
},
// 其他 EMA-aware MCP server ...
}
}
4. MCP server 端 EMA-aware 授权服务器(完整实现)
// src/auth/ema-authorization-server.ts
import express from 'express';
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const router = express.Router();
const OKTA_ORG = process.env.OKTA_ORG || 'https://your-org.okta.com';
const OKTA_JWKS_URL = `${OKTA_ORG}/oauth2/v1/keys`;
const PRIVATE_KEY = process.env.MCP_PRIVATE_KEY!; // RS256 private key
const jwksClientInstance = jwksClient({ jwksUri: OKTA_JWKS_URL });
function getSigningKey(header: any, callback: any) {
jwksClientInstance.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
callback(null, key.getPublicKey());
});
}
// ID-JAG 验证
async function verifyIdJag(token: string, expectedAud: string): Promise<any> {
return new Promise((resolve, reject) => {
jwt.verify(token, getSigningKey, {
algorithms: ['RS256'],
audience: expectedAud,
issuer: OKTA_ORG,
}, (err, decoded) => {
if (err) return reject(err);
// 额外校验 EMA 扩展
const payload = decoded as any;
if (!payload.act || !payload.act.sub) {
return reject(new Error('Missing act claim - not an EMA ID-JAG'));
}
resolve(payload);
});
});
}
// IdP 策略校验(基于 groups)
async function checkIdpPolicy(payload: any, resource: string, action: string): Promise<boolean> {
const groups = payload.groups || [];
// 简化示例:实际应从 Okta Admin API 拉 group → resource 映射
const policy = {
'mcp-linear-users': { resource: 'linear-mcp', scopes: ['mcp:read', 'mcp:write'] },
'mcp-supabase-dba': { resource: 'supabase-mcp', scopes: ['mcp:read', 'mcp:write', 'mcp:admin'] },
};
for (const group of groups) {
const rule = policy[group];
if (rule && rule.resource === resource && rule.scopes.includes(action)) {
return true;
}
}
return false;
}
// OAuth 2.0 Token Endpoint(接受 ID-JAG → 签发 access token)
router.post('/oauth2/token', express.urlencoded({ extended: true }), async (req, res) => {
try {
const { grant_type, subject_token, subject_token_type, client_id } = req.body;
if (grant_type !== 'urn:ietf:params:oauth:grant-type:token-exchange') {
return res.status(400).json({ error: 'unsupported_grant_type' });
}
if (subject_token_type !== 'urn:ietf:params:oauth:token-type:id_jag') {
return res.status(400).json({ error: 'invalid_subject_token_type' });
}
// 1. 验证 ID-JAG(签名 + audience + issuer + EMA 扩展)
const payload = await verifyIdJag(subject_token, client_id);
// 2. IdP 策略校验(group / role 映射)
const allowed = await checkIdpPolicy(payload, client_id, 'mcp:*');
if (!allowed) {
return res.status(403).json({ error: 'access_denied' });
}
// 3. 签发 access token
const accessToken = jwt.sign(
{
sub: payload.sub,
scope: 'mcp:read mcp:write',
aud: client_id,
act: payload.act, // 保留 actor 信息,方便审计
},
PRIVATE_KEY,
{
algorithm: 'RS256',
expiresIn: '1h',
issuer: `https://${client_id}.example.com`,
jwtid: `${Date.now()}-${Math.random()}`,
}
);
res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
scope: 'mcp:read mcp:write',
});
} catch (err: any) {
console.error('Token exchange failed:', err);
res.status(401).json({ error: 'invalid_grant', error_description: err.message });
}
});
export default router;
常见坑与规避清单
坑 1:ID-JAG audience 校验不严,导致跨 MCP server 重放攻击
症状: 员工 A 在 Okta 拿到 ID-JAG(aud=linear-mcp),攻击者拦截后改成 aud=supabase-mcp 重放,supabase-mcp 信以为真,给员工 A 签发了 supabase access token——员工 A 拿到了他不应有的 supabase 权限。
根因: MCP server 端没有严格校验 ID-JAG 的 aud claim,等于把”哪个员工能换哪个 server 的 token”这一决定权外包给了客户端。
解决:
// ❌ 错误:信任客户端传来的 client_id
async function badVerify(token: string, clientId: string) {
return jwt.verify(token, publicKey); // 没校验 aud
}
// ✅ 正确:audience claim 必须等于本 server 的 client_id
async function goodVerify(token: string) {
return jwt.verify(token, getSigningKey, {
algorithms: ['RS256'],
audience: 'linear-mcp', // 硬编码本 server 的 client_id
issuer: OKTA_ORG,
});
}
额外防御:在签发 access token 时也要校验 aud claim 跟当前 server 一致,避免”用 linear 的 token 去调 supabase”。
坑 2:把 ID-JAG 当 access token 用,导致长期暴露
症状: 开发者偷懒,把 Okta 签发的 ID-JAG 直接塞到 MCP request 的 Authorization: Bearer 头里——ID-JAG 有效期 5 分钟,但 ID-JAG 包含员工邮箱、姓名、组别等 PII,SIEM 日志里全是 PII。
根因: 不理解 ID-JAG 是”身份断言”,不是”访问令牌”——前者给你的是”我是谁”,后者给你的是”我能干什么”。
解决:
- 必须先换 access token(短期、scope 限定、audience 限定)再用
- access token 签发时不要包含邮箱 / 姓名等 PII,只保留
sub + scope + aud - SIEM 日志记录 access token 而非 ID-JAG(access token 不含 PII)
坑 3:ID-JAG / access token 本地明文缓存,VSCode 插件读得到
症状: 开发者图省事,把 ID-JAG / access token 缓存到 ~/.vscode/mcp_tokens.json 明文文件——任何 VSCode 插件都能读(VSCode 扩展 API 默认能读用户主目录文件)。
根因: VSCode 扩展权限模型默认信任所有扩展,token 落地到文件就等于公开。
解决:
- 强制使用
keytar(Windows Credential Manager / macOS Keychain / Linux Secret Service)加密存储 - 或者用 VSCode SecretStorage API(
context.secrets.store) - 永远不要把 token 写到
mcp_tokens.json/localStorage/IndexedDB
// ✅ 正确:用 VSCode SecretStorage
import { ExtensionContext } from 'vscode';
export async function cacheAccessToken(context: ExtensionContext, server: string, token: string) {
await context.secrets.store(`mcp-token-${server}`, token);
}
export async function getAccessToken(context: ExtensionContext, server: string): Promise<string | undefined> {
return context.secrets.get(`mcp-token-${server}`);
}
坑 4:access token 有效期设太长,安全事件无法快速止血
症状: 开发者觉得 1 小时太短,每次重新换麻烦,把 access token TTL 设成 7 天——员工离职 / 转岗 7 天内还能用 MCP server 调敏感数据。
根因: 把”用户体验”凌驾于”安全”之上——Okta 撤销账号后,access token 不会自动失效。
解决:
- access token TTL ≤ 1 小时(EMA 推荐的默认值)
- ID-JAG TTL ≤ 5 分钟(短到不能重放)
- 员工离职 / 转岗后 Okta Admin 禁用账号 → 1 小时内所有 client 自动失活
- 如果非要长 TTL,用 refresh token + rotation(每次 refresh 撤销上一个)
坑 5:fallback 到 per-user OAuth 后没审计,攻击者故意触发降级
症状: 攻击者故意构造畸形 ID-JAG 触发 MCP server fallback 到 per-user OAuth 流程,per-user OAuth 流程没有 IdP 审计,攻击者借此绕开企业策略。
根因: 设计 fallback 时只考虑”用户用得了”,没考虑”攻击者会不会故意触发 fallback”。
解决:
- Fallback 触发时必须写 SIEM 告警(“CISO: IdP 降级事件”)
- Fallback 路径单独审计(单独 log,不能跟 EMA 路径混)
- Fallback 触发后要求用户在 X 分钟内手动重连 IdP(否则禁用 access)
- Fallback 的 access token 短于 EMA 路径(如 15 分钟),逼用户重连
async function fallbackToPerUserOAuth(req: Request, res: Response) {
// 1. 告警 SIEM
await siem.alert({
severity: 'high',
event: 'mcp_ema_fallback_triggered',
user_id: req.body.user_id,
mcp_server: req.params.server,
reason: req.body.failure_reason,
});
// 2. 签发短 TTL access token
const accessToken = jwt.sign({ sub: req.body.user_id, scope: 'mcp:read', aud: req.params.server }, PRIVATE_KEY, {
expiresIn: '15m', // 比 EMA 路径短
issuer: req.params.server,
});
res.json({ access_token: accessToken, expires_in: 900 });
}
坑 6:企业 IdP group 配置错误,员工能用任意 MCP tool
症状: 管理员在 Okta 后台配 group / role 映射时配错——mcp-figma-designers 组被错误地加到 mcp-supabase-dba 权限上,设计师意外能删库。
根因: 配置 UI 不直观(Okta group → resource mapping 没有强类型校验),配错不会报错,只会在调用时才生效——而调用时往往已经晚了。
解决:
- 用 IaC 工具管 Okta 配置(Terraform + Okta provider / Pulumi),所有 group 映射写代码、code review、CI 测试
- 加一层 MCP server 端兜底校验:即使 IdP 配错,MCP server 也按”白名单 group”再校验一次
- MCP tool 级权限细分(read / write / admin),不要”线性 group 给全部权限”
# Terraform: okta-mcp-group-mapping.tf
resource "okta_group" "mcp_supabase_dba" {
name = "mcp-supabase-dba"
}
resource "okta_group_rule" "mcp_supabase_dba_only" {
groupid = okta_group.mcp_supabase_dba.id
# 显式白名单:只有 dba@your-org.com 邮箱才能进这个 group
expression = "user.email.endsWith(\"@your-org.com\") && user.department==\"Engineering\""
# 强制 MFA
actions = {
assign_user_to_groups = [okta_group.mcp_supabase_dba.id]
}
}
坑 7:MCP server 升级到 EMA 后,没保留 per-user OAuth fallback,用户被锁出
症状: 部署 MCP server 新版本直接关掉 per-user OAuth endpoint,老用户 / 测试环境 / 个人开发者全部无法连接——GitHub issue 一片骂声。
根因: EMA 还是 stable 不到 1 个月的新协议,不是所有 client 都支持,不是所有 IdP 都支持(目前只有 Okta),一刀切会锁死非 Okta 用户。
解决:
- 保留 per-user OAuth endpoint 作为 fallback(至少 6 个月)
- 客户端启动时自动探测 IdP 支持 → 优先用 EMA,不支持则 per-user OAuth
- 文档明确标注 EMA 路径需要:Okta IdP + Visual Studio Code 1.95+ / Claude Code 1.0.40+ / Claude Desktop 0.9.0+
- 非 Okta 用户清晰路径:先注册 Okta org(免费 30 天试用)→ 迁移 → 切 EMA
坑 8:EMA 接入后,没同步 OpenRath Session 一等公民设计
症状: MCP EMA 单独跑通了,但 6/18 文章 OpenRath v1.2.1 的 Session 一等公民 架构里,跨 MCP server 的状态共享 没利用 EMA 的 act claim——Agent 在 Session A 拿到的 ID-JAG 不能直接在 Session B 用,多 Session 协同失效。
根因: MCP EMA 是协议级授权,OpenRath Session 是应用级状态,两者没有自动联动——开发者需要手动桥接。
解决:
- OpenRath Session 启动时,绑定 ID-JAG 到 Session context(用
act.sub作为 Session ID 的前缀) - 跨 Session 共享 MCP tool 时,复用同一个 ID-JAG(前提是 IdP 同意)
- 审计日志关联 Session ID 和 ID-JAG 的
jti,方便事后追溯
// OpenRath + MCP EMA 桥接示例
import { Session } from 'openrath';
async function bindIdJagToSession(session: Session, idJag: string) {
const payload = jwt.decode(idJag) as any;
// 用 ID-JAG 的 act.sub 作为 Session ID 前缀
// 保证同 agent 跨 Session 共享授权
session.context.mcp = {
idp: 'okta',
idJagJti: payload.jti, // 用于审计关联
actSub: payload.act.sub, // agent id
userSub: payload.sub, // 员工 id
expiresAt: payload.exp,
};
}
// 跨 Session 共享 MCP tool
async function callMcpToolAcrossSessions(sessionA: Session, sessionB: Session, tool: string) {
// Session A 和 Session B 共享同一个 ID-JAG
// (前提是同一个 act.sub + 同一个 IdP session)
const idJag = sessionA.context.mcp.idJag;
return await callMcpToolWithIdJag(idJag, tool);
}
成本/性能/维护权衡
成本对比(per 1000 员工规模,年化)
| 维度 | 传统:per-user OAuth | MCP EMA + Okta XAA |
|---|---|---|
| 员工手动接 OAuth 时间 | ~5 分钟/server × 5 server × 1000 人 = 417 工时/季度(每加一个 server 都要全员重接) | 0(SSO 一次,零接触) |
| Onboarding 新员工 | ~30 分钟(每个 server 单独配置) | ~1 分钟(SSO 自动连接) |
| Offboarding 离职员工 | ~30 分钟(每个 server 单独撤销)+ 残留风险 | Okta 禁用账号,1 小时内自动失活 |
| IdP 授权 | 无 | Okta Workforce = $8/user/月(1000 人 = $96k/年) Okta Starter = $2/user/月(小公司备选) |
| MCP server 改造成本 | 无 | 1 个 MCP server ≈ 40-80 工时(含 ID-JAG 验证 + 策略 + 审计) 1000 人公司 5 个 server ≈ 200-400 工时一次性 |
| 审计 / 合规 | 自建审计(每个 server 单独) | 集中审计(Okta + MCP server 端双写) 节省约 50% 合规工时 |
| 年化总成本 | 传统 OAuth 工时 + 自建审计 ≈ $50-100k/年 | Okta 授权 + 一次性改造成本 ≈ $100-130k/年(首年) 次年起 ≈ $96-110k/年 |
关键判断:
- < 50 人小团队:per-user OAuth 更划算(Okta 授权 $96k/年 vs 工时 $5-10k/年)
- 50-500 人中型企业:MCP EMA ROI 开始显现(合规 + 审计 + 离职清理节省 > Okta 授权费)
- > 500 人大型企业 / 金融 / 医疗 / 政府:MCP EMA 必须上(合规是 hard requirement)
性能对比(per MCP request)
| 指标 | 传统 per-user OAuth | MCP EMA(首次) | MCP EMA(缓存命中) |
|---|---|---|---|
| 首请求延迟(用户冷启动) | ~2-5s(弹 OAuth 同意页 + 用户手动同意) | ~200-500ms(IdP SSO + ID-JAG 签发) | ~50-100ms(access token 缓存) |
| 稳态请求延迟(热路径) | ~50-100ms(OAuth token 缓存) | ~50-100ms(access token 缓存) | ~50-100ms |
| access token 续签 | 用户手动(断流) | 自动(ID-JAG → 新 token,200ms 内) | 不需要(cache 命中) |
| Okta JWKS 拉取 | 0(无 JWKS) | 首次 ~100ms(缓存 24h) | 0(缓存) |
| MCP server CPU 开销 | 低(仅 OAuth 验证) | 中(JWKS + JWT 验签 + 策略校验,每次 ~5-10ms) | 中 |
| Okta API QPS | 0 | ~1-10 QPS(per 1000 人,取决于缓存策略) | 0(缓存命中) |
关键判断:
- 冷启动:EMA 路径比 per-user OAuth 快 5-10 倍(无 OAuth 弹窗)
- 稳态:两者相当(都在 50-100ms 量级)
- Okta JWKS 拉取:建议缓存 24h,避免每个 request 都拉
维护权衡
收益:
- 零接触 OAuth——员工体验质变(不再每加 server 都全员重接)
- 集中治理——CISO 在 IdP 后台一条策略搞定所有 MCP server
- 快速止血——员工离职 / 转岗 1 小时内自动失活(access token 短期)
- 跨产品线共享——Anthropic 共享 MCP 层让 Claude / Claude Code / Cowork 一次授权三端共享
- 审计完整——Okta + MCP server 双写审计,SIEM 集中分析
代价:
- 依赖 Okta——目前 EMA 首个支持 IdP 只有 Okta,被锁定的风险(其他 IdP 接入中但未 stable)
- 改造工时——每个 MCP server 改造 40-80 工时(ID-JAG 验证 + 策略 + 审计)
- 协议年轻——EMA stable 才 1 个月,社区生态不成熟(多数 MCP server 还没接入)
- fallback 复杂——必须保留 per-user OAuth 作为 fallback,增加维护面
- 跨 Session 桥接——OpenRath Session 一等公民 + EMA 联动需要手动桥接(参考坑 8)
决策建议(按项目规模分档):
| 项目规模 | 推荐方案 | 理由 |
|---|---|---|
| 个人 / < 10 人 | per-user OAuth | 工时成本 < IdP 授权 + 改造成本 |
| 小团队(10-50 人) | per-user OAuth + IdP(任何) | 工时成本仍可控 |
| 中型企业(50-500 人) | MCP EMA + Okta XAA | ROI 平衡点 |
| 大型企业 / 金融 / 医疗(500+ 人) | MCP EMA + Okta Workforce + MCP server 全改造 | 合规是 hard requirement |
| 多 Agent 平台(OpenRath / LangGraph) | MCP EMA + Session 桥接 | 必须做跨 Session 状态共享 |
一周内可执行行动清单
按优先级排,每条都应该能在 1-2 小时内完成:
Day 1(盘点 + IdP 准备,4 小时):
- 跑
mcp-inventory.md盘点:列出所有 MCP server + 部署模式 + 授权模式 + 用户数 + 敏感数据 - 注册 Okta 开发者账号(developer.okta.com/signup)
- 创建 Org + 第一个 App(OIDC Web App)
- 在 Okta Admin Console 启用 Cross App Access(XAA)协议
- 跑通 Okta XAA Demo Flow(okta-cross-app-access-demo)
Day 2(MCP server 接入 EMA,4 小时):
- 部署 1 个 MCP server(Linear)self-host
- 实现
src/auth/ema.ts(ID-JAG 验证 + access token 签发 + IdP 策略) - 配 Linear API key
- 用
curl验证 EMA Token Exchange 流程
Day 3(VSCode IDE 启用 EMA,2 小时):
- 更新 VSCode 到 ≥ 1.95
- 配
.vscode/mcp.json启用 EMA - 在 VSCode Copilot Chat 调用 Linear MCP 工具
- 验证:无 OAuth 弹窗 + 重启 IDE 仍保持连接
Day 4(Claude Code CLI 启用 EMA,2 小时):
- 更新 Claude Code 到 ≥ 1.0.40
- 配
~/.claude/mcp_settings.json启用 Anthropic 共享 MCP 层 - 跑 Claude Code → Linear MCP 工具调用
- 验证:Claude Desktop / Cowork 同 org 自动共享授权
Day 5(扩展到 3 个 MCP server + group 策略,4 小时):
- 部署 Supabase MCP server + 接入 EMA
- 配 Figma MCP(官方 SaaS)+ EMA
- 在 Okta Admin Console 创建 3 个 group(linear / supabase-dba / figma)
- 配 group → resource 映射(linear → linear-mcp,supabase-dba → supabase-mcp + MFA,figma → figma-mcp)
- 验证:不同 group 只能调对应 MCP server
Day 6(安全审计 + 合规文档,4 小时):
- 跑
ema-security-audit.mdchecklist(身份验证 / 授权 / 审计 / 隔离 / Fallback) - 写
ema-compliance-doc.md(数据流 / 数据驻留 / 撤销流程 / 应急) - 跑回归测试(单元测试 + E2E 测试)
- 给 CISO 演示一遍:审计日志、撤销流程、IdP 降级
Day 7(路线图 + 文档化,2 小时):
- 写 30 天路线图(扩到 7 个 SaaS + 3 个 client)
- 写 90 天路线图(全组织推广 + ASI 路径 4 工程化基线)
- 产出一页纸备忘:
mcp-ema-cheatsheet.md(核心命令 + 关键决策点) - 给 VP/CISO/法务 walkthrough
Day 6-7 缓冲:
- 处理 Day 1-5 跑出来的真实问题(坑 1-8 任选)
- 准备 Day 8-14 的下一个 MCP server 改造(Asana / Atlassian 任选 1)
一句话总结: MCP EMA + Okta XAA + ID-JAG JWT + Claude/VSCode 双 client 一周跑通 = 把 MCP server 治理从「每用户手动接 OAuth」升级到「企业 IdP 一次授权、跨产品线共享、1 小时内自动撤销」——这是「Agent 工具链 = 企业 IT 一级资产」的工程化基线,也是 ASI 路径 4 多智能体协同(6/17 DeepMind 报告)的可治理前提。
本文为每日技术落地实战,所有命令和配置在 2026-06 基于 MCP EMA stable spec + Okta XAA protocol + Visual Studio Code 1.95+ / Claude Code 1.0.40+ 验证。