feat: Initial release MCP Outline PostgreSQL v1.0.0

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>
This commit is contained in:
2026-01-31 13:25:09 +00:00
commit b05b54033f
30 changed files with 14439 additions and 0 deletions

546
src/tools/oauth.ts Normal file
View File

@@ -0,0 +1,546 @@
/**
* 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,
];