Skip to content

Add storage adapters for MCP SDK compatibility #17

@koistya

Description

@koistya

Description

There's a type mismatch between oauth-callback's storage format and the MCP SDK's storage format. The mcp-client-gen package uses MCP SDK's types (OAuthTokens, OAuthClientInformationFull) while oauth-callback uses different types (Tokens, ClientInfo). This forces developers to write adapter layers to bridge the two systems, adding unnecessary complexity and potential for errors.

Current Problem

When integrating oauth-callback with MCP SDK, developers encounter type incompatibilities:

// MCP SDK types (from @modelcontextprotocol/sdk)
interface OAuthTokens {
  accessToken: string;
  refreshToken?: string;
  expiresIn?: number;
  tokenType?: string;
  scope?: string;
}

interface OAuthClientInformationFull {
  clientId: string;
  clientSecret?: string;
  authorizationEndpoint: string;
  tokenEndpoint: string;
  scopes?: string[];
}

// oauth-callback types
interface Tokens {
  access_token: string;
  refresh_token?: string;
  expires_at?: string;  // Note: different from MCP's expiresIn
  token_type?: string;
  scope?: string;
}

interface ClientInfo {
  client_id: string;     // Note: snake_case vs camelCase
  client_secret?: string;
  // Different structure...
}

This mismatch forces developers to write error-prone adapter code:

// ❌ Current situation - manual adapter required
function adaptTokens(mcpTokens: OAuthTokens): Tokens {
  return {
    access_token: mcpTokens.accessToken,
    refresh_token: mcpTokens.refreshToken,
    expires_at: mcpTokens.expiresIn 
      ? new Date(Date.now() + mcpTokens.expiresIn * 1000).toISOString()
      : undefined,
    token_type: mcpTokens.tokenType,
    scope: mcpTokens.scope
  };
}

// Every integration needs this boilerplate!

What needs to be done

  1. Create adapter utilities that convert between oauth-callback and MCP SDK types
  2. Provide storage adapters that wrap oauth-callback stores for MCP SDK compatibility
  3. Add bidirectional conversion functions for tokens and client information
  4. Document integration patterns with MCP SDK

Why this matters

This is a high-priority issue because:

  • MCP ecosystem integration: MCP (Model Context Protocol) is becoming a standard for AI tool integration
  • Developer friction: Every MCP integration requires writing the same adapter code
  • Error-prone: Manual conversion between formats leads to bugs
  • Maintenance burden: Changes in either library break integrations
  • Growing adoption: More developers are building MCP servers and need OAuth

Without built-in adapters:

  • Developers waste time writing boilerplate
  • Integration errors from incorrect type mapping
  • Inconsistent implementations across projects
  • Barrier to MCP SDK adoption

Implementation considerations

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

  1. Peer dependency vs built-in: Should MCP SDK be a peer dependency or should adapters work without it installed?

  2. Type safety: How do we maintain type safety when MCP SDK types might not be available?

  3. Alternative approach: Should oauth-callback adopt MCP SDK's type format as the standard instead?

  4. Versioning: How do we handle different versions of MCP SDK with potentially different types?

  5. Scope: Should we also provide adapters for other popular OAuth libraries (Passport.js, node-oauth2-server)?

Suggested implementation

Option 1: Dedicated adapter module

// src/adapters/mcp.ts
import type { Tokens, TokenStore, ClientInfo } from '../types';

// Re-declare MCP types to avoid hard dependency
interface MCPOAuthTokens {
  accessToken: string;
  refreshToken?: string;
  expiresIn?: number;
  tokenType?: string;
  scope?: string;
}

interface MCPOAuthClientInfo {
  clientId: string;
  clientSecret?: string;
  authorizationEndpoint: string;
  tokenEndpoint: string;
  scopes?: string[];
}

interface MCPTokenStore {
  get(key: string): Promise<MCPOAuthTokens | null>;
  set(key: string, tokens: MCPOAuthTokens): Promise<void>;
  delete(key: string): Promise<void>;
  clear(): Promise<void>;
}

/**
 * Converts MCP SDK tokens to oauth-callback format
 * @example
 * ```typescript
 * import { fromMCPTokens } from 'oauth-callback/adapters/mcp';
 * 
 * const mcpTokens = await mcpStore.get('key');
 * const tokens = fromMCPTokens(mcpTokens);
 * ```
 */
export function fromMCPTokens(mcp: MCPOAuthTokens): Tokens {
  return {
    access_token: mcp.accessToken,
    refresh_token: mcp.refreshToken,
    expires_at: mcp.expiresIn 
      ? new Date(Date.now() + mcp.expiresIn * 1000).toISOString()
      : undefined,
    token_type: mcp.tokenType || 'Bearer',
    scope: mcp.scope
  };
}

/**
 * Converts oauth-callback tokens to MCP SDK format
 */
export function toMCPTokens(tokens: Tokens): MCPOAuthTokens {
  const expiresIn = tokens.expires_at
    ? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000)
    : undefined;
    
  return {
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiresIn: expiresIn && expiresIn > 0 ? expiresIn : undefined,
    tokenType: tokens.token_type,
    scope: tokens.scope
  };
}

/**
 * Converts MCP client info to oauth-callback format
 */
export function fromMCPClientInfo(mcp: MCPOAuthClientInfo): ClientInfo {
  return {
    client_id: mcp.clientId,
    client_secret: mcp.clientSecret,
    // Map other fields as needed
  };
}

/**
 * Converts oauth-callback client info to MCP format
 */
