feat: Add 52 new tools bringing total to 160

New modules (11):
- teams.ts (5 tools): Team/workspace management
- integrations.ts (6 tools): External integrations (Slack, embeds)
- notifications.ts (4 tools): User notification management
- subscriptions.ts (4 tools): Document subscription management
- templates.ts (5 tools): Document template management
- imports-tools.ts (4 tools): Import job management
- emojis.ts (3 tools): Custom emoji management
- user-permissions.ts (3 tools): Permission management
- bulk-operations.ts (6 tools): Batch operations
- advanced-search.ts (6 tools): Faceted search, recent, orphaned, duplicates
- analytics.ts (6 tools): Usage statistics and insights

Updated:
- src/index.ts: Import and register all new tools
- src/tools/index.ts: Export all new modules
- CHANGELOG.md: Version 1.2.0 entry
- CLAUDE.md: Updated tool count to 160
- CONTINUE.md: Updated state documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 13:53:27 +00:00
parent fa0e052620
commit 83b70f557e
17 changed files with 3054 additions and 67 deletions

222
src/tools/teams.ts Normal file
View File

@@ -0,0 +1,222 @@
/**
* MCP Outline PostgreSQL - Teams Tools
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
import { Pool } from 'pg';
import { BaseTool, ToolResponse } from '../types/tools.js';
import { isValidUUID, sanitizeInput } from '../utils/security.js';
/**
* teams.info - Get team details
*/
const getTeam: BaseTool<{ id?: string }> = {
name: 'outline_get_team',
description: 'Get detailed information about a team (workspace). If no ID provided, returns the first/default team.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Team ID (UUID, optional)' },
},
},
handler: async (args, pgClient): Promise<ToolResponse> => {
let query = `
SELECT
t.id, t.name, t.subdomain, t.domain, t."avatarUrl",
t.sharing, t."documentEmbeds", t."guestSignin", t."inviteRequired",
t."collaborativeEditing", t."defaultUserRole", t."memberCollectionCreate",
t."memberTeamCreate", t."passkeysEnabled", t.description, t.preferences,
t."lastActiveAt", t."suspendedAt", t."createdAt", t."updatedAt",
(SELECT COUNT(*) FROM users WHERE "teamId" = t.id AND "deletedAt" IS NULL) as "userCount",
(SELECT COUNT(*) FROM collections WHERE "teamId" = t.id AND "deletedAt" IS NULL) as "collectionCount",
(SELECT COUNT(*) FROM documents WHERE "teamId" = t.id AND "deletedAt" IS NULL) as "documentCount"
FROM teams t
WHERE t."deletedAt" IS NULL
`;
const params: any[] = [];
if (args.id) {
if (!isValidUUID(args.id)) throw new Error('Invalid team ID format');
query += ` AND t.id = $1`;
params.push(args.id);
}
query += ` LIMIT 1`;
const result = await pgClient.query(query, params);
if (result.rows.length === 0) throw new Error('Team not found');
return {
content: [{ type: 'text', text: JSON.stringify({ data: result.rows[0] }, null, 2) }],
};
},
};
/**
* teams.update - Update team settings
*/
const updateTeam: BaseTool<{
id: string;
name?: string;
sharing?: boolean;
document_embeds?: boolean;
guest_signin?: boolean;
invite_required?: boolean;
default_user_role?: string;
}> = {
name: 'outline_update_team',
description: 'Update team settings and preferences.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Team ID (UUID)' },
name: { type: 'string', description: 'Team name' },
sharing: { type: 'boolean', description: 'Allow document sharing' },
document_embeds: { type: 'boolean', description: 'Allow document embeds' },
guest_signin: { type: 'boolean', description: 'Allow guest signin' },
invite_required: { type: 'boolean', description: 'Require invite to join' },
default_user_role: { type: 'string', enum: ['admin', 'member', 'viewer'], description: 'Default role for new users' },
},
required: ['id'],
},
handler: async (args, pgClient): Promise<ToolResponse> => {
if (!isValidUUID(args.id)) throw new Error('Invalid team ID format');
const updates: string[] = ['"updatedAt" = NOW()'];
const params: any[] = [];
let idx = 1;
if (args.name) { updates.push(`name = $${idx++}`); params.push(sanitizeInput(args.name)); }
if (args.sharing !== undefined) { updates.push(`sharing = $${idx++}`); params.push(args.sharing); }
if (args.document_embeds !== undefined) { updates.push(`"documentEmbeds" = $${idx++}`); params.push(args.document_embeds); }
if (args.guest_signin !== undefined) { updates.push(`"guestSignin" = $${idx++}`); params.push(args.guest_signin); }
if (args.invite_required !== undefined) { updates.push(`"inviteRequired" = $${idx++}`); params.push(args.invite_required); }
if (args.default_user_role) { updates.push(`"defaultUserRole" = $${idx++}`); params.push(args.default_user_role); }
params.push(args.id);
const result = await pgClient.query(
`UPDATE teams SET ${updates.join(', ')} WHERE id = $${idx} AND "deletedAt" IS NULL RETURNING *`,
params
);
if (result.rows.length === 0) throw new Error('Team not found');
return {
content: [{ type: 'text', text: JSON.stringify({ data: result.rows[0], message: 'Team updated' }, null, 2) }],
};
},
};
/**
* teams.stats - Get team statistics
*/
const getTeamStats: BaseTool<{ id?: string }> = {
name: 'outline_get_team_stats',
description: 'Get comprehensive statistics for a team including users, documents, collections, and activity.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Team ID (UUID, optional - uses default team)' },
},
},
handler: async (args, pgClient): Promise<ToolResponse> => {
let teamCondition = '';
const params: any[] = [];
if (args.id) {
if (!isValidUUID(args.id)) throw new Error('Invalid team ID format');
teamCondition = `AND "teamId" = $1`;
params.push(args.id);
}
const stats = await pgClient.query(`
SELECT
(SELECT COUNT(*) FROM users WHERE "deletedAt" IS NULL ${teamCondition}) as "totalUsers",
(SELECT COUNT(*) FROM users WHERE role = 'admin' AND "deletedAt" IS NULL ${teamCondition}) as "adminUsers",
(SELECT COUNT(*) FROM users WHERE "suspendedAt" IS NOT NULL AND "deletedAt" IS NULL ${teamCondition}) as "suspendedUsers",
(SELECT COUNT(*) FROM documents WHERE "deletedAt" IS NULL ${teamCondition.replace('"teamId"', 'd."teamId"')}) as "totalDocuments",
(SELECT COUNT(*) FROM documents WHERE template = true AND "deletedAt" IS NULL ${teamCondition.replace('"teamId"', 'd."teamId"')}) as "templateDocuments",
(SELECT COUNT(*) FROM documents WHERE "publishedAt" IS NOT NULL AND "deletedAt" IS NULL ${teamCondition.replace('"teamId"', 'd."teamId"')}) as "publishedDocuments",
(SELECT COUNT(*) FROM collections WHERE "deletedAt" IS NULL ${teamCondition.replace('"teamId"', 'c."teamId"')}) as "totalCollections",
(SELECT COUNT(*) FROM groups WHERE "deletedAt" IS NULL ${teamCondition.replace('"teamId"', 'g."teamId"')}) as "totalGroups",
(SELECT COUNT(*) FROM shares ${args.id ? 'WHERE "teamId" = $1' : ''}) as "totalShares",
(SELECT COUNT(*) FROM integrations WHERE "deletedAt" IS NULL ${teamCondition.replace('"teamId"', 'i."teamId"')}) as "totalIntegrations"
`, params);
return {
content: [{ type: 'text', text: JSON.stringify({ data: stats.rows[0] }, null, 2) }],
};
},
};
/**
* teams.domains - List team domains
*/
const listTeamDomains: BaseTool<{ team_id?: string }> = {
name: 'outline_list_team_domains',
description: 'List allowed domains for a team. Domains control who can sign up.',
inputSchema: {
type: 'object',
properties: {
team_id: { type: 'string', description: 'Team ID (UUID, optional)' },
},
},
handler: async (args, pgClient): Promise<ToolResponse> => {
let query = `
SELECT
td.id, td.name, td."teamId", td."createdById", td."createdAt",
u.name as "createdByName", t.name as "teamName"
FROM team_domains td
LEFT JOIN users u ON td."createdById" = u.id
LEFT JOIN teams t ON td."teamId" = t.id
`;
const params: any[] = [];
if (args.team_id) {
if (!isValidUUID(args.team_id)) throw new Error('Invalid team_id format');
query += ` WHERE td."teamId" = $1`;
params.push(args.team_id);
}
query += ` ORDER BY td."createdAt" DESC`;
const result = await pgClient.query(query, params);
return {
content: [{ type: 'text', text: JSON.stringify({ data: result.rows }, null, 2) }],
};
},
};
/**
* teams.updateSettings - Update team preferences
*/
const updateTeamSettings: BaseTool<{ id: string; preferences: Record<string, any> }> = {
name: 'outline_update_team_settings',
description: 'Update team preferences (JSON settings object).',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Team ID (UUID)' },
preferences: { type: 'object', description: 'Preferences object to merge' },
},
required: ['id', 'preferences'],
},
handler: async (args, pgClient): Promise<ToolResponse> => {
if (!isValidUUID(args.id)) throw new Error('Invalid team ID format');
const result = await pgClient.query(
`UPDATE teams SET preferences = COALESCE(preferences, '{}'::jsonb) || $1::jsonb, "updatedAt" = NOW()
WHERE id = $2 AND "deletedAt" IS NULL
RETURNING id, name, preferences`,
[JSON.stringify(args.preferences), args.id]
);
if (result.rows.length === 0) throw new Error('Team not found');
return {
content: [{ type: 'text', text: JSON.stringify({ data: result.rows[0], message: 'Settings updated' }, null, 2) }],
};
},
};
export const teamsTools: BaseTool<any>[] = [getTeam, updateTeam, getTeamStats, listTeamDomains, updateTeamSettings];