-
-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Description
The MCP SDK (@modelcontextprotocol/sdk) has specific requirements and quirks that make integration with oauth-callback more complex than necessary. These SDK-specific behaviors require special handling that isn't obvious to developers, leading to integration failures and confusion.
Current Problems
When using oauth-callback's browserAuth() with MCP SDK, developers encounter several undocumented requirements:
// ❌ Current situation - mysterious failures
const authProvider = browserAuth({
store: fileStore()
});
// MCP SDK expects these specific methods/behaviors:
// 1. getPendingAuthCode() must exist and return specific format
// 2. addClientAuthentication must be undefined (not null, not a function)
// 3. invalidateCredentials() called during token exchange (unexpected timing)
// Developers have to discover these through trial and error!Specific MCP SDK Quirks
-
getPendingAuthCode()requirement:// MCP SDK expects this method with specific return format getPendingAuthCode(): { url: string; cleanupUrl: string } | undefined { // Must return pending auth URL or undefined }
-
addClientAuthenticationmust beundefined:// ❌ This breaks MCP SDK addClientAuthentication: null // SDK checks === undefined // ✅ This works addClientAuthentication: undefined // or omit entirely
-
invalidateCredentials()timing:// MCP SDK calls this DURING token exchange, not just on error // This can clear tokens at unexpected times invalidateCredentials(): Promise<void> { // Must handle being called mid-flow }
What needs to be done
- Create MCP compatibility mode that handles all SDK-specific requirements
- Implement required methods with correct signatures and behaviors
- Add guards for unexpected method calls (like invalidateCredentials during success)
- Provide preset configuration for common MCP scenarios
- Document MCP-specific behaviors clearly
Why this matters
- Developer frustration: These quirks aren't documented, causing hours of debugging
- Integration failures: Apps break mysteriously when MCP SDK updates
- Adoption barrier: Makes oauth-callback harder to use with MCP
- Support burden: These issues generate repeated support questions
Without MCP compatibility mode:
- Every developer rediscovers the same quirks
- Brittle integrations that break easily
- Incompatible with MCP SDK updates
- Poor developer experience
Implementation considerations
-
Version detection: Should we detect MCP SDK version and adjust behavior accordingly?
-
Strict mode: Should MCP mode enforce stricter behaviors or be more lenient?
-
Alternative approach: Should we contribute these fixes upstream to MCP SDK instead?
-
Breaking changes: How do we handle if MCP SDK changes these requirements?
-
Testing strategy: How do we test against different MCP SDK versions?
Suggested implementation
Option 1: MCP Compatibility Mode
// src/auth/mcp-compat.ts
import type { OAuthProvider } from '@modelcontextprotocol/sdk';
import { browserAuth, type BrowserAuthOptions } from './browser-auth';
/**
* Creates an MCP SDK-compatible OAuth provider with all required quirks handled
*
* @example
* ```typescript
* import { mcpAuth } from 'oauth-callback/mcp';
*
* const authProvider = mcpAuth({
* store: fileStore(),
* clientId: 'optional-client-id'
* });
*
* // Use directly with MCP SDK - all quirks handled!
* const transport = new StreamableHTTPClientTransport(url, {
* authProvider
* });
* ```
*/
export function mcpAuth(options: BrowserAuthOptions = {}): OAuthProvider {
const baseProvider = browserAuth(options);
let pendingAuth: { url: string; cleanupUrl: string } | undefined;
let isTokenExchangeActive = false;
return {
// Required: MCP SDK expects exactly these methods
async getTokens(signal?: AbortSignal): Promise<any> {
return baseProvider.getTokens(signal);
},
async authorize(params: any, signal?: AbortSignal): Promise<any> {
// Track that we're starting authorization
const authUrl = params.url || buildAuthUrl(params);
pendingAuth = {
url: authUrl,
cleanupUrl: `http://localhost:${options.port || 3000}/cleanup`
};
try {
isTokenExchangeActive = true;
const result = await baseProvider.authorize(params, signal);
return result;
} finally {
isTokenExchangeActive = false;
pendingAuth = undefined;
}
},
// QUIRK 1: MCP SDK requires this method
getPendingAuthCode(): { url: string; cleanupUrl: string } | undefined {
return pendingAuth;
},
// QUIRK 2: Must be undefined, not null or missing
addClientAuthentication: undefined as any,
// QUIRK 3: Called during token exchange, must handle gracefully
async invalidateCredentials(): Promise<void> {
// Only actually invalidate if NOT in middle of token exchange
if (!isTokenExchangeActive) {
await baseProvider.invalidateCredentials?.();
}
// Otherwise ignore - MCP SDK calls this at weird times
},
// Optional: MCP SDK checks for this
async refreshTokens(signal?: AbortSignal): Promise<any> {
if (baseProvider.refreshTokens) {
return baseProvider.refreshTokens(signal);
}
// If no refresh available, invalidate to force re-auth
await this.invalidateCredentials();
throw new Error('Token refresh not available');
}
};
}
/**
* MCP-specific configuration presets
*/
export const MCPPresets = {
/**
* Standard MCP server configuration
*/
standard: {
port: 3000,
callbackPath: '/callback',
validateState: true,
pkce: true, // MCP servers often require PKCE
},
/**
* GitHub Copilot MCP configuration
*/
copilot: {
port: 3000,
callbackPath: '/auth/callback',
scope: 'read write',
// Copilot-specific settings
},
/**
* Anthropic Claude MCP configuration
*/
claude: {
port: 3000,
callbackPath: '/callback',
// Claude-specific settings
}
};
/**
* Helper to detect if running in MCP context
*/
export function isMCPEnvironment(): boolean {
// Check for MCP-specific environment variables or context
return (
process.env.MCP_SERVER === 'true' ||
process.env.MODEL_CONTEXT_PROTOCOL === '1' ||
// Check if MCP SDK is loaded
typeof globalThis.__MCP_VERSION__ !== 'undefined'
);
}
/**
* Enhanced browserAuth that auto-detects MCP context
*/
export function smartAuth(options: BrowserAuthOptions = {}) {
if (isMCPEnvironment() || options.mcpMode) {
return mcpAuth(options);
}
return browserAuth(options);
}Option 2: Extend existing browserAuth with MCP mode
// src/auth/browser-auth.ts
export interface BrowserAuthOptions {
// ... existing options
/**
* Enable MCP SDK compatibility mode
* Handles all MCP-specific quirks automatically
*/
mcpMode?: boolean | 'strict' | 'lenient';
/**
* MCP preset configuration
*/
mcpPreset?: 'standard' | 'copilot' | 'claude';
}
export function browserAuth(options: BrowserAuthOptions = {}) {
// Apply MCP preset if specified
if (options.mcpPreset) {
options = { ...MCPPresets[options.mcpPreset], ...options };
}
const provider = createBaseProvider(options);
// Wrap with MCP compatibility if needed
if (options.mcpMode) {
return wrapForMCP(provider, options.mcpMode);
}
return provider;
}
function wrapForMCP(provider: any, mode: boolean | 'strict' | 'lenient') {
let pendingAuth: any;
let tokenExchangeGuard = false;
// Add MCP-required methods
provider.getPendingAuthCode = () => pendingAuth;
// Ensure addClientAuthentication is exactly undefined
if ('addClientAuthentication' in provider) {
delete provider.addClientAuthentication;
}
provider.addClientAuthentication = undefined;
// Wrap invalidateCredentials with guard
const originalInvalidate = provider.invalidateCredentials;
provider.invalidateCredentials = async function() {
if (mode === 'strict' && tokenExchangeGuard) {
console.warn('MCP SDK called invalidateCredentials during token exchange - ignoring');
return;
}
if (originalInvalidate) {
return originalInvalidate.call(this);
}
};
// Track token exchange state
const originalAuthorize = provider.authorize;
provider.authorize = async function(...args: any[]) {
tokenExchangeGuard = true;
try {
const result = await originalAuthorize.apply(this, args);
return result;
} finally {
tokenExchangeGuard = false;
}
};
return provider;
}Option 3: MCP compatibility utilities
// src/mcp/utils.ts
/**
* Validates that a provider meets MCP SDK requirements
*/
export function validateMCPProvider(provider: any): string[] {
const issues: string[] = [];
// Check required methods
if (typeof provider.getTokens !== 'function') {
issues.push('Missing required method: getTokens()');
}
if (typeof provider.authorize !== 'function') {
issues.push('Missing required method: authorize()');
}
if (typeof provider.getPendingAuthCode !== 'function') {
issues.push('Missing required method: getPendingAuthCode()');
}
// Check problematic properties
if (provider.addClientAuthentication !== undefined) {
issues.push('addClientAuthentication must be undefined');
}
if (provider.invalidateCredentials && typeof provider.invalidateCredentials !== 'function') {
issues.push('invalidateCredentials must be a function or undefined');
}
return issues;
}
/**
* Patches a provider to be MCP-compatible
*/
export function patchForMCP(provider: any): any {
const issues = validateMCPProvider(provider);
if (issues.length === 0) {
return provider; // Already compatible
}
// Apply automatic fixes
const patched = { ...provider };
if (!patched.getPendingAuthCode) {
patched.getPendingAuthCode = () => undefined;
}
if ('addClientAuthentication' in patched) {
patched.addClientAuthentication = undefined;
}
// Add guard for invalidateCredentials
if (patched.invalidateCredentials) {
const original = patched.invalidateCredentials;
let guard = false;
patched.invalidateCredentials = function(...args: any[]) {
if (guard) {
console.warn('Blocked recursive invalidateCredentials call');
return Promise.resolve();
}
guard = true;
try {
return original.apply(this, args);
} finally {
guard = false;
}
};
}
return patched;
}Usage examples
// Example 1: Simple MCP mode
import { browserAuth } from 'oauth-callback';
const authProvider = browserAuth({
mcpMode: true, // Enable all MCP quirks handling
store: fileStore()
});
// Example 2: Using MCP preset
import { browserAuth } from 'oauth-callback';
const authProvider = browserAuth({
mcpPreset: 'claude', // Use Claude-specific settings
store: fileStore()
});
// Example 3: Direct MCP auth
import { mcpAuth } from 'oauth-callback/mcp';
const authProvider = mcpAuth({
clientId: 'my-client',
store: fileStore()
});
// Example 4: Auto-detection
import { smartAuth } from 'oauth-callback';
// Automatically uses MCP mode if detected
const authProvider = smartAuth({
store: fileStore()
});
// Example 5: Validation and patching
import { validateMCPProvider, patchForMCP } from 'oauth-callback/mcp';
const provider = createCustomProvider();
const issues = validateMCPProvider(provider);
if (issues.length > 0) {
console.warn('Provider has MCP compatibility issues:', issues);
const fixed = patchForMCP(provider);
// Use fixed provider
}Testing requirements
- Method existence: Verify all required MCP methods are present
- Undefined handling: Test that addClientAuthentication is exactly undefined
- Guard behavior: Test invalidateCredentials guard during token exchange
- MCP SDK integration: Test with actual MCP SDK if possible
- Edge cases: Test timeout, cancellation, concurrent auth flows
- Preset validation: Verify presets work with their intended servers
Documentation needs
- Add "MCP SDK Integration" guide to docs
- Document all MCP-specific quirks and why they exist
- Provide troubleshooting guide for common MCP issues
- Include examples for each MCP preset
Skills required
- TypeScript
- OAuth 2.0 provider implementation
- MCP SDK knowledge (helpful)
- Defensive programming
- API compatibility patterns
Difficulty
Easy to Medium - Mostly about handling edge cases and adding compatibility wrappers. Good for someone who wants to improve integration robustness!
Related
- Issue Add storage adapters for MCP SDK compatibility #17 (Storage interface mismatch) - Related MCP compatibility issue
- MCP SDK: @modelcontextprotocol/sdk
- mcp-client-gen package that discovered these quirks