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:
546
src/tools/oauth.ts
Normal file
546
src/tools/oauth.ts
Normal 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,
|
||||
];
|
||||
Reference in New Issue
Block a user