📋 Prerequisites
📝 Feature Summary
When a supervisor agent dispatches to a sub-agent via type: Agent, the _SubagentInterceptor should forward the Authorization header from the parent agent's session.state["headers"], so that the sub-agent's downstream MCP calls can propagate the caller's JWT end-to-end.
❓ Problem Statement / Motivation
When using allowedHeaders: [Authorization] on Agent CRs, the user's JWT propagates correctly from the incoming A2A request to outbound MCP calls — but only for direct agent invocations. In multi-agent setups where a coordinator (supervisor) dispatches to sub-agents via type: Agent, the JWT is silently dropped at the A2A hop.
_SubagentInterceptor (_remote_a2a_tool.py:63-77) currently only forwards two headers:
x-kagent-source: agent
x-user-id (from context.state)
The Authorization header stored in context.state["headers"] (populated by _agent_executor.py and consumed by create_header_provider in types.py) is not forwarded to the sub-agent.
This means:
| Path |
JWT at MCP backend? |
| Browser → agent → MCP |
✅ Yes (allowedHeaders works) |
| Browser → coordinator → sub-agent → MCP |
❌ No (JWT lost at coordinator → sub-agent hop) |
💡 Proposed Solution
The fix requires two changes — the interceptor alone isn't enough because the headers are never passed into the ClientCallContext it receives.
Change 1: Pass session headers into ClientCallContext.state
In _handle_first_call() (line 242) and _handle_resume() (line 384), the ClientCallContext is constructed with only x-user-id:
# Current — headers from session.state are not included
call_context = ClientCallContext(state={_USER_ID_CONTEXT_KEY: tool_context.session.user_id})
The session headers (populated by _agent_executor.py from the incoming request) need to be forwarded:
# Proposed — include session headers so the interceptor can forward them
ctx_state = {_USER_ID_CONTEXT_KEY: tool_context.session.user_id}
session_headers = tool_context.session.state.get("headers", {})
if session_headers:
ctx_state["headers"] = session_headers
call_context = ClientCallContext(state=ctx_state)
This must be applied in both _handle_first_call() (line 242) and _handle_resume() (line 384).
Change 2: Forward Authorization in _SubagentInterceptor
With headers now available in context.state, the interceptor can forward them:
class _SubagentInterceptor(ClientCallInterceptor):
async def intercept(self, method_name, request_payload, http_kwargs, agent_card, context):
headers = dict(http_kwargs.get("headers", {}))
headers[_SOURCE_HEADER] = _SOURCE_SUBAGENT
if context:
if _USER_ID_CONTEXT_KEY in context.state:
headers["x-user-id"] = context.state[_USER_ID_CONTEXT_KEY]
# Forward Authorization from the parent agent's incoming request
# so sub-agents can propagate it to downstream MCP backends
request_headers = context.state.get("headers", {})
for key, value in request_headers.items():
if key.lower() == "authorization":
headers[key] = value
break
http_kwargs["headers"] = headers
return request_payload, http_kwargs
🔄 Alternatives Considered
No response
🎯 Affected Service(s)
None
📚 Additional Context
background of our setup: How coordinator → sub-agent dispatch works
In kagent's multi-agent setup, agents are deployed as individual pods behind agentgateway, which acts as both an A2A (agent-to-agent) and MCP (model-context-protocol) gateway:
┌─────────┐ A2A (8080) ┌──────────────────┐ A2A (8080) ┌─────────────┐ MCP (9090) ┌─────────────┐
│ Browser │ ──────────────► │ agentgateway │ ──────────────► │ sub-agent │ ──────────────► │ MCP backend │
│ (+ JWT) │ │ (A2A listener) │ │ (kagent) │ │ (toolserver)│
└─────────┘ └──────────────────┘ └─────────────┘ └─────────────┘
│ │
│ routes by agent name │ MCP calls via agentgateway
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ coordinator │ │ agentgateway │
│ agent (kagent) │ │ (MCP listener) │
└──────────────────┘ └──────────────────┘
The coordinator → sub-agent hop happens when:
- A request arrives at a coordinator agent (via agentgateway A2A listener on port 8080)
- The coordinator's LLM decides to invoke a tool of
type: Agent (a sub-agent reference)
_SubagentInterceptor builds the outbound HTTP request to the sub-agent's A2A endpoint — this is where the Authorization header is dropped
- The sub-agent receives the request without the JWT, processes it, and calls MCP backends via agentgateway's MCP listener (port 9090)
- The MCP backend never sees the original user's JWT
When agentgateway is configured with JWT authentication on the MCP listener, it can validate/enforce identity on MCP calls — but only if the JWT survives the full chain. The coordinator → sub-agent A2A hop is the gap.
Additional Context
create_header_provider() in types.py:36 already reads from session.state["headers"] and forwards Authorization when listed in allowedHeaders — but this only applies to the agent → MCP path, not the coordinator → sub-agent A2A path.
- When STS is configured, the STS-provided token should still take precedence on the sub-agent side (same security model as
create_header_provider).
- Verified on kagent v0.9.0 with agentgateway v1.1.0. Tested by sending a JWT via curl to a coordinator-agent → sub-agent → MCP chain. agentgateway logs confirm
jwt.sub is present on direct calls and absent on coordinator-dispatched calls.
🙋 Are you willing to contribute?
📋 Prerequisites
📝 Feature Summary
When a supervisor agent dispatches to a sub-agent via
type: Agent, the_SubagentInterceptorshould forward theAuthorizationheader from the parent agent'ssession.state["headers"], so that the sub-agent's downstream MCP calls can propagate the caller's JWT end-to-end.❓ Problem Statement / Motivation
When using
allowedHeaders: [Authorization]on Agent CRs, the user's JWT propagates correctly from the incoming A2A request to outbound MCP calls — but only for direct agent invocations. In multi-agent setups where a coordinator (supervisor) dispatches to sub-agents viatype: Agent, the JWT is silently dropped at the A2A hop._SubagentInterceptor(_remote_a2a_tool.py:63-77) currently only forwards two headers:x-kagent-source: agentx-user-id(fromcontext.state)The
Authorizationheader stored incontext.state["headers"](populated by_agent_executor.pyand consumed bycreate_header_providerintypes.py) is not forwarded to the sub-agent.This means:
allowedHeadersworks)💡 Proposed Solution
The fix requires two changes — the interceptor alone isn't enough because the headers are never passed into the
ClientCallContextit receives.Change 1: Pass session headers into
ClientCallContext.stateIn
_handle_first_call()(line 242) and_handle_resume()(line 384), theClientCallContextis constructed with onlyx-user-id:The session headers (populated by
_agent_executor.pyfrom the incoming request) need to be forwarded:This must be applied in both
_handle_first_call()(line 242) and_handle_resume()(line 384).Change 2: Forward Authorization in
_SubagentInterceptorWith headers now available in
context.state, the interceptor can forward them:🔄 Alternatives Considered
No response
🎯 Affected Service(s)
None
📚 Additional Context
background of our setup: How coordinator → sub-agent dispatch works
In kagent's multi-agent setup, agents are deployed as individual pods behind agentgateway, which acts as both an A2A (agent-to-agent) and MCP (model-context-protocol) gateway:
The coordinator → sub-agent hop happens when:
type: Agent(a sub-agent reference)_SubagentInterceptorbuilds the outbound HTTP request to the sub-agent's A2A endpoint — this is where the Authorization header is droppedWhen agentgateway is configured with JWT authentication on the MCP listener, it can validate/enforce identity on MCP calls — but only if the JWT survives the full chain. The coordinator → sub-agent A2A hop is the gap.
Additional Context
create_header_provider()intypes.py:36already reads fromsession.state["headers"]and forwardsAuthorizationwhen listed inallowedHeaders— but this only applies to the agent → MCP path, not the coordinator → sub-agent A2A path.create_header_provider).jwt.subis present on direct calls and absent on coordinator-dispatched calls.🙋 Are you willing to contribute?