kriasoft / kriasoft/oauth-callback

Add storage adapters for MCP SDK compatibility

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

Description

## 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:

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

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

```typescript
// 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;
set(key: string, tokens: MCPOAuthTokens): Promise;
delete(key: string): Promise;
clear(): Promise;
}

/**
* 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 {
const tokens = await store.get(key);
return tokens ? toMCPTokens(tokens) : null;
},

async set(key: string, mcpTokens: MCPOAuthTokens): Promise {
await store.set(key, fromMCPTokens(mcpTokens));
},

async delete(key: string): Promise {
await store.delete(key);
},

async clear(): Promise {
await store.clear();
}
};
}

/**
* Creates an oauth-callback compatible store from MCP store
*/
export function fromMCPStore(mcpStore: MCPTokenStore): TokenStore {
return {
async get(key: string): Promise {
const mcpTokens = await mcpStore.get(key);
return mcpTokens ? fromMCPTokens(mcpTokens) : null;
},

async set(key: string, tokens: Tokens): Promise {
await mcpStore.set(key, toMCPTokens(tokens));
},

async delete(key: string): Promise {
await mcpStore.delete(key);
},

async clear(): Promise {
await mcpStore.clear();
}
};
}
```

### Option 2: Enhanced browserAuth with MCP compatibility

```typescript
// 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 {
const tokens = await baseProvider.getTokens();
return tokens ? toMCPTokens(tokens) : null;
},

async setTokens(mcpTokens: MCPOAuthTokens): Promise {
await baseProvider.setTokens(fromMCPTokens(mcpTokens));
},

// ... other methods
};
}
```

### Option 3: Export compatibility layer

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

```typescript
// 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
- mcp-client-gen package that highlighted this issue
- MCP SDK: @modelcontextprotocol/sdk
- Issue #16 (missing type exports) - related type compatibility issue

Contributor guide

Open the contributing guide

Research direction

Start by reading the existing token and client-information types, TokenStore implementation, package exports, and the proposed src/adapters/mcp.ts, src/auth/browser-auth.ts, and README locations. Resolve the adapter scope, MCP dependency and versioning approach before implementing. Done means documented bidirectional conversions and store compatibility with round-trip, edge-case, type-safety, and integration tests.

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
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.