mcp-bridge is a small local proxy that lets Claude Desktop speak MCP over local stdio while the real MCP server runs behind a secure Streamable-HTTP endpoint.
Claude Desktop <-> local stdio <-> mcp-bridge <-> HTTPS Streamable HTTP <-> remote MCP server
The bridge intentionally uses only the official @modelcontextprotocol/sdk runtime transports for MCP framing, Streamable-HTTP behavior, and OAuth client flows. The local code validates configuration, attaches authentication headers when configured, handles OAuth browser login when enabled, forwards JSON-RPC messages, and cleans up remote sessions on termination.
The Build MCPB workflow builds an organization-specific .mcpb bundle without needing a local Node.js environment. Trigger it manually from the Actions tab (or gh workflow run build-mcpb.yml -f ...) with these inputs:
| Input | Required | Description |
|---|---|---|
mcpb_name |
Yes | Extension name (manifest name, used in the bundle filename). |
mcpb_display_name |
Yes | Human-readable display name. |
version |
Yes | MCPB version to embed in the manifest and filename. |
remote_mcp_url |
Yes | Remote Streamable-HTTP MCP endpoint URL. |
privacy_policies |
Yes | Privacy policy URL to embed in the manifest. |
ca_bundle_url |
No | URL to fetch an optional PEM CA bundle to embed. Leave blank to build without one. |
The workflow installs production dependencies, optionally downloads the CA bundle over HTTPS, and runs npm run build:mcpb. The resulting .mcpb is uploaded as a workflow artifact named <mcpb_name>-mcpb, retained for specific days.
- Node.js 24 or newer on macOS, Windows, or Linux.
- A reachable Streamable-HTTP MCP endpoint, for example
https://mcp.example.com/mcp. - On Linux, a URL opener for OAuth browser login. The bridge honors the
BROWSERenvironment variable and otherwise tries common openers in order. - Optional OAuth 2.1/DCR browser login, bearer token, API key, or static headers required by your firewalled endpoint.
From a public GitHub repository tag:
npx -y github:skroutz/mcp-bridge#v0.0.1 --url https://mcp.example.com/mcpAfter publishing to npm:
npx -y mcp-bridge --url https://mcp.example.com/mcpFor secrets, prefer environment variables or a config file over command-line flags because command-line arguments may be visible in process listings.
Claude Desktop starts MCP servers from its claude_desktop_config.json.
macOS path:
~/Library/Application Support/Claude/claude_desktop_config.json
Windows path:
%APPDATA%\Claude\claude_desktop_config.json
Linux path:
~/.config/Claude/claude_desktop_config.json
GitHub-backed npx configuration:
{
"mcpServers": {
"secure-remote": {
"command": "npx",
"args": [
"-y",
"github:skroutz/mcp-bridge#v0.0.1"
],
"env": {
"MCP_BRIDGE_URL": "https://mcp.example.com/mcp",
"MCP_BRIDGE_BEARER_TOKEN": "replace-with-token"
}
}
}
}OAuth-backed configuration:
{
"mcpServers": {
"skroutz-mcp": {
"command": "npx",
"args": [
"-y",
"github:skroutz/mcp-bridge#main"
],
"env": {
"MCP_BRIDGE_URL": "https://mcp.example.com/mcp",
"MCP_BRIDGE_OAUTH": "true"
}
}
}
}On Windows, if Claude cannot resolve npx directly, run where npx in Command Prompt and set command to the full npx.cmd path.
This repository can also produce a Claude Desktop MCPB extension for organization-wide distribution. The bundle vendors the bridge code and node_modules into a single .mcpb zip archive, so users do not need to install Node dependencies or edit claude_desktop_config.json manually.
The committed MCPB manifest is a public-safe template. Do not commit generated organization-specific .mcpb files or manifests with internal endpoints.
Build the bundle from a clean checkout:
npm ci --omit=dev
npm run build:mcpbThe generated file is written to dist/skroutz-mcp-bridge-<version>.mcpb.
The MCPB template lives at mcpb/manifest.template.json. It configures Claude Desktop to run the bundled bridge with:
MCP_BRIDGE_URLfrom the extension's Remote MCP URL setting.MCP_BRIDGE_OAUTH=trueso the remote server's OAuth/DCR browser flow is used.MCP_BRIDGE_OAUTH_CALLBACK_PORTfrom the extension's OAuth callback port setting.- Optional
NODE_EXTRA_CA_CERTSandMCP_BRIDGE_CA_BUNDLEfrom either a bundled CA certificate or the extension's Internal CA certificate file setting.
Build an organization-specific artifact by passing release-time environment variables. The generated artifact remains ignored by git:
MCPB_NAME="your-org-mcp" \
MCPB_DISPLAY_NAME="Your Org MCP" \
MCPB_REMOTE_MCP_URL="https://mcp.example.com/mcp" \
MCPB_PRIVACY_POLICIES="https://example.com/privacy" \
npm run build:mcpbWhen MCPB_REMOTE_MCP_URL is set, the generated manifest bakes the URL and OAuth callback port directly into the private artifact instead of asking each user to configure them during installation. Override the fixed callback port with MCPB_OAUTH_CALLBACK_PORT if needed.
The npm/npx package and MCPB extension require Node.js 24 or newer.
If Claude needs a corporate TLS proxy CA to reach the remote MCP server, keep the PEM file outside git and bundle it only into the generated .mcpb:
MCPB_CA_BUNDLE="./company-ca.pem" \
npm run build:mcpbWhen MCPB_CA_BUNDLE is set, the build copies the PEM to certs/ca-bundle.pem inside the ignored .mcpb artifact and sets both NODE_EXTRA_CA_CERTS=${__dirname}/certs/ca-bundle.pem and MCP_BRIDGE_CA_BUNDLE=${__dirname}/certs/ca-bundle.pem in the generated manifest. MCP_BRIDGE_CA_BUNDLE is read by the bridge itself, so TLS trust does not depend only on Claude Desktop honoring Node's startup CA environment variable. Do not use this for private keys or client certificates.
For Claude.ai organization settings, upload the generated .mcpb as a local MCP extension. Users should then see the configured local MCP server in Claude Desktop without pasting JSON. On first use, the bridge will start the remote OAuth flow and cache the resulting client registration and tokens in the user's OS config directory.
If a remote authorization server invalidates a previously registered DCR client, the bridge clears that endpoint's cached session and starts one fresh DCR/browser authorization cycle automatically. Before opening a browser, it preflights the authorization URL so an authorization-endpoint HTTP 400 containing invalid_client also triggers this recovery. The retry is deliberately limited to one per bridge start to avoid loops or registration rate limits.
Some authorization servers report rejected refresh tokens or a token/client mismatch as invalid_request. When that error comes specifically from a refresh-token request, the bridge performs the same bounded session reset and browser authorization. Generic HTTP failures and invalid_request responses from registration or authorization-code exchange do not trigger this reset. Normal invalid_grant recovery remains handled by the SDK.
Before uploading a new MCPB release:
npm run check
npm run pack:dry-run
npm run build:mcpb
unzip -l dist/skroutz-mcp-bridge-$(node -p "require('./package.json').version").mcpb | head -50To debug the exact MCPB launch path from Terminal, run the emulator against either a .mcpb file or Claude's already-unpacked extension directory:
npm run emulate:mcpb -- --mcpb dist/skroutz-mcp-bridge-0.0.1.mcpb --clean-envnpm run emulate:mcpb -- --mcpb "/path/to/unpacked/extension-directory" --clean-envThe emulator reads manifest.json, resolves ${__dirname}, starts the configured Node server with the manifest environment, sends a Claude-style initialize request, and writes captured output to dist/mcpb-emulate.stdout and dist/mcpb-emulate.stderr.
For OAuth-protected remote MCP servers, run a one-time login before starting Claude Desktop. This avoids Claude timing out while the first MCP initialize request waits for browser authorization.
npx -y github:skroutz/mcp-bridge#main \
--oauth-login \
--url https://mcp.example.com/mcpLogin-only OAuth HTTP requests use a default 30 second timeout so a broken endpoint cannot hang forever. Override it while debugging with --timeout-ms, for example --timeout-ms 10000.
The bridge will:
- Discover OAuth metadata from the remote MCP server or protected-resource challenge.
- Dynamically register a public client when the authorization server supports DCR.
- Open the system browser for interactive login.
- Receive the authorization callback on the first available loopback port starting at
33418. - Store OAuth client information and tokens in the user config directory.
When Claude Desktop launches a normal bridge process, browser opening is delayed for five seconds. This allows Claude's short-lived compatibility probe to exit before it can create a stale authorization tab. Explicit --oauth-login runs open the browser immediately.
The default OAuth cache locations are:
- macOS:
~/Library/Application Support/mcp-bridge/oauth-cache.json - Linux:
$XDG_CONFIG_HOME/mcp-bridge/oauth-cache.json, or~/.config/mcp-bridge/oauth-cache.json - Windows:
%APPDATA%\mcp-bridge\oauth-cache.json
Use --oauth-clear-cache (or MCP_BRIDGE_OAUTH_CLEAR_CACHE=true) to clear only the session matching the current remote URL, callback URL, and OAuth scope before continuing. Use --oauth-storage when an administrator needs to place the cache elsewhere.
After login completes, restart Claude Desktop with MCP_BRIDGE_OAUTH=true in the server config.
The configured callback port is the initial start of the search range. The bridge reuses a previously successful port when possible; if the preferred port is occupied, it increments until it can bind a listener, then uses that exact URI for client registration and authorization. To start from a different port, set the same explicit value in both the login command and Claude config:
npx -y github:skroutz/mcp-bridge#main \
--oauth-login \
--url https://mcp.example.com/mcp \
--oauth-callback-port 33419{
"mcpServers": {
"skroutz-mcp": {
"command": "npx",
"args": [
"-y",
"github:skroutz/mcp-bridge#main"
],
"env": {
"MCP_BRIDGE_URL": "https://mcp.example.com/mcp",
"MCP_BRIDGE_OAUTH": "true",
"MCP_BRIDGE_OAUTH_CALLBACK_PORT": "33419"
}
}
}
}If your company endpoint uses an internal CA, add the same CA bundle to both the one-time login command and Claude Desktop config. Prefer this over disabling TLS verification:
NODE_EXTRA_CA_CERTS=/absolute/path/to/company-ca.pem \
npx -y github:skroutz/mcp-bridge#main \
--oauth-login \
--url https://mcp.example.com/mcp{
"mcpServers": {
"skroutz-mcp": {
"command": "npx",
"args": [
"-y",
"github:skroutz/mcp-bridge#main"
],
"env": {
"MCP_BRIDGE_URL": "https://mcp.example.com/mcp",
"MCP_BRIDGE_OAUTH": "true",
"NODE_EXTRA_CA_CERTS": "/absolute/path/to/company-ca.pem"
}
}
}
}Environment variables:
| Variable | Description |
|---|---|
MCP_BRIDGE_URL |
Required remote Streamable-HTTP MCP endpoint. Must be HTTPS unless MCP_BRIDGE_ALLOW_HTTP=true. |
MCP_BRIDGE_BEARER_TOKEN |
Optional bearer token sent as Authorization: Bearer <token>. |
MCP_BRIDGE_API_KEY |
Optional API key sent as X-API-Key. |
MCP_BRIDGE_HEADERS |
Optional JSON object of additional HTTP headers. |
MCP_BRIDGE_CONFIG |
Optional JSON config file path. Supports ~ and relative paths. |
MCP_BRIDGE_OAUTH |
Set to true to enable OAuth 2.1/DCR browser login for the remote endpoint. |
MCP_BRIDGE_OAUTH_LOGIN |
Set to true to run login only, cache credentials, then exit. Equivalent to --oauth-login. |
MCP_BRIDGE_OAUTH_CLEAR_CACHE |
Set to true to clear the current OAuth session before continuing. Equivalent to --oauth-clear-cache. |
MCP_BRIDGE_OAUTH_CALLBACK_PORT |
Optional loopback callback port. Default: 33418. |
MCP_BRIDGE_OAUTH_STORAGE |
Optional OAuth cache file path. Defaults to the user config directory. |
MCP_BRIDGE_OAUTH_SCOPE |
Optional OAuth scope override. |
MCP_BRIDGE_OAUTH_OPEN_BROWSER |
Set to false for headless login/debugging. The authorization URL is still written to stderr. |
MCP_BRIDGE_CA_BUNDLE |
Optional PEM CA bundle read directly by the bridge HTTP client for MCP and OAuth requests. |
MCP_BRIDGE_ALLOW_HTTP |
Set to true only for local development endpoints. |
MCP_BRIDGE_TIMEOUT_MS |
Optional fetch timeout. Disabled by default because MCP responses may stream. |
MCP_BRIDGE_MAX_BUFFER_SIZE |
Optional local stdio read buffer size in bytes. Default: 10485760. |
NODE_EXTRA_CA_CERTS |
Node.js TLS option for adding an internal CA bundle. MCPB builds also set MCP_BRIDGE_CA_BUNDLE so the bridge can load the PEM itself. |
CLI flags:
mcp-bridge \
--url https://mcp.example.com/mcp \
--bearer-token "$MCP_TOKEN" \
--header "X-Tenant:tenant-a"Config file:
{
"url": "https://mcp.example.com/mcp",
"bearerToken": "replace-with-token",
"apiKey": "replace-with-key",
"headers": {
"X-Tenant": "tenant-a"
},
"oauth": false,
"oauthClearCache": false,
"oauthCallbackPort": 33418,
"oauthOpenBrowser": true,
"caBundle": "/absolute/path/to/company-ca.pem",
"timeoutMs": 120000,
"maxBufferSize": 10485760
}Precedence is config file, then environment variables, then CLI flags.
- HTTPS is required by default.
- stdout is reserved for MCP messages; logs are written to stderr.
- Secrets are redacted from bridge logs.
- OAuth token/client-registration cache files are stored outside the repository in the user config directory with private file permissions where supported by the OS.
- Concurrent bridge processes coordinate OAuth by remote connector throughout the connection, including refreshes and SSE reconnects. Ownership covers the token request and saving its response; waiting requests reload the cache before attempting another refresh. Authenticated MCP requests remain concurrent. Different connectors may authorize simultaneously on different loopback ports.
- Healthy requests do not reserve an OAuth callback port. If authorization needs a different callback port, the bridge discards the previous client registration and its tokens together. Replacing or invalidating a client also discards its associated tokens.
- Credentials embedded in endpoint URLs are rejected. Use environment variables or a config file instead.
- Static bearer/API-key auth and OAuth browser auth are mutually exclusive modes.
- Headers controlled by the Streamable-HTTP transport, such as
content-type,accept,mcp-session-id, andmcp-protocol-version, cannot be overridden. - The bridge sends a Streamable-HTTP session termination request during normal shutdown when the remote server provided a session ID.
npm install
npm run check
npm run pack:dry-run
npm run build:mcpbRun against a local development MCP server:
MCP_BRIDGE_ALLOW_HTTP=true MCP_BRIDGE_URL=http://127.0.0.1:3000/mcp npm start-
Keep
mainpassing locally:npm ci npm run check npm run pack:dry-run npm run build:mcpb
-
Update
package.jsonversion:npm version patch --no-git-tag-version
-
Commit the release version:
git add package.json package-lock.json git commit -m "Release v0.1.1" -
Create and push a signed tag:
git tag -s v0.1.1 -m "v0.1.1" git push origin main v0.1.1 -
Create a GitHub release from the tag. Include:
- The exact
npx -y github:skroutz/mcp-bridge#v0.1.1command. - The generated
.mcpbartifact fromdist/. - Supported Node.js version.
- Configuration changes.
- Security notes and dependency version.
- The exact
-
Upload the
.mcpbartifact to Claude.ai organization settings for managed local-MCP distribution. -
Smoke-test the release tag on macOS, Windows, and Linux:
npx -y github:skroutz/mcp-bridge#v0.1.1 --help
-
Optional npm publication:
npm publish --provenance