n8n-nodes-a2a-protocol
Version:
Agent2Agent (A2A) Protocol nodes for n8n - Enable agent interoperability, communication, and MCP integration
528 lines (527 loc) • 29 kB
JavaScript
"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;