UNPKG

bottlenecks-mcp-server

Version:

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

587 lines 24.9 kB
/** * MCP CRUD Operations Tools * Provides AI agents with create, update, and validation capabilities for bottlenecks */ /** * Create a new bottleneck */ export function createCreateBottleneckTool(client) { return { type: 'call_tool', name: 'create_bottleneck', description: 'Create a new bottleneck with title, description, and metadata', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Clear, descriptive title of the bottleneck', minLength: 5, maxLength: 200, }, description: { type: 'string', description: 'Detailed description using MDX format with sections', minLength: 50, }, tags: { type: 'array', items: { type: 'string' }, description: 'Categorization tags for the bottleneck', maxItems: 10, }, is_public: { type: 'boolean', description: 'Whether the bottleneck should be publicly visible', default: false, }, organization_id: { type: 'string', description: 'ID of the organization this bottleneck belongs to', format: 'uuid', }, card_type: { type: 'string', description: 'Type of card generation: AI (default), HYBRID, or REAL', enum: ['AI', 'HYBRID', 'REAL'], default: 'AI', }, validate_only: { type: 'boolean', description: 'Only validate the data without creating the bottleneck', default: false, }, }, required: ['title', 'description'], }, handler: async (args) => { try { // Validate required permissions const hasWritePermission = await client.checkPermissions(['write']); if (!hasWritePermission) { return { content: [ { type: 'text', text: 'Error: Write permission required to create bottlenecks. Please check your API key scopes.', }, ], isError: true, }; } // Prepare the data const bottleneckData = { title: args.title, description: args.description, tags: args.tags || [], is_public: args.is_public || false, organization_id: args.organization_id, card_type: args.card_type || 'AI', }; // If validation only, use the validation endpoint if (args.validate_only) { const validationResponse = await client.request('/api/mcp/validate-bottleneck', { method: 'POST', body: bottleneckData, requireAuth: true, }); if (!validationResponse.success) { return { content: [ { type: 'text', text: `Validation error: ${validationResponse.error}`, }, ], isError: true, }; } const validation = validationResponse.data; let content = `# Validation Results\n\n`; if (validation.valid) { content += `✅ **Valid:** The bottleneck data passes all validation checks.\n\n`; } else { content += `❌ **Invalid:** The bottleneck data has validation errors.\n\n`; content += `## Errors\n\n`; validation.errors.forEach((error) => { content += `- **${error.field}:** ${error.message}\n`; }); content += '\n'; } if (validation.warnings && validation.warnings.length > 0) { content += `## Warnings\n\n`; validation.warnings.forEach((warning) => { content += `- **${warning.field}:** ${warning.message}\n`; }); content += '\n'; } return { content: [ { type: 'text', text: content, }, ], }; } // Create the bottleneck const response = await client.request('/api/cards', { method: 'POST', body: bottleneckData, requireAuth: true, }); if (!response.success) { return { content: [ { type: 'text', text: `Error creating bottleneck: ${response.error}`, }, ], isError: true, }; } const bottleneck = response.data.data; let content = `# Bottleneck Created Successfully! 🎉\n\n`; content += `**ID:** \`${bottleneck.id}\`\n`; content += `**Title:** ${bottleneck.title}\n`; content += `**Slug:** \`${bottleneck.slug}\`\n`; content += `**Public:** ${bottleneck.is_public ? 'Yes' : 'No'}\n`; content += `**Created:** ${new Date(bottleneck.created_at).toLocaleDateString()}\n`; if (bottleneck.tags && bottleneck.tags.length > 0) { content += `**Tags:** ${bottleneck.tags.map((tag) => `\`${tag}\``).join(', ')}\n`; } content += `\n**View Online:** [${bottleneck.title}](${client.getApiBaseUrl()}/cards/${bottleneck.slug})\n`; content += `**Direct URL:** \`${client.getApiBaseUrl()}/cards/${bottleneck.slug}\`\n`; content += `\n**Next steps:**\n`; content += `- Use \`upload_file\` to add supporting documents\n`; content += `- Use \`update_bottleneck\` to modify the content\n`; content += `- Use \`get_bottleneck\` to view the full details\n`; return { content: [ { type: 'text', text: content, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error in create_bottleneck: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }; } /** * Update an existing bottleneck */ export function createUpdateBottleneckTool(client) { return { type: 'call_tool', name: 'update_bottleneck', description: 'Update an existing bottleneck with new information', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Bottleneck ID to update', format: 'uuid', }, title: { type: 'string', description: 'New title for the bottleneck', minLength: 5, maxLength: 200, }, description: { type: 'string', description: 'New description using MDX format', minLength: 50, }, tags: { type: 'array', items: { type: 'string' }, description: 'New tags for the bottleneck', maxItems: 10, }, is_public: { type: 'boolean', description: 'Update public visibility', }, organization_id: { type: 'string', description: 'Update organization assignment', format: 'uuid', }, validate_only: { type: 'boolean', description: 'Only validate the changes without applying them', default: false, }, }, required: ['id'], }, handler: async (args) => { try { // Validate required permissions const hasWritePermission = await client.checkPermissions(['write']); if (!hasWritePermission) { return { content: [ { type: 'text', text: 'Error: Write permission required to update bottlenecks. Please check your API key scopes.', }, ], isError: true, }; } // Prepare the update data (only include provided fields) const updateData = {}; if (args.title !== undefined) updateData.title = args.title; if (args.description !== undefined) updateData.description = args.description; if (args.tags !== undefined) updateData.tags = args.tags; if (args.is_public !== undefined) updateData.is_public = args.is_public; if (args.organization_id !== undefined) updateData.organization_id = args.organization_id; // Check if any updates were provided if (Object.keys(updateData).length === 0) { return { content: [ { type: 'text', text: 'Error: No update fields provided. Specify at least one field to update.', }, ], isError: true, }; } // If validation only, validate the changes if (args.validate_only) { const validationResponse = await client.request('/api/mcp/validate-bottleneck', { method: 'POST', body: { ...updateData, id: args.id }, requireAuth: true, }); if (!validationResponse.success) { return { content: [ { type: 'text', text: `Validation error: ${validationResponse.error}`, }, ], isError: true, }; } const validation = validationResponse.data; let content = `# Update Validation Results\n\n`; if (validation.valid) { content += `✅ **Valid:** The update data passes all validation checks.\n\n`; content += `**Fields to update:**\n`; Object.keys(updateData).forEach((field) => { content += `- ${field}\n`; }); } else { content += `❌ **Invalid:** The update data has validation errors.\n\n`; content += `## Errors\n\n`; validation.errors.forEach((error) => { content += `- **${error.field}:** ${error.message}\n`; }); } return { content: [ { type: 'text', text: content, }, ], }; } // Apply the update const response = await client.request(`/api/cards/${args.id}`, { method: 'PUT', body: updateData, requireAuth: true, }); if (!response.success) { return { content: [ { type: 'text', text: `Error updating bottleneck: ${response.error}`, }, ], isError: true, }; } const bottleneck = response.data; let content = `# Bottleneck Updated Successfully! ✅\n\n`; content += `**ID:** \`${bottleneck.id}\`\n`; content += `**Title:** ${bottleneck.title}\n`; content += `**Updated:** ${new Date(bottleneck.updated_at).toLocaleDateString()}\n`; content += `\n**Fields updated:**\n`; Object.keys(updateData).forEach((field) => { content += `- ${field}\n`; }); content += `\n**View Online:** [${bottleneck.title}](${client.getApiBaseUrl()}/cards/${bottleneck.slug})\n`; content += `**Direct URL:** \`${client.getApiBaseUrl()}/cards/${bottleneck.slug}\`\n`; content += `\n**Use \`get_bottleneck\` to view the updated content.**\n`; return { content: [ { type: 'text', text: content, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error in update_bottleneck: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }; } /** * Validate bottleneck data without creating/updating */ export function createValidateBottleneckDataTool(client) { return { type: 'call_tool', name: 'validate_bottleneck_data', description: 'Validate bottleneck data against schema and business rules without creating or updating', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Title to validate', minLength: 1, maxLength: 200, }, description: { type: 'string', description: 'Description to validate (MDX format)', minLength: 1, }, tags: { type: 'array', items: { type: 'string' }, description: 'Tags to validate', maxItems: 10, }, check_duplicates: { type: 'boolean', description: 'Check for duplicate titles/slugs', default: true, }, check_mdx_format: { type: 'boolean', description: 'Validate MDX format and sections', default: true, }, check_taxonomy: { type: 'boolean', description: 'Validate tags against taxonomy', default: true, }, }, required: [], }, handler: async (args) => { try { // Client-side validation first const errors = []; const warnings = []; // Title validation if (args.title) { if (args.title.length < 5) { errors.push({ field: 'title', message: 'Title must be at least 5 characters long', code: 'MIN_LENGTH', }); } if (args.title.length > 200) { errors.push({ field: 'title', message: 'Title must be no more than 200 characters long', code: 'MAX_LENGTH', }); } if (args.title.toLowerCase().includes('bottleneck')) { warnings.push({ field: 'title', message: 'Consider avoiding the word "bottleneck" in the title for better specificity', code: 'GENERIC_TERM', }); } } // Description validation if (args.description) { if (args.description.length < 50) { errors.push({ field: 'description', message: 'Description must be at least 50 characters long', code: 'MIN_LENGTH', }); } if (args.check_mdx_format) { // Basic MDX validation - let the server handle detailed section validation if (args.description.length < 100) { warnings.push({ field: 'description', message: 'Description seems short for a comprehensive bottleneck analysis', code: 'SHORT_DESCRIPTION', }); } } } // Summary validation (if provided as separate field) if (args.summary) { if (args.summary.length < 20) { errors.push({ field: 'summary', message: 'Summary must be at least 20 characters long', code: 'MIN_LENGTH', }); } if (args.summary.length > 500) { errors.push({ field: 'summary', message: 'Summary must be no more than 500 characters long', code: 'MAX_LENGTH', }); } } // Tags validation if (args.tags) { if (args.tags.length > 10) { errors.push({ field: 'tags', message: 'Maximum 10 tags allowed', code: 'MAX_ITEMS', }); } args.tags.forEach((tag, index) => { if (tag.length === 0) { errors.push({ field: `tags[${index}]`, message: 'Tag cannot be empty', code: 'EMPTY_TAG', }); } if (tag !== tag.toLowerCase()) { warnings.push({ field: `tags[${index}]`, message: 'Tags should be lowercase', code: 'CASE_CONVENTION', }); } if (tag.includes(' ')) { warnings.push({ field: `tags[${index}]`, message: 'Tags should use hyphens instead of spaces', code: 'SPACE_IN_TAG', }); } }); } // Server-side validation if we have data to validate if (args.title || args.description || args.tags) { const validationData = { title: args.title, description: args.description, tags: args.tags, }; const response = await client.request('/api/mcp/validate-bottleneck', { method: 'POST', body: validationData, requireAuth: true, }); if (response.success && response.data) { // Merge server-side validation results if (response.data.errors) { errors.push(...response.data.errors); } if (response.data.warnings) { warnings.push(...response.data.warnings); } } } // Format results const isValid = errors.length === 0; let content = `# Validation Results\n\n`; if (isValid) { content += `✅ **Valid:** All validation checks passed!\n\n`; } else { content += `❌ **Invalid:** Found ${errors.length} error(s)\n\n`; } if (errors.length > 0) { content += `## Errors\n\n`; errors.forEach((error, index) => { content += `${index + 1}. **${error.field}:** ${error.message} \`(${error.code})\`\n`; }); content += '\n'; } if (warnings.length > 0) { content += `## Warnings\n\n`; warnings.forEach((warning, index) => { content += `${index + 1}. **${warning.field}:** ${warning.message} \`(${warning.code})\`\n`; }); content += '\n'; } if (isValid) { content += `**Ready to create/update!** Use \`create_bottleneck\` or \`update_bottleneck\` with this data.\n`; } else { content += `**Fix the errors above before creating/updating the bottleneck.**\n`; } return { content: [ { type: 'text', text: content, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error in validate_bottleneck_data: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }; } //# sourceMappingURL=crud-operations.js.map