n8n-nodes-a2a-protocol
Version:
Agent2Agent (A2A) Protocol nodes for n8n - Enable agent interoperability, communication, and MCP integration
892 lines (878 loc) • 32.5 kB
JavaScript
;
/**
* N8N A2A Protocol - URL Utilities
* Provides dynamic URL detection for A2A nodes based on n8n instance configuration
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PREDEFINED_AGENTS = exports.DEFAULT_A2A_PORTS = void 0;
exports.storeActiveServer = storeActiveServer;
exports.getActiveServer = getActiveServer;
exports.stopPreviousServerInstance = stopPreviousServerInstance;
exports.cleanupActiveServer = cleanupActiveServer;
exports.listActiveServers = listActiveServers;
exports.isPortAvailable = isPortAvailable;
exports.findNextAvailablePort = findNextAvailablePort;
exports.checkA2APortsAvailability = checkA2APortsAvailability;
exports.generatePortErrorMessage = generatePortErrorMessage;
exports.getAvailableRegistryPort = getAvailableRegistryPort;
exports.getAvailableAgentPort = getAvailableAgentPort;
exports.checkPredefinedAgentsAvailability = checkPredefinedAgentsAvailability;
exports.getRegistryPlaceholder = getRegistryPlaceholder;
exports.getAgentPlaceholder = getAgentPlaceholder;
exports.detectInstanceUrl = detectInstanceUrl;
exports.buildUrl = buildUrl;
exports.getDefaultAgentUrl = getDefaultAgentUrl;
exports.getDefaultCallbackUrl = getDefaultCallbackUrl;
exports.getDefaultRegistryUrl = getDefaultRegistryUrl;
exports.createAgentEndpoints = createAgentEndpoints;
exports.logUrlConfiguration = logUrlConfiguration;
exports.detectRuntimeUrl = detectRuntimeUrl;
exports.detectInstanceUrlEnhanced = detectInstanceUrlEnhanced;
exports.getPortProcessInfo = getPortProcessInfo;
exports.killProcess = killProcess;
exports.handlePortConflict = handlePortConflict;
exports.validateNodePort = validateNodePort;
exports.validateWorkflowPorts = validateWorkflowPorts;
const net_1 = require("net");
/**
* Default A2A port configurations
*/
exports.DEFAULT_A2A_PORTS = {
REGISTRY: 8080,
AGENTS: {
TRANSLATOR: 7000,
CALCULATOR: 7001,
CODING: 8000,
ORCHESTRATOR: 9000,
}
};
/**
* Predefined A2A agent configurations
*/
exports.PREDEFINED_AGENTS = [
{ port: exports.DEFAULT_A2A_PORTS.AGENTS.TRANSLATOR, name: 'Translator Agent', id: 'translator-agent' },
{ port: exports.DEFAULT_A2A_PORTS.AGENTS.CALCULATOR, name: 'Calculator Agent', id: 'calculator-agent' },
{ port: exports.DEFAULT_A2A_PORTS.AGENTS.CODING, name: 'Coding Agent', id: 'coding-agent' },
{ port: exports.DEFAULT_A2A_PORTS.AGENTS.ORCHESTRATOR, name: 'Orchestrator Agent', id: 'orchestrator-agent' },
];
/**
* Global A2A Server Registry using UUID-based server management
* ⚠️ IMPROVED: Using UUID for unique server identification and proper cleanup
*/
const A2A_SERVER_REGISTRY = {}; // Server instances by UUID
/**
* Global storage for A2A node-to-server mapping
* Maps nodeId to serverId for proper lifecycle management
*/
const A2A_NODE_SERVERS = new Map();
/**
* Generate unique server ID
*/
function generateServerId() {
return `server_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
/**
* Store an A2A server using UUID-based registry pattern
* ⚠️ IMPROVED: Following your UUID server registry approach
*/
function storeActiveServer(nodeId, server, port, serviceType) {
// Close any existing server for this node first
const existing = A2A_NODE_SERVERS.get(nodeId);
if (existing) {
closeServerById(existing.serverId);
}
// Generate unique server identifier
const serverId = generateServerId();
// Store server in registry by serverId
A2A_SERVER_REGISTRY[serverId] = server;
// Map nodeId to serverId
A2A_NODE_SERVERS.set(nodeId, {
serverId,
port,
serviceType,
startedAt: new Date().toISOString(),
});
return serverId;
}
/**
* Close a server by ID (following your approach)
*/
function closeServerById(serverId) {
const server = A2A_SERVER_REGISTRY[serverId];
if (!server) {
return false;
}
try {
server.close(() => {
delete A2A_SERVER_REGISTRY[serverId];
});
return true;
}
catch (error) {
delete A2A_SERVER_REGISTRY[serverId];
return false;
}
}
/**
* Get stored server info for a node (using new registry pattern)
*/
function getActiveServer(nodeId) {
return A2A_NODE_SERVERS.get(nodeId) || null;
}
/**
* Stop previous server instance for a node (using UUID registry pattern)
* ⚠️ IMPROVED: Uses your server registry approach for safe closure
*/
async function stopPreviousServerInstance(nodeId) {
const nodeInfo = A2A_NODE_SERVERS.get(nodeId);
if (!nodeInfo) {
return {
stopped: false,
message: `No previous server instance found for node ${nodeId}`
};
}
const success = closeServerById(nodeInfo.serverId);
A2A_NODE_SERVERS.delete(nodeId);
if (success) {
// Wait a moment for the port to be released
await new Promise(resolve => setTimeout(resolve, 100));
return {
stopped: true,
message: `Successfully stopped previous ${nodeInfo.serviceType} server on port ${nodeInfo.port}`
};
}
else {
return {
stopped: false,
message: `Failed to stop previous server`
};
}
}
/**
* Clean up stored server reference (using UUID registry pattern)
* ⚠️ IMPROVED: Uses your server registry approach for safe cleanup
*/
function cleanupActiveServer(nodeId) {
const nodeInfo = A2A_NODE_SERVERS.get(nodeId);
if (nodeInfo) {
closeServerById(nodeInfo.serverId);
A2A_NODE_SERVERS.delete(nodeId);
}
}
/**
* List all stored active servers (using new registry pattern)
*/
function listActiveServers() {
return Array.from(A2A_NODE_SERVERS.entries()).map(([nodeId, serverInfo]) => ({
nodeId,
port: serverInfo.port,
serviceType: serverInfo.serviceType,
startedAt: serverInfo.startedAt,
serverId: serverInfo.serverId
}));
}
/**
* Check if a specific port is available (both IPv4 and IPv6)
*/
async function isPortAvailable(port, host = 'localhost') {
// Check IPv4
const ipv4Available = await new Promise((resolve) => {
const server = (0, net_1.createServer)();
server.listen(port, host, () => {
server.close(() => {
resolve(true);
});
});
server.on('error', () => {
resolve(false);
});
});
if (!ipv4Available) {
return false;
}
// Check IPv6 (which is what's causing the EADDRINUSE error)
const ipv6Available = await new Promise((resolve) => {
const server = (0, net_1.createServer)();
// Try to bind to IPv6 specifically
server.listen(port, '::', () => {
server.close(() => {
resolve(true);
});
});
server.on('error', () => {
resolve(false);
});
});
return ipv4Available && ipv6Available;
}
/**
* Find the next available port starting from a given port
*/
async function findNextAvailablePort(startPort, maxTries = 10) {
for (let i = 0; i < maxTries; i++) {
const port = startPort + i;
if (await isPortAvailable(port)) {
return port;
}
}
return null;
}
/**
* Check if all required A2A ports are available
*/
async function checkA2APortsAvailability() {
const conflicts = [];
const suggestions = [];
// Check registry port
const registryAvailable = await isPortAvailable(exports.DEFAULT_A2A_PORTS.REGISTRY);
if (!registryAvailable) {
conflicts.push({ port: exports.DEFAULT_A2A_PORTS.REGISTRY, service: 'A2A Registry' });
const altRegistryPort = await findNextAvailablePort(exports.DEFAULT_A2A_PORTS.REGISTRY + 1);
if (altRegistryPort !== null) {
suggestions.push({ port: altRegistryPort, service: 'A2A Registry (Alternative)' });
}
}
// Check agent ports
for (const agent of exports.PREDEFINED_AGENTS) {
const agentAvailable = await isPortAvailable(agent.port);
if (!agentAvailable) {
conflicts.push({ port: agent.port, service: `A2A Agent (${agent.name})` });
const altAgentPort = await findNextAvailablePort(agent.port + 100);
if (altAgentPort !== null) {
suggestions.push({ port: altAgentPort, service: `${agent.name} (Alternative)` });
}
}
}
return {
available: conflicts.length === 0,
conflicts,
suggestions,
};
}
/**
* Generate port availability error message for users
*/
function generatePortErrorMessage(conflicts) {
if (conflicts.length === 0)
return '';
const conflictList = conflicts.map(c => ` • Port ${c.port} (${c.service})`).join('\n');
return `❌ A2A Port Conflicts Detected:
${conflictList}
🔧 Solutions:
1. Stop processes using these ports
2. Use different ports in your A2A configuration
3. Check for duplicate A2A instances running
💡 To find processes using these ports:
Windows: netstat -ano | findstr :<PORT>
Linux/Mac: lsof -i :<PORT>
Please resolve port conflicts before starting A2A servers.`;
}
/**
* Gets an available port for A2A registry with fallback
*/
async function getAvailableRegistryPort(preferredPort = exports.DEFAULT_A2A_PORTS.REGISTRY) {
if (await isPortAvailable(preferredPort)) {
return preferredPort;
}
// Try common registry ports
const commonRegistryPorts = [8080, 8081, 8082, 8083, 8090, 9080];
for (const port of commonRegistryPorts) {
if (await isPortAvailable(port)) {
return port;
}
}
// Find any available port starting from 8080
const availablePort = await findNextAvailablePort(8080);
if (availablePort === null) {
throw new Error('No available ports found for A2A registry');
}
return availablePort;
}
/**
* Gets an available port for A2A agent with fallback
*/
async function getAvailableAgentPort(preferredPort = exports.DEFAULT_A2A_PORTS.AGENTS.TRANSLATOR) {
if (await isPortAvailable(preferredPort)) {
return preferredPort;
}
// Find next available port in the 7000-9000 range
const availablePort = await findNextAvailablePort(7000, 100);
if (availablePort === null) {
throw new Error('No available ports found for A2A agent');
}
return availablePort;
}
/**
* Checks availability of predefined A2A agent ports
*/
async function checkPredefinedAgentsAvailability() {
const agents = [...exports.PREDEFINED_AGENTS];
for (const agent of agents) {
agent.available = await isPortAvailable(agent.port);
}
return agents;
}
/**
* Gets placeholder URLs using dynamic configuration
*/
function getRegistryPlaceholder() {
const config = detectInstanceUrl();
return buildUrl(config, exports.DEFAULT_A2A_PORTS.REGISTRY);
}
function getAgentPlaceholder() {
const config = detectInstanceUrl();
return buildUrl(config, exports.DEFAULT_A2A_PORTS.AGENTS.TRANSLATOR);
}
/**
* Detects the current n8n instance URL configuration
* Priority order:
* 1. N8N standard environment variables (WEBHOOK_URL, N8N_EDITOR_BASE_URL, etc.)
* 2. N8N configuration variables (N8N_HOST, N8N_PROTOCOL, N8N_PORT)
* 3. Runtime detection from request context (if available)
* 4. Common deployment environment variables
* 5. SSL/TLS detection
* 6. Default to localhost
*/
function detectInstanceUrl() {
// 1. Check for standard n8n webhook URL (most reliable)
if (process.env.WEBHOOK_URL) {
try {
const url = new URL(process.env.WEBHOOK_URL);
return {
protocol: url.protocol.slice(0, -1),
hostname: url.hostname,
port: url.port ? parseInt(url.port, 10) : undefined,
};
}
catch (error) {
console.warn('⚠️ Invalid WEBHOOK_URL format:', process.env.WEBHOOK_URL);
}
}
// 2. Check for n8n editor base URL
if (process.env.N8N_EDITOR_BASE_URL) {
try {
const url = new URL(process.env.N8N_EDITOR_BASE_URL);
return {
protocol: url.protocol.slice(0, -1),
hostname: url.hostname,
port: url.port ? parseInt(url.port, 10) : undefined,
};
}
catch (error) {
console.warn('⚠️ Invalid N8N_EDITOR_BASE_URL format:', process.env.N8N_EDITOR_BASE_URL);
}
}
// 3. Check for n8n webhook tunnel URL
if (process.env.WEBHOOK_TUNNEL_URL) {
try {
const url = new URL(process.env.WEBHOOK_TUNNEL_URL);
return {
protocol: url.protocol.slice(0, -1),
hostname: url.hostname,
port: url.port ? parseInt(url.port, 10) : undefined,
};
}
catch (error) {
console.warn('⚠️ Invalid WEBHOOK_TUNNEL_URL format:', process.env.WEBHOOK_TUNNEL_URL);
}
}
// 4. Check for explicit n8n host configuration
if (process.env.N8N_HOST) {
return {
protocol: process.env.N8N_PROTOCOL || detectProtocol(),
hostname: process.env.N8N_HOST,
port: process.env.N8N_PORT ? parseInt(process.env.N8N_PORT, 10) : undefined,
};
}
// 5. Try to detect from browser/request context (when available)
// This would work when nodes are executed in response to HTTP requests
if (typeof window !== 'undefined' && window.location) {
return {
protocol: window.location.protocol.slice(0, -1),
hostname: window.location.hostname,
port: window.location.port ? parseInt(window.location.port, 10) : undefined,
};
}
// 6. Check for common deployment environment variables
const commonHostVars = [
'PUBLIC_URL',
'BASE_URL',
'CANONICAL_HOST',
'VIRTUAL_HOST',
'HOST',
'HOSTNAME',
'SERVER_NAME',
'DOMAIN',
];
for (const envVar of commonHostVars) {
const value = process.env[envVar];
if (value) {
try {
// Try to parse as URL first
if (value.startsWith('http')) {
const url = new URL(value);
return {
protocol: url.protocol.slice(0, -1),
hostname: url.hostname,
port: url.port ? parseInt(url.port, 10) : undefined,
};
}
else {
// Treat as hostname
return {
protocol: detectProtocol(),
hostname: value,
};
}
}
catch (error) {
console.warn(`⚠️ Invalid ${envVar} format:`, value);
}
}
}
// 7. Default to localhost with protocol detection
return {
protocol: detectProtocol(),
hostname: 'localhost',
};
}
/**
* Detects the protocol (http/https) based on various indicators
*/
function detectProtocol() {
// Check for n8n SSL configuration
if (process.env.N8N_SSL_KEY && process.env.N8N_SSL_CERT) {
return 'https';
}
// Check for explicit SSL/TLS indicators
const httpsIndicators = [
process.env.N8N_PROTOCOL === 'https',
process.env.SSL_ENABLED === 'true',
process.env.HTTPS === 'true',
process.env.TLS === 'true',
process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1',
// Check if running on standard HTTPS port
process.env.PORT === '443' || process.env.N8N_PORT === '443',
];
return httpsIndicators.some(indicator => indicator) ? 'https' : 'http';
}
/**
* Builds a complete URL from configuration and path
*/
function buildUrl(config, port, path = '') {
const { protocol, hostname } = config;
// Smart port handling
let portStr = '';
if (config.port) {
// Use explicit port from config
portStr = `:${config.port}`;
}
else if (port !== 80 && port !== 443) {
// Use provided port unless it's standard HTTP/HTTPS port
if (!(protocol === 'https' && port === 443) && !(protocol === 'http' && port === 80)) {
portStr = `:${port}`;
}
}
const cleanPath = path.startsWith('/') ? path : `/${path}`;
return `${protocol}://${hostname}${portStr}${cleanPath}`;
}
/**
* Gets the default URL for A2A agent services
*/
function getDefaultAgentUrl(port) {
const config = detectInstanceUrl();
return buildUrl(config, port);
}
/**
* Gets the default callback URL pattern for A2A callbacks
*/
function getDefaultCallbackUrl(port) {
const config = detectInstanceUrl();
return buildUrl(config, port, '/tasks/TASK_ID/status');
}
/**
* Gets the default registry URL
*/
function getDefaultRegistryUrl(port = exports.DEFAULT_A2A_PORTS.REGISTRY) {
const config = detectInstanceUrl();
return buildUrl(config, port);
}
/**
* Creates endpoint URLs for an agent
*/
function createAgentEndpoints(port) {
const baseUrl = getDefaultAgentUrl(port);
// Remove trailing slash to avoid double slashes
const cleanBaseUrl = baseUrl.replace(/\/+$/, '');
return {
endpoint: baseUrl,
tasks: `${cleanBaseUrl}/tasks`,
health: `${cleanBaseUrl}/health`,
capabilities: `${cleanBaseUrl}/capabilities`,
};
}
/**
* Logs the detected URL configuration for debugging
*/
function logUrlConfiguration(port) {
const config = detectInstanceUrl();
const baseUrl = buildUrl(config, port);
console.log('🌐 A2A URL Configuration:');
console.log(` Protocol: ${config.protocol}`);
console.log(` Hostname: ${config.hostname}`);
console.log(` Port: ${config.port || port}`);
console.log(` Base URL: ${baseUrl}`);
console.log(` Detection: ${getDetectionSource()}`);
}
function getDetectionSource() {
// Check in same order as detectInstanceUrl()
if (process.env.WEBHOOK_URL)
return 'WEBHOOK_URL (n8n standard)';
if (process.env.N8N_EDITOR_BASE_URL)
return 'N8N_EDITOR_BASE_URL (n8n standard)';
if (process.env.WEBHOOK_TUNNEL_URL)
return 'WEBHOOK_TUNNEL_URL (n8n standard)';
if (process.env.N8N_HOST)
return 'N8N_HOST environment variable';
if (typeof window !== 'undefined' && window.location) {
return 'Browser location (runtime detection)';
}
const commonHostVars = ['PUBLIC_URL', 'BASE_URL', 'CANONICAL_HOST', 'VIRTUAL_HOST', 'HOST', 'HOSTNAME', 'SERVER_NAME', 'DOMAIN'];
for (const envVar of commonHostVars) {
if (process.env[envVar])
return `${envVar} environment variable`;
}
return 'Default localhost fallback';
}
/**
* Attempts to detect the current n8n instance URL from runtime context
* This can be called from within node execution to get more accurate detection
*/
function detectRuntimeUrl() {
// Try to detect from various runtime sources
// 1. Check if we're in a browser context
if (typeof window !== 'undefined' && window.location) {
return {
protocol: window.location.protocol.slice(0, -1),
hostname: window.location.hostname,
port: window.location.port ? parseInt(window.location.port, 10) : undefined,
};
}
// 2. Check if we can access request headers (in Express context)
try {
// This would work if we're in an HTTP request context
// Note: This is a conceptual approach - actual implementation would depend on n8n's internal structure
if (global.req && global.req.headers) {
const req = global.req;
const host = req.headers.host;
const protocol = req.headers['x-forwarded-proto'] || (req.secure ? 'https' : 'http');
if (host) {
const [hostname, port] = host.split(':');
return {
protocol: protocol,
hostname,
port: port ? parseInt(port, 10) : undefined,
};
}
}
}
catch (error) {
// Runtime detection failed, that's ok
}
return null;
}
/**
* Enhanced URL detection that tries runtime detection first, then falls back to environment
*/
function detectInstanceUrlEnhanced() {
// Try runtime detection first
const runtimeConfig = detectRuntimeUrl();
if (runtimeConfig) {
return runtimeConfig;
}
// Fall back to environment-based detection
return detectInstanceUrl();
}
/**
* Check if a port is being used by an A2A service and get process info
*/
async function getPortProcessInfo(port) {
return new Promise((resolve) => {
const { exec } = require('child_process');
// Use netstat to find the process using the port
exec(`netstat -ano | findstr :${port}`, (error, stdout) => {
if (error || !stdout) {
resolve({ inUse: false });
return;
}
// Parse netstat output to get PID
const lines = stdout.split('\n').filter(line => line.includes('LISTENING'));
if (lines.length === 0) {
resolve({ inUse: false });
return;
}
// Extract PID from the last column
const pidMatch = lines[0].trim().split(/\s+/).pop();
const pid = pidMatch ? parseInt(pidMatch, 10) : undefined;
if (!pid) {
resolve({ inUse: true, pid: undefined });
return;
}
// Check what process is running with this PID
exec(`tasklist /FI "PID eq ${pid}" /FO CSV`, (error, stdout) => {
if (error) {
resolve({ inUse: true, pid });
return;
}
const isNodeProcess = stdout.includes('node.exe');
if (!isNodeProcess) {
resolve({
inUse: true,
pid,
isA2AService: false,
serviceType: 'other',
canRestart: false
});
return;
}
// For Node.js processes, we assume they might be A2A services
// We'll check by trying to hit the health endpoint
checkA2AServiceType(port).then(serviceType => {
resolve({
inUse: true,
pid,
isA2AService: serviceType !== 'other',
serviceType,
canRestart: serviceType === 'registry' || serviceType === 'agent'
});
}).catch(() => {
resolve({
inUse: true,
pid,
isA2AService: false,
serviceType: 'other',
canRestart: false
});
});
});
});
});
}
/**
* Check if a service on a port is an A2A service and what type
*/
async function checkA2AServiceType(port) {
try {
// Try to hit the health endpoint
const response = await fetch(`http://localhost:${port}/health`, {
signal: AbortSignal.timeout(2000)
});
if (response.ok) {
const data = await response.json();
// Check for A2A Registry indicators
if (data.registry_name || data.total_agents !== undefined) {
return 'registry';
}
// Check for A2A Agent indicators
if (data.agent_id || data.processing_mode) {
return 'agent';
}
}
return 'other';
}
catch (error) {
return 'other';
}
}
/**
* Kill a process by PID (Windows-specific)
*/
async function killProcess(pid) {
return new Promise((resolve) => {
const { exec } = require('child_process');
exec(`taskkill /PID ${pid} /F`, (error) => {
if (error) {
console.error(`Failed to kill process ${pid}:`, error.message);
resolve(false);
}
else {
console.log(`✅ Successfully killed process ${pid}`);
resolve(true);
}
});
});
}
/**
* Smart port management - kills A2A services for restart, errors for conflicts
*/
async function handlePortConflict(port, serviceType, serviceName) {
const processInfo = await getPortProcessInfo(port);
if (!processInfo.inUse) {
return {
canProceed: true,
message: `Port ${port} is available`,
action: 'proceed'
};
}
// If it's an A2A service of the same type, kill it and restart
if (processInfo.isA2AService && processInfo.serviceType === serviceType && processInfo.pid) {
console.log(`🔄 ${serviceName}: Detected existing ${serviceType} on port ${port} (PID: ${processInfo.pid})`);
console.log(`🔄 ${serviceName}: Killing previous instance to restart...`);
const killed = await killProcess(processInfo.pid);
if (killed) {
// Wait a moment for the port to be released
await new Promise(resolve => setTimeout(resolve, 1000));
return {
canProceed: true,
message: `Killed previous ${serviceType} instance and restarting on port ${port}`,
action: 'killed_and_restart'
};
}
else {
return {
canProceed: false,
message: `Failed to kill previous ${serviceType} instance on port ${port}`,
action: 'error'
};
}
}
// If it's a different type of service, show conflict error
const alternativePort = await findNextAvailablePort(port + 1, 10);
const conflicts = [{ port, service: serviceName }];
const errorMessage = generatePortErrorMessage(conflicts);
let suggestionMessage = '';
if (alternativePort) {
suggestionMessage = `\n💡 Suggested alternative: Port ${alternativePort} is available.\n\nTo fix this:\n1. Change the port to ${alternativePort}\n2. Or stop the process using port ${port}`;
}
else {
suggestionMessage = `\n💡 No nearby ports available. Try ports in different ranges.`;
}
return {
canProceed: false,
message: `${errorMessage}${suggestionMessage}`,
action: 'error'
};
}
/**
* Validate port availability and show user-friendly errors for workflow validation
* ⚠️ ENHANCED: Performs comprehensive validation that prevents race conditions
*/
async function validateNodePort(port, serviceType, serviceName, nodeId) {
// STEP 1: Check if this exact node already has a server on this port
const storedServer = getActiveServer(nodeId);
if (storedServer && storedServer.port === port) {
return { isValid: true }; // Same node will replace itself
}
// STEP 2: Check if ANY other A2A server is using this port
const allActiveServers = listActiveServers();
const conflictingServer = allActiveServers.find(server => server.port === port && server.nodeId !== nodeId);
if (conflictingServer) {
const alternativePort = await findNextAvailablePort(port + 1, 10);
const errorMessage = `🚫 PORT CONFLICT: Cannot activate workflow
❌ CONFLICT DETAILS:
• Node "${serviceName}" wants to use port ${port}
• Port ${port} is already occupied by: ${conflictingServer.nodeId}
• Conflicting service type: ${conflictingServer.serviceType}
• Server ID: ${conflictingServer.serverId}
🔧 SOLUTIONS TO FIX THIS:
1. ✅ Change port ${port} to ${alternativePort || 'different port'} in node "${serviceName}"
2. Remove or disable the conflicting node: ${conflictingServer.nodeId}
3. Use unique ports for each A2A node in your workflow
💡 ACTION REQUIRED: Update the port number in "${serviceName}" to resolve this conflict before activating the workflow.`;
return {
isValid: false,
errorMessage,
suggestedPort: alternativePort !== null ? alternativePort : undefined
};
}
// STEP 3: Check if port is available (not used by external services)
const portAvailable = await isPortAvailable(port);
if (portAvailable) {
return { isValid: true };
}
// STEP 4: Port is in use by external service - check what's using it
const processInfo = await getPortProcessInfo(port);
const alternativePort = await findNextAvailablePort(port + 1, 10);
const serviceTypeText = processInfo.isA2AService ?
`external A2A ${processInfo.serviceType}` :
`external service (PID: ${processInfo.pid})`;
const errorMessage = `🚫 PORT CONFLICT: Cannot activate workflow
❌ CONFLICT DETAILS:
• Node "${serviceName}" wants to use port ${port}
• Port ${port} is already occupied by: ${serviceTypeText}
• Process ID: ${processInfo.pid || 'unknown'}
• Service Type: ${processInfo.isA2AService ? processInfo.serviceType : 'non-A2A'}
🔧 SOLUTIONS TO FIX THIS:
1. ✅ Change port ${port} to ${alternativePort || 'different port'} in node "${serviceName}"
2. Stop the conflicting process: ${processInfo.pid ? `taskkill /PID ${processInfo.pid} /F` : 'identify and stop the service'}
3. Check for duplicate A2A instances running outside n8n
💡 ACTION REQUIRED: Update the port number in "${serviceName}" to resolve this conflict before activating the workflow.`;
return {
isValid: false,
errorMessage,
suggestedPort: alternativePort !== null ? alternativePort : undefined
};
}
/**
* Check for A2A port conflicts across multiple nodes
*/
async function validateWorkflowPorts(nodes) {
const errors = [];
// Check for duplicates within the workflow itself
const portMap = new Map();
for (const node of nodes) {
if (!portMap.has(node.port)) {
portMap.set(node.port, []);
}
portMap.get(node.port).push({
nodeId: node.nodeId,
serviceName: node.serviceName
});
}
// Check for internal conflicts
for (const [port, nodeList] of portMap.entries()) {
if (nodeList.length > 1) {
const conflictNames = nodeList.map(n => n.serviceName).join(', ');
const alternativePort = await findNextAvailablePort(port + 1, 10);
// Add error for all conflicting nodes except the first one
for (let i = 1; i < nodeList.length; i++) {
errors.push({
nodeId: nodeList[i].nodeId,
errorMessage: `🚫 PORT CONFLICT: Multiple nodes using same port
❌ CONFLICT DETAILS:
• Port ${port} is used by multiple nodes in this workflow
• Conflicting nodes: ${conflictNames}
• Each A2A node must use a unique port number
🔧 SOLUTIONS TO FIX THIS:
1. ✅ Change this node to use port ${alternativePort || 'different port'}
2. Assign unique ports to each A2A node in your workflow
3. Check your workflow configuration for duplicate port assignments
💡 ACTION REQUIRED: Update the port number in this node to resolve the conflict before activating the workflow.`,
suggestedPort: alternativePort !== null ? alternativePort : undefined
});
}
}
}
// Check external conflicts for each unique port
const checkedPorts = new Set();
for (const node of nodes) {
if (checkedPorts.has(node.port))
continue;
checkedPorts.add(node.port);
// Skip if already has internal conflict
if (portMap.get(node.port).length > 1)
continue;
const validation = await validateNodePort(node.port, node.serviceType, node.serviceName, node.nodeId);
if (!validation.isValid && validation.errorMessage) {
errors.push({
nodeId: node.nodeId,
errorMessage: validation.errorMessage,
suggestedPort: validation.suggestedPort
});
}
}
return {
isValid: errors.length === 0,
errors
};
}