UNPKG

@the_cfdude/productboard-mcp

Version:

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

1,246 lines (1,211 loc) • 28.5 kB
#!/usr/bin/env node /** * Generate tool manifest from actual MCP server tools */ import { writeFileSync, mkdirSync, existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PROJECT_ROOT = join(__dirname, '..'); const OUTPUT_DIR = join(PROJECT_ROOT, 'generated'); const MANIFEST_PATH = join(OUTPUT_DIR, 'manifest.json'); // Ensure output directory exists if (!existsSync(OUTPUT_DIR)) { mkdirSync(OUTPUT_DIR, { recursive: true }); } interface ToolInfo { category: string; operation: string; description: string; requiredParams: string[]; optionalParams: string[]; implementation: string; } interface CategoryInfo { displayName: string; description: string; tools: string[]; } interface ToolManifest { version: string; generated: string; categories: Record<string, CategoryInfo>; tools: Record<string, ToolInfo>; } /** * Tools that have static implementations and should NOT be included in manifest * These tools have their own detailed schemas in their respective modules */ const STATIC_IMPLEMENTATION_TOOLS = [ 'create_feature', 'update_feature', 'delete_feature', 'get_features', 'get_feature', 'create_component', 'update_component', 'get_components', 'get_component', 'create_product', 'update_product', 'get_products', 'get_product', 'create_note', 'update_note', 'delete_note', 'get_notes', 'get_note', 'create_company', 'update_company', 'delete_company', 'get_companies', 'get_company', 'create_user', 'update_user', 'delete_user', 'get_users', 'get_user', 'create_release', 'update_release', 'delete_release', 'get_releases', 'get_release', 'create_release_group', 'update_release_group', 'delete_release_group', 'get_release_groups', 'get_release_group', 'create_webhook', 'list_webhooks', 'get_webhook', 'delete_webhook', 'create_objective', 'update_objective', 'delete_objective', 'get_objectives', 'get_objective', 'create_initiative', 'update_initiative', 'delete_initiative', 'get_initiatives', 'get_initiative', 'create_key_result', 'update_key_result', 'delete_key_result', 'get_key_results', 'get_key_result', 'get_custom_fields', 'get_custom_field', 'get_custom_fields_values', 'get_custom_field_value', 'set_custom_field_value', 'delete_custom_field_value', 'get_feature_statuses', ]; /** * Load tools from actual MCP server implementations */ async function loadMcpTools(): Promise<{ tools: Record<string, ToolInfo>; categories: Record<string, CategoryInfo>; }> { const tools: Record<string, ToolInfo> = {}; const categories: Record<string, CategoryInfo> = {}; // Helper to register a category const registerCategory = ( key: string, displayName: string, description: string ) => { if (!categories[key]) { categories[key] = { displayName, description, tools: [], }; } }; // Helper to register a tool const registerTool = ( name: string, category: string, description: string, requiredParams: string[], optionalParams: string[] ) => { // Skip static implementation tools from manifest but add to category if (STATIC_IMPLEMENTATION_TOOLS.includes(name)) { console.log(`ā­ļø Skipping ${name} (static implementation)`); // Still add to category tools array if (categories[category]) { categories[category].tools.push(name); } return; } tools[name] = { category, operation: name.toUpperCase(), description, requiredParams, optionalParams, implementation: 'mcp', }; if (categories[category]) { categories[category].tools.push(name); } }; // Notes tools registerCategory('notes', 'Notes', 'Customer feedback and notes management'); registerTool( 'create_note', 'notes', 'Create a new note in Productboard', ['title', 'content'], [ 'ownerEmail', 'userEmail', 'userName', 'userExternalId', 'companyDomain', 'sourceOrigin', 'sourceRecordId', 'displayUrl', 'tags', ] ); registerTool( 'get_notes', 'notes', 'List all notes with filtering and pagination', [], [ 'limit', 'startWith', 'detail', 'includeSubData', 'allTags', 'anyTag', 'companyId', 'createdFrom', 'createdTo', 'dateFrom', 'dateTo', 'featureId', 'ownerEmail', 'source', 'term', 'updatedFrom', 'updatedTo', ] ); registerTool( 'get_note', 'notes', 'Get a specific note by ID', ['id'], ['detail', 'includeSubData'] ); registerTool( 'update_note', 'notes', 'Update an existing note', ['id'], ['title', 'content', 'tags'] ); registerTool('delete_note', 'notes', 'Delete a note', ['id'], []); registerTool( 'bulk_add_note_followers', 'notes', 'Add followers to a note', ['noteId'], ['body'] ); registerTool( 'remove_note_follower', 'notes', 'Remove a follower from a note', ['noteId', 'email'], [] ); registerTool('list_tags', 'notes', 'List tags', ['noteId'], []); registerTool( 'create_note_tag', 'notes', 'Create a tag', ['noteId', 'tagName'], [] ); registerTool( 'delete_note_tag', 'notes', 'Remove a tag from a note', ['noteId', 'tagName'], [] ); registerTool('list_links', 'notes', 'List links', ['noteId'], []); registerTool( 'create_link', 'notes', 'Create a link', ['noteId', 'entityId'], [] ); registerTool( 'list_feedback_form_configurations', 'notes', 'List all feedback form configurations', [], [] ); registerTool( 'get_feedback_form_configuration', 'notes', 'Retrieve a feedback form configuration', ['id'], [] ); registerTool( 'submit_feedback_form', 'notes', 'Submit a feedback form', [], ['body'] ); // Features tools (includes components and products) registerCategory( 'features', 'Features', 'Product features, components, and products management' ); registerTool( 'create_feature', 'features', 'Create a new feature in Productboard', ['name'], ['description', 'status', 'owner', 'parent'] ); registerTool( 'get_features', 'features', 'List all features in Productboard', [], [ 'limit', 'startWith', 'detail', 'includeSubData', 'archived', 'noteId', 'ownerEmail', 'parentId', 'statusId', 'statusName', ] ); registerTool( 'get_feature', 'features', 'Get a specific feature by ID', ['id'], ['detail', 'includeSubData'] ); registerTool( 'update_feature', 'features', 'Update a feature. Supports both standard fields and custom fields - pass custom field names as additional parameters (e.g., "T-Shirt Sizing": "large").', ['id'], [ 'name', 'description', 'status', 'owner', 'archived', 'timeframe', 'parentId', 'componentId', 'productId', '...customFields', ] ); registerTool('delete_feature', 'features', 'Delete a feature', ['id'], []); registerTool( 'create_component', 'features', 'Create a new component in Productboard', ['name'], ['description', 'parent'] ); registerTool( 'get_components', 'features', 'List all components in Productboard', [], ['limit', 'startWith', 'detail', 'includeSubData', 'productId'] ); registerTool( 'get_component', 'features', 'Get a specific component by ID', ['id'], ['detail', 'includeSubData'] ); registerTool( 'update_component', 'features', 'Update a component', ['id'], ['name', 'description'] ); registerTool( 'get_products', 'features', 'List all products in Productboard', [], ['limit', 'startWith', 'detail', 'includeSubData'] ); registerTool( 'get_product', 'features', 'Get a specific product by ID', ['id'], ['detail', 'includeSubData'] ); registerTool( 'update_product', 'features', 'Update a product', ['id'], ['name', 'description'] ); // Companies tools (includes users) registerCategory('companies', 'Companies', 'Company and user management'); registerTool( 'create_company', 'companies', 'Create a new company', ['name'], ['description', 'domain', 'externalId'] ); registerTool( 'get_companies', 'companies', 'List all companies', [], [ 'detail', 'featureId', 'hasNotes', 'includeSubData', 'limit', 'startWith', 'term', ] ); registerTool( 'get_company', 'companies', 'Retrieve a specific company', ['id'], ['detail', 'includeSubData'] ); registerTool( 'update_company', 'companies', 'Update a company', ['id', 'body'], [] ); registerTool('delete_company', 'companies', 'Delete a company', ['id'], []); registerTool( 'create_company_field', 'companies', 'Create a company field', ['body'], [] ); registerTool( 'list_company_fields', 'companies', 'Retrieve company fields', [], [] ); registerTool( 'get_company_field', 'companies', 'Retrieve a company field', ['id'], [] ); registerTool( 'update_company_field', 'companies', 'Update a company field', ['id', 'body'], [] ); registerTool( 'delete_company_field', 'companies', 'Delete a company field', ['id'], [] ); registerTool( 'get_company_field_value', 'companies', 'Retrieve company field value', ['companyId', 'companyCustomFieldId'], [] ); registerTool( 'set_company_field_value', 'companies', 'Sets company field value', ['companyId', 'companyCustomFieldId', 'body'], [] ); registerTool( 'delete_company_field_value', 'companies', 'Delete company field value', ['companyId', 'companyCustomFieldId'], [] ); registerTool( 'get_users', 'companies', 'List all users in Productboard', [], ['detail', 'includeSubData', 'limit', 'startWith'] ); registerTool( 'create_user', 'companies', 'Create a new user in Productboard', ['email'], ['name', 'role', 'companyId', 'externalId'] ); registerTool( 'get_user', 'companies', 'Get a specific user by ID', ['id'], ['detail', 'includeSubData'] ); registerTool( 'update_user', 'companies', 'Update an existing user', ['id'], ['name', 'role', 'companyId', 'externalId'] ); registerTool('delete_user', 'companies', 'Delete a user', ['id'], []); // Webhooks tools registerCategory('webhooks', 'Webhooks', 'Webhook subscription management'); registerTool( 'post_webhook', 'webhooks', 'Create a new subscription', ['body'], [] ); registerTool('get_webhooks', 'webhooks', 'List all subscriptions', [], []); registerTool( 'get_webhook', 'webhooks', 'Get a specific webhook subscription by ID', ['id'], ['detail', 'includeSubData'] ); registerTool( 'delete_webhook', 'webhooks', 'Delete a webhook subscription', ['id'], [] ); // Objectives tools (includes initiatives and key results) registerCategory( 'objectives', 'Objectives', 'Strategic objectives, initiatives, and key results management' ); registerTool( 'get_objectives', 'objectives', 'List all objectives', [], ['detail', 'includeSubData', 'limit', 'startWith'] ); registerTool( 'create_objective', 'objectives', 'Create a new objective', ['name'], ['description', 'startDate', 'endDate', 'ownerId'] ); registerTool( 'get_objective', 'objectives', 'Get a specific objective by ID', ['id'], ['detail', 'includeSubData'] ); registerTool( 'update_objective', 'objectives', 'Update an existing objective', ['id'], ['name', 'description', 'startDate', 'endDate', 'ownerId'] ); registerTool( 'delete_objective', 'objectives', 'Delete an objective', ['id'], [] ); registerTool( 'list_links_objective_to_features', 'objectives', 'List features linked to a specific objective', ['id'], [] ); registerTool( 'list_links_objective_to_initiatives', 'objectives', 'List initiatives linked to a specific objective', ['id'], [] ); registerTool( 'create_objective_to_initiative_link', 'objectives', 'Create a new link between an objective and an initiative', ['id', 'initiativeId'], [] ); registerTool( 'delete_objective_to_initiative_link', 'objectives', 'Delete a link between an objective and an initiative', ['id', 'initiativeId'], [] ); registerTool( 'create_objective_to_feature_link', 'objectives', 'Create a new link between an objective and a feature', ['id', 'featureId'], [] ); registerTool( 'delete_objective_to_feature_link', 'objectives', 'Delete a link between an objective and a feature', ['id', 'featureId'], [] ); registerTool( 'get_initiatives', 'objectives', 'List all initiatives', [], ['detail', 'includeSubData', 'limit', 'startWith'] ); registerTool( 'create_initiative', 'objectives', 'Create a new initiative', ['name'], ['description', 'ownerId', 'status'] ); registerTool( 'get_initiative', 'objectives', 'Get a specific initiative by ID', ['id'], ['detail', 'includeSubData'] ); registerTool( 'update_initiative', 'objectives', 'Update an existing initiative', ['id'], ['name', 'description', 'ownerId', 'status'] ); registerTool( 'delete_initiative', 'objectives', 'Delete an initiative', ['id'], [] ); registerTool( 'list_links_initiative_to_objectives', 'objectives', 'List objectives linked to a specific initiative', ['id'], [] ); registerTool( 'list_links_initiative_to_features', 'objectives', 'List features linked to a specific initiative', ['id'], [] ); registerTool( 'create_initiative_to_objective_link', 'objectives', 'Create a new link between an initiative and an objective', ['id', 'objectiveId'], [] ); registerTool( 'delete_initiative_to_objective_link', 'objectives', 'Delete a link between an initiative and an objective', ['id', 'objectiveId'], [] ); registerTool( 'create_initiative_to_feature_link', 'objectives', 'Create a new link between an initiative and a feature', ['id', 'featureId'], [] ); registerTool( 'delete_initiative_to_feature_link', 'objectives', 'Delete a link between an initiative and a feature', ['id', 'featureId'], [] ); registerTool( 'get_key_results', 'objectives', 'List all key results', [], ['detail', 'includeSubData', 'limit', 'startWith'] ); registerTool( 'create_key_result', 'objectives', 'Create a new key result', ['name', 'objectiveId', 'type', 'targetValue'], ['currentValue', 'startValue'] ); registerTool( 'get_key_result', 'objectives', 'Get a specific key result by ID', ['id'], ['detail', 'includeSubData'] ); registerTool( 'update_key_result', 'objectives', 'Update an existing key result', ['id'], ['name', 'targetValue', 'currentValue'] ); registerTool( 'delete_key_result', 'objectives', 'Delete a key result', ['id'], [] ); // Custom fields tools registerCategory( 'custom-fields', 'Custom Fields', 'Custom field management for hierarchy entities' ); registerTool( 'get_custom_fields', 'custom-fields', 'List all custom fields for hierarchy entities', ['type'], [] ); registerTool( 'get_custom_fields_values', 'custom-fields', 'List all custom field values', [], ['customField.id', 'hierarchyEntity.id', 'type'] ); registerTool( 'get_custom_field', 'custom-fields', 'Retrieve a specific custom field', ['id'], [] ); registerTool( 'get_custom_field_value', 'custom-fields', 'Retrieve a custom field value for a hierarchy entity', ['customField.id', 'hierarchyEntity.id'], [] ); registerTool( 'set_custom_field_value', 'custom-fields', 'Set value of a custom field for a hierarchy entity', ['customField.id', 'hierarchyEntity.id', 'body'], [] ); registerTool( 'delete_custom_field_value', 'custom-fields', 'Delete value of a custom field for a hierarchy entity', ['customField.id', 'hierarchyEntity.id'], [] ); registerTool( 'get_feature_statuses', 'custom-fields', 'List all feature statuses', [], [] ); // Releases tools registerCategory( 'releases', 'Releases', 'Release and release group management' ); registerTool( 'create_release_group', 'releases', 'Create a new release group', ['name'], ['description', 'isDefault'] ); registerTool( 'list_release_groups', 'releases', 'List all release groups', [], [] ); registerTool( 'get_release_group', 'releases', 'Get a specific release group by ID', ['id'], ['detail', 'includeSubData'] ); registerTool( 'update_release_group', 'releases', 'Update an existing release group', ['id'], ['name', 'description', 'isDefault'] ); registerTool( 'delete_release_group', 'releases', 'Delete a release group', ['id'], [] ); registerTool( 'create_release', 'releases', 'Create a new release', ['name', 'releaseGroupId'], ['description', 'startDate', 'releaseDate', 'state'] ); registerTool( 'list_releases', 'releases', 'List all releases', [], ['releaseGroup.id'] ); registerTool( 'get_release', 'releases', 'Get a specific release by ID', ['id'], ['detail', 'includeSubData'] ); registerTool( 'update_release', 'releases', 'Update an existing release', ['id'], ['name', 'description', 'startDate', 'releaseDate', 'state'] ); registerTool('delete_release', 'releases', 'Delete a release', ['id'], []); registerTool( 'list_feature_release_assignments', 'releases', 'List all feature release assignments', [], [ 'feature.id', 'release.id', 'release.state', 'release.timeframe.endDate.from', 'release.timeframe.endDate.to', ] ); registerTool( 'get_feature_release_assignment', 'releases', 'Retrieve a feature release assignment', ['release.id', 'feature.id'], [] ); registerTool( 'update_feature_release_assignment', 'releases', 'Update a feature release assignment', ['release.id', 'feature.id', 'body'], [] ); // Plugin integrations tools registerCategory( 'plugin-integrations', 'Plugin Integrations', 'Plugin integration management' ); registerTool( 'post_plugin_integration', 'plugin-integrations', 'Create a plugin integration', ['body'], [] ); registerTool( 'get_plugin_integrations', 'plugin-integrations', 'List all plugin integrations', [], [] ); registerTool( 'get_plugin_integration', 'plugin-integrations', 'Retrieve a plugin integration', ['id'], [] ); registerTool( 'patch_plugin_integration', 'plugin-integrations', 'Update a plugin integration', ['id', 'body'], [] ); registerTool( 'put_plugin_integration', 'plugin-integrations', 'Update a plugin integration', ['id', 'body'], [] ); registerTool( 'delete_plugin_integration', 'plugin-integrations', 'Delete a plugin integration', ['id'], [] ); registerTool( 'get_plugin_integration_connections', 'plugin-integrations', 'List all plugin integration connections', ['id'], [] ); registerTool( 'get_plugin_integration_connection', 'plugin-integrations', 'Retrieve a plugin integration connection', ['id', 'featureId'], [] ); registerTool( 'put_plugin_integration_connection', 'plugin-integrations', 'Set a plugin integration connection', ['id', 'featureId', 'body'], [] ); registerTool( 'delete_plugin_integration_connection', 'plugin-integrations', 'Delete a plugin integration connection', ['id', 'featureId'], [] ); // Jira integrations tools registerCategory( 'jira-integrations', 'Jira Integrations', 'Jira integration management' ); registerTool( 'get_jira_integration', 'jira-integrations', 'Retrieve a Jira integration', ['id'], [] ); registerTool( 'get_jira_integrations', 'jira-integrations', 'List all Jira integrations', [], [] ); registerTool( 'get_jira_integration_connection', 'jira-integrations', 'Retrieve a Jira integration connection', ['id', 'featureId'], [] ); registerTool( 'get_jira_integration_connections', 'jira-integrations', 'List all Jira integration connections', ['id'], ['connection.issueId', 'connection.issueKey'] ); // Search tool (custom) registerCategory( 'search', 'Universal Search', 'Universal search across all Productboard entities' ); registerTool( 'search', 'search', 'Universal search across all Productboard entities with flexible filtering and output control', ['entityType'], [ 'filters', 'operators', 'output', 'limit', 'startWith', 'detail', 'includeSubData', ] ); // Performance tools registerCategory( 'performance', 'Performance Tools', 'High-performance lightweight tools for status checking, validation, and monitoring' ); registerTool( 'check_entity_status', 'performance', 'Check status for multiple entities with minimal data transfer. Optimized for quick status overview of large entity sets.', ['entityType', 'ids'], ['fields', 'format', 'useCache', 'instance', 'workspaceId'] ); registerTool( 'validate_entity_existence', 'performance', 'Validate existence of multiple entities efficiently. Returns missing/existing entity lists.', ['entityType', 'ids'], ['returnMissing', 'returnExisting', 'useCache', 'instance', 'workspaceId'] ); registerTool( 'track_batch_progress', 'performance', 'Track progress for batch operations using custom markers (e.g., status:completed, customField).', ['entityType', 'ids', 'progressMarker'], ['includeDetails', 'groupBy', 'useCache', 'instance', 'workspaceId'] ); registerTool( 'get_entity_counts', 'performance', 'Get entity counts without fetching full data. Optimized for dashboard metrics and overview statistics.', ['entityType'], ['filters', 'useCache', 'instance', 'workspaceId'] ); registerTool( 'perform_health_check', 'performance', 'Perform comprehensive system health check including API connectivity, cache status, and memory usage.', [], [ 'includeDetails', 'includeCacheStats', 'includeMemoryStats', 'instance', 'workspaceId', ] ); registerTool( 'get_performance_stats', 'performance', 'Get detailed performance statistics including response times, cache hit rates, and percentiles.', [], [ 'operation', 'includePercentiles', 'clearOldMetrics', 'instance', 'workspaceId', ] ); registerTool( 'perform_cleanup', 'performance', 'Clear caches and perform system cleanup. Useful for memory management and troubleshooting.', [], ['clearCache', 'clearMetrics', 'forceGC', 'instance', 'workspaceId'] ); // Bulk operations tools registerCategory( 'bulk-operations', 'Bulk Operations', 'Efficient bulk update operations with change tracking and diff analysis' ); registerTool( 'perform_bulk_update', 'bulk-operations', 'Perform batch updates with diff tracking and change analysis. Supports features, notes, companies, users, and objectives.', ['entityType', 'updates'], [ 'batchSize', 'concurrency', 'continueOnError', 'validateBeforeUpdate', 'trackChanges', 'diffFormat', 'includeUnchanged', 'instance', 'workspaceId', ] ); registerTool( 'compare_entities', 'bulk-operations', 'Compare current entity state with proposed changes without making updates. Shows what would change.', ['entityType', 'comparisons'], [ 'diffFormat', 'highlightSignificant', 'includeUnchanged', 'instance', 'workspaceId', ] ); registerTool( 'validate_bulk_update', 'bulk-operations', 'Validate bulk update request without executing. Checks for errors and potential issues.', ['entityType', 'updates'], ['checkExistence', 'validateFields', 'instance', 'workspaceId'] ); // Context-aware tools registerCategory( 'context-aware', 'Context-Aware Features', 'Intelligent response adaptation and user context management with personalization' ); registerTool( 'set_user_context', 'context-aware', 'Set user context for intelligent response adaptation and personalization.', ['sessionId'], ['userPreferences', 'workspaceContext', 'instanceContext'] ); registerTool( 'get_user_context', 'context-aware', 'Retrieve current user context and preferences.', ['sessionId'], [] ); registerTool( 'adapt_response', 'context-aware', 'Adapt a response based on user context and preferences. Provides intelligent formatting and guidance.', ['sessionId', 'query', 'originalResponse'], ['includeGuidance', 'includeSuggestions'] ); registerTool( 'add_adaptation_rule', 'context-aware', 'Add custom adaptation rule for response processing. Allows personalized response handling.', ['name', 'description', 'priority', 'condition', 'adaptation'], ['sessionId'] ); registerTool( 'clear_user_context', 'context-aware', 'Clear all context data for a session. Useful for privacy or starting fresh.', ['sessionId'], [] ); registerTool( 'get_context_stats', 'context-aware', 'Get system statistics about context-aware features usage and performance.', [], [] ); return { tools, categories }; } /** * Main function to generate manifest */ async function main(): Promise<void> { console.log('šŸš€ Starting tool manifest generation...'); try { console.log('šŸ“– Loading MCP server tools...'); const { tools, categories } = await loadMcpTools(); const manifest: ToolManifest = { version: '1.0.0', generated: new Date().toISOString(), categories, tools, }; // Convert arrays to single-line format for CI compatibility let manifestJson = JSON.stringify(manifest, null, 2); // Convert multiline arrays to single-line arrays using regex manifestJson = manifestJson.replace( /\[\s*\n\s*([^\]]+)\n\s*\]/g, (match, content) => { const items = content .split(',') .map((item: string) => item.trim()) .filter((item: string) => item.length > 0); return `[${items.join(', ')}]`; } ); writeFileSync(MANIFEST_PATH, manifestJson); console.log( `āœ… Generated manifest with ${Object.keys(tools).length} tools across ${Object.keys(categories).length} categories` ); console.log(`šŸ“ Output: ${MANIFEST_PATH}`); // Summary console.log('\nšŸ“Š Category Summary:'); Object.entries(categories).forEach(([key, category]) => { console.log( ` - ${category.displayName}: ${category.tools.length} tools` ); }); } catch (error) { console.error('āŒ Error generating manifest:', error); process.exit(1); } } // Run the script main();