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