Skip to content

feat(server): add mcpToolNameResolver helper for signature auth#916

Merged
bokelley merged 1 commit intomainfrom
bokelley/eval-pr-914
Apr 24, 2026
Merged

feat(server): add mcpToolNameResolver helper for signature auth#916
bokelley merged 1 commit intomainfrom
bokelley/eval-pr-914

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

  • Export mcpToolNameResolver from @adcp/client/server — a default resolveOperation callback for MCP agents wiring RFC 9421 signature auth. Parses req.rawBody as JSON-RPC and returns params.name when method === 'tools/call'.
  • Rewrite the four JSDoc examples in src/lib/server/auth-signature.ts to use the helper instead of copy-pasting the same 6-line parser.
  • Unit tests cover the happy path plus every undefined-returning case (non-tools/call method, missing params, non-string name, malformed JSON, missing/empty rawBody).

Motivation

The RFC 9421 signing composition helpers (verifySignatureAsAuthenticator, requireSignatureWhenPresent, requireAuthenticatedOrSigned) take a resolveOperation callback whose MCP implementation is identical across every seller agent:

resolveOperation: req => {
  try {
    const body = JSON.parse(req.rawBody ?? '');
    if (body.method === 'tools/call') return body.params?.name;
  } catch {}
  return undefined;
}

That block appears four times in auth-signature.ts JSDoc alone, plus once verbatim in test/auth-signature-compose.test.js, plus every time it's pasted into a new adapter. Ships as a one-line import instead:

resolveOperation: mcpToolNameResolver,

A2A agents use a different envelope and still need a bespoke resolver — the helper is explicitly MCP.

Follow-up to #914 (docs-only signing guide): once this lands I'll propose a rewrite of Step 4 of SIGNING-GUIDE.md using requireAuthenticatedOrSigned + mcpToolNameResolver, collapsing the ~25-line manual composition to ~8 lines.

Test plan

  • New tests: node --test test/lib/mcp-tool-name-resolver.test.js (7/7 pass locally)
  • Regression: existing auth-signature-compose.test.js still green (37/37)
  • npm run build clean
  • tsc --noEmit clean (ran in pre-push)

🤖 Generated with Claude Code

The RFC 9421 signing composition helpers (`verifySignatureAsAuthenticator`,
`requireSignatureWhenPresent`, `requireAuthenticatedOrSigned`) all take a
`resolveOperation` callback that, for MCP agents, parses `req.rawBody` as
JSON-RPC and returns `params.name` when `method === 'tools/call'`. Every
adapter ships the same 6-line parser; the SDK JSDoc inlines it four
separate times.

Export `mcpToolNameResolver` so adapter authors pass `resolveOperation:
mcpToolNameResolver` instead of copy-pasting the parser. A2A agents use a
different envelope — bespoke resolvers still apply there.

JSDoc examples in `auth-signature.ts` are rewritten to use the helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit b74a9ce into main Apr 24, 2026
9 checks passed
bokelley added a commit that referenced this pull request Apr 24, 2026
…edOrSigned

Apply the review feedback on #914 against the SDK ergonomics
shipped in #916 (mcpToolNameResolver) and #917 (defaults +
createAgentSignedFetch preset):

SIGNING-GUIDE.md
- Step 3 (buyer-side): lead with createAgentSignedFetch one-call
  preset for the single-seller case; keep buildAgentSigningFetch
  as the multi-seller fallback. Drops the verbose CapabilityCache
  wiring from the headline example.
- Step 4 (seller-side, Express middleware): drop explicit
  InMemoryReplayStore / InMemoryRevocationStore (now default
  per #917). Use mcpToolNameResolver instead of the inlined
  JSON-RPC parser. Add a one-liner about swapping in shared
  stores for horizontally scaled fleets.
- Step 4 (composing with bearer auth): replace the three-variable
  manual composition (signatureAuth / bearerAuth /
  requireSignatureWhenPresent) with requireAuthenticatedOrSigned
  in one call. Now also enforces the spec's
  request_signature_required 401 via requiredFor: [...MUTATING_TASKS]
  — previously absent from the example.

BUILD-AN-AGENT.md
- Mirror the requireAuthenticatedOrSigned + mcpToolNameResolver +
  MUTATING_TASKS pattern in the Request Signing snippet so the
  two guides agree.

Functional surface unchanged; the manual composition still works
for callers that need finer control. The recommended path is just
shorter.
bokelley added a commit that referenced this pull request Apr 25, 2026
* docs: add request signing guide and surface it from README + BUILD-AN-AGENT

Adds docs/guides/SIGNING-GUIDE.md covering the full RFC 9421 signing
workflow: key generation, JWKS/brand.json publication, client-side
signing, server-side verification, webhook signing, capability
declaration, key rotation, and conformance testing.

Updates README.md to link the guide from the AI Agents section and
rewrites the Security > Request Signing section with better orientation.

Adds a Request Signing section to BUILD-AN-AGENT.md showing
requireSignatureWhenPresent composition and webhook signer config.

* chore: add empty changeset for docs-only PR

* docs(signing): collapse manual auth composition to requireAuthenticatedOrSigned

Apply the review feedback on #914 against the SDK ergonomics
shipped in #916 (mcpToolNameResolver) and #917 (defaults +
createAgentSignedFetch preset):

