Skip to main content
Glama

Superglue MCP

Official
by superglue-ai
route.ts13.4 kB
import { ExtendedSuperglueClient, OAuthState } from '@/src/lib/oauth-utils'; import { NextRequest, NextResponse } from 'next/server'; const OAUTH_STATE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes interface OAuthTokenResponse { access_token: string; refresh_token?: string; token_type?: string; expires_at?: string; expires_in?: number; } async function exchangeCodeForToken( code: string, tokenUrl: string, clientId: string, clientSecret: string, redirectUri: string, state?: string ): Promise<OAuthTokenResponse> { if (!clientId || !clientSecret) { throw new Error('[OAUTH_STAGE:TOKEN_EXCHANGE] OAuth client credentials not configured for authorization code flow'); } let response: Response; try { response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', }, body: new URLSearchParams({ grant_type: 'authorization_code', code, client_id: clientId, client_secret: clientSecret, redirect_uri: redirectUri, ...(state ? { state } : {}), }), }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Network error'; throw new Error(`[OAUTH_STAGE:TOKEN_EXCHANGE] Failed to reach OAuth provider token endpoint at ${tokenUrl}: ${errMsg}`); } if (!response.ok) { const errorText = await response.text(); throw new Error(`[OAUTH_STAGE:TOKEN_EXCHANGE] OAuth provider rejected token exchange (HTTP ${response.status}). Provider response: ${errorText.slice(0, 500)}`); } try { return response.json(); } catch (error) { const responseText = await response.text(); throw new Error(`[OAUTH_STAGE:TOKEN_EXCHANGE] OAuth provider returned invalid JSON response. Expected JSON with access_token, but received: ${responseText.slice(0, 500)}. This usually means the token_url is incorrect or the provider is not returning a proper OAuth token response.`); } } async function exchangeClientCredentialsForToken( tokenUrl: string, clientId: string, clientSecret: string ): Promise<OAuthTokenResponse> { if (!clientId || !clientSecret) { throw new Error('[OAUTH_STAGE:TOKEN_EXCHANGE] OAuth client credentials not configured for client credentials flow'); } let response: Response; try { response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret, }), }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Network error'; throw new Error(`[OAUTH_STAGE:TOKEN_EXCHANGE] Failed to reach OAuth provider token endpoint at ${tokenUrl}: ${errMsg}`); } if (!response.ok) { const errorText = await response.text(); throw new Error(`[OAUTH_STAGE:TOKEN_EXCHANGE] OAuth provider rejected token exchange for client credentials flow (HTTP ${response.status}). Provider response: ${errorText.slice(0, 500)}`); } const responseText = await response.text(); try { return JSON.parse(responseText); } catch (error) { throw new Error(`[OAUTH_STAGE:TOKEN_EXCHANGE] OAuth provider returned invalid JSON response. Expected JSON with access_token, but received: ${responseText.slice(0, 500)}. This usually means the token_url is incorrect or the provider is not returning a proper OAuth token response.`); } } function buildRedirectUrl(origin: string, path: string, params: Record<string, string>): string { const url = new URL(path, origin); Object.entries(params).forEach(([key, value]) => { url.searchParams.set(key, value); }); return url.toString(); } // Centralized OAuth callback HTML response generator function createOAuthCallbackHTML( type: 'success' | 'error', message: string, integrationId: string, origin: string, tokens?: any, suppressErrorUI?: boolean ): string { const isError = type === 'error'; const title = isError ? 'OAuth Connection Failed' : 'OAuth Connection Successful!'; const color = isError ? '#dc2626' : '#16a34a'; const actionText = isError ? 'You can close this window and try again.' : 'You can close this window now.'; // Properly escape message for JavaScript (including newlines which break string literals) const escapedMessage = message .replace(/\\/g, '\\\\') .replace(/'/g, "\\'") .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r'); return ` <!DOCTYPE html> <html> <head> <title>OAuth ${isError ? 'Error' : 'Success'}</title> </head> <body> <div style="text-align: center; padding: 50px; font-family: system-ui;"> <h2 style="color: ${color};">${title}</h2> <p>${message}</p> <p style="margin-top: 20px;">${actionText}</p> </div> <script> if (window.opener) { try { window.opener.postMessage({ type: 'oauth-${type}', integrationId: '${integrationId}', message: '${escapedMessage}', tokens: ${tokens ? JSON.stringify(tokens) : 'undefined'} }, '${origin}'); } catch (e) { console.error('Failed to notify parent window:', e); } if (!${isError} || ${suppressErrorUI}) { setTimeout(() => window.close(), 100); } } else { window.location.href = '${origin}/integrations?${isError ? 'error' : 'success'}=oauth_${type}&integration=${integrationId}&message=' + encodeURIComponent('${escapedMessage}'); } </script> </body> </html> `; } export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl; // Get the correct origin const forwardedHost = request.headers.get('x-forwarded-host'); const forwardedProto = request.headers.get('x-forwarded-proto') || 'https'; const host = request.headers.get('host'); // Determine the correct origin: // 1. Use x-forwarded-host if available // 2. Use host header with forwarded proto if x-forwarded-proto is set // 3. Fall back to request origin let origin: string; if (forwardedHost) { origin = `${forwardedProto}://${forwardedHost}`; } else if (request.headers.get('x-forwarded-proto') && host) { origin = `${forwardedProto}://${host}`; } else { origin = request.nextUrl.origin; } // Force HTTPS for production domains if (origin.startsWith('http://') && origin.includes('superglue.cloud')) { origin = origin.replace('http://', 'https://'); } // Extract OAuth parameters const code = searchParams.get('code'); const grantTypeParam = searchParams.get('grant_type'); const state = searchParams.get('state'); const error = searchParams.get('error'); const errorDescription = searchParams.get('error_description'); // Handle OAuth provider errors if (error) { const errorMsg = `[OAUTH_STAGE:AUTHORIZATION] OAuth provider returned error during user authorization: ${error}${errorDescription ? ` - ${errorDescription}` : ''}. This error occurred before the token exchange step.`; let integrationId = 'unknown'; let suppressErrorUI = false; try { if (state) { const stateData = JSON.parse(atob(state)) as OAuthState; integrationId = stateData.integrationId || 'unknown'; suppressErrorUI = stateData.suppressErrorUI || false; } } catch { } const html = createOAuthCallbackHTML('error', errorMsg, integrationId, origin, undefined, suppressErrorUI); return new NextResponse(html, { headers: { 'Content-Type': 'text/html' } }); } if ((!code && grantTypeParam !== 'client_credentials') || !state) { const errorMsg = !code ? '[OAUTH_STAGE:CALLBACK] No authorization code received from OAuth provider. The user may have denied access or the OAuth provider did not redirect properly.' : '[OAUTH_STAGE:CALLBACK] No state parameter received from OAuth provider. This indicates a malformed OAuth callback.'; return NextResponse.redirect( buildRedirectUrl(origin, '/integrations', { error: !code ? 'no_code' : 'no_state', message: errorMsg }) ); } try { const stateData = JSON.parse(atob(state)) as OAuthState & { token_url?: string }; const { integrationId, apiKey, timestamp, client_credentials_uid, templateId, clientId, token_url, suppressErrorUI } = stateData; if (Date.now() - timestamp >= OAUTH_STATE_EXPIRY_MS) { throw new Error('[OAUTH_STAGE:VALIDATION] OAuth state expired (older than 5 minutes). Please start the OAuth flow again.'); } const endpoint = process.env.GRAPHQL_ENDPOINT; const client = new ExtendedSuperglueClient({ endpoint, apiKey }); const resolved = await client.getOAuthClientCredentials({ templateId, clientCredentialsUid: client_credentials_uid }); if (!resolved?.client_secret || !resolved?.client_id) { throw new Error('[OAUTH_STAGE:CREDENTIAL_RESOLUTION] OAuth client credentials could not be resolved from backend. The client_id or client_secret may not have been properly stored.'); } let tokenData: OAuthTokenResponse; if (grantTypeParam === 'client_credentials') { tokenData = await exchangeClientCredentialsForToken(String(token_url), resolved.client_id, resolved.client_secret); } else { const redirectUri = stateData.redirectUri || `${origin}/api/auth/callback`; tokenData = await exchangeCodeForToken(code as string, String(token_url), resolved.client_id, resolved.client_secret, redirectUri, state); } if (!tokenData || typeof tokenData !== 'object') { throw new Error('[OAUTH_STAGE:TOKEN_VALIDATION] Invalid token response from OAuth provider - expected object with access_token field'); } const { access_token, refresh_token, ...additionalFields } = tokenData; if (!access_token) { throw new Error('[OAUTH_STAGE:TOKEN_VALIDATION] No access_token field in OAuth provider response. The provider may require different OAuth configuration or the token_url may be incorrect.'); } // Package the tokens for the frontend to handle const tokens = { access_token, refresh_token: refresh_token || access_token, token_type: additionalFields.token_type || 'Bearer', expires_at: additionalFields.expires_at || (additionalFields.expires_in ? new Date(Date.now() + additionalFields.expires_in * 1000).toISOString() : undefined), }; if (grantTypeParam === 'client_credentials') { return NextResponse.json({ type: 'oauth-success', integrationId, message: 'OAuth connection completed successfully!', tokens }); } else { const html = createOAuthCallbackHTML('success', 'OAuth connection completed successfully!', integrationId, origin, tokens, suppressErrorUI); return new NextResponse(html, { headers: { 'Content-Type': 'text/html' } }); } } catch (error) { console.error('OAuth callback error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Try to extract integration ID from state if available let integrationId = 'unknown'; let isClientCredentials = false; let suppressErrorUI = false; try { if (state) { const stateData = JSON.parse(atob(state)) as OAuthState; integrationId = stateData.integrationId || 'unknown'; suppressErrorUI = stateData.suppressErrorUI || false; } isClientCredentials = grantTypeParam === 'client_credentials'; } catch { // Ignore state parsing errors, use default } if (isClientCredentials) { return NextResponse.json({ type: 'oauth-error', integrationId, message: errorMessage }, { status: 400 }); } else { const html = createOAuthCallbackHTML('error', errorMessage, integrationId, origin, undefined, suppressErrorUI); return new NextResponse(html, { headers: { 'Content-Type': 'text/html' } }); } } }

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/superglue-ai/superglue'

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