feat(mcp): agent-auth consumer — assertion signing + CIMD publish (flair#623-adjacent / #159 consumer)#663
Draft
tps-flint wants to merge 2 commits into
Draft
feat(mcp): agent-auth consumer — assertion signing + CIMD publish (flair#623-adjacent / #159 consumer)#663tps-flint wants to merge 2 commits into
tps-flint wants to merge 2 commits into
Conversation
Flair-side consumer for headless agent-auth to a Harper MCP /mcp endpoint (RFC 7523 client_credentials + private_key_jwt), built against the ACTUAL plugin contracts read from HarperFast/oauth PR #165 (clientAssertion.ts, merged @ d48c3b2) and PR #167 (CIMD, merged @ f0da8a1) — not guessed. - src/mcp-client-assertion.ts + `flair mcp token`: builds and signs the RFC 7523 client_assertion JWT (EdDSA, iss=sub=client_id, aud=token endpoint, exp-iat <=60s hard-capped, jti) using the agent's existing Ed25519 key. Key loading mirrors src/cli.ts's existing resolveKeyPath/buildEd25519Auth exactly (same search order, same format cascade) — no new key material, no new dependency. - resources/MCPClientMetadata.ts + mcp-client-metadata-fields.ts: serves each agent's Client ID Metadata Document at GET /MCPClientMetadata/{id} (mirrors AgentCard.ts's public-discovery posture), JWKS = the agent's existing Ed25519 public key as a JWK OKP. - The token-endpoint POST is intentionally stubbed (requestMcpAccessToken always throws, marked `pending #162`) — that grant is an open ISSUE upstream, not yet a PR, so its contract isn't final. Tests mirror the actual plugin verification/validation contracts (read from the PR diffs) rather than reimplementing them for production use. 35 new unit tests, full suite green (1971 pass), CLI verified end-to-end with a throwaway test key. See docs/notes/mcp-agent-auth-consumer.md for what's built, what's stubbed pending #162, and open contract questions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| jwk = agentPublicKeyToJwk(String(agent.publicKey), agentId); | ||
| } catch (err: any) { | ||
| return new Response( | ||
| JSON.stringify({ error: "invalid_agent_key", message: err?.message ?? String(err) }), |
…#167 claim oauth#161 ("client_credentials (2/4): CIMD-first client resolution") and #162 ("client_credentials (3/4): token-endpoint grant") landed as formal, re-scoped issue specs since #663 was opened. Tighten this consumer to match them exactly and correct a stale inaccuracy that had crept into the code: - Fix "PR #167 merged @ commit f0da8a1" claims across mcp-client-metadata-fields.ts, MCPClientMetadata.ts, src/cli.ts, and both test files — #167 is an open draft PR, not merged. Re-cite #161 as the formal CIMD shape spec our document targets. - buildCimdDocument: add a defensive runtime guard rejecting any jwk carrying a private `d` component (oauth#161's "reject any key carrying private d"), belt-and-suspenders since the JWK crosses a runtime boundary even though Ed25519Jwk's TS type has no `d` field. Document that response_types (like redirect_uris) is deliberately never emitted for client_credentials-only CIMD clients — #161 blesses both omissions explicitly now, not just our own hedge. - Confirm + document that buildTokenRequestForm's RFC 8707 `resource` pass-through (already wired via defaultMcpResource()/flair mcp token's CLI defaulting) matches #162's request-side scope; add a cross-check test proving the form's client_id equals the assertion's iss/sub. - docs/notes/mcp-agent-auth-consumer.md: rewrite to the current oauth state (#160/PR165 merged, #161 open, #162 open issue, #167 open draft), resolve the former "open questions" section using #161's answers (redirect_uris/response_types omission is now blessed; the allowedHosts gate replaces the old DCR initialAccessToken gate), and add a deployment coordination note for AS operators. - Tests: 5 new assertions (private-d rejection, response_types omission, non-empty/single-key jwks, form/assertion client_id cross-check, client_assertion_type exact value). Full suite: 1976 pass / 0 fail. Still stubbed pending #162 landing: the live token round-trip (requestMcpAccessToken), end-to-end tests against a real Harper, and verifying the AS's actual discovery-metadata additions. PR stays draft. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Dependency fully published: @harperfast/oauth@2.2.0 is on npm (2026-07-12T00:07Z) and contains the complete #159 train — #170 (client_credentials + RFC7523 private_key_jwt, the shape this issue consumes) and #171 (issuance + CIMD rate limiting). Nothing upstream blocks this issue anymore. Two consumer requirements the 2.2.0 rate limiter (#171) adds to this client, fold into the spec:
🤖 Generated with Claude Code |
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.
Summary
Flair-side consumer for headless agent-auth to a Harper MCP
/mcpendpoint (RFC 7523client_credentials+private_key_jwt). Built against the actual plugin contracts read directly from HarperFast/oauth's merged PRs — not guessed. Two of the three pieces are fully built + tested; the third (the live token round-trip) is a deliberate, clearly-marked stub because its upstream contract isn't final yet.Design docs:
~/ops/FLAIR-AGENT-AUTH-CONSUMER-SPEC.md,~/ops/FLAIR-CLOUD-AGENT-BETA-ALIGNMENT.md. Full writeup:docs/notes/mcp-agent-auth-consumer.md(added in this PR).What's built + tested
1. Assertion signing —
flair mcp token(src/mcp-client-assertion.ts)client_assertionJWT: header{alg:"EdDSA", typ:"JWT"}, claimsiss=sub=client_id,aud=token endpoint,exp-iat <= 60s(hard-capped, not just defaulted),iat, randomjti. Signed withnode:cryptoalone — no new dependency, matching this repo's existing signing style (flair-client.mjs,src/cli.ts'sbuildEd25519Auth).src/lib/mcp/clientAssertion.ts, merged @d48c3b2) verifies — read directly from the PR diff.test/unit/mcp-client-assertion.test.tsincludes a mirror of Hash-based embedding fallback provides no semantic quality #165's verification (alg pinning, iss/sub/aud exact match, exp/iat window, jti presence, Ed25519 signature check) and proves our assertions pass it, plus negative cases: tampered payload, wrongaud, wrong signing key, expired assertion.resolveAgentKeyPath/loadEd25519PrivateKeyFromFile) mirrorssrc/cli.ts's existingresolveKeyPath/buildEd25519Authexactly — same search order (--keys-dir/FLAIR_KEY_DIR/~/.flair/keys/~/.tps/secrets/flair), same format cascade (raw seed, base64 seed, base64 PKCS8 DER, PEM fallback). No new key material.2. CIMD publish —
resources/MCPClientMetadata.tsGET /MCPClientMetadata/{agentId}(public, unauthenticated — mirrorsAgentCard.ts's A2A-card posture).client_id= that URL itself;jwks= the agent's existingAgent.publicKeyre-expressed as a JWK OKP.resources/mcp-client-metadata-fields.ts) is Harper-free and tested against a mirror of HarperFast/oauth PR chore: upgrade Harper to 5.0.0-beta.6 #167'svalidateCimdDocument(src/lib/mcp/cimd.ts, merged @f0da8a1).What's stubbed — pending #162
requestMcpAccessTokenalways throws, clearly markedpending #162. The token-endpoint grant (grant_type=client_credentials) is tracked as HarperFast/oauth issue #162 — an open issue, not yet a PR, so its exact request/response contract isn't final.buildTokenRequestForm(pure, tested) shows the shape the design docs call for; wiring a live POST now would mean guessing and re-doing it once #162 lands, so the seam is left explicit instead.Also not in this slice (deferred, see docs/notes):
flair agent register-mcp-client(the admin-gated Credential-writing command from the consumer spec) — its shape depends on open questions below — and the end-to-end/negative-path tests against a real spawned Harper (needs the live grant to exist first).Contract questions for #162 (read directly against #167's merged code, not guessed)
Our CIMD document intentionally targets the shape our agents actually need (
grant_types: ["client_credentials"],token_endpoint_auth_method: "private_key_jwt"), which today's merged #167 validator does not yet accept:clientValidator.ts'sSUPPORTED_GRANT_TYPESis{authorization_code, refresh_token}— willclient_credentialsbe added as part of docs: add custom ports note to README #162, or a separate follow-up?cimd.tshardcodes CIMD clients totoken_endpoint_auth_method === 'none'(its own comment saysprivate_key_jwtactivates with Add test coverage for CLI commands #159) — same question.redirect_urisis required + non-empty today (inherited from the DCR-shaped validator) — meaningless for a pureclient_credentialsagent that never does a redirect flow. We deliberately omitted it rather than invent a placeholder (which could get silently accepted onto an unintended surface if a future validator loosens without dropping this requirement) — will it become optional forprivate_key_jwt+client_credentials-only CIMD clients?Fetching our document against today's deployed AS fails closed (400, missing/invalid field) rather than silently degrading to a weaker auth method — intentional interim behavior, not a bug.
Test plan
bun test test/unit/mcp-client-assertion.test.ts test/unit/mcp-client-metadata-fields.test.ts— 35 new tests, all passbun test test/unit— full suite, 1971 pass / 0 fail (no regressions)tsc -p tsconfig.cli.json --noEmitandtsc -p tsconfig.json --noEmit— clean (one pre-existing, unrelated error inauth-middleware.tsconfirmed present onorigin/mainbefore this change)npm run build && npm run build:cli— production build succeeds, emitsdist/mcp-client-assertion.js,dist/resources/MCPClientMetadata.js,dist/resources/mcp-client-metadata-fields.jsflair mcp tokenwith a throwaway Ed25519 test key — signs, prints the assertion + claims, correctly derives--client-id/--token-endpoint/--resourcedefaults fromFLAIR_MCP_ISSUER, and fails closed with clear errors when the key or issuer config is missing🤖 Generated with Claude Code
Co-Authored-By: Claude Fable 5 noreply@anthropic.com