Skip to content

MCP client OAuth (outbound)

Node Wire can act as an OAuth 2.1 client when connecting to remote HTTP MCP servers that require authorization per the MCP Authorization specification (2025-11-25).

This is separate from:

  • Inbound MCP server auth (NW_MCP_API_KEY, NW_MCP_JWT_SECRET) — clients calling node-wire’s MCP server.
  • Connector OAuth (OAuth2AuthProvider) — Epic/Cerner/Salesforce credentials for connector actions. (Stripe uses static_token, not OAuth2.)

Configuration

Build a client configuration:

from node_wire_runtime.mcp_client import (
    AuthClientConfig,
    AuthConfig,
    McpClientConfig,
    McpServerConfig,
)

config = McpClientConfig(
    server=McpServerConfig(url="https://mcp.example.com/mcp"),
    auth=AuthConfig(
        scopes="mcp:tools",
        client=AuthClientConfig(id="", secret=""),  # empty → use DCR when supported
    ),
)

Settings

The following settings control the client:

Setting Default Purpose
server.url (required) Canonical MCP server URL; RFC 8707 resource
auth.discovery.cacheTtlSeconds 3600 Discovery metadata cache TTL
auth.dcr.enabled true Attempt RFC 7591 when registration_endpoint exists
auth.client.id / secret empty Override when AS has no DCR
auth.scopes empty Requested scopes
auth.production false When true, require https:// MCP server URL and configured-url HTTPS redirect; disables HTTP loopback
auth.redirect.mode loopback loopback or configured-url (defaults to configured-url when auth.production=true)
auth.redirect.url http://127.0.0.1:0/callback HTTPS callback URL for hosted / production mode
auth.token.refreshLeadSeconds 60 Proactive refresh lead time
auth.token.store os-keychain Token storage backend: os-keychain or configured-secret-store (NW_MCP_OAUTH_TOKEN_STORE)

configured-secret-store persists tokens through the same host-managed SecretProvider overlay every other runtime credential write uses (OverlaySecretProvider — see SecretProviderTokenStore in mcp_client/token_storage.py), not raw os.environ. Like the analogous host-supplied credential tier for generated connectors in nw-connector-builder-scope.md, this write is process-local only — Node Wire does not own durable secret storage. A host that needs the token to survive a restart must persist it itself via whatever backend it configures (env, AWS/Azure/GCP/Vault, or its own config-store-driven overlay writes).

Discovery

Run discovery against the server:

from node_wire_runtime.mcp_client import discover, discovery_cache_for_config

cache = discovery_cache_for_config(config)
result = await discover(config, www_authenticate=challenge_header, cache=cache)

Discovery runs on the first MCP request without a token, or after a 401 with WWW-Authenticate resource_metadata.

Authorization

For a desktop app, use the loopback flow:

from node_wire_runtime.mcp_client import AuthorizationCodeFlow

flow = AuthorizationCodeFlow(config)
tokens = await flow.run_loopback_authorization(open_browser=True)

For a hosted or production app, use the configured URL flow:

url = await flow.start_authorization(open_browser=True)
# User completes login; your app receives redirect at auth.redirect.url
tokens = await flow.complete_authorization_with_callback_url(callback_url)

Production hardening (auth.production=true)

Set NW_MCP_OAUTH_PRODUCTION=true (or auth.production=True in code) for server deployments:

  • mcp.server.url must use https://
  • auth.redirect.mode must be configured-url
  • auth.redirect.url must use https:// (HTTP loopback is rejected)
  • Re-authorization cannot use run_loopback_authorization; the host app must expose an HTTPS callback route or inject TokenManager(reauthorize=...) / McpOAuthClient(reauthorize=...)

Example environment (see also sample.env):

NW_MCP_OAUTH_ENABLED=true
NW_MCP_OAUTH_PRODUCTION=true
NW_MCP_SERVER_URL=https://mcp.example.com/mcp
NW_MCP_OAUTH_REDIRECT_MODE=configured-url
NW_MCP_OAUTH_REDIRECT_URL=https://app.example.com/oauth/mcp/callback

Desktop / local development keeps the default (NW_MCP_OAUTH_PRODUCTION unset or false) with auth.redirect.mode=loopback.

Token manager and HTTP client

Create an HTTP MCP client and list its tools:

from node_wire_runtime.mcp_client import McpOAuthClient, create_http_mcp_client

# Factory: NW_MCP_OAUTH_ENABLED=true, or legacy TOOLHIVE_MCP_BEARER_TOKEN for static auth
client = create_http_mcp_client("https://mcp.example.com/mcp")

# Production: pass reauthorize when tokens must be renewed without loopback
# client = create_http_mcp_client("https://mcp.example.com/mcp", reauthorize=my_reauth_fn)

tools = await client.list_tools()

Environment variables: see sample.env (NW_MCP_OAUTH_*).

STDIO

STDIO MCP transports use environment credentials only; this OAuth client does not apply.

ToolHive and playground integration

agents.toolhive and playground/scenarios.py call create_http_mcp_client() for HTTP MCP URLs:

  1. TOOLHIVE_MCP_BEARER_TOKEN / TOOLHIVE_MCP_API_KEY → legacy static Bearer (no OAuth).
  2. NW_MCP_OAUTH_ENABLED=true → McpOAuthClient with discovery, DCR, PKCE, token refresh.

Package layout

Implementation lives in src/node_wire_runtime/mcp_client/:

Module Purpose
config.py Operator settings (Settings)
env_config.py Builds McpClientConfig from NW_MCP_OAUTH_* env vars
discovery.py RFC 9728 + RFC 8414 metadata discovery
dcr.py RFC 7591 dynamic client registration
oauth_flow.py Authorization code + PKCE + resource parameter
redirect_listener.py Loopback redirect callback (desktop)
storage.py Persisted DCR registrations per issuer
token_storage.py Persisted access/refresh tokens per issuer
token_manager.py TokenManager — caches and refreshes tokens ahead of expiry
client.py McpOAuthClient / create_http_mcp_client() — the client factory used by ToolHive/playground integration
challenges.py PKCE challenge/verifier generation
exceptions.py McpOAuthConfigurationError and related error types

Internal demo script

Run the mock OAuth demo:

uv run python scripts/demo_mcp_oauth_mock.py