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
JavaScript
;
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;
}
}