UNPKG

ai-expert-workflow-mcp

Version:

Enhanced AI Expert Workflow MCP Server with structured conversation flow, topic tracking, and optional Task Master integration

383 lines (375 loc) 20.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.waitForMcpSdk = waitForMcpSdk; exports.createMCPServer = createMCPServer; // Import the McpServer class using require instead of import const experts_1 = require("./experts"); const aiUtils_1 = require("./utils/aiUtils"); const fileUtils_1 = require("./utils/fileUtils"); const state_1 = require("./state"); const expertHandler_1 = require("./handlers/expertHandler"); // Enable debug logging if DEBUG env variable is set global.isDebugMode = process.env.DEBUG?.includes('mcp') || false; function log(...args) { if (global.isDebugMode) { console.log('[AI-EXPERT-MCP]', ...args); } } function logError(...args) { console.error('[AI-EXPERT-MCP-ERROR]', ...args); } // Load the MCP SDK synchronously let mcpServerLoaded = false; try { const mcpModule = require('@modelcontextprotocol/sdk/server/mcp.js'); global.McpServer = mcpModule.McpServer; mcpServerLoaded = true; log('MCP SDK loaded successfully'); } catch (error) { logError('Failed to load MCP SDK synchronously:', error); // Fallback to async loading Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/server/mcp.js'))).then(module => { global.McpServer = module.McpServer; mcpServerLoaded = true; log('MCP SDK loaded asynchronously'); }).catch(error => { logError('Error importing MCP SDK asynchronously:', error); process.exit(1); }); } // Retry mechanism for server creation async function waitForMcpSdk(maxRetries = 5, delayMs = 1000) { let retries = 0; while (!mcpServerLoaded && retries < maxRetries) { log(`Waiting for MCP SDK to load... (attempt ${retries + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delayMs)); retries++; } return mcpServerLoaded; } function createMCPServer() { // Check if MCP SDK is loaded if (!global.McpServer) { logError('MCP SDK not loaded yet. Please try again in a moment.'); return null; } try { log('Creating MCP server instance'); const server = new global.McpServer({ name: 'ai-expert-workflow', version: '1.0.0' }); // Register the consultExpert tool try { log('Registering consultExpert tool'); // Create a state map to track workflow state for each conversation const consultStates = new Map(); server.tool('consultExpert', { type: 'object', properties: { role: { type: 'string', description: 'The expert role to consult with (productManager, uxDesigner, softwareArchitect)' }, projectInfo: { type: 'string', description: 'Brief description of the project or message to the expert' }, conversationId: { type: 'string', description: 'Unique identifier for the conversation' } }, required: ['role', 'projectInfo'] }, async (params) => { log('consultExpert tool called with params:', params); const { role, projectInfo, conversationId = 'default' } = params; if (!experts_1.experts[role]) { return { error: `Unknown expert role: ${role}. Available roles: ${Object.keys(experts_1.experts).join(', ')}` }; } const expert = experts_1.experts[role]; // Map role to stage const stageMap = { 'productManager': experts_1.EXPERT_WORKFLOW_STAGES.PRODUCT_DEFINITION, 'uxDesigner': experts_1.EXPERT_WORKFLOW_STAGES.UX_DESIGN, 'softwareArchitect': experts_1.EXPERT_WORKFLOW_STAGES.TECHNICAL_PLANNING }; // Get or initialize the workflow state for this conversation if (!consultStates.has(conversationId)) { // Create a new state with the selected expert's stage as the current stage const newState = JSON.parse(JSON.stringify(state_1.initialState)); newState.currentStage = stageMap[role]; consultStates.set(conversationId, newState); log('Initialized new consultation state for conversation:', conversationId); } const state = consultStates.get(conversationId); // If this is the first message, initialize with welcome message if (state.stageData[state.currentStage]?.completedTopics.length === 0) { const welcomeMessage = `# Consulting with ${expert.title} Thank you for providing your project description. I'll help you define the ${state.currentStage.replace('_', ' ')} for your project. ${(0, experts_1.getStageIntroduction)(state.currentStage)} Let's start by discussing your project: ${projectInfo}`; // Initialize the state with the first message const result = await (0, expertHandler_1.handleExpertInteraction)(projectInfo, state); consultStates.set(conversationId, result.updatedState); return { content: welcomeMessage }; } try { // Handle the interaction using our expert handler const result = await (0, expertHandler_1.handleExpertInteraction)(projectInfo, state); // Update the state in our map consultStates.set(conversationId, result.updatedState); // If a document was generated, save it if (result.document && result.isComplete) { // Save to regular file const filename = `${expert.outputFormat.replace(/\\s+/g, '_').toLowerCase()}.md`; await (0, fileUtils_1.saveDocument)(result.document, filename); // Also save to scripts directory const scriptPath = await (0, fileUtils_1.saveExpertDocument)(result.document, role); // Add info about saved document result.response += `\n\nDocument saved to ${filename} and ${scriptPath}.`; // If this is a PRD, also set up Task Master integration if (role === 'productManager') { await (0, fileUtils_1.setupTaskMasterIntegration)(); // Add Task Master info to the response result.response += `\n\nYou can now use Task Master to parse this PRD with: "Can you parse the PRD at scripts/prd.txt and generate tasks?"`; } } return { content: result.response }; } catch (error) { logError('Error in consultExpert:', error); return { error: `Error consulting with ${expert.title}: ${error instanceof Error ? error.message : String(error)}` }; } }); } catch (error) { logError('Failed to register consultExpert tool:', error); } // Register the generateDocument tool try { log('Registering generateDocument tool'); // Create a state map to track workflow state for each conversation const documentStates = new Map(); server.tool('generateDocument', { type: 'object', properties: { role: { type: 'string', description: 'The expert role to use for document generation' }, projectDetails: { type: 'string', description: 'Detailed project information or conversation summary' }, saveForTaskMaster: { type: 'boolean', description: 'Whether to save the document in a format compatible with Task Master' }, conversationId: { type: 'string', description: 'Unique identifier for the conversation' } }, required: ['role', 'projectDetails'] }, async (params) => { log('generateDocument tool called with params:', params); const { role, projectDetails, saveForTaskMaster: saveForTM = false, conversationId = 'default' } = params; if (!experts_1.experts[role]) { return { error: `Unknown expert role: ${role}. Available roles: ${Object.keys(experts_1.experts).join(', ')}` }; } const expert = experts_1.experts[role]; // Map role to stage const stageMap = { 'productManager': experts_1.EXPERT_WORKFLOW_STAGES.PRODUCT_DEFINITION, 'uxDesigner': experts_1.EXPERT_WORKFLOW_STAGES.UX_DESIGN, 'softwareArchitect': experts_1.EXPERT_WORKFLOW_STAGES.TECHNICAL_PLANNING }; // Get or initialize the workflow state for this conversation if (!documentStates.has(conversationId)) { // Create a new state with the selected expert's stage as the current stage const newState = JSON.parse(JSON.stringify(state_1.initialState)); newState.currentStage = stageMap[role]; documentStates.set(conversationId, newState); log('Initialized new document state for conversation:', conversationId); } const state = documentStates.get(conversationId); try { // First, mark the current stage as complete state.stageData[state.currentStage] = { ...state.stageData[state.currentStage], completed: true }; // Generate the document using the template const template = await (0, fileUtils_1.readTemplate)(expert.templatePath); const document = await (0, aiUtils_1.generateExpertDocument)(role, template, projectDetails); // Save the document to the state state.stageData[state.currentStage].document = document; // Save the document to a file const filename = `${expert.outputFormat.replace(/\\s+/g, '_').toLowerCase()}.md`; await (0, fileUtils_1.saveDocument)(document, filename); // Also save to scripts directory const scriptPath = await (0, fileUtils_1.saveExpertDocument)(document, role); // Prepare message about saved documents let saveMessage = `\n\nDocument saved to ${filename} and ${scriptPath}.`; // If this is a PRD and saveForTaskMaster is true, also set up Task Master integration let taskMasterMessage = ''; if (role === 'productManager' && saveForTM) { await (0, fileUtils_1.setupTaskMasterIntegration)(); taskMasterMessage = `\n\nYou can now use Task Master to parse this PRD with: "Can you parse the PRD at scripts/prd.txt and generate tasks?"`; } // Update the state in our map documentStates.set(conversationId, state); return { content: `# ${expert.outputFormat}\n\n${document}${saveMessage}${taskMasterMessage}` }; } catch (error) { logError('Error in generateDocument:', error); return { error: `Error generating ${expert.outputFormat}: ${error instanceof Error ? error.message : String(error)}` }; } }); } catch (error) { logError('Failed to register generateDocument tool:', error); } // Register the expertWorkflow tool try { log('Registering expertWorkflow tool'); // Create a state map to track workflow state for each conversation const workflowStates = new Map(); server.tool('expertWorkflow', { type: 'object', properties: { projectDescription: { type: 'string', description: 'Description of the project or message to the expert' }, conversationId: { type: 'string', description: 'Unique identifier for the conversation' } }, required: ['projectDescription'] }, async (params) => { log('expertWorkflow tool called with params:', params); try { const { projectDescription, conversationId = 'default' } = params; log('Processing request with input:', projectDescription); // Get or initialize the workflow state for this conversation if (!workflowStates.has(conversationId)) { workflowStates.set(conversationId, JSON.parse(JSON.stringify(state_1.initialState))); log('Initialized new workflow state for conversation:', conversationId); } const state = workflowStates.get(conversationId); // If this is the first message and it looks like a project description, initialize with welcome message if (state.completedStages.length === 0 && state.stageData[state.currentStage].completedTopics.length === 0) { const isInitialDescription = projectDescription.length > 30 && !projectDescription.toLowerCase().includes('?') && !projectDescription.toLowerCase().includes('hello') && !projectDescription.toLowerCase().includes('hi '); if (isInitialDescription) { const welcomeMessage = `# Welcome to the AI Expert Workflow Thank you for providing your project description. I'll guide you through a comprehensive product development process with three expert consultations: 1. **Product Definition** with an AI Product Manager 2. **UX Design** with an AI UX Designer 3. **Technical Architecture** with an AI Software Architect ${(0, experts_1.getStageIntroduction)(experts_1.EXPERT_WORKFLOW_STAGES.PRODUCT_DEFINITION)} Let's start by discussing your project: ${projectDescription}`; return { content: welcomeMessage }; } } // Handle the interaction using our expert handler const result = await (0, expertHandler_1.handleExpertInteraction)(projectDescription, state); // Update the state in our map workflowStates.set(conversationId, result.updatedState); // If a document was generated, save it if (result.document && result.isComplete) { const currentStage = result.updatedState.currentStage; const expertName = currentStage === experts_1.EXPERT_WORKFLOW_STAGES.PRODUCT_DEFINITION ? 'productManager' : currentStage === experts_1.EXPERT_WORKFLOW_STAGES.UX_DESIGN ? 'uxDesigner' : 'softwareArchitect'; const expert = experts_1.experts[expertName]; // Save to regular file const filename = `${expert.outputFormat.replace(/\\s+/g, '_').toLowerCase()}.md`; await (0, fileUtils_1.saveDocument)(result.document, filename); // Also save to scripts directory const scriptPath = await (0, fileUtils_1.saveExpertDocument)(result.document, expertName); // Add info about saved document result.response += `\n\nDocument saved to ${filename} and ${scriptPath}.`; // If this is a PRD, save it as initial_prd.txt if (currentStage === experts_1.EXPERT_WORKFLOW_STAGES.PRODUCT_DEFINITION) { // Save as initial_prd.txt since this is just the first phase await (0, aiUtils_1.saveForTaskMaster)(result.document, true); // Don't set up Task Master integration yet, as we'll wait for the comprehensive document result.response += `\n\nInitial PRD saved to scripts/initial_prd.txt. After completing all phases, a comprehensive document will be saved as scripts/prd.txt for Task Master.`; } // If all stages are complete, inform the user that the PRD has been updated with all expert input if (result.updatedState.completedStages.length === Object.keys(experts_1.EXPERT_WORKFLOW_STAGES).length - 1 && result.updatedState.stageData[result.updatedState.currentStage].completed) { // Add info about the complete PRD result.response += `\n\nThe PRD file (scripts/prd.txt) has been continuously updated throughout all phases and now contains a comprehensive specification with input from all three experts. You can use this file with Task Master.`; } } return { content: result.response }; } catch (error) { logError('Error in expertWorkflow:', error); return { error: `Error executing expertWorkflow: ${error instanceof Error ? error.message : String(error)}` }; } }); } catch (error) { logError('Failed to register expertWorkflow tool:', error); } log('All tools registered successfully'); return server; } catch (error) { logError('Error creating MCP server:', error); return null; } }