Skip to content

Add MCP compatibility mode to handle SDK-specific requirements #18

@koistya

Description

@koistya

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

  1. getPendingAuthCode() requirement:

    // MCP SDK expects this method with specific return format
    getPendingAuthCode(): { url: string; cleanupUrl: string } | undefined {
      // Must return pending auth URL or undefined
    }
  2. addClientAuthentication must be undefined:

    // ❌ This breaks MCP SDK
    addClientAuthentication: null  // SDK checks === undefined
    
    // ✅ This works
    addClientAuthentication: undefined  // or omit entirely
  3. 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

  1. Create MCP compatibility mode that handles all SDK-specific requirements
  2. Implement required methods with correct signatures and behaviors
  3. Add guards for unexpected method calls (like invalidateCredentials during success)
  4. Provide preset configuration for common MCP scenarios
  5. 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

⚠️ Note: This feature requires critical thinking during implementation. Consider:

  1. Version detection: Should we detect MCP SDK version and adjust behavior accordingly?

  2. Strict mode: Should MCP mode enforce stricter behaviors or be more lenient?

  3. Alternative approach: Should we contribute these fixes upstream to MCP SDK instead?

  4. Breaking changes: How do we handle if MCP SDK changes these requirements?

  5. 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

  1. Method existence: Verify all required MCP methods are present
  2. Undefined handling: Test that addClientAuthentication is exactly undefined
  3. Guard behavior: Test invalidateCredentials guard during token exchange
  4. MCP SDK integration: Test with actual MCP SDK if possible
  5. Edge cases: Test timeout, cancellation, concurrent auth flows
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions