UNPKG

n8n-nodes-a2a-protocol

Version:

Agent2Agent (A2A) Protocol nodes for n8n - Enable agent interoperability, communication, and MCP integration

395 lines (394 loc) 17.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.A2AAgentRegistry = void 0; const n8n_workflow_1 = require("n8n-workflow"); const express_1 = __importDefault(require("express")); const urlUtils_1 = require("../../utils/urlUtils"); class A2AAgentRegistry { constructor() { this.description = { displayName: 'A2A Agent Registry', name: 'a2aAgentRegistry', icon: 'file:a2a-registry.svg', group: ['trigger'], version: 1, subtitle: '={{$parameter["registryName"]}}', description: 'A2A Agent Registry for discovering and managing agents', defaults: { name: 'A2A Agent Registry', }, inputs: [], outputs: ["main" /* NodeConnectionType.Main */], credentials: [], properties: [ { displayName: 'Registry Name', name: 'registryName', type: 'string', default: 'N8N A2A Registry', description: 'Name of the agent registry', required: true, }, { displayName: 'Port', name: 'port', type: 'number', default: urlUtils_1.DEFAULT_A2A_PORTS.REGISTRY, description: '⚠️ Port to listen on for registry requests. NOTE: Port conflicts will be detected during workflow activation, not save. Run "node validate-a2a-ports.js" to check for conflicts before saving.', required: true, }, { displayName: 'Enable Agent Discovery', name: 'enableDiscovery', type: 'boolean', default: true, description: 'Allow agents to be discovered through this registry', }, { displayName: 'Authentication Mode', name: 'authMode', type: 'options', options: [ { name: 'None', value: 'none', description: 'No authentication required', }, { name: 'Bearer Token', value: 'bearer_token', description: 'Require Bearer token authentication', }, { name: 'API Key', value: 'api_key', description: 'Require API key authentication', }, ], default: 'none', description: 'Authentication method for registry requests', }, { displayName: 'Registry Configuration', name: 'registryConfig', type: 'json', default: `{ "description": "N8N-powered A2A Agent Registry", "version": "1.0.0", "supported_protocols": ["a2a-v1"], "features": ["agent_discovery", "capability_indexing", "health_monitoring"] }`, description: 'Registry configuration in JSON format', }, ], }; } async trigger() { const port = this.getNodeParameter('port'); const registryName = this.getNodeParameter('registryName'); const enableDiscovery = this.getNodeParameter('enableDiscovery'); const authMode = this.getNodeParameter('authMode'); const registryConfigStr = this.getNodeParameter('registryConfig'); // Generate stable node ID for this configuration (without timestamp) const nodeId = `registry_${registryName}_${port}`; // ✅ VALIDATE PORT BEFORE STARTING - This will show popup errors that prevent workflow activation const portValidation = await (0, urlUtils_1.validateNodePort)(port, 'registry', `A2A Registry (${registryName})`, nodeId); if (!portValidation.isValid && portValidation.errorMessage) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `❌ Port ${port} is already in use. Please choose a different port for A2A Registry (${registryName}).`); } // Stop previous server instance if it exists (safe - only closes Express servers) await (0, urlUtils_1.stopPreviousServerInstance)(nodeId); // Parse registry configuration let parsedConfig; try { parsedConfig = JSON.parse(registryConfigStr); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid registry configuration JSON: ${error.message}`); } // Initialize storage const agentRegistry = new Map(); const capabilityIndex = new Map(); // capability -> agent_ids const eventQueue = []; // Queue for events to emit to workflow // Get dynamic endpoints const endpoints = (0, urlUtils_1.createAgentEndpoints)(port); const urlConfig = (0, urlUtils_1.detectInstanceUrl)(); const app = (0, express_1.default)(); app.use(express_1.default.json()); // Process event queue and emit to workflow const processEventQueue = () => { if (eventQueue.length > 0) { const eventsToEmit = eventQueue.splice(0); // Get all events and clear queue eventsToEmit.forEach(eventData => { this.emit([this.helpers.returnJsonArray([eventData])]); }); } }; // Check for new events every 100ms const eventProcessor = setInterval(processEventQueue, 100); app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-A2A-*'); if (req.method === 'OPTIONS') return res.sendStatus(200); next(); }); // Authentication middleware if (authMode !== 'none') { app.use((req, res, next) => { if (req.path === '/health' || req.path === '/info') { return next(); } const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).json({ error: 'Authentication required', auth_mode: authMode }); } if (authMode === 'bearer_token' && !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Bearer token required' }); } if (authMode === 'api_key' && !authHeader.startsWith('ApiKey ')) { return res.status(401).json({ error: 'API key required' }); } next(); }); } // Helper function to update capability index const updateCapabilityIndex = (agentId, capabilities) => { // Remove agent from all capability sets first capabilityIndex.forEach(agentSet => agentSet.delete(agentId)); // Add agent to capability sets if (capabilities && Array.isArray(capabilities)) { capabilities.forEach(cap => { const capName = typeof cap === 'string' ? cap : cap.name; if (capName) { if (!capabilityIndex.has(capName)) { capabilityIndex.set(capName, new Set()); } capabilityIndex.get(capName).add(agentId); } }); } }; // Registry info endpoint app.get('/info', (req, res) => { const registryInfo = parsedConfig.registry_info || {}; res.json(Object.assign(Object.assign({}, registryInfo), { endpoint: endpoints.endpoint, total_agents: agentRegistry.size, active_agents: Array.from(agentRegistry.values()).filter(a => a.status === 'active').length, capabilities_indexed: capabilityIndex.size, timestamp: new Date().toISOString() })); }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', registry_name: registryName, total_agents: agentRegistry.size, uptime: process.uptime(), timestamp: new Date().toISOString(), }); }); // Agent registration endpoint app.post('/v1/agents', (req, res) => { try { const agentData = req.body; const agentId = agentData.agent_id; if (!agentId) { return res.status(400).json({ error: 'Missing agent_id in registration data' }); } // Store agent data with registration timestamp const registrationData = Object.assign(Object.assign({}, agentData), { status: 'active', registered_at: new Date().toISOString(), last_heartbeat: new Date().toISOString(), registry_endpoint: endpoints.endpoint }); agentRegistry.set(agentId, registrationData); // Update capability index updateCapabilityIndex(agentId, agentData.capabilities); // Add registration event to queue eventQueue.push({ event_type: 'agent_registered', agent_id: agentId, agent_data: registrationData, registry_name: registryName, total_agents: agentRegistry.size, timestamp: new Date().toISOString(), }); res.status(201).json({ success: true, message: 'Agent registered successfully', agent_id: agentId, registry_endpoint: endpoints.endpoint, timestamp: new Date().toISOString(), }); } catch (error) { res.status(500).json({ error: 'Agent registration failed', message: error.message, }); } }); // Agent discovery endpoint app.get('/v1/agents/discover', (req, res) => { if (!enableDiscovery) { return res.status(403).json({ error: 'Agent discovery is disabled', }); } const capability = req.query.capability; const status = req.query.status || 'active'; const limit = parseInt(req.query.limit) || 50; let agents = Array.from(agentRegistry.values()); // Filter by status if (status) { agents = agents.filter(agent => agent.status === status); } // Filter by capability if (capability) { const agentIdsWithCapability = capabilityIndex.get(capability); if (agentIdsWithCapability) { agents = agents.filter(agent => agentIdsWithCapability.has(agent.agent_id)); } else { agents = []; } } // Apply limit agents = agents.slice(0, limit); // Add discovery event to queue eventQueue.push({ event_type: 'agents_discovered', query: { capability, status, limit }, agents_found: agents.length, registry_name: registryName, timestamp: new Date().toISOString(), }); res.json({ agents, total_found: agents.length, query: { capability, status, limit }, registry_info: { name: registryName, endpoint: endpoints.endpoint, total_agents: agentRegistry.size, }, timestamp: new Date().toISOString(), }); }); // Get specific agent info app.get('/v1/agents/:agentId', (req, res) => { const agentId = req.params.agentId; const agent = agentRegistry.get(agentId); if (!agent) { return res.status(404).json({ error: 'Agent not found', agent_id: agentId, }); } res.json(Object.assign(Object.assign({}, agent), { timestamp: new Date().toISOString() })); }); // Agent heartbeat endpoint app.post('/v1/agents/:agentId/heartbeat', (req, res) => { const agentId = req.params.agentId; const agent = agentRegistry.get(agentId); if (!agent) { return res.status(404).json({ error: 'Agent not found', agent_id: agentId, }); } // Update heartbeat timestamp agent.last_heartbeat = new Date().toISOString(); agent.status = 'active'; agentRegistry.set(agentId, agent); res.json({ success: true, message: 'Heartbeat received', agent_id: agentId, timestamp: new Date().toISOString(), }); }); // Unregister agent app.delete('/v1/agents/:agentId', (req, res) => { const agentId = req.params.agentId; const agent = agentRegistry.get(agentId); if (!agent) { return res.status(404).json({ error: 'Agent not found', agent_id: agentId, }); } // Remove from registry and capability index agentRegistry.delete(agentId); capabilityIndex.forEach(agentSet => agentSet.delete(agentId)); // Add unregistration event to queue eventQueue.push({ event_type: 'agent_unregistered', agent_id: agentId, agent_data: agent, registry_name: registryName, total_agents: agentRegistry.size, timestamp: new Date().toISOString(), }); res.json({ success: true, message: 'Agent unregistered successfully', agent_id: agentId, timestamp: new Date().toISOString(), }); }); // Registry statistics app.get('/v1/stats', (req, res) => { const stats = { registry_name: registryName, total_agents: agentRegistry.size, active_agents: Array.from(agentRegistry.values()).filter(a => a.status === 'active').length, capabilities: Array.from(capabilityIndex.keys()), capability_distribution: Object.fromEntries(Array.from(capabilityIndex.entries()).map(([cap, agents]) => [cap, agents.size])), agent_status_distribution: Array.from(agentRegistry.values()).reduce((acc, agent) => { const status = agent.status || 'unknown'; const currentCount = acc[status] || 0; acc[status] = currentCount + 1; return acc; }, {}), endpoint: endpoints.endpoint, tasks: endpoints.tasks, health: endpoints.health, timestamp: new Date().toISOString(), }; res.json(stats); }); const server = app.listen(port, () => { // Store the server reference for this node instance (0, urlUtils_1.storeActiveServer)(nodeId, server, port, 'registry'); }); // Handle server startup errors server.on('error', async (error) => { if (error.code === 'EADDRINUSE') { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `❌ Port ${port} is already in use. Please choose a different port for A2A Registry (${registryName}).`); } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `❌ A2A Registry server error: ${error.message}`); } }); return { closeFunction: async () => { server.close(); agentRegistry.clear(); capabilityIndex.clear(); clearInterval(eventProcessor); eventQueue.length = 0; // Clear event queue (0, urlUtils_1.cleanupActiveServer)(nodeId); // Clean up stored server reference }, manualTriggerFunction: async () => { this.emit([this.helpers.returnJsonArray([Object.assign(Object.assign({ event_type: 'registry_ready', message: 'A2A Registry is ready to accept agent registrations', registry_name: registryName }, endpoints), { total_agents: agentRegistry.size, discovery_enabled: enableDiscovery, auth_mode: authMode, timestamp: new Date().toISOString() })])]); }, }; } } exports.A2AAgentRegistry = A2AAgentRegistry;