UNPKG

n8n-nodes-a2a-protocol

Version:

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

528 lines (527 loc) 29 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.A2AClientAgent = void 0; const n8n_workflow_1 = require("n8n-workflow"); const urlUtils_1 = require("../../utils/urlUtils"); class A2AClientAgent { constructor() { this.description = { displayName: 'A2A Client Agent', name: 'a2aClientAgent', icon: 'file:a2a-client.svg', group: ['transform'], version: 1, subtitle: '={{$parameter["operation"]}}', description: 'Send tasks to A2A (Agent-to-Agent) servers', defaults: { name: 'A2A Client Agent', }, inputs: ["main" /* NodeConnectionType.Main */], outputs: ["main" /* NodeConnectionType.Main */], credentials: [], properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Send Task', value: 'sendTask', description: 'Send a task to an A2A agent server', action: 'Send task to A2A server', }, { name: 'Check Task Status', value: 'checkStatus', description: 'Check the status of a submitted task', action: 'Check task status', }, { name: 'Get Agent Capabilities', value: 'getCapabilities', description: 'Retrieve agent capabilities from A2A server', action: 'Get agent capabilities', }, ], default: 'sendTask', }, // Registry Configuration { displayName: 'Registry URL', name: 'registryUrl', type: 'string', default: (0, urlUtils_1.getDefaultRegistryUrl)(), placeholder: (0, urlUtils_1.getDefaultRegistryUrl)(), description: 'URL of the A2A registry for agent discovery', required: true, }, // Agent Selection { displayName: 'Available Agents', name: 'selectedAgent', type: 'options', typeOptions: { loadOptionsMethod: 'getAvailableAgents', }, default: 'manual', description: 'Select an A2A agent from the registry or choose manual entry', required: true, }, // Manual Agent Configuration (when 'manual' is selected) { displayName: 'Agent Server URL', name: 'agentServerUrl', type: 'string', default: '', placeholder: 'Auto-detected based on your n8n instance', description: 'URL of the A2A agent server (leave empty for auto-detection)', required: false, displayOptions: { show: { selectedAgent: ['manual'], }, }, }, { displayName: 'Agent Server Port', name: 'agentServerPort', type: 'number', default: urlUtils_1.DEFAULT_A2A_PORTS.AGENTS.TRANSLATOR, description: 'Port of the A2A agent server', required: true, displayOptions: { show: { selectedAgent: ['manual'], agentServerUrl: [''], }, }, }, // Task Configuration { displayName: 'Task Type', name: 'taskType', type: 'string', default: 'general', description: 'Type of task to send to the agent', displayOptions: { show: { operation: ['sendTask'], }, }, }, { displayName: 'Task Description', name: 'taskDescription', type: 'string', default: 'Task sent from N8N workflow', description: 'Description of the task', displayOptions: { show: { operation: ['sendTask'], }, }, }, { displayName: 'Task Data', name: 'taskData', type: 'json', default: '{\n "message": "Hello from N8N",\n "data": "sample data"\n}', description: 'JSON data to send with the task', displayOptions: { show: { operation: ['sendTask'], }, }, }, { displayName: 'Processing Mode', name: 'processingMode', type: 'options', options: [ { name: 'Synchronous', value: 'sync', description: 'Wait for task completion and get results immediately', }, { name: 'Asynchronous', value: 'async', description: 'Submit task and get task ID for later status checking', }, ], default: 'sync', description: 'How to process the task', displayOptions: { show: { operation: ['sendTask'], }, }, }, // Status Check Configuration { displayName: 'Task ID', name: 'taskId', type: 'string', default: '', placeholder: 'task_1234567890_abcdef', description: 'ID of the task to check status for', required: true, displayOptions: { show: { operation: ['checkStatus'], }, }, }, // Advanced Options { displayName: 'Advanced Options', name: 'advancedOptions', type: 'collection', placeholder: 'Add Option', default: {}, options: [ { displayName: 'Client ID', name: 'clientId', type: 'string', default: 'n8n-client', description: 'Identifier for this client', }, { displayName: 'Session ID', name: 'sessionId', type: 'string', default: '', description: 'Session ID for task correlation (auto-generated if empty)', }, { displayName: 'Correlation ID', name: 'correlationId', type: 'string', default: '', description: 'Correlation ID for task tracking (auto-generated if empty)', }, { displayName: 'Timeout (seconds)', name: 'timeout', type: 'number', default: 30, description: 'Timeout for the request in seconds', }, { displayName: 'Required Capabilities', name: 'requiredCapabilities', type: 'string', default: '', placeholder: 'general_processing,workflow_orchestration', description: 'Comma-separated list of required agent capabilities', displayOptions: { show: { '/operation': ['sendTask'], }, }, }, ], }, ], }; this.methods = { loadOptions: { async getAvailableAgents() { // Get registry URL from parameters const registryUrl = this.getNodeParameter('registryUrl'); // Ensure proper URL formatting (remove trailing slash if present) const cleanRegistryUrl = registryUrl.replace(/\/+$/, ''); const discoveryUrl = `${cleanRegistryUrl}/v1/agents/discover`; try { const response = await this.helpers.request({ method: 'GET', url: discoveryUrl, json: true, timeout: 10000, // 10 second timeout }); const agents = response.agents || []; // Add manual option first const options = [{ name: '🔧 Manual Configuration', value: 'manual', description: 'Manually specify agent server details', }]; // Add separator if (agents.length > 0) { options.push({ name: '📋 Discovered Agents', value: 'separator', description: '──────────────────────', }); } // Add agents from registry agents.forEach((agent) => { var _a, _b; // Handle A2A-compliant structure (capabilities = object, skills = array) const skillCount = ((_a = agent.skills) === null || _a === void 0 ? void 0 : _a.length) || 0; const capabilityKeys = agent.capabilities ? Object.keys(agent.capabilities).length : 0; const statusIcon = agent.status === 'active' ? '🟢' : '🔴'; // Create description from skills (functional abilities) const skillsList = ((_b = agent.skills) === null || _b === void 0 ? void 0 : _b.map((skill) => skill.name || skill.id).join(', ')) || 'No skills listed'; options.push({ name: `${statusIcon} ${agent.name || agent.agent_id} (${skillCount} skills, ${capabilityKeys} capabilities)`, value: JSON.stringify({ agent_id: agent.agent_id, name: agent.name, endpoint: agent.endpoint, capabilities: agent.capabilities, skills: agent.skills, status: agent.status, }), description: `${agent.endpoint} - Skills: ${skillsList}`, }); }); if (agents.length === 0) { options.push({ name: '⚠️ No agents discovered', value: 'none', description: 'No active agents found in registry', }); } return options; } catch (error) { // Fallback if registry is not available return [ { name: '🔧 Manual Configuration', value: 'manual', description: 'Manually specify agent server details', }, { name: '❌ Registry Unavailable', value: 'registry_error', description: `Cannot connect to registry at ${discoveryUrl}: ${error.message}`, } ]; } }, }, }; } async execute() { const items = this.getInputData(); const returnData = []; for (let i = 0; i < items.length; i++) { try { const operation = this.getNodeParameter('operation', i); const advancedOptions = this.getNodeParameter('advancedOptions', i); const selectedAgent = this.getNodeParameter('selectedAgent', i); // Determine agent server URL let agentServerUrl; let agentInfo = {}; if (selectedAgent === 'manual') { // Manual configuration agentServerUrl = this.getNodeParameter('agentServerUrl', i); if (!agentServerUrl) { const agentServerPort = this.getNodeParameter('agentServerPort', i); agentServerUrl = (0, urlUtils_1.getDefaultAgentUrl)(agentServerPort); } } else if (selectedAgent === 'none' || selectedAgent === 'registry_error') { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No valid agent selected. Please check registry connection or use manual configuration.', { itemIndex: i }); } else { // Agent selected from registry try { agentInfo = JSON.parse(selectedAgent); // Use the endpoint without trailing slash to avoid double slashes agentServerUrl = agentInfo.endpoint.replace(/\/+$/, ''); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid agent selection: ${error.message}`, { itemIndex: i }); } } const timeout = (advancedOptions.timeout || 30) * 1000; // Convert to milliseconds const clientId = advancedOptions.clientId || 'n8n-client'; const sessionId = advancedOptions.sessionId || `session_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; const correlationId = advancedOptions.correlationId || `corr_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; if (operation === 'sendTask') { const taskType = this.getNodeParameter('taskType', i); const taskDescription = this.getNodeParameter('taskDescription', i); const taskDataStr = this.getNodeParameter('taskData', i); const processingMode = this.getNodeParameter('processingMode', i); const requiredCapabilitiesStr = advancedOptions.requiredCapabilities || ''; let taskData; try { taskData = JSON.parse(taskDataStr); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid JSON in task data: ${error.message}`, { itemIndex: i }); } const requiredCapabilities = requiredCapabilitiesStr ? requiredCapabilitiesStr.split(',').map((cap) => cap.trim()).filter(Boolean) : []; // Generate unique request ID for JSON-RPC 2.0 const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; // Prepare JSON-RPC 2.0 task payload const taskPayload = { jsonrpc: "2.0", method: "submitTask", params: Object.assign(Object.assign({ type: taskType, description: taskDescription }, taskData), { required_capabilities: requiredCapabilities, context: { source: 'n8n_workflow', client_id: clientId, workflow_item_index: i, original_input: items[i].json, selected_agent: agentInfo, }, processing_mode: processingMode }), id: requestId, }; // Prepare headers const headers = { 'Content-Type': 'application/json', 'X-A2A-Client-ID': clientId, 'X-A2A-Session-ID': sessionId, 'X-A2A-Correlation-ID': correlationId, 'X-A2A-Processing-Mode': processingMode, 'User-Agent': 'n8n-a2a-client/1.0', }; // Send task to agent server const taskUrl = `${agentServerUrl}/tasks`; const response = await fetch(taskUrl, { method: 'POST', headers, body: JSON.stringify(taskPayload), }); // Check if response is JSON const contentType = response.headers.get('content-type'); let responseData; if (contentType && contentType.includes('application/json')) { responseData = await response.json(); } else { const textResponse = await response.text(); responseData = { error: 'Non-JSON response', content_type: contentType, response_text: textResponse }; } // Handle JSON-RPC 2.0 response format if (response.ok && responseData.jsonrpc === "2.0") { if (responseData.result) { // Successful JSON-RPC 2.0 response const result = responseData.result; returnData.push({ json: Object.assign(Object.assign(Object.assign({}, items[i].json), { a2a_success: true, a2a_request_id: requestId, a2a_response_id: responseData.id, a2a_task_id: result.task_id, a2a_session_id: result.session_id, a2a_correlation_id: result.correlation_id, a2a_status: result.status, a2a_processing_mode: result.processing_mode, a2a_response: responseData, a2a_agent_url: agentServerUrl, a2a_agent_info: agentInfo, a2a_sent_at: new Date().toISOString() }), (processingMode === 'sync' && result.workflow_result && { a2a_result: result.workflow_result, a2a_completed_at: result.completed_at, })), }); } else if (responseData.error) { // JSON-RPC 2.0 error response returnData.push({ json: Object.assign(Object.assign({}, items[i].json), { a2a_success: false, a2a_request_id: requestId, a2a_response_id: responseData.id, a2a_error: `JSON-RPC Error ${responseData.error.code}: ${responseData.error.message}`, a2a_error_data: responseData.error.data, a2a_response: responseData, a2a_agent_url: agentServerUrl, a2a_agent_info: agentInfo, a2a_failed_at: new Date().toISOString() }), }); } } else { // Non-JSON-RPC 2.0 or error response returnData.push({ json: Object.assign(Object.assign({}, items[i].json), { a2a_success: false, a2a_request_id: requestId, a2a_error: response.ok ? 'Invalid JSON-RPC 2.0 response format' : `HTTP ${response.status}: ${responseData.message || responseData.error || 'Unknown error'}`, a2a_response: responseData, a2a_agent_url: agentServerUrl, a2a_agent_info: agentInfo, a2a_failed_at: new Date().toISOString() }), }); } } else if (operation === 'checkStatus') { const taskId = this.getNodeParameter('taskId', i); const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; // JSON-RPC 2.0 request for status check const statusPayload = { jsonrpc: "2.0", method: "getTaskStatus", params: { task_id: taskId }, id: requestId, }; // Check task status with JSON-RPC 2.0 const response = await fetch(`${agentServerUrl}/tasks`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-A2A-Client-ID': clientId, 'User-Agent': 'n8n-a2a-client/1.0', }, body: JSON.stringify(statusPayload), }); const responseData = await response.json(); // Handle JSON-RPC 2.0 status response if (response.ok && responseData.jsonrpc === "2.0") { if (responseData.result) { const result = responseData.result; returnData.push({ json: Object.assign(Object.assign(Object.assign(Object.assign({}, items[i].json), { a2a_success: true, a2a_request_id: requestId, a2a_response_id: responseData.id, a2a_task_id: result.task_id, a2a_status: result.status, a2a_response: responseData, a2a_agent_url: agentServerUrl, a2a_agent_info: agentInfo, a2a_checked_at: new Date().toISOString() }), (result.workflow_result && { a2a_result: result.workflow_result, })), (result.completed_at && { a2a_completed_at: result.completed_at, })), }); } else if (responseData.error) { returnData.push({ json: Object.assign(Object.assign({}, items[i].json), { a2a_success: false, a2a_request_id: requestId, a2a_response_id: responseData.id, a2a_error: `JSON-RPC Error ${responseData.error.code}: ${responseData.error.message}`, a2a_error_data: responseData.error.data, a2a_response: responseData, a2a_agent_url: agentServerUrl, a2a_agent_info: agentInfo, a2a_task_id: taskId, a2a_failed_at: new Date().toISOString() }), }); } } else { returnData.push({ json: Object.assign(Object.assign({}, items[i].json), { a2a_success: false, a2a_request_id: requestId, a2a_error: response.ok ? 'Invalid JSON-RPC 2.0 response format' : `HTTP ${response.status}: ${responseData.message || responseData.error || 'Unknown error'}`, a2a_response: responseData, a2a_agent_url: agentServerUrl, a2a_agent_info: agentInfo, a2a_task_id: taskId, a2a_failed_at: new Date().toISOString() }), }); } } else if (operation === 'getCapabilities') { const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; // JSON-RPC 2.0 request for capabilities const capabilitiesPayload = { jsonrpc: "2.0", method: "getCapabilities", params: {}, id: requestId, }; // Get agent capabilities with JSON-RPC 2.0 const response = await fetch(`${agentServerUrl}/tasks`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-A2A-Client-ID': clientId, 'User-Agent': 'n8n-a2a-client/1.0', }, body: JSON.stringify(capabilitiesPayload), }); const responseData = await response.json(); // Handle JSON-RPC 2.0 capabilities response if (response.ok && responseData.jsonrpc === "2.0") { if (responseData.result) { const result = responseData.result; returnData.push({ json: Object.assign(Object.assign({}, items[i].json), { a2a_success: true, a2a_request_id: requestId, a2a_response_id: responseData.id, a2a_agent_id: result.agent_id, a2a_capabilities: result.capabilities, a2a_supported_protocols: result.supported_protocols, a2a_supported_modalities: result.supported_modalities, a2a_processing_modes: result.processing_modes, a2a_response: responseData, a2a_agent_url: agentServerUrl, a2a_agent_info: agentInfo, a2a_retrieved_at: new Date().toISOString() }), }); } else if (responseData.error) { returnData.push({ json: Object.assign(Object.assign({}, items[i].json), { a2a_success: false, a2a_request_id: requestId, a2a_response_id: responseData.id, a2a_error: `JSON-RPC Error ${responseData.error.code}: ${responseData.error.message}`, a2a_error_data: responseData.error.data, a2a_response: responseData, a2a_agent_url: agentServerUrl, a2a_agent_info: agentInfo, a2a_failed_at: new Date().toISOString() }), }); } } else { returnData.push({ json: Object.assign(Object.assign({}, items[i].json), { a2a_success: false, a2a_request_id: requestId, a2a_error: response.ok ? 'Invalid JSON-RPC 2.0 response format' : `HTTP ${response.status}: ${responseData.message || responseData.error || 'Unknown error'}`, a2a_response: responseData, a2a_agent_url: agentServerUrl, a2a_agent_info: agentInfo, a2a_failed_at: new Date().toISOString() }), }); } } } catch (error) { returnData.push({ json: Object.assign(Object.assign({}, items[i].json), { a2a_success: false, a2a_error: error.message, a2a_operation: this.getNodeParameter('operation', i), a2a_error_timestamp: new Date().toISOString() }), }); } } return [returnData]; } } exports.A2AClientAgent = A2AClientAgent;