86 tools across 12 modules for direct PostgreSQL access to Outline Wiki: - Documents (19), Collections (14), Users (9), Groups (8) - Comments (6), Shares (5), Revisions (3), Events (3) - Attachments (5), File Operations (4), OAuth (8), Auth (2) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
547 lines
13 KiB
TypeScript
547 lines
13 KiB
TypeScript
/**
|
|
* 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<OAuthClientArgs> = {
|
|
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<ToolResponse> => {
|
|
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<GetOAuthClientArgs> = {
|
|
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<ToolResponse> => {
|
|
const { id } = args;
|
|
|
|
const result = await pgClient.query<OAuthClient>(
|
|
`
|
|
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<CreateOAuthClientArgs> = {
|
|
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<ToolResponse> => {
|
|
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<UpdateOAuthClientArgs> = {
|
|
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<ToolResponse> => {
|
|
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<GetOAuthClientArgs> = {
|
|
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<ToolResponse> => {
|
|
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<GetOAuthClientArgs> = {
|
|
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<ToolResponse> => {
|
|
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<PaginationArgs> = {
|
|
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<ToolResponse> => {
|
|
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<ToolResponse> => {
|
|
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<any>[] = [
|
|
listOAuthClients,
|
|
getOAuthClient,
|
|
createOAuthClient,
|
|
updateOAuthClient,
|
|
rotateOAuthClientSecret,
|
|
deleteOAuthClient,
|
|
listOAuthAuthentications,
|
|
deleteOAuthAuthentication,
|
|
];
|