Skip to main content

trialmatch-mcp-bridge

A local stdio↔HTTPS bridge for Claude Desktop (THI-882). Deliberately not the real MCP server — no tool logic here at all. It does PKCE + local-loopback login against Cognito, caches a refresh token, and relays every MCP call from Desktop to the AgentCore Gateway over HTTPS with Authorization: Bearer <token> attached. The actual tools live in ../src/trialmatch_criteria_mcp/, deployed to AgentCore Runtime behind that Gateway.

Setup

Published to PyPI — this is the recommended way to run it, no repo access or git credentials needed at all:

uvx trialmatch-mcp-bridge --login   # one-time interactive sign-in; opens your browser

This matters beyond convenience: some MCP hosts run their server subprocesses in a sandbox that can't reach your normal git credentials — confirmed live, Claude Desktop (an MSIX/Windows-Store packaged app) runs in an AppContainer that can't access the interactive user's gh-configured git credential helper, so a git+https:// dependency against this (private) repo fails there even though it works fine from a plain terminal on the same machine. A public PyPI package has no credential step to fail.

Alternative, if you're already working in a local clone of this repo:

cd bridge
uv sync
uv run trialmatch-mcp-bridge --login

Either way, this caches a refresh token at ~/.trialmatch-mcp/credentials.json (0600). Subsequent runs refresh silently — you shouldn't normally need --login again. This refresh happens mid-session too, not just at startup: the bridge process is long-lived (hours, sometimes days), far outliving a single Cognito access token's own ~1hr lifetime, so BearerTokenAuth (auth.py) attaches a freshly-checked token to every request and transparently retries once with a silently-refreshed one if the Gateway ever rejects a request with 401/403 — a live session shouldn't need a manual restart just because the token it started with went stale.

If the refresh token itself has expired or been revoked (e.g. after a long weekend, or an explicit Cognito revocation), that retry falls back to a full interactive PKCE login — opening a browser mid-session, the same login flow --login runs, on a background thread so it can't freeze the rest of the relay while it waits. This exists because most MCP hosts (Claude Desktop included) never restart or re---login this process on their own: without it, an expired refresh token meant every tool call failed until you noticed and manually disabled/re-enabled the plugin (which only "fixed" it by respawning the process, landing back on this same interactive fallback at startup). If nobody's watching for the browser window within 5 minutes (e.g. this bridge is running headless), the attempt times out and the original failure surfaces as normal — run --login yourself from an interactive terminal in that case.

This covers a stale token specifically, not a stale MCP session: if the deployed Runtime itself gets redeployed to a new version mid-session, the underlying streamable-HTTP session this process already established can stop being honored regardless of how fresh the bearer token is — that still needs a manual restart of this process (or of Claude Desktop's connection to it) today.

Configuration

Zero arguments (trialmatch-mcp-bridge, --login or otherwise) connects to TrialMatch's own dev target — this is the default every existing Claude Desktop/Code config relies on, and still works exactly as before. Three sources of configuration, highest priority first:

  1. A CLI flag, always wins outright: --gateway-url, --cognito-domain, --client-id, --scopes, --redirect-port, --credentials-path.
  2. A TRIALMATCH_BRIDGE_* env var already set in the shell — e.g. TRIALMATCH_BRIDGE_CLIENT_ID — wins over --env's own preset (the pre-existing escape hatch for testing against a different deployment without a code change).
  3. --env {dev,prod} — TrialMatch's own shorthand for its two known AgentCore targets (settings.py's ENV_PRESETS). Defaults to dev if nothing else was given.
# TrialMatch's own targets (unchanged)
trialmatch-mcp-bridge --login                 # dev (default)
trialmatch-mcp-bridge --login --env prod      # prod

# Point this same bridge at a *different* AgentCore Gateway + Cognito CUSTOM_JWT setup entirely --
# --gateway-url/--cognito-domain/--client-id must all be given together (no --env in this case),
# so a typo or omission fails loudly instead of silently falling back to TrialMatch's dev preset
# for whichever one was left out.
trialmatch-mcp-bridge --login \
  --gateway-url https://your-gateway.gateway.bedrock-agentcore.us-east-1.amazonaws.com/mcp \
  --cognito-domain your-pool.auth.us-east-1.amazoncognito.com \
  --client-id your-app-client-id

--credentials-path defaults to a location derived automatically from --cognito-domain + --client-id (a stable hash, not the values themselves) — switching between two different generic configs never silently reuses or overwrites the wrong cached token, without needing to think about --credentials-path at all for the common case of just one config.

Claude Desktop / Claude Code configuration

Add to claude_desktop_config.json (Desktop) or .mcp.json (Code):

{
  "mcpServers": {
    "trialmatch-criteria": {
      "command": "uvx",
      "args": ["trialmatch-mcp-bridge"]
    }
  }
}

