UNPKG

@the_cfdude/productboard-mcp

Version:

Model Context Protocol server for Productboard REST API with dynamic tool loading

795 lines (695 loc) 22.9 kB
/** * Documentation access tool */ import { withContext, formatResponse } from '../utils/tool-wrapper.js'; import { ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { readFileSync, existsSync, readdirSync, statSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { toolDocumentation, categoryDocumentation, parameterDocumentation, } from '../documentation/tool-documentation.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** * Setup documentation tool definitions */ export function setupDocumentationTools() { return [ { name: 'get_docs', description: `Access comprehensive documentation for Productboard MCP This tool provides access to various documentation resources including: - Tool examples with detailed usage patterns - Tool categories and their descriptions - Parameter documentation - Best practices and workflows - README and quick start guides - Cheat sheets and quick references Use this tool to understand how to effectively use any Productboard MCP functionality.`, inputSchema: { type: 'object', properties: { type: { type: 'string', enum: [ 'tool-examples', 'tool-categories', 'parameter-guide', 'best-practices', 'workflows', 'cheatsheet', 'readme', 'tool-help', 'all-tools', ], description: `Type of documentation to retrieve: - tool-examples: Comprehensive examples for all tools - tool-categories: Overview of tool categories and their purposes - parameter-guide: Detailed guide for common parameters - best-practices: Best practices for using the MCP server - workflows: Step-by-step workflow examples - cheatsheet: Quick reference guide - readme: Main README file - tool-help: Detailed help for a specific tool (requires toolName) - all-tools: Complete documentation for all tools`, }, toolName: { type: 'string', description: 'Specific tool name (required only when type is "tool-help")', }, category: { type: 'string', description: 'Filter by category (optional, e.g., "notes", "features", "companies")', }, format: { type: 'string', enum: ['markdown', 'json'], default: 'markdown', description: 'Output format (default: markdown)', }, }, required: ['type'], }, handler: async (args: any) => { return withContext(async _context => { const { type, toolName, category, format = 'markdown' } = args; try { let content: string; switch (type) { case 'tool-examples': content = generateToolExamples(category); break; case 'tool-categories': content = generateCategoryDocumentation(); break; case 'parameter-guide': content = generateParameterGuide(); break; case 'best-practices': content = generateBestPractices(); break; case 'workflows': content = generateWorkflows(); break; case 'cheatsheet': content = generateCheatsheet(); break; case 'readme': content = getReadmeContent(); break; case 'tool-help': if (!toolName) { throw new Error( 'toolName is required when type is "tool-help"' ); } content = getGeneratedToolHelp(toolName); break; case 'all-tools': content = generateAllToolsDocumentation(); break; default: throw new Error(`Unknown documentation type: ${type}`); } if (format === 'json') { return formatResponse({ type, content, metadata: { generated: new Date().toISOString(), version: '1.0.0', }, }); } return formatResponse(content); } catch (error: any) { throw { code: ErrorCode.InvalidRequest, message: `Failed to retrieve documentation: ${error.message}`, }; } }); }, }, ]; } /** * Handle documentation tool requests */ export async function handleDocumentationTool( tool: string, args: Record<string, any> ) { const toolDef = setupDocumentationTools().find(t => t.name === tool); if (!toolDef) { throw new Error(`Unknown documentation tool: ${tool}`); } return await (toolDef.handler as any)(args); } function generateToolExamples(category?: string): string { let content = '# Productboard MCP Tool Examples\n\n'; if (category) { content += `Examples for ${category} tools.\n\n`; } else { content += 'Comprehensive examples for all tools organized by category.\n\n'; } // Group tools by category const toolsByCategory: Record<string, Array<[string, any]>> = { notes: [], features: [], companies: [], releases: [], objectives: [], other: [], }; Object.entries(toolDocumentation).forEach(([toolName, doc]) => { let matched = false; for (const cat of Object.keys(toolsByCategory)) { if (toolName.includes(cat.slice(0, -1))) { toolsByCategory[cat].push([toolName, doc]); matched = true; break; } } if (!matched) { toolsByCategory.other.push([toolName, doc]); } }); // Filter by category if specified const categoriesToShow = category ? [category] : Object.keys(toolsByCategory).filter( cat => toolsByCategory[cat].length > 0 ); categoriesToShow.forEach(cat => { if (!toolsByCategory[cat] || toolsByCategory[cat].length === 0) return; const catDoc = categoryDocumentation[cat as keyof typeof categoryDocumentation]; if (catDoc) { content += `## ${catDoc.name}\n\n`; content += `${catDoc.description}\n\n`; } else { content += `## ${cat.charAt(0).toUpperCase() + cat.slice(1)}\n\n`; } toolsByCategory[cat].forEach(([toolName, doc]) => { content += `### ${toolName}\n\n`; content += `${doc.description}\n\n`; doc.examples.forEach((example: any, i: number) => { content += `#### Example ${i + 1}: ${example.title}\n\n`; content += `${example.description}\n\n`; content += '```json\n'; content += JSON.stringify( { tool: toolName, arguments: example.input, }, null, 2 ); content += '\n```\n\n'; if (example.expectedOutput) { content += '**Expected Response:**\n```json\n'; content += JSON.stringify(example.expectedOutput, null, 2); content += '\n```\n\n'; } if (example.notes) { content += `> **Note:** ${example.notes}\n\n`; } }); }); }); return content; } function generateCategoryDocumentation(): string { let content = '# Productboard MCP Tool Categories\n\n'; content += 'Tools are organized into logical categories for easier discovery and use.\n\n'; Object.entries(categoryDocumentation).forEach(([key, category]) => { content += `## ${category.name}\n\n`; content += `${category.description}\n\n`; content += category.overview.trim() + '\n\n'; if (category.commonWorkflows && category.commonWorkflows.length > 0) { content += '### Common Workflows\n\n'; category.commonWorkflows.forEach(workflow => { content += `#### ${workflow.name}\n\n`; workflow.steps.forEach((step, i) => { content += `${i + 1}. ${step}\n`; }); content += '\n'; }); } // List tools in this category const toolsInCategory = Object.entries(toolDocumentation) .filter(([name]) => name.toLowerCase().includes(key.slice(0, -1))) .map(([name, doc]) => ({ name, description: doc.description })); if (toolsInCategory.length > 0) { content += '### Available Tools\n\n'; toolsInCategory.forEach(tool => { content += `- **${tool.name}**: ${tool.description}\n`; }); content += '\n'; } }); return content; } function generateParameterGuide(): string { let content = '# Parameter Guide\n\n'; content += 'Detailed documentation for common parameters used across multiple tools.\n\n'; Object.entries(parameterDocumentation).forEach(([paramName, param]) => { content += `## ${paramName}\n\n`; content += `**Description:** ${param.description}\n\n`; content += `**Type:** ${param.type}\n\n`; if ('format' in param && param.format) { content += `**Format:** ${param.format}\n\n`; } if ('values' in param && param.values) { content += '### Possible Values\n\n'; Object.entries(param.values).forEach(([value, desc]) => { content += `- **${value}**: ${desc}\n`; }); content += '\n'; } if ('constraints' in param && param.constraints) { content += '### Constraints\n\n'; Object.entries(param.constraints).forEach(([constraint, value]) => { content += `- **${constraint}**: ${value}\n`; }); content += '\n'; } if ('notes' in param && param.notes) { content += `### Notes\n\n${param.notes}\n\n`; } if (param.examples && param.examples.length > 0) { content += '### Examples\n\n'; param.examples.forEach(ex => { content += `- \`${ex.value}\`: ${ex.useCase}\n`; }); content += '\n'; } }); return content; } function generateBestPractices(): string { let content = '# Best Practices Guide\n\n'; content += 'Best practices for using Productboard MCP effectively.\n\n'; content += '## General Best Practices\n\n'; content += '1. **Use appropriate detail levels**: Start with "basic" for performance, use "full" only when needed\n'; content += '2. **Implement pagination**: Use limit/offset for large datasets (max 100 items per request)\n'; content += '3. **Cache responses**: Store frequently accessed data like companies and users locally\n'; content += '4. **Handle errors gracefully**: Implement retry logic with exponential backoff\n'; content += "5. **Use consistent naming**: Follow your team's conventions for tags and labels\n"; content += '6. **Link related entities**: Connect notes to features, features to releases\n'; content += '7. **Use date filters**: Narrow results to relevant time periods\n'; content += '8. **Batch operations**: Group related API calls when possible\n\n'; // Collect best practices from all tools const allPractices: Record<string, Set<string>> = {}; Object.entries(toolDocumentation).forEach(([toolName, doc]) => { if (doc.bestPractices) { doc.bestPractices.forEach(practice => { // Categorize by tool type const category = toolName.split('_')[0]; if (!allPractices[category]) { allPractices[category] = new Set(); } allPractices[category].add(practice); }); } }); Object.entries(allPractices).forEach(([category, practices]) => { content += `## ${category.charAt(0).toUpperCase() + category.slice(1)} Best Practices\n\n`; Array.from(practices).forEach(practice => { content += `- ${practice}\n`; }); content += '\n'; }); return content; } function generateWorkflows(): string { let content = '# Common Workflows\n\n'; content += 'Step-by-step guides for common product management workflows.\n\n'; const workflows = [ { title: 'Feedback to Feature Workflow', description: 'Transform customer feedback into roadmap features', steps: [ { step: 'Collect Customer Feedback', tool: 'create_note', description: 'Import feedback from various sources', example: { title: 'Mobile app performance issue', content: 'App crashes when uploading large files', tags: ['bug', 'mobile', 'high-priority'], user: { email: 'customer@example.com' }, }, }, { step: 'Find Related Feedback', tool: 'list_notes', description: 'Search for similar feedback to understand scope', example: { term: 'crash upload', tags: ['mobile'], dateFrom: '2024-01-01', }, }, { step: 'Create Feature', tool: 'create_feature', description: 'Create a feature to address the feedback', example: { name: 'Improve file upload reliability', description: 'Fix crashes and improve performance for large file uploads', priority: 8.5, effort: 13, status: 'candidate', }, }, { step: 'Link Feedback', tool: 'link_note_to_feature', description: 'Connect all related feedback to the feature', example: { noteId: 'note_123', featureId: 'feat_456', }, }, ], }, { title: 'Release Planning Workflow', description: 'Plan and manage product releases', steps: [ { step: 'Create Release', tool: 'create_release', description: 'Define the release timeline', example: { name: 'Q1 2025 Release', description: 'Major performance improvements and bug fixes', startDate: '2025-01-01', endDate: '2025-03-31', }, }, { step: 'Find Candidate Features', tool: 'list_features', description: 'Identify features ready for release', example: { status: 'candidate', detail: 'standard', limit: 50, }, }, { step: 'Prioritize Features', tool: 'list_features', description: 'Filter by priority and effort', example: { status: 'candidate', minPriority: 7, maxEffort: 13, }, }, { step: 'Assign to Release', tool: 'update_feature_release_assignment', description: 'Add features to the release', example: { featureId: 'feat_789', releaseId: 'rel_123', }, }, ], }, { title: 'Customer Segmentation Workflow', description: 'Analyze feedback by customer segment', steps: [ { step: 'Create Company', tool: 'create_company', description: 'Set up company with segmentation data', example: { name: 'Enterprise Customer Inc', domain: 'enterprise.com', customFields: { tier: 'enterprise', arr: '500000', industry: 'finance', }, }, }, { step: 'Get Company Feedback', tool: 'list_notes', description: 'Retrieve all feedback from the company', example: { companyId: 'comp_123', detail: 'full', }, }, { step: 'Analyze by Segment', tool: 'list_companies', description: 'Compare feedback across segments', example: { customField: 'tier', customFieldValue: 'enterprise', }, }, ], }, ]; workflows.forEach(workflow => { content += `## ${workflow.title}\n\n`; content += `${workflow.description}\n\n`; workflow.steps.forEach((step, i) => { content += `### Step ${i + 1}: ${step.step}\n\n`; content += `**Tool:** \`${step.tool}\`\n\n`; content += `${step.description}\n\n`; content += '**Example:**\n```json\n'; content += JSON.stringify( { tool: step.tool, arguments: step.example, }, null, 2 ); content += '\n```\n\n'; }); }); return content; } function generateCheatsheet(): string { return `# Productboard MCP Cheatsheet ## 🚀 Quick Reference ### Most Common Tools | Tool | Purpose | Key Parameters | |------|---------|----------------| | \`create_note\` | Add customer feedback | title, content, tags, user | | \`list_notes\` | Search feedback | term, tags, dateFrom, limit | | \`create_feature\` | Add to roadmap | name, description, priority, effort | | \`list_features\` | Find features | status, term, detail | | \`create_company\` | Add customer | name, domain | | \`create_release\` | Plan release | name, startDate, endDate | ### Common Parameters | Parameter | Values | Default | Usage | |-----------|--------|---------|--------| | \`detail\` | basic, standard, full | standard | Response detail level | | \`limit\` | 1-100 | 100 | Items per page | | \`offset\` | 0+ | 0 | Pagination offset | | \`includeSubData\` | true/false | false | Include nested data | | \`dateFrom/To\` | YYYY-MM-DD | - | Date filtering | ### Status Values **Features:** - new - candidate - planned - in-progress - released **Notes:** - active - processed - archived ### Quick Examples #### Get Recent High-Priority Feedback \`\`\`json { "tool": "list_notes", "arguments": { "tags": ["high-priority"], "dateFrom": "2025-01-01", "limit": 25 } } \`\`\` #### Create Feature from Feedback \`\`\`json { "tool": "create_feature", "arguments": { "name": "Dark mode support", "description": "Enable dark theme", "priority": 8, "effort": 5 } } \`\`\` #### Find In-Progress Features \`\`\`json { "tool": "list_features", "arguments": { "status": "in-progress", "detail": "basic" } } \`\`\` ### Performance Tips 1. Use \`detail: "basic"\` for lists 2. Paginate with \`limit: 25\` for UI 3. Cache company/user lookups 4. Filter by date to reduce results 5. Use search terms to narrow scope ### Error Codes | Code | Meaning | Action | |------|---------|---------| | 400 | Bad request | Check parameters | | 401 | Unauthorized | Check API token | | 404 | Not found | Verify IDs exist | | 429 | Rate limited | Retry with backoff |`; } function generateToolHelp(toolName: string): string { const doc = toolDocumentation[toolName]; if (!doc) { return `# Tool Not Found The tool "${toolName}" was not found in the documentation. Available tools include: ${Object.keys(toolDocumentation) .map(t => `- ${t}`) .join('\n')} Use \`get_docs\` with \`type: "all-tools"\` to see documentation for all tools.`; } let content = `# Tool: ${toolName}\n\n`; content += `**Description:** ${doc.description}\n\n`; if (doc.detailedDescription) { content += `## Detailed Description\n${doc.detailedDescription.trim()}\n\n`; } content += `## Examples\n\n`; doc.examples.forEach((example, i) => { content += `### Example ${i + 1}: ${example.title}\n\n`; content += `${example.description}\n\n`; content += '**Request:**\n```json\n'; content += JSON.stringify( { tool: toolName, arguments: example.input, }, null, 2 ); content += '\n```\n\n'; if (example.expectedOutput) { content += '**Expected Response:**\n```json\n'; content += JSON.stringify(example.expectedOutput, null, 2); content += '\n```\n\n'; } if (example.notes) { content += `> **Note:** ${example.notes}\n\n`; } }); if (doc.commonErrors && doc.commonErrors.length > 0) { content += `## Common Errors\n\n`; doc.commonErrors.forEach(error => { content += `### ${error.error}\n\n`; content += `- **Cause:** ${error.cause}\n`; content += `- **Solution:** ${error.solution}\n\n`; }); } if (doc.bestPractices && doc.bestPractices.length > 0) { content += `## Best Practices\n\n`; doc.bestPractices.forEach(practice => { content += `- ${practice}\n`; }); content += '\n'; } if (doc.relatedTools && doc.relatedTools.length > 0) { content += `## Related Tools\n\n`; content += doc.relatedTools.map(tool => `- \`${tool}\``).join('\n'); content += '\n'; } return content; } function generateAllToolsDocumentation(): string { let content = '# All Tools Documentation\n\n'; content += 'Complete documentation for all Productboard MCP tools.\n\n'; content += '## Table of Contents\n\n'; Object.keys(toolDocumentation).forEach(tool => { content += `- [${tool}](#${tool.replace(/_/g, '-')})\n`; }); content += '\n---\n\n'; Object.entries(toolDocumentation).forEach(([toolName, _doc]) => { content += generateToolHelp(toolName); content += '\n---\n\n'; }); return content; } function getReadmeContent(): string { try { const readmePath = join(dirname(__dirname), '..', 'README.md'); if (existsSync(readmePath)) { return readFileSync(readmePath, 'utf-8'); } return '# README not found\n\nThe README.md file could not be located.'; } catch (error) { return `# Error reading README\n\nFailed to read README.md: ${error}`; } } function getGeneratedToolHelp(toolName: string): string { try { // First check if generated docs exist const generatedDir = join(dirname(__dirname), '..', 'generated'); if (!existsSync(generatedDir)) { return generateToolHelp(toolName); // Fallback to old method } // Find the tool's documentation file const categories = readdirSync(generatedDir).filter((f: string) => statSync(join(generatedDir, f)).isDirectory() ); for (const category of categories) { const toolFile = join(generatedDir, category, `${toolName}.md`); if (existsSync(toolFile)) { return readFileSync(toolFile, 'utf-8'); } } // If not found, fallback to old method or return not found const doc = toolDocumentation[toolName]; if (doc) { return generateToolHelp(toolName); } return `# Tool Not Found The tool "${toolName}" was not found in the documentation. Use \`get_docs\` with \`type: "all-tools"\` to see documentation for all tools.`; } catch (error) { return `# Error reading tool documentation Failed to read documentation for "${toolName}": ${error}`; } }