From eee0d14714a761b9a146ff5484717a37a2af38c7 Mon Sep 17 00:00:00 2001 From: William Hou Date: Mon, 12 May 2025 16:15:27 -0400 Subject: [PATCH] support optional static oauth client info instead of requiring dynamic client registration --- README.md | 26 ++++++++++++++++++++++++- src/client.ts | 9 +++++++-- src/lib/node-oauth-client-provider.ts | 11 ++++++++++- src/lib/types.ts | 9 +++++++++ src/lib/utils.ts | 28 +++++++++++++++++++++++---- src/proxy.ts | 9 +++++++-- 6 files changed, 82 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bc70b15..e61e44b 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,30 @@ npx mcp-remote https://example.remote/server --transport sse-only - `http-only`: Only uses HTTP transport, fails if the server doesn't support it - `sse-only`: Only uses SSE transport, fails if the server doesn't support it +#### Static OAuth Client Metadata + +MCP Remote supports providing static OAuth client metadata instead of using the mcp-remote defaults. +This is useful when connecting to OAuth servers that expect specific client/software IDs or scopes. + +Provide the client metadata as JSON with the `--static-oauth-client-metadata` flag: + +```bash +npx mcp-remote https://example.remote/server --static-oauth-client-metadata '{ "scope": "space separated scopes" }' +``` + +### Static OAuth Client Information + +MCP Remote supports providing static OAuth client information instead of using dynamic client registration. +This is useful when connecting to OAuth servers that require pre-registered clients. + +Provide the client information with the `--static-oauth-client-info` flag: + +```bash +export MCP_REMOTE_CLIENT_ID=xxx +export MCP_REMOTE_CLIENT_SECRET=yyy +npx mcp-remote https://example.remote/server --static-oauth-client-info "{ \"client_id\": \"$MCP_REMOTE_CLIENT_ID\", \"client_secret\": \"$MCP_REMOTE_CLIENT_SECRET\" }" +``` + ### Claude Desktop [Official Docs](https://modelcontextprotocol.io/quickstart/user) @@ -187,7 +211,7 @@ Then restarting your MCP client. ### Check your Node version -Make sure that the version of Node you have installed is [18 or +Make sure that the version of Node you have installed is [18 or higher](https://modelcontextprotocol.io/quickstart/server). Claude Desktop will use your system version of Node, even if you have a newer version installed elsewhere. diff --git a/src/client.ts b/src/client.ts index 4e0c14c..c233a3e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -22,6 +22,7 @@ import { connectToRemoteServer, TransportStrategy, } from './lib/utils' +import { StaticOAuthClientInformationFull, StaticOAuthClientMetadata } from './lib/types' import { createLazyAuthCoordinator } from './lib/coordination' /** @@ -32,6 +33,8 @@ async function runClient( callbackPort: number, headers: Record, transportStrategy: TransportStrategy = 'http-first', + staticOAuthClientMetadata: StaticOAuthClientMetadata, + staticOAuthClientInfo: StaticOAuthClientInformationFull, ) { // Set up event emitter for auth flow const events = new EventEmitter() @@ -47,6 +50,8 @@ async function runClient( serverUrl, callbackPort, clientName: 'MCP CLI Client', + staticOAuthClientMetadata, + staticOAuthClientInfo, }) // Create the client @@ -152,8 +157,8 @@ async function runClient( // Parse command-line arguments and run the client parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts [callback-port]') - .then(({ serverUrl, callbackPort, headers, transportStrategy }) => { - return runClient(serverUrl, callbackPort, headers, transportStrategy) + .then(({ serverUrl, callbackPort, headers, transportStrategy, staticOAuthClientMetadata, staticOAuthClientInfo }) => { + return runClient(serverUrl, callbackPort, headers, transportStrategy, staticOAuthClientMetadata, staticOAuthClientInfo) }) .catch((error) => { console.error('Fatal error:', error) diff --git a/src/lib/node-oauth-client-provider.ts b/src/lib/node-oauth-client-provider.ts index 806f3af..6e2e4af 100644 --- a/src/lib/node-oauth-client-provider.ts +++ b/src/lib/node-oauth-client-provider.ts @@ -7,9 +7,10 @@ import { OAuthTokens, OAuthTokensSchema, } from '@modelcontextprotocol/sdk/shared/auth.js' -import type { OAuthProviderOptions } from './types' +import type { OAuthProviderOptions, StaticOAuthClientMetadata } from './types' import { readJsonFile, writeJsonFile, readTextFile, writeTextFile } from './mcp-auth-config' import { getServerUrlHash, log, MCP_REMOTE_VERSION } from './utils' +import { StaticOAuthClientInformationFull } from './types' /** * Implements the OAuthClientProvider interface for Node.js environments. @@ -22,6 +23,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { private clientUri: string private softwareId: string private softwareVersion: string + private staticOAuthClientMetadata: StaticOAuthClientMetadata + private staticOAuthClientInfo: StaticOAuthClientInformationFull /** * Creates a new NodeOAuthClientProvider @@ -34,6 +37,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { this.clientUri = options.clientUri || 'https://github.com/modelcontextprotocol/mcp-cli' this.softwareId = options.softwareId || '2e6dc280-f3c3-4e01-99a7-8181dbd1d23d' this.softwareVersion = options.softwareVersion || MCP_REMOTE_VERSION + this.staticOAuthClientMetadata = options.staticOAuthClientMetadata + this.staticOAuthClientInfo = options.staticOAuthClientInfo } get redirectUrl(): string { @@ -50,6 +55,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { client_uri: this.clientUri, software_id: this.softwareId, software_version: this.softwareVersion, + ...this.staticOAuthClientMetadata, } } @@ -58,6 +64,9 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @returns The client information or undefined */ async clientInformation(): Promise { + if (this.staticOAuthClientInfo) { + return this.staticOAuthClientInfo + } // log('Reading client info') return readJsonFile(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema) } diff --git a/src/lib/types.ts b/src/lib/types.ts index 723b93f..14becf8 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,5 @@ import { EventEmitter } from 'events' +import { OAuthClientInformationFull, OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js' /** * Options for creating an OAuth client provider @@ -20,6 +21,10 @@ export interface OAuthProviderOptions { softwareId?: string /** Software version to use for OAuth registration */ softwareVersion?: string + /** Static OAuth client metadata to override default OAuth client metadata */ + staticOAuthClientMetadata?: StaticOAuthClientMetadata + /** Static OAuth client information to use instead of OAuth registration */ + staticOAuthClientInfo?: StaticOAuthClientInformationFull } /** @@ -33,3 +38,7 @@ export interface OAuthCallbackServerOptions { /** Event emitter to signal when auth code is received */ events: EventEmitter } + +// optional tatic OAuth client information +export type StaticOAuthClientMetadata = OAuthClientMetadata | null | undefined +export type StaticOAuthClientInformationFull = OAuthClientInformationFull | null | undefined diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 572550c..adc9a90 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -10,7 +10,7 @@ export const REASON_TRANSPORT_FALLBACK = 'falling-back-to-alternate-transport' // Transport strategy types export type TransportStrategy = 'sse-only' | 'http-only' | 'sse-first' | 'http-first' -import { OAuthCallbackServerOptions } from './types' +import { OAuthCallbackServerOptions, StaticOAuthClientInformationFull, StaticOAuthClientMetadata } from './types' import express from 'express' import net from 'net' import crypto from 'crypto' @@ -311,8 +311,8 @@ export function setupOAuthCallbackServerWithLongPoll(options: OAuthCallbackServe Authorization successful! You may close this window and return to the CLI. @@ -426,6 +426,26 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, } } + let staticOAuthClientMetadata: StaticOAuthClientMetadata = null + const staticOAuthClientMetadataIndex = args.indexOf('--static-oauth-client-metadata') + if (staticOAuthClientMetadataIndex !== -1 && staticOAuthClientMetadataIndex < args.length - 1) { + staticOAuthClientMetadata = JSON.parse(args[staticOAuthClientMetadataIndex + 1]) + if (staticOAuthClientMetadata) { + log(`Using static OAuth client metadata`) + } + } + + // parse static OAuth client information, if provided + // defaults to OAuth dynamic client registration + let staticOAuthClientInfo: StaticOAuthClientInformationFull = null + const staticOAuthClientInfoIndex = args.indexOf('--static-oauth-client-info') + if (staticOAuthClientInfoIndex !== -1 && staticOAuthClientInfoIndex < args.length - 1) { + staticOAuthClientInfo = JSON.parse(args[staticOAuthClientInfoIndex + 1]) + if (staticOAuthClientInfo) { + log(`Using static OAuth client information`) + } + } + if (!serverUrl) { log(usage) process.exit(1) @@ -468,7 +488,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, }) } - return { serverUrl, callbackPort, headers, transportStrategy } + return { serverUrl, callbackPort, headers, transportStrategy, staticOAuthClientMetadata, staticOAuthClientInfo } } /** diff --git a/src/proxy.ts b/src/proxy.ts index 7263a95..42e31b2 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -21,6 +21,7 @@ import { MCP_REMOTE_VERSION, TransportStrategy, } from './lib/utils' +import { StaticOAuthClientInformationFull, StaticOAuthClientMetadata } from './lib/types' import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider' import { createLazyAuthCoordinator } from './lib/coordination' @@ -32,6 +33,8 @@ async function runProxy( callbackPort: number, headers: Record, transportStrategy: TransportStrategy = 'http-first', + staticOAuthClientMetadata: StaticOAuthClientMetadata, + staticOAuthClientInfo: StaticOAuthClientInformationFull, ) { // Set up event emitter for auth flow const events = new EventEmitter() @@ -47,6 +50,8 @@ async function runProxy( serverUrl, callbackPort, clientName: 'MCP CLI Proxy', + staticOAuthClientMetadata, + staticOAuthClientInfo, }) // Create the STDIO transport for local connections @@ -136,8 +141,8 @@ to the CA certificate file. If using claude_desktop_config.json, this might look // Parse command-line arguments and run the proxy parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts [callback-port]') - .then(({ serverUrl, callbackPort, headers, transportStrategy }) => { - return runProxy(serverUrl, callbackPort, headers, transportStrategy) + .then(({ serverUrl, callbackPort, headers, transportStrategy, staticOAuthClientMetadata, staticOAuthClientInfo }) => { + return runProxy(serverUrl, callbackPort, headers, transportStrategy, staticOAuthClientMetadata, staticOAuthClientInfo) }) .catch((error) => { log('Fatal error:', error)