Or, from a local clone:

{
  "mcpServers": {
    "trialmatch-criteria": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/TrialMatch-Criteria-MCP/bridge", "trialmatch-mcp-bridge"]
    }
  }
}

(Or point command at the installed trialmatch-mcp-bridge console script directly if you've installed this package outside a uv-managed venv.)

Not Claude Code's built-in remote-MCP OAuth support (/mcp add <gateway-url> or .mcp.json's "type": "http" + oauth block) — confirmed live (claude mcp add --transport http ... <gateway-url> against this exact Gateway) that this fails immediately with Incompatible auth server: does not support dynamic client registration, before PKCE or token exchange are even reached. Cognito has no OAuth Dynamic Client Registration endpoint (RFC 7591) at all — a permanent Cognito limitation, not a bug on Cognito's side — and Claude Code's MCP OAuth client currently has no fallback when the auth server doesn't support DCR, even when a pre-configured client id is available (anthropics/claude-code#67258; this project hit the identical symptom against Cognito, adding to that report's existing Box repro). (An earlier version of this note cited anthropics/claude-code#35846 — Cognito's discovery document not advertising code_challenge_methods_supported — as the reason; that condition is real and independently confirmed against this pool too, but it was never actually the failure this bridge exists to route around, since the DCR failure above happens first.) This bridge sidesteps the whole problem by using a real, pre-registered Cognito App Client (created via this repo's own CDK stack) and doing PKCE itself — it never attempts DCR in the first place.

Publishing

Automatic, via .github/workflows/ci.yml's publish-bridge job — no manual version bump, no git tags, no stored PyPI token (uses PyPI's OIDC "Trusted Publishing"). On a merge to main that touches bridge/**: bumps the PATCH version (computed from PyPI's own current "latest", not from anything in this repo) and publishes it.

Gated behind the pypi GitHub Environment (repo Settings → Environments → pypi), which has a deployment-branch policy restricting it to main only — per PyPI's own Trusted Publishing guidance, a dedicated environment for the publishing workflow is "strongly encouraged, especially if your repository has maintainers with commit access who shouldn't have PyPI publishing access." (No pre-release/dev-version channel — deliberately kept to just this one path; testing a specific in-progress change is simpler done straight from a local clone, see Setup above, than by publishing and pinning a throwaway version.)

One-time setup (already done, noted here in case the project is ever re-created):

  1. Create the pypi GitHub Environment with a deployment-branch policy restricting it to main (done via gh api repos/Third-Opinion/TrialMatch-Criteria-MCP/environments/pypi, no UI needed — see git history for the exact call).
  2. A PyPI account with access registers a pending trusted publisher at https://pypi.org/manage/account/publishing/ for project trialmatch-mcp-bridge, owner Third-Opinion, repo TrialMatch-Criteria-MCP, workflow ci.yml, environment name pypi.

Test

uv run pytest
uv run ruff check . && uv run ruff format --check .
uv run mypy src

Unit tests mock the Cognito token endpoint and the loopback callback — no browser or real Cognito pool needed to run them. A real end-to-end test still needs an actual browser login once (--login), since PKCE Authorization Code flow is inherently interactive by design.

Metadata

Release files for trialmatch-mcp-bridge 0.1.10

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for trialmatch-mcp-bridge 0.1.10
File Size Uploaded Provenance Actions
trialmatch_mcp_bridge-0.1.10.tar.gz 89.7 kB Without provenance Details for trialmatch_mcp_bridge-0.1.10.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for trialmatch-mcp-bridge 0.1.10
File Interpreter ABI Platform Actions
trialmatch_mcp_bridge-0.1.10-py3-none-any.whl Python 3 none any Details for trialmatch_mcp_bridge-0.1.10-py3-none-any.whl

Total release size: 115.1 kB

Release files / trialmatch_mcp_bridge-0.1.10.tar.gz

Download URL trialmatch_mcp_bridge-0.1.10.tar.gz
Size 89.7 kB
Tags Source
SHA-256 checksum
How to use checksums
2ba9a04444744980c1decc6804f3c99c5c82f17db8b2fe8ab554dac3d199ae66
BLAKE2b-256 checksum
How to use checksums
ea3ccae26b4f709005362980c465440b0dac94acfe560bd9e3227a0403211325
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / trialmatch_mcp_bridge-0.1.10-py3-none-any.whl

Download URL trialmatch_mcp_bridge-0.1.10-py3-none-any.whl
Size 25.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ee058ff6bb185a6421d4b210924a6a95241680bd73ece94447cd3d1d4dbe7ca5
BLAKE2b-256 checksum
How to use checksums
e503f3170868632a98f1b2bbdd7e731df45bf8eb5bf0a913a2fe37bda03e1577
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for trialmatch-mcp-bridge 0.1.10

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page