bmad-method-mcp
Version:
Breakthrough Method of Agile AI-driven Development with Enhanced MCP Integration
208 lines (168 loc) • 5.6 kB
JavaScript
const { spawn } = require('child_process');
const { EventEmitter } = require('events');
const path = require('path');
class MCPClient extends EventEmitter {
constructor(dbPath) {
super();
this.dbPath = dbPath;
this.hubProcess = null;
this.requestId = 0;
this.pendingRequests = new Map();
this.connected = false;
}
async connect() {
if (this.connected) {
return;
}
return new Promise((resolve, reject) => {
const hubCliPath = path.join(__dirname, '..', 'mcp-hub', 'cli.js');
this.hubProcess = spawn('node', [hubCliPath, 'start'], {
cwd: path.dirname(this.dbPath),
stdio: ['pipe', 'pipe', 'pipe']
});
this.hubProcess.stdout.setEncoding('utf8');
this.hubProcess.stderr.setEncoding('utf8');
let buffer = '';
this.hubProcess.stdout.on('data', (chunk) => {
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line in buffer
for (const line of lines) {
if (line.trim()) {
try {
const response = JSON.parse(line);
this.handleResponse(response);
} catch (error) {
console.error('Failed to parse MCP response:', line, error);
}
}
}
});
// Log stderr for debugging
this.hubProcess.stderr.on('data', (chunk) => {
const message = chunk.toString().trim();
if (message.includes('MCP Hub server started')) {
this.connected = true;
resolve();
} else {
console.error('MCP Hub stderr:', message);
}
});
this.hubProcess.on('error', (error) => {
this.connected = false;
reject(error);
});
this.hubProcess.on('exit', (code) => {
this.connected = false;
console.error(`MCP Hub process exited with code ${code}`);
});
// Timeout if server doesn't start
setTimeout(() => {
if (!this.connected) {
reject(new Error('MCP Hub startup timeout'));
}
}, 10000);
});
}
async call(method, params = {}) {
if (!this.connected) {
await this.connect();
}
const id = ++this.requestId;
const request = { id, method, params };
return new Promise((resolve, reject) => {
this.pendingRequests.set(id, { resolve, reject });
this.hubProcess.stdin.write(JSON.stringify(request) + '\n');
// Timeout after 30 seconds
setTimeout(() => {
if (this.pendingRequests.has(id)) {
this.pendingRequests.delete(id);
reject(new Error(`MCP request timeout for method: ${method}`));
}
}, 30000);
});
}
handleResponse(response) {
if (response.id && this.pendingRequests.has(response.id)) {
const { resolve, reject } = this.pendingRequests.get(response.id);
this.pendingRequests.delete(response.id);
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
}
}
// Project Management
async createProject(name, description = '') {
return await this.call('createProject', { name, description });
}
async getProject() {
return await this.call('getProject');
}
// Document Management
async createDocument(type, title, content, status = 'DRAFT') {
return await this.call('createDocument', { type, title, content, status });
}
async updateDocument(id, content, status) {
return await this.call('updateDocument', { id, content, status });
}
async getDocument(id) {
return await this.call('getDocument', { id });
}
async listDocuments(type = null) {
return await this.call('listDocuments', { type });
}
// Epic Management
async createEpic(epicNum, title, description) {
return await this.call('createEpic', { epicNum, title, description });
}
// Task Management
async createTask(epicNum, storyNum, title, description, assignee = null) {
return await this.call('createTask', { epicNum, storyNum, title, description, assignee });
}
async getTasks(filters = {}) {
return await this.call('getTasks', { filters });
}
async updateTaskStatus(id, status, assignee = null) {
return await this.call('updateTaskStatus', { id, status, assignee });
}
async getNextStory(epicNum) {
return await this.call('getNextStory', { epicNum });
}
// Sprint Management
async createSprint(name, goal, startDate = null, endDate = null) {
return await this.call('createSprint', { name, goal, startDate, endDate });
}
async getActiveSprint() {
return await this.call('getActiveSprint');
}
async getSprints() {
return await this.call('getSprints');
}
// Convenience Methods
async queryTasks(options = {}) {
const filters = {};
if (options.epic) filters.epicNum = options.epic;
if (options.status) filters.status = options.status;
if (options.assignee) filters.assignee = options.assignee;
if (options.sprint) filters.sprintId = options.sprint;
return await this.getTasks(filters);
}
async getIncompleteTasks() {
return await this.getTasks({
status: ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'BLOCKED']
});
}
async getTasksByEpic(epicNum) {
return await this.getTasks({ epicNum });
}
async disconnect() {
if (this.hubProcess) {
this.hubProcess.kill();
this.hubProcess = null;
this.connected = false;
}
}
}
module.exports = { MCPClient };