Bug: add resource parameter to MCP OAuth Flow (#4981)

Co-authored-by: Your Name <you@example.com>
This commit is contained in:
Brian Ray
2025-07-27 14:09:45 -04:00
committed by GitHub
parent 576cebc928
commit c45c14ee0e
3 changed files with 49 additions and 10 deletions

View File

@@ -151,6 +151,7 @@ describe('MCPOAuthProvider', () => {
expect.objectContaining({ accessToken: 'access_token_123' }), expect.objectContaining({ accessToken: 'access_token_123' }),
'test-client-id', 'test-client-id',
'https://auth.example.com/token', 'https://auth.example.com/token',
undefined,
); );
}); });
@@ -551,6 +552,7 @@ describe('MCPOAuthProvider', () => {
expect.objectContaining({ accessToken: 'new_access_token' }), expect.objectContaining({ accessToken: 'new_access_token' }),
'test-client-id', 'test-client-id',
'https://auth.example.com/token', 'https://auth.example.com/token',
undefined,
); );
}); });

View File

@@ -272,11 +272,13 @@ export class MCPOAuthProvider {
* *
* @param config OAuth configuration * @param config OAuth configuration
* @param pkceParams PKCE parameters * @param pkceParams PKCE parameters
* @param mcpServerUrl The MCP server URL to use as the resource parameter
* @returns The authorization URL * @returns The authorization URL
*/ */
private static buildAuthorizationUrl( private static buildAuthorizationUrl(
config: MCPOAuthConfig, config: MCPOAuthConfig,
pkceParams: PKCEParams, pkceParams: PKCEParams,
mcpServerUrl?: string,
): string { ): string {
const redirectUri = const redirectUri =
config.redirectUri || config.redirectUri ||
@@ -296,10 +298,15 @@ export class MCPOAuthProvider {
} }
// Add resource parameter for MCP OAuth spec compliance // Add resource parameter for MCP OAuth spec compliance
params.append( // Use the MCP server URL if provided, otherwise fall back to authorization URL
'resource', const resourceUrl = mcpServerUrl || config.authorizationUrl!;
OAuthUtils.buildResourceParameter(config.authorizationUrl!), try {
params.append('resource', OAuthUtils.buildResourceParameter(resourceUrl));
} catch (error) {
throw new Error(
`Invalid resource URL: "${resourceUrl}". ${getErrorMessage(error)}`,
); );
}
return `${config.authorizationUrl}?${params.toString()}`; return `${config.authorizationUrl}?${params.toString()}`;
} }
@@ -310,12 +317,14 @@ export class MCPOAuthProvider {
* @param config OAuth configuration * @param config OAuth configuration
* @param code Authorization code * @param code Authorization code
* @param codeVerifier PKCE code verifier * @param codeVerifier PKCE code verifier
* @param mcpServerUrl The MCP server URL to use as the resource parameter
* @returns The token response * @returns The token response
*/ */
private static async exchangeCodeForToken( private static async exchangeCodeForToken(
config: MCPOAuthConfig, config: MCPOAuthConfig,
code: string, code: string,
codeVerifier: string, codeVerifier: string,
mcpServerUrl?: string,
): Promise<OAuthTokenResponse> { ): Promise<OAuthTokenResponse> {
const redirectUri = const redirectUri =
config.redirectUri || config.redirectUri ||
@@ -334,10 +343,15 @@ export class MCPOAuthProvider {
} }
// Add resource parameter for MCP OAuth spec compliance // Add resource parameter for MCP OAuth spec compliance
params.append( // Use the MCP server URL if provided, otherwise fall back to token URL
'resource', const resourceUrl = mcpServerUrl || config.tokenUrl!;
OAuthUtils.buildResourceParameter(config.tokenUrl!), try {
params.append('resource', OAuthUtils.buildResourceParameter(resourceUrl));
} catch (error) {
throw new Error(
`Invalid resource URL: "${resourceUrl}". ${getErrorMessage(error)}`,
); );
}
const response = await fetch(config.tokenUrl!, { const response = await fetch(config.tokenUrl!, {
method: 'POST', method: 'POST',
@@ -362,12 +376,15 @@ export class MCPOAuthProvider {
* *
* @param config OAuth configuration * @param config OAuth configuration
* @param refreshToken The refresh token * @param refreshToken The refresh token
* @param tokenUrl The token endpoint URL
* @param mcpServerUrl The MCP server URL to use as the resource parameter
* @returns The new token response * @returns The new token response
*/ */
static async refreshAccessToken( static async refreshAccessToken(
config: MCPOAuthConfig, config: MCPOAuthConfig,
refreshToken: string, refreshToken: string,
tokenUrl: string, tokenUrl: string,
mcpServerUrl?: string,
): Promise<OAuthTokenResponse> { ): Promise<OAuthTokenResponse> {
const params = new URLSearchParams({ const params = new URLSearchParams({
grant_type: 'refresh_token', grant_type: 'refresh_token',
@@ -384,7 +401,15 @@ export class MCPOAuthProvider {
} }
// Add resource parameter for MCP OAuth spec compliance // Add resource parameter for MCP OAuth spec compliance
params.append('resource', OAuthUtils.buildResourceParameter(tokenUrl)); // Use the MCP server URL if provided, otherwise fall back to token URL
const resourceUrl = mcpServerUrl || tokenUrl;
try {
params.append('resource', OAuthUtils.buildResourceParameter(resourceUrl));
} catch (error) {
throw new Error(
`Invalid resource URL: "${resourceUrl}". ${getErrorMessage(error)}`,
);
}
const response = await fetch(tokenUrl, { const response = await fetch(tokenUrl, {
method: 'POST', method: 'POST',
@@ -534,7 +559,11 @@ export class MCPOAuthProvider {
const pkceParams = this.generatePKCEParams(); const pkceParams = this.generatePKCEParams();
// Build authorization URL // Build authorization URL
const authUrl = this.buildAuthorizationUrl(config, pkceParams); const authUrl = this.buildAuthorizationUrl(
config,
pkceParams,
mcpServerUrl,
);
console.log('\nOpening browser for OAuth authentication...'); console.log('\nOpening browser for OAuth authentication...');
console.log('If the browser does not open, please visit:'); console.log('If the browser does not open, please visit:');
@@ -584,6 +613,7 @@ export class MCPOAuthProvider {
config, config,
code, code,
pkceParams.codeVerifier, pkceParams.codeVerifier,
mcpServerUrl,
); );
// Convert to our token format // Convert to our token format
@@ -605,6 +635,7 @@ export class MCPOAuthProvider {
token, token,
config.clientId, config.clientId,
config.tokenUrl, config.tokenUrl,
mcpServerUrl,
); );
console.log('Authentication successful! Token saved.'); console.log('Authentication successful! Token saved.');
@@ -664,6 +695,7 @@ export class MCPOAuthProvider {
config, config,
token.refreshToken, token.refreshToken,
credentials.tokenUrl, credentials.tokenUrl,
credentials.mcpServerUrl,
); );
// Update stored token // Update stored token
@@ -683,6 +715,7 @@ export class MCPOAuthProvider {
newToken, newToken,
config.clientId, config.clientId,
credentials.tokenUrl, credentials.tokenUrl,
credentials.mcpServerUrl,
); );
return newToken.accessToken; return newToken.accessToken;

View File

@@ -28,6 +28,7 @@ export interface MCPOAuthCredentials {
token: MCPOAuthToken; token: MCPOAuthToken;
clientId?: string; clientId?: string;
tokenUrl?: string; tokenUrl?: string;
mcpServerUrl?: string;
updatedAt: number; updatedAt: number;
} }
@@ -91,12 +92,14 @@ export class MCPOAuthTokenStorage {
* @param token The OAuth token to save * @param token The OAuth token to save
* @param clientId Optional client ID used for this token * @param clientId Optional client ID used for this token
* @param tokenUrl Optional token URL used for this token * @param tokenUrl Optional token URL used for this token
* @param mcpServerUrl Optional MCP server URL
*/ */
static async saveToken( static async saveToken(
serverName: string, serverName: string,
token: MCPOAuthToken, token: MCPOAuthToken,
clientId?: string, clientId?: string,
tokenUrl?: string, tokenUrl?: string,
mcpServerUrl?: string,
): Promise<void> { ): Promise<void> {
await this.ensureConfigDir(); await this.ensureConfigDir();
@@ -107,6 +110,7 @@ export class MCPOAuthTokenStorage {
token, token,
clientId, clientId,
tokenUrl, tokenUrl,
mcpServerUrl,
updatedAt: Date.now(), updatedAt: Date.now(),
}; };