/**
* OAuth Consent Page for MCP Client Authorization
*
* Displayed when an MCP client (Claude.ai, Claude Code, etc.) requests
* authorization to access the MCP server on behalf of the user.
*/
/**
* Generate consent page HTML from OAuth parameters
*/
export function getConsentPage(params: URLSearchParams): string {
const clientId = params.get('client_id') || 'Unknown Client';
const clientName = params.get('client_name') || clientId;
const scope = params.get('scope') || 'basic';
const state = params.get('state') || '';
const redirectUri = params.get('redirect_uri') || '';
// Parse scopes for display
const scopes = scope.split(' ').filter(Boolean);
const scopeDescriptions: Record<string, string> = {
'openid': 'Verify your identity',
'profile': 'Access your profile information',
'email': 'Access your email address',
'offline_access': 'Remember your authorization',
'tools:execute': 'Execute MCP tools on your behalf',
'tools:read': 'Read available tools',
'resources:read': 'Access resources',
};
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authorize ${clientName}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.card {
background: rgba(24, 24, 27, 0.95);
border: 1px solid rgba(63, 63, 70, 0.5);
border-radius: 16px;
padding: 40px;
max-width: 480px;
width: 100%;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.client-icon {
width: 64px;
height: 64px;
background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24px;
}
.client-icon svg {
width: 32px;
height: 32px;
fill: white;
}
h1 {
color: #fafafa;
font-size: 1.5rem;
font-weight: 600;
text-align: center;
margin-bottom: 8px;
}
.subtitle {
color: #a1a1aa;
text-align: center;
margin-bottom: 32px;
font-size: 0.95rem;
}
.client-name {
color: #14b8a6;
font-weight: 600;
}
.scopes {
background: rgba(39, 39, 42, 0.5);
border: 1px solid #3f3f46;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
}
.scopes-title {
color: #a1a1aa;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.scope-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid #3f3f46;
}
.scope-item:last-child { border-bottom: none; }
.scope-icon {
width: 20px;
height: 20px;
color: #10b981;
flex-shrink: 0;
margin-top: 2px;
}
.scope-text {
color: #e4e4e7;
font-size: 0.95rem;
}
.buttons {
display: flex;
gap: 12px;
}
.btn {
flex: 1;
padding: 14px 20px;
border-radius: 10px;
border: none;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn:hover { transform: translateY(-1px); }
.btn-allow {
background: #10b981;
color: white;
}
.btn-allow:hover { background: #059669; }
.btn-deny {
background: #3f3f46;
color: #e4e4e7;
}
.btn-deny:hover { background: #52525b; }
.warning {
color: #71717a;
font-size: 0.8rem;
text-align: center;
margin-top: 20px;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="card">
<div class="client-icon">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
</div>
<h1>Authorization Request</h1>
<p class="subtitle">
<span class="client-name">${escapeHtml(clientName)}</span> wants to access your account
</p>
<div class="scopes">
<div class="scopes-title">This will allow the application to:</div>
${scopes.map(s => `
<div class="scope-item">
<svg class="scope-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span class="scope-text">${scopeDescriptions[s] || s}</span>
</div>
`).join('')}
</div>
<form method="POST" action="/api/auth/oauth2/consent">
<input type="hidden" name="client_id" value="${escapeHtml(clientId)}">
<input type="hidden" name="scope" value="${escapeHtml(scope)}">
<input type="hidden" name="state" value="${escapeHtml(state)}">
<input type="hidden" name="redirect_uri" value="${escapeHtml(redirectUri)}">
<div class="buttons">
<button type="submit" name="consent" value="denied" class="btn btn-deny">Deny</button>
<button type="submit" name="consent" value="granted" class="btn btn-allow">Allow</button>
</div>
</form>
<p class="warning">
By allowing, you agree to share the information above with this application.
You can revoke access at any time from your account settings.
</p>
</div>
</body>
</html>`;
}
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}