feat(mcp): Add session state-based JWT token propagation for MCP tools#3675
feat(mcp): Add session state-based JWT token propagation for MCP tools#3675timof1308 wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a secure and flexible mechanism for propagating ephemeral data, like JWT tokens, from the client to MCP tools. The use of a non-persistent request_state that overrides the session state is a great design for handling sensitive, short-lived data. The addition of header_provider and declarative configuration options (state_header_mapping, state_header_format) in McpToolset makes this feature powerful and easy to use. The code is well-structured and thoroughly tested. My review includes a couple of suggestions to enhance the robustness of the new header provider logic by adding warnings for when non-primitive data types are used from the state, which could prevent silent misconfigurations.
There was a problem hiding this comment.
Code Review
This pull request introduces a secure mechanism for propagating ephemeral, request-specific data like JWT tokens to MCP tools. The addition of request_state to the InvocationContext and its integration into the ReadonlyContext is a clean solution to avoid persisting sensitive data. The new configuration options state_header_mapping and state_header_format in McpToolset provide a flexible, declarative way to manage headers. The changes are well-tested with new unit tests that cover the new functionality thoroughly. I have one suggestion to improve maintainability by reducing code duplication.
|
+1 |
c6d2245 to
d211763
Compare
|
Hi @timof1308 , Thank you for your work on this pull request. We appreciate the effort you've invested. |
|
thank you @ryanaiagent the CI is now pending on the latest commit (d0c7829). Once the workflow finishes, all tests should pass. Let me know if anything else is needed! |
|
+1 |
1 similar comment
|
+1 |
|
Hi @timof1308 ,This PR now has merge conflicts that require changes from your end. Could you please rebase your branch with the latest main branch to address these? Once this is complete, please let us know so we can proceed with the review. |
d0c7829 to
f5e20b0
Compare
|
hi @ryanaiagent, rebased with upstream main and reapplied |
02be37c to
6de52f9
Compare
02be37c to
087ee78
Compare
94038a3 to
fc287ca
Compare
381f70f to
6e513d2
Compare
e24335e to
d7f1652
Compare
|
Hi @timof1308 , can you fix the failing unit tests |
|
hi @ryanaiagent, I've pushed a fix for the unit tests The failures in however I noticed other failures in the logs (related to pubsub and litellm), but those appear to be present in the upstream adk-python repo |
5bf9574 to
038a129
Compare
289687e to
5c7d479
Compare
|
@ryanaiagent file formatting has been applied |
|
Hi @timof1308 , Your PR has been received by the team and is currently under review. We will provide feedback as soon as we have an update to share. |
|
Hi @GWeale , can you please review this. |
bc4e197 to
6052bca
Compare
7e127b5 to
849b25c
Compare
|
@rohityan any update on this PR? |
849b25c to
68180ed
Compare
78337dc to
bd15172
Compare
36258ca to
66208c0
Compare
dcfc5b8 to
4a330e8
Compare
4a330e8 to
a6b1a39
Compare
Add a non-persisted request_state dict to InvocationContext that is threaded through Runner.run_async and the AdkWebServer. ReadonlyContext.state now returns a ChainMap merging request_state over session.state, so ephemeral keys (e.g. tokens) take precedence without being persisted.
Introduce create_session_state_header_provider and create_combined_header_provider for extracting session state values into HTTP headers with automatic sanitization. Add credential_key shorthand for Bearer token propagation, state_header_mapping config for arbitrary state-to-header mappings, and strict mode for type validation. Header names and values are validated per RFC 7230 to prevent injection attacks.
Add comprehensive tests for request_state merging in ReadonlyContext, header provider creation, state_header_mapping config, credential_key shorthand, RFC 7230 header validation, CRLF injection prevention, and strict mode. Update existing mocks to include request_state.
Upstream merged credential_key for AuthConfig in McpToolset (commit 282db87). Remove the PR's credential_key Bearer token shorthand to avoid the duplicate parameter conflict. The same use case is covered by state_header_mapping with state_header_format.
…tate.py The header-check CI was failing because this new test file was missing the required copyright/license header.
- Add CRLF/TAB to _DANGEROUS_CHARS and sanitize auth headers - Standardize header merge order (header_provider takes precedence) - Add model validator for state_header_mapping/state_header_format - Warn on duplicate header names in combined providers - Add request_state/state_delta/invocation_id to sync Runner.run() - Use TYPE_CHECKING guard in types.py
- Move sanitization from _get_auth_headers to _execute_with_session boundary - Replace verbose _DANGEROUS_CHARS literal with frozenset comprehension - Remove redundant CRLF strip already handled by sanitize_header_value - Use lazy %-formatting instead of f-strings in logging - Clarify docstrings for run() Optional new_message and from_config()
c57a3a6 to
e7f43fb
Compare
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
1. Main Tracking Issue:
This PR is a core component of the main MCP support effort tracked in #3449
2. Issues Closed/Addressed:
3. Related Open Issues:
Problem
Currently, MCP tools in ADK lack a secure, standardized way to propagate per-user authentication tokens (like JWTs) from the client to the MCP server. Developers often have to hardcode credentials or modify core FastAPI endpoints to pass these headers, which is not scalable or secure for multi-user environments. Additionally, storing short-lived, sensitive tokens in the persistent
session.stateis a security risk as they may be logged or stored in the database.Solution
A mechanism to propagate ephemeral state (
request_state) from theRunAgentRequestthrough to theInvocationContext, plus authentication header support forMcpToolsetat three levels of flexibility:credential_key: Convenience parameter that reads a token from session state and sends it asAuthorization: Bearer <token>. Aligns withcredential_keyon other ADK toolsets (e.g.,OpenAPIToolset)state_header_mapping/state_header_format: Declarative config for mapping arbitrary state keys to HTTP headers with format templatesheader_provider: Full control via a callable for dynamic/computed headersAll three levels resolve to the same
HeaderProvidertype internally — no separate code paths. They can be freely combined.Key changes:
request_state: Added toInvocationContextfor ephemeral data that overridessession.statebut is not persistedReadonlyContext.state: Mergesrequest_stateoversession.stateviaChainMap(ephemeral takes precedence)_internal.py: RFC 7230-compliant header validation and sanitization utilitiesMcpToolset._execute_with_sessionandMcpTool._run_async_implcreate_session_state_header_provider: Utility to generate header provider functionsMcpToolsetConfig(rejects invalid header names, orphaned format strings, CRLF injection)Review Fixes (latest commit)
Addressed code review findings:
_execute_with_session,_run_async_impl), removing redundant intermediate sanitization_DANGEROUS_CHARSset literal with concisefrozensetcomprehensioncreate_session_state_header_provider(already handled bysanitize_header_value)Runner.run()Optionalnew_messageandMcpToolset.from_config()header_provider limitationTesting Plan
Unit Tests:
tests/unittests/tools/mcp_tool/test_jwt_token_propagation.py(37 tests): Header generation,credential_key,state_header_mapping,request_stateprecedence, RFC 7230 validation, config parsing, CRLF injection protection, combined provider duplicate warnings.tests/unittests/agents/test_readonly_context_state.py(3 tests):ReadonlyContext.statemerges ephemeral and persistent state with correct precedence and immutability.tests/unittests/agents/test_readonly_context.py: No regressions.tests/unittests/tools/mcp_tool/test_mcp_toolset.py: No regressions in existing toolset functionality.Manual End-to-End (E2E) Tests:
Verified against a local FastMCP server with a mock LLM agent:
run_agentwithrequest_state={"jwt_token": "test-token-123"}Authorization: Bearer test-token-123Checklist
Usage Examples
Option 1:
credential_key(simplest — Bearer token from state)YAML:
Python:
Option 2:
state_header_mapping(custom headers and formats)YAML:
Python:
Client-side: Passing the token