Skip to main content

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.

Set the credential with the auth_type field and the fields required by that authentication mode. AISIX Cloud and the open-source AISIX gateway support the same modes through different management paths.

Prerequisites

Before starting, prepare the following:

  • For AISIX Cloud, an environment and a write-scoped admin token. For On-Premises, follow the AISIX Cloud Quickstart. To request Hybrid Cloud access, contact API7.
  • Complete Set Up MCP Gateway for 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 AISIX Cloud examples.

Authentication Modes

Choose the mode that matches how the upstream MCP server expects AISIX to authenticate:

auth_typeUpstream credentialHow AISIX presents it
noneNoneNo credential is sent.
bearerBearer token in secretAuthorization: Bearer <secret>
api_keyAPI key in secretx-api-key: <secret>
oauth2client_id + token_url + secretAISIX obtains an access token, then sends Authorization: Bearer <access_token>.

The secret holds the plaintext credential AISIX presents upstream. 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 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 On-Premises quickstart uses http://localhost:8080/api.
export AISIX_CP="http://localhost:8080/api"
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.

Open-Source AISIX Gateway

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:

resources.yaml (MCP server authentication)
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.

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, configured in the resources file an open-source AISIX gateway loads, and it applies to both type: mcp and type: openapi — so a REST API exposed as tools receives the headers on every tool call.

resources.yaml (forward the caller's credential to an internal server)
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.

A credential slot — authorization, proxy-authorization, x-api-key, api-key, x-goog-api-key, cookie — and the trace-context headers traceparent and tracestate are forwarded only when a pattern names them exactly. A glob such as * or x-* never matches them, because forwarding a credential or a trace context is an explicit act rather than something a broad pattern should sweep up.

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.

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 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:

resources.yaml (authenticated Everything server)
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 AISIX Cloud projects the update or the open-source 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 AISIX Cloud budgets for MCP tool calls.
  • Guardrails: inspect MCP tool arguments and results.
  • Upstream request headers: the same forward_client_headers field on the other two faces AISIX proxies through, and the full list of headers no pattern can reach.