support optional static oauth client info instead of requiring dynamic client registration
This commit is contained in:
parent
bd75a1cdf0
commit
eee0d14714
6 changed files with 82 additions and 10 deletions
24
README.md
24
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
|
- `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
|
- `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
|
### Claude Desktop
|
||||||
|
|
||||||
[Official Docs](https://modelcontextprotocol.io/quickstart/user)
|
[Official Docs](https://modelcontextprotocol.io/quickstart/user)
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
connectToRemoteServer,
|
connectToRemoteServer,
|
||||||
TransportStrategy,
|
TransportStrategy,
|
||||||
} from './lib/utils'
|
} from './lib/utils'
|
||||||
|
import { StaticOAuthClientInformationFull, StaticOAuthClientMetadata } from './lib/types'
|
||||||
import { createLazyAuthCoordinator } from './lib/coordination'
|
import { createLazyAuthCoordinator } from './lib/coordination'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,6 +33,8 @@ async function runClient(
|
||||||
callbackPort: number,
|
callbackPort: number,
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
transportStrategy: TransportStrategy = 'http-first',
|
transportStrategy: TransportStrategy = 'http-first',
|
||||||
|
staticOAuthClientMetadata: StaticOAuthClientMetadata,
|
||||||
|
staticOAuthClientInfo: StaticOAuthClientInformationFull,
|
||||||
) {
|
) {
|
||||||
// Set up event emitter for auth flow
|
// Set up event emitter for auth flow
|
||||||
const events = new EventEmitter()
|
const events = new EventEmitter()
|
||||||
|
@ -47,6 +50,8 @@ async function runClient(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
callbackPort,
|
callbackPort,
|
||||||
clientName: 'MCP CLI Client',
|
clientName: 'MCP CLI Client',
|
||||||
|
staticOAuthClientMetadata,
|
||||||
|
staticOAuthClientInfo,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create the client
|
// Create the client
|
||||||
|
@ -152,8 +157,8 @@ async function runClient(
|
||||||
|
|
||||||
// Parse command-line arguments and run the client
|
// Parse command-line arguments and run the client
|
||||||
parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts <https://server-url> [callback-port]')
|
parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts <https://server-url> [callback-port]')
|
||||||
.then(({ serverUrl, callbackPort, headers, transportStrategy }) => {
|
.then(({ serverUrl, callbackPort, headers, transportStrategy, staticOAuthClientMetadata, staticOAuthClientInfo }) => {
|
||||||
return runClient(serverUrl, callbackPort, headers, transportStrategy)
|
return runClient(serverUrl, callbackPort, headers, transportStrategy, staticOAuthClientMetadata, staticOAuthClientInfo)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Fatal error:', error)
|
console.error('Fatal error:', error)
|
||||||
|
|
|
@ -7,9 +7,10 @@ import {
|
||||||
OAuthTokens,
|
OAuthTokens,
|
||||||
OAuthTokensSchema,
|
OAuthTokensSchema,
|
||||||
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
} 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 { readJsonFile, writeJsonFile, readTextFile, writeTextFile } from './mcp-auth-config'
|
||||||
import { getServerUrlHash, log, MCP_REMOTE_VERSION } from './utils'
|
import { getServerUrlHash, log, MCP_REMOTE_VERSION } from './utils'
|
||||||
|
import { StaticOAuthClientInformationFull } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the OAuthClientProvider interface for Node.js environments.
|
* Implements the OAuthClientProvider interface for Node.js environments.
|
||||||
|
@ -22,6 +23,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
private clientUri: string
|
private clientUri: string
|
||||||
private softwareId: string
|
private softwareId: string
|
||||||
private softwareVersion: string
|
private softwareVersion: string
|
||||||
|
private staticOAuthClientMetadata: StaticOAuthClientMetadata
|
||||||
|
private staticOAuthClientInfo: StaticOAuthClientInformationFull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new NodeOAuthClientProvider
|
* Creates a new NodeOAuthClientProvider
|
||||||
|
@ -34,6 +37,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
this.clientUri = options.clientUri || 'https://github.com/modelcontextprotocol/mcp-cli'
|
this.clientUri = options.clientUri || 'https://github.com/modelcontextprotocol/mcp-cli'
|
||||||
this.softwareId = options.softwareId || '2e6dc280-f3c3-4e01-99a7-8181dbd1d23d'
|
this.softwareId = options.softwareId || '2e6dc280-f3c3-4e01-99a7-8181dbd1d23d'
|
||||||
this.softwareVersion = options.softwareVersion || MCP_REMOTE_VERSION
|
this.softwareVersion = options.softwareVersion || MCP_REMOTE_VERSION
|
||||||
|
this.staticOAuthClientMetadata = options.staticOAuthClientMetadata
|
||||||
|
this.staticOAuthClientInfo = options.staticOAuthClientInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
get redirectUrl(): string {
|
get redirectUrl(): string {
|
||||||
|
@ -50,6 +55,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
client_uri: this.clientUri,
|
client_uri: this.clientUri,
|
||||||
software_id: this.softwareId,
|
software_id: this.softwareId,
|
||||||
software_version: this.softwareVersion,
|
software_version: this.softwareVersion,
|
||||||
|
...this.staticOAuthClientMetadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +64,9 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
* @returns The client information or undefined
|
* @returns The client information or undefined
|
||||||
*/
|
*/
|
||||||
async clientInformation(): Promise<OAuthClientInformation | undefined> {
|
async clientInformation(): Promise<OAuthClientInformation | undefined> {
|
||||||
|
if (this.staticOAuthClientInfo) {
|
||||||
|
return this.staticOAuthClientInfo
|
||||||
|
}
|
||||||
// log('Reading client info')
|
// log('Reading client info')
|
||||||
return readJsonFile<OAuthClientInformation>(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema)
|
return readJsonFile<OAuthClientInformation>(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
|
import { OAuthClientInformationFull, OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for creating an OAuth client provider
|
* Options for creating an OAuth client provider
|
||||||
|
@ -20,6 +21,10 @@ export interface OAuthProviderOptions {
|
||||||
softwareId?: string
|
softwareId?: string
|
||||||
/** Software version to use for OAuth registration */
|
/** Software version to use for OAuth registration */
|
||||||
softwareVersion?: string
|
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 */
|
/** Event emitter to signal when auth code is received */
|
||||||
events: EventEmitter
|
events: EventEmitter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// optional tatic OAuth client information
|
||||||
|
export type StaticOAuthClientMetadata = OAuthClientMetadata | null | undefined
|
||||||
|
export type StaticOAuthClientInformationFull = OAuthClientInformationFull | null | undefined
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const REASON_TRANSPORT_FALLBACK = 'falling-back-to-alternate-transport'
|
||||||
|
|
||||||
// Transport strategy types
|
// Transport strategy types
|
||||||
export type TransportStrategy = 'sse-only' | 'http-only' | 'sse-first' | 'http-first'
|
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 express from 'express'
|
||||||
import net from 'net'
|
import net from 'net'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
|
@ -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) {
|
if (!serverUrl) {
|
||||||
log(usage)
|
log(usage)
|
||||||
process.exit(1)
|
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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
MCP_REMOTE_VERSION,
|
MCP_REMOTE_VERSION,
|
||||||
TransportStrategy,
|
TransportStrategy,
|
||||||
} from './lib/utils'
|
} from './lib/utils'
|
||||||
|
import { StaticOAuthClientInformationFull, StaticOAuthClientMetadata } from './lib/types'
|
||||||
import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider'
|
import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider'
|
||||||
import { createLazyAuthCoordinator } from './lib/coordination'
|
import { createLazyAuthCoordinator } from './lib/coordination'
|
||||||
|
|
||||||
|
@ -32,6 +33,8 @@ async function runProxy(
|
||||||
callbackPort: number,
|
callbackPort: number,
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
transportStrategy: TransportStrategy = 'http-first',
|
transportStrategy: TransportStrategy = 'http-first',
|
||||||
|
staticOAuthClientMetadata: StaticOAuthClientMetadata,
|
||||||
|
staticOAuthClientInfo: StaticOAuthClientInformationFull,
|
||||||
) {
|
) {
|
||||||
// Set up event emitter for auth flow
|
// Set up event emitter for auth flow
|
||||||
const events = new EventEmitter()
|
const events = new EventEmitter()
|
||||||
|
@ -47,6 +50,8 @@ async function runProxy(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
callbackPort,
|
callbackPort,
|
||||||
clientName: 'MCP CLI Proxy',
|
clientName: 'MCP CLI Proxy',
|
||||||
|
staticOAuthClientMetadata,
|
||||||
|
staticOAuthClientInfo,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create the STDIO transport for local connections
|
// 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
|
// Parse command-line arguments and run the proxy
|
||||||
parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts <https://server-url> [callback-port]')
|
parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts <https://server-url> [callback-port]')
|
||||||
.then(({ serverUrl, callbackPort, headers, transportStrategy }) => {
|
.then(({ serverUrl, callbackPort, headers, transportStrategy, staticOAuthClientMetadata, staticOAuthClientInfo }) => {
|
||||||
return runProxy(serverUrl, callbackPort, headers, transportStrategy)
|
return runProxy(serverUrl, callbackPort, headers, transportStrategy, staticOAuthClientMetadata, staticOAuthClientInfo)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
log('Fatal error:', error)
|
log('Fatal error:', error)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue