aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
468 lines (410 loc) • 12.4 kB
JavaScript
/**
* Droid MCP Server
* Bridges Claude Code to Factory Droid via Model Context Protocol
*
* Features:
* - Session tracking with unique IDs
* - Full request/response logging
* - Automatic archival support
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { spawn } from 'child_process';
import { writeFileSync, mkdirSync, existsSync, symlinkSync, unlinkSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import { randomBytes } from 'crypto';
// Base paths for logging
const PROJECT_ROOT = process.env.DROID_PROJECT_ROOT || process.cwd();
const AIWG_DROID_DIR = join(PROJECT_ROOT, '.aiwg', 'droid');
const SESSIONS_DIR = join(AIWG_DROID_DIR, 'sessions');
const CURRENT_LINK = join(AIWG_DROID_DIR, 'current');
// Ensure directories exist
function ensureDirectories() {
const dirs = [
AIWG_DROID_DIR,
SESSIONS_DIR,
join(AIWG_DROID_DIR, 'archive')
];
for (const dir of dirs) {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
}
/**
* Generate a unique session ID
*/
function generateSessionId() {
const now = new Date();
const timestamp = now.toISOString().replace(/[-:]/g, '').replace('T', '-').split('.')[0];
const random = randomBytes(4).toString('hex');
return `${timestamp}-${random}`;
}
/**
* Create a session directory and return its path
*/
function createSession(sessionId, toolName, args) {
ensureDirectories();
const sessionDir = join(SESSIONS_DIR, sessionId);
mkdirSync(sessionDir, { recursive: true });
// Write request metadata
const request = {
sessionId,
tool: toolName,
args,
startTime: new Date().toISOString(),
status: 'in_progress'
};
writeFileSync(
join(sessionDir, 'request.json'),
JSON.stringify(request, null, 2)
);
// Update current symlink
try {
if (existsSync(CURRENT_LINK)) {
unlinkSync(CURRENT_LINK);
}
symlinkSync(sessionDir, CURRENT_LINK);
} catch (e) {
// Symlink may fail on some systems, non-critical
}
return sessionDir;
}
/**
* Complete a session with response data
*/
function completeSession(sessionDir, result, duration) {
const requestPath = join(sessionDir, 'request.json');
const request = JSON.parse(readFileSync(requestPath, 'utf8'));
// Update request with completion info
request.endTime = new Date().toISOString();
request.duration = duration;
request.status = result.success ? 'completed' : 'failed';
writeFileSync(requestPath, JSON.stringify(request, null, 2));
// Write response
writeFileSync(
join(sessionDir, 'response.json'),
JSON.stringify({
success: result.success,
exitCode: result.exitCode,
output: result.output,
error: result.error || null,
stderr: result.stderr || null
}, null, 2)
);
// Write combined log for easy reading
const log = `
================================================================================
DROID SESSION: ${request.sessionId}
================================================================================
Tool: ${request.tool}
Started: ${request.startTime}
Ended: ${request.endTime}
Duration: ${duration}ms
Status: ${request.status}
Exit Code: ${result.exitCode}
--- PROMPT ---
${request.args.prompt || 'N/A'}
--- OUTPUT ---
${result.output || '(no output)'}
--- ERRORS ---
${result.error || result.stderr || '(none)'}
================================================================================
`.trim();
writeFileSync(join(sessionDir, 'session.log'), log);
}
// Create MCP server
const server = new Server(
{
name: 'droid-mcp-server',
version: '2.0.0',
},
{
capabilities: {
tools: {},
},
}
);
/**
* Execute a Droid command and return the result
*/
async function executeDroid(prompt, options = {}) {
const {
autoLevel = 'medium',
model = 'claude-opus-4-5-20251101',
cwd = process.cwd(),
timeout = 300000 // 5 minutes default
} = options;
return new Promise((resolve, reject) => {
const args = ['exec'];
// Add autonomy level
if (autoLevel && autoLevel !== 'readonly') {
args.push('--auto', autoLevel);
}
// Add model if specified
if (model) {
args.push('--model', model);
}
// Add working directory
args.push('--cwd', cwd);
// Add the prompt
args.push(prompt);
const droidProcess = spawn('droid', args, {
cwd: cwd,
env: { ...process.env },
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
droidProcess.stdout.on('data', (data) => {
stdout += data.toString();
});
droidProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
// Timeout handler
const timeoutId = setTimeout(() => {
droidProcess.kill('SIGTERM');
reject(new Error(`Droid execution timed out after ${timeout}ms`));
}, timeout);
droidProcess.on('close', (code) => {
clearTimeout(timeoutId);
if (code === 0) {
resolve({
success: true,
output: stdout,
stderr: stderr || null,
exitCode: code
});
} else {
resolve({
success: false,
output: stdout,
error: stderr || `Process exited with code ${code}`,
exitCode: code
});
}
});
droidProcess.on('error', (err) => {
clearTimeout(timeoutId);
reject(new Error(`Failed to spawn Droid: ${err.message}`));
});
});
}
/**
* Check if Droid is available
*/
async function checkDroidAvailable() {
return new Promise((resolve) => {
const check = spawn('droid', ['--version'], { stdio: ['pipe', 'pipe', 'pipe'] });
let version = '';
check.stdout.on('data', (data) => {
version += data.toString();
});
check.on('close', (code) => {
resolve({
available: code === 0,
version: version.trim()
});
});
check.on('error', () => {
resolve({ available: false, version: null });
});
});
}
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'droid_exec',
description: `Execute a task using Factory Droid AI agent. Droid is excellent for:
- Quick batch fixes (linting, formatting, refactoring)
- Code modifications that don't need conversation context
- Automated fixes (TypeScript errors, test failures)
- File operations across multiple files
Autonomy levels:
- readonly: Only read operations (safe analysis)
- low: Basic file operations (docs, comments, formatting)
- medium: Development operations (npm install, git commit, builds)
- high: Production operations (git push, deployments)`,
inputSchema: {
type: 'object',
properties: {
prompt: {
type: 'string',
description: 'The task description for Droid to execute'
},
autoLevel: {
type: 'string',
enum: ['readonly', 'low', 'medium', 'high'],
default: 'medium',
description: 'Autonomy level controlling what operations Droid can perform'
},
model: {
type: 'string',
default: 'claude-opus-4-5-20251101',
description: 'Model to use for Droid execution'
},
cwd: {
type: 'string',
description: 'Working directory for Droid (defaults to current directory)'
},
timeout: {
type: 'number',
default: 300000,
description: 'Timeout in milliseconds (default: 5 minutes)'
}
},
required: ['prompt']
}
},
{
name: 'droid_status',
description: 'Check if Factory Droid is available and get version info',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'droid_analyze',
description: 'Use Droid in read-only mode to analyze code without making changes. Safe for exploration and planning.',
inputSchema: {
type: 'object',
properties: {
prompt: {
type: 'string',
description: 'What to analyze (e.g., "review security of auth module", "find performance issues")'
},
cwd: {
type: 'string',
description: 'Working directory for analysis'
}
},
required: ['prompt']
}
}
]
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'droid_exec': {
const sessionId = generateSessionId();
const sessionDir = createSession(sessionId, name, args);
const startTime = Date.now();
try {
const result = await executeDroid(args.prompt, {
autoLevel: args.autoLevel || 'medium',
model: args.model,
cwd: args.cwd || PROJECT_ROOT,
timeout: args.timeout || 300000
});
const duration = Date.now() - startTime;
completeSession(sessionDir, result, duration);
return {
content: [
{
type: 'text',
text: JSON.stringify({ ...result, sessionId }, null, 2)
}
]
};
} catch (error) {
const duration = Date.now() - startTime;
const errorResult = { success: false, error: error.message, exitCode: -1 };
completeSession(sessionDir, errorResult, duration);
return {
content: [
{
type: 'text',
text: JSON.stringify({ ...errorResult, sessionId }, null, 2)
}
],
isError: true
};
}
}
case 'droid_status': {
const status = await checkDroidAvailable();
// Count sessions
let sessionCount = 0;
try {
const { readdirSync } = await import('fs');
sessionCount = readdirSync(SESSIONS_DIR).filter(f => !f.startsWith('.')).length;
} catch (e) {}
return {
content: [
{
type: 'text',
text: JSON.stringify({
available: status.available,
version: status.version,
sessionsDir: SESSIONS_DIR,
totalSessions: sessionCount,
message: status.available
? `Droid v${status.version} is ready (${sessionCount} sessions logged)`
: 'Droid is not available in PATH'
}, null, 2)
}
]
};
}
case 'droid_analyze': {
const sessionId = generateSessionId();
const sessionDir = createSession(sessionId, name, args);
const startTime = Date.now();
try {
const result = await executeDroid(args.prompt, {
autoLevel: 'readonly',
cwd: args.cwd || PROJECT_ROOT,
timeout: 120000 // 2 min for analysis
});
const duration = Date.now() - startTime;
completeSession(sessionDir, result, duration);
return {
content: [
{
type: 'text',
text: JSON.stringify({ ...result, sessionId }, null, 2)
}
]
};
} catch (error) {
const duration = Date.now() - startTime;
const errorResult = { success: false, error: error.message, exitCode: -1 };
completeSession(sessionDir, errorResult, duration);
return {
content: [
{
type: 'text',
text: JSON.stringify({ ...errorResult, sessionId }, null, 2)
}
],
isError: true
};
}
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// Start the server
async function main() {
ensureDirectories();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Droid MCP Server v2.0.0 started (with session logging)');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});