feat: add forward_headers passthrough to remote::model-context-protocol#5257
feat: add forward_headers passthrough to remote::model-context-protocol#5257skamenan7 wants to merge 1 commit intollamastack:mainfrom
Conversation
bfbfeb5 to
a07a34e
Compare
9410037 to
c430e0d
Compare
8a88160 to
f349f68
Compare
e28014e to
28184fe
Compare
it's actually a security feature that users must always pass their token on each call. plus, that isn't changed by this pr, only the number of ways the token can be passed is expanded. stack has had a thorny problem for a long time: by design we don't hold user credentials, we (0) let admin configure credentials and we (1) let users pass through credentials (via headers); we recommend (1) over (0); and, the api spec dictates that we accept action requests that require credentials w/o a clear way for a user to provide them. consider -
today, the path for anyone using a stack w/o admin supplied credentials is to be more verbose - or @skamenan7 is this the problem you're aiming to solve? if not, please expand on what your users are trying to do. please phrase is in the user's terms, e.g. "trying to use tool xyz on mcp server pqr via a /v1/responses". |
d835e6e to
ccaf562
Compare
|
this branch has been updated 24 times in the last 7 hours. i'm marking it as draft until it is ready for review. |
|
Thanks @mattf, yeah, the closest user-facing shape is your first example. The In the case this PR is targeting, the backing implementation is a registered tool_runtime:
- provider_id: model-context-protocol
provider_type: remote::model-context-protocol
config:
forward_headers:
maas_token: Authorization
tenant_id: X-Tenant-IDIn that setup, the user still passes Today, if the downstream MCP server needs caller-scoped auth, the fallback is to switch to the verbose inline MCP form and pass |
Fair call. I rebased it too many times and that made the branch noisy to review. It's now a single commit on top of current |
|
The failing CI appears to be infra, not this change. |
ccaf562 to
65a68a5
Compare
|
Hi @mattf, CI is working now after a rebase triggered the CI. PTAL. I had been rebasing too often to keep the PR up to date hence many updates. I have not changed the implementation after opening at all. I hope I answered your questions above. If not, I can follow up more. Thanks. |
Adds
forward_headersandextra_blocked_headerstoMCPProviderConfig, wiring per-request header forwarding intolist_runtime_toolsandinvoke_tool. This lets deployers map keys fromX-LlamaStack-Provider-Datato outbound HTTP headers so request-scoped auth tokens (MaaS API keys, tenant IDs, etc.) reach the downstream MCP server without the caller passing them viaauthorization=on every tool call.Follows the same
forward_headerspattern introduced for inference and safety passthrough in #5134. Authorization-mapped values are split out and passed via theauthorization=param —prepare_mcp_headers()rejectsAuthorizationin the headers dict directly, so it flows through the dedicated param instead.What changed
MCPProviderConfig: addedforward_headers: dict[str, str] | Noneandextra_blocked_headers: list[str]with config-time validation viavalidate_forward_headers_config()fromproviders/utils/forward_headers.pyMCPProviderDataValidator: addedmodel_config = ConfigDict(extra="allow")so deployer-defined keys survive Pydantic parsing (key names are operator-configured at deploy time and can't be declared as typed fields)ModelContextProtocolImpl: new_get_forwarded_headers_and_auth()reads the allowlist from provider data, splits Authorization for theauthorization=param, returns non-auth headers separately. Bothlist_runtime_toolsandinvoke_toolmerge forwarded headers with the legacymcp_headersURI-keyed path (kept for backward compat). Explicitauthorization=from the caller wins over forwarded auth.Config example:
Test plan
Unit/integration tests — tests covering config validation, header forwarding, auth splitting, default-deny enforcement, missing-key soft-skip, and wiring through
list_runtime_toolsandinvoke_tool:Local testing with mock MCP server — ran a mock MCP server that captures all inbound headers and exposes them at
/debug/last-headers. Started llama-stack with theforward_headersconfig above, then verified via the agent API that:maas_api_tokenfromX-LlamaStack-Provider-Dataarrived asAuthorization: Bearer <token>on the downstream servertenant_idarrived asX-Tenant-IDsecret_internal) did not appear in any downstream header (default-deny confirmed)Note:
invoke_toolandlist_runtime_toolsare internal methods called by the agents layer and not exposed as HTTP endpoints (changed in upstream PRs #4997 and #5246), so full e2e requires a model server via the agent API.Checklist
forward_headers/extra_blocked_headersoptional with backward-compatible defaults — configs without them parse correctlyNone, empty dict, populated dictMCPProviderDataValidatorusesextra="allow"(intentional — deployer key names can't be pre-declared)MCPProviderConfigusesextra="forbid"build_forwarded_headers()andvalidate_forward_headers_config()fromproviders/utils/forward_headers.py— no duplicationmcp_headersURI-keyed path preserved for backward compatSummary by Sourcery
Add configurable per-request header forwarding support to remote passthrough providers, including MCP tool runtime, inference, and safety, using a shared forward_headers utility with stricter validation and default-deny behavior.
New Features:
Enhancements:
Tests: