Skip to content

Commit f325028

Browse files
geelenWilliam Hou
andauthored
Static oauth client info (#85)
* support optional static oauth client info instead of requiring dynamic client registration * tweak README.md * resolve merge conflicts * add/document @ static oauth metadata/info filepaths --------- Co-authored-by: William Hou <[email protected]>
1 parent dfe1eab commit f325028

File tree

6 files changed

+132
-42
lines changed

6 files changed

+132
-42
lines changed

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,37 @@ npx mcp-remote https://example.remote/server --transport sse-only
152152
- `http-only`: Only uses HTTP transport, fails if the server doesn't support it
153153
- `sse-only`: Only uses SSE transport, fails if the server doesn't support it
154154

155+
### Static OAuth Client Metadata
156+
157+
MCP Remote supports providing static OAuth client metadata instead of using the mcp-remote defaults.
158+
This is useful when connecting to OAuth servers that expect specific client/software IDs or scopes.
159+
160+
Provide the client metadata as a JSON string or as a `@` prefixed filepath with the `--static-oauth-client-metadata` flag:
161+
162+
```bash
163+
npx mcp-remote https://example.remote/server --static-oauth-client-metadata '{ "scope": "space separated scopes" }'
164+
# uses node readfile, so you probably want to use absolute paths if you're not sure what the cwd is
165+
npx mcp-remote https://example.remote/server --static-oauth-client-metadata '@/Users/username/Library/Application Support/Claude/oauth_client_metadata.json'
166+
```
167+
168+
### Static OAuth Client Information
169+
170+
Per the [spec](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#2-4-dynamic-client-registration),
171+
servers are encouraged but not required to support [OAuth dynamic client registration](https://datatracker.ietf.org/doc/html/rfc7591).
172+
173+
For these servers, MCP Remote supports providing static OAuth client information instead.
174+
This is useful when connecting to OAuth servers that require pre-registered clients.
175+
176+
Provide the client metadata as a JSON string or as a `@` prefixed filepath with the `--static-oauth-client-info` flag:
177+
178+
```bash
179+
export MCP_REMOTE_CLIENT_ID=xxx
180+
export MCP_REMOTE_CLIENT_SECRET=yyy
181+
npx mcp-remote https://example.remote/server --static-oauth-client-info "{ \"client_id\": \"$MCP_REMOTE_CLIENT_ID\", \"client_secret\": \"$MCP_REMOTE_CLIENT_SECRET\" }"
182+
# uses node readfile, so you probably want to use absolute paths if you're not sure what the cwd is
183+
npx mcp-remote https://example.remote/server --static-oauth-client-info '@/Users/username/Library/Application Support/Claude/oauth_client_info.json'
184+
```
185+
155186
### Claude Desktop
156187

157188
[Official Docs](https://modelcontextprotocol.io/quickstart/user)
@@ -208,7 +239,7 @@ Then restarting your MCP client.
208239

209240
### Check your Node version
210241

211-
Make sure that the version of Node you have installed is [18 or
242+
Make sure that the version of Node you have installed is [18 or
212243
higher](https://modelcontextprotocol.io/quickstart/server). Claude
213244
Desktop will use your system version of Node, even if you have a newer
214245
version installed elsewhere.

src/client.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
connectToRemoteServer,
2323
TransportStrategy,
2424
} from './lib/utils'
25+
import { StaticOAuthClientInformationFull, StaticOAuthClientMetadata } from './lib/types'
2526
import { createLazyAuthCoordinator } from './lib/coordination'
2627

2728
/**
@@ -33,6 +34,8 @@ async function runClient(
3334
headers: Record<string, string>,
3435
transportStrategy: TransportStrategy = 'http-first',
3536
host: string,
37+
staticOAuthClientMetadata: StaticOAuthClientMetadata,
38+
staticOAuthClientInfo: StaticOAuthClientInformationFull,
3639
) {
3740
// Set up event emitter for auth flow
3841
const events = new EventEmitter()
@@ -49,6 +52,8 @@ async function runClient(
4952
callbackPort,
5053
host,
5154
clientName: 'MCP CLI Client',
55+
staticOAuthClientMetadata,
56+
staticOAuthClientInfo,
5257
})
5358

5459
// Create the client
@@ -154,8 +159,8 @@ async function runClient(
154159

155160
// Parse command-line arguments and run the client
156161
parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx client.ts <https://server-url> [callback-port] [--debug]')
157-
.then(({ serverUrl, callbackPort, headers, transportStrategy, host, debug }) => {
158-
return runClient(serverUrl, callbackPort, headers, transportStrategy, host)
162+
.then(({ serverUrl, callbackPort, headers, transportStrategy, host, staticOAuthClientMetadata, staticOAuthClientInfo }) => {
163+
return runClient(serverUrl, callbackPort, headers, transportStrategy, host, staticOAuthClientMetadata, staticOAuthClientInfo)
159164
})
160165
.catch((error) => {
161166
console.error('Fatal error:', error)

src/lib/node-oauth-client-provider.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import {
66
OAuthTokens,
77
OAuthTokensSchema,
88
} from '@modelcontextprotocol/sdk/shared/auth.js'
9-
import type { OAuthProviderOptions } from './types'
9+
import type { OAuthProviderOptions, StaticOAuthClientMetadata } from './types'
1010
import { readJsonFile, writeJsonFile, readTextFile, writeTextFile } from './mcp-auth-config'
11+
import { StaticOAuthClientInformationFull } from './types'
1112
import { getServerUrlHash, log, debugLog, DEBUG, MCP_REMOTE_VERSION } from './utils'
1213

1314
/**
@@ -21,6 +22,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
2122
private clientUri: string
2223
private softwareId: string
2324
private softwareVersion: string
25+
private staticOAuthClientMetadata: StaticOAuthClientMetadata
26+
private staticOAuthClientInfo: StaticOAuthClientInformationFull
2427

2528
/**
2629
* Creates a new NodeOAuthClientProvider
@@ -33,6 +36,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
3336
this.clientUri = options.clientUri || 'https://github.com/modelcontextprotocol/mcp-cli'
3437
this.softwareId = options.softwareId || '2e6dc280-f3c3-4e01-99a7-8181dbd1d23d'
3538
this.softwareVersion = options.softwareVersion || MCP_REMOTE_VERSION
39+
this.staticOAuthClientMetadata = options.staticOAuthClientMetadata
40+
this.staticOAuthClientInfo = options.staticOAuthClientInfo
3641
}
3742

3843
get redirectUrl(): string {
@@ -49,6 +54,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
4954
client_uri: this.clientUri,
5055
software_id: this.softwareId,
5156
software_version: this.softwareVersion,
57+
...this.staticOAuthClientMetadata,
5258
}
5359
}
5460

@@ -58,6 +64,10 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
5864
*/
5965
async clientInformation(): Promise<OAuthClientInformationFull | undefined> {
6066
if (DEBUG) await debugLog(this.serverUrlHash, 'Reading client info')
67+
if (this.staticOAuthClientInfo) {
68+
if (DEBUG) await debugLog(this.serverUrlHash, 'Returning static client info')
69+
return this.staticOAuthClientInfo
70+
}
6171
const clientInfo = await readJsonFile<OAuthClientInformationFull>(this.serverUrlHash, 'client_info.json', OAuthClientInformationFullSchema)
6272
if (DEBUG) await debugLog(this.serverUrlHash, 'Client info result:', clientInfo ? 'Found' : 'Not found')
6373
return clientInfo
@@ -81,16 +91,16 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
8191
await debugLog(this.serverUrlHash, 'Reading OAuth tokens')
8292
await debugLog(this.serverUrlHash, 'Token request stack trace:', new Error().stack)
8393
}
84-
94+
8595
const tokens = await readJsonFile<OAuthTokens>(this.serverUrlHash, 'tokens.json', OAuthTokensSchema)
86-
96+
8797
if (DEBUG) {
8898
if (tokens) {
8999
const expiresAt = new Date(tokens.expires_at)
90100
const now = new Date()
91101
const expiresAtTime = expiresAt.getTime()
92102
const timeLeft = !isNaN(expiresAtTime) ? Math.round((expiresAtTime - now.getTime()) / 1000) : 0
93-
103+
94104
// Alert if expires_at produces an invalid date
95105
if (isNaN(expiresAtTime)) {
96106
await debugLog(this.serverUrlHash, '⚠️ WARNING: Invalid expires_at detected while reading tokens ⚠️', {
@@ -99,8 +109,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
99109
stack: new Error('Invalid expires_at timestamp').stack
100110
})
101111
}
102-
103-
await debugLog(this.serverUrlHash, 'Token result:', {
112+
113+
await debugLog(this.serverUrlHash, 'Token result:', {
104114
found: true,
105115
hasAccessToken: !!tokens.access_token,
106116
hasRefreshToken: !!tokens.refresh_token,
@@ -112,7 +122,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
112122
await debugLog(this.serverUrlHash, 'Token result: Not found')
113123
}
114124
}
115-
125+
116126
return tokens
117127
}
118128

@@ -126,7 +136,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
126136
const now = new Date()
127137
const expiresAtTime = expiresAt.getTime()
128138
const timeLeft = !isNaN(expiresAtTime) ? Math.round((expiresAtTime - now.getTime()) / 1000) : 0
129-
139+
130140
// Alert if expires_at produces an invalid date
131141
if (isNaN(expiresAtTime)) {
132142
await debugLog(this.serverUrlHash, '⚠️ WARNING: Invalid expires_at detected in tokens ⚠️', {
@@ -135,15 +145,15 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
135145
stack: new Error('Invalid expires_at timestamp').stack
136146
})
137147
}
138-
139-
await debugLog(this.serverUrlHash, 'Saving tokens', {
148+
149+
await debugLog(this.serverUrlHash, 'Saving tokens', {
140150
hasAccessToken: !!tokens.access_token,
141151
hasRefreshToken: !!tokens.refresh_token,
142152
expiresIn: `${timeLeft} seconds`,
143153
expiresAt: tokens.expires_at
144154
})
145155
}
146-
156+
147157
await writeJsonFile(this.serverUrlHash, 'tokens.json', tokens)
148158
}
149159

@@ -153,9 +163,9 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
153163
*/
154164
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
155165
log(`\nPlease authorize this client by visiting:\n${authorizationUrl.toString()}\n`)
156-
166+
157167
if (DEBUG) await debugLog(this.serverUrlHash, 'Redirecting to authorization URL', authorizationUrl.toString())
158-
168+
159169
try {
160170
await open(authorizationUrl.toString())
161171
log('Browser opened automatically.')

src/lib/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { EventEmitter } from 'events'
2+
import { OAuthClientInformationFull, OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js'
23

34
/**
45
* Options for creating an OAuth client provider
@@ -22,6 +23,10 @@ export interface OAuthProviderOptions {
2223
softwareId?: string
2324
/** Software version to use for OAuth registration */
2425
softwareVersion?: string
26+
/** Static OAuth client metadata to override default OAuth client metadata */
27+
staticOAuthClientMetadata?: StaticOAuthClientMetadata
28+
/** Static OAuth client information to use instead of OAuth registration */
29+
staticOAuthClientInfo?: StaticOAuthClientInformationFull
2530
}
2631

2732
/**
@@ -35,3 +40,7 @@ export interface OAuthCallbackServerOptions {
3540
/** Event emitter to signal when auth code is received */
3641
events: EventEmitter
3742
}
43+
44+
// optional tatic OAuth client information
45+
export type StaticOAuthClientMetadata = OAuthClientMetadata | null | undefined
46+
export type StaticOAuthClientInformationFull = OAuthClientInformationFull | null | undefined

0 commit comments

Comments
 (0)