Skip to content
This repository was archived by the owner on Dec 18, 2025. It is now read-only.

Commit 6e36247

Browse files
ruggishopJustinBeaudry
authored andcommitted
📮 feat: Custom OAuth Headers Support for MCP Server Config (danny-avila#10014)
* add oauth_headers field to mcp options * wrap fetch to pass oauth headers * fix order * consolidate headers passing * fix tests
1 parent 269aaad commit 6e36247

File tree

8 files changed

+304
-35
lines changed

8 files changed

+304
-35
lines changed

api/server/controllers/UserController.js

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -327,30 +327,43 @@ const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => {
327327
const revocationEndpointAuthMethodsSupported =
328328
serverConfig.oauth?.revocation_endpoint_auth_methods_supported ??
329329
clientMetadata.revocation_endpoint_auth_methods_supported;
330+
const oauthHeaders = serverConfig.oauth_headers ?? {};
330331

331332
if (tokens?.access_token) {
332333
try {
333-
await MCPOAuthHandler.revokeOAuthToken(serverName, tokens.access_token, 'access', {
334-
serverUrl: serverConfig.url,
335-
clientId: clientInfo.client_id,
336-
clientSecret: clientInfo.client_secret ?? '',
337-
revocationEndpoint,
338-
revocationEndpointAuthMethodsSupported,
339-
});
334+
await MCPOAuthHandler.revokeOAuthToken(
335+
serverName,
336+
tokens.access_token,
337+
'access',
338+
{
339+
serverUrl: serverConfig.url,
340+
clientId: clientInfo.client_id,
341+
clientSecret: clientInfo.client_secret ?? '',
342+
revocationEndpoint,
343+
revocationEndpointAuthMethodsSupported,
344+
},
345+
oauthHeaders,
346+
);
340347
} catch (error) {
341348
logger.error(`Error revoking OAuth access token for ${serverName}:`, error);
342349
}
343350
}
344351

345352
if (tokens?.refresh_token) {
346353
try {
347-
await MCPOAuthHandler.revokeOAuthToken(serverName, tokens.refresh_token, 'refresh', {
348-
serverUrl: serverConfig.url,
349-
clientId: clientInfo.client_id,
350-
clientSecret: clientInfo.client_secret ?? '',
351-
revocationEndpoint,
352-
revocationEndpointAuthMethodsSupported,
353-
});
354+
await MCPOAuthHandler.revokeOAuthToken(
355+
serverName,
356+
tokens.refresh_token,
357+
'refresh',
358+
{
359+
serverUrl: serverConfig.url,
360+
clientId: clientInfo.client_id,
361+
clientSecret: clientInfo.client_secret ?? '',
362+
revocationEndpoint,
363+
revocationEndpointAuthMethodsSupported,
364+
},
365+
oauthHeaders,
366+
);
354367
} catch (error) {
355368
logger.error(`Error revoking OAuth refresh token for ${serverName}:`, error);
356369
}

api/server/routes/__tests__/mcp.spec.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,13 @@ describe('MCP Routes', () => {
127127
}),
128128
};
129129

130+
const mockMcpManager = {
131+
getRawConfig: jest.fn().mockReturnValue({}),
132+
};
133+
130134
getLogStores.mockReturnValue({});
131135
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
136+
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
132137

133138
MCPOAuthHandler.initiateOAuthFlow.mockResolvedValue({
134139
authorizationUrl: 'https://oauth.example.com/auth',
@@ -146,6 +151,7 @@ describe('MCP Routes', () => {
146151
'test-server',
147152
'https://test-server.com',
148153
'test-user-id',
154+
{},
149155
{ clientId: 'test-client-id' },
150156
);
151157
});
@@ -314,6 +320,7 @@ describe('MCP Routes', () => {
314320
};
315321
const mockMcpManager = {
316322
getUserConnection: jest.fn().mockResolvedValue(mockUserConnection),
323+
getRawConfig: jest.fn().mockReturnValue({}),
317324
};
318325
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
319326

@@ -336,6 +343,7 @@ describe('MCP Routes', () => {
336343
'test-flow-id',
337344
'test-auth-code',
338345
mockFlowManager,
346+
{},
339347
);
340348
expect(MCPTokenStorage.storeTokens).toHaveBeenCalledWith(
341349
expect.objectContaining({
@@ -392,6 +400,11 @@ describe('MCP Routes', () => {
392400
getLogStores.mockReturnValue({});
393401
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
394402

403+
const mockMcpManager = {
404+
getRawConfig: jest.fn().mockReturnValue({}),
405+
};
406+
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
407+
395408
const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({
396409
code: 'test-auth-code',
397410
state: 'test-flow-id',
@@ -427,6 +440,7 @@ describe('MCP Routes', () => {
427440

428441
const mockMcpManager = {
429442
getUserConnection: jest.fn().mockRejectedValue(new Error('Reconnection failed')),
443+
getRawConfig: jest.fn().mockReturnValue({}),
430444
};
431445
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
432446

@@ -1234,6 +1248,7 @@ describe('MCP Routes', () => {
12341248
getUserConnection: jest.fn().mockResolvedValue({
12351249
fetchTools: jest.fn().mockResolvedValue([]),
12361250
}),
1251+
getRawConfig: jest.fn().mockReturnValue({}),
12371252
};
12381253
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
12391254

@@ -1281,6 +1296,7 @@ describe('MCP Routes', () => {
12811296
.fn()
12821297
.mockResolvedValue([{ name: 'test-tool', description: 'Test tool' }]),
12831298
}),
1299+
getRawConfig: jest.fn().mockReturnValue({}),
12841300
};
12851301
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
12861302

api/server/routes/mcp.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ router.get('/:serverName/oauth/initiate', requireJwtAuth, async (req, res) => {
6565
serverName,
6666
serverUrl,
6767
userId,
68+
getOAuthHeaders(serverName),
6869
oauthConfig,
6970
);
7071

@@ -132,7 +133,12 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
132133
});
133134

134135
logger.debug('[MCP OAuth] Completing OAuth flow');
135-
const tokens = await MCPOAuthHandler.completeOAuthFlow(flowId, code, flowManager);
136+
const tokens = await MCPOAuthHandler.completeOAuthFlow(
137+
flowId,
138+
code,
139+
flowManager,
140+
getOAuthHeaders(serverName),
141+
);
136142
logger.info('[MCP OAuth] OAuth flow completed, tokens received in callback route');
137143

138144
/** Persist tokens immediately so reconnection uses fresh credentials */
@@ -538,4 +544,10 @@ router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => {
538544
}
539545
});
540546

547+
function getOAuthHeaders(serverName) {
548+
const mcpManager = getMCPManager();
549+
const serverConfig = mcpManager.getRawConfig(serverName);
550+
return serverConfig?.oauth_headers ?? {};
551+
}
552+
541553
module.exports = router;

packages/api/src/mcp/MCPConnectionFactory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export class MCPConnectionFactory {
142142
serverName: metadata.serverName,
143143
clientInfo: metadata.clientInfo,
144144
},
145+
this.serverConfig.oauth_headers ?? {},
145146
this.serverConfig.oauth,
146147
);
147148
};
@@ -161,6 +162,7 @@ export class MCPConnectionFactory {
161162
this.serverName,
162163
data.serverUrl || '',
163164
this.userId!,
165+
config?.oauth_headers ?? {},
164166
config?.oauth,
165167
);
166168

@@ -358,6 +360,7 @@ export class MCPConnectionFactory {
358360
this.serverName,
359361
serverUrl,
360362
this.userId!,
363+
this.serverConfig.oauth_headers ?? {},
361364
this.serverConfig.oauth,
362365
);
363366

packages/api/src/mcp/__tests__/MCPConnectionFactory.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ describe('MCPConnectionFactory', () => {
255255
'test-server',
256256
'https://api.example.com',
257257
'user123',
258+
{},
258259
undefined,
259260
);
260261
expect(oauthOptions.oauthStart).toHaveBeenCalledWith('https://auth.example.com');

0 commit comments

Comments
 (0)