SIGNING-GUIDE.md
- Step 3 (buyer-side): lead with createAgentSignedFetch one-call
  preset for the single-seller case; keep buildAgentSigningFetch
  as the multi-seller fallback. Drops the verbose CapabilityCache
  wiring from the headline example.
- Step 4 (seller-side, Express middleware): drop explicit
  InMemoryReplayStore / InMemoryRevocationStore (now default
  per #917). Use mcpToolNameResolver instead of the inlined
  JSON-RPC parser. Add a one-liner about swapping in shared
  stores for horizontally scaled fleets.
- Step 4 (composing with bearer auth): replace the three-variable
  manual composition (signatureAuth / bearerAuth /
  requireSignatureWhenPresent) with requireAuthenticatedOrSigned
  in one call. Now also enforces the spec's
  request_signature_required 401 via requiredFor: [...MUTATING_TASKS]
  — previously absent from the example.

BUILD-AN-AGENT.md
- Mirror the requireAuthenticatedOrSigned + mcpToolNameResolver +
  MUTATING_TASKS pattern in the Request Signing snippet so the
  two guides agree.

Functional surface unchanged; the manual composition still works
for callers that need finer control. The recommended path is just
shorter.

* fix(docs+signing): apply expert review on signing guide

Six must-fix items raised by code-reviewer + ad-tech-protocol-expert
on the post-polish state:

1. MUTATING_TASKS imports were wrong — only @adcp/client (root)
   exports it, not @adcp/client/server. Buyer copies would have
   compile-errored. Split the imports.

2. Capability override key is `request_signing`, not
   `signed_requests`. Wrong key gets silently dropped — verifier
   wires up but get_adcp_capabilities advertises nothing, so
   buyers don't sign. Fixed in both guides; add a clarifying
   sentence so the trap is visible.

3. Widen `mcpToolNameResolver` parameter type from
   `IncomingMessage & { rawBody?: string }` to `{ rawBody?: string }`.
   Function only reads rawBody, so widening lets it satisfy both
   the `verifySignatureAsAuthenticator` (IncomingMessage-shaped)
   and `createExpressVerifier` (ExpressLike-shaped) call sites
   without casts. No runtime change.

4. Error code table was wrong — `missing_signature` /
   `invalid_signature` etc. don't exist. The real codes
   (cross-checked against compliance/cache/3.0.0/test-vectors/
   request-signing/negative/) are `request_signature_*`. Replace
   the seven-row table with the full 15-row table from the vectors
   and note that they're a separate signature-error namespace
   surfaced via WWW-Authenticate (not entries in
   enums/error-code.json).

5. Signature coverage misstated. content-digest is conditional
   on covers_content_digest, not always covered. Reword.

6. brand.json shape was wrong. agents[] requires {type, url, id};
   `capabilities` and `adcp_use` arrays don't exist in
   schemas/cache/3.0.0/brand.json. Fix the example, list the
   `type` enum.

Also reframed the "mandatory in 3.1+" claim to "4.0,
spend-committing operations" per
schemas/cache/3.0.0/protocol/get-adcp-capabilities-response.json,
and softened `requiredFor: [...MUTATING_TASKS]` to a narrow
example with MUTATING_TASKS as the upper bound (the spec stance
in 3.0 is "empty by default; populate selectively per
counterparty").

Conformance vector counts (12 positive + 27 negative = 39)
verified against the disk layout.

---------

Co-authored-by: Brian O'Kelley <bokelley@scope3.com>
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