@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
235 lines (223 loc) • 9.29 kB
text/typescript
/**
* Tool registration and setup
* Implements 3-tier architecture: workflows → resource operations → power user tools
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
// Import tool handlers
import { setupNotesTools } from './notes.js';
import { setupFeaturesTools } from './features.js';
import { setupProductsTools } from './products.js';
import { setupComponentsTools } from './components.js';
import { setupCompaniesTools } from './companies.js';
import { setupUsersTools } from './users.js';
import { setupReleasesTools } from './releases.js';
import { setupWebhooksTools } from './webhooks.js';
import { setupObjectivesTools } from './objectives.js';
import { setupCustomFieldsTools } from './custom-fields.js';
import { setupPluginIntegrationsTools } from './plugin-integrations.js';
import { setupJiraIntegrationsTools } from './jira-integrations.js';
import { setupDocumentationTools } from './documentation.js';
import { setupSearchTools } from './search.js';
import { setupPerformanceTools } from './performance.js';
import { setupBulkOperationsTools } from './bulk-operations.js';
import { setupContextAwareTools } from './context-aware.js';
import { ToolDefinition } from '../types/tool-types.js';
import { SearchParams } from '../types/search-types.js';
import { SessionState } from '../session-manager.js';
/**
* Setup all tool handlers for the server with session support
*/
export function setupToolHandlers(
server: Server,
_session?: SessionState
): void {
// Tool definitions registry
const tools: ToolDefinition[] = [];
// Register tool categories
tools.push(...setupSearchTools());
tools.push(...setupPerformanceTools());
tools.push(...setupBulkOperationsTools());
tools.push(...setupContextAwareTools());
tools.push(...setupNotesTools());
tools.push(...setupFeaturesTools());
tools.push(...setupProductsTools());
tools.push(...setupComponentsTools());
tools.push(...setupCompaniesTools());
tools.push(...setupUsersTools());
tools.push(...setupReleasesTools());
tools.push(...setupWebhooksTools());
tools.push(...setupObjectivesTools());
tools.push(...setupCustomFieldsTools());
tools.push(...setupPluginIntegrationsTools());
tools.push(...setupJiraIntegrationsTools());
tools.push(...setupDocumentationTools());
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async request => {
const { name, arguments: args } = request.params;
try {
// Route to appropriate handler based on tool name patterns
if (name === 'search') {
const { handleSearchTool } = await import('./search.js');
// Debug logging
console.error('[DEBUG] Original args:', JSON.stringify(args, null, 2));
console.error('[DEBUG] Original output type:', typeof args?.output);
console.error('[DEBUG] Original output value:', args?.output);
// Parse any JSON-stringified parameters - more robust approach
const parsedArgs = args ? { ...args } : {};
// Handle output parameter that might come in as a stringified array
if (parsedArgs.output && typeof parsedArgs.output === 'string') {
console.error(
'[DEBUG] Attempting to parse stringified output:',
parsedArgs.output
);
// Try to parse as JSON if it looks like an array or object
if (
(parsedArgs.output.startsWith('[') &&
parsedArgs.output.endsWith(']')) ||
(parsedArgs.output.startsWith('{') &&
parsedArgs.output.endsWith('}'))
) {
try {
const parsed = JSON.parse(parsedArgs.output);
parsedArgs.output = parsed;
console.error(
'[DEBUG] Successfully parsed output to:',
parsed,
'type:',
typeof parsed
);
} catch (e) {
console.error(
'[DEBUG] Failed to parse output, leaving as string:',
(e as Error).message
);
// If parsing fails, leave as string (might be a preset like "ids-only")
}
} else {
console.error(
'[DEBUG] Output string does not look like JSON, leaving as-is'
);
}
} else {
console.error(
'[DEBUG] Output is not a string, type:',
typeof parsedArgs.output
);
}
console.error('[DEBUG] Final parsed args output:', parsedArgs.output);
return await handleSearchTool(
name,
parsedArgs as unknown as SearchParams
);
} else if (
name.includes('note') ||
name.includes('tag') ||
name.includes('link')
) {
const { handleNotesTool } = await import('./notes.js');
return await handleNotesTool(name, args || {});
} else if (name.includes('feature')) {
const { handleFeaturesTool } = await import('./features.js');
return await handleFeaturesTool(name, args || {});
} else if (name.includes('product')) {
const { handleProductsTool } = await import('./products.js');
return await handleProductsTool(name, args || {});
} else if (name.includes('component')) {
const { handleComponentsTool } = await import('./components.js');
return await handleComponentsTool(name, args || {});
} else if (
name === 'get_custom_fields' ||
name.startsWith('get_custom_field') ||
name.startsWith('set_custom_field') ||
name.startsWith('delete_custom_field') ||
name === 'get_feature_statuses'
) {
const { handleCustomFieldsTool } = await import('./custom-fields.js');
return await handleCustomFieldsTool(name, args || {});
} else if (name.includes('plugin_integration')) {
const { handlePluginIntegrationsTool } = await import(
'./plugin-integrations.js'
);
return await handlePluginIntegrationsTool(name, args || {});
} else if (name.includes('jira_integration')) {
const { handleJiraIntegrationsTool } = await import(
'./jira-integrations.js'
);
return await handleJiraIntegrationsTool(name, args || {});
} else if (name.includes('company')) {
const { handleCompaniesTool } = await import('./companies.js');
return await handleCompaniesTool(name, args || {});
} else if (name.includes('user')) {
const { handleUsersTool } = await import('./users.js');
return await handleUsersTool(name, args || {});
} else if (name.includes('release')) {
const { handleReleasesTool } = await import('./releases.js');
return await handleReleasesTool(name, args || {});
} else if (name.includes('webhook')) {
const { handleWebhooksTool } = await import('./webhooks.js');
return await handleWebhooksTool(name, args || {});
} else if (
name.includes('objective') ||
name.includes('initiative') ||
name.includes('key_result')
) {
const { handleObjectivesTool } = await import('./objectives.js');
return await handleObjectivesTool(name, args || {});
} else if (name === 'get_docs') {
const { handleDocumentationTool } = await import('./documentation.js');
return await handleDocumentationTool(name, args || {});
} else if (
name.includes('entity_status') ||
name.includes('entity_existence') ||
name.includes('batch_progress') ||
name.includes('entity_counts') ||
name.includes('health_check') ||
name.includes('performance_stats') ||
name.includes('cleanup')
) {
const { handlePerformanceTool } = await import('./performance.js');
return await handlePerformanceTool(name, args || {});
} else if (
name === 'perform_bulk_update' ||
name === 'compare_entities' ||
name === 'validate_bulk_update'
) {
const { handleBulkOperationsTool } = await import(
'./bulk-operations.js'
);
return await handleBulkOperationsTool(name, args || {});
} else if (
name === 'set_user_context' ||
name === 'get_user_context' ||
name === 'adapt_response' ||
name === 'add_adaptation_rule' ||
name === 'clear_user_context' ||
name === 'get_context_stats'
) {
const { handleContextAwareTool } = await import('./context-aware.js');
return await handleContextAwareTool(name, args || {});
} else {
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
console.error(`Error in tool ${name}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}