feat(mcp): JWKS 端点 + 身份注入迁移到 _meta(#459 follow-up) - #541
Open
ncw1992120 wants to merge 4 commits into
Open
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
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{"keys":[{"kty":"RSA","use":"sig","alg":"RS256","kid":"...","n":"base64url","e":"base64url"}]}permitAll(公钥是公开信息)Feature 2: 身份注入从 args 迁移到
_meta身份(明文 + token)从 tool arguments JSON 迁移到 MCP
_meta字段(tools/call的调用方控制通道)。语义更干净,且彻底消除伪造风险——模型无法写入_meta。__mateclaw_user__/__mateclaw_token___meta读mateclaw_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()返回_metakey-value mapProgressAwareMcpToolCallback:直接callTool路径现在在 progress token 或身份转发激活时触发(两者都需要_meta)。将 progress + 身份合并到一个_metamap 中。args 是模型原文(身份不再在 args 中)McpIdentityForwardProperties:新增META_USER_KEY/META_TOKEN_KEY常量;旧USER_ARG/TOKEN_ARG标记@Deprecated供迁移参考测试
McpIdentityForwardServiceTest:JWKS 验签往返(mint token → 从 JWKS 推导公钥 → 验签)、token 禁用/密钥错误时 JWKS 为空IdentityForwardingToolCallbackTest:重写为_metaAPI 测试ProgressAwareMcpToolCallbackTest:新增_meta身份注入测试,用ArgumentCaptor<CallToolRequest>断言_meta内容;progress + 身份合并;fallback 丢失身份(记录日志)文档
更新
zh/mcp.md+en/mcp.md:_meta迁移说明、PythonContext.meta示例、JWKS 端点 +PyJWKClient验签示例。