iap-mcp-proxy 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iap_mcp_proxy-0.4.1/PKG-INFO +163 -0
- iap_mcp_proxy-0.4.1/README.md +143 -0
- iap_mcp_proxy-0.4.1/pyproject.toml +47 -0
- iap_mcp_proxy-0.4.1/setup.cfg +4 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/__init__.py +5 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/__main__.py +6 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/__init__.py +5 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/adc.py +20 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/impersonate.py +35 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/oauth.py +270 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/random.py +10 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/signjwt.py +58 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/source.py +158 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/transport.py +78 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/bridge/__init__.py +5 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/bridge/bridge.py +705 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/bridge/sse.py +51 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/cli.py +232 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/config.py +86 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/iap/__init__.py +1 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/iap/classify.py +94 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/logging.py +33 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/PKG-INFO +163 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/SOURCES.txt +33 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/dependency_links.txt +1 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/entry_points.txt +2 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/requires.txt +15 -0
- iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/top_level.txt +1 -0
- iap_mcp_proxy-0.4.1/tests/test_auth_oauth.py +241 -0
- iap_mcp_proxy-0.4.1/tests/test_auth_source.py +86 -0
- iap_mcp_proxy-0.4.1/tests/test_auth_transport.py +119 -0
- iap_mcp_proxy-0.4.1/tests/test_bridge.py +528 -0
- iap_mcp_proxy-0.4.1/tests/test_cli.py +40 -0
- iap_mcp_proxy-0.4.1/tests/test_iap_classify.py +82 -0
- iap_mcp_proxy-0.4.1/tests/test_signjwt.py +37 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iap-mcp-proxy
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Local stdio bridge for MCP clients connecting to IAP-protected remote MCP servers
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: google-api-python-client>=2.100
|
|
8
|
+
Requires-Dist: google-auth>=2.29
|
|
9
|
+
Requires-Dist: google-auth-oauthlib>=1.2
|
|
10
|
+
Requires-Dist: httpx>=0.28
|
|
11
|
+
Requires-Dist: keyring>=25.0
|
|
12
|
+
Requires-Dist: platformdirs>=4.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
16
|
+
Provides-Extra: publish
|
|
17
|
+
Requires-Dist: twine>=5; extra == "publish"
|
|
18
|
+
Requires-Dist: keyring>=24; extra == "publish"
|
|
19
|
+
Requires-Dist: keyrings.google-artifactregistry-auth>=0.0.3; extra == "publish"
|
|
20
|
+
|
|
21
|
+
# iap-mcp-proxy
|
|
22
|
+
|
|
23
|
+
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)**.
|
|
24
|
+
|
|
25
|
+
The Google Cloud counterpart to [`aws/mcp-proxy-for-aws`](https://github.com/aws/mcp-proxy-for-aws).
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
┌──────────────┐ stdio ┌───────────────┐ HTTPS + ID token ┌─────┐ ┌────────────┐
|
|
29
|
+
│ MCP client │ ────────────────────► │ iap-mcp-proxy │ ───────────────────► │ IAP │ ───► │ MCP server │
|
|
30
|
+
│ (Claude etc.)│ │ (this tool) │ Proxy-Authorization │ │ │ (Cloud Run)│
|
|
31
|
+
└──────────────┘ └───────────────┘ └─────┘ └────────────┘
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
From [PyPI](https://pypi.org/project/iap-mcp-proxy/):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install iap-mcp-proxy
|
|
42
|
+
# or
|
|
43
|
+
uv pip install iap-mcp-proxy
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
ExtensibilityAI also publishes the same versions to the private Artifact Registry PyPI index for internal consumers.
|
|
47
|
+
|
|
48
|
+
Or install from source with [uv](https://docs.astral.sh/uv/):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/ExtensibilityAI/iap-mcp-proxy.git
|
|
52
|
+
cd iap-mcp-proxy
|
|
53
|
+
uv sync
|
|
54
|
+
uv run iap-mcp-proxy --version
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
1. Make sure you have credentials IAP will accept (see [Credentials](#credentials)):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
gcloud auth application-default login
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
2. Add the proxy to your MCP client config. Cursor / Claude Desktop:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"internal-tools": {
|
|
71
|
+
"command": "iap-mcp-proxy",
|
|
72
|
+
"args": [
|
|
73
|
+
"--audience", "1234567890-abc.apps.googleusercontent.com",
|
|
74
|
+
"https://mcp.internal.example.com/mcp"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Claude Code:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
claude mcp add internal-tools -- iap-mcp-proxy \
|
|
85
|
+
--audience 1234567890-abc.apps.googleusercontent.com \
|
|
86
|
+
https://mcp.internal.example.com/mcp
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
iap-mcp-proxy [flags] <UPSTREAM_URL>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Flag | Env var | Default | Description |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| `--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). |
|
|
98
|
+
| `--credentials` | `IAP_MCP_CREDENTIALS` | `auto` | `auto`, `adc`, `impersonate`, `oauth`, `signjwt`. |
|
|
99
|
+
| `--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. |
|
|
100
|
+
| `--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. |
|
|
101
|
+
| `--refresh-margin` | `IAP_MCP_REFRESH_MARGIN` | `5m` | Refresh the ID token this long before expiry. |
|
|
102
|
+
| `--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. |
|
|
103
|
+
| `--log-level` | `IAP_MCP_LOG` | `warn` | `debug` / `info` / `warn` / `error`. Logs go to stderr only. |
|
|
104
|
+
| `--version` | — | — | Print version and exit. |
|
|
105
|
+
|
|
106
|
+
## Credentials
|
|
107
|
+
|
|
108
|
+
With `--credentials=auto` (the default), sources are tried in this order:
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
2. **ADC** — if Application Default Credentials are a service account key or workload credential, mint an ID token directly.
|
|
112
|
+
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).
|
|
113
|
+
|
|
114
|
+
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)).
|
|
115
|
+
|
|
116
|
+
The principal must hold `roles/iap.httpsResourceAccessor` on the IAP resource.
|
|
117
|
+
|
|
118
|
+
## Supported IAP configurations
|
|
119
|
+
|
|
120
|
+
| IAP deployment | OAuth client | Token the proxy sends | `--audience` | Status |
|
|
121
|
+
|---|---|---|---|---|
|
|
122
|
+
| Behind an external HTTPS load balancer | Custom | OIDC ID token (`impersonate` / `adc` / `oauth`) | IAP OAuth client ID (`NNN.apps.googleusercontent.com`) | Supported |
|
|
123
|
+
| Direct Cloud Run IAP | Google-managed (default) | Self-signed service-account JWT (`signjwt`) | exact `*.run.app` endpoint (or `/*`) | Supported — use `--credentials=signjwt` |
|
|
124
|
+
| Direct Cloud Run IAP | Custom / allow-listed | OIDC ID token | OAuth client ID | Expected to work, not yet verified |
|
|
125
|
+
|
|
126
|
+
- 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>`.
|
|
127
|
+
- 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`.
|
|
128
|
+
|
|
129
|
+
## Notes
|
|
130
|
+
|
|
131
|
+
- 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`.
|
|
132
|
+
- 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.
|
|
133
|
+
- 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.
|
|
134
|
+
- 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.
|
|
135
|
+
- 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.
|
|
136
|
+
- Exit codes: `0` clean shutdown, `1` fatal error, `2` auth bootstrap failure.
|
|
137
|
+
|
|
138
|
+
See [`docs/setup-gcp.md`](docs/setup-gcp.md) for setting up IAP in both deployment modes.
|
|
139
|
+
|
|
140
|
+
## Development
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
uv sync --extra dev
|
|
144
|
+
uv run pytest
|
|
145
|
+
uv run ruff check
|
|
146
|
+
uv build
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Releasing
|
|
150
|
+
|
|
151
|
+
Public PyPI uploads run only when a GitHub Release is published (`publish-pypi.yml` + Trusted Publishing).
|
|
152
|
+
|
|
153
|
+
1. Bump `version` in `pyproject.toml` and `__version__` in `src/iap_mcp_proxy/__init__.py` on `main`.
|
|
154
|
+
2. Merge to `main` (Deploy may publish that version to Artifact Registry as usual).
|
|
155
|
+
3. Create tag `vX.Y.Z` and **Publish a GitHub Release** for that tag.
|
|
156
|
+
4. Approve the `pypi` GitHub Environment job if required reviewers are set.
|
|
157
|
+
5. Verify: `pip install iap-mcp-proxy==X.Y.Z` from a clean environment (no Artifact Registry auth).
|
|
158
|
+
|
|
159
|
+
Routine merges to `main` do **not** publish to public PyPI.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
Apache-2.0
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# iap-mcp-proxy
|
|
2
|
+
|
|
3
|
+
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)**.
|
|
4
|
+
|
|
5
|
+
The Google Cloud counterpart to [`aws/mcp-proxy-for-aws`](https://github.com/aws/mcp-proxy-for-aws).
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌──────────────┐ stdio ┌───────────────┐ HTTPS + ID token ┌─────┐ ┌────────────┐
|
|
9
|
+
│ MCP client │ ────────────────────► │ iap-mcp-proxy │ ───────────────────► │ IAP │ ───► │ MCP server │
|
|
10
|
+
│ (Claude etc.)│ │ (this tool) │ Proxy-Authorization │ │ │ (Cloud Run)│
|
|
11
|
+
└──────────────┘ └───────────────┘ └─────┘ └────────────┘
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
From [PyPI](https://pypi.org/project/iap-mcp-proxy/):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install iap-mcp-proxy
|
|
22
|
+
# or
|
|
23
|
+
uv pip install iap-mcp-proxy
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
ExtensibilityAI also publishes the same versions to the private Artifact Registry PyPI index for internal consumers.
|
|
27
|
+
|
|
28
|
+
Or install from source with [uv](https://docs.astral.sh/uv/):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/ExtensibilityAI/iap-mcp-proxy.git
|
|
32
|
+
cd iap-mcp-proxy
|
|
33
|
+
uv sync
|
|
34
|
+
uv run iap-mcp-proxy --version
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
1. Make sure you have credentials IAP will accept (see [Credentials](#credentials)):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
gcloud auth application-default login
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. Add the proxy to your MCP client config. Cursor / Claude Desktop:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"internal-tools": {
|
|
51
|
+
"command": "iap-mcp-proxy",
|
|
52
|
+
"args": [
|
|
53
|
+
"--audience", "1234567890-abc.apps.googleusercontent.com",
|
|
54
|
+
"https://mcp.internal.example.com/mcp"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Claude Code:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
claude mcp add internal-tools -- iap-mcp-proxy \
|
|
65
|
+
--audience 1234567890-abc.apps.googleusercontent.com \
|
|
66
|
+
https://mcp.internal.example.com/mcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
iap-mcp-proxy [flags] <UPSTREAM_URL>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Flag | Env var | Default | Description |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| `--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). |
|
|
78
|
+
| `--credentials` | `IAP_MCP_CREDENTIALS` | `auto` | `auto`, `adc`, `impersonate`, `oauth`, `signjwt`. |
|
|
79
|
+
| `--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. |
|
|
80
|
+
| `--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. |
|
|
81
|
+
| `--refresh-margin` | `IAP_MCP_REFRESH_MARGIN` | `5m` | Refresh the ID token this long before expiry. |
|
|
82
|
+
| `--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. |
|
|
83
|
+
| `--log-level` | `IAP_MCP_LOG` | `warn` | `debug` / `info` / `warn` / `error`. Logs go to stderr only. |
|
|
84
|
+
| `--version` | — | — | Print version and exit. |
|
|
85
|
+
|
|
86
|
+
## Credentials
|
|
87
|
+
|
|
88
|
+
With `--credentials=auto` (the default), sources are tried in this order:
|
|
89
|
+
|
|
90
|
+
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.
|
|
91
|
+
2. **ADC** — if Application Default Credentials are a service account key or workload credential, mint an ID token directly.
|
|
92
|
+
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).
|
|
93
|
+
|
|
94
|
+
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)).
|
|
95
|
+
|
|
96
|
+
The principal must hold `roles/iap.httpsResourceAccessor` on the IAP resource.
|
|
97
|
+
|
|
98
|
+
## Supported IAP configurations
|
|
99
|
+
|
|
100
|
+
| IAP deployment | OAuth client | Token the proxy sends | `--audience` | Status |
|
|
101
|
+
|---|---|---|---|---|
|
|
102
|
+
| Behind an external HTTPS load balancer | Custom | OIDC ID token (`impersonate` / `adc` / `oauth`) | IAP OAuth client ID (`NNN.apps.googleusercontent.com`) | Supported |
|
|
103
|
+
| Direct Cloud Run IAP | Google-managed (default) | Self-signed service-account JWT (`signjwt`) | exact `*.run.app` endpoint (or `/*`) | Supported — use `--credentials=signjwt` |
|
|
104
|
+
| Direct Cloud Run IAP | Custom / allow-listed | OIDC ID token | OAuth client ID | Expected to work, not yet verified |
|
|
105
|
+
|
|
106
|
+
- 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>`.
|
|
107
|
+
- 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`.
|
|
108
|
+
|
|
109
|
+
## Notes
|
|
110
|
+
|
|
111
|
+
- 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`.
|
|
112
|
+
- 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.
|
|
113
|
+
- 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.
|
|
114
|
+
- 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.
|
|
115
|
+
- 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.
|
|
116
|
+
- Exit codes: `0` clean shutdown, `1` fatal error, `2` auth bootstrap failure.
|
|
117
|
+
|
|
118
|
+
See [`docs/setup-gcp.md`](docs/setup-gcp.md) for setting up IAP in both deployment modes.
|
|
119
|
+
|
|
120
|
+
## Development
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
uv sync --extra dev
|
|
124
|
+
uv run pytest
|
|
125
|
+
uv run ruff check
|
|
126
|
+
uv build
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Releasing
|
|
130
|
+
|
|
131
|
+
Public PyPI uploads run only when a GitHub Release is published (`publish-pypi.yml` + Trusted Publishing).
|
|
132
|
+
|
|
133
|
+
1. Bump `version` in `pyproject.toml` and `__version__` in `src/iap_mcp_proxy/__init__.py` on `main`.
|
|
134
|
+
2. Merge to `main` (Deploy may publish that version to Artifact Registry as usual).
|
|
135
|
+
3. Create tag `vX.Y.Z` and **Publish a GitHub Release** for that tag.
|
|
136
|
+
4. Approve the `pypi` GitHub Environment job if required reviewers are set.
|
|
137
|
+
5. Verify: `pip install iap-mcp-proxy==X.Y.Z` from a clean environment (no Artifact Registry auth).
|
|
138
|
+
|
|
139
|
+
Routine merges to `main` do **not** publish to public PyPI.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
Apache-2.0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "iap-mcp-proxy"
|
|
7
|
+
version = "0.4.1"
|
|
8
|
+
description = "Local stdio bridge for MCP clients connecting to IAP-protected remote MCP servers"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"google-api-python-client>=2.100",
|
|
13
|
+
"google-auth>=2.29",
|
|
14
|
+
"google-auth-oauthlib>=1.2",
|
|
15
|
+
"httpx>=0.28",
|
|
16
|
+
"keyring>=25.0",
|
|
17
|
+
"platformdirs>=4.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8",
|
|
23
|
+
"ruff>=0.4",
|
|
24
|
+
]
|
|
25
|
+
publish = [
|
|
26
|
+
"twine>=5",
|
|
27
|
+
"keyring>=24",
|
|
28
|
+
"keyrings.google-artifactregistry-auth>=0.0.3",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
iap-mcp-proxy = "iap_mcp_proxy.cli:main"
|
|
33
|
+
|
|
34
|
+
[[tool.uv.index]]
|
|
35
|
+
name = "extensibility-ai-pypi"
|
|
36
|
+
url = "https://oauth2accesstoken@us-central1-python.pkg.dev/extensibility-ai/pypi/simple/"
|
|
37
|
+
explicit = true
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
44
|
+
pythonpath = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
select = ["E4", "E7", "E9", "F"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Application Default Credentials ID token minting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from google.auth.transport.requests import Request
|
|
6
|
+
from google.oauth2 import id_token
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ADCSource:
|
|
10
|
+
"""Mint OIDC ID tokens from Application Default Credentials."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, audience: str) -> None:
|
|
13
|
+
self._audience = audience
|
|
14
|
+
self._request = Request()
|
|
15
|
+
|
|
16
|
+
def token(self) -> str:
|
|
17
|
+
try:
|
|
18
|
+
return id_token.fetch_id_token(self._request, self._audience)
|
|
19
|
+
except Exception as exc:
|
|
20
|
+
raise RuntimeError(f"minting ID token from ADC: {exc}") from exc
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""IAM Credentials generateIdToken impersonation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import google.auth
|
|
6
|
+
from google.auth import impersonated_credentials
|
|
7
|
+
from google.auth.transport.requests import Request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ImpersonateSource:
|
|
11
|
+
"""Mint OIDC ID tokens by impersonating a target service account."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, service_account: str, audience: str) -> None:
|
|
14
|
+
self._service_account = service_account
|
|
15
|
+
self._audience = audience
|
|
16
|
+
self._request = Request()
|
|
17
|
+
|
|
18
|
+
def token(self) -> str:
|
|
19
|
+
try:
|
|
20
|
+
source_credentials, _ = google.auth.default()
|
|
21
|
+
target = impersonated_credentials.IDTokenCredentials(
|
|
22
|
+
source_credentials=source_credentials,
|
|
23
|
+
target_principal=self._service_account,
|
|
24
|
+
target_audience=self._audience,
|
|
25
|
+
include_email=True,
|
|
26
|
+
)
|
|
27
|
+
target.refresh(self._request)
|
|
28
|
+
if not target.token:
|
|
29
|
+
raise RuntimeError("impersonation returned empty token")
|
|
30
|
+
return target.token
|
|
31
|
+
except Exception as exc:
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
f"impersonating {self._service_account}: {exc} "
|
|
34
|
+
"(does the caller have roles/iam.serviceAccountTokenCreator on it?)",
|
|
35
|
+
) from exc
|