export function toMCPClientInfo(
  info: ClientInfo,
  endpoints: { authorizationEndpoint: string; tokenEndpoint: string }
): MCPOAuthClientInfo {
  return {
    clientId: info.client_id,
    clientSecret: info.client_secret,
    authorizationEndpoint: endpoints.authorizationEndpoint,
    tokenEndpoint: endpoints.tokenEndpoint,
    scopes: info.scope?.split(' ')
  };
}

/**
 * Wraps an oauth-callback TokenStore to be compatible with MCP SDK
 * @example
 * ```typescript
 * import { fileStore } from 'oauth-callback';
 * import { createMCPStore } from 'oauth-callback/adapters/mcp';
 * 
 * const store = fileStore();
 * const mcpStore = createMCPStore(store);
 * 
 * // Now use mcpStore with MCP SDK
 * const transport = new StreamableHTTPClientTransport(url, {
 *   authProvider: {
 *     getTokens: () => mcpStore.get('default'),
 *     setTokens: (tokens) => mcpStore.set('default', tokens)
 *   }
 * });
 * ```
 */
export function createMCPStore(store: TokenStore): MCPTokenStore {
  return {
    async get(key: string): Promise<MCPOAuthTokens | null> {
      const tokens = await store.get(key);
      return tokens ? toMCPTokens(tokens) : null;
    },
    
    async set(key: string, mcpTokens: MCPOAuthTokens): Promise<void> {
      await store.set(key, fromMCPTokens(mcpTokens));
    },
    
    async delete(key: string): Promise<void> {
      await store.delete(key);
    },
    
    async clear(): Promise<void> {
      await store.clear();
    }
  };
}

/**
 * Creates an oauth-callback compatible store from MCP store
 */
export function fromMCPStore(mcpStore: MCPTokenStore): TokenStore {
  return {
    async get(key: string): Promise<Tokens | null> {
      const mcpTokens = await mcpStore.get(key);
      return mcpTokens ? fromMCPTokens(mcpTokens) : null;
    },
    
    async set(key: string, tokens: Tokens): Promise<void> {
      await mcpStore.set(key, toMCPTokens(tokens));
    },
    
    async delete(key: string): Promise<void> {
      await mcpStore.delete(key);
    },
    
    async clear(): Promise<void> {
      await mcpStore.clear();
    }
  };
}

Option 2: Enhanced browserAuth with MCP compatibility

// src/auth/browser-auth.ts

export interface BrowserAuthOptions {
  // ... existing options
  
  /**
   * Enable MCP SDK compatibility mode
   * Automatically converts between token formats
   */
  mcpCompatibility?: boolean;
}

export function browserAuth(options: BrowserAuthOptions = {}) {
  if (options.mcpCompatibility) {
    // Return MCP-compatible provider
    return createMCPAuthProvider(options);
  }
  
  // Standard implementation
  return createStandardAuthProvider(options);
}

function createMCPAuthProvider(options: BrowserAuthOptions) {
  const baseProvider = createStandardAuthProvider(options);
  
  // Wrap with format conversion
  return {
    async getTokens(): Promise<MCPOAuthTokens | null> {
      const tokens = await baseProvider.getTokens();
      return tokens ? toMCPTokens(tokens) : null;
    },
    
    async setTokens(mcpTokens: MCPOAuthTokens): Promise<void> {
      await baseProvider.setTokens(fromMCPTokens(mcpTokens));
    },
    
    // ... other methods
  };
}

Option 3: Export compatibility layer

// src/index.ts
export * as MCP from './adapters/mcp';

// Usage:
import { MCP } from 'oauth-callback';

const mcpStore = MCP.createMCPStore(fileStore());
const tokens = MCP.fromMCPTokens(mcpTokens);

Usage examples

// Example 1: Using oauth-callback with MCP SDK
import { fileStore } from 'oauth-callback';
import { createMCPStore } from 'oauth-callback/adapters/mcp';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';

const store = createMCPStore(fileStore());
const authProvider = {
  getTokens: () => store.get('mcp-server'),
  setTokens: (tokens) => store.set('mcp-server', tokens)
};

// Example 2: Converting existing MCP tokens
import { fromMCPTokens, toMCPTokens } from 'oauth-callback/adapters/mcp';

const mcpTokens = await mcpClient.getTokens();
const oauthTokens = fromMCPTokens(mcpTokens);
// Use with oauth-callback
await myStore.set('key', oauthTokens);

// Example 3: Bidirectional compatibility
const adapter = createBidirectionalAdapter(oauthStore, mcpStore);
await adapter.sync(); // Sync tokens between systems

Testing requirements

  1. Round-trip conversion: Verify tokens survive conversion in both directions
  2. Edge cases: Handle missing fields, expired tokens, null values
  3. Type safety: Ensure TypeScript catches type mismatches
  4. Integration tests: Test with actual MCP SDK if available
  5. Performance: Verify adapters don't add significant overhead

Documentation needs

  • Add "MCP Integration" section to README
  • Provide complete examples for common MCP scenarios
  • Document field mapping between formats
  • Migration guide for existing MCP integrations

Skills required

  • TypeScript (type conversion and adapters)
  • OAuth 2.0 token formats
  • Adapter pattern implementation
  • API design (maintaining compatibility)
  • MCP SDK knowledge (helpful but not required)

Difficulty

Easy - This is primarily about creating type conversions and adapter wrappers. Great for someone who wants to improve integration capabilities!

Priority

HIGH - This directly impacts anyone trying to use oauth-callback with MCP SDK, which is increasingly common for AI tool development.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or requestgood first issueGood for newcomers

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions