kriasoft / kriasoft/oauth-callback

Add MCP compatibility mode to handle SDK-specific requirements

Open
#18 0 comments 0 reactions 0 assignees View on GitHub
enhancement good first issue
Dominant language
TypeScript
Stars
11
Forks
4
PR merge metrics
No merged PRs in 30d

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:

```typescript
// ❌ 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**:
```typescript
// 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`**:
```typescript
// ❌ This breaks MCP SDK
addClientAuthentication: null // SDK checks === undefined

// ✅ This works
addClientAuthentication: undefined // or omit entirely
```

3. **`invalidateCredentials()` timing**:
```typescript
// MCP SDK calls this DURING token exchange, not just on error
// This can clear tokens at unexpected times
invalidateCredentials(): Promise {
// 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

```typescript
// 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 {
return baseProvider.getTokens(signal);
},

async authorize(params: any, signal?: AbortSignal): Promise {
// 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 {
// 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 {
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

```typescript
// 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

```typescript
// 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

```typescript
// 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
- Issue #17 (Storage interface mismatch) - Related MCP compatibility issue
- MCP SDK: @modelcontextprotocol/sdk
- mcp-client-gen package that discovered these quirks

Contributor guide

Open the contributing guide

Research direction

Start by reading the existing browserAuth entry point referenced in src/auth/browser-auth.ts, then compare the proposed src/auth/mcp-compat.ts and src/mcp/utils.ts approaches. Confirm the MCP provider requirements, presets, guards, and version-testing strategy before choosing a design; done means documented, tested compatibility with the required method signatures and behaviors.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, authentication
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.