bmad-method-mcp
Version:
Breakthrough Method of Agile AI-driven Development with Enhanced MCP Integration
699 lines (625 loc) • 22.6 kB
JavaScript
/**
* BMad Unified Server v2 - Well-Structured Service Architecture
* Single service exposing both MCP transport for AI agents and REST API for web UI
* Implements proper service layer with dependency injection and best practices
*/
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const express = require('express');
const path = require('path');
const {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema
} = require('@modelcontextprotocol/sdk/types.js');
const BMadStorage = require('./storage/sqlite-adapter.js');
const BMadResources = require('./resources/bmad-resources.js');
const BMadPrompts = require('./prompts/bmad-prompts.js');
const { createBMadServiceContainer } = require('./services/ServiceContainer.js');
const AutoWebUILauncher = require('./AutoWebUILauncher.js');
const configLoader = require('./config/ConfigLoader.js');
/**
* Logger with different levels
*/
class Logger {
constructor(level = 'info') {
this.level = level;
this.levels = { debug: 0, info: 1, warn: 2, error: 3 };
}
debug(...args) {
if (this.levels[this.level] <= 0) {
console.log('\x1b[36m[DEBUG]\x1b[0m', ...args);
}
}
info(...args) {
if (this.levels[this.level] <= 1) {
console.log('\x1b[32m[INFO]\x1b[0m', ...args);
}
}
warn(...args) {
if (this.levels[this.level] <= 2) {
console.warn('\x1b[33m[WARN]\x1b[0m', ...args);
}
}
error(...args) {
if (this.levels[this.level] <= 3) {
console.error('\x1b[31m[ERROR]\x1b[0m', ...args);
}
}
}
class BMadUnifiedServer {
constructor() {
// Load configuration
this.config = configLoader.load();
// Initialize logger
this.logger = new Logger(this.config.server.log_level);
// MCP Server for AI agents
this.mcpServer = new Server(
{
name: this.config.mcp.name,
version: this.config.mcp.version,
description: this.config.mcp.description,
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
}
);
// Express server for REST API and Web UI
this.expressApp = express();
this.port = this.config.server.rest_port;
// Service container and components
this.serviceContainer = null;
this.storage = null;
this.resources = null;
this.prompts = null;
// Services
this.taskService = null;
this.sprintService = null;
this.projectService = null;
// Auto Web UI Launcher (like Serena's dashboard)
this.webUILauncher = new AutoWebUILauncher(this.logger);
this.webUILauncher.webUIPort = this.config.webui.port;
this.webUILauncher.setEnabled(this.config.webui.auto_start);
this.webUILauncher.setAutoOpen(this.config.webui.auto_open);
}
async initialize() {
this.logger.info('🚀 Initializing BMad Unified Server v2...');
// Initialize storage (single source of truth)
this.storage = new BMadStorage();
await this.storage.initialize();
this.logger.info('✅ Database initialized');
// Initialize service container with dependency injection
this.serviceContainer = createBMadServiceContainer(this.storage, this.logger);
const services = await this.serviceContainer.initializeServices();
// Extract services from container
this.taskService = services.taskService;
this.sprintService = services.sprintService;
this.projectService = services.projectService;
this.logger.info('✅ Service layer initialized');
// Initialize supporting components
this.resources = new BMadResources(this.storage);
this.prompts = new BMadPrompts(this.storage);
this.logger.info('✅ MCP components initialized');
// Setup servers
await this.setupMCPServer();
this.logger.info('✅ MCP server configured');
await this.setupRESTAPI();
this.logger.info('✅ REST API configured');
// Setup graceful shutdown
this.setupGracefulShutdown();
// Auto-start Web UI when running in MCP mode (like Serena)
const serverMode = process.env.BMAD_SERVER_MODE || 'dual';
if (configLoader.shouldAutoStartWebUI(serverMode)) {
this.logger.info('🌐 Auto-starting Web UI dashboard...');
await this.webUILauncher.autoStartWebUI();
}
}
async setupMCPServer() {
// Register MCP tools with proper schemas
this.mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'bmad_create_task',
description: 'Create a new task/story in the project',
inputSchema: {
type: 'object',
properties: {
epic_num: { type: 'number', description: 'Epic number to assign task to' },
title: { type: 'string', description: 'Task title' },
description: { type: 'string', description: 'Detailed task description' },
assignee: { type: 'string', default: 'dev', description: 'Task assignee' },
priority: { type: 'string', enum: ['HIGH', 'MEDIUM', 'LOW'], default: 'MEDIUM' }
},
required: ['title']
}
},
{
name: 'bmad_query_tasks',
description: 'Query tasks with filters',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', enum: ['TODO', 'IN_PROGRESS', 'BLOCKED', 'REVIEW', 'DONE'] },
assignee: { type: 'string' },
epic_num: { type: 'number' },
story_num: { type: 'number' },
priority: { type: 'string', enum: ['HIGH', 'MEDIUM', 'LOW'] },
sprint_id: { type: 'number' }
}
}
},
{
name: 'bmad_update_task_status',
description: 'Update task status and assignment',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number', description: 'Task ID' },
status: { type: 'string', enum: ['TODO', 'IN_PROGRESS', 'BLOCKED', 'REVIEW', 'DONE'] },
assignee: { type: 'string' }
},
required: ['id']
}
},
{
name: 'bmad_delete_task',
description: 'Delete a task',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number', description: 'Task ID to delete' }
},
required: ['id']
}
},
{
name: 'bmad_get_project_progress',
description: 'Get comprehensive project progress statistics',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'bmad_create_sprint',
description: 'Create a new sprint',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Sprint name' },
goal: { type: 'string', description: 'Sprint goal' },
start_date: { type: 'string', format: 'date-time' },
end_date: { type: 'string', format: 'date-time' }
},
required: ['name', 'goal']
}
},
{
name: 'bmad_get_current_sprint',
description: 'Get the current active sprint',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'bmad_complete_sprint',
description: 'Complete the current sprint',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number', description: 'Sprint ID to complete' }
},
required: ['id']
}
},
{
name: 'bmad_get_project_analytics',
description: 'Get project analytics and insights',
inputSchema: {
type: 'object',
properties: {
days: { type: 'number', default: 30, description: 'Days to analyze' }
}
}
}
]
};
});
// Register MCP tool execution with service layer
this.mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
let result;
switch (name) {
case 'bmad_create_task':
result = await this.taskService.createTask(args);
break;
case 'bmad_query_tasks':
result = await this.taskService.queryTasks(args);
break;
case 'bmad_update_task_status':
result = await this.taskService.updateTaskStatus(args);
break;
case 'bmad_delete_task':
result = await this.taskService.deleteTask(args.id);
break;
case 'bmad_get_project_progress':
result = await this.projectService.getProjectProgress();
break;
case 'bmad_create_sprint':
result = await this.sprintService.createSprint(args);
break;
case 'bmad_get_current_sprint':
result = await this.sprintService.getCurrentSprint();
break;
case 'bmad_complete_sprint':
result = await this.sprintService.completeSprint(args.id);
break;
case 'bmad_get_project_analytics':
result = await this.projectService.getProjectAnalytics(args);
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
this.logger.error(`MCP tool error (${name}):`, error.message);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
message: error.message,
code: 'MCP_TOOL_ERROR',
timestamp: new Date().toISOString()
}
}, null, 2)
}
],
isError: true
};
}
});
// Register MCP resources
this.mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'bmad://project/info',
name: 'Project Information',
description: 'Basic project metadata and configuration'
},
{
uri: 'bmad://project/progress',
name: 'Project Progress',
description: 'Real-time project completion statistics'
},
{
uri: 'bmad://project/analytics',
name: 'Project Analytics',
description: 'Advanced project analytics and insights'
},
{
uri: 'bmad://tasks/all',
name: 'All Tasks',
description: 'Complete list of all project tasks'
},
{
uri: 'bmad://tasks/todo',
name: 'TODO Tasks',
description: 'Tasks with TODO status'
},
{
uri: 'bmad://tasks/in-progress',
name: 'In Progress Tasks',
description: 'Currently active tasks'
},
{
uri: 'bmad://tasks/current-sprint',
name: 'Current Sprint Tasks',
description: 'Tasks in the active sprint'
},
{
uri: 'bmad://sprints/current',
name: 'Current Sprint',
description: 'Active sprint details'
},
{
uri: 'bmad://sprints/all',
name: 'All Sprints',
description: 'Complete list of all sprints'
}
]
};
});
// Register MCP resource reading with service layer
this.mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
try {
let result;
switch (uri) {
case 'bmad://project/info':
result = await this.projectService.getProjectInfo();
break;
case 'bmad://project/progress':
result = await this.projectService.getProjectProgress();
break;
case 'bmad://project/analytics':
result = await this.projectService.getProjectAnalytics();
break;
case 'bmad://tasks/all':
result = await this.taskService.queryTasks();
break;
case 'bmad://tasks/todo':
result = await this.taskService.queryTasks({ status: 'TODO' });
break;
case 'bmad://tasks/in-progress':
result = await this.taskService.queryTasks({ status: 'IN_PROGRESS' });
break;
case 'bmad://tasks/current-sprint':
result = await this.taskService.getCurrentSprintTasks();
break;
case 'bmad://sprints/current':
result = await this.sprintService.getCurrentSprint();
break;
case 'bmad://sprints/all':
result = await this.sprintService.getAllSprints();
break;
default:
throw new Error(`Unknown resource: ${uri}`);
}
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
this.logger.error(`MCP resource error (${uri}):`, error.message);
throw new Error(`Failed to read resource ${uri}: ${error.message}`);
}
});
}
async setupRESTAPI() {
// Middleware with proper error handling
this.expressApp.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
this.expressApp.use(express.json({ limit: '10mb' }));
this.expressApp.use(express.static(path.join(__dirname, 'public')));
// Request logging middleware
this.expressApp.use((req, res, next) => {
this.logger.debug(`${req.method} ${req.path}`);
next();
});
// Task Management API
this.expressApp.post('/api/tasks', this.asyncHandler(async (req, res) => {
const result = await this.taskService.createTask(req.body);
res.status(201).json(result);
}));
this.expressApp.get('/api/tasks', this.asyncHandler(async (req, res) => {
const result = await this.taskService.queryTasks(req.query);
res.json(result);
}));
this.expressApp.put('/api/tasks/:id', this.asyncHandler(async (req, res) => {
const result = await this.taskService.updateTaskStatus({
id: parseInt(req.params.id),
...req.body
});
res.json(result);
}));
this.expressApp.delete('/api/tasks/:id', this.asyncHandler(async (req, res) => {
const result = await this.taskService.deleteTask(parseInt(req.params.id));
res.json(result);
}));
this.expressApp.get('/api/tasks/current-sprint', this.asyncHandler(async (req, res) => {
const result = await this.taskService.getCurrentSprintTasks();
res.json(result);
}));
// Sprint Management API
this.expressApp.post('/api/sprints', this.asyncHandler(async (req, res) => {
const result = await this.sprintService.createSprint(req.body);
res.status(201).json(result);
}));
this.expressApp.get('/api/sprints/current', this.asyncHandler(async (req, res) => {
const result = await this.sprintService.getCurrentSprint();
res.json(result);
}));
this.expressApp.get('/api/sprints', this.asyncHandler(async (req, res) => {
const result = await this.sprintService.getAllSprints(req.query);
res.json(result);
}));
this.expressApp.put('/api/sprints/:id', this.asyncHandler(async (req, res) => {
const result = await this.sprintService.updateSprint({
id: parseInt(req.params.id),
...req.body
});
res.json(result);
}));
this.expressApp.post('/api/sprints/:id/complete', this.asyncHandler(async (req, res) => {
const result = await this.sprintService.completeSprint(parseInt(req.params.id));
res.json(result);
}));
this.expressApp.get('/api/sprints/:id/progress', this.asyncHandler(async (req, res) => {
const result = await this.sprintService.getSprintProgress(parseInt(req.params.id));
res.json(result);
}));
// Project Management API
this.expressApp.get('/api/progress', this.asyncHandler(async (req, res) => {
const result = await this.projectService.getProjectProgress();
res.json(result);
}));
this.expressApp.get('/api/project/info', this.asyncHandler(async (req, res) => {
const result = await this.projectService.getProjectInfo();
res.json(result);
}));
this.expressApp.get('/api/project/analytics', this.asyncHandler(async (req, res) => {
const result = await this.projectService.getProjectAnalytics(req.query);
res.json(result);
}));
this.expressApp.get('/api/project/export/:format', this.asyncHandler(async (req, res) => {
const result = await this.projectService.exportProjectData(req.params.format);
res.json(result);
}));
// Web UI status and control
this.expressApp.get('/api/webui/status', this.asyncHandler(async (req, res) => {
const status = this.webUILauncher.getStatus();
res.json(status);
}));
this.expressApp.post('/api/webui/start', this.asyncHandler(async (req, res) => {
await this.webUILauncher.autoStartWebUI();
res.json({ success: true, message: 'Web UI start initiated' });
}));
this.expressApp.post('/api/webui/stop', this.asyncHandler(async (req, res) => {
await this.webUILauncher.stopWebUI();
res.json({ success: true, message: 'Web UI stopped' });
}));
// Health check with service container status
this.expressApp.get('/health', this.asyncHandler(async (req, res) => {
const containerHealth = await this.serviceContainer.healthCheck();
const webUIStatus = this.webUILauncher.getStatus();
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
services: {
mcp: 'running',
rest: 'running',
database: 'connected',
webUI: webUIStatus
},
serviceContainer: containerHealth
});
}));
// Global error handler
this.expressApp.use((error, req, res, next) => {
this.logger.error('REST API error:', error.message);
res.status(500).json({
success: false,
error: {
message: error.message,
code: 'REST_API_ERROR',
timestamp: new Date().toISOString()
}
});
});
// 404 handler
this.expressApp.use((req, res) => {
res.status(404).json({
success: false,
error: {
message: `Endpoint not found: ${req.method} ${req.path}`,
code: 'ENDPOINT_NOT_FOUND',
timestamp: new Date().toISOString()
}
});
});
}
/**
* Async handler wrapper for Express routes
* @param {Function} fn - Async route handler
* @returns {Function} Express middleware
*/
asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
setupGracefulShutdown() {
const shutdown = async (signal) => {
this.logger.info(`Received ${signal}, shutting down gracefully...`);
try {
if (this.serviceContainer) {
await this.serviceContainer.shutdown();
}
if (this.autoWebUI) {
await this.autoWebUI.stop();
}
if (this.storage) {
await this.storage.close();
}
this.logger.info('Shutdown complete');
process.exit(0);
} catch (error) {
this.logger.error('Error during shutdown:', error.message);
process.exit(1);
}
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
}
async startMCPServer() {
const transport = new StdioServerTransport();
await this.mcpServer.connect(transport);
this.logger.info('🔗 MCP server connected via stdio');
// Web UI should already be started during initialization if enabled
}
async startRESTServer() {
return new Promise((resolve) => {
this.expressApp.listen(this.port, () => {
this.logger.info(`🌐 REST API server running on http://localhost:${this.port}`);
this.logger.info(`📊 Health check: http://localhost:${this.port}/health`);
resolve();
});
});
}
async start() {
await this.initialize();
// Determine mode based on environment
const mode = process.env.BMAD_SERVER_MODE || 'dual';
switch (mode) {
case 'mcp':
this.logger.info('🎯 Starting in MCP-only mode');
await this.startMCPServer();
break;
case 'rest':
this.logger.info('🎯 Starting in REST-only mode');
await this.startRESTServer();
break;
case 'dual':
default:
this.logger.info('🎯 Starting in dual mode (MCP + REST)');
// Start REST server first, then MCP
await this.startRESTServer();
await this.startMCPServer();
break;
}
}
}
// Start server
if (require.main === module) {
const server = new BMadUnifiedServer();
server.start().catch((error) => {
console.error('Failed to start server:', error);
process.exit(1);
});
}
module.exports = BMadUnifiedServer;