support optional static oauth client info instead of requiring dynamic client registration

This commit is contained in:
William Hou 2025-05-12 16:15:27 -04:00
parent bd75a1cdf0
commit eee0d14714
No known key found for this signature in database
6 changed files with 82 additions and 10 deletions

View file

@ -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.

View file

@ -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<string, string>,
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 <https://server-url> [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)

View file

@ -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<OAuthClientInformation | undefined> {
if (this.staticOAuthClientInfo) {
return this.staticOAuthClientInfo
}
// log('Reading client info')
return readJsonFile<OAuthClientInformation>(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema)
}

View file

@ -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

View file

@ -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.
<script>
// If this is a non-interactive session (no manual approval step was required) then
// this should automatically close the window. If not, this will have no effect and
// If this is a non-interactive session (no manual approval step was required) then
// this should automatically close the window. If not, this will have no effect and
// the user will see the message above.
window.close();
</script>
@ -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 }
}
/**

View file

@ -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<string, string>,
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 <https://server-url> [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)