Metadata-Version: 2.4
Name: iap-mcp-proxy
Version: 0.4.1
Summary: Local stdio bridge for MCP clients connecting to IAP-protected remote MCP servers
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: google-api-python-client>=2.100
Requires-Dist: google-auth>=2.29
Requires-Dist: google-auth-oauthlib>=1.2
Requires-Dist: httpx>=0.28
Requires-Dist: keyring>=25.0
Requires-Dist: platformdirs>=4.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Provides-Extra: publish
Requires-Dist: twine>=5; extra == "publish"
Requires-Dist: keyring>=24; extra == "publish"
Requires-Dist: keyrings.google-artifactregistry-auth>=0.0.3; extra == "publish"

# 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`](https://github.com/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](https://pypi.org/project/iap-mcp-proxy/):

```bash
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](https://docs.astral.sh/uv/):

```bash
git clone https://github.com/ExtensibilityAI/iap-mcp-proxy.git
cd iap-mcp-proxy
uv sync
uv run iap-mcp-proxy --version
```

## Quick start

1. Make sure you have credentials IAP will accept (see [Credentials](#credentials)):

   ```bash
   gcloud auth application-default login
   ```

2. Add the proxy to your MCP client config. Cursor / Claude Desktop:

   ```json
   {
     "mcpServers": {
       "internal-tools": {
         "command": "iap-mcp-proxy",
         "args": [
           "--audience", "1234567890-abc.apps.googleusercontent.com",
           "https://mcp.internal.example.com/mcp"
         ]
       }
     }
   }
   ```

   Claude Code:

   ```bash
   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](#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:

1. **Impersonation** — if `--impersonate-service-account` is set, mint ID tokens via the IAM Credentials API (`generateIdToken`) using your ADC as the base identity. Requires `roles/iam.serviceAccountTokenCreator` on the target SA. Best for CI and shared team setups.
2. **ADC** — if Application Default Credentials are a service account key or workload credential, mint an ID token directly.
3. **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: `0600` file under your user config dir). Requires a desktop OAuth client in the same project as the IAP resource, supplied via `IAP_MCP_OAUTH_CLIENT_ID` / `IAP_MCP_OAUTH_CLIENT_SECRET` — see [Google's docs on programmatic IAP authentication](https://cloud.google.com/iap/docs/authentication-howto).

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](#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`/`adc` work 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-auth` and it is forwarded verbatim as `Authorization`.
- 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 `initialize` handshake 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-ID` instead of losing the response.
- After `initialize`, the proxy opens the standalone GET SSE stream so server-initiated messages reach the client, reconnecting with `Last-Event-ID` if the stream drops.
- Exit codes: `0` clean shutdown, `1` fatal error, `2` auth bootstrap failure.

See [`docs/setup-gcp.md`](docs/setup-gcp.md) for setting up IAP in both deployment modes.

## Development

```bash
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).

1. Bump `version` in `pyproject.toml` and `__version__` in `src/iap_mcp_proxy/__init__.py` on `main`.
2. Merge to `main` (Deploy may publish that version to Artifact Registry as usual).
3. Create tag `vX.Y.Z` and **Publish a GitHub Release** for that tag.
4. Approve the `pypi` GitHub Environment job if required reviewers are set.
5. Verify: `pip install iap-mcp-proxy==X.Y.Z` from a clean environment (no Artifact Registry auth).

Routine merges to `main` do **not** publish to public PyPI.

## License

Apache-2.0
