Skip to content

feat(server): middleware parity, auth, A2A parser hook, startup log#246

Merged
bokelley merged 1 commit intomainfrom
bokelley/server-middleware-parity
Apr 20, 2026
Merged

feat(server): middleware parity, auth, A2A parser hook, startup log#246
bokelley merged 1 commit intomainfrom
bokelley/server-middleware-parity

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

Second of three PRs on salesagent's feedback. Closes 5 items in one cohesive server-surface change.

Item What
#1 BearerTokenAuthMiddleware + Principal + auth_context_factory in adcp.server.auth. Seller supplies validate_token(token) -> Principal | None; framework owns ContextVars, discovery bypass, constant-time compare, reset-in-finally. Example shrinks 243 → 89 lines.
#3 A2A message_parser hook. New MessageParser type alias; kwarg on ADCPAgentExecutor, create_a2a_server, serve. Custom parsers can compose with _default_parse_request for mixed-legacy clients.
#7 MCP middleware parity with A2A. Same SkillMiddleware list works on both transports via a shared _dispatch_with_middleware helper.
#8 Docs for @mcp.tool() passthrough on create_mcp_server's return value (no code change — FastMCP is returned directly already).
#9 Startup log — "mcp server advertising X of Y tools" INFO, plus DEBUG listing of unadvertised names. Catches the "renamed a handler method, silently dropped from tools/list" incident pattern.

All additive; no breaking changes. Existing middleware= users on A2A see identical behavior (composition logic factored into shared helper, A2A delegates).

Tested

  • +45 new tests (15 auth middleware, 5 startup log, 3 MCP middleware composition, 5 A2A message parser, 17 covering happy-path + edge cases).
  • Full suite: 1980 passed, 22 skipped locally (1937 → 1980).
  • ruff, mypy clean.

Test plan

  • CI green across Python 3.10–3.13
  • Review: is auth_context_factory the right API shape, or should sellers subclass BearerTokenAuthMiddleware to customise what gets written into metadata?
  • Review: is it right that MCP middleware receives a synthesised default ToolContext when no context_factory is configured, or should it match A2A's pattern of falling through to a transport-derived context?
  • Review: is the _peek_jsonrpc batch-fails-closed behavior the right default? The alternative is to iterate batch members and apply the discovery gate per-entry — more permissive, more complex.

Related

Part of triaging feedback from salesagent (primary downstream of adcp.server).

🤖 Generated with Claude Code

bokelley added a commit that referenced this pull request Apr 20, 2026
Security review findings (HIGH/MEDIUM from security-reviewer):

- auth.py: _parse_bearer_header() replaces auth_header.removeprefix —
  case-insensitive scheme per RFC 7235, tolerates folded whitespace.
- auth.py: validator exceptions wrapped try/except → 401 with
  logger.exception. No stack-trace leak.
- auth.py/auth_context_factory: principal-supplied metadata can no
  longer shadow SDK-owned keys (audit-field-injection close).
- auth.py/_peek_jsonrpc: explicit request._body = body after peek.

Code-review findings:

- a2a_server.py: _log_advertised_tools moved from ADCPAgentExecutor
  __init__ to create_a2a_server (symmetric with MCP placement).
- tests/test_server_startup_log.py: regex-match instead of tokens[3].
- tests/test_a2a_server.py: replace inline _make_handler() with the
  module-level _TestHandler — fixes Python 3.10 CI failure.

+8 new tests. 1990 tests passing, mypy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gitguardian
Copy link
Copy Markdown

gitguardian Bot commented Apr 20, 2026

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Closes 5 items from salesagent's feedback on adopting adcp.server in
one cohesive server/transport surface change.

SkillMiddleware parity across transports (#7)
---------------------------------------------
The A2A executor's per-skill middleware (PR #233) is now available on
MCP too. Same SkillMiddleware type alias, same composition semantics
(outermost-first, _step recursion), same call_next contract — a
middleware list written against one transport works unchanged on the
other.

- src/adcp/server/serve.py: new module-level _dispatch_with_middleware
  that A2A's _dispatch_with_middleware delegates to.
- create_mcp_server, _register_handler_tools, _register_tool accept
  middleware=[SkillMiddleware]; _register_tool wraps caller in the
  chain between context build and handler invocation.
- serve() already exposed the kwarg for A2A; now forwards to MCP too.

BearerTokenAuthMiddleware in adcp.server.auth (#1)
--------------------------------------------------
The pattern in examples/mcp_with_auth_middleware.py was four
security-critical concerns (ContextVar carrier, constant-time compare,
discovery bypass, reset-in-finally); every downstream copy-pasted it.
Now shipped as a class.

- src/adcp/server/auth.py: BearerTokenAuthMiddleware, Principal
  (frozen dataclass), TokenValidator, auth_context_factory,
  constant_time_token_match. Seller supplies validate_token; framework
  owns the ContextVar plumbing, RFC 7235 scheme parsing (case-
  insensitive + whitespace-folded), discovery bypass, peek_jsonrpc
  with explicit request._body cache, fail-closed validator exception
  handling, principal metadata that can't shadow SDK audit keys.
- examples/mcp_with_auth_middleware.py shrunk 243 → 89 lines.

A2A message_parser hook (#3)
----------------------------
ADCPAgentExecutor._parse_request was hardcoded to
DataPart({'skill': ..., 'parameters': ...}). Sellers fronting JSON-RPC
or vendor-specific shapes had to subclass privately.

- src/adcp/server/a2a_server.py: new MessageParser type alias,
  message_parser= kwarg on ADCPAgentExecutor, create_a2a_server,
  _serve_a2a, serve(). Default = _default_parse_request (was inline).

Startup advertised-tools log (#9)
---------------------------------
- src/adcp/server/serve.py: _log_advertised_tools() runs from
  _register_handler_tools (MCP) and create_a2a_server (A2A).
  INFO: 'X of Y tools advertised'; DEBUG: list of unadvertised.

Custom tools doc (#8)
---------------------
docs/handler-authoring.md: new section covering the @mcp.tool()
passthrough on create_mcp_server's return value.

Expert-review followups (security + code review)
-------------------------------------------------
- _parse_bearer_header: case-insensitive scheme, folded whitespace.
- validator exceptions → 401 (no stack-trace leak).
- principal metadata can't shadow SDK-owned keys (tool_name,
  transport).
- explicit request._body = body after peek.
- tests use regex to match log messages (not positional tokens).
- Python 3.10 skipif on two new A2A create_a2a_server tests (a2a-sdk
  starlette integration requires 3.11+; matches pre-existing skip).

Tests
-----
+53 tests across three new/modified test files. 1990 tests passing,
mypy clean.

Closes #224, #225, #226, #240, #241 salesagent feedback items #1,
#3, #7, #8, #9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley force-pushed the bokelley/server-middleware-parity branch from ddc51ae to c9d7bbc Compare April 20, 2026 20:27
@bokelley bokelley merged commit 549d190 into main Apr 20, 2026
10 checks passed
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