Skip to content

Support request header transformation hook before upstream GraphQL calls #642

@ramapalani

Description

@ramapalani

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:

  1. ValidToken is pub(crate) — the struct is internal to apollo-mcp-server and 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>,
    }
  2. Hard-coded to Bearer formatValidToken wraps Authorization<Bearer> from the headers crate. When typed_insert is called, it always produces Authorization: Bearer <token>. There is no way to output a custom format like ours

  3. Populated exclusively by Apollo's OAuth middlewareValidToken is only created inside the validate() method after successful JWT decoding. External code has no way to inject a custom ValidToken into 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions