/** * MCP Outline PostgreSQL - OAuth Tools * Manages OAuth applications and user authentications * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import { Pool } from 'pg'; import { BaseTool, ToolResponse, OAuthClientArgs, GetOAuthClientArgs, CreateOAuthClientArgs, UpdateOAuthClientArgs, PaginationArgs, } from '../types/tools.js'; interface OAuthClient { id: string; name: string; secret: string; redirectUris: string[]; description?: string; teamId: string; createdById: string; createdAt: Date; updatedAt: Date; } interface OAuthAuthentication { id: string; providerId: string; userId: string; teamId: string; scopes: string[]; createdAt: Date; updatedAt: Date; } /** * oauth_clients.list - List OAuth applications */ const listOAuthClients: BaseTool = { name: 'outline_oauth_clients_list', description: 'List all registered OAuth applications/clients. Returns client details including name, redirect URIs, and creation info.', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of results to return', default: 25, }, offset: { type: 'number', description: 'Number of results to skip', default: 0, }, }, }, handler: async (args, pgClient): Promise => { const { limit = 25, offset = 0 } = args; const result = await pgClient.query( ` SELECT oc.*, u.name as "createdByName", u.email as "createdByEmail" FROM oauth_clients oc LEFT JOIN users u ON oc."createdById" = u.id ORDER BY oc."createdAt" DESC LIMIT $1 OFFSET $2 `, [limit, offset] ); const clients = result.rows.map((row) => ({ id: row.id, name: row.name, redirectUris: row.redirectUris, description: row.description, teamId: row.teamId, createdById: row.createdById, createdByName: row.createdByName, createdByEmail: row.createdByEmail, createdAt: row.createdAt, updatedAt: row.updatedAt, // Secret omitted for security })); return { content: [ { type: 'text', text: JSON.stringify( { data: clients, pagination: { offset, limit, total: result.rowCount || 0, }, }, null, 2 ), }, ], }; }, }; /** * oauth_clients.info - Get OAuth client details */ const getOAuthClient: BaseTool = { name: 'outline_oauth_clients_info', description: 'Get detailed information about a specific OAuth client/application by ID.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'OAuth client UUID', }, }, required: ['id'], }, handler: async (args, pgClient): Promise => { const { id } = args; const result = await pgClient.query( ` SELECT oc.*, u.name as "createdByName", u.email as "createdByEmail" FROM oauth_clients oc LEFT JOIN users u ON oc."createdById" = u.id WHERE oc.id = $1 `, [id] ); if (result.rows.length === 0) { throw new Error(`OAuth client not found: ${id}`); } const client = result.rows[0]; return { content: [ { type: 'text', text: JSON.stringify( { data: client, }, null, 2 ), }, ], }; }, }; /** * oauth_clients.create - Register new OAuth application */ const createOAuthClient: BaseTool = { name: 'outline_oauth_clients_create', description: 'Register a new OAuth application/client. Generates client credentials for OAuth flow integration.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Application name', }, redirect_uris: { type: 'array', items: { type: 'string' }, description: 'List of allowed redirect URIs for OAuth flow', }, description: { type: 'string', description: 'Optional application description', }, }, required: ['name', 'redirect_uris'], }, handler: async (args, pgClient): Promise => { const { name, redirect_uris, description } = args; // Generate random client secret (in production, use crypto.randomBytes) const secret = `sk_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`; const result = await pgClient.query( ` INSERT INTO oauth_clients (name, secret, "redirectUris", description) VALUES ($1, $2, $3, $4) RETURNING * `, [name, secret, JSON.stringify(redirect_uris), description] ); return { content: [ { type: 'text', text: JSON.stringify( { data: result.rows[0], message: 'OAuth client created successfully. Store the secret securely - it will not be shown again.', }, null, 2 ), }, ], }; }, }; /** * oauth_clients.update - Modify OAuth application settings */ const updateOAuthClient: BaseTool = { name: 'outline_oauth_clients_update', description: 'Update OAuth client/application settings such as name, redirect URIs, or description.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'OAuth client UUID', }, name: { type: 'string', description: 'New application name', }, redirect_uris: { type: 'array', items: { type: 'string' }, description: 'Updated list of allowed redirect URIs', }, description: { type: 'string', description: 'Updated description', }, }, required: ['id'], }, handler: async (args, pgClient): Promise => { const { id, name, redirect_uris, description } = args; const updates: string[] = []; const values: any[] = []; let paramIndex = 1; if (name !== undefined) { updates.push(`name = $${paramIndex}`); values.push(name); paramIndex++; } if (redirect_uris !== undefined) { updates.push(`"redirectUris" = $${paramIndex}`); values.push(JSON.stringify(redirect_uris)); paramIndex++; } if (description !== undefined) { updates.push(`description = $${paramIndex}`); values.push(description); paramIndex++; } if (updates.length === 0) { throw new Error('No fields to update'); } updates.push(`"updatedAt" = NOW()`); values.push(id); const result = await pgClient.query( ` UPDATE oauth_clients SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING * `, values ); if (result.rows.length === 0) { throw new Error(`OAuth client not found: ${id}`); } return { content: [ { type: 'text', text: JSON.stringify( { data: result.rows[0], }, null, 2 ), }, ], }; }, }; /** * oauth_clients.rotate_secret - Generate new client secret */ const rotateOAuthClientSecret: BaseTool = { name: 'outline_oauth_clients_rotate_secret', description: 'Generate a new client secret for an OAuth application. The old secret will be invalidated.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'OAuth client UUID', }, }, required: ['id'], }, handler: async (args, pgClient): Promise => { const { id } = args; const newSecret = `sk_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`; const result = await pgClient.query( ` UPDATE oauth_clients SET secret = $1, "updatedAt" = NOW() WHERE id = $2 RETURNING id, name, secret, "updatedAt" `, [newSecret, id] ); if (result.rows.length === 0) { throw new Error(`OAuth client not found: ${id}`); } return { content: [ { type: 'text', text: JSON.stringify( { data: result.rows[0], message: 'Secret rotated successfully. Update your application configuration with the new secret.', }, null, 2 ), }, ], }; }, }; /** * oauth_clients.delete - Remove OAuth application */ const deleteOAuthClient: BaseTool = { name: 'outline_oauth_clients_delete', description: 'Delete an OAuth client/application. This will revoke all active authentications using this client.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'OAuth client UUID to delete', }, }, required: ['id'], }, handler: async (args, pgClient): Promise => { const { id } = args; const result = await pgClient.query( ` DELETE FROM oauth_clients WHERE id = $1 RETURNING id, name `, [id] ); if (result.rows.length === 0) { throw new Error(`OAuth client not found: ${id}`); } return { content: [ { type: 'text', text: JSON.stringify( { data: { success: true, deleted: result.rows[0], }, }, null, 2 ), }, ], }; }, }; /** * oauth_authentications.list - List user OAuth authentications */ const listOAuthAuthentications: BaseTool = { name: 'outline_oauth_authentications_list', description: 'List all OAuth authentications (user authorizations). Shows which users have granted access to which providers.', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of results to return', default: 25, }, offset: { type: 'number', description: 'Number of results to skip', default: 0, }, }, }, handler: async (args, pgClient): Promise => { const { limit = 25, offset = 0 } = args; const result = await pgClient.query( ` SELECT oa.*, u.name as "userName", u.email as "userEmail" FROM oauth_authentications oa LEFT JOIN users u ON oa."userId" = u.id ORDER BY oa."createdAt" DESC LIMIT $1 OFFSET $2 `, [limit, offset] ); return { content: [ { type: 'text', text: JSON.stringify( { data: result.rows, pagination: { offset, limit, total: result.rowCount || 0, }, }, null, 2 ), }, ], }; }, }; /** * oauth_authentications.delete - Revoke OAuth authentication */ const deleteOAuthAuthentication: BaseTool<{ id: string }> = { name: 'outline_oauth_authentications_delete', description: 'Revoke an OAuth authentication. This disconnects the user from the OAuth provider.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'OAuth authentication UUID to revoke', }, }, required: ['id'], }, handler: async (args, pgClient): Promise => { const { id } = args; const result = await pgClient.query( ` DELETE FROM oauth_authentications WHERE id = $1 RETURNING id, "providerId", "userId" `, [id] ); if (result.rows.length === 0) { throw new Error(`OAuth authentication not found: ${id}`); } return { content: [ { type: 'text', text: JSON.stringify( { data: { success: true, revoked: result.rows[0], }, }, null, 2 ), }, ], }; }, }; // Export all OAuth tools export const oauthTools: BaseTool[] = [ listOAuthClients, getOAuthClient, createOAuthClient, updateOAuthClient, rotateOAuthClientSecret, deleteOAuthClient, listOAuthAuthentications, deleteOAuthAuthentication, ];