iap-mcp-proxy
A client-side bridge that lets generic MCP clients (Claude Desktop, Claude Code, Cursor, ...) connect to remote MCP servers protected by Google Cloud Identity-Aware Proxy (IAP).
The Google Cloud counterpart to aws/mcp-proxy-for-aws.
┌──────────────┐ stdio ┌───────────────┐ HTTPS + ID token ┌─────┐ ┌────────────┐
│ MCP client │ ────────────────────► │ iap-mcp-proxy │ ───────────────────► │ IAP │ ───► │ MCP server │
│ (Claude etc.)│ │ (this tool) │ Proxy-Authorization │ │ │ (Cloud Run)│
└──────────────┘ └───────────────┘ └─────┘ └────────────┘
IAP expects a Google-issued OIDC ID token (or, for managed Cloud Run IAP, a self-signed service-account JWT). The MCP spec's OAuth 2.1 flow cannot produce these tokens, so generic clients get a 401/redirect and stop. This proxy runs locally, obtains and refreshes Google credentials, attaches them as Proxy-Authorization (IAP consumes and strips this header), and forwards MCP traffic (Streamable HTTP) upstream.
Install
From PyPI:
pip install iap-mcp-proxy
# or
uv pip install iap-mcp-proxy
ExtensibilityAI also publishes the same versions to the private Artifact Registry PyPI index for internal consumers.
Or install from source with uv:
git clone https://github.com/ExtensibilityAI/iap-mcp-proxy.git
cd iap-mcp-proxy
uv sync
uv run iap-mcp-proxy --version
Quick start
-
Make sure you have credentials IAP will accept (see Credentials):
gcloud auth application-default login
-
Add the proxy to your MCP client config. Cursor / Claude Desktop:
{ "mcpServers": { "internal-tools": { "command": "iap-mcp-proxy", "args": [ "--audience", "1234567890-abc.apps.googleusercontent.com", "https://mcp.internal.example.com/mcp" ] } } }
Claude Code:
claude mcp add internal-tools -- iap-mcp-proxy \ --audience 1234567890-abc.apps.googleusercontent.com \ https://mcp.internal.example.com/mcp
Usage
iap-mcp-proxy [flags] <UPSTREAM_URL>
| Flag | Env var | Default | Description |
|---|---|---|---|
--audience |
IAP_MCP_AUDIENCE |
origin of UPSTREAM_URL |
OIDC token audience. LB-backed IAP: the IAP OAuth client ID (NNN.apps.googleusercontent.com). Direct Cloud Run IAP: depends on the OAuth client — see Supported IAP configurations. |
--credentials |
IAP_MCP_CREDENTIALS |
auto |
auto, adc, impersonate, oauth, signjwt. |
--impersonate-service-account |
IAP_MCP_IMPERSONATE_SA |
— | Target SA email. impersonate mints an ID token as it; signjwt signs a self-signed JWT as it. |
--downstream-auth |
IAP_MCP_DOWNSTREAM_AUTH |
— | Value forwarded as the upstream Authorization header. Supports env:VAR_NAME indirection so secrets stay out of client config files. |
--refresh-margin |
IAP_MCP_REFRESH_MARGIN |
5m |
Refresh the ID token this long before expiry. |
--timeout |
IAP_MCP_TIMEOUT |
120s |
Upstream timeout: total for JSON responses, idle (time between reads) for SSE streams — so long-running streaming tool calls are not killed while data or keepalives keep arriving. |
--log-level |
IAP_MCP_LOG |
warn |
debug / info / warn / error. Logs go to stderr only. |
--version |
— | — | Print version and exit. |
Credentials
With --credentials=auto (the default), sources are tried in this order:
- Impersonation — if
--impersonate-service-accountis set, mint ID tokens via the IAM Credentials API (generateIdToken) using your ADC as the base identity. Requiresroles/iam.serviceAccountTokenCreatoron the target SA. Best for CI and shared team setups. - ADC — if Application Default Credentials are a service account key or workload credential, mint an ID token directly.
- Desktop OAuth — gcloud user credentials cannot mint arbitrary-audience ID tokens, so the proxy falls back to an installed-app OAuth flow: first run opens a browser for Google sign-in; the refresh token is stored in your OS keychain (fallback:
0600file under your user config dir). Requires a desktop OAuth client in the same project as the IAP resource, supplied viaIAP_MCP_OAUTH_CLIENT_ID/IAP_MCP_OAUTH_CLIENT_SECRET— see Google's docs on programmatic IAP authentication.
These OIDC modes need IAP to use a custom OAuth client (LB-backed IAP, or a managed-client service with a separate allow-listed OAuth client). For modern managed-client direct Cloud Run IAP, --credentials=signjwt is the simplest route — it needs no OAuth client (see Supported IAP configurations).
The principal must hold roles/iap.httpsResourceAccessor on the IAP resource.
Supported IAP configurations
| IAP deployment | OAuth client | Token the proxy sends | --audience |
Status |
|---|---|---|---|---|
| Behind an external HTTPS load balancer | Custom | OIDC ID token (impersonate / adc / oauth) |
IAP OAuth client ID (NNN.apps.googleusercontent.com) |
Supported |
| Direct Cloud Run IAP | Google-managed (default) | Self-signed service-account JWT (signjwt) |
exact *.run.app endpoint (or /*) |
Supported — use --credentials=signjwt |
| Direct Cloud Run IAP | Custom / allow-listed | OIDC ID token | OAuth client ID | Expected to work, not yet verified |
- Since Google shut down the IAP OAuth Admin API (March 2026), newly-secured apps default to a Google-managed OAuth client, which rejects Google-issued OIDC ID tokens. Use
--credentials=signjwt --impersonate-service-account=<SA>. - Managed-client IAP can also accept OIDC ID tokens via a separate allow-listed OAuth client; then
--credentials=impersonate/adcwork with that client ID as--audience.
Notes
- The IAP token travels in
Proxy-Authorization, which IAP consumes and strips — your app never sees it. If your app has its own auth, pass it with--downstream-authand it is forwarded verbatim asAuthorization. - On a 401 (or a 302 into Google sign-in) the proxy refreshes the token and retries once; a second failure is surfaced to the MCP client as a JSON-RPC error with an actionable message on stderr.
- If the upstream reports the session expired (HTTP 404 — e.g. after a Cloud Run redeploy), the proxy transparently replays the cached
initializehandshake to obtain a fresh session and retries the request; the stdio client never notices. - If a streaming (SSE) response drops mid-tool-call and the server tags events with IDs, the proxy resumes it with
Last-Event-IDinstead of losing the response. - After
initialize, the proxy opens the standalone GET SSE stream so server-initiated messages reach the client, reconnecting withLast-Event-IDif the stream drops. - Exit codes:
0clean shutdown,1fatal error,2auth bootstrap failure.
See docs/setup-gcp.md for setting up IAP in both deployment modes.
Development
uv sync --extra dev
uv run pytest
uv run ruff check
uv build
Releasing
Public PyPI uploads run only when a GitHub Release is published (publish-pypi.yml + Trusted Publishing).
- Bump
versioninpyproject.tomland__version__insrc/iap_mcp_proxy/__init__.pyonmain. - Merge to
main(Deploy may publish that version to Artifact Registry as usual). - Create tag
vX.Y.Zand Publish a GitHub Release for that tag. - Approve the
pypiGitHub Environment job if required reviewers are set. - Verify:
pip install iap-mcp-proxy==X.Y.Zfrom a clean environment (no Artifact Registry auth).
Routine merges to main do not publish to public PyPI.
License
Apache-2.0
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file iap_mcp_proxy-0.4.1.tar.gz.
File metadata
- Download URL: iap_mcp_proxy-0.4.1.tar.gz
- Upload date:
- Size: 31.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
90c32d7821f6fc848818b885a4826424d6dd8236761367e295859bd17d34915d
|
|
| MD5 |
b3aab0a356e5669d5466c9e2b79b01f6
|
|
| BLAKE2b-256 |
77140998cac3f6ebcdf0b457f05efed5a36e3c0c6628cfa6442f74405171d428
|
Provenance
The following attestation bundles were made for iap_mcp_proxy-0.4.1.tar.gz:
Publisher:
publish-pypi.yml on ExtensibilityAI/iap-mcp-proxy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
iap_mcp_proxy-0.4.1.tar.gz -
Subject digest:
90c32d7821f6fc848818b885a4826424d6dd8236761367e295859bd17d34915d - Sigstore transparency entry: 2665358320
- Sigstore integration time:
-
Permalink:
ExtensibilityAI/iap-mcp-proxy@283ac8823ca71b37a1b5e23ef7c24bccc94c43e7 -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/ExtensibilityAI
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@283ac8823ca71b37a1b5e23ef7c24bccc94c43e7 -
Trigger Event:
release
-
Statement type:
File details
Details for the file iap_mcp_proxy-0.4.1-py3-none-any.whl.
File metadata
- Download URL: iap_mcp_proxy-0.4.1-py3-none-any.whl
- Upload date:
- Size: 25.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c11073d3bcd833a749a0a682226a96d9fe6a99e92c52355590a79cd5271d3557
|
|
| MD5 |
52d4394dd50f5b9ec5f32861c0463301
|
|
| BLAKE2b-256 |
ae52fbb4ba0f51795b5a8efbb5ad1a62ae2cd636b652eda07c7ca239edb4143f
|
Provenance
The following attestation bundles were made for iap_mcp_proxy-0.4.1-py3-none-any.whl:
Publisher:
publish-pypi.yml on ExtensibilityAI/iap-mcp-proxy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
iap_mcp_proxy-0.4.1-py3-none-any.whl -
Subject digest:
c11073d3bcd833a749a0a682226a96d9fe6a99e92c52355590a79cd5271d3557 - Sigstore transparency entry: 2665358332
- Sigstore integration time:
-
Permalink:
ExtensibilityAI/iap-mcp-proxy@283ac8823ca71b37a1b5e23ef7c24bccc94c43e7 -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/ExtensibilityAI
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@283ac8823ca71b37a1b5e23ef7c24bccc94c43e7 -
Trigger Event:
release
-
Statement type: