UNPKG

@todo-for-ai/mcp

Version:

Model Context Protocol server for Todo for AI task management system with Streamable HTTP transport. Provides AI assistants with access to task management, project information, and feedback submission capabilities through modern HTTP-based communication.

732 lines 32.9 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { TodoApiClient } from './api-client.js'; import { logger } from './logger.js'; import { CONFIG } from './config.js'; import { TransportFactory } from './transports/factory.js'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; // Get package version const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8')); const VERSION = packageJson.version; export class TodoMcpServer { server; apiClient; transport; instanceId; constructor() { // Generate unique instance ID for concurrent support this.instanceId = `mcp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; logger.info('[MCP_SERVER] Starting TodoMcpServer initialization...', { instanceId: this.instanceId, version: VERSION, timestamp: new Date().toISOString(), nodeVersion: process.version, platform: process.platform, configLogLevel: CONFIG.logLevel }); logger.debug('[MCP_SERVER] Creating MCP Server instance...', { serverName: 'todo-for-ai-mcp', serverVersion: VERSION, capabilities: ['tools'], instanceId: this.instanceId }); this.server = new Server({ name: 'todo-for-ai-mcp', version: VERSION, }, { capabilities: { tools: {}, }, }); logger.info(`[MCP_SERVER] MCP Server instance created: ${this.instanceId}`, { serverName: 'todo-for-ai-mcp', serverVersion: VERSION, instanceId: this.instanceId }); logger.debug('[MCP_SERVER] Initializing API client...', { apiBaseUrl: CONFIG.apiBaseUrl, hasToken: !!CONFIG.apiToken, timeout: CONFIG.apiTimeout, instanceId: this.instanceId }); this.apiClient = new TodoApiClient(CONFIG); logger.debug('[MCP_SERVER] Setting up request handlers...', { instanceId: this.instanceId }); this.setupHandlers(); logger.info('[MCP_SERVER] TodoMcpServer initialization complete', { instanceId: this.instanceId, handlersSetup: true, apiClientReady: true, serverReady: true }); } setupHandlers() { logger.debug('[MCP_SERVER] Setting up request handlers...', { instanceId: this.instanceId, handlersToSetup: ['ListTools', 'CallTool'] }); // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { logger.debug('[MCP_SERVER] ListTools request received', { instanceId: this.instanceId, timestamp: new Date().toISOString() }); logger.debug('Received list_tools request'); const tools = [ { name: 'get_project_tasks_by_name', description: 'Get all pending tasks for a project by project name, sorted by creation time', inputSchema: { type: 'object', properties: { project_name: { type: 'string', description: 'The name of the project to get tasks for', }, status_filter: { type: 'array', items: { type: 'string', enum: ['todo', 'in_progress', 'review'], }, description: 'Filter tasks by status (default: todo, in_progress, review)', default: ['todo', 'in_progress', 'review'], }, }, required: ['project_name'], }, }, { name: 'get_task_by_id', description: 'Get detailed task information by task ID', inputSchema: { type: 'object', properties: { task_id: { type: 'integer', description: 'The ID of the task to retrieve', }, }, required: ['task_id'], }, }, { name: 'submit_task_feedback', description: 'Submit feedback for a completed or in-progress task', inputSchema: { type: 'object', properties: { task_id: { type: 'integer', description: 'The ID of the task to provide feedback for', }, project_name: { type: 'string', description: 'The name of the project this task belongs to', }, feedback_content: { type: 'string', description: 'The feedback content describing what was done', }, status: { type: 'string', enum: ['in_progress', 'review', 'done', 'cancelled'], description: 'The new status of the task after feedback', }, ai_identifier: { type: 'string', description: 'Identifier of the AI providing feedback (optional)', }, }, required: ['task_id', 'project_name', 'feedback_content', 'status'], }, }, { name: 'create_task', description: 'Create a new task in the specified project', inputSchema: { type: 'object', properties: { project_id: { type: 'integer', description: 'The ID of the project to create the task in', }, title: { type: 'string', description: 'The title of the task', }, content: { type: 'string', description: 'The detailed content/description of the task', }, status: { type: 'string', enum: ['todo', 'in_progress', 'review', 'done', 'cancelled'], description: 'The initial status of the task (default: todo)', default: 'todo', }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'urgent'], description: 'The priority of the task (default: medium)', default: 'medium', }, assignee: { type: 'string', description: 'The person assigned to this task (optional)', }, due_date: { type: 'string', description: 'The due date in YYYY-MM-DD format (optional)', }, estimated_hours: { type: 'number', description: 'Estimated hours to complete the task (optional)', }, tags: { type: 'array', items: { type: 'string' }, description: 'Tags associated with the task (optional)', }, related_files: { type: 'array', items: { type: 'string' }, description: 'Files related to this task (optional)', }, is_ai_task: { type: 'boolean', description: 'Whether this task was created by AI (default: true)', default: true, }, ai_identifier: { type: 'string', description: 'Identifier of the AI creating the task (optional)', }, }, required: ['project_id', 'title'], }, }, { name: 'get_project_info', description: 'Get detailed project information including statistics and configuration. Provide either project_id or project_name.', inputSchema: { type: 'object', properties: { project_id: { type: 'integer', description: 'The ID of the project to retrieve (optional if project_name is provided)', }, project_name: { type: 'string', description: 'The name of the project to retrieve (optional if project_id is provided)', }, }, required: [], }, }, { name: 'list_user_projects', description: 'List all projects that the current user has access to, with proper permission checking', inputSchema: { type: 'object', properties: { status_filter: { type: 'string', enum: ['active', 'archived', 'all'], description: 'Filter projects by status (default: active)', default: 'active', }, include_stats: { type: 'boolean', description: 'Whether to include project statistics (default: false)', default: false, }, }, required: [], }, }, ]; logger.info(`Returning ${tools.length} available tools`); return { tools }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; const requestId = Date.now() + '-' + Math.random().toString(36).substr(2, 9); const callStartTime = Date.now(); logger.info(`[MCP_SERVER] ========== TOOL CALL START ==========`, { requestId, toolName: name, instanceId: this.instanceId, timestamp: new Date().toISOString(), callStartTime }); logger.info(`[MCP_SERVER] Tool call received: ${name}`, { requestId, toolName: name, hasArgs: !!args, argsCount: args ? Object.keys(args).length : 0, argsKeys: args ? Object.keys(args) : [], argsSize: args ? JSON.stringify(args).length : 0, instanceId: this.instanceId, memoryUsage: process.memoryUsage() }); logger.debug(`[MCP_SERVER] Tool call full arguments: ${name}`, { requestId, args, argsStringified: JSON.stringify(args, null, 2) }); try { let result; const startTime = Date.now(); switch (name) { case 'get_project_tasks_by_name': logger.info(`[MCP_SERVER] Executing get_project_tasks_by_name`, { requestId, instanceId: this.instanceId, projectName: args?.project_name, hasProjectName: !!args?.project_name }); result = await this.handleGetProjectTasksByName(args); break; case 'get_task_by_id': logger.info(`[MCP_SERVER] Executing get_task_by_id`, { requestId, instanceId: this.instanceId, taskId: args?.task_id, hasTaskId: !!args?.task_id }); result = await this.handleGetTaskById(args); break; case 'submit_task_feedback': logger.info(`[MCP_SERVER] Executing submit_task_feedback`, { requestId, instanceId: this.instanceId, taskId: args?.task_id, status: args?.status, hasContent: !!args?.feedback_content }); result = await this.handleSubmitTaskFeedback(args); break; case 'create_task': logger.info(`[MCP_SERVER] Executing create_task`, { requestId, instanceId: this.instanceId, projectId: args?.project_id, title: args?.title, priority: args?.priority }); result = await this.handleCreateTask(args); break; case 'get_project_info': logger.info(`[MCP_SERVER] Executing get_project_info`, { requestId, instanceId: this.instanceId, projectId: args?.project_id, projectName: args?.project_name, hasProjectId: !!args?.project_id, hasProjectName: !!args?.project_name }); result = await this.handleGetProjectInfo(args); break; case 'list_user_projects': logger.info(`[MCP_SERVER] Executing list_user_projects`, { requestId, instanceId: this.instanceId, statusFilter: args?.status_filter, includeStats: args?.include_stats }); result = await this.handleListUserProjects(args); break; default: const error = new Error(`Unknown tool: ${name}`); logger.error(`[MCP_SERVER] Unknown tool requested`, { requestId, instanceId: this.instanceId, toolName: name, error: error.message, availableTools: ['get_project_tasks_by_name', 'get_task_by_id', 'submit_task_feedback', 'create_task', 'get_project_info', 'list_user_projects'] }); throw error; } const duration = Date.now() - startTime; const totalCallDuration = Date.now() - callStartTime; logger.info(`[MCP_SERVER] Tool call completed successfully: ${name}`, { requestId, instanceId: this.instanceId, duration: `${duration}ms`, totalCallDuration: `${totalCallDuration}ms`, hasResult: !!result, resultType: typeof result, resultSize: result ? JSON.stringify(result).length : 0, memoryUsage: process.memoryUsage() }); logger.debug(`[MCP_SERVER] Tool call result structure: ${name}`, { requestId, result: result, resultKeys: result && typeof result === 'object' ? Object.keys(result) : [] }); logger.info(`[MCP_SERVER] ========== TOOL CALL END ==========`, { requestId, toolName: name, instanceId: this.instanceId, success: true, totalDuration: `${totalCallDuration}ms`, timestamp: new Date().toISOString() }); return result; } catch (error) { const totalCallDuration = Date.now() - callStartTime; logger.error(`[MCP_SERVER] Tool call failed: ${name}`, { requestId, instanceId: this.instanceId, toolName: name, totalCallDuration: `${totalCallDuration}ms`, error: error instanceof Error ? error.message : String(error), errorType: error instanceof Error ? error.constructor.name : typeof error, stack: error instanceof Error ? error.stack : undefined, args, memoryUsage: process.memoryUsage() }); logger.error(`[MCP_SERVER] ========== TOOL CALL END (ERROR) ==========`, { requestId, toolName: name, instanceId: this.instanceId, success: false, totalDuration: `${totalCallDuration}ms`, errorMessage: error instanceof Error ? error.message : String(error), timestamp: new Date().toISOString() }); throw error; } }); } async handleGetProjectTasksByName(args) { const result = await this.apiClient.getProjectTasksByName(args); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } async handleGetTaskById(args) { const result = await this.apiClient.getTaskById(args); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } async handleSubmitTaskFeedback(args) { const result = await this.apiClient.submitTaskFeedback(args); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } async handleCreateTask(args) { const result = await this.apiClient.createTask(args); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } async handleGetProjectInfo(args) { const handlerStartTime = Date.now(); const handlerId = `handler-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`; logger.info('[MCP_SERVER] ========== HANDLER START: handleGetProjectInfo ==========', { handlerId, instanceId: this.instanceId, args, hasProjectId: !!args?.project_id, hasProjectName: !!args?.project_name, timestamp: new Date().toISOString() }); logger.debug('[MCP_SERVER] handleGetProjectInfo input validation', { handlerId, projectId: args?.project_id, projectName: args?.project_name, argsType: typeof args, argsKeys: args ? Object.keys(args) : [], isValidInput: !!(args?.project_id || args?.project_name) }); try { logger.info('[MCP_SERVER] handleGetProjectInfo calling API client...', { handlerId, instanceId: this.instanceId, apiMethod: 'getProjectInfo', args }); const apiCallStartTime = Date.now(); const result = await this.apiClient.getProjectInfo(args); const apiCallDuration = Date.now() - apiCallStartTime; logger.info('[MCP_SERVER] handleGetProjectInfo API call successful', { handlerId, instanceId: this.instanceId, apiCallDuration: `${apiCallDuration}ms`, projectId: result.id, projectName: result.name, projectStatus: result.status, hasStats: !!result.statistics, hasRecentTasks: !!result.recent_tasks, resultSize: JSON.stringify(result).length, resultKeys: Object.keys(result) }); logger.debug('[MCP_SERVER] handleGetProjectInfo API result details', { handlerId, result: result, statistics: result.statistics, recentTasks: result.recent_tasks }); logger.debug('[MCP_SERVER] handleGetProjectInfo preparing response...', { handlerId, responseFormat: 'MCP tool response', contentType: 'text', willStringify: true }); const response = { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; const handlerDuration = Date.now() - handlerStartTime; logger.info('[MCP_SERVER] handleGetProjectInfo response prepared', { handlerId, instanceId: this.instanceId, handlerDuration: `${handlerDuration}ms`, responseSize: JSON.stringify(response).length, contentType: response.content[0]?.type, contentCount: response.content.length, textLength: response.content[0]?.text?.length }); logger.info('[MCP_SERVER] ========== HANDLER END: handleGetProjectInfo ==========', { handlerId, instanceId: this.instanceId, success: true, totalDuration: `${handlerDuration}ms`, timestamp: new Date().toISOString() }); return response; } catch (error) { const handlerDuration = Date.now() - handlerStartTime; logger.error('[MCP_SERVER] handleGetProjectInfo failed', { handlerId, instanceId: this.instanceId, handlerDuration: `${handlerDuration}ms`, args, error: error instanceof Error ? error.message : String(error), errorType: error instanceof Error ? error.constructor.name : typeof error, stack: error instanceof Error ? error.stack : undefined }); logger.error('[MCP_SERVER] ========== HANDLER END: handleGetProjectInfo (ERROR) ==========', { handlerId, instanceId: this.instanceId, success: false, totalDuration: `${handlerDuration}ms`, errorMessage: error instanceof Error ? error.message : String(error), timestamp: new Date().toISOString() }); throw error; } } async handleListUserProjects(args) { const handlerStartTime = Date.now(); const handlerId = `handler-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`; logger.info('[MCP_SERVER] ========== HANDLER START: handleListUserProjects ==========', { handlerId, instanceId: this.instanceId, args, hasStatusFilter: !!args?.status_filter, hasIncludeStats: !!args?.include_stats, timestamp: new Date().toISOString() }); logger.debug('[MCP_SERVER] handleListUserProjects input validation', { handlerId, statusFilter: args?.status_filter, includeStats: args?.include_stats, argsType: typeof args, argsKeys: args ? Object.keys(args) : [] }); try { logger.info('[MCP_SERVER] handleListUserProjects calling API client...', { handlerId, instanceId: this.instanceId, apiMethod: 'listUserProjects', args }); const apiCallStartTime = Date.now(); const result = await this.apiClient.listUserProjects(args); const apiCallDuration = Date.now() - apiCallStartTime; logger.info('[MCP_SERVER] handleListUserProjects API call successful', { handlerId, instanceId: this.instanceId, apiCallDuration: `${apiCallDuration}ms`, projectsCount: result.total || 0, statusFilter: result.status_filter, includeStats: result.include_stats, resultSize: JSON.stringify(result).length, resultKeys: Object.keys(result) }); logger.debug('[MCP_SERVER] handleListUserProjects API result details', { handlerId, result: result, projects: result.projects }); logger.debug('[MCP_SERVER] handleListUserProjects preparing response...', { handlerId, responseFormat: 'MCP tool response', contentType: 'text', willStringify: true }); const response = { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; const handlerDuration = Date.now() - handlerStartTime; logger.info('[MCP_SERVER] handleListUserProjects response prepared', { handlerId, instanceId: this.instanceId, handlerDuration: `${handlerDuration}ms`, responseSize: JSON.stringify(response).length, contentType: response.content[0]?.type, contentCount: response.content.length, textLength: response.content[0]?.text?.length }); logger.info('[MCP_SERVER] ========== HANDLER END: handleListUserProjects ==========', { handlerId, instanceId: this.instanceId, success: true, totalDuration: `${handlerDuration}ms`, timestamp: new Date().toISOString() }); return response; } catch (error) { const handlerDuration = Date.now() - handlerStartTime; logger.error('[MCP_SERVER] handleListUserProjects failed', { handlerId, instanceId: this.instanceId, handlerDuration: `${handlerDuration}ms`, args, error: error instanceof Error ? error.message : String(error), errorType: error instanceof Error ? error.constructor.name : typeof error, stack: error instanceof Error ? error.stack : undefined }); logger.error('[MCP_SERVER] ========== HANDLER END: handleListUserProjects (ERROR) ==========', { handlerId, instanceId: this.instanceId, success: false, totalDuration: `${handlerDuration}ms`, errorMessage: error instanceof Error ? error.message : String(error), timestamp: new Date().toISOString() }); throw error; } } async run() { logger.info('[MCP_SERVER] Starting Todo for AI MCP Server...', { instanceId: this.instanceId, apiBaseUrl: CONFIG.apiBaseUrl, hasApiToken: !!CONFIG.apiToken, logLevel: CONFIG.logLevel, httpPort: CONFIG.httpPort, httpHost: CONFIG.httpHost, version: VERSION }); logger.debug('[MCP_SERVER] Creating transport...', { instanceId: this.instanceId, configuredTransport: CONFIG.transport, httpPort: CONFIG.httpPort, httpHost: CONFIG.httpHost }); try { // Use transport factory to create appropriate transport this.transport = TransportFactory.create(CONFIG); logger.debug('[MCP_SERVER] Starting transport...', { instanceId: this.instanceId, transportType: this.transport.getType(), httpPort: CONFIG.httpPort, httpHost: CONFIG.httpHost, timestamp: new Date().toISOString() }); await this.transport.start(this.server); const transportType = this.transport.getType(); const logData = { instanceId: this.instanceId, apiBaseUrl: CONFIG.apiBaseUrl, transport: transportType, connected: true, ready: true, timestamp: new Date().toISOString() }; // Add HTTP-specific info if using HTTP transport if (transportType === 'http') { logData.httpPort = CONFIG.httpPort; logData.httpHost = CONFIG.httpHost; logData.httpUrl = `http://${CONFIG.httpHost}:${CONFIG.httpPort}`; } logger.info('[MCP_SERVER] Todo for AI MCP Server is running', logData); } catch (error) { logger.error('[MCP_SERVER] Failed to start MCP Server', { instanceId: this.instanceId, httpPort: CONFIG.httpPort, httpHost: CONFIG.httpHost, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }); throw error; } } async stop() { logger.info('[MCP_SERVER] Stopping Todo for AI MCP Server...', { instanceId: this.instanceId, transport: this.transport?.getType() || 'unknown' }); try { if (this.transport) { await this.transport.stop(); this.transport = undefined; } logger.info('[MCP_SERVER] Todo for AI MCP Server stopped successfully', { instanceId: this.instanceId }); } catch (error) { logger.error('[MCP_SERVER] Error stopping MCP Server', { instanceId: this.instanceId, error: error instanceof Error ? error.message : String(error) }); throw error; } } } //# sourceMappingURL=server.js.map