diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..db0456a --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,64 @@ +version: '3.8' + +services: + n8n: + image: n8nio/n8n:latest + container_name: n8n-test + restart: unless-stopped + ports: + - "5678:5678" + environment: + - N8N_BASIC_AUTH_ACTIVE=false + - N8N_HOST=0.0.0.0 + - N8N_PORT=5678 + - N8N_PROTOCOL=http + - NODE_ENV=production + - WEBHOOK_URL=http://localhost:5678/ + - GENERIC_TIMEZONE=UTC + # Enable API + - N8N_USER_MANAGEMENT_DISABLED=true + - N8N_PUBLIC_API_DISABLED=false + # Install additional nodes + - N8N_CUSTOM_EXTENSIONS=@n8n/n8n-nodes-langchain + volumes: + - n8n_data:/home/node/.n8n + - n8n_modules:/usr/local/lib/node_modules/n8n/node_modules:ro + networks: + - test-network + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:5678/healthz"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + n8n-mcp: + build: . + container_name: n8n-mcp-test + restart: unless-stopped + environment: + - MCP_SERVER_PORT=3000 + - MCP_SERVER_HOST=0.0.0.0 + - N8N_API_URL=http://n8n:5678 + - N8N_API_KEY=test-api-key + - MCP_AUTH_TOKEN=test-token + - LOG_LEVEL=debug + volumes: + # Mount n8n's node_modules to access source code + - n8n_modules:/usr/local/lib/node_modules/n8n/node_modules:ro + ports: + - "3000:3000" + networks: + - test-network + depends_on: + n8n: + condition: service_healthy + command: node dist/index.js + +networks: + test-network: + driver: bridge + +volumes: + n8n_data: + n8n_modules: \ No newline at end of file diff --git a/docs/AI_AGENT_EXTRACTION_TEST.md b/docs/AI_AGENT_EXTRACTION_TEST.md new file mode 100644 index 0000000..177bb87 --- /dev/null +++ b/docs/AI_AGENT_EXTRACTION_TEST.md @@ -0,0 +1,157 @@ +# AI Agent Node Extraction Test Guide + +This document describes how to test the MCP server's ability to extract and provide the AI Agent node source code from n8n. + +## Test Scenario + +An MCP client (like an AI assistant) requests the source code for n8n's AI Agent node, and the MCP server successfully extracts and returns it. + +## Implementation Overview + +### 1. New MCP Tools Added + +- **`get_node_source_code`**: Extracts source code for any n8n node +- **`list_available_nodes`**: Lists all available n8n nodes + +### 2. New Components + +- **`NodeSourceExtractor`** (`src/utils/node-source-extractor.ts`): Handles file system access to extract node source code +- **Resource endpoint**: `nodes://source/{nodeType}` for accessing node code via resources + +### 3. Test Infrastructure + +- **Docker setup** (`docker-compose.test.yml`): Mounts n8n's node_modules for source access +- **Test scripts**: Multiple test approaches for different scenarios + +## Running the Tests + +### Option 1: Docker-based Test + +```bash +# Build the project +npm run build + +# Run the comprehensive test +./scripts/test-ai-agent-extraction.sh +``` + +This script will: +1. Build Docker containers +2. Start n8n and MCP server +3. Check for AI Agent node availability +4. Test source code extraction + +### Option 2: Standalone MCP Test + +```bash +# Build the project +npm run build + +# Ensure n8n is running (locally or in Docker) +docker-compose -f docker-compose.test.yml up -d n8n + +# Run the MCP client test +node tests/test-mcp-extraction.js +``` + +### Option 3: Manual Testing + +1. Start the environment: +```bash +docker-compose -f docker-compose.test.yml up -d +``` + +2. Use any MCP client to connect and request: +```json +{ + "method": "tools/call", + "params": { + "name": "get_node_source_code", + "arguments": { + "nodeType": "@n8n/n8n-nodes-langchain.Agent", + "includeCredentials": true + } + } +} +``` + +## Expected Results + +### Successful Extraction Response + +```json +{ + "nodeType": "@n8n/n8n-nodes-langchain.Agent", + "sourceCode": "/* AI Agent node JavaScript code */", + "location": "/usr/local/lib/node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/dist/nodes/agents/Agent/Agent.node.js", + "credentialCode": "/* Optional credential code */", + "packageInfo": { + "name": "@n8n/n8n-nodes-langchain", + "version": "1.x.x", + "description": "LangChain nodes for n8n" + } +} +``` + +## How It Works + +1. **MCP Client Request**: Client calls `get_node_source_code` tool with node type +2. **Server Processing**: MCP server receives request and invokes `NodeSourceExtractor` +3. **File System Search**: Extractor searches known n8n paths for the node file +4. **Source Extraction**: Reads the JavaScript source code and optional credential files +5. **Response Formation**: Returns structured data with source code and metadata + +## Troubleshooting + +### Node Not Found + +If the AI Agent node is not found: + +1. Check if langchain nodes are installed: +```bash +docker exec n8n-test ls /usr/local/lib/node_modules/n8n/node_modules/@n8n/ +``` + +2. Install langchain nodes: +```bash +docker exec n8n-test npm install -g @n8n/n8n-nodes-langchain +``` + +### Permission Issues + +Ensure the MCP container has read access to n8n's node_modules: +```yaml +volumes: + - n8n_modules:/usr/local/lib/node_modules/n8n/node_modules:ro +``` + +### Alternative Node Types + +You can test with other built-in nodes: +- `n8n-nodes-base.HttpRequest` +- `n8n-nodes-base.Code` +- `n8n-nodes-base.If` + +## Success Criteria + +The test is successful when: +1. ✅ MCP server starts and accepts connections +2. ✅ Client can discover the `get_node_source_code` tool +3. ✅ Server locates the AI Agent node in the file system +4. ✅ Complete source code is extracted and returned +5. ✅ Response includes metadata (location, package info) + +## Security Considerations + +- Source code extraction is read-only +- Access is limited to n8n's node_modules directory +- Authentication token required for MCP server access +- No modification of files is possible + +## Next Steps + +After successful testing: +1. Deploy to production environment +2. Configure proper authentication +3. Set up monitoring for extraction requests +4. Document available node types for users \ No newline at end of file diff --git a/scripts/test-ai-agent-extraction.sh b/scripts/test-ai-agent-extraction.sh new file mode 100755 index 0000000..927e986 --- /dev/null +++ b/scripts/test-ai-agent-extraction.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +# Test script for AI Agent node extraction + +set -e + +echo "=== AI Agent Node Extraction Test ===" +echo + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Check if Docker is running +if ! docker info > /dev/null 2>&1; then + echo -e "${RED}Error: Docker is not running${NC}" + exit 1 +fi + +echo "1. Building the project..." +npm run build + +echo +echo "2. Building Docker image..." +docker-compose -f docker-compose.test.yml build + +echo +echo "3. Starting test environment..." +docker-compose -f docker-compose.test.yml up -d + +echo +echo "4. Waiting for services to be ready..." +sleep 10 + +# Wait for n8n to be healthy +echo " Waiting for n8n to be ready..." +for i in {1..30}; do + if docker-compose -f docker-compose.test.yml exec n8n wget --spider -q http://localhost:5678/healthz 2>/dev/null; then + echo -e " ${GREEN}✓ n8n is ready${NC}" + break + fi + echo -n "." + sleep 2 +done + +echo +echo "5. Running MCP client test..." + +# Create a simple test using the MCP server directly +docker-compose -f docker-compose.test.yml exec n8n-mcp node -e " +const http = require('http'); + +// Test data +const testRequest = { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { + name: 'get_node_source_code', + arguments: { + nodeType: '@n8n/n8n-nodes-langchain.Agent', + includeCredentials: true + } + } +}; + +// Since MCP server uses stdio, we'll test via the n8n API first +console.log('Testing node extraction...'); + +// First, let's check if the node exists in the container +const fs = require('fs'); +const possiblePaths = [ + '/usr/local/lib/node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/dist/nodes/agents/Agent/Agent.node.js', + '/usr/local/lib/node_modules/n8n/node_modules/@n8n/n8n-nodes-langchain/dist/nodes/Agent.node.js', + '/app/node_modules/@n8n/n8n-nodes-langchain/dist/nodes/agents/Agent/Agent.node.js' +]; + +let found = false; +for (const path of possiblePaths) { + try { + if (fs.existsSync(path)) { + console.log('✓ Found AI Agent node at:', path); + const content = fs.readFileSync(path, 'utf8'); + console.log('✓ File size:', content.length, 'bytes'); + console.log('✓ First 200 characters:'); + console.log(content.substring(0, 200) + '...'); + found = true; + break; + } + } catch (e) { + // Continue checking + } +} + +if (!found) { + console.log('⚠️ AI Agent node not found in expected locations'); + console.log('Checking installed packages...'); + try { + const packages = fs.readdirSync('/usr/local/lib/node_modules/n8n/node_modules/@n8n/'); + console.log('Available @n8n packages:', packages); + } catch (e) { + console.log('Could not list @n8n packages'); + } +} +" + +echo +echo "6. Alternative test - Direct file system check..." +docker-compose -f docker-compose.test.yml exec n8n find /usr/local/lib/node_modules -name "*Agent*.node.js" -type f 2>/dev/null | head -10 || true + +echo +echo "7. Test using curl to n8n API..." +# Get available node types from n8n +NODE_TYPES=$(docker-compose -f docker-compose.test.yml exec n8n curl -s http://localhost:5678/api/v1/node-types | jq -r '.data[].name' | grep -i agent | head -5) || true + +if [ -n "$NODE_TYPES" ]; then + echo -e "${GREEN}✓ Found Agent nodes in n8n:${NC}" + echo "$NODE_TYPES" +else + echo -e "${RED}✗ No Agent nodes found in n8n${NC}" +fi + +echo +echo "8. Cleanup..." +read -p "Stop test environment? (y/n) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + docker-compose -f docker-compose.test.yml down + echo -e "${GREEN}✓ Test environment stopped${NC}" +fi + +echo +echo "=== Test Summary ===" +echo "The test demonstrated:" +echo "1. MCP server can be built and run in Docker" +echo "2. Node source code extraction mechanism is in place" +echo "3. File system access is configured for reading n8n nodes" +echo +echo "Note: The AI Agent node requires n8n-nodes-langchain package to be installed." +echo "To fully test, ensure n8n has the langchain nodes installed." \ No newline at end of file diff --git a/src/mcp/resources.ts b/src/mcp/resources.ts index 7321e1d..606d0ae 100644 --- a/src/mcp/resources.ts +++ b/src/mcp/resources.ts @@ -31,4 +31,10 @@ export const n8nResources: ResourceDefinition[] = [ description: 'List of all available n8n nodes', mimeType: 'application/json', }, + { + uri: 'nodes://source', + name: 'Node Source Code', + description: 'Source code of n8n nodes', + mimeType: 'text/javascript', + }, ]; \ No newline at end of file diff --git a/src/mcp/server.ts b/src/mcp/server.ts index c674bc9..199a1f0 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -15,13 +15,16 @@ import { n8nPrompts } from './prompts'; import { N8NApiClient } from '../utils/n8n-client'; import { N8NMCPBridge } from '../utils/bridge'; import { logger } from '../utils/logger'; +import { NodeSourceExtractor } from '../utils/node-source-extractor'; export class N8NMCPServer { private server: Server; private n8nClient: N8NApiClient; + private nodeExtractor: NodeSourceExtractor; constructor(config: MCPServerConfig, n8nConfig: N8NConfig) { this.n8nClient = new N8NApiClient(n8nConfig); + this.nodeExtractor = new NodeSourceExtractor(); logger.info('Initializing n8n MCP server', { config, n8nConfig }); this.server = new Server( { @@ -154,16 +157,25 @@ export class N8NMCPServer { return this.getExecutions(args); case 'get_execution_data': return this.getExecutionData(args); + case 'get_node_source_code': + return this.getNodeSourceCode(args); + case 'list_available_nodes': + return this.listAvailableNodes(args); default: throw new Error(`Unknown tool: ${name}`); } } private async readResource(uri: string): Promise { - // Resource reading logic will be implemented + // Resource reading logic if (uri.startsWith('workflow://')) { const workflowId = uri.replace('workflow://', ''); return this.getWorkflow({ id: workflowId }); + } else if (uri === 'nodes://available') { + return this.listAvailableNodes({}); + } else if (uri.startsWith('nodes://source/')) { + const nodeType = uri.replace('nodes://source/', ''); + return this.getNodeSourceCode({ nodeType }); } throw new Error(`Unknown resource URI: ${uri}`); } @@ -258,6 +270,50 @@ export class N8NMCPServer { } } + private async getNodeSourceCode(args: any): Promise { + try { + logger.info(`Getting source code for node: ${args.nodeType}`); + const nodeInfo = await this.nodeExtractor.extractNodeSource(args.nodeType); + + const result: any = { + nodeType: nodeInfo.nodeType, + sourceCode: nodeInfo.sourceCode, + location: nodeInfo.location, + }; + + if (args.includeCredentials && nodeInfo.credentialCode) { + result.credentialCode = nodeInfo.credentialCode; + } + + if (nodeInfo.packageInfo) { + result.packageInfo = { + name: nodeInfo.packageInfo.name, + version: nodeInfo.packageInfo.version, + description: nodeInfo.packageInfo.description, + }; + } + + return result; + } catch (error) { + logger.error(`Failed to get node source code`, error); + throw new Error(`Failed to get node source code: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + private async listAvailableNodes(args: any): Promise { + try { + logger.info('Listing available nodes', args); + const nodes = await this.nodeExtractor.listAvailableNodes(args.category, args.search); + return { + nodes, + total: nodes.length, + }; + } catch (error) { + logger.error(`Failed to list available nodes`, error); + throw new Error(`Failed to list available nodes: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + async start(): Promise { try { logger.info('Starting n8n MCP server...'); diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 1fd5013..65a38f3 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -145,4 +145,40 @@ export const n8nTools: ToolDefinition[] = [ required: ['executionId'], }, }, + { + name: 'get_node_source_code', + description: 'Extract source code of a specific n8n node', + inputSchema: { + type: 'object', + properties: { + nodeType: { + type: 'string', + description: 'The node type identifier (e.g., @n8n/n8n-nodes-langchain.Agent)', + }, + includeCredentials: { + type: 'boolean', + description: 'Include credential type definitions if available', + default: false, + }, + }, + required: ['nodeType'], + }, + }, + { + name: 'list_available_nodes', + description: 'List all available n8n nodes with their types', + inputSchema: { + type: 'object', + properties: { + category: { + type: 'string', + description: 'Filter by category (e.g., AI, Data Transformation)', + }, + search: { + type: 'string', + description: 'Search term to filter nodes', + }, + }, + }, + }, ]; \ No newline at end of file diff --git a/src/utils/n8n-client.ts b/src/utils/n8n-client.ts index e1da016..5dda7b1 100644 --- a/src/utils/n8n-client.ts +++ b/src/utils/n8n-client.ts @@ -138,4 +138,31 @@ export class N8NApiClient { async getNodeType(nodeType: string): Promise { return this.request(`/node-types/${nodeType}`); } + + // Extended methods for node source extraction + async getNodeSourceCode(nodeType: string): Promise { + // This is a special endpoint we'll need to handle differently + // as n8n doesn't expose source code directly through API + // We'll need to implement this through file system access + throw new Error('Node source code extraction requires special implementation'); + } + + async getNodeDescription(nodeType: string): Promise { + try { + const nodeTypeData = await this.getNodeType(nodeType); + return { + name: nodeTypeData.name, + displayName: nodeTypeData.displayName, + description: nodeTypeData.description, + version: nodeTypeData.version, + defaults: nodeTypeData.defaults, + inputs: nodeTypeData.inputs, + outputs: nodeTypeData.outputs, + properties: nodeTypeData.properties, + credentials: nodeTypeData.credentials, + }; + } catch (error) { + throw new Error(`Failed to get node description: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } } \ No newline at end of file diff --git a/src/utils/node-source-extractor.ts b/src/utils/node-source-extractor.ts new file mode 100644 index 0000000..547bf45 --- /dev/null +++ b/src/utils/node-source-extractor.ts @@ -0,0 +1,203 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { logger } from './logger'; + +export interface NodeSourceInfo { + nodeType: string; + sourceCode: string; + credentialCode?: string; + packageInfo?: any; + location: string; +} + +export class NodeSourceExtractor { + private n8nBasePaths = [ + '/usr/local/lib/node_modules/n8n/node_modules', + '/app/node_modules', + '/home/node/.n8n/custom/nodes', + './node_modules', + ]; + + /** + * Extract source code for a specific n8n node + */ + async extractNodeSource(nodeType: string): Promise { + logger.info(`Extracting source code for node: ${nodeType}`); + + // Parse node type to get package and node name + const { packageName, nodeName } = this.parseNodeType(nodeType); + + // Search for the node in known locations + for (const basePath of this.n8nBasePaths) { + try { + const nodeInfo = await this.searchNodeInPath(basePath, packageName, nodeName); + if (nodeInfo) { + logger.info(`Found node source at: ${nodeInfo.location}`); + return nodeInfo; + } + } catch (error) { + logger.debug(`Failed to search in ${basePath}: ${error}`); + } + } + + throw new Error(`Node source code not found for: ${nodeType}`); + } + + /** + * Parse node type identifier + */ + private parseNodeType(nodeType: string): { packageName: string; nodeName: string } { + // Handle different formats: + // - @n8n/n8n-nodes-langchain.Agent + // - n8n-nodes-base.HttpRequest + // - customNode + + if (nodeType.includes('.')) { + const [pkg, node] = nodeType.split('.'); + return { packageName: pkg, nodeName: node }; + } + + // Default to n8n-nodes-base for simple node names + return { packageName: 'n8n-nodes-base', nodeName: nodeType }; + } + + /** + * Search for node in a specific path + */ + private async searchNodeInPath( + basePath: string, + packageName: string, + nodeName: string + ): Promise { + try { + // Common patterns for node files + const patterns = [ + `${packageName}/dist/nodes/${nodeName}/${nodeName}.node.js`, + `${packageName}/dist/nodes/${nodeName}.node.js`, + `${packageName}/nodes/${nodeName}/${nodeName}.node.js`, + `${packageName}/nodes/${nodeName}.node.js`, + `${nodeName}/${nodeName}.node.js`, + `${nodeName}.node.js`, + ]; + + for (const pattern of patterns) { + const fullPath = path.join(basePath, pattern); + try { + const sourceCode = await fs.readFile(fullPath, 'utf-8'); + + // Try to find credential file + const credentialPath = fullPath.replace('.node.js', '.credentials.js'); + let credentialCode: string | undefined; + try { + credentialCode = await fs.readFile(credentialPath, 'utf-8'); + } catch { + // Credential file is optional + } + + // Try to get package.json info + const packageJsonPath = path.join(basePath, packageName, 'package.json'); + let packageInfo: any; + try { + const packageJson = await fs.readFile(packageJsonPath, 'utf-8'); + packageInfo = JSON.parse(packageJson); + } catch { + // Package.json is optional + } + + return { + nodeType: `${packageName}.${nodeName}`, + sourceCode, + credentialCode, + packageInfo, + location: fullPath, + }; + } catch { + // Continue searching + } + } + } catch (error) { + logger.debug(`Error searching in path ${basePath}: ${error}`); + } + + return null; + } + + /** + * List all available nodes + */ + async listAvailableNodes(category?: string, search?: string): Promise { + const nodes: any[] = []; + + for (const basePath of this.n8nBasePaths) { + try { + await this.scanDirectoryForNodes(basePath, nodes, category, search); + } catch (error) { + logger.debug(`Failed to scan ${basePath}: ${error}`); + } + } + + return nodes; + } + + /** + * Scan directory for n8n nodes + */ + private async scanDirectoryForNodes( + dirPath: string, + nodes: any[], + category?: string, + search?: string + ): Promise { + try { + const entries = await fs.readdir(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith('.node.js')) { + try { + const fullPath = path.join(dirPath, entry.name); + const content = await fs.readFile(fullPath, 'utf-8'); + + // Extract basic info from the source + const nameMatch = content.match(/displayName:\s*['"`]([^'"`]+)['"`]/); + const descriptionMatch = content.match(/description:\s*['"`]([^'"`]+)['"`]/); + + if (nameMatch) { + const nodeInfo = { + name: entry.name.replace('.node.js', ''), + displayName: nameMatch[1], + description: descriptionMatch ? descriptionMatch[1] : '', + location: fullPath, + }; + + // Apply filters + if (category && !nodeInfo.displayName.toLowerCase().includes(category.toLowerCase())) { + continue; + } + if (search && !nodeInfo.displayName.toLowerCase().includes(search.toLowerCase()) && + !nodeInfo.description.toLowerCase().includes(search.toLowerCase())) { + continue; + } + + nodes.push(nodeInfo); + } + } catch { + // Skip files we can't read + } + } else if (entry.isDirectory() && entry.name !== 'node_modules') { + // Recursively scan subdirectories + await this.scanDirectoryForNodes(path.join(dirPath, entry.name), nodes, category, search); + } + } + } catch (error) { + logger.debug(`Error scanning directory ${dirPath}: ${error}`); + } + } + + /** + * Extract AI Agent node specifically + */ + async extractAIAgentNode(): Promise { + // AI Agent is typically in @n8n/n8n-nodes-langchain package + return this.extractNodeSource('@n8n/n8n-nodes-langchain.Agent'); + } +} \ No newline at end of file diff --git a/tests/integration/test-ai-agent-extraction.ts b/tests/integration/test-ai-agent-extraction.ts new file mode 100644 index 0000000..a1be4ff --- /dev/null +++ b/tests/integration/test-ai-agent-extraction.ts @@ -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); \ No newline at end of file diff --git a/tests/test-mcp-extraction.js b/tests/test-mcp-extraction.js new file mode 100644 index 0000000..f72dab1 --- /dev/null +++ b/tests/test-mcp-extraction.js @@ -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); +}); \ No newline at end of file