u
This commit is contained in:
@@ -20,6 +20,26 @@ function getBaseUrl(req) {
|
||||
return `${proto}://${host}`;
|
||||
}
|
||||
|
||||
function getFrontendUrl(req) {
|
||||
if (process.env.FRONTEND_URL) {
|
||||
return process.env.FRONTEND_URL.replace(/\/$/, '');
|
||||
}
|
||||
const proto = req.headers['x-forwarded-proto'] || req.protocol || 'http';
|
||||
const host = req.get('host') || `127.0.0.1:${env.port}`;
|
||||
const hostname = host.split(':')[0];
|
||||
return `${proto}://${hostname}:4209`;
|
||||
}
|
||||
|
||||
function mcpErrorStatus(error) {
|
||||
if (!error) return 500;
|
||||
if (error.message === 'No company selected') return 400;
|
||||
if (error.message === 'Not authorized' || error.message === 'User is no longer authorized') return 403;
|
||||
if (error.message === 'Unauthorized') return 401;
|
||||
if (error.message?.endsWith(' not found') || error.message?.includes('not found')) return 404;
|
||||
if (error.message?.includes('required') || error.message?.includes('Invalid')) return 400;
|
||||
return 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a candidate redirect_uri matches any registered redirect URI.
|
||||
* Per RFC 8252 Section 7.3 (OAuth 2.0 for Native Apps):
|
||||
@@ -177,7 +197,7 @@ function mcpAuthMiddleware(req, res, next) {
|
||||
* @param {import('express').Application} app
|
||||
* @param {string} [_basePath='']
|
||||
*/
|
||||
export function register(app, _basePath = '') {
|
||||
export function register(app, basePath = '') {
|
||||
/**
|
||||
* @openapi
|
||||
* /.well-known/oauth-protected-resource:
|
||||
@@ -216,9 +236,10 @@ export function register(app, _basePath = '') {
|
||||
*/
|
||||
const handleAuthServerMetadata = (req, res) => {
|
||||
const baseUrl = getBaseUrl(req);
|
||||
const frontendUrl = getFrontendUrl(req);
|
||||
res.json({
|
||||
issuer: baseUrl,
|
||||
authorization_endpoint: `${baseUrl}/oauth/authorize`,
|
||||
authorization_endpoint: `${frontendUrl}/oauth/authorize`,
|
||||
token_endpoint: `${baseUrl}/oauth/token`,
|
||||
registration_endpoint: `${baseUrl}/oauth/register`,
|
||||
introspection_endpoint: `${baseUrl}/oauth/introspect`,
|
||||
@@ -308,175 +329,41 @@ export function register(app, _basePath = '') {
|
||||
* get:
|
||||
* tags: [MCP]
|
||||
* summary: OAuth 2.1 Authorization Endpoint (RFC 6749 & RFC 7636)
|
||||
* description: Authorization prompt for end-users to grant access to an MCP client.
|
||||
* description: Redirects browser to frontend authorization consent page.
|
||||
* security: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: client_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: redirect_uri
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: response_type
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [code]
|
||||
* - in: query
|
||||
* name: code_challenge
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: code_challenge_method
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [S256]
|
||||
* - in: query
|
||||
* name: state
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Authorization / Consent HTML form
|
||||
* 302:
|
||||
* description: Redirect back to client callback
|
||||
* description: Redirects to frontend /oauth/authorize
|
||||
*/
|
||||
app.get('/oauth/authorize', async (req, res) => {
|
||||
const {
|
||||
client_id,
|
||||
redirect_uri,
|
||||
response_type,
|
||||
code_challenge,
|
||||
code_challenge_method = 'S256',
|
||||
state = '',
|
||||
scope = 'mcp:tools',
|
||||
} = req.query;
|
||||
|
||||
if (!client_id) {
|
||||
return res.status(400).send('Missing client_id');
|
||||
}
|
||||
|
||||
const client = await mcpDb.getClient(client_id);
|
||||
if (!client) {
|
||||
return res.status(400).send(`Unknown client_id: ${client_id}`);
|
||||
}
|
||||
|
||||
let registeredUris = [];
|
||||
try {
|
||||
registeredUris = JSON.parse(client.redirectUris);
|
||||
} catch {
|
||||
registeredUris = [client.redirectUris];
|
||||
}
|
||||
|
||||
if (!redirect_uri || !isRedirectUriAllowed(redirect_uri, registeredUris)) {
|
||||
return res.status(400).send('Invalid or unregistered redirect_uri');
|
||||
}
|
||||
|
||||
if (response_type !== 'code') {
|
||||
const redirectUrl = new URL(redirect_uri);
|
||||
redirectUrl.searchParams.set('error', 'unsupported_response_type');
|
||||
if (state) redirectUrl.searchParams.set('state', state);
|
||||
return res.redirect(redirectUrl.toString());
|
||||
}
|
||||
|
||||
if (!code_challenge || code_challenge_method !== 'S256') {
|
||||
const redirectUrl = new URL(redirect_uri);
|
||||
redirectUrl.searchParams.set('error', 'invalid_request');
|
||||
redirectUrl.searchParams.set('error_description', 'PKCE code_challenge with method S256 is required');
|
||||
if (state) redirectUrl.searchParams.set('state', state);
|
||||
return res.redirect(redirectUrl.toString());
|
||||
}
|
||||
|
||||
// If user is logged in
|
||||
if (req.user) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Authorize MCP Client</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||
.card { background: #1e293b; padding: 2.5rem; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); width: 100%; max-width: 440px; text-align: center; border: 1px solid #334155; }
|
||||
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; color: #38bdf8; }
|
||||
p { color: #94a3b8; font-size: 0.95rem; line-height: 1.5; }
|
||||
.user-box { background: #0f172a; border-radius: 8px; padding: 0.75rem 1rem; margin: 1.25rem 0; text-align: left; border: 1px solid #334155; }
|
||||
.user-email { font-weight: 600; color: #f1f5f9; }
|
||||
.btn-row { display: flex; gap: 1rem; margin-top: 1.5rem; }
|
||||
button { flex: 1; padding: 0.75rem 1rem; border-radius: 6px; font-weight: 600; cursor: pointer; border: none; font-size: 0.95rem; transition: background 0.2s; }
|
||||
.btn-allow { background: #0284c7; color: #fff; }
|
||||
.btn-allow:hover { background: #0369a1; }
|
||||
.btn-deny { background: #334155; color: #94a3b8; }
|
||||
.btn-deny:hover { background: #475569; color: #f8fafc; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Authorize Application</h1>
|
||||
<p><strong>${escapeHtml(client.clientName)}</strong> would like access to your Dashback MCP tools.</p>
|
||||
<div class="user-box">
|
||||
<div style="font-size:0.75rem; color:#64748b; text-transform:uppercase; letter-spacing:0.05em;">Authorized Account</div>
|
||||
<div class="user-email">${escapeHtml(req.user.email)}</div>
|
||||
</div>
|
||||
<form method="POST" action="/oauth/authorize">
|
||||
<input type="hidden" name="client_id" value="${escapeHtml(client_id)}">
|
||||
<input type="hidden" name="redirect_uri" value="${escapeHtml(redirect_uri)}">
|
||||
<input type="hidden" name="code_challenge" value="${escapeHtml(code_challenge)}">
|
||||
<input type="hidden" name="code_challenge_method" value="${escapeHtml(code_challenge_method)}">
|
||||
<input type="hidden" name="state" value="${escapeHtml(state)}">
|
||||
<input type="hidden" name="scope" value="${escapeHtml(scope)}">
|
||||
<div class="btn-row">
|
||||
<button type="submit" name="action" value="deny" class="btn-deny">Cancel</button>
|
||||
<button type="submit" name="action" value="allow" class="btn-allow">Authorize</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
return res.setHeader('Content-Type', 'text/html').send(html);
|
||||
}
|
||||
|
||||
// If user is not logged in: display login form with allowlisted email input
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Sign in to Authorize</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||
.card { background: #1e293b; padding: 2.5rem; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); width: 100%; max-width: 440px; text-align: center; border: 1px solid #334155; }
|
||||
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; color: #38bdf8; }
|
||||
p { color: #94a3b8; font-size: 0.95rem; line-height: 1.5; margin-bottom: 1.5rem; }
|
||||
input[type="email"] { width: 100%; box-sizing: border-box; padding: 0.75rem 1rem; border-radius: 6px; border: 1px solid #334155; background: #0f172a; color: #f8fafc; font-size: 1rem; margin-bottom: 1.25rem; }
|
||||
input[type="email"]:focus { outline: none; border-color: #38bdf8; }
|
||||
button { width: 100%; padding: 0.75rem 1rem; border-radius: 6px; font-weight: 600; cursor: pointer; border: none; font-size: 0.95rem; background: #0284c7; color: #fff; transition: background 0.2s; }
|
||||
button:hover { background: #0369a1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Dashback Sign In</h1>
|
||||
<p>Sign in with your authorized Dashback email to grant <strong>${escapeHtml(client.clientName)}</strong> access.</p>
|
||||
<form method="POST" action="/oauth/authorize">
|
||||
<input type="hidden" name="client_id" value="${escapeHtml(client_id)}">
|
||||
<input type="hidden" name="redirect_uri" value="${escapeHtml(redirect_uri)}">
|
||||
<input type="hidden" name="code_challenge" value="${escapeHtml(code_challenge)}">
|
||||
<input type="hidden" name="code_challenge_method" value="${escapeHtml(code_challenge_method)}">
|
||||
<input type="hidden" name="state" value="${escapeHtml(state)}">
|
||||
<input type="hidden" name="scope" value="${escapeHtml(scope)}">
|
||||
<input type="email" name="email" placeholder="user@example.com" required autofocus>
|
||||
<button type="submit" name="action" value="allow">Sign In & Authorize</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
return res.setHeader('Content-Type', 'text/html').send(html);
|
||||
const frontendUrl = getFrontendUrl(req);
|
||||
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
|
||||
return res.redirect(`${frontendUrl}/oauth/authorize${qs}`);
|
||||
});
|
||||
|
||||
app.post('/oauth/authorize', async (req, res) => {
|
||||
@@ -798,8 +685,412 @@ export function register(app, _basePath = '') {
|
||||
|
||||
app.get('/mcp', mcpAuthMiddleware, handleSessionRequest);
|
||||
app.delete('/mcp', mcpAuthMiddleware, handleSessionRequest);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /mcp/authorize:
|
||||
* get:
|
||||
* tags: [MCP]
|
||||
* summary: Get MCP authorization prompt details
|
||||
* description: Fetches client information and current user context for the frontend MCP authorization consent screen.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: client_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: redirect_uri
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: response_type
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [code]
|
||||
* - in: query
|
||||
* name: code_challenge
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: code_challenge_method
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [S256]
|
||||
* - in: query
|
||||
* name: state
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: scope
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Client and user consent metadata
|
||||
* 400:
|
||||
* description: Invalid request or unregistered redirect_uri
|
||||
* 401:
|
||||
* description: User not authenticated
|
||||
* 404:
|
||||
* description: Client not found
|
||||
*/
|
||||
app.get(`${basePath}/mcp/authorize`, async (req, res) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const {
|
||||
client_id,
|
||||
redirect_uri,
|
||||
response_type = 'code',
|
||||
code_challenge,
|
||||
code_challenge_method = 'S256',
|
||||
state = '',
|
||||
scope = 'mcp:tools',
|
||||
} = req.query;
|
||||
|
||||
if (!client_id) {
|
||||
return res.status(400).json({
|
||||
error: 'invalid_request',
|
||||
error_description: 'Missing client_id',
|
||||
});
|
||||
}
|
||||
|
||||
const client = await mcpDb.getClient(client_id);
|
||||
if (!client) {
|
||||
return res.status(404).json({
|
||||
error: 'invalid_client',
|
||||
error_description: `Unknown client_id: ${client_id}`,
|
||||
});
|
||||
}
|
||||
|
||||
let registeredUris = [];
|
||||
try {
|
||||
registeredUris = JSON.parse(client.redirectUris);
|
||||
} catch {
|
||||
registeredUris = [client.redirectUris];
|
||||
}
|
||||
|
||||
if (!redirect_uri || !isRedirectUriAllowed(redirect_uri, registeredUris)) {
|
||||
return res.status(400).json({
|
||||
error: 'invalid_request',
|
||||
error_description: 'Invalid or unregistered redirect_uri',
|
||||
});
|
||||
}
|
||||
|
||||
if (response_type !== 'code') {
|
||||
return res.status(400).json({
|
||||
error: 'unsupported_response_type',
|
||||
error_description: 'Only response_type=code is supported',
|
||||
});
|
||||
}
|
||||
|
||||
if (!code_challenge || code_challenge_method !== 'S256') {
|
||||
return res.status(400).json({
|
||||
error: 'invalid_request',
|
||||
error_description: 'PKCE code_challenge with method S256 is required',
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
client: {
|
||||
clientId: client.clientId,
|
||||
clientName: client.clientName,
|
||||
scope: client.scope,
|
||||
},
|
||||
user: {
|
||||
id: req.user.id,
|
||||
email: req.user.email,
|
||||
},
|
||||
params: {
|
||||
clientId: client_id,
|
||||
redirectUri: redirect_uri,
|
||||
codeChallenge: code_challenge,
|
||||
codeChallengeMethod: code_challenge_method,
|
||||
state,
|
||||
scope: scope || client.scope || 'mcp:tools',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[mcp] Authorize prompt error:', err);
|
||||
res.status(mcpErrorStatus(err)).json({ error: 'server_error', error_description: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /mcp/authorize:
|
||||
* post:
|
||||
* tags: [MCP]
|
||||
* summary: Grant or deny MCP client authorization
|
||||
* description: Issues an OAuth authorization code or refusal for the requesting MCP client on behalf of the authenticated user.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [client_id, redirect_uri, action]
|
||||
* properties:
|
||||
* client_id:
|
||||
* type: string
|
||||
* redirect_uri:
|
||||
* type: string
|
||||
* code_challenge:
|
||||
* type: string
|
||||
* code_challenge_method:
|
||||
* type: string
|
||||
* state:
|
||||
* type: string
|
||||
* scope:
|
||||
* type: string
|
||||
* action:
|
||||
* type: string
|
||||
* enum: [allow, deny]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success with redirect URL containing code or error
|
||||
* 400:
|
||||
* description: Invalid request parameters
|
||||
* 401:
|
||||
* description: User not authenticated
|
||||
* 404:
|
||||
* description: Client not found
|
||||
*/
|
||||
app.post(`${basePath}/mcp/authorize`, async (req, res) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const {
|
||||
client_id,
|
||||
redirect_uri,
|
||||
code_challenge,
|
||||
code_challenge_method = 'S256',
|
||||
state = '',
|
||||
scope = 'mcp:tools',
|
||||
action = 'allow',
|
||||
} = req.body || {};
|
||||
|
||||
if (!client_id) {
|
||||
return res.status(400).json({
|
||||
error: 'invalid_request',
|
||||
error_description: 'Missing client_id',
|
||||
});
|
||||
}
|
||||
|
||||
const client = await mcpDb.getClient(client_id);
|
||||
if (!client) {
|
||||
return res.status(404).json({
|
||||
error: 'invalid_client',
|
||||
error_description: `Unknown client_id: ${client_id}`,
|
||||
});
|
||||
}
|
||||
|
||||
let registeredUris = [];
|
||||
try {
|
||||
registeredUris = JSON.parse(client.redirectUris);
|
||||
} catch {
|
||||
registeredUris = [client.redirectUris];
|
||||
}
|
||||
|
||||
if (!redirect_uri || !isRedirectUriAllowed(redirect_uri, registeredUris)) {
|
||||
return res.status(400).json({
|
||||
error: 'invalid_request',
|
||||
error_description: 'Invalid or unregistered redirect_uri',
|
||||
});
|
||||
}
|
||||
|
||||
const redirectUrl = new URL(redirect_uri);
|
||||
|
||||
if (action === 'deny') {
|
||||
redirectUrl.searchParams.set('error', 'access_denied');
|
||||
if (state) redirectUrl.searchParams.set('state', state);
|
||||
|
||||
if (req.is('application/json') || req.headers.accept?.includes('application/json')) {
|
||||
return res.json({ ok: false, redirectUrl: redirectUrl.toString() });
|
||||
}
|
||||
return res.redirect(redirectUrl.toString());
|
||||
}
|
||||
|
||||
if (!code_challenge || code_challenge_method !== 'S256') {
|
||||
return res.status(400).json({
|
||||
error: 'invalid_request',
|
||||
error_description: 'PKCE code_challenge with method S256 is required',
|
||||
});
|
||||
}
|
||||
|
||||
const code = randomBytes(32).toString('hex');
|
||||
await mcpDb.createAuthCode({
|
||||
code,
|
||||
clientId: client_id,
|
||||
googleAuthId: req.user.id,
|
||||
redirectUri: redirect_uri,
|
||||
codeChallenge: code_challenge,
|
||||
codeChallengeMethod: code_challenge_method,
|
||||
scope: scope || client.scope || 'mcp:tools',
|
||||
expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 minutes
|
||||
});
|
||||
|
||||
redirectUrl.searchParams.set('code', code);
|
||||
if (state) redirectUrl.searchParams.set('state', state);
|
||||
|
||||
if (req.is('application/json') || req.headers.accept?.includes('application/json')) {
|
||||
return res.json({ ok: true, redirectUrl: redirectUrl.toString() });
|
||||
}
|
||||
return res.redirect(redirectUrl.toString());
|
||||
} catch (err) {
|
||||
console.error('[mcp] Authorize action error:', err);
|
||||
res.status(mcpErrorStatus(err)).json({ error: 'server_error', error_description: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /mcp/authorizations:
|
||||
* get:
|
||||
* tags: [MCP]
|
||||
* summary: List active MCP client authorizations
|
||||
* description: Lists all MCP clients authorized by the authenticated user with token counts and authorization timestamps.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of active user authorizations
|
||||
* 401:
|
||||
* description: User not authenticated
|
||||
*/
|
||||
app.get(`${basePath}/mcp/authorizations`, async (req, res) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
const authorizations = await mcpDb.listAuthorizations(req.user.id);
|
||||
res.json({ authorizations });
|
||||
} catch (err) {
|
||||
console.error('[mcp] List authorizations error:', err);
|
||||
res.status(mcpErrorStatus(err)).json({ error: 'server_error', error_description: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /mcp/authorizations/{clientId}:
|
||||
* delete:
|
||||
* tags: [MCP]
|
||||
* summary: Revoke MCP client authorizations
|
||||
* description: Revokes all tokens and authorizations granted to an MCP client by the authenticated user.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: clientId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Authorizations revoked
|
||||
* 400:
|
||||
* description: Missing clientId
|
||||
* 401:
|
||||
* description: User not authenticated
|
||||
*/
|
||||
app.delete(`${basePath}/mcp/authorizations/:clientId`, async (req, res) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
const { clientId } = req.params;
|
||||
if (!clientId) {
|
||||
return res.status(400).json({ error: 'invalid_request', error_description: 'clientId is required' });
|
||||
}
|
||||
await mcpDb.revokeAuthorizations(req.user.id, clientId);
|
||||
res.json({ ok: true, message: 'Authorizations revoked' });
|
||||
} catch (err) {
|
||||
console.error('[mcp] Revoke authorizations error:', err);
|
||||
res.status(mcpErrorStatus(err)).json({ error: 'server_error', error_description: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /mcp/clients:
|
||||
* get:
|
||||
* tags: [MCP]
|
||||
* summary: List registered MCP clients
|
||||
* description: Lists all registered OAuth MCP clients.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of registered clients
|
||||
* 401:
|
||||
* description: User not authenticated
|
||||
*/
|
||||
app.get(`${basePath}/mcp/clients`, async (req, res) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
const clients = await mcpDb.listClients();
|
||||
res.json({ clients });
|
||||
} catch (err) {
|
||||
console.error('[mcp] List clients error:', err);
|
||||
res.status(mcpErrorStatus(err)).json({ error: 'server_error', error_description: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /mcp/clients/{clientId}:
|
||||
* delete:
|
||||
* tags: [MCP]
|
||||
* summary: Delete registered MCP client
|
||||
* description: Deletes an MCP client registration.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: clientId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Client deleted
|
||||
* 400:
|
||||
* description: Missing clientId
|
||||
* 401:
|
||||
* description: User not authenticated
|
||||
*/
|
||||
app.delete(`${basePath}/mcp/clients/:clientId`, async (req, res) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
const { clientId } = req.params;
|
||||
if (!clientId) {
|
||||
return res.status(400).json({ error: 'invalid_request', error_description: 'clientId is required' });
|
||||
}
|
||||
await mcpDb.deleteClient(clientId);
|
||||
res.json({ ok: true, message: 'Client deleted' });
|
||||
} catch (err) {
|
||||
console.error('[mcp] Delete client error:', err);
|
||||
res.status(mcpErrorStatus(err)).json({ error: 'server_error', error_description: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (typeof str !== 'string') return '';
|
||||
return str
|
||||
|
||||
Reference in New Issue
Block a user