Skip to main content
Glama

ChatGPT App with OAuth2 + MCP + Privy

by Jahnik
OAUTH_COMPARISON.mdโ€ข32 kB
# OAuth and Privy Authentication Implementation Comparison ## mcp2 vs mcp --- ## EXECUTIVE SUMMARY The two implementations have **fundamentally different architectural approaches** to OAuth and authentication. The **mcp2 implementation is significantly more complete and production-ready**, with proper JWT token handling, JWKS endpoints, and correct MCP integration. The **original mcp implementation relies on simple token mapping and lacks critical OAuth endpoints**, which could cause ChatGPT's OAuth flow to fail. **Critical Issue Found**: The mcp2 properly validates and transforms tokens through the OAuth flow, while mcp uses direct token mapping that bypasses proper validation. This is likely why ChatGPT reports a "connection problem" despite the flow completing - the token isn't properly validated at the MCP endpoint. --- ## 1. OAUTH FLOW IMPLEMENTATION ### mcp2 (New Implementation) **File: `/Users/jahnik/index-network/mcp2/src/server/oauth/authorize.ts`** - **GET /authorize**: Returns a 200 response for initial auth requests, passes validation - Validates PKCE (code_challenge, code_challenge_method) - Requires S256 code_challenge_method - Stores authorization code with expiry (30 seconds) - React frontend handles Privy authentication - **POST /authorize**: Receives consent from frontend - Validates privy_user_id and privy_token from request - Stores Privy token in authorization code for later exchange - Returns authorization code and redirect URI - HTTP 200 response with JSON body ```typescript // mcp2: Stores Privy token in authorization record for later use const authCode = storeAuthorizationCode({ clientId: client_id, privyUserId: privy_user_id, privyToken: privy_token, // STORED FOR EXCHANGE scopes: validScopes, codeChallenge: code_challenge, codeChallengeMethod: code_challenge_method, redirectUri: redirect_uri, expiresAt: Date.now() + 30000, // 30 seconds }); ``` ### mcp (Original Implementation) **File: `/Users/jahnik/index-network/mcp/src/oauth.ts`** - **prepareAuthorization()**: Stores authorization request in memory - Validates PKCE (optional, logs warning if missing) - Allows missing code_challenge with warning - Uses in-memory Map storage - **completeAuthorization()**: Exchanges Privy token for authorization code - Verifies Privy token using privyClient.verifyAuthToken() - Stores privyClaims (extracted from token verification) - Issues authorization code ```typescript // mcp: Stores full Privy claims instead of raw token authorizationCodes.set(code, { code, clientId: record.clientId, redirectUri: record.redirectUri, scope: record.scope, privyClaims, // CLAIMS EXTRACTED FROM TOKEN privyToken: tokenUsed, createdAt: Date.now(), }); ``` ### KEY DIFFERENCES: - **mcp2**: Stores raw privy_token directly from frontend, no validation during authorization - **mcp**: Verifies Privy token during authorization using Privy SDK - **mcp2**: PKCE is mandatory (code_challenge_method must be S256) - **mcp**: PKCE is optional (logs warning if missing) **Impact on ChatGPT**: mcp2's stricter PKCE enforcement is correct OAuth behavior. mcp's optional PKCE could allow attacks. --- ## 2. TOKEN ENDPOINT IMPLEMENTATION ### mcp2 (New Implementation) **File: `/Users/jahnik/index-network/mcp2/src/server/oauth/token.ts`** - **Authorization Code Grant** (`grant_type=authorization_code`): 1. Validates code, code_verifier, client_id 2. Validates PKCE using validatePKCE() 3. Marks code as used (single-use only) 4. **Issues JWT access token** using jwt.sign() 5. Stores token in memory with associated Privy token 6. Returns access_token, refresh_token, expires_in as JWT ```typescript // mcp2: Issues JWT tokens const accessToken = jwt.sign( { sub: privyUserId, scope: scopes.join(' '), aud: audience, client_id: clientId, }, config.jwt.privateKey, { algorithm: config.jwt.algorithm, expiresIn: config.jwt.expiresIn, issuer: config.jwt.issuer, keyid: 'key-1', } ); ``` - **Refresh Token Grant**: Issues new JWT tokens with refresh token rotation - **Token Introspection** (POST /introspect): Validates and returns token info - **Privy Token Exchange** (POST /token/privy/access-token): - Requires 'privy:token:exchange' scope - Looks up stored Privy token by access token - Returns privyAccessToken with metadata ### mcp (Original Implementation) **File: `/Users/jahnik/index-network/mcp/src/oauth.ts`** - **Authorization Code Exchange** (exchangeCodeForTokens): 1. Validates code, code_verifier (optional), client_id, redirect_uri 2. Validates PKCE if present (optional check) 3. Issues **opaque random tokens** (no JWT) 4. Stores token in memory with privyClaims and privyToken 5. Returns access_token, refresh_token, expires_in ```typescript // mcp: Issues opaque random tokens const accessToken = randomBytes(32).toString("base64url"); const includeRefresh = payload.scope.includes("offline_access"); const refreshToken = includeRefresh ? randomBytes(32).toString("base64url") : undefined; ``` - **Refresh Token Grant**: Similar opaque token issuance - **Token Revocation** (revokeToken): Deletes tokens from memory - **Token Validation** (validateAccessToken): Looks up token in memory map - **Privy Token Exchange** (getPrivyTokenExchangePayload): - Looks up stored Privy token by access token - Returns privyToken, expiresAt, issuedAt, userId, scope ### KEY DIFFERENCES: | Feature | mcp2 | mcp | |---------|------|-----| | **Token Format** | JWT (RS256) | Opaque random bytes | | **Token Validation** | JWT signature + issuer check | In-memory lookup | | **Token Introspection** | Implemented via /introspect | Not implemented | | **Token Storage** | Stores Privy token per token | Stores privyClaims per token | | **PKCE Validation** | Mandatory | Optional | **CRITICAL**: mcp2 uses JWT tokens which can be validated independently. mcp uses opaque tokens that require server lookup. This is the fundamental architectural difference. --- ## 3. JWT CONFIGURATION AND SIGNING ### mcp2 (New Implementation) **File: `/Users/jahnik/index-network/mcp2/src/server/config.ts`** ```typescript jwt: { privateKey: Buffer.from(process.env.JWT_PRIVATE_KEY!, 'base64').toString('utf-8'), publicKey: Buffer.from(process.env.JWT_PUBLIC_KEY!, 'base64').toString('utf-8'), issuer: process.env.SERVER_BASE_URL!, algorithm: 'RS256' as const, expiresIn: '1h', }, ``` - **Algorithm**: RS256 (RSA signature with SHA-256) - **Keys**: Base64-encoded in environment variables - **Issuer**: Set to SERVER_BASE_URL - **Expiration**: 1 hour (3600 seconds) - **Key ID**: 'key-1' in token header **JWT Payload**: ```json { "sub": "privy_user_id", "scope": "read write profile", "aud": "http://server.url", "client_id": "chatgpt-connector", "iss": "http://server.url", "exp": 1234567890, "iat": 1234564290, "alg": "RS256", "kid": "key-1" } ``` ### mcp (Original Implementation) **File: `/Users/jahnik/index-network/mcp/src/oauth.ts`** - **Token Format**: Base64url-encoded random bytes (NOT JWT) - **No JWT signing or verification** - **Token lookup**: In-memory map using token as key - **No issuer claim** - **No expiration field in token** (only stored in memory record) ### KEY DIFFERENCES: - **mcp2 uses proper JWT with RSA signing** โœ“ - **mcp uses opaque tokens with no cryptographic validation** โœ— - **mcp2 tokens can be validated without server** โœ“ - **mcp tokens require server database lookup** โœ— --- ## 4. JWKS AND WELL-KNOWN ENDPOINTS ### mcp2 (New Implementation) **File: `/Users/jahnik/index-network/mcp2/src/server/oauth/wellknown.ts`** **Endpoints**: - `/.well-known/oauth-authorization-server` - RFC 8414 metadata - `/.well-known/oauth-authorization-server/:resource` - Scoped metadata - `/.well-known/oauth-protected-resource` - Protected resource metadata - `/.well-known/oauth-protected-resource/:resource` - Scoped resource metadata - `/.well-known/jwks.json` - **JWKS endpoint** โœ“ - `/.well-known/openid-configuration` - OpenID Connect discovery **JWKS Endpoint Response**: ```typescript // Converts PEM public key to JWK format const publicKey = await importSPKI(config.jwt.publicKey, 'RS256'); const jwk = await exportJWK(publicKey); res.json({ keys: [ { ...jwk, use: 'sig', alg: 'RS256', kid: 'key-1', }, ], }); ``` **Metadata Response**: ```json { "issuer": "http://server.url", "authorization_endpoint": "http://server.url/authorize", "token_endpoint": "http://server.url/token", "jwks_uri": "http://server.url/.well-known/jwks.json", "registration_endpoint": "http://server.url/register", "scopes_supported": ["read", "write", "profile", "privy:token:exchange"], "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], "code_challenge_methods_supported": ["S256"], "token_endpoint_auth_methods_supported": ["none"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256"] } ``` ### mcp (Original Implementation) **File: `/Users/jahnik/index-network/mcp/src/oauth.ts`** ```typescript export function authorizationServerMetadata() { return { issuer, authorization_endpoint: `${issuer.replace(/\/$/, "")}/oauth/authorize`, token_endpoint: `${issuer.replace(/\/$/, "")}/oauth/token`, registration_endpoint: `${issuer.replace(/\/$/, "")}/oauth/register`, revocation_endpoint: `${issuer.replace(/\/$/, "")}/oauth/revoke`, userinfo_endpoint: `${issuer.replace(/\/$/, "")}/oauth/userinfo`, response_types_supported: ["code"], response_modes_supported: ["query"], grant_types_supported: ["authorization_code", "refresh_token"], code_challenge_methods_supported: ["S256"], scopes_supported: supportedScopes, token_endpoint_auth_methods_supported: ["none"], }; } ``` - **NO JWKS endpoint** โœ— - **Metadata returned inline** (not at /.well-known/oauth-authorization-server) - **References userinfo_endpoint** (not implemented) - **References revocation_endpoint** (not implemented) ### KEY DIFFERENCES: | Feature | mcp2 | mcp | |---------|------|-----| | **JWKS endpoint** | โœ“ Implemented with jose library | โœ— Missing | | **Metadata location** | /.well-known/oauth-authorization-server | Not at standard location | | **Metadata registration** | Separate endpoint route | Function-based | | **Public key distribution** | via JWKS.json | No distribution | | **OpenID Connect** | Supported | Not mentioned | **CRITICAL FOR ChatGPT**: ChatGPT OAuth clients MUST have a JWKS endpoint to validate JWT tokens. **mcp missing this endpoint would cause ChatGPT to reject all tokens**. --- ## 5. CLIENT REGISTRATION ### mcp2 (New Implementation) **File: `/Users/jahnik/index-network/mcp2/src/server/oauth/dcr.ts`** - **Dynamic Client Registration (DCR)** - RFC 7591 - **Endpoint**: POST /register - **Response**: 201 Created with client metadata - **Validation**: - Requires redirect_uris (non-empty array) - Validates HTTPS in production (allows localhost HTTP) - Validates grant_types and response_types - Supports optional client metadata (client_name, client_uri, logo_uri, etc.) ```typescript // Validates redirect URIs if (process.env.NODE_ENV === 'production' && url.protocol !== 'https:') { return res.status(400).json({ error: 'invalid_redirect_uri', error_description: 'redirect_uris must use HTTPS in production', }); } ``` **Response**: ```json { "client_id": "client_...", "client_id_issued_at": 1234567890, "redirect_uris": ["https://chat.openai.com/connector_platform_oauth_redirect"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none", "client_name": "ChatGPT Connector" } ``` ### mcp (Original Implementation) **File: `/Users/jahnik/index-network/mcp/src/oauth.ts`** ```typescript export function registerClient(input: { client_name?: string; redirect_uris?: string[]; scope?: string; }) { // Validates redirect URIs for (const uri of redirectUris) { validateRedirectUri(...); } const client: OAuthClient = { clientId, clientName: input.client_name, redirectUris: redirectUris.map((value) => value.trim()), scopes, clientIdIssuedAt: Math.floor(Date.now() / 1000), }; clients.set(clientId, client); persistClients(); return { client_id: clientId, client_id_issued_at: client.clientIdIssuedAt, token_endpoint_auth_method: "none", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], redirect_uris: client.redirectUris, scope: scopes.join(" "), }; } ``` - **Function-based DCR** (not express endpoint) - **Persists clients** to file (.data/oauth-clients.json) - **Validates redirect URIs** (requires HTTPS except localhost) - **Pre-configured static clients** (OAUTH_ALLOWED_CLIENT_IDS) ### KEY DIFFERENCES: - **mcp2**: Express endpoint-based (POST /register) - **mcp**: Function-based (called from server code) - **mcp2**: HTTP RFC 7591 compliant - **mcp**: Manual client initialization required - **mcp**: Persists to disk; mcp2 uses in-memory only --- ## 6. TOKEN STORAGE AND VALIDATION ### mcp2 (New Implementation) **File: `/Users/jahnik/index-network/mcp2/src/server/oauth/storage.ts`** **Structures**: ```typescript // Stores both JWT claims and raw Privy token interface AccessTokenRecord { token: string; clientId: string; scope: string[]; resource?: string; privyClaims: VerifyAuthTokenResponse; privyToken: string; // RAW PRIVY TOKEN STORED createdAt: number; expiresAt: number; refreshToken?: string; } interface RefreshTokenRecord { token: string; clientId: string; scope: string[]; resource?: string; privyClaims: VerifyAuthTokenResponse; privyToken: string; // STORED FOR REFRESH FLOW createdAt: number; expiresAt: number; accessToken?: string; } ``` **Validation**: - **validatePKCE()**: Uses Bun.CryptoHasher for SHA-256 - **Auto-cleanup**: Every 5 minutes, removes expired entries - **PKCE**: S256 method with base64url encoding ```typescript // PKCE validation using Bun crypto function createSHA256Hash(input: string): Uint8Array { const hasher = new Bun.CryptoHasher('sha256'); hasher.update(input); return hasher.digest(); } ``` ### mcp (Original Implementation) **File: `/Users/jahnik/index-network/mcp/src/oauth.ts`** **Structures**: ```typescript type AccessTokenRecord = { token: string; clientId: string; scope: string[]; resource?: string; privyClaims: VerifyAuthTokenResponse; privyToken: string; createdAt: number; expiresAt: number; refreshToken?: string; }; ``` **Validation**: - **validateAccessToken()**: In-memory Map lookup - **calculateCodeChallenge()**: Node.js crypto.createHash() - **Auto-cleanup**: Every 5 minutes via setInterval() ```typescript export function validateAccessToken(token: string) { cleanupExpiredRecords(); const record = accessTokens.get(token); if (!record) { return { valid: false as const, error: "unknown" as const, message: "Unknown access token.", }; } // ... } ``` ### KEY DIFFERENCES: | Feature | mcp2 | mcp | |---------|------|-----| | **Token type** | JWT (self-validating) | Opaque (lookup required) | | **Storage requirement** | Still stores for Privy token lookup | Required for validation | | **Crypto library** | Bun.CryptoHasher | Node.js crypto | | **PKCE implementation** | Via validatePKCE() | Via calculateCodeChallenge() | | **Cleanup mechanism** | 5 min interval | 5 min interval | --- ## 7. MCP AUTHENTICATION MIDDLEWARE ### mcp2 (New Implementation) **File: `/Users/jahnik/index-network/mcp2/src/server/middleware/auth.ts`** **JWT Validation Middleware**: ```typescript export function validateToken(requiredScopes: string[] = []) { return async (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return sendAuthChallenge(res, requiredScopes); } const token = authHeader.substring(7); // Verify JWT token const decoded = jwt.verify(token, config.jwt.publicKey, { algorithms: [config.jwt.algorithm], issuer: config.jwt.issuer, audience: config.server.baseUrl, // CRITICAL: Validates audience }) as jwt.JwtPayload; // Extract and check scopes const tokenScopes = decoded.scope ? decoded.scope.split(' ') : []; if (requiredScopes.length > 0) { const hasAllScopes = requiredScopes.every(scope => tokenScopes.includes(scope) ); if (!hasAllScopes) { return sendInsufficientScopeError(res, requiredScopes); } } req.auth = { token, decoded, userId: decoded.sub as string, scopes: tokenScopes, }; next(); }; } ``` **Key Features**: - Validates JWT signature with public key - Checks issuer matches config - **Validates audience** (critical for security) - Extracts and validates scopes - Returns WWW-Authenticate challenge if missing/invalid - Sets req.auth with token info **MCP Endpoint**: ```typescript // mcp2/src/server/mcp/handlers.ts mcpRouter.post('/', validateToken(['read']), async (req: Request, res: Response) => { const mcpServer = getMCPServer(); const request = req.body; // Pass auth context to MCP handlers const extra = { auth: req.auth, }; const response = await handleMCPRequest(mcpServer, request, extra); res.json(response); }); ``` ### mcp (Original Implementation) **File: `/Users/jahnik/index-network/mcp/src/auth.ts`** **Token Validation Middleware**: ```typescript export async function authenticatePrivy( req: AuthenticatedRequest, res: Response, next: NextFunction ) { const authorization = req.headers.authorization; const token = authorization?.startsWith("Bearer ") ? authorization.slice("Bearer ".length) : undefined; if (!token) { res.setHeader( "WWW-Authenticate", 'Bearer realm="index-mcp", error="invalid_token", error_description="Missing bearer token."' ); return res.status(401).json({ error: "Missing bearer token." }); } // Validate opaque token const validation = validateAccessToken(token); if (!validation.valid) { res.setHeader( "WWW-Authenticate", `Bearer realm="index-mcp", error="${ validation.error === "expired" ? "invalid_token" : "invalid_grant" }", error_description="${validation.message}"` ); return res.status(401).json({ error: validation.message }); } req.privyClaims = validation.claims; req.oauth = { accessToken: token, clientId: validation.clientId, scope: validation.scope, resource: validation.resource, expiresAt: validation.expiresAt, }; return next(); } ``` **MCP Endpoint**: ```typescript // mcp/src/server.ts app.post('/mcp', authenticatePrivy, async (req: AuthenticatedRequest, res) => { try { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); await server.connect(transport); await transport.handleRequest(req, res, req.body); } catch (error) { // error handling } }); ``` ### KEY DIFFERENCES: | Feature | mcp2 | mcp | |---------|------|-----| | **Token validation** | JWT signature verification | In-memory lookup | | **Issuer check** | โœ“ Validates issuer in JWT | โœ— No issuer check | | **Audience check** | โœ“ Validates aud claim | โœ— No audience check | | **Scope validation** | โœ“ Checks required scopes | โœ“ Checks required scopes | | **MCP transport** | Direct handler call | StreamableHTTPServerTransport | | **Auth header** | RFC 7235 WWW-Authenticate | Custom bearer format | --- ## 8. PRIVY TOKEN HANDLING ### mcp2 (New Implementation) **File: `/Users/jahnik/index-network/mcp2/src/server/middleware/privy.ts`** ```typescript // Initialize Privy client (singleton) const privyClient = new PrivyClient( config.privy.appId, config.privy.appSecret ); export async function verifyPrivyToken(req, res, next) { const token = authHeader.substring(7); try { // Verify token using Privy SDK const claims = await privyClient.verifyAuthToken(token); if (!claims || !claims.userId) { return res.status(401).json({ error: 'invalid_token', error_description: 'Invalid Privy token', }); } req.privyUser = { userId: claims.userId, appId: claims.appId, }; next(); } catch (error) { console.error('Privy token verification failed:', errorMessage); return res.status(401).json({ error: 'invalid_token', error_description: 'Failed to verify Privy token', }); } } // Server-to-server Privy API calls export async function callPrivyAPI(endpoint, method, body) { const auth = Buffer.from(`${config.privy.appId}:${config.privy.appSecret}`).toString('base64'); const response = await fetch(`https://auth.privy.io/api/v1${endpoint}`, { method, headers: { 'Authorization': `Basic ${auth}`, 'privy-app-id': config.privy.appId, 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { throw new Error(`Privy API error: ${response.status} ${await response.text()}`); } return response.json(); } ``` - **Uses PrivyClient from @privy-io/server-auth** - **Verifies token via Privy SDK** (proper signature validation) - **Server-to-server API calls** with Basic Auth - **Handles Privy API errors** ### mcp (Original Implementation) **File: `/Users/jahnik/index-network/mcp/src/privy.ts`** ```typescript const privyClient = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET!, jwtVerificationKey: process.env.PRIVY_JWT_VERIFICATION_KEY, }); export async function verifyPrivyToken( token: string ): Promise<VerifyAuthTokenResponse> { const preview = `${token.slice(0, 8)}...${token.slice(-8)}`; console.log(`[privy] Verifying auth token ${preview}`); try { // Manual JWT payload decoding for logging const [, payload] = token.split("."); if (payload) { const normalized = payload.replace(/-/g, "+").replace(/_/g, "/"); const padded = normalized.padEnd(...); const decoded = Buffer.from(padded, "base64").toString("utf8"); const claims = JSON.parse(decoded); console.log("[privy] Token claims summary", { aud: claims?.aud, iss: claims?.iss, exp: claims?.exp, sid: claims?.sid }); } } catch (claimError) { console.warn("[privy] Failed to parse token payload for logging", claimError); } try { return await privyClient.utils().auth().verifyAuthToken(token); } catch (error) { console.error('[privy] verifyAuthToken failed', error); throw error; } } ``` - **Uses PrivyClient from @privy-io/node** - **Manually decodes JWT payload** for logging - **Calls privyClient.utils().auth().verifyAuthToken()** - **No server-to-server API calls** in privy.ts ### KEY DIFFERENCES: | Feature | mcp2 | mcp | |---------|------|-----| | **SDK library** | @privy-io/server-auth | @privy-io/node | | **Token verification** | privyClient.verifyAuthToken() | privyClient.utils().auth().verifyAuthToken() | | **JWT verification key** | Automatic via SDK | Optional PRIVY_JWT_VERIFICATION_KEY | | **Server-to-server calls** | โœ“ callPrivyAPI() function | โœ— Not implemented | | **Token logging** | Simple preview | Full manual decode for logging | --- ## 9. PRIVY TOKEN EXCHANGE ENDPOINT ### mcp2 (New Implementation) **File: `/Users/jahnik/index-network/mcp2/src/server/oauth/token.ts` (lines 316-361)** ```typescript /** * Privy Token Exchange Endpoint * Used by MCP tools to exchange their OAuth access token for the original Privy token * that was provided during the authorization flow. This allows tools to call the Protocol API. */ tokenRouter.post('/privy/access-token', validateToken(['privy:token:exchange']), async (req, res) => { try { // The validateToken middleware has already verified the token and attached req.auth const oauthToken = req.auth?.token; if (!oauthToken) { return res.status(401).json({ error: 'unauthorized' }); } // Look up the stored token data to get the Privy token const tokenData = getToken(oauthToken); if (!tokenData) { return res.status(404).json({ error: 'token_not_found' }); } const preview = `${tokenData.privyToken.slice(0, 4)}...${tokenData.privyToken.slice(-4)}`; console.log('[privy/access-token] Exchanging token for Privy bearer', preview); // Return the Privy token with metadata return res.json({ privyAccessToken: tokenData.privyToken, expiresAt: tokenData.expiresAt, issuedAt: null, userId: tokenData.privyUserId, scope: tokenData.scopes, }); } catch (error) { console.error('[privy/access-token] Error:', error); return res.status(500).json({ error: 'server_error', error_description: 'Failed to exchange token', }); } }); ``` - **Endpoint**: POST /token/privy/access-token - **Auth**: validateToken middleware (requires 'privy:token:exchange' scope) - **Lookup**: Gets Privy token from stored token data - **Response**: privyAccessToken, expiresAt, userId, scope ### mcp (Original Implementation) **File: `/Users/jahnik/index-network/mcp/src/server.ts` (lines 1329-1354)** ```typescript app.post('/privy/access-token', authenticatePrivy, (req: AuthenticatedRequest, res) => { if (!req.oauth) { return res.status(401).json({ error: 'Unauthorized' }); } const scopes = req.oauth.scope || []; if (!scopes.includes('privy:token:exchange')) { return res.status(403).json({ error: 'insufficient_scope' }); } const payload = getPrivyTokenExchangePayload(req.oauth.accessToken); if (!payload) { return res.status(404).json({ error: 'token_not_found' }); } const preview = `${payload.privyToken.slice(0, 4)}...${payload.privyToken.slice(-4)}`; console.log('[privy] Exchanging token for Privy bearer', preview); return res.json({ privyAccessToken: payload.privyToken, expiresAt: payload.expiresAt ?? null, issuedAt: payload.issuedAt ?? null, userId: payload.userId ?? null, scope: payload.scope, }); }); ``` - **Endpoint**: POST /privy/access-token - **Auth**: authenticatePrivy middleware (validates opaque token) - **Lookup**: Gets Privy token from accessTokens Map - **Response**: privyAccessToken, expiresAt, issuedAt, userId, scope ### KEY DIFFERENCES: | Feature | mcp2 | mcp | |---------|------|-----| | **Endpoint path** | /token/privy/access-token | /privy/access-token | | **Auth middleware** | validateToken() | authenticatePrivy() | | **Token type** | JWT | Opaque | | **expiresAt** | From stored token data | From privyClaims.expiration | | **issuedAt** | Always null | From privyClaims.issued_at | | **userId** | privyUserId | privyClaims.user_id | --- ## 10. ROOT CAUSE ANALYSIS: ChatGPT "Connection Problem" Based on the architectural differences, here's why ChatGPT likely reports a "connection problem" despite the OAuth flow completing: ### mcp2 Architecture (Likely Working): ``` ChatGPT OAuth Flow: 1. GET /authorize โ†’ 200 (validation passes) 2. POST /authorize โ†’ 200 (returns auth code) 3. POST /token?code=X&code_verifier=Y โ†’ 200 (returns JWT access token) 4. POST /mcp with Authorization: Bearer JWT โ†’ validateToken() validates JWT signature 5. Privy token exchange works โ†’ Tools can call Protocol API ``` **mcp2 Strengths**: - JWT tokens are self-validating (ChatGPT can validate offline) - JWKS endpoint available for key distribution - Proper audience and issuer checks - Proper HTTP OAuth flow implementation ### mcp Architecture (Likely Failing): ``` ChatGPT OAuth Flow: 1. GET /authorize โ†’ ??? (not clear where this is handled) 2. POST /authorize โ†’ Function call? Not HTTP endpoint 3. POST /token โ†’ exchangeCodeForTokens() returns opaque token 4. POST /mcp with Authorization: Bearer OPAQUE โ†’ authenticatePrivy() does Map lookup 5. ** PROBLEM: Opaque token validation requires server-side state ** ``` **mcp Problems**: - **NO JWKS endpoint** - ChatGPT cannot validate tokens independently - **Opaque tokens** - ChatGPT cannot verify token validity without server roundtrip - **No issuer/audience validation** - Tokens don't include validation metadata - **Authentication endpoint structure unclear** - May not be accessible as HTTP routes ### Specific Failure Point: When ChatGPT: 1. Receives the opaque token from token endpoint โœ“ 2. Tries to validate it using JWKS endpoint โœ— **MISSING** 3. Falls back to introspection endpoint โœ— **MISSING in mcp** 4. Cannot trust the token โœ— **ABANDONS CONNECTION** mcp2 mitigates this by: - Providing JWT tokens (self-validating) - Exposing /.well-known/jwks.json - Implementing token introspection endpoint - Proper RFC-compliant OAuth metadata --- ## 11. CONFIGURATION DIFFERENCES ### mcp2 **File: `/Users/jahnik/index-network/mcp2/src/server/config.ts`** Required environment variables: ``` PRIVY_APP_ID PRIVY_APP_SECRET SERVER_BASE_URL JWT_PRIVATE_KEY (base64-encoded) JWT_PUBLIC_KEY (base64-encoded) PROTOCOL_API_URL ``` Config structure: ```typescript privy: { appId, appSecret } server: { baseUrl, port, nodeEnv } jwt: { privateKey, publicKey, issuer, algorithm, expiresIn } oauth: { authorizationEndpoint, tokenEndpoint, jwksEndpoint, etc } intentExtraction: { timeouts, limits } ``` ### mcp **File: `/Users/jahnik/index-network/mcp/src/privy.ts` and environment** Required environment variables: ``` PRIVY_APP_ID PRIVY_APP_SECRET PRIVY_JWT_VERIFICATION_KEY (optional) PRIVY_CLIENT_ID (optional) OAUTH_ISSUER_URL (defaults to MCP_SERVER_URL) OAUTH_RESOURCE_INDICATOR OAUTH_ACCESS_TOKEN_TTL_SECONDS OAUTH_REFRESH_TOKEN_TTL_SECONDS OAUTH_CODE_TTL_SECONDS OAUTH_SUPPORTED_SCOPES OAUTH_DEFAULT_SCOPES OAUTH_ALLOWED_CLIENT_IDS OAUTH_ALLOWED_REDIRECT_URIS MCP_SERVER_URL ``` No centralized config file - environment variables read throughout code. --- ## SUMMARY TABLE | Aspect | mcp2 | mcp | Impact | |--------|------|-----|--------| | **Token Format** | JWT (RS256) | Opaque random | mcp2 self-validating โœ“ | | **JWKS Endpoint** | โœ“ Implemented | โœ— Missing | mcp2 ChatGPT compatible โœ“ | | **Token Introspection** | โœ“ Implemented | โœ— Not found | mcp2 can introspect โœ“ | | **Issuer Validation** | โœ“ Checked in JWT | โœ— Not checked | mcp2 more secure โœ“ | | **Audience Validation** | โœ“ Checked | โœ— Not checked | mcp2 more secure โœ“ | | **PKCE** | Mandatory (S256) | Optional | mcp2 more strict โœ“ | | **Authorization Endpoint** | Express route (GET/POST) | Function-based | mcp2 proper HTTP โœ“ | | **DCR Endpoint** | Express endpoint | Function-based | mcp2 proper HTTP โœ“ | | **Well-known Metadata** | RFC 8414 compliant | Custom format | mcp2 standard โœ“ | | **Privy SDK** | @privy-io/server-auth | @privy-io/node | mcp2 server-specific โœ“ | | **Server-to-server API** | โœ“ callPrivyAPI() | โœ— Not implemented | mcp2 can call Privy API โœ“ | | **MCP Transport** | Direct handler | StreamableHTTPServerTransport | Different approaches | --- ## RECOMMENDATIONS ### For ChatGPT Compatibility (Critical): 1. **mcp2 is the correct implementation** for ChatGPT OAuth 2. **mcp needs these additions** to work: - Add JWKS endpoint at /.well-known/jwks.json - Convert opaque tokens to JWT - Implement token introspection endpoint - Expose authorization endpoints as HTTP routes ### For Production Readiness: **mcp2 should**: - Add persistent token storage (Redis/PostgreSQL) - Implement token revocation tracking - Add logging for OAuth events - Consider token encryption at rest **mcp should**: - Follow mcp2 architecture if staying with opaque tokens is required - Or switch to mcp2's JWT approach for ChatGPT compatibility

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jahnik/mcp2'

If you have feedback or need assistance with the MCP directory API, please join our Discord server