UNPKG

bottlenecks-mcp-server

Version:

Model Context Protocol server for Bottlenecks database - enables AI agents like Claude to interact with bottleneck data

528 lines 23.6 kB
/** * MCP Read Operations Tools * Provides AI agents with search, list, and get capabilities for bottlenecks */ /** * Search bottlenecks with filters and pagination */ export function createSearchBottlenecksTool(client) { return { type: 'call_tool', name: 'search_bottlenecks', description: 'Search for bottlenecks using various filters and criteria', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Text search query (searches title and description)', maxLength: 200, }, tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags (AND logic)', maxItems: 10, }, author_id: { type: 'string', description: 'Filter by author ID', format: 'uuid', }, organization_id: { type: 'string', description: 'Filter by organization ID', format: 'uuid', }, is_public: { type: 'boolean', description: 'Filter by public/private status', }, approval_status: { type: 'string', description: 'Filter by approval status (admin only)', enum: ['approved', 'pending', 'rejected', 'draft'], }, include_unapproved: { type: 'boolean', description: 'Include unapproved cards (admin only)', default: false, }, created_after: { type: 'string', description: 'Filter by creation date (ISO 8601)', format: 'date-time', }, created_before: { type: 'string', description: 'Filter by creation date (ISO 8601)', format: 'date-time', }, has_attachments: { type: 'boolean', description: 'Filter by presence of file attachments', }, page: { type: 'number', description: 'Page number (1-based)', minimum: 1, default: 1, }, limit: { type: 'number', description: 'Number of results per page', minimum: 1, maximum: 100, default: 20, }, sort_by: { type: 'string', description: 'Sort field', enum: ['created_at', 'updated_at', 'title', 'relevance'], default: 'created_at', }, sort_order: { type: 'string', description: 'Sort order', enum: ['asc', 'desc'], default: 'desc', }, }, required: [], }, handler: async (args) => { try { // Build query parameters const params = new URLSearchParams(); if (args.query) params.append('q', args.query); if (args.tags && args.tags.length > 0) { args.tags.forEach((tag) => params.append('tags', tag)); } if (args.author_id) params.append('author_id', args.author_id); if (args.organization_id) params.append('organization_id', args.organization_id); if (args.is_public !== undefined) params.append('is_public', args.is_public.toString()); if (args.created_after) params.append('created_after', args.created_after); if (args.created_before) params.append('created_before', args.created_before); if (args.has_attachments !== undefined) params.append('has_attachments', args.has_attachments.toString()); if (args.approval_status) params.append('status', args.approval_status); if (args.page) params.append('page', args.page.toString()); if (args.limit) params.append('limit', args.limit.toString()); if (args.sort_by) params.append('sort_by', args.sort_by); if (args.sort_order) params.append('sort_order', args.sort_order); // Use admin endpoint if accessing unapproved content or specific approval status const useAdminEndpoint = args.include_unapproved || args.approval_status; const endpoint = useAdminEndpoint ? `/api/admin/cards?${params.toString()}` : `/api/cards?${params.toString()}`; const response = await client.request(endpoint, { method: 'GET', requireAuth: true, }); if (!response.success) { return { content: [ { type: 'text', text: `Error searching bottlenecks: ${response.error}`, }, ], isError: true, }; } const data = response.data; let content = `# Search Results\n\n`; // Handle both regular API response (data.cards) and admin API response (data array or data.data) const cards = data.cards || data.data || data || []; if (!Array.isArray(cards) || cards.length === 0) { content += `No bottlenecks found matching your criteria.\n\n`; content += `**Search parameters:**\n`; if (args.query) content += `- Query: "${args.query}"\n`; if (args.tags) content += `- Tags: ${args.tags.join(', ')}\n`; if (args.is_public !== undefined) content += `- Public: ${args.is_public}\n`; return { content: [ { type: 'text', text: content, }, ], }; } const total = data.total || data.pagination?.total || cards.length; const currentPage = data.page || data.pagination?.page || args.page || 1; content += `Found **${total}** bottlenecks (showing ${cards.length} on page ${currentPage})\n\n`; cards.forEach((bottleneck, index) => { content += `## ${index + 1}. [${bottleneck.title || 'Untitled'}](${client.getApiBaseUrl()}/cards/${bottleneck.slug || bottleneck.id})\n`; content += `**ID:** \`${bottleneck.id || 'N/A'}\`\n`; content += `**Slug:** \`${bottleneck.slug || 'N/A'}\`\n`; content += `**Created:** ${bottleneck.created_at ? new Date(bottleneck.created_at).toLocaleDateString() : 'N/A'}\n`; content += `**Status:** ${bottleneck.status || 'unknown'}\n`; content += `**Public:** ${bottleneck.is_public ? 'Yes' : 'No'}\n`; if (bottleneck.tags && bottleneck.tags.length > 0) { content += `**Tags:** ${bottleneck.tags.map((tag) => `\`${tag}\``).join(', ')}\n`; } if (bottleneck.file_attachments && bottleneck.file_attachments.length > 0) { content += `**Attachments:** ${bottleneck.file_attachments.length} files\n`; } // Show description preview (first 200 chars) const description = bottleneck.content_mdx || bottleneck.summary || bottleneck.description; if (description) { const preview = description.length > 200 ? description.substring(0, 200) + '...' : description; content += `**Preview:** ${preview.replace(/\n/g, ' ')}\n`; } content += '\n'; }); // Pagination info if (data.hasMore) { content += `**Next page:** Use \`page: ${data.page + 1}\` to see more results\n`; } content += `\n**Use \`get_bottleneck\` with an ID to see full details.**\n`; return { content: [ { type: 'text', text: content, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error in search_bottlenecks: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }; } /** * Get a specific bottleneck by ID */ export function createGetBottleneckTool(client) { return { type: 'call_tool', name: 'get_bottleneck', description: 'Get detailed information about a specific bottleneck by ID', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Bottleneck ID (UUID)', format: 'uuid', }, include_attachments: { type: 'boolean', description: 'Include file attachment details', default: true, }, format: { type: 'string', description: 'Response format', enum: ['markdown', 'json', 'raw'], default: 'markdown', }, }, required: ['id'], }, handler: async (args) => { try { const params = new URLSearchParams(); if (args.include_attachments) params.append('include_attachments', 'true'); const response = await client.request(`/api/cards/${args.id}?${params.toString()}`, { method: 'GET', requireAuth: false, // Allow access to public cards without auth }); if (!response.success) { return { content: [ { type: 'text', text: `Error getting bottleneck: ${response.error}`, }, ], isError: true, }; } const bottleneck = response.data.card || response.data; if (args.format === 'json') { return { content: [ { type: 'text', text: JSON.stringify(bottleneck, null, 2), }, ], }; } if (args.format === 'raw') { return { content: [ { type: 'text', text: bottleneck.content_mdx || bottleneck.description || 'No description available', }, ], }; } // Markdown format (default) let content = `# ${bottleneck.title || 'Untitled'}\n\n`; content += `**ID:** \`${bottleneck.id || 'N/A'}\`\n`; content += `**Slug:** \`${bottleneck.slug || 'N/A'}\`\n`; content += `**Created:** ${bottleneck.created_at ? new Date(bottleneck.created_at).toLocaleDateString() : 'N/A'}\n`; content += `**Updated:** ${bottleneck.updated_at ? new Date(bottleneck.updated_at).toLocaleDateString() : 'N/A'}\n`; content += `**Public:** ${bottleneck.is_public ? 'Yes' : 'No'}\n`; if (bottleneck.tags && bottleneck.tags.length > 0) { content += `**Tags:** ${bottleneck.tags.map((tag) => `\`${tag}\``).join(', ')}\n`; } if (bottleneck.author_id) { content += `**Author ID:** \`${bottleneck.author_id}\`\n`; } if (bottleneck.organization_id) { content += `**Organization ID:** \`${bottleneck.organization_id}\`\n`; } content += `\n**View Online:** [${bottleneck.title || 'Untitled'}](${client.getApiBaseUrl()}/cards/${bottleneck.slug || bottleneck.id})\n`; content += `**Direct URL:** \`${client.getApiBaseUrl()}/cards/${bottleneck.slug || bottleneck.id}\`\n`; content += '\n---\n\n'; // Add the full description (use content_mdx field from database) if (bottleneck.content_mdx || bottleneck.description) { content += bottleneck.content_mdx || bottleneck.description; } else { content += '*No description available*'; } // Add file attachments if present if (args.include_attachments && bottleneck.file_attachments && bottleneck.file_attachments.length > 0) { content += '\n\n---\n\n## File Attachments\n\n'; bottleneck.file_attachments.forEach((file, index) => { content += `### ${index + 1}. ${file.original_filename}\n`; content += `- **ID:** \`${file.id}\`\n`; content += `- **Type:** ${file.upload_type}\n`; content += `- **Size:** ${(file.file_size / 1024 / 1024).toFixed(2)} MB\n`; content += `- **Format:** ${file.mime_type}\n`; content += `- **Status:** ${file.processing_status}\n`; content += `- **Public:** ${file.is_public ? 'Yes' : 'No'}\n`; if (file.description) { content += `- **Description:** ${file.description}\n`; } content += `- **Uploaded:** ${new Date(file.created_at).toLocaleDateString()}\n`; content += '\n'; }); content += `**Use \`get_file_content\` or \`download_file\` to access these files.**\n`; } return { content: [ { type: 'text', text: content, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error in get_bottleneck: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }; } /** * List bottlenecks with basic information */ export function createListBottlenecksTool(client) { return { type: 'call_tool', name: 'list_bottlenecks', description: 'Get a paginated list of bottlenecks with basic information', inputSchema: { type: 'object', properties: { page: { type: 'number', description: 'Page number (1-based)', minimum: 1, default: 1, }, limit: { type: 'number', description: 'Number of results per page', minimum: 1, maximum: 100, default: 20, }, sort_by: { type: 'string', description: 'Sort field', enum: ['created_at', 'updated_at', 'title'], default: 'created_at', }, sort_order: { type: 'string', description: 'Sort order', enum: ['asc', 'desc'], default: 'desc', }, public_only: { type: 'boolean', description: 'Only show public bottlenecks', default: false, }, include_unapproved: { type: 'boolean', description: 'Include unapproved cards (admin only)', default: false, }, approval_status: { type: 'string', description: 'Filter by approval status (admin only)', enum: ['approved', 'pending', 'rejected', 'draft'], }, }, required: [], }, handler: async (args) => { try { const params = new URLSearchParams(); if (args.page) params.append('page', args.page.toString()); if (args.limit) params.append('limit', args.limit.toString()); if (args.sort_by) params.append('sort_by', args.sort_by); if (args.sort_order) params.append('sort_order', args.sort_order); if (args.public_only) params.append('is_public', 'true'); if (args.approval_status) params.append('status', args.approval_status); // Use admin endpoint if accessing unapproved content or specific approval status const useAdminEndpoint = args.include_unapproved || args.approval_status; const endpoint = useAdminEndpoint ? `/api/admin/cards?${params.toString()}` : `/api/cards?${params.toString()}`; const response = await client.request(endpoint, { method: 'GET', requireAuth: !args.public_only || useAdminEndpoint, }); if (!response.success) { return { content: [ { type: 'text', text: `Error listing bottlenecks: ${response.error}`, }, ], isError: true, }; } const data = response.data || {}; let content = `# Bottlenecks List\n\n`; // Handle both regular API response (data.cards) and admin API response (data array or data.data) const cards = data.cards || data.data || data || []; if (!Array.isArray(cards) || cards.length === 0) { content += `No bottlenecks found.\n`; return { content: [ { type: 'text', text: content, }, ], }; } const total = data.total || data.pagination?.total || cards.length; const page = data.pagination?.page || args.page || 1; const limit = data.pagination?.limit || args.limit || 20; content += `**Total:** ${total} bottlenecks\n`; content += `**Page:** ${page} of ${Math.ceil(total / limit)}\n`; content += `**Showing:** ${cards.length} results\n\n`; // Create a table-like format content += `| # | Title | Status | Created | Tags | Files |\n`; content += `|---|-------|--------|---------|------|-------|\n`; cards.forEach((bottleneck, index) => { const rowNum = (page - 1) * limit + index + 1; const title = bottleneck.title.length > 40 ? bottleneck.title.substring(0, 37) + '...' : bottleneck.title; const status = bottleneck.status || 'unknown'; const created = new Date(bottleneck.created_at).toLocaleDateString(); const tags = bottleneck.tags && bottleneck.tags.length > 0 ? bottleneck.tags.slice(0, 2).join(', ') + (bottleneck.tags.length > 2 ? '...' : '') : '-'; const files = bottleneck.file_attachments ? bottleneck.file_attachments.length : 0; content += `| ${rowNum} | ${title} | ${status} | ${created} | ${tags} | ${files} |\n`; }); content += '\n'; // Navigation info if (data.page > 1) { content += `**Previous page:** Use \`page: ${data.page - 1}\`\n`; } if (data.hasMore) { content += `**Next page:** Use \`page: ${data.page + 1}\`\n`; } content += `\n**Use \`get_bottleneck\` with an ID to see full details.**\n`; content += `**Use \`search_bottlenecks\` for filtered results.**\n`; return { content: [ { type: 'text', text: content, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error in list_bottlenecks: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }; } //# sourceMappingURL=read-operations.js.map