Upstream Authentication
Each registered MCP server can define how AISIX authenticates to the upstream server. AISIX holds any upstream credential gateway-side and presents it when listing tools or forwarding a tool call. The caller API key an MCP client sends to AISIX authenticates the caller to AISIX, and by default no caller header reaches the upstream server. A server that must see one — including the caller's own credential — opts in with forward_client_headers.
Two modes act on the caller's behalf instead of with a gateway credential: oauth_delegate relays the token the caller sends for the upstream, and oauth2_token_exchange exchanges the caller's Microsoft Entra ID token for a token to the upstream.
Set the credential with the auth_type field and the fields required by that authentication mode. The same modes are available through the control-plane path for AISIX Enterprise and AISIX Cloud and the resources-file path for AISIX OSS.
Prerequisites
Before starting, prepare the following:
- For AISIX Enterprise or AISIX Cloud, an environment and a write-scoped admin token. For AISIX Enterprise, follow the AISIX Enterprise getting-started guide. To request AISIX Cloud access, contact API7.
- Complete Set Up MCP Gateway for AISIX Enterprise, AISIX Cloud, or the open-source AISIX gateway. To run the optional end-to-end verification, keep the same shell, gateway, Everything test server, and temporary Docker network running.
- cURL for the request examples.
Authentication Modes
Choose the mode that matches how the upstream MCP server expects AISIX to authenticate:
auth_type | Upstream credential | How AISIX presents it |
|---|---|---|
none | None | No credential is sent. |
bearer | Bearer token in secret | Authorization: Bearer <secret> |
api_key | API key in secret | x-api-key: <secret> |
oauth2 | client_id + token_url + secret | AISIX obtains an access token, then sends Authorization: Bearer <access_token>. |
oauth_delegate | None | AISIX relays the caller's own Authorization header unchanged. See Delegate the Caller's Token. |
oauth2_token_exchange | token_exchange_profile + client_id + token_url + secret + scopes | AISIX exchanges the caller's verified JWT for an access token, then sends Authorization: Bearer <access_token>. See Entra On-Behalf-Of Token Exchange. |
The secret holds the plaintext credential AISIX presents upstream, or the client secret AISIX authenticates to the token endpoint with. It is used only gateway-side and is never sent to the calling client. To rotate a credential, update the resource with a new secret.
Use HTTPS for a credentialed upstream. When a bearer token, API key, or OAuth credential is configured on an http:// URL, the gateway logs a warning because the credential crosses the network in plaintext. For OAuth, the gateway also warns when token_url uses http:// because the client secret is sent to that endpoint.
Configure Upstream Authentication
Configure upstream authentication using the management path for your deployment.
AISIX Enterprise or AISIX Cloud
The AISIX Cloud Admin API creates the server and its upstream credential together. allowed_environments lists the environments that receive the server. A server with an empty or missing list is exposed to no environment, so every example below includes the target environment.
Export the control-plane connection values:
# AISIX_CP includes /api and has no trailing slash
# The local AISIX Enterprise quickstart uses http://localhost:8080/api
export AISIX_CP="YOUR_AISIX_CONTROL_PLANE_API_URL"
export AISIX_TOKEN="YOUR_ADMIN_TOKEN"
export ENV_ID="YOUR_ENVIRONMENT_ID"
No Authentication
Use none when the upstream MCP server does not require a credential, such as a server reachable only on a trusted internal network.
The following example creates a server resource without an upstream credential:
curl -sS -X POST "$AISIX_CP/mcp_servers" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "runbooks",
"url": "https://runbooks.internal/mcp",
"auth_type": "none",
"allowed_environments": ["'"$ENV_ID"'"]
}'
auth_type defaults to none, so you can also omit it. Leave secret, client_id, token_url, and scopes unset for a none server.
Bearer Token
Use bearer when the upstream server expects a static token in the Authorization header.
The following example creates a server resource with the bearer token AISIX should send upstream:
curl -sS -X POST "$AISIX_CP/mcp_servers" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "github",
"url": "https://mcp.example.com/mcp",
"auth_type": "bearer",
"secret": "YOUR_UPSTREAM_MCP_TOKEN",
"allowed_environments": ["'"$ENV_ID"'"]
}'
❶ bearer sends Authorization: Bearer <secret> on every request to this upstream.
❷ secret is required and must be non-empty.
API Key
Use api_key when the upstream server expects a key in the x-api-key header.
The following example creates a server resource with the API key AISIX should send upstream:
curl -sS -X POST "$AISIX_CP/mcp_servers" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "catalog",
"url": "https://catalog.example.com/mcp",
"auth_type": "api_key",
"secret": "YOUR_UPSTREAM_API_KEY",
"allowed_environments": ["'"$ENV_ID"'"]
}'
❶ api_key sends x-api-key: <secret> on every request to this upstream.
❷ secret is required and must be non-empty.
OAuth 2.0 Client Credentials
Use oauth2 when the upstream server accepts OAuth 2.0 access tokens and you have machine-to-machine client credentials for it.
AISIX exchanges the client credentials at the token endpoint, sends the access token upstream, and reuses it until shortly before it expires. If the upstream server rejects a token as unauthorized, AISIX discards the cached token and obtains a fresh one on the next call.
The following example creates a server resource with the OAuth client credentials AISIX should use for this upstream:
curl -sS -X POST "$AISIX_CP/mcp_servers" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "orders",
"url": "https://orders.example.com/mcp",
"auth_type": "oauth2",
"client_id": "aisix-gateway",
"token_url": "https://auth.example.com/oauth/token",
"secret": "YOUR_OAUTH_CLIENT_SECRET",
"scopes": ["mcp.read", "mcp.write"],
"allowed_environments": ["'"$ENV_ID"'"]
}'
❶ oauth2 uses the OAuth 2.0 client credentials grant for this upstream.
❷ client_id, token_url, and secret are required for an oauth2 server.
❸ scopes is optional. AISIX joins the values with spaces into the token request's scope parameter.
AISIX keeps the access token gateway-side and never returns it to the caller.
AISIX OSS
Add this authenticated github entry to mcp_servers; replace it if that name already exists. Keep the other resources unchanged, and supply the secret through environment interpolation:
mcp_servers:
- name: github
type: mcp
url: https://mcp.example.com/mcp
auth_type: bearer
secret: ${GITHUB_MCP_TOKEN}
Set GITHUB_MCP_TOKEN in the gateway process environment. For none, omit secret. For api_key, the same secret field is sent as x-api-key. For oauth2, add client_id, token_url, and secret, with optional scopes. For oauth_delegate, omit secret; for oauth2_token_exchange, see Entra On-Behalf-Of Token Exchange.
Environment variables belong to the gateway process. A running process cannot receive a variable that you add or change in the host shell. After adding or rotating an environment-supplied credential, validate the resources file with the new value and restart the process. For a container, recreate it with the new environment value. Use a reload only when the resource file changes and the running process already has every referenced variable with the intended value.
If validation or reload fails, the gateway never applies the invalid entry; on reload it keeps serving the last valid configuration. See the CLI Reference and Configuration Status.
Credential Handling
AISIX validates credential fields against auth_type when you create or update a server. A server with invalid credential fields is rejected at write time.
If a credential later stops working, such as after a secret is rotated or revoked, only that server's tools become unavailable. tools/list omits that server's tools, while a direct permitted call returns a generic JSON-RPC internal error. Other registered MCP servers keep working. AISIX logs the detailed failure and does not expose credential details to the calling agent.
Forward Caller Headers to an Upstream Server
The credential above is the gateway's. An internal MCP server that authorizes on the end user instead needs the caller's own header, and forward_client_headers is how it gets one. It is an array of header-name patterns, empty by default, available through the control-plane and resources-file paths, and it applies to both type: mcp and type: openapi — so a REST API exposed as tools receives the headers on every tool call.
Through the AISIX Cloud Admin API, set it when you create the server or patch it later. A patch replaces the stored list; send an empty array to clear it:
curl -sS -X PATCH "$AISIX_CP/mcp_servers/$MCP_SERVER_ID" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"forward_client_headers": ["authorization", "x-trace-*"]
}'
In the dashboard, the same setting is the Forward client headers box under Advanced on the server form, one header name or glob per line.
In the resources file an open-source AISIX gateway loads:
mcp_servers:
- name: runbooks
type: mcp
url: https://runbooks.internal/mcp
auth_type: none
forward_client_headers:
- authorization
- x-trace-*
Each entry is an exact header name or a name with a single * wildcard, matched case-insensitively. A forwarded header reaches the server whatever AISIX would otherwise have done with it.
Naming a credential slot hands the server the caller's credential in place of the gateway's, never both. authorization is the slot bearer and oauth2 fill; for api_key the slot is whatever api_key_header names, which is x-api-key unless a type: openapi server overrides it. Naming that slot on a server that also configures auth_type means the caller's value wins for callers who send it. A server that validates the aud claim rejects a token minted for the gateway.
oauth_delegate and oauth2_token_exchange own authorization themselves, so naming it on such a server has no effect. In particular, it cannot put an Authorization header that carried the gateway key back on the upstream request.
A credential slot and the trace-context headers traceparent and tracestate are forwarded only when a pattern names them exactly. The credential slots include the AWS SigV4 headers x-amz-security-token, x-amz-date, and x-amz-content-sha256. A glob such as *, x-*, or x-amz-* never matches any of them. Forwarding a credential or a trace context is an explicit act, not something a broad pattern should sweep up.
The exact-name protection described in the linked section does not automatically include a custom api_key_header. For example, if a type: openapi server uses x-mcp-token, a pattern such as x-* matches that header. A caller who sends it then supplies the upstream credential in place of the gateway's configured credential.
The MCP session slots mcp-session-id, mcp-protocol-version, and last-event-id are never forwarded. They name the session the caller holds with AISIX, not the one AISIX opens upstream, and an upstream server refuses a session id it never issued. The rest of what no pattern can reach — host, hop-by-hop headers, the x-aisix-* namespace, and the headers describing a body AISIX re-serializes — is listed under Caller Headers AISIX Never Forwards.
Delegate the Caller's Token
Use oauth_delegate when the upstream server authorizes each end user with its own OAuth tokens, and the client already holds such a token. AISIX stores no credential for the server. It relays the caller's Authorization header to the upstream unchanged.
The caller therefore sends two credentials: its gateway key in x-aisix-api-key, and its token for the upstream in Authorization. AISIX relays Authorization only when the gateway key arrived in x-aisix-api-key. An Authorization header that carried the gateway key itself is never sent upstream. When the caller sends no upstream token, AISIX calls the server without credentials and the server's own response decides the outcome. AISIX does not obtain the upstream token for the client; the client gets it from the upstream's authorization server.
oauth_delegate applies to type: mcp and type: openapi servers. No forward_client_headers entry is needed.
Configure a Delegate Server
Through the AISIX Cloud Admin API, create the server with auth_type: oauth_delegate and no credential fields:
curl -sS -X POST "$AISIX_CP/mcp_servers" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "runbooks",
"url": "https://runbooks.example.com/mcp",
"auth_type": "oauth_delegate",
"allowed_environments": ["'"$ENV_ID"'"]
}'
Leave secret, client_id, token_url, and scopes unset. Switching an existing server to oauth_delegate clears its stored secret.
In the dashboard, choose Delegate caller token as the server's Authentication. The form has no credential inputs.
In the resources file an open-source AISIX gateway loads:
mcp_servers:
- name: runbooks
type: mcp
url: https://runbooks.example.com/mcp
auth_type: oauth_delegate
Which Servers Receive the Token
The caller's token reaches only the servers its MCP access rules reach. A delegate server outside the caller's tool access grant is contacted without it.
On /mcp/{server}, only that server receives the token. On the aggregated /mcp, the caller sends a single Authorization header, and AISIX relays that same token to every delegate server the caller's grant reaches. When different delegate servers need different tokens, connect the client to each server's /mcp/{server} entry instead of the aggregated endpoint.
Connect a Client
The client sets both headers on every request. For example, a Claude Code project can declare the server in .mcp.json, with both values taken from the environment:
{
"mcpServers": {
"runbooks": {
"type": "http",
"url": "https://gateway.example.com/mcp/runbooks",
"headers": {
"x-aisix-api-key": "${AISIX_MCP_KEY}",
"Authorization": "Bearer ${RUNBOOKS_TOKEN}"
}
}
}
}
Replace https://gateway.example.com with the gateway's proxy URL. Claude Code expands ${VAR} in headers from its environment. See the Claude Code MCP documentation for the configuration scopes and other variable rules.
To check the server with cURL:
curl -sS -X POST "$AISIX_PROXY/mcp/runbooks" \
-H "x-aisix-api-key: $AISIX_MCP_KEY" \
-H "Authorization: Bearer $RUNBOOKS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
x-aisix-api-key itself is never forwarded upstream. See Send the Gateway Key in x-aisix-api-key for how AISIX reads it.
Gateway Version Requirement
oauth_delegate and oauth2_token_exchange require a gateway released after 1.7.1. A gateway at 1.7.1 or earlier cannot load a server that uses either value. While any gateway registered in the target environment runs 1.7.1 or earlier, the AISIX Cloud Admin API and the dashboard refuse to save such a server with 422 and the error code DP_INCOMPATIBLE. Upgrade the gateways in that environment first.
Optional: Verify with a Local Test Proxy
The configuration above defines the credential AISIX should send, but the Everything server from the setup guide accepts requests without authenticating them. It therefore cannot prove that the gateway presented the expected token. For an end-to-end local check, put a small bearer-authenticated reverse proxy in front of the server and verify both accepted and rejected requests.
The proxy is for local testing only: it uses plain HTTP and removes the bearer token before forwarding an accepted request to the Everything server.
The helper below contains the local proxy setup. Copy it as-is; the remaining steps configure and test AISIX.
Start the local bearer-checking proxy
Export a distinct upstream token, then define and start the test proxy on the existing Docker network:
export MCP_UPSTREAM_TOKEN="mcp-upstream-test-token"
start_mcp_auth_proxy() {
docker rm -f aisix-mcp-auth >/dev/null 2>&1 || true
docker run -d --name aisix-mcp-auth \
--network aisix-mcp \
-e UPSTREAM_TOKEN="$1" \
caddy:2.11.4-alpine \
sh -c 'caddy run --config /dev/stdin --adapter caddyfile <<EOF
:3002 {
@authorized header Authorization "Bearer $UPSTREAM_TOKEN"
handle @authorized {
reverse_proxy aisix-mcp-everything:3001 {
header_up -Authorization
header_up Host {upstream_hostport}
}
}
handle {
respond "upstream authentication failed" 401
}
}
EOF'
for attempt in $(seq 1 30); do
docker logs aisix-mcp-auth 2>&1 |
grep -q "serving initial configuration" && return
sleep 1
done
docker logs aisix-mcp-auth >&2
return 1
}
start_mcp_auth_proxy "$MCP_UPSTREAM_TOKEN"
Configure AISIX to Use the Proxy
Update the existing everything server to use http://aisix-mcp-auth:3002/mcp, auth_type: bearer, and MCP_UPSTREAM_TOKEN as its secret.
For AISIX Enterprise or AISIX Cloud, update the server created by the setup guide:
curl -fsS -X PATCH "$AISIX_CP/mcp_servers/$MCP_SERVER_ID" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF | jq
{
"url": "http://aisix-mcp-auth:3002/mcp",
"auth_type": "bearer",
"secret": "${MCP_UPSTREAM_TOKEN}"
}
EOF
For an open-source AISIX gateway, replace the existing everything entry and preserve the other resources:
mcp_servers:
- name: everything
type: mcp
url: http://aisix-mcp-auth:3002/mcp
auth_type: bearer
secret: ${MCP_UPSTREAM_TOKEN}
Add MCP_UPSTREAM_TOKEN to the gateway container environment. Because the setup guide did not start the container with this variable, validate the complete file with the variable and recreate the container. Keep the mounts, ports, and other environment variables from the quickstart. See Reload a Resources File.
For the open-source path, recreating the gateway container disconnects it from the temporary MCP network. Connect the replacement container before sending a test request:
docker network connect aisix-mcp "$AISIX_GATEWAY_CONTAINER"
Verify a Valid Credential
After the control plane projects the update or the open-source AISIX gateway restarts, call the permitted tool:
MCP_RESPONSE=$(curl -fsS -X POST "$AISIX_PROXY/mcp" \
-H "Authorization: Bearer $AISIX_MCP_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "everything__echo",
"arguments": {"message": "authenticated through AISIX"}
}
}')
echo "$MCP_RESPONSE" | jq -e \
'.result.content[] | select(.text == "Echo: authenticated through AISIX")'
The command prints the matching result. The proxy accepts a request only when it carries the gateway-held token, so this response confirms that AISIX replaced the caller's Authorization header with the upstream credential. The proxy removes that credential before forwarding to the Everything server.
Verify a Rejected Credential
Make the proxy expect a different token without changing the credential in AISIX:
start_mcp_auth_proxy "deliberately-wrong-token"
MCP_FAILURE=$(curl -fsS -X POST "$AISIX_PROXY/mcp" \
-H "Authorization: Bearer $AISIX_MCP_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "everything__echo",
"arguments": {"message": "this call should fail"}
}
}')
echo "$MCP_FAILURE" | jq -e \
'.error.code == -32603 and
.error.message == "upstream MCP server '\''everything'\'' failed to call tool"'
if echo "$MCP_FAILURE" | grep -qF "$MCP_UPSTREAM_TOKEN" || \
echo "$MCP_FAILURE" | grep -qF "$AISIX_MCP_KEY"; then
echo "credential found in client response" >&2
exit 1
fi
The jq command prints true, and the credential check produces no output. This upstream-authentication failure uses HTTP 200, so inspect the JSON-RPC error object rather than the HTTP status. A failed upstream also contributes no tools to tools/list; other reachable servers still contribute theirs.
Restore the expected token before continuing:
start_mcp_auth_proxy "$MCP_UPSTREAM_TOKEN"
Keep aisix-mcp-auth running while you use this authenticated local setup. Remove it before running the cleanup commands in the setup guide:
docker rm -f aisix-mcp-auth
Next Steps
You now understand how AISIX authenticates to upstream MCP servers. Use these guides to control which callers can reach those tools and how their traffic is governed:
- Control tool access: scope caller API keys to specific tools, whole servers, or every tool.
- Rate limits and budgets: apply request and concurrency limits, and configure budgets for MCP tool calls.
- Guardrails: inspect MCP tool arguments and results.
- Upstream request headers: the same
forward_client_headersfield on the other faces AISIX proxies through, and the full list of headers no pattern can reach.