#!/usr/bin/env node
/**
* MCP Tool Factory Server
*
* Infrastructure for spawning permission-graded, hot-loadable MCP tool classes
* with conversational negotiation.
*
* Phase 1 (M1): Hot-Reload Infrastructure
* Phase 2 (M2): Conversational Negotiation
* Phase 3 (M3): State Continuity via Supabase Cloud
*
* Environment: Inherits from parent process (Claude Desktop config)
*/
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 { ToolRegistry } from './core/tool-registry.js';
import { FileWatcher } from './core/file-watcher.js';
import { ConversationManager } from './core/conversation-manager.js';
import { InfrastructureRegistry } from './core/infrastructure-registry.js';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Main server initialization
*/
async function main() {
console.error('[Server] Starting MCP Tool Factory...');
// Initialize MCP server
const server = new Server(
{
name: 'mcp-tool-factory',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// M5: Initialize conversation manager (handles tool registry internally)
const conversationManager = new ConversationManager();
// Wait for tools to load from registry
await conversationManager.waitForToolsLoaded();
// Initialize infrastructure registry for hot-reload (M1 Extension)
const infraRegistry = new InfrastructureRegistry();
// Register infrastructure for hot-reload
infraRegistry.register('conversation-manager', ConversationManager, conversationManager);
console.error('[Server] ✓ conversation-manager registered for hot-reload');
// M5: Access tool registry from conversation manager
const toolRegistry = conversationManager.registry;
// Initialize file watchers for auto-reload (M1 Task 1.2 + M1 Extension)
// Separate watcher instances required (each manages single chokidar instance)
const toolWatcher = new FileWatcher(toolRegistry, infraRegistry);
toolWatcher.start(`${path.join(__dirname, 'tools')}/**/*.js`);
const infraWatcher = new FileWatcher(toolRegistry, infraRegistry);
infraWatcher.start(`${path.join(__dirname, 'core')}/**/*.js`);
// M5.5: Helper to generate precise schema from actionSchemas (Task 5.9)
function generateToolSchema(toolClass: any) {
const identity = toolClass?.identity;
const actionSchemas = toolClass?.actionSchemas;
// If tool has actionSchemas, generate flat schema with action enum
if (actionSchemas && Object.keys(actionSchemas).length > 0) {
const actions = Object.keys(actionSchemas);
const properties: any = {
action: {
type: 'string',
enum: actions,
description: `Action to perform. Available: ${actions.join(', ')}`,
},
conversationId: {
type: 'string',
description: 'Conversation ID for multi-user isolation (optional, defaults to "default")',
},
};
// Collect all possible parameters from all actions
const allParams = new Set<string>();
Object.values(actionSchemas).forEach((schema: any) => {
if (schema.params) {
schema.params.forEach((param: string) => allParams.add(param));
}
});
// Add all parameters as optional (action determines which are actually used)
for (const param of allParams) {
if (param === 'data') {
properties[param] = { type: 'object', description: 'Resource data (for create/update actions)' };
} else if (param === 'steps') {
properties[param] = { type: 'array', description: 'Pipeline steps', items: { type: 'object' } };
} else if (param === 'actions') {
properties[param] = { type: 'array', description: 'Aggregate actions', items: { type: 'object' } };
} else if (param === 'name') {
properties[param] = { type: 'string', description: 'Resource name' };
} else {
properties[param] = { type: 'string', description: `${param} parameter` };
}
}
return {
type: 'object',
properties,
required: ['action'],
additionalProperties: false,
$schema: 'http://json-schema.org/draft-07/schema#',
};
}
// Fallback to generic schema (backward compatible)
return {
type: 'object',
properties: {
action: {
type: 'string',
description: `Action to perform. Available: ${identity?.capabilities.join(', ')}`,
},
name: {
type: 'string',
description: 'Resource name (for data-tool operations like create-resource, read-resource, update-resource)',
},
data: {
type: 'object',
description: 'Resource data (for data-tool create-resource and update-resource operations)',
},
conversationId: {
type: 'string',
description: 'Conversation ID for multi-user isolation (optional, defaults to "default")',
},
},
required: ['action'],
additionalProperties: false,
$schema: 'http://json-schema.org/draft-07/schema#',
};
}
// Handle ListTools request
server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = toolRegistry.listTools().map((toolName) => {
const toolClass = toolRegistry.getTool(toolName);
const identity = toolClass?.identity;
return {
name: identity?.name || toolName,
description: `${identity?.name} v${identity?.version} - Capabilities: ${identity?.capabilities.join(', ')}`,
inputSchema: generateToolSchema(toolClass),
};
});
console.error(`[Server] ListTools: returning ${tools.length} tools`);
return { tools };
});
// Handle CallTool request (M5: Multi-Tool Gateway)
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
console.error(`[Server] CallTool: ${name} with args:`, args);
// M5: Tool name from MCP call is now used as explicitTool override
// Actions are routed via capability matching unless explicit tool specified
const explicitTool = name !== 'mcp-tool-factory' ? name : undefined;
// Extract action from arguments
const action = (args as any).action || 'greet';
// Conversation ID: use provided or default to single-user mode
// TODO(M3.2): Extract from MCP session for multi-user isolation
const conversationId = (args as any).conversationId || 'default';
// Get current conversation manager (may be hot-reloaded instance)
const currentConversationManager = infraRegistry.get('conversation-manager') || conversationManager;
// M5: Negotiate execution through conversation manager with new signature
// Tools discovered via registry, actions routed via capability matching
const result = await currentConversationManager.negotiate(
conversationId,
action,
args,
explicitTool
);
console.error(`[Server] Negotiation result:`, result);
// Return MCP-formatted response
if (result.success) {
return {
content: [
{
type: 'text',
text: typeof result.output === 'string'
? result.output
: JSON.stringify(result.output, null, 2),
},
],
};
} else if (result.requiresApproval) {
// Approval required response (M2 Task 2.3)
return {
content: [
{
type: 'text',
text: String(result.output || result.approvalReason),
},
],
};
} else {
// Error response
return {
content: [
{
type: 'text',
text: `Error: ${result.error}`,
},
],
isError: true,
};
}
});
// Start server with stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('[Server] ✓ MCP Tool Factory running on stdio');
console.error('[Server] Watching for tool changes...');
}
main().catch((error) => {
console.error('[Server] Fatal error:', error);
process.exit(1);
});