Add AI Agent node source code extraction capability
This commit implements the ability to extract n8n node source code through MCP:
Features:
- New MCP tools: get_node_source_code and list_available_nodes
- NodeSourceExtractor utility for file system access to n8n nodes
- Support for extracting any n8n node including AI Agent from @n8n/n8n-nodes-langchain
- Resource endpoint for accessing node source: nodes://source/{nodeType}
Testing:
- Docker test environment with mounted n8n node_modules
- Multiple test scripts for different scenarios
- Comprehensive test documentation
- Standalone MCP client test demonstrating full extraction flow
The implementation successfully demonstrates:
1. MCP server can access n8n's installed nodes
2. Source code can be extracted and returned to MCP clients
3. Full metadata including package info and file locations
4. Support for credential code extraction when available
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
133
tests/integration/test-ai-agent-extraction.ts
Normal file
133
tests/integration/test-ai-agent-extraction.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { spawn } from 'child_process';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Integration test for AI Agent node extraction
|
||||
* This simulates an MCP client requesting the AI Agent code from n8n
|
||||
*/
|
||||
async function testAIAgentExtraction() {
|
||||
console.log('=== AI Agent Node Extraction Test ===\n');
|
||||
|
||||
// Create MCP client
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'test-mcp-client',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
console.log('1. Starting MCP server...');
|
||||
const serverPath = path.join(__dirname, '../../dist/index.js');
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: [serverPath],
|
||||
env: {
|
||||
...process.env,
|
||||
N8N_API_URL: process.env.N8N_API_URL || 'http://localhost:5678',
|
||||
N8N_API_KEY: process.env.N8N_API_KEY || 'test-key',
|
||||
LOG_LEVEL: 'debug',
|
||||
},
|
||||
});
|
||||
|
||||
await client.connect(transport);
|
||||
console.log('✓ Connected to MCP server\n');
|
||||
|
||||
// Test 1: List available tools
|
||||
console.log('2. Listing available tools...');
|
||||
const toolsResponse = await client.request(
|
||||
{ method: 'tools/list' },
|
||||
{}
|
||||
);
|
||||
console.log(`✓ Found ${toolsResponse.tools.length} tools`);
|
||||
|
||||
const hasNodeSourceTool = toolsResponse.tools.some(
|
||||
(tool: any) => tool.name === 'get_node_source_code'
|
||||
);
|
||||
console.log(`✓ Node source extraction tool available: ${hasNodeSourceTool}\n`);
|
||||
|
||||
// Test 2: List available nodes
|
||||
console.log('3. Listing available nodes...');
|
||||
const listNodesResponse = await client.request(
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'list_available_nodes',
|
||||
arguments: {
|
||||
search: 'agent',
|
||||
},
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
console.log(`✓ Found nodes matching 'agent':`);
|
||||
const content = JSON.parse(listNodesResponse.content[0].text);
|
||||
content.nodes.forEach((node: any) => {
|
||||
console.log(` - ${node.displayName}: ${node.description}`);
|
||||
});
|
||||
console.log();
|
||||
|
||||
// Test 3: Extract AI Agent node source code
|
||||
console.log('4. Extracting AI Agent node source code...');
|
||||
const aiAgentResponse = await client.request(
|
||||
{
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'get_node_source_code',
|
||||
arguments: {
|
||||
nodeType: '@n8n/n8n-nodes-langchain.Agent',
|
||||
includeCredentials: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const result = JSON.parse(aiAgentResponse.content[0].text);
|
||||
console.log('✓ Successfully extracted AI Agent node:');
|
||||
console.log(` - Node Type: ${result.nodeType}`);
|
||||
console.log(` - Location: ${result.location}`);
|
||||
console.log(` - Source Code Length: ${result.sourceCode.length} characters`);
|
||||
console.log(` - Has Credential Code: ${!!result.credentialCode}`);
|
||||
|
||||
if (result.packageInfo) {
|
||||
console.log(` - Package: ${result.packageInfo.name} v${result.packageInfo.version}`);
|
||||
}
|
||||
|
||||
// Show a snippet of the code
|
||||
console.log('\n5. Source Code Preview:');
|
||||
console.log('```javascript');
|
||||
console.log(result.sourceCode.substring(0, 500) + '...');
|
||||
console.log('```\n');
|
||||
|
||||
// Test 4: Use resource endpoint
|
||||
console.log('6. Testing resource endpoint...');
|
||||
const resourceResponse = await client.request(
|
||||
{
|
||||
method: 'resources/read',
|
||||
params: {
|
||||
uri: 'nodes://source/@n8n/n8n-nodes-langchain.Agent',
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
console.log('✓ Successfully read node source via resource endpoint\n');
|
||||
|
||||
console.log('=== Test Completed Successfully ===');
|
||||
|
||||
await client.close();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error);
|
||||
await client.close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testAIAgentExtraction().catch(console.error);
|
||||
197
tests/test-mcp-extraction.js
Normal file
197
tests/test-mcp-extraction.js
Normal file
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Standalone test for MCP AI Agent node extraction
|
||||
* This demonstrates how an MCP client would request and receive the AI Agent code
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
green: '\x1b[32m',
|
||||
red: '\x1b[31m',
|
||||
blue: '\x1b[34m',
|
||||
yellow: '\x1b[33m',
|
||||
reset: '\x1b[0m'
|
||||
};
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
async function runMCPTest() {
|
||||
log('\n=== MCP AI Agent Extraction Test ===\n', 'blue');
|
||||
|
||||
// Start the MCP server as a subprocess
|
||||
const serverPath = path.join(__dirname, '../dist/index.js');
|
||||
const mcp = spawn('node', [serverPath], {
|
||||
env: {
|
||||
...process.env,
|
||||
N8N_API_URL: 'http://localhost:5678',
|
||||
N8N_API_KEY: 'test-key',
|
||||
LOG_LEVEL: 'info'
|
||||
}
|
||||
});
|
||||
|
||||
let buffer = '';
|
||||
|
||||
// Handle server output
|
||||
mcp.stderr.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
if (output.includes('MCP server started')) {
|
||||
log('✓ MCP Server started successfully', 'green');
|
||||
sendRequest();
|
||||
}
|
||||
});
|
||||
|
||||
mcp.stdout.on('data', (data) => {
|
||||
buffer += data.toString();
|
||||
|
||||
// Try to parse complete JSON-RPC messages
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const response = JSON.parse(line);
|
||||
handleResponse(response);
|
||||
} catch (e) {
|
||||
// Not a complete JSON message yet
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mcp.on('close', (code) => {
|
||||
log(`\nMCP server exited with code ${code}`, code === 0 ? 'green' : 'red');
|
||||
});
|
||||
|
||||
// Send test requests
|
||||
let requestId = 1;
|
||||
|
||||
function sendRequest() {
|
||||
// Step 1: Initialize
|
||||
log('\n1. Initializing MCP connection...', 'yellow');
|
||||
sendMessage({
|
||||
jsonrpc: '2.0',
|
||||
id: requestId++,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: {
|
||||
name: 'test-client',
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(message) {
|
||||
const json = JSON.stringify(message);
|
||||
mcp.stdin.write(json + '\n');
|
||||
}
|
||||
|
||||
function handleResponse(response) {
|
||||
if (response.error) {
|
||||
log(`✗ Error: ${response.error.message}`, 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle different response types
|
||||
if (response.id === 1) {
|
||||
// Initialize response
|
||||
log('✓ Initialized successfully', 'green');
|
||||
log(` Server: ${response.result.serverInfo.name} v${response.result.serverInfo.version}`, 'green');
|
||||
|
||||
// Step 2: List tools
|
||||
log('\n2. Listing available tools...', 'yellow');
|
||||
sendMessage({
|
||||
jsonrpc: '2.0',
|
||||
id: requestId++,
|
||||
method: 'tools/list',
|
||||
params: {}
|
||||
});
|
||||
} else if (response.id === 2) {
|
||||
// Tools list response
|
||||
const tools = response.result.tools;
|
||||
log(`✓ Found ${tools.length} tools`, 'green');
|
||||
|
||||
const nodeSourceTool = tools.find(t => t.name === 'get_node_source_code');
|
||||
if (nodeSourceTool) {
|
||||
log('✓ Node source extraction tool available', 'green');
|
||||
|
||||
// Step 3: Call the tool to get AI Agent code
|
||||
log('\n3. Requesting AI Agent node source code...', 'yellow');
|
||||
sendMessage({
|
||||
jsonrpc: '2.0',
|
||||
id: requestId++,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'get_node_source_code',
|
||||
arguments: {
|
||||
nodeType: '@n8n/n8n-nodes-langchain.Agent',
|
||||
includeCredentials: true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (response.id === 3) {
|
||||
// Tool call response
|
||||
try {
|
||||
const content = response.result.content[0];
|
||||
if (content.type === 'text') {
|
||||
const result = JSON.parse(content.text);
|
||||
|
||||
log('\n✓ Successfully extracted AI Agent node!', 'green');
|
||||
log('\n=== Extraction Results ===', 'blue');
|
||||
log(`Node Type: ${result.nodeType}`);
|
||||
log(`Location: ${result.location}`);
|
||||
log(`Source Code Size: ${result.sourceCode.length} bytes`);
|
||||
|
||||
if (result.packageInfo) {
|
||||
log(`Package: ${result.packageInfo.name} v${result.packageInfo.version}`);
|
||||
}
|
||||
|
||||
if (result.credentialCode) {
|
||||
log(`Credential Code: Available (${result.credentialCode.length} bytes)`);
|
||||
}
|
||||
|
||||
// Show code preview
|
||||
log('\n=== Code Preview ===', 'blue');
|
||||
const preview = result.sourceCode.substring(0, 400);
|
||||
console.log(preview + '...\n');
|
||||
|
||||
log('✓ Test completed successfully!', 'green');
|
||||
}
|
||||
} catch (e) {
|
||||
log(`✗ Failed to parse response: ${e.message}`, 'red');
|
||||
}
|
||||
|
||||
// Close the connection
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
process.on('SIGINT', () => {
|
||||
log('\nInterrupted, closing MCP server...', 'yellow');
|
||||
mcp.kill();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// Run the test
|
||||
log('Starting MCP AI Agent extraction test...', 'blue');
|
||||
log('This test will:', 'blue');
|
||||
log('1. Start an MCP server', 'blue');
|
||||
log('2. Request the AI Agent node source code', 'blue');
|
||||
log('3. Display the extracted code\n', 'blue');
|
||||
|
||||
runMCPTest().catch(error => {
|
||||
log(`\nTest failed: ${error.message}`, 'red');
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user