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.
Files changed (35) hide show
  1. iap_mcp_proxy-0.4.1/PKG-INFO +163 -0
  2. iap_mcp_proxy-0.4.1/README.md +143 -0
  3. iap_mcp_proxy-0.4.1/pyproject.toml +47 -0
  4. iap_mcp_proxy-0.4.1/setup.cfg +4 -0
  5. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/__init__.py +5 -0
  6. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/__main__.py +6 -0
  7. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/__init__.py +5 -0
  8. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/adc.py +20 -0
  9. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/impersonate.py +35 -0
  10. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/oauth.py +270 -0
  11. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/random.py +10 -0
  12. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/signjwt.py +58 -0
  13. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/source.py +158 -0
  14. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/auth/transport.py +78 -0
  15. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/bridge/__init__.py +5 -0
  16. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/bridge/bridge.py +705 -0
  17. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/bridge/sse.py +51 -0
  18. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/cli.py +232 -0
  19. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/config.py +86 -0
  20. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/iap/__init__.py +1 -0
  21. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/iap/classify.py +94 -0
  22. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy/logging.py +33 -0
  23. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/PKG-INFO +163 -0
  24. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/SOURCES.txt +33 -0
  25. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/dependency_links.txt +1 -0
  26. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/entry_points.txt +2 -0
  27. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/requires.txt +15 -0
  28. iap_mcp_proxy-0.4.1/src/iap_mcp_proxy.egg-info/top_level.txt +1 -0
  29. iap_mcp_proxy-0.4.1/tests/test_auth_oauth.py +241 -0
  30. iap_mcp_proxy-0.4.1/tests/test_auth_source.py +86 -0
  31. iap_mcp_proxy-0.4.1/tests/test_auth_transport.py +119 -0
  32. iap_mcp_proxy-0.4.1/tests/test_bridge.py +528 -0
  33. iap_mcp_proxy-0.4.1/tests/test_cli.py +40 -0
  34. iap_mcp_proxy-0.4.1/tests/test_iap_classify.py +82 -0
  35. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Local stdio bridge for MCP clients connecting to IAP-protected remote MCP servers."""
2
+
3
+ __version__ = "0.4.1"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ """Module entry point: python -m iap_mcp_proxy."""
2
+
3
+ from iap_mcp_proxy.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,5 @@
1
+ """Credential sources for authenticating to Google Cloud IAP."""
2
+
3
+ from iap_mcp_proxy.auth.source import AuthConfig, CachedSource, NewSource
4
+
5
+ __all__ = ["AuthConfig", "CachedSource", "NewSource"]
@@ -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