414 lines
14 KiB
JavaScript
414 lines
14 KiB
JavaScript
/**
|
|
* MCP Tools for Worker System
|
|
*
|
|
* Exposes worker functionality via Model Context Protocol tools.
|
|
*/
|
|
// ============================================================================
|
|
// Input Validation
|
|
// ============================================================================
|
|
const ALLOWED_WORKERS = new Set([
|
|
'performance', 'health', 'security', 'adr', 'ddd',
|
|
'patterns', 'learning', 'cache', 'git', 'swarm'
|
|
]);
|
|
const ALLOWED_SEVERITIES = new Set(['info', 'warning', 'critical']);
|
|
const ALLOWED_FORMATS = new Set(['json', 'string']);
|
|
function validateWorkerName(name) {
|
|
if (typeof name !== 'string')
|
|
return null;
|
|
if (!ALLOWED_WORKERS.has(name))
|
|
return null;
|
|
return name;
|
|
}
|
|
function validateNumber(value, min = 0, max = 10000) {
|
|
if (typeof value !== 'number')
|
|
return null;
|
|
if (value < min || value > max)
|
|
return null;
|
|
return value;
|
|
}
|
|
function validateString(value, allowedSet) {
|
|
if (typeof value !== 'string')
|
|
return null;
|
|
if (allowedSet && !allowedSet.has(value))
|
|
return null;
|
|
return value;
|
|
}
|
|
function sanitizeErrorMessage(error) {
|
|
// Return generic message to avoid information disclosure
|
|
if (error instanceof Error) {
|
|
// Only expose safe error types
|
|
if (error.message.includes('not found') || error.message.includes('Invalid')) {
|
|
return error.message;
|
|
}
|
|
}
|
|
return 'An internal error occurred';
|
|
}
|
|
// ============================================================================
|
|
// Worker MCP Tools
|
|
// ============================================================================
|
|
export const workerRunTool = {
|
|
name: 'worker/run',
|
|
description: 'Run a specific background worker immediately. Available workers: performance, health, security, adr, ddd, patterns, learning, cache, git, swarm',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
worker: {
|
|
type: 'string',
|
|
description: 'Name of the worker to run',
|
|
enum: ['performance', 'health', 'security', 'adr', 'ddd', 'patterns', 'learning', 'cache', 'git', 'swarm'],
|
|
},
|
|
},
|
|
required: ['worker'],
|
|
},
|
|
handler: async (input, manager) => {
|
|
// Validate input
|
|
const workerName = validateWorkerName(input.worker);
|
|
if (!workerName) {
|
|
return {
|
|
content: [{ type: 'text', text: 'Invalid worker name' }],
|
|
isError: true,
|
|
};
|
|
}
|
|
try {
|
|
const result = await manager.runWorker(workerName);
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
success: result.success,
|
|
worker: result.worker,
|
|
duration: result.duration,
|
|
data: result.data,
|
|
alerts: result.alerts,
|
|
error: result.error,
|
|
}, null, 2),
|
|
}],
|
|
};
|
|
}
|
|
catch (error) {
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Error running worker: ${sanitizeErrorMessage(error)}`,
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
},
|
|
};
|
|
export const workerStatusTool = {
|
|
name: 'worker/status',
|
|
description: 'Get status of all background workers including run counts, errors, and last results',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
worker: {
|
|
type: 'string',
|
|
description: 'Optional: Get status for a specific worker',
|
|
},
|
|
},
|
|
},
|
|
handler: async (input, manager) => {
|
|
const status = manager.getStatus();
|
|
// Validate optional worker name
|
|
if (input.worker !== undefined) {
|
|
const workerName = validateWorkerName(input.worker);
|
|
if (!workerName) {
|
|
return {
|
|
content: [{ type: 'text', text: 'Invalid worker name' }],
|
|
isError: true,
|
|
};
|
|
}
|
|
const worker = status.workers.find(w => w.name === workerName);
|
|
if (!worker) {
|
|
return {
|
|
content: [{ type: 'text', text: `Worker '${workerName}' not found` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(worker, null, 2) }],
|
|
};
|
|
}
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
running: status.running,
|
|
platform: status.platform,
|
|
uptime: Math.round(status.uptime / 1000) + 's',
|
|
totalRuns: status.totalRuns,
|
|
workers: status.workers.map(w => ({
|
|
name: w.name,
|
|
status: w.status,
|
|
runCount: w.runCount,
|
|
errorCount: w.errorCount,
|
|
avgDuration: Math.round(w.avgDuration) + 'ms',
|
|
lastRun: w.lastRun?.toISOString(),
|
|
})),
|
|
}, null, 2),
|
|
}],
|
|
};
|
|
},
|
|
};
|
|
export const workerAlertsTool = {
|
|
name: 'worker/alerts',
|
|
description: 'Get recent alerts from worker runs (threshold violations)',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
limit: {
|
|
type: 'number',
|
|
description: 'Maximum number of alerts to return (default: 20)',
|
|
},
|
|
severity: {
|
|
type: 'string',
|
|
description: 'Filter by severity level',
|
|
enum: ['info', 'warning', 'critical'],
|
|
},
|
|
},
|
|
},
|
|
handler: async (input, manager) => {
|
|
// Validate inputs
|
|
const limit = validateNumber(input.limit, 1, 100) ?? 20;
|
|
const severity = input.severity !== undefined
|
|
? validateString(input.severity, ALLOWED_SEVERITIES)
|
|
: undefined;
|
|
if (input.severity !== undefined && !severity) {
|
|
return {
|
|
content: [{ type: 'text', text: 'Invalid severity level' }],
|
|
isError: true,
|
|
};
|
|
}
|
|
let alerts = manager.getAlerts(limit);
|
|
if (severity) {
|
|
alerts = alerts.filter(a => a.severity === severity);
|
|
}
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: alerts.length > 0
|
|
? JSON.stringify(alerts.map(a => ({
|
|
worker: a.worker,
|
|
severity: a.severity,
|
|
message: a.message,
|
|
metric: a.metric,
|
|
value: a.value,
|
|
threshold: a.threshold,
|
|
timestamp: a.timestamp.toISOString(),
|
|
})), null, 2)
|
|
: 'No alerts',
|
|
}],
|
|
};
|
|
},
|
|
};
|
|
export const workerHistoryTool = {
|
|
name: 'worker/history',
|
|
description: 'Get historical metrics from worker runs for trend analysis',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
worker: {
|
|
type: 'string',
|
|
description: 'Filter by worker name',
|
|
},
|
|
limit: {
|
|
type: 'number',
|
|
description: 'Maximum number of history entries (default: 50)',
|
|
},
|
|
},
|
|
},
|
|
handler: async (input, manager) => {
|
|
// Validate inputs
|
|
const workerName = input.worker !== undefined
|
|
? validateWorkerName(input.worker)
|
|
: undefined;
|
|
if (input.worker !== undefined && !workerName) {
|
|
return {
|
|
content: [{ type: 'text', text: 'Invalid worker name' }],
|
|
isError: true,
|
|
};
|
|
}
|
|
const limit = validateNumber(input.limit, 1, 1000) ?? 50;
|
|
const history = manager.getHistory(workerName ?? undefined, limit);
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: history.length > 0
|
|
? JSON.stringify(history, null, 2)
|
|
: 'No history available',
|
|
}],
|
|
};
|
|
},
|
|
};
|
|
export const workerStatuslineTool = {
|
|
name: 'worker/statusline',
|
|
description: 'Get formatted statusline data for display',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
format: {
|
|
type: 'string',
|
|
description: 'Output format: json or string',
|
|
enum: ['json', 'string'],
|
|
},
|
|
},
|
|
},
|
|
handler: async (input, manager) => {
|
|
// Validate format
|
|
const format = validateString(input.format, ALLOWED_FORMATS) ?? 'json';
|
|
if (format === 'string') {
|
|
return {
|
|
content: [{ type: 'text', text: manager.getStatuslineString() }],
|
|
};
|
|
}
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: JSON.stringify(manager.getStatuslineData(), null, 2),
|
|
}],
|
|
};
|
|
},
|
|
};
|
|
export const workerRunAllTool = {
|
|
name: 'worker/run-all',
|
|
description: 'Run all enabled workers immediately',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
concurrency: {
|
|
type: 'number',
|
|
description: 'Maximum concurrent workers (default: 5)',
|
|
},
|
|
},
|
|
},
|
|
handler: async (input, manager) => {
|
|
// Validate concurrency (1-10 range)
|
|
const concurrency = validateNumber(input.concurrency, 1, 10) ?? 5;
|
|
try {
|
|
const results = await manager.runAll(concurrency);
|
|
const summary = {
|
|
total: results.length,
|
|
success: results.filter(r => r.success).length,
|
|
failed: results.filter(r => !r.success).length,
|
|
totalDuration: results.reduce((sum, r) => sum + r.duration, 0),
|
|
alerts: results.flatMap(r => r.alerts || []),
|
|
results: results.map(r => ({
|
|
worker: r.worker,
|
|
success: r.success,
|
|
duration: r.duration,
|
|
error: r.error,
|
|
})),
|
|
};
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
|
|
};
|
|
}
|
|
catch (error) {
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Error running workers: ${sanitizeErrorMessage(error)}`,
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
},
|
|
};
|
|
export const workerStartTool = {
|
|
name: 'worker/start',
|
|
description: 'Start the worker manager with automatic scheduling',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
autoSave: {
|
|
type: 'boolean',
|
|
description: 'Enable automatic state saving (default: true)',
|
|
},
|
|
statuslineUpdate: {
|
|
type: 'boolean',
|
|
description: 'Enable statusline file updates (default: true)',
|
|
},
|
|
},
|
|
},
|
|
handler: async (input, manager) => {
|
|
// Validate boolean inputs
|
|
const autoSave = typeof input.autoSave === 'boolean' ? input.autoSave : true;
|
|
const statuslineUpdate = typeof input.statuslineUpdate === 'boolean' ? input.statuslineUpdate : true;
|
|
try {
|
|
await manager.start({ autoSave, statuslineUpdate });
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: 'Worker manager started with scheduling enabled',
|
|
}],
|
|
};
|
|
}
|
|
catch (error) {
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Error starting worker manager: ${sanitizeErrorMessage(error)}`,
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
},
|
|
};
|
|
export const workerStopTool = {
|
|
name: 'worker/stop',
|
|
description: 'Stop the worker manager and save state',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {},
|
|
},
|
|
handler: async (_input, manager) => {
|
|
try {
|
|
await manager.stop();
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: 'Worker manager stopped and state saved',
|
|
}],
|
|
};
|
|
}
|
|
catch (error) {
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Error stopping worker manager: ${sanitizeErrorMessage(error)}`,
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
},
|
|
};
|
|
// ============================================================================
|
|
// Tool Registry
|
|
// ============================================================================
|
|
export const workerMCPTools = [
|
|
workerRunTool,
|
|
workerStatusTool,
|
|
workerAlertsTool,
|
|
workerHistoryTool,
|
|
workerStatuslineTool,
|
|
workerRunAllTool,
|
|
workerStartTool,
|
|
workerStopTool,
|
|
];
|
|
/**
|
|
* Create a tool handler function for MCP server integration
|
|
*/
|
|
export function createWorkerToolHandler(manager) {
|
|
return async (toolName, input) => {
|
|
const tool = workerMCPTools.find(t => t.name === toolName);
|
|
if (!tool) {
|
|
return {
|
|
content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
return tool.handler(input, manager);
|
|
};
|
|
}
|
|
//# sourceMappingURL=mcp-tools.js.map
|