Skip to content

Commit 396af15

Browse files
feat(mcp-oauth): support static clientId for servers without DCR
Add optional oauth.clientId field to MCP server configuration schema. When provided, the OAuth provider uses this clientId directly instead of performing Dynamic Client Registration (DCR). This enables connections to OAuth-protected MCP servers that don't support RFC 7591 DCR. Changes: - BaseConfigSchema: add oauth.clientId optional field - McpOAuthClientProvider: accept clientId in create() options, use it in registerClientIfNeeded() to skip DCR - McpHub: pass oauth.clientId from config to the provider - Tests: 2 new tests covering static clientId and precedence over cache Aligned with VS Code 1.122 feature: MCP OAuth with custom clientId.
1 parent 4d71e5f commit 396af15

3 files changed

Lines changed: 65 additions & 1 deletion

File tree

src/services/mcp/McpHub.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ const BaseConfigSchema = z.object({
7575
alwaysAllow: z.array(z.string()).default([]),
7676
watchPaths: z.array(z.string()).optional(), // paths to watch for changes and restart server
7777
disabledTools: z.array(z.string()).default([]),
78+
oauth: z
79+
.object({
80+
clientId: z.string().optional(),
81+
})
82+
.optional(),
7883
})
7984

8085
// Custom error messages for better user feedback
@@ -807,6 +812,7 @@ export class McpHub {
807812

808813
const authProvider = await McpOAuthClientProvider.create(configInjected.url, this.secretStorage, name, {
809814
skipDiscovery,
815+
clientId: configInjected.oauth?.clientId,
810816
})
811817

812818
// Pre-register the OAuth client so the SDK can skip its own

src/services/mcp/McpOAuthClientProvider.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
8686
private readonly _authServerMeta: Record<string, any> | null,
8787
private readonly _resourceIndicator: string | null,
8888
private readonly _clientName: string,
89+
private readonly _staticClientId?: string,
8990
) {}
9091

9192
/**
@@ -102,7 +103,7 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
102103
serverUrl: string,
103104
secretStorage: SecretStorageService,
104105
serverName?: string,
105-
options?: { skipDiscovery?: boolean },
106+
options?: { skipDiscovery?: boolean; clientId?: string },
106107
): Promise<McpOAuthClientProvider> {
107108
let authServerMeta: Record<string, any> | null = null
108109
let resourceIndicator: string | null = null
@@ -151,6 +152,7 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
151152
authServerMeta,
152153
resourceIndicator,
153154
serverName || "Roo Code",
155+
options?.clientId,
154156
)
155157
}
156158

@@ -228,6 +230,17 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
228230
async registerClientIfNeeded(): Promise<void> {
229231
if (this._clientInfo) return // already registered
230232

233+
// If a static clientId was provided (e.g. from mcp.json oauth.clientId),
234+
// use it directly instead of performing Dynamic Client Registration.
235+
// This enables connections to OAuth servers that don't support DCR.
236+
if (this._staticClientId) {
237+
this._clientInfo = {
238+
client_id: this._staticClientId,
239+
redirect_uris: [this.redirectUrl],
240+
}
241+
return
242+
}
243+
231244
// Check if we have a cached client_id from previous registration
232245
const cachedData = await this._secretStorage.getOAuthData(this._serverUrl)
233246
if (cachedData?.client_info) {

src/services/mcp/__tests__/McpOAuthClientProvider.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,4 +849,49 @@ describe("McpOAuthClientProvider", () => {
849849
await provider.close()
850850
})
851851
})
852+
853+
describe("static clientId support", () => {
854+
it("should use static clientId instead of performing DCR", async () => {
855+
const secretStorage = createMockSecretStorage()
856+
const provider = await McpOAuthClientProvider.create(
857+
"https://example.com/mcp",
858+
secretStorage,
859+
"test-server",
860+
{ clientId: "my-static-client-id" },
861+
)
862+
863+
await provider.registerClientIfNeeded()
864+
865+
const info = await provider.clientInformation()
866+
expect(info?.client_id).toBe("my-static-client-id")
867+
await provider.close()
868+
})
869+
870+
it("should use static clientId even when cached data exists", async () => {
871+
setupCallbackServerMock()
872+
const secretStorage = createMockSecretStorage()
873+
874+
await secretStorage.saveOAuthData("https://example.com/mcp", {
875+
tokens: { access_token: "cached-token", token_type: "Bearer" },
876+
expires_at: Date.now() + 3600_000,
877+
client_info: {
878+
client_id: "cached-client-id",
879+
redirect_uris: ["http://localhost:0/callback"],
880+
},
881+
})
882+
883+
const provider = await McpOAuthClientProvider.create(
884+
"https://example.com/mcp",
885+
secretStorage,
886+
"test-server",
887+
{ clientId: "my-static-client-id" },
888+
)
889+
890+
await provider.registerClientIfNeeded()
891+
892+
const info = await provider.clientInformation()
893+
expect(info?.client_id).toBe("my-static-client-id")
894+
await provider.close()
895+
})
896+
})
852897
})

0 commit comments

Comments
 (0)