mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
refactor: restructure project to monorepo with apps directory
This commit is contained in:
349
apps/app/electron/services/mcp-server-stdio.js
Normal file
349
apps/app/electron/services/mcp-server-stdio.js
Normal file
@@ -0,0 +1,349 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Standalone STDIO MCP Server for Automaker Tools
|
||||
*
|
||||
* This script runs as a standalone process and communicates via JSON-RPC 2.0
|
||||
* over stdin/stdout. It implements the MCP protocol to expose the UpdateFeatureStatus
|
||||
* tool to Codex CLI.
|
||||
*
|
||||
* Environment variables:
|
||||
* - AUTOMAKER_PROJECT_PATH: Path to the project directory
|
||||
* - AUTOMAKER_IPC_CHANNEL: IPC channel name for callback communication (optional, uses default)
|
||||
*/
|
||||
|
||||
const readline = require('readline');
|
||||
const path = require('path');
|
||||
|
||||
// Redirect all console.log output to stderr to avoid polluting MCP stdout
|
||||
const originalConsoleLog = console.log;
|
||||
console.log = (...args) => {
|
||||
console.error(...args);
|
||||
};
|
||||
|
||||
// Set up readline interface for line-by-line JSON-RPC input
|
||||
// IMPORTANT: Use a separate output stream for readline to avoid interfering with JSON-RPC stdout
|
||||
// We'll write JSON-RPC responses directly to stdout, not through readline
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: null, // Don't use stdout for readline output
|
||||
terminal: false
|
||||
});
|
||||
|
||||
let initialized = false;
|
||||
let projectPath = null;
|
||||
let ipcChannel = null;
|
||||
|
||||
// Get configuration from environment
|
||||
projectPath = process.env.AUTOMAKER_PROJECT_PATH || process.cwd();
|
||||
ipcChannel = process.env.AUTOMAKER_IPC_CHANNEL || 'mcp:update-feature-status';
|
||||
|
||||
// Load dependencies (these will be available in the Electron app context)
|
||||
let featureLoader;
|
||||
let electron;
|
||||
|
||||
// Try to load Electron IPC if available (when running from Electron app)
|
||||
try {
|
||||
// In Electron, we can use IPC directly
|
||||
if (typeof require !== 'undefined') {
|
||||
// Check if we're in Electron context
|
||||
const electronModule = require('electron');
|
||||
if (electronModule && electronModule.ipcMain) {
|
||||
electron = electronModule;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Not in Electron context, will use alternative method
|
||||
}
|
||||
|
||||
// Load feature loader
|
||||
// Try multiple paths since this script might be run from different contexts
|
||||
try {
|
||||
// First try relative path (when run from electron/services/)
|
||||
featureLoader = require('./feature-loader');
|
||||
} catch (e) {
|
||||
try {
|
||||
// Try absolute path resolution
|
||||
const featureLoaderPath = path.resolve(__dirname, 'feature-loader.js');
|
||||
delete require.cache[require.resolve(featureLoaderPath)];
|
||||
featureLoader = require(featureLoaderPath);
|
||||
} catch (e2) {
|
||||
// If still fails, try from parent directory
|
||||
try {
|
||||
featureLoader = require(path.join(__dirname, '..', 'services', 'feature-loader'));
|
||||
} catch (e3) {
|
||||
console.error('[McpServerStdio] Error loading feature-loader:', e3.message);
|
||||
console.error('[McpServerStdio] Tried paths:', [
|
||||
'./feature-loader',
|
||||
path.resolve(__dirname, 'feature-loader.js'),
|
||||
path.join(__dirname, '..', 'services', 'feature-loader')
|
||||
]);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send JSON-RPC response
|
||||
* CRITICAL: Must write directly to stdout, not via console.log
|
||||
* MCP protocol requires ONLY JSON-RPC messages on stdout
|
||||
*/
|
||||
function sendResponse(id, result, error = null) {
|
||||
const response = {
|
||||
jsonrpc: '2.0',
|
||||
id
|
||||
};
|
||||
|
||||
if (error) {
|
||||
response.error = error;
|
||||
} else {
|
||||
response.result = result;
|
||||
}
|
||||
|
||||
// Write directly to stdout with newline (MCP uses line-delimited JSON)
|
||||
process.stdout.write(JSON.stringify(response) + '\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send JSON-RPC notification
|
||||
* CRITICAL: Must write directly to stdout, not via console.log
|
||||
*/
|
||||
function sendNotification(method, params) {
|
||||
const notification = {
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params
|
||||
};
|
||||
|
||||
// Write directly to stdout with newline (MCP uses line-delimited JSON)
|
||||
process.stdout.write(JSON.stringify(notification) + '\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle MCP initialize request
|
||||
*/
|
||||
async function handleInitialize(params, id) {
|
||||
initialized = true;
|
||||
|
||||
sendResponse(id, {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {}
|
||||
},
|
||||
serverInfo: {
|
||||
name: 'automaker-tools',
|
||||
version: '1.0.0'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tools/list request
|
||||
*/
|
||||
async function handleToolsList(params, id) {
|
||||
sendResponse(id, {
|
||||
tools: [
|
||||
{
|
||||
name: 'UpdateFeatureStatus',
|
||||
description: 'Update the status of a feature. Use this tool instead of directly modifying feature files to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
featureId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the feature to update'
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['backlog', 'in_progress', 'verified'],
|
||||
description: 'The new status for the feature. Note: If skipTests=true, verified will be converted to waiting_approval automatically.'
|
||||
},
|
||||
summary: {
|
||||
type: 'string',
|
||||
description: 'A brief summary of what was implemented/changed. This will be displayed on the Kanban card. Example: "Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx"'
|
||||
}
|
||||
},
|
||||
required: ['featureId', 'status']
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tools/call request
|
||||
*/
|
||||
async function handleToolsCall(params, id) {
|
||||
const { name, arguments: args } = params;
|
||||
|
||||
if (name !== 'UpdateFeatureStatus') {
|
||||
sendResponse(id, null, {
|
||||
code: -32601,
|
||||
message: `Unknown tool: ${name}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { featureId, status, summary } = args;
|
||||
|
||||
if (!featureId || !status) {
|
||||
sendResponse(id, null, {
|
||||
code: -32602,
|
||||
message: 'Missing required parameters: featureId and status are required'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the feature to check skipTests flag
|
||||
const features = await featureLoader.loadFeatures(projectPath);
|
||||
const feature = features.find((f) => f.id === featureId);
|
||||
|
||||
if (!feature) {
|
||||
sendResponse(id, null, {
|
||||
code: -32602,
|
||||
message: `Feature ${featureId} not found`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval
|
||||
let finalStatus = status;
|
||||
if (status === 'verified' && feature.skipTests === true) {
|
||||
finalStatus = 'waiting_approval';
|
||||
}
|
||||
|
||||
// Call the update callback via IPC or direct call
|
||||
// Since we're in a separate process, we need to use IPC to communicate back
|
||||
// For now, we'll call the feature loader directly since it has the update method
|
||||
await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, { summary });
|
||||
|
||||
const statusMessage = finalStatus !== status
|
||||
? `Successfully updated feature ${featureId} to status "${finalStatus}" (converted from "${status}" because skipTests=true)${summary ? ` with summary: "${summary}"` : ''}`
|
||||
: `Successfully updated feature ${featureId} to status "${finalStatus}"${summary ? ` with summary: "${summary}"` : ''}`;
|
||||
|
||||
sendResponse(id, {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: statusMessage
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[McpServerStdio] UpdateFeatureStatus error:', error);
|
||||
sendResponse(id, null, {
|
||||
code: -32603,
|
||||
message: `Failed to update feature status: ${error.message}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle JSON-RPC request
|
||||
*/
|
||||
async function handleRequest(line) {
|
||||
let request;
|
||||
|
||||
try {
|
||||
request = JSON.parse(line);
|
||||
} catch (e) {
|
||||
sendResponse(null, null, {
|
||||
code: -32700,
|
||||
message: 'Parse error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate JSON-RPC 2.0 structure
|
||||
if (request.jsonrpc !== '2.0') {
|
||||
sendResponse(request.id || null, null, {
|
||||
code: -32600,
|
||||
message: 'Invalid Request'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { method, params, id } = request;
|
||||
|
||||
// Handle notifications (no id)
|
||||
if (id === undefined) {
|
||||
// Handle notifications if needed
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle requests
|
||||
try {
|
||||
switch (method) {
|
||||
case 'initialize':
|
||||
await handleInitialize(params, id);
|
||||
break;
|
||||
|
||||
case 'tools/list':
|
||||
if (!initialized) {
|
||||
sendResponse(id, null, {
|
||||
code: -32002,
|
||||
message: 'Server not initialized'
|
||||
});
|
||||
return;
|
||||
}
|
||||
await handleToolsList(params, id);
|
||||
break;
|
||||
|
||||
case 'tools/call':
|
||||
if (!initialized) {
|
||||
sendResponse(id, null, {
|
||||
code: -32002,
|
||||
message: 'Server not initialized'
|
||||
});
|
||||
return;
|
||||
}
|
||||
await handleToolsCall(params, id);
|
||||
break;
|
||||
|
||||
default:
|
||||
sendResponse(id, null, {
|
||||
code: -32601,
|
||||
message: `Method not found: ${method}`
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[McpServerStdio] Error handling request:', error);
|
||||
sendResponse(id, null, {
|
||||
code: -32603,
|
||||
message: `Internal error: ${error.message}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process stdin line by line
|
||||
rl.on('line', async (line) => {
|
||||
if (!line.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await handleRequest(line);
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
rl.on('error', (error) => {
|
||||
console.error('[McpServerStdio] Readline error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle process termination
|
||||
process.on('SIGTERM', () => {
|
||||
rl.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
rl.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Log startup
|
||||
console.error('[McpServerStdio] Starting MCP server for automaker-tools');
|
||||
console.error(`[McpServerStdio] Project path: ${projectPath}`);
|
||||
console.error(`[McpServerStdio] IPC channel: ${ipcChannel}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user