@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
210 lines (182 loc) • 6.46 kB
text/typescript
/**
* Dynamic tool registration and setup with lazy loading
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { join } from 'path';
import { existsSync } from 'fs';
import { ToolRegistry } from './registry.js';
import { loadConfig } from '../config.js';
import { debugLog } from '../utils/debug-logger.js';
import { SessionState } from '../session-manager.js';
/**
* Get enabled categories from configuration
*/
function getEnabledCategories(): string[] {
const config = loadConfig();
const toolConfig = config.toolCategories;
if (!toolConfig) {
// Default categories if no configuration - enable ALL categories
return [];
}
// Handle profile-based configuration
if (
toolConfig.activeProfile &&
toolConfig.profiles?.[toolConfig.activeProfile]
) {
return toolConfig.profiles[toolConfig.activeProfile];
}
// Handle explicit enabled/disabled lists
if (toolConfig.enabled) {
// Check if wildcard is used
if (toolConfig.enabled.includes('*')) {
// Return empty array to signal "all categories"
return [];
}
return toolConfig.enabled;
}
// If only disabled list provided, enable all except disabled
if (toolConfig.disabled) {
// This would require loading manifest to get all categories
// For now, return default minus disabled
const defaults = [
'notes',
'features',
'companies',
'users',
'releases',
'webhooks',
];
return defaults.filter(cat => !toolConfig.disabled!.includes(cat));
}
// Default categories - enable ALL categories
return [];
}
/**
* Setup dynamic tool handlers for the server
*/
export async function setupDynamicToolHandlers(
server: Server,
session?: SessionState
) {
// Create tool registry
const registry = new ToolRegistry(getEnabledCategories());
// Load manifest
const manifestPath = join(process.cwd(), 'generated', 'manifest.json');
// Check if manifest exists, if not use static tools
if (!existsSync(manifestPath)) {
console.warn('Tool manifest not found, falling back to static tools');
// Import and use the original static setup
const module = await import('./index.js');
module.setupToolHandlers(server, session);
return;
}
// Load manifest and register tools
try {
registry.loadManifest(manifestPath);
// Register tool loaders from manifest - AWAIT this!
await registry.registerFromManifest();
console.error('Successfully registered tools from manifest');
// Register custom MCP tools (not from OpenAPI)
const { setupSearchTools } = await import('./search.js');
const searchTools = setupSearchTools();
for (const tool of searchTools) {
registry.registerCustomTool(tool.name, tool);
}
console.error('Successfully registered custom MCP tools');
} catch (error) {
console.error('Failed to load tool manifest:', error);
// Fall back to static tools
const module = await import('./index.js');
module.setupToolHandlers(server, session);
return;
}
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = await registry.getToolDefinitions();
// DEBUG: Log tool counts
console.error(`📊 Total tools available: ${tools.length}`);
const createTools = tools.filter(t => t.name.startsWith('create_'));
console.error(
`✨ Create tools available: ${createTools.map(t => t.name).join(', ')}`
);
// DEBUG: Log create_component schema
const createComponentTool = tools.find(t => t.name === 'create_component');
if (createComponentTool) {
console.error(
'🔍 DEBUG: create_component tool being served to MCP client:'
);
console.error(JSON.stringify(createComponentTool, null, 2));
} else {
console.error('❌ create_component tool NOT FOUND in tools list');
}
return { tools };
});
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async request => {
const { name, arguments: args } = request.params;
debugLog('index-dynamic', 'Tool called via MCP', { name, args });
try {
const result = await registry.executeTool(name, args || {});
debugLog('index-dynamic', 'Tool execution completed', {
name,
hasContent: !!result?.content,
contentLength: result?.content?.[0]?.text?.length || 0,
});
return result;
} catch (error) {
if (error instanceof McpError) {
throw error;
}
console.error(`Error in tool ${name}:`, error);
// Check if this is an axios error with response data
if ((error as any).response) {
const status = (error as any).response.status;
const data = (error as any).response.data;
// Include full error details for AI agents to understand
const errorDetails = {
status,
data,
message: error instanceof Error ? error.message : String(error),
tool: name,
hint: 'Check the data field for API-specific error details',
};
// Construct a helpful error message
let message = `API request failed with status ${status}`;
if (data?.errors) {
message += `: ${JSON.stringify(data.errors)}`;
} else if (data?.message) {
message += `: ${data.message}`;
} else if (data) {
message += `: ${JSON.stringify(data)}`;
}
throw new McpError(ErrorCode.InternalError, message, errorDetails);
}
// For non-axios errors, include as much detail as possible
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`,
{
originalError:
error instanceof Error ? error.toString() : String(error),
tool: name,
}
);
}
});
// Handle configuration updates (could be triggered by a special tool)
server.onerror = error => {
console.error('[MCP Error]', error);
// Check if we need to reload configuration
if (error.message?.includes('configuration')) {
const newCategories = getEnabledCategories();
registry.updateEnabledCategories(newCategories);
// Categories updated successfully
}
};
}