-
Notifications
You must be signed in to change notification settings - Fork 63
Description
Support request header transformation hook before upstream GraphQL calls
Summary
Add a builder option that allows consumers to provide a callback for transforming HTTP headers before they are sent to the upstream GraphQL endpoint. This would enable custom authentication schemes and header manipulation without requiring an intermediary proxy.
Motivation
Apollo MCP Server currently provides two mechanisms for managing upstream request headers:
- Static headers via
.headers()— merged into every request - Dynamic forwarding via
.forward_headers()— passes named headers from MCP clients as-is
These cover standard use cases well, but they don't support scenarios where headers from the MCP client need to be transformed before reaching the upstream API. Today, the only option is to stand up a local HTTP proxy between Apollo MCP Server and the upstream, which adds operational complexity, an extra network hop, and additional code to maintain.
Real-world use case: Pass user context from MCP Client to GraphQL API
Our GraphQL APIs use a custom authentication format where it expects, user and the app context in the Authorization header:
The MCP client sends user context but the MCP server must inject app context and reformat the Authorization header before the request reaches the API.
With forward_headers, the user identity arrives at Apollo — but there's no way to transform it into the format the upstream expects. We currently work around this with a local auth proxy (axum server on port+1), which is functional but adds unnecessary complexity.
Proposed API
A header_transform (or request_interceptor) callback on the Server builder:
Server::builder()
.header_transform(|headers: &mut HeaderMap| {
// Modify headers in-place before they're sent upstream
if let Some(auth) = headers.get(AUTHORIZATION) {
let transformed = transform_auth(auth);
headers.insert(AUTHORIZATION, transformed);
}
})
// ... other builder methods
.build()
.start()
.await?;Integration point
In headers.rs, the transform would run at the end of build_request_headers, after static headers, forwarded headers, and token passthrough have all been applied:
pub fn build_request_headers(
static_headers: &HeaderMap,
forward_header_names: &ForwardHeaders,
incoming_headers: &HeaderMap,
extensions: &Extensions,
disable_auth_token_passthrough: bool,
header_transform: Option<&dyn Fn(&mut HeaderMap)>, // <-- new parameter
) -> HeaderMap {
let mut headers = static_headers.clone();
forward_headers(forward_header_names, incoming_headers, &mut headers);
if !disable_auth_token_passthrough {
if let Some(token) = extensions.get::<ValidToken>() {
headers.typed_insert(token.deref().clone());
}
}
if let Some(session_id) = incoming_headers.get("mcp-session-id") {
headers.insert("mcp-session-id", session_id.clone());
}
// Apply consumer-provided transformation
if let Some(transform) = header_transform {
transform(&mut headers);
}
headers
}Alternatives considered
| Approach | Drawback |
|---|---|
| Local HTTP proxy | Extra process/port, additional network hop, more code to maintain |
| Custom fork of apollo-mcp-server | Maintenance burden on every upstream release |
Leveraging ValidToken / auth token passthrough |
See details below |
Why ValidToken / auth token passthrough doesn't work
We considered using the existing ValidToken mechanism (disable_auth_token_passthrough: false) to inject a custom Authorization header. This doesn't work for three reasons:
-
ValidTokenispub(crate)— the struct is internal toapollo-mcp-serverand cannot be constructed or referenced by external consumers:// crates/apollo-mcp-server/src/auth/valid_token.rs pub(crate) struct ValidToken { pub(crate) token: Authorization<Bearer>, pub(crate) scopes: Vec<String>, }
-
Hard-coded to Bearer format —
ValidTokenwrapsAuthorization<Bearer>from theheaderscrate. Whentyped_insertis called, it always producesAuthorization: Bearer <token>. There is no way to output a custom format like ours -
Populated exclusively by Apollo's OAuth middleware —
ValidTokenis only created inside thevalidate()method after successful JWT decoding. External code has no way to inject a customValidTokeninto the request extensions — the auth middleware owns that pipeline end-to-end.
In short, the ValidToken path is a closed, Bearer-only pipeline with no extension points for custom authentication schemes.
Additional context
- This would be a backward-compatible, additive change — the callback is optional and defaults to no-op.
- The same hook would be useful for any custom auth scheme (HMAC signing, custom token exchange, header-based routing, etc.).
- Happy to contribute a PR if the team is open to this direction.