Lifecycle Hooks

Hook into the Apollo MCP Server request lifecycle with Rhai


Lifecycle hooks enable your Rhai scripts run at specific points during request processing. To use a hook, define a function in rhai/main.rhai with the hook's name. The server calls that function automatically when it reaches the lifecycle event.

If a hook function isn't defined, the server skips it.

on_execute_graphql_operation

This hook is called before every outgoing GraphQL HTTP request. Use this hook to inspect or modify the request endpoint and headers before they're sent to your GraphQL API.

Rhai
1fn on_execute_graphql_operation(ctx) {
2    // ctx gives you access to the outgoing request
3}

Context object

The ctx parameter provides these properties:

PropertyTypeAccessDescription
endpointStringread/writeThe URL of the GraphQL endpoint for the request.
headersHeaderMapread/writeThe HTTP headers for the request.
incoming_requestHttpPartsread-onlyThe original HTTP request received by the MCP server. Only available when using HTTP transport.

Working with headers

Headers use index syntax for reading and writing:

Rhai
1fn on_execute_graphql_operation(ctx) {
2    // Read a header value (returns empty string if not present)
3    let auth = ctx.headers["authorization"];
4
5    // Set a header
6    ctx.headers["x-request-id"] = "abc-123";
7}

The incoming_request object

When the MCP server uses HTTP transport (streamable_http), ctx.incoming_request gives you read-only access to the original request that the MCP client sent to the server.

PropertyTypeDescription
methodStringThe HTTP method (for example, "POST").
uriStringThe request URI path (for example, "/mcp").
headersHeaderMapThe HTTP headers from the incoming request.
note
When using stdio transport, incoming_request is empty because there is no HTTP request.

Example: Copy a header to a different name

Read a header from the incoming request and write it to the outgoing request under a different name:

Rhai
1fn on_execute_graphql_operation(ctx) {
2    let token = ctx.incoming_request.headers["authorization"];
3
4    if token != "" {
5        ctx.headers["x-forwarded-auth"] = token;
6    }
7}

Example: Route to a different endpoint

Change the target GraphQL endpoint based on request properties:

Rhai
1fn on_execute_graphql_operation(ctx) {
2    let env = Env::get("GRAPHQL_REGION");
3
4    if env == "eu" {
5        ctx.endpoint = "https://eu.api.example.com/graphql";
6    }
7}

Error handling with throw

Use throw inside a hook to abort the current request and return an error to the MCP client. Throw a map with message and code fields for a structured error response:

Rhai
1fn on_execute_graphql_operation(ctx) {
2    let token = ctx.incoming_request.headers["authorization"];
3
4    if token == "" {
5        throw #{
6            message: "Missing authorization header",
7            code: ErrorCode::INVALID_REQUEST
8        };
9    }
10}

Error codes

ConstantDescription
ErrorCode::INVALID_REQUESTThe request is invalid. Use this for client errors like missing headers or bad input.
ErrorCode::INTERNAL_ERRORAn internal server error. This is the default if no code is provided.

Throw behavior

  • Throwing a map with message and code returns a structured error to the client with those values.

  • Throwing a map without a message field defaults the message to "Internal error".

  • Throwing a non-map value (for example, a plain string) returns a generic internal error. The thrown value is logged server-side but not sent to the client.

caution
When you throw a non-map value, the actual error message isn't forwarded to the MCP client. If you want to expose a specific error message to the client, throw a map with a message field.

Global state

Variables defined at the top level of main.rhai persist across all hook calls for the lifetime of the server. This is useful for values that don't change between requests, like environment-based configuration:

Rhai
1let backend_url = Env::get("BACKEND_URL");
2let api_key = Env::get("API_KEY");
3
4fn on_execute_graphql_operation(ctx) {
5    ctx.endpoint = backend_url;
6    ctx.headers["x-api-key"] = api_key;
7}
caution
Global variables are shared across all hook invocations. Avoid relying on a mutable global state because the order in which concurrent requests execute hooks can vary.
Feedback

Edit on GitHub

Ask Community