Skip to content

feat(mcp): JWKS 端点 + 身份注入迁移到 _meta(#459 follow-up) - #541

Open
ncw1992120 wants to merge 4 commits into
mateaix:devfrom
ncw1992120:feat/mcp-jwks-and-meta
Open

feat(mcp): JWKS 端点 + 身份注入迁移到 _meta(#459 follow-up)#541
ncw1992120 wants to merge 4 commits into
mateaix:devfrom
ncw1992120:feat/mcp-jwks-and-meta

Conversation

@ncw1992120

Copy link
Copy Markdown
Contributor

概述

ISSUE #459 的两个 follow-up:(1) JWKS 端点实现公钥自动分发;(2) 身份注入从 tool args 迁移到 MCP _meta 字段。

Feature 1: JWKS 端点(公钥自动分发)

REST 后端现在可以通过标准 JWKS URL 获取 RSA 公钥来验签 identity token,不再需要带外配置。

  • 新端点GET /api/v1/mcp/.well-known/jwks.json(公开,无鉴权)
  • 公钥推导:从已解析的 RSAPrivateCrtKey(modulus + public exponent)直接推导,无需单独配置 publicKeyPem
  • 标准 JWKS 格式{"keys":[{"kty":"RSA","use":"sig","alg":"RS256","kid":"...","n":"base64url","e":"base64url"}]}
  • SecurityConfig:将端点加入 permitAll(公钥是公开信息)

Feature 2: 身份注入从 args 迁移到 _meta

身份(明文 + token)从 tool arguments JSON 迁移到 MCP _meta 字段(tools/call 的调用方控制通道)。语义更干净,且彻底消除伪造风险——模型无法写入 _meta

⚠️ 对 MCP server 实现者的 Breaking Change

  • :从 args 读 __mateclaw_user__ / __mateclaw_token__
  • :从 _metamateclaw_user / mateclaw_token(通过 FastMCP 的 Context.meta 或 SDK 等价物)

实现

  • McpIdentityForwardService:新增 resolveMeta() 返回 Map<String,String>_meta 使用;移除旧的 resolve()/Injection(args-based API);新增 publicKeyJwks()
  • IdentityForwardingToolCallback:简化为类型标记 + 身份提供者;移除 args JSON 合并(inject/withClaim);新增 metaInjection() 返回 _meta key-value map
  • ProgressAwareMcpToolCallback:直接 callTool 路径现在在 progress token 身份转发激活时触发(两者都需要 _meta)。将 progress + 身份合并到一个 _meta map 中。args 是模型原文(身份不再在 args 中)
  • McpIdentityForwardProperties:新增 META_USER_KEY/META_TOKEN_KEY 常量;旧 USER_ARG/TOKEN_ARG 标记 @Deprecated 供迁移参考

测试

  • McpIdentityForwardServiceTest:JWKS 验签往返(mint token → 从 JWKS 推导公钥 → 验签)、token 禁用/密钥错误时 JWKS 为空
  • IdentityForwardingToolCallbackTest:重写为 _meta API 测试
  • ProgressAwareMcpToolCallbackTest:新增 _meta 身份注入测试,用 ArgumentCaptor<CallToolRequest> 断言 _meta 内容;progress + 身份合并;fallback 丢失身份(记录日志)
  • 145 个 MCP + context 测试全绿

文档

更新 zh/mcp.md + en/mcp.md_meta 迁移说明、Python Context.meta 示例、JWKS 端点 + PyJWKClient 验签示例。

…follow-up)

Two follow-up features for the MCP identity-forward (mateaix#459):

## Feature 1: JWKS endpoint for public key distribution

REST backends can now verify identity tokens by fetching the RSA public key
from a well-known JWKS URL instead of out-of-band PEM config.

- New endpoint: GET /api/v1/mcp/.well-known/jwks.json (public, no auth)
- McpIdentityForwardService.publicKeyJwks() derives the public key from the
  parsed RSAPrivateCrtKey (modulus + public exponent) — no separate
  publicKeyPem config needed.
- Returns standard JWKS: {"keys":[{"kty":"RSA","use":"sig","alg":"RS256",
  "kid":"...","n":"base64url","e":"base64url"}]}
- SecurityConfig: added the endpoint to permitAll (public key is public).

## Feature 2: Identity injection moved from tool args to MCP _meta

Identity (plaintext + token) now travels in the MCP _meta field (the
caller-controlled channel on tools/call) instead of the tool arguments JSON
(the model-controlled channel). This is semantically cleaner and removes
spoof risk entirely — the model cannot author _meta.

### Breaking change for MCP server implementers
- OLD: read identity from args `__mateclaw_user__` / `__mateclaw_token__`
- NEW: read identity from `_meta` `mateclaw_user` / `mateclaw_token`
  (via the MCP Context.meta in FastMCP / SDK equivalents)

### Implementation
- McpIdentityForwardService: new resolveMeta() returns Map<String,String>
  for _meta; removed old resolve()/Injection (args-based API).
- IdentityForwardingToolCallback: simplified to a type marker + identity
  provider; removed args JSON merge (inject/withClaim); new metaInjection()
  returns the _meta key-value map.
- ProgressAwareMcpToolCallback: direct callTool path now triggers on
  progress token OR identity forwarding (both need _meta). Merges progress
  + identity into one _meta map. Args are model-verbatim (identity is NOT
  in args anymore).
- McpIdentityForwardProperties: new META_USER_KEY/META_TOKEN_KEY constants;
  old USER_ARG/TOKEN_ARG kept @deprecated for migration reference.

## Tests
- McpIdentityForwardServiceTest: JWKS verification round-trip (mint token →
  derive public key from JWKS → verify), JWKS empty when token disabled/bad.
- IdentityForwardingToolCallbackTest: rewritten for _meta API.
- ProgressAwareMcpToolCallbackTest: new _meta identity injection tests with
  ArgumentCaptor<CallToolRequest> asserting _meta contents; progress +
  identity merge; fallback drops identity (logged).
- 145 MCP + context tests pass.

## Docs
- Updated zh/en mcp.md: _meta migration, Python Context.meta examples,
  JWKS endpoint + PyJWKClient verification example.
- CRITICAL: RFC 7518 violation in JWK n/e encoding — BigInteger.toByteArray()
  prepends a 0x00 sign byte for positive numbers with the high bit set (every
  RSA modulus), producing a 256× larger modulus to strict JWT parsers (PyJWT,
  java-jwt). Fixed base64UrlEncode() to strip the leading zero. Test hardened
  to assert byte length = ceil(bitLength/8) and modulus equality without the
  BigInteger(1,...) signum crutch.

- MEDIUM: signingKey() re-logged ERROR on every call when PEM was blank/null —
  lastAttemptedPem was set to null, so the fast-path guard
  (lastAttemptedPem != null) never short-circuited. Added a parseAttempted
  boolean to dedup regardless of PEM nullability.

- MEDIUM: JWKS endpoint lacked Cache-Control header. Added
  Cache-Control: public, max-age=300 + documented kid-on-rotation requirement.

- LOW: non-CRT signing key silently returned empty JWKS with no log. Added a
  warning when the key is non-null but not RSAPrivateCrtKey.
…teaix#541 R2)

Round-2 review found the round-1 parseAttempted fix (preventing repeated
ERROR logging on blank/null PEM) had no regression test. Added a
ListAppender-based test that calls resolveMeta 3× with a blank PEM and
asserts the "private-key-pem is empty" ERROR fires exactly once, not per
call. Guards against a refactor removing the dedup thinking it's redundant.
Round-3 review found the docs' Python examples used a non-existent FastMCP
API: `ctx.meta` doesn't exist on the official mcp SDK's Context class, and
the meta value is a Pydantic model (not a dict), so `.get()` crashes with
AttributeError. Every server implementer copying the example would hit a
runtime crash on the first identity-forwarded call.

Fixed all 4 code blocks (plaintext + token, en + zh) to use the correct
pattern: `ctx.request_context.meta` + `getattr(raw_meta, "mateclaw_user", None)`.

Verified: wrapping chain, instanceof detection, audience freshness,
no-context overload safety, SecurityConfig ordering, RFC 7518 encoding — all
correct per round-3 review.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant