feat(pallas): add opt-in bearer token forwarding to downstream MCP servers

Introduce per-server `forward_inbound_auth` flag that controls whether the
inbound MCP bearer token is propagated to outbound MCP transport calls.
Implemented as a fast-agent monkey-patch auto-installed on package import,
preventing accidental credential leakage to unrelated downstream servers.

Update docs to describe the two bearer token consumers (LLM provider
passthrough and opt-in downstream MCP forwarding) with a config example.
This commit is contained in:
2026-05-03 17:17:50 -04:00
parent 95fa6e6fc0
commit be71709608
3 changed files with 101 additions and 1 deletions

View File

@@ -6,3 +6,8 @@ Reads deployment topology from agents.yaml in the working directory.
"""
__version__ = "0.1.0"
from pallas import _fastagent_patch as _fastagent_patch
_fastagent_patch.install()

View File

@@ -0,0 +1,75 @@
"""Forward the inbound bearer token to opted-in downstream MCP servers.
fast-agent (≤0.6.19) captures the inbound `Authorization: Bearer <X>` into
the `request_bearer_token` ContextVar but does NOT propagate it to
outgoing MCP transport calls — `_prepare_headers_and_auth` only reads
`server_config.headers`. This patch wraps that function so a
downstream server marked `forward_inbound_auth: true` in
fastagent.config.yaml receives the same bearer the FastAgent itself
was called with.
Opt-in is per-server because a FastAgent with multiple downstream MCP
attachments (e.g. Mnemosyne + a public weather server) must not leak
its credentials to every endpoint.
TODO: drop after the equivalent change lands in fast-agent upstream.
"""
from __future__ import annotations
import logging
from fast_agent.config import MCPServerSettings as _MCPServerSettings
from fast_agent.mcp import mcp_connection_manager as _mcm
from fast_agent.mcp.auth.context import request_bearer_token
logger = logging.getLogger(__name__)
_AUTH_HEADER_KEYS = {"authorization", "x-hf-authorization"}
_original_prepare = _mcm._prepare_headers_and_auth
def _prepare_headers_and_auth_with_forward(server_config, **kwargs):
headers, oauth_auth, user_auth_keys = _original_prepare(server_config, **kwargs)
if not getattr(server_config, "forward_inbound_auth", False):
return headers, oauth_auth, user_auth_keys
if user_auth_keys:
return headers, oauth_auth, user_auth_keys
if oauth_auth is not None:
return headers, oauth_auth, user_auth_keys
inbound = request_bearer_token.get()
if not inbound:
return headers, oauth_auth, user_auth_keys
headers = dict(headers)
headers["Authorization"] = f"Bearer {inbound}"
user_auth_keys = set(user_auth_keys) | {"Authorization"}
logger.debug(
"fastagent_forward_inbound_auth",
extra={"server": getattr(server_config, "name", None)},
)
return headers, oauth_auth, user_auth_keys
def _allow_extras_on_server_settings() -> None:
# MCPServerSettings ships with default `extra="ignore"`, so unknown YAML
# keys (including our `forward_inbound_auth` opt-in) get silently dropped.
# Flip to `extra="allow"` and rebuild so the field round-trips.
cfg = dict(_MCPServerSettings.model_config)
if cfg.get("extra") == "allow":
return
cfg["extra"] = "allow"
_MCPServerSettings.model_config = cfg
_MCPServerSettings.model_rebuild(force=True)
def install() -> None:
if getattr(_mcm._prepare_headers_and_auth, "_pallas_forward_patched", False):
return
_allow_extras_on_server_settings()
_prepare_headers_and_auth_with_forward._pallas_forward_patched = True # type: ignore[attr-defined]
_mcm._prepare_headers_and_auth = _prepare_headers_and_auth_with_forward