Refactor to focused n8n node documentation MCP server

Major refactoring to align with actual requirements:
- Purpose: Serve n8n node code/documentation to AI agents only
- No workflow execution or management features
- Complete node information including source code, docs, and examples

New features:
- Node documentation service with SQLite FTS5 search
- Documentation fetcher from n8n-docs repository
- Example workflow generator for each node type
- Simplified MCP tools focused on node information
- Complete database rebuild with all node data

MCP Tools:
- list_nodes: List available nodes
- get_node_info: Get complete node information
- search_nodes: Full-text search across nodes
- get_node_example: Get usage examples
- get_node_source_code: Get source code only
- get_node_documentation: Get documentation only
- rebuild_database: Rebuild entire database
- get_database_statistics: Database stats

Database schema includes:
- Node source code and metadata
- Official documentation from n8n-docs
- Generated usage examples
- Full-text search capabilities
- Category and type filtering

Updated README with:
- Clear purpose statement
- Claude Desktop installation instructions
- Complete tool documentation
- Troubleshooting guide

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-06-07 22:11:30 +00:00
parent 96809d0c9f
commit d32af279c0
17 changed files with 2484 additions and 359 deletions

99
src/db/schema-v2.sql Normal file
View File

@@ -0,0 +1,99 @@
-- Main nodes table with documentation and examples
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_type TEXT UNIQUE NOT NULL, -- e.g., "n8n-nodes-base.if"
name TEXT NOT NULL, -- e.g., "If"
display_name TEXT, -- e.g., "If"
description TEXT, -- Brief description from node definition
category TEXT, -- e.g., "Core Nodes", "Flow"
subcategory TEXT, -- More specific categorization
icon TEXT, -- Icon identifier/path
-- Source code
source_code TEXT NOT NULL, -- Full node source code
credential_code TEXT, -- Credential type definitions
code_hash TEXT NOT NULL, -- Hash for change detection
code_length INTEGER NOT NULL, -- Source code size
-- Documentation
documentation_markdown TEXT, -- Full markdown documentation from n8n-docs
documentation_url TEXT, -- URL to documentation page
-- Example usage
example_workflow TEXT, -- JSON example workflow using this node
example_parameters TEXT, -- JSON example of node parameters
properties_schema TEXT, -- JSON schema of node properties
-- Metadata
package_name TEXT NOT NULL, -- e.g., "n8n-nodes-base"
version TEXT, -- Node version
codex_data TEXT, -- Additional codex/metadata JSON
aliases TEXT, -- JSON array of alternative names
-- Flags
has_credentials INTEGER DEFAULT 0,
is_trigger INTEGER DEFAULT 0, -- Whether it's a trigger node
is_webhook INTEGER DEFAULT 0, -- Whether it's a webhook node
-- Timestamps
extracted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_nodes_package_name ON nodes(package_name);
CREATE INDEX IF NOT EXISTS idx_nodes_category ON nodes(category);
CREATE INDEX IF NOT EXISTS idx_nodes_code_hash ON nodes(code_hash);
CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
CREATE INDEX IF NOT EXISTS idx_nodes_is_trigger ON nodes(is_trigger);
-- Full Text Search virtual table for comprehensive search
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
node_type,
name,
display_name,
description,
category,
documentation_markdown,
aliases,
content=nodes,
content_rowid=id
);
-- Triggers to keep FTS in sync
CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes
BEGIN
INSERT INTO nodes_fts(rowid, node_type, name, display_name, description, category, documentation_markdown, aliases)
VALUES (new.id, new.node_type, new.name, new.display_name, new.description, new.category, new.documentation_markdown, new.aliases);
END;
CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes
BEGIN
DELETE FROM nodes_fts WHERE rowid = old.id;
END;
CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes
BEGIN
DELETE FROM nodes_fts WHERE rowid = old.id;
INSERT INTO nodes_fts(rowid, node_type, name, display_name, description, category, documentation_markdown, aliases)
VALUES (new.id, new.node_type, new.name, new.display_name, new.description, new.category, new.documentation_markdown, new.aliases);
END;
-- Table for storing node documentation versions
CREATE TABLE IF NOT EXISTS documentation_sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL, -- 'n8n-docs-repo', 'inline', 'generated'
commit_hash TEXT, -- Git commit hash if from repo
fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Statistics table
CREATE TABLE IF NOT EXISTS extraction_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
total_nodes INTEGER NOT NULL,
nodes_with_docs INTEGER NOT NULL,
nodes_with_examples INTEGER NOT NULL,
total_code_size INTEGER NOT NULL,
total_docs_size INTEGER NOT NULL,
extraction_date DATETIME DEFAULT CURRENT_TIMESTAMP
);

58
src/index-v2.ts Normal file
View File

@@ -0,0 +1,58 @@
import dotenv from 'dotenv';
import { N8NDocumentationMCPServer } from './mcp/server-v2';
import { MCPServerConfig } from './types';
import { logger } from './utils/logger';
import { NodeDocumentationService } from './services/node-documentation-service';
// Load environment variables
dotenv.config();
async function main() {
const config: MCPServerConfig = {
port: parseInt(process.env.MCP_SERVER_PORT || '3000', 10),
host: process.env.MCP_SERVER_HOST || 'localhost',
authToken: process.env.MCP_AUTH_TOKEN,
};
// Check if we should rebuild the database on startup
const rebuildOnStart = process.env.REBUILD_ON_START === 'true';
if (rebuildOnStart) {
logger.info('Rebuilding database on startup...');
const service = new NodeDocumentationService();
try {
const stats = await service.rebuildDatabase();
logger.info('Database rebuild complete:', stats);
} catch (error) {
logger.error('Failed to rebuild database:', error);
process.exit(1);
} finally {
service.close();
}
}
const server = new N8NDocumentationMCPServer(config);
try {
await server.start();
} catch (error) {
logger.error('Failed to start MCP server:', error);
process.exit(1);
}
}
// Handle graceful shutdown
process.on('SIGINT', () => {
logger.info('Received SIGINT, shutting down MCP server...');
process.exit(0);
});
process.on('SIGTERM', () => {
logger.info('Received SIGTERM, shutting down MCP server...');
process.exit(0);
});
main().catch((error) => {
logger.error('Unhandled error:', error);
process.exit(1);
});

435
src/mcp/server-v2.ts Normal file
View File

@@ -0,0 +1,435 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListResourcesRequestSchema,
ListToolsRequestSchema,
McpError,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { NodeDocumentationService } from '../services/node-documentation-service';
import { nodeDocumentationTools } from './tools-v2';
import { logger } from '../utils/logger';
import { MCPServerConfig } from '../types';
/**
* MCP Server focused on serving n8n node documentation and code
*/
export class N8NDocumentationMCPServer {
private server: Server;
private nodeService: NodeDocumentationService;
constructor(config: MCPServerConfig) {
logger.info('Initializing n8n Documentation MCP server', { config });
this.server = new Server(
{
name: 'n8n-node-documentation',
version: '2.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
this.nodeService = new NodeDocumentationService();
this.setupHandlers();
}
private setupHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: nodeDocumentationTools,
}));
// List available resources
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'nodes://list',
name: 'Available n8n Nodes',
description: 'List of all available n8n nodes',
mimeType: 'application/json',
},
{
uri: 'nodes://statistics',
name: 'Database Statistics',
description: 'Statistics about the node documentation database',
mimeType: 'application/json',
},
],
}));
// Read resources
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
try {
if (uri === 'nodes://list') {
const nodes = await this.nodeService.listNodes();
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(nodes.map(n => ({
nodeType: n.nodeType,
name: n.name,
displayName: n.displayName,
category: n.category,
description: n.description,
hasDocumentation: !!n.documentation,
hasExample: !!n.exampleWorkflow,
})), null, 2),
},
],
};
}
if (uri === 'nodes://statistics') {
const stats = this.nodeService.getStatistics();
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(stats, null, 2),
},
],
};
}
// Handle specific node URIs like nodes://info/n8n-nodes-base.if
const nodeMatch = uri.match(/^nodes:\/\/info\/(.+)$/);
if (nodeMatch) {
const nodeType = nodeMatch[1];
const nodeInfo = await this.nodeService.getNodeInfo(nodeType);
if (!nodeInfo) {
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${nodeType}`);
}
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(nodeInfo, null, 2),
},
],
};
}
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
} catch (error) {
logger.error('Resource read error:', error);
throw error instanceof McpError ? error : new McpError(
ErrorCode.InternalError,
`Failed to read resource: ${error instanceof Error ? error.message : String(error)}`
);
}
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'list_nodes':
return await this.handleListNodes(args);
case 'get_node_info':
return await this.handleGetNodeInfo(args);
case 'search_nodes':
return await this.handleSearchNodes(args);
case 'get_node_example':
return await this.handleGetNodeExample(args);
case 'get_node_source_code':
return await this.handleGetNodeSourceCode(args);
case 'get_node_documentation':
return await this.handleGetNodeDocumentation(args);
case 'rebuild_database':
return await this.handleRebuildDatabase(args);
case 'get_database_statistics':
return await this.handleGetStatistics();
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error) {
logger.error(`Tool execution error (${name}):`, error);
throw error instanceof McpError ? error : new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
private async handleListNodes(args: any): Promise<any> {
const nodes = await this.nodeService.listNodes();
// Apply filters
let filtered = nodes;
if (args.category) {
filtered = filtered.filter(n => n.category === args.category);
}
if (args.packageName) {
filtered = filtered.filter(n => n.packageName === args.packageName);
}
if (args.isTrigger !== undefined) {
filtered = filtered.filter(n => n.isTrigger === args.isTrigger);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(filtered.map(n => ({
nodeType: n.nodeType,
name: n.name,
displayName: n.displayName,
category: n.category,
description: n.description,
packageName: n.packageName,
hasDocumentation: !!n.documentation,
hasExample: !!n.exampleWorkflow,
isTrigger: n.isTrigger,
isWebhook: n.isWebhook,
})), null, 2),
},
],
};
}
private async handleGetNodeInfo(args: any): Promise<any> {
if (!args.nodeType) {
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
}
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
if (!nodeInfo) {
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
nodeType: nodeInfo.nodeType,
name: nodeInfo.name,
displayName: nodeInfo.displayName,
description: nodeInfo.description,
category: nodeInfo.category,
packageName: nodeInfo.packageName,
sourceCode: nodeInfo.sourceCode,
credentialCode: nodeInfo.credentialCode,
documentation: nodeInfo.documentation,
documentationUrl: nodeInfo.documentationUrl,
exampleWorkflow: nodeInfo.exampleWorkflow,
exampleParameters: nodeInfo.exampleParameters,
propertiesSchema: nodeInfo.propertiesSchema,
isTrigger: nodeInfo.isTrigger,
isWebhook: nodeInfo.isWebhook,
}, null, 2),
},
],
};
}
private async handleSearchNodes(args: any): Promise<any> {
if (!args.query) {
throw new McpError(ErrorCode.InvalidParams, 'query is required');
}
const results = await this.nodeService.searchNodes({
query: args.query,
category: args.category,
limit: args.limit || 20,
});
// Filter by documentation if requested
let filtered = results;
if (args.hasDocumentation) {
filtered = filtered.filter(n => !!n.documentation);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(filtered.map(n => ({
nodeType: n.nodeType,
name: n.name,
displayName: n.displayName,
category: n.category,
description: n.description,
hasDocumentation: !!n.documentation,
hasExample: !!n.exampleWorkflow,
})), null, 2),
},
],
};
}
private async handleGetNodeExample(args: any): Promise<any> {
if (!args.nodeType) {
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
}
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
if (!nodeInfo) {
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
}
if (!nodeInfo.exampleWorkflow) {
return {
content: [
{
type: 'text',
text: `No example available for node: ${args.nodeType}`,
},
],
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify(nodeInfo.exampleWorkflow, null, 2),
},
],
};
}
private async handleGetNodeSourceCode(args: any): Promise<any> {
if (!args.nodeType) {
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
}
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
if (!nodeInfo) {
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
}
const response: any = {
nodeType: nodeInfo.nodeType,
sourceCode: nodeInfo.sourceCode,
};
if (args.includeCredentials && nodeInfo.credentialCode) {
response.credentialCode = nodeInfo.credentialCode;
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async handleGetNodeDocumentation(args: any): Promise<any> {
if (!args.nodeType) {
throw new McpError(ErrorCode.InvalidParams, 'nodeType is required');
}
const nodeInfo = await this.nodeService.getNodeInfo(args.nodeType);
if (!nodeInfo) {
throw new McpError(ErrorCode.InvalidRequest, `Node not found: ${args.nodeType}`);
}
if (!nodeInfo.documentation) {
return {
content: [
{
type: 'text',
text: `No documentation available for node: ${args.nodeType}`,
},
],
};
}
const content = args.format === 'plain'
? nodeInfo.documentation.replace(/[#*`]/g, '')
: nodeInfo.documentation;
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
private async handleRebuildDatabase(args: any): Promise<any> {
logger.info('Starting database rebuild...');
const stats = await this.nodeService.rebuildDatabase();
return {
content: [
{
type: 'text',
text: JSON.stringify({
message: 'Database rebuild complete',
stats,
}, null, 2),
},
],
};
}
private async handleGetStatistics(): Promise<any> {
const stats = this.nodeService.getStatistics();
return {
content: [
{
type: 'text',
text: JSON.stringify(stats, null, 2),
},
],
};
}
async start(): Promise<void> {
logger.info('Starting n8n Documentation MCP server...');
const transport = new StdioServerTransport();
await this.server.connect(transport);
logger.info('n8n Documentation MCP server started successfully');
}
async stop(): Promise<void> {
logger.info('Stopping n8n Documentation MCP server...');
await this.server.close();
this.nodeService.close();
logger.info('Server stopped');
}
}

144
src/mcp/tools-v2.ts Normal file
View File

@@ -0,0 +1,144 @@
import { ToolDefinition } from '../types';
/**
* Simplified MCP tools focused on serving n8n node documentation and code
*/
export const nodeDocumentationTools: ToolDefinition[] = [
{
name: 'list_nodes',
description: 'List all available n8n nodes with basic information',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Filter by category (e.g., "Core Nodes", "Flow", "Data Transformation")',
},
packageName: {
type: 'string',
description: 'Filter by package name (e.g., "n8n-nodes-base")',
},
isTrigger: {
type: 'boolean',
description: 'Filter to show only trigger nodes',
},
},
},
},
{
name: 'get_node_info',
description: 'Get complete information about a specific n8n node including source code, documentation, and examples',
inputSchema: {
type: 'object',
properties: {
nodeType: {
type: 'string',
description: 'The node type or name (e.g., "n8n-nodes-base.if", "If", "webhook")',
},
},
required: ['nodeType'],
},
},
{
name: 'search_nodes',
description: 'Search for n8n nodes by name, description, or documentation content',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (searches in node names, descriptions, and documentation)',
},
category: {
type: 'string',
description: 'Filter by category',
},
hasDocumentation: {
type: 'boolean',
description: 'Filter to show only nodes with documentation',
},
limit: {
type: 'number',
description: 'Maximum number of results to return',
default: 20,
},
},
required: ['query'],
},
},
{
name: 'get_node_example',
description: 'Get example workflow/usage for a specific n8n node',
inputSchema: {
type: 'object',
properties: {
nodeType: {
type: 'string',
description: 'The node type or name',
},
},
required: ['nodeType'],
},
},
{
name: 'get_node_source_code',
description: 'Get only the source code of a specific n8n node',
inputSchema: {
type: 'object',
properties: {
nodeType: {
type: 'string',
description: 'The node type or name',
},
includeCredentials: {
type: 'boolean',
description: 'Include credential type definitions if available',
default: false,
},
},
required: ['nodeType'],
},
},
{
name: 'get_node_documentation',
description: 'Get only the documentation for a specific n8n node',
inputSchema: {
type: 'object',
properties: {
nodeType: {
type: 'string',
description: 'The node type or name',
},
format: {
type: 'string',
enum: ['markdown', 'plain'],
description: 'Documentation format',
default: 'markdown',
},
},
required: ['nodeType'],
},
},
{
name: 'rebuild_database',
description: 'Rebuild the entire node database with latest information from n8n and documentation',
inputSchema: {
type: 'object',
properties: {
includeDocumentation: {
type: 'boolean',
description: 'Include documentation from n8n-docs repository',
default: true,
},
},
},
},
{
name: 'get_database_statistics',
description: 'Get statistics about the node database',
inputSchema: {
type: 'object',
properties: {},
},
},
];

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env node
import { NodeDocumentationService } from '../services/node-documentation-service';
import { logger } from '../utils/logger';
async function rebuildDatabase() {
console.log('🔄 Starting complete database rebuild...\n');
const service = new NodeDocumentationService();
try {
const startTime = Date.now();
console.log('1⃣ Initializing services...');
console.log('2⃣ Fetching n8n-docs repository...');
console.log('3⃣ Discovering available nodes...');
console.log('4⃣ Extracting node information...\n');
const stats = await service.rebuildDatabase();
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log('\n📊 Rebuild Results:');
console.log(` Total nodes processed: ${stats.total}`);
console.log(` Successfully stored: ${stats.successful}`);
console.log(` Failed: ${stats.failed}`);
console.log(` Duration: ${duration}s`);
if (stats.errors.length > 0) {
console.log('\n⚠ First 5 errors:');
stats.errors.slice(0, 5).forEach(error => {
console.log(` - ${error}`);
});
if (stats.errors.length > 5) {
console.log(` ... and ${stats.errors.length - 5} more errors`);
}
}
// Get final statistics
const dbStats = service.getStatistics();
console.log('\n📈 Database Statistics:');
console.log(` Total nodes: ${dbStats.totalNodes}`);
console.log(` Nodes with documentation: ${dbStats.nodesWithDocs}`);
console.log(` Nodes with examples: ${dbStats.nodesWithExamples}`);
console.log(` Trigger nodes: ${dbStats.triggerNodes}`);
console.log(` Webhook nodes: ${dbStats.webhookNodes}`);
console.log(` Total packages: ${dbStats.totalPackages}`);
console.log('\n✨ Database rebuild complete!');
} catch (error) {
console.error('\n❌ Database rebuild failed:', error);
process.exit(1);
} finally {
service.close();
}
}
// Run if called directly
if (require.main === module) {
rebuildDatabase().catch(error => {
console.error(error);
process.exit(1);
});
}
export { rebuildDatabase };

View File

@@ -0,0 +1,547 @@
import Database from 'better-sqlite3';
import { createHash } from 'crypto';
import path from 'path';
import { promises as fs } from 'fs';
import { logger } from '../utils/logger';
import { NodeSourceExtractor } from '../utils/node-source-extractor';
import { DocumentationFetcher } from '../utils/documentation-fetcher';
import { ExampleGenerator } from '../utils/example-generator';
interface NodeInfo {
nodeType: string;
name: string;
displayName: string;
description: string;
category?: string;
subcategory?: string;
icon?: string;
sourceCode: string;
credentialCode?: string;
documentation?: string;
documentationUrl?: string;
exampleWorkflow?: any;
exampleParameters?: any;
propertiesSchema?: any;
packageName: string;
version?: string;
codexData?: any;
aliases?: string[];
hasCredentials: boolean;
isTrigger: boolean;
isWebhook: boolean;
}
interface SearchOptions {
query?: string;
nodeType?: string;
packageName?: string;
category?: string;
hasCredentials?: boolean;
isTrigger?: boolean;
limit?: number;
}
export class NodeDocumentationService {
private db: Database.Database;
private extractor: NodeSourceExtractor;
private docsFetcher: DocumentationFetcher;
constructor(dbPath?: string) {
const databasePath = dbPath || process.env.NODE_DB_PATH || path.join(process.cwd(), 'data', 'nodes.db');
// Ensure directory exists
const dbDir = path.dirname(databasePath);
if (!require('fs').existsSync(dbDir)) {
require('fs').mkdirSync(dbDir, { recursive: true });
}
this.db = new Database(databasePath);
this.extractor = new NodeSourceExtractor();
this.docsFetcher = new DocumentationFetcher();
// Initialize database with new schema
this.initializeDatabase();
logger.info('Node Documentation Service initialized');
}
private initializeDatabase(): void {
// Execute the schema directly
const schema = `
-- Main nodes table with documentation and examples
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_type TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
display_name TEXT,
description TEXT,
category TEXT,
subcategory TEXT,
icon TEXT,
-- Source code
source_code TEXT NOT NULL,
credential_code TEXT,
code_hash TEXT NOT NULL,
code_length INTEGER NOT NULL,
-- Documentation
documentation_markdown TEXT,
documentation_url TEXT,
-- Example usage
example_workflow TEXT,
example_parameters TEXT,
properties_schema TEXT,
-- Metadata
package_name TEXT NOT NULL,
version TEXT,
codex_data TEXT,
aliases TEXT,
-- Flags
has_credentials INTEGER DEFAULT 0,
is_trigger INTEGER DEFAULT 0,
is_webhook INTEGER DEFAULT 0,
-- Timestamps
extracted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_nodes_package_name ON nodes(package_name);
CREATE INDEX IF NOT EXISTS idx_nodes_category ON nodes(category);
CREATE INDEX IF NOT EXISTS idx_nodes_code_hash ON nodes(code_hash);
CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
CREATE INDEX IF NOT EXISTS idx_nodes_is_trigger ON nodes(is_trigger);
-- Full Text Search
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
node_type,
name,
display_name,
description,
category,
documentation_markdown,
aliases,
content=nodes,
content_rowid=id
);
-- Triggers for FTS
CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes
BEGIN
INSERT INTO nodes_fts(rowid, node_type, name, display_name, description, category, documentation_markdown, aliases)
VALUES (new.id, new.node_type, new.name, new.display_name, new.description, new.category, new.documentation_markdown, new.aliases);
END;
CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes
BEGIN
DELETE FROM nodes_fts WHERE rowid = old.id;
END;
CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes
BEGIN
DELETE FROM nodes_fts WHERE rowid = old.id;
INSERT INTO nodes_fts(rowid, node_type, name, display_name, description, category, documentation_markdown, aliases)
VALUES (new.id, new.node_type, new.name, new.display_name, new.description, new.category, new.documentation_markdown, new.aliases);
END;
-- Documentation sources table
CREATE TABLE IF NOT EXISTS documentation_sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL,
commit_hash TEXT,
fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Statistics table
CREATE TABLE IF NOT EXISTS extraction_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
total_nodes INTEGER NOT NULL,
nodes_with_docs INTEGER NOT NULL,
nodes_with_examples INTEGER NOT NULL,
total_code_size INTEGER NOT NULL,
total_docs_size INTEGER NOT NULL,
extraction_date DATETIME DEFAULT CURRENT_TIMESTAMP
);
`;
this.db.exec(schema);
}
/**
* Store complete node information including docs and examples
*/
async storeNode(nodeInfo: NodeInfo): Promise<void> {
const hash = this.generateHash(nodeInfo.sourceCode);
const stmt = this.db.prepare(`
INSERT OR REPLACE INTO nodes (
node_type, name, display_name, description, category, subcategory, icon,
source_code, credential_code, code_hash, code_length,
documentation_markdown, documentation_url,
example_workflow, example_parameters, properties_schema,
package_name, version, codex_data, aliases,
has_credentials, is_trigger, is_webhook
) VALUES (
@nodeType, @name, @displayName, @description, @category, @subcategory, @icon,
@sourceCode, @credentialCode, @hash, @codeLength,
@documentation, @documentationUrl,
@exampleWorkflow, @exampleParameters, @propertiesSchema,
@packageName, @version, @codexData, @aliases,
@hasCredentials, @isTrigger, @isWebhook
)
`);
stmt.run({
nodeType: nodeInfo.nodeType,
name: nodeInfo.name,
displayName: nodeInfo.displayName || nodeInfo.name,
description: nodeInfo.description || '',
category: nodeInfo.category || 'Other',
subcategory: nodeInfo.subcategory || null,
icon: nodeInfo.icon || null,
sourceCode: nodeInfo.sourceCode,
credentialCode: nodeInfo.credentialCode || null,
hash,
codeLength: nodeInfo.sourceCode.length,
documentation: nodeInfo.documentation || null,
documentationUrl: nodeInfo.documentationUrl || null,
exampleWorkflow: nodeInfo.exampleWorkflow ? JSON.stringify(nodeInfo.exampleWorkflow) : null,
exampleParameters: nodeInfo.exampleParameters ? JSON.stringify(nodeInfo.exampleParameters) : null,
propertiesSchema: nodeInfo.propertiesSchema ? JSON.stringify(nodeInfo.propertiesSchema) : null,
packageName: nodeInfo.packageName,
version: nodeInfo.version || null,
codexData: nodeInfo.codexData ? JSON.stringify(nodeInfo.codexData) : null,
aliases: nodeInfo.aliases ? JSON.stringify(nodeInfo.aliases) : null,
hasCredentials: nodeInfo.hasCredentials ? 1 : 0,
isTrigger: nodeInfo.isTrigger ? 1 : 0,
isWebhook: nodeInfo.isWebhook ? 1 : 0
});
}
/**
* Get complete node information
*/
async getNodeInfo(nodeType: string): Promise<NodeInfo | null> {
const stmt = this.db.prepare(`
SELECT * FROM nodes WHERE node_type = ? OR name = ? COLLATE NOCASE
`);
const row = stmt.get(nodeType, nodeType);
if (!row) return null;
return this.rowToNodeInfo(row);
}
/**
* Search nodes with various filters
*/
async searchNodes(options: SearchOptions): Promise<NodeInfo[]> {
let query = 'SELECT * FROM nodes WHERE 1=1';
const params: any = {};
if (options.query) {
query += ` AND id IN (
SELECT rowid FROM nodes_fts
WHERE nodes_fts MATCH @query
)`;
params.query = options.query;
}
if (options.nodeType) {
query += ' AND node_type LIKE @nodeType';
params.nodeType = `%${options.nodeType}%`;
}
if (options.packageName) {
query += ' AND package_name = @packageName';
params.packageName = options.packageName;
}
if (options.category) {
query += ' AND category = @category';
params.category = options.category;
}
if (options.hasCredentials !== undefined) {
query += ' AND has_credentials = @hasCredentials';
params.hasCredentials = options.hasCredentials ? 1 : 0;
}
if (options.isTrigger !== undefined) {
query += ' AND is_trigger = @isTrigger';
params.isTrigger = options.isTrigger ? 1 : 0;
}
query += ' ORDER BY name LIMIT @limit';
params.limit = options.limit || 20;
const stmt = this.db.prepare(query);
const rows = stmt.all(params);
return rows.map(row => this.rowToNodeInfo(row));
}
/**
* List all nodes
*/
async listNodes(): Promise<NodeInfo[]> {
const stmt = this.db.prepare('SELECT * FROM nodes ORDER BY name');
const rows = stmt.all();
return rows.map(row => this.rowToNodeInfo(row));
}
/**
* Extract and store all nodes with documentation
*/
async rebuildDatabase(): Promise<{
total: number;
successful: number;
failed: number;
errors: string[];
}> {
logger.info('Starting complete database rebuild...');
// Clear existing data
this.db.exec('DELETE FROM nodes');
this.db.exec('DELETE FROM extraction_stats');
// Ensure documentation repository is available
await this.docsFetcher.ensureDocsRepository();
const stats = {
total: 0,
successful: 0,
failed: 0,
errors: [] as string[]
};
try {
// Get all available nodes
const availableNodes = await this.extractor.listAvailableNodes();
stats.total = availableNodes.length;
logger.info(`Found ${stats.total} nodes to process`);
// Process nodes in batches
const batchSize = 10;
for (let i = 0; i < availableNodes.length; i += batchSize) {
const batch = availableNodes.slice(i, i + batchSize);
await Promise.all(batch.map(async (node) => {
try {
// Build node type from package name and node name
const nodeType = `n8n-nodes-base.${node.name}`;
// Extract source code
const nodeData = await this.extractor.extractNodeSource(nodeType);
if (!nodeData || !nodeData.sourceCode) {
throw new Error('Failed to extract node source');
}
// Parse node definition to get metadata
const nodeDefinition = this.parseNodeDefinition(nodeData.sourceCode);
// Get documentation
const docs = await this.docsFetcher.getNodeDocumentation(nodeType);
// Generate example
const example = ExampleGenerator.generateFromNodeDefinition(nodeDefinition);
// Prepare node info
const nodeInfo: NodeInfo = {
nodeType: nodeType,
name: node.name,
displayName: nodeDefinition.displayName || node.displayName || node.name,
description: nodeDefinition.description || node.description || '',
category: nodeDefinition.category || 'Other',
subcategory: nodeDefinition.subcategory,
icon: nodeDefinition.icon,
sourceCode: nodeData.sourceCode,
credentialCode: nodeData.credentialCode,
documentation: docs?.markdown,
documentationUrl: docs?.url,
exampleWorkflow: example,
exampleParameters: example.nodes[0]?.parameters,
propertiesSchema: nodeDefinition.properties,
packageName: nodeData.packageInfo?.name || 'n8n-nodes-base',
version: nodeDefinition.version,
codexData: nodeDefinition.codex,
aliases: nodeDefinition.alias,
hasCredentials: !!nodeData.credentialCode,
isTrigger: node.name.toLowerCase().includes('trigger'),
isWebhook: node.name.toLowerCase().includes('webhook')
};
// Store in database
await this.storeNode(nodeInfo);
stats.successful++;
logger.debug(`Processed node: ${nodeType}`);
} catch (error) {
stats.failed++;
const errorMsg = `Failed to process ${node.name}: ${error instanceof Error ? error.message : String(error)}`;
stats.errors.push(errorMsg);
logger.error(errorMsg);
}
}));
logger.info(`Progress: ${Math.min(i + batchSize, availableNodes.length)}/${stats.total} nodes processed`);
}
// Store statistics
this.storeStatistics(stats);
logger.info(`Database rebuild complete: ${stats.successful} successful, ${stats.failed} failed`);
} catch (error) {
logger.error('Database rebuild failed:', error);
throw error;
}
return stats;
}
/**
* Parse node definition from source code
*/
private parseNodeDefinition(sourceCode: string): any {
try {
// Try to extract the description object from the source
const descMatch = sourceCode.match(/description\s*[:=]\s*({[\s\S]*?})\s*[,;]/);
if (descMatch) {
// Clean up the match and try to parse it
const descStr = descMatch[1]
.replace(/(['"])?([a-zA-Z0-9_]+)(['"])?\s*:/g, '"$2":') // Quote property names
.replace(/:\s*'([^']*)'/g, ': "$1"') // Convert single quotes to double
.replace(/,\s*}/g, '}'); // Remove trailing commas
return JSON.parse(descStr);
}
} catch (error) {
logger.debug('Failed to parse node definition:', error);
}
// Return minimal definition if parsing fails
return {
displayName: '',
description: '',
properties: []
};
}
/**
* Convert database row to NodeInfo
*/
private rowToNodeInfo(row: any): NodeInfo {
return {
nodeType: row.node_type,
name: row.name,
displayName: row.display_name,
description: row.description,
category: row.category,
subcategory: row.subcategory,
icon: row.icon,
sourceCode: row.source_code,
credentialCode: row.credential_code,
documentation: row.documentation_markdown,
documentationUrl: row.documentation_url,
exampleWorkflow: row.example_workflow ? JSON.parse(row.example_workflow) : null,
exampleParameters: row.example_parameters ? JSON.parse(row.example_parameters) : null,
propertiesSchema: row.properties_schema ? JSON.parse(row.properties_schema) : null,
packageName: row.package_name,
version: row.version,
codexData: row.codex_data ? JSON.parse(row.codex_data) : null,
aliases: row.aliases ? JSON.parse(row.aliases) : null,
hasCredentials: row.has_credentials === 1,
isTrigger: row.is_trigger === 1,
isWebhook: row.is_webhook === 1
};
}
/**
* Generate hash for content
*/
private generateHash(content: string): string {
return createHash('sha256').update(content).digest('hex');
}
/**
* Store extraction statistics
*/
private storeStatistics(stats: any): void {
const stmt = this.db.prepare(`
INSERT INTO extraction_stats (
total_nodes, nodes_with_docs, nodes_with_examples,
total_code_size, total_docs_size
) VALUES (?, ?, ?, ?, ?)
`);
// Calculate sizes
const sizeStats = this.db.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN documentation_markdown IS NOT NULL THEN 1 ELSE 0 END) as with_docs,
SUM(CASE WHEN example_workflow IS NOT NULL THEN 1 ELSE 0 END) as with_examples,
SUM(code_length) as code_size,
SUM(LENGTH(documentation_markdown)) as docs_size
FROM nodes
`).get() as any;
stmt.run(
stats.successful,
sizeStats?.with_docs || 0,
sizeStats?.with_examples || 0,
sizeStats?.code_size || 0,
sizeStats?.docs_size || 0
);
}
/**
* Get database statistics
*/
getStatistics(): any {
const stats = this.db.prepare(`
SELECT
COUNT(*) as totalNodes,
COUNT(DISTINCT package_name) as totalPackages,
SUM(code_length) as totalCodeSize,
SUM(CASE WHEN documentation_markdown IS NOT NULL THEN 1 ELSE 0 END) as nodesWithDocs,
SUM(CASE WHEN example_workflow IS NOT NULL THEN 1 ELSE 0 END) as nodesWithExamples,
SUM(has_credentials) as nodesWithCredentials,
SUM(is_trigger) as triggerNodes,
SUM(is_webhook) as webhookNodes
FROM nodes
`).get() as any;
const packages = this.db.prepare(`
SELECT package_name as package, COUNT(*) as count
FROM nodes
GROUP BY package_name
ORDER BY count DESC
`).all();
return {
totalNodes: stats?.totalNodes || 0,
totalPackages: stats?.totalPackages || 0,
totalCodeSize: stats?.totalCodeSize || 0,
nodesWithDocs: stats?.nodesWithDocs || 0,
nodesWithExamples: stats?.nodesWithExamples || 0,
nodesWithCredentials: stats?.nodesWithCredentials || 0,
triggerNodes: stats?.triggerNodes || 0,
webhookNodes: stats?.webhookNodes || 0,
packageDistribution: packages
};
}
/**
* Close database connection
*/
close(): void {
this.db.close();
}
}

View File

@@ -0,0 +1,241 @@
import { promises as fs } from 'fs';
import path from 'path';
import { logger } from './logger';
import { execSync } from 'child_process';
interface NodeDocumentation {
markdown: string;
url: string;
examples?: any[];
}
export class DocumentationFetcher {
private docsPath: string;
private docsRepoUrl = 'https://github.com/n8n-io/n8n-docs.git';
private cloned = false;
constructor(docsPath?: string) {
this.docsPath = docsPath || path.join(process.cwd(), 'temp', 'n8n-docs');
}
/**
* Clone or update the n8n-docs repository
*/
async ensureDocsRepository(): Promise<void> {
try {
// Check if directory exists
const exists = await fs.access(this.docsPath).then(() => true).catch(() => false);
if (!exists) {
logger.info('Cloning n8n-docs repository...');
await fs.mkdir(path.dirname(this.docsPath), { recursive: true });
execSync(`git clone --depth 1 ${this.docsRepoUrl} ${this.docsPath}`, {
stdio: 'pipe'
});
logger.info('n8n-docs repository cloned successfully');
} else {
logger.info('Updating n8n-docs repository...');
execSync('git pull --ff-only', {
cwd: this.docsPath,
stdio: 'pipe'
});
logger.info('n8n-docs repository updated');
}
this.cloned = true;
} catch (error) {
logger.error('Failed to clone/update n8n-docs repository:', error);
throw error;
}
}
/**
* Get documentation for a specific node
*/
async getNodeDocumentation(nodeType: string): Promise<NodeDocumentation | null> {
if (!this.cloned) {
await this.ensureDocsRepository();
}
try {
// Convert node type to documentation path
// e.g., "n8n-nodes-base.if" -> "if"
const nodeName = this.extractNodeName(nodeType);
// Common documentation paths to check
const possiblePaths = [
path.join(this.docsPath, 'docs', 'integrations', 'builtin', 'core-nodes', `${nodeName}.md`),
path.join(this.docsPath, 'docs', 'integrations', 'builtin', 'app-nodes', `${nodeName}.md`),
path.join(this.docsPath, 'docs', 'integrations', 'builtin', 'trigger-nodes', `${nodeName}.md`),
path.join(this.docsPath, 'docs', 'code-examples', 'expressions', `${nodeName}.md`),
// Generic search in docs folder
path.join(this.docsPath, 'docs', '**', `${nodeName}.md`)
];
for (const docPath of possiblePaths) {
try {
const content = await fs.readFile(docPath, 'utf-8');
const url = this.generateDocUrl(docPath);
return {
markdown: content,
url,
examples: this.extractExamples(content)
};
} catch (error) {
// Continue to next path
continue;
}
}
// If no exact match, try to find by searching
const foundPath = await this.searchForNodeDoc(nodeName);
if (foundPath) {
const content = await fs.readFile(foundPath, 'utf-8');
return {
markdown: content,
url: this.generateDocUrl(foundPath),
examples: this.extractExamples(content)
};
}
logger.warn(`No documentation found for node: ${nodeType}`);
return null;
} catch (error) {
logger.error(`Failed to get documentation for ${nodeType}:`, error);
return null;
}
}
/**
* Extract node name from node type
*/
private extractNodeName(nodeType: string): string {
// Handle different node type formats
// "n8n-nodes-base.if" -> "if"
// "@n8n/n8n-nodes-langchain.Agent" -> "agent"
const parts = nodeType.split('.');
const name = parts[parts.length - 1];
return name.toLowerCase();
}
/**
* Search for node documentation file
*/
private async searchForNodeDoc(nodeName: string): Promise<string | null> {
try {
const result = execSync(
`find ${this.docsPath}/docs -name "*.md" -type f | grep -i "${nodeName}" | head -1`,
{ encoding: 'utf-8', stdio: 'pipe' }
).trim();
return result || null;
} catch (error) {
return null;
}
}
/**
* Generate documentation URL from file path
*/
private generateDocUrl(filePath: string): string {
const relativePath = path.relative(this.docsPath, filePath);
const urlPath = relativePath
.replace(/^docs\//, '')
.replace(/\.md$/, '')
.replace(/\\/g, '/');
return `https://docs.n8n.io/${urlPath}`;
}
/**
* Extract code examples from markdown content
*/
private extractExamples(markdown: string): any[] {
const examples: any[] = [];
// Extract JSON code blocks
const jsonCodeBlockRegex = /```json\n([\s\S]*?)```/g;
let match;
while ((match = jsonCodeBlockRegex.exec(markdown)) !== null) {
try {
const json = JSON.parse(match[1]);
examples.push(json);
} catch (error) {
// Not valid JSON, skip
}
}
// Extract workflow examples
const workflowExampleRegex = /## Example.*?\n([\s\S]*?)(?=\n##|\n#|$)/gi;
while ((match = workflowExampleRegex.exec(markdown)) !== null) {
const exampleText = match[1];
// Try to find JSON in the example section
const jsonMatch = exampleText.match(/```json\n([\s\S]*?)```/);
if (jsonMatch) {
try {
const json = JSON.parse(jsonMatch[1]);
examples.push(json);
} catch (error) {
// Not valid JSON
}
}
}
return examples;
}
/**
* Get all available documentation files
*/
async getAllDocumentationFiles(): Promise<Map<string, string>> {
if (!this.cloned) {
await this.ensureDocsRepository();
}
const docMap = new Map<string, string>();
try {
const findDocs = execSync(
`find ${this.docsPath}/docs -name "*.md" -type f | grep -E "(core-nodes|app-nodes|trigger-nodes)/"`,
{ encoding: 'utf-8', stdio: 'pipe' }
).trim().split('\n');
for (const docPath of findDocs) {
if (!docPath) continue;
const filename = path.basename(docPath, '.md');
const content = await fs.readFile(docPath, 'utf-8');
// Try to extract the node type from the content
const nodeTypeMatch = content.match(/node[_-]?type[:\s]+["']?([^"'\s]+)["']?/i);
if (nodeTypeMatch) {
docMap.set(nodeTypeMatch[1], docPath);
} else {
// Use filename as fallback
docMap.set(filename, docPath);
}
}
logger.info(`Found ${docMap.size} documentation files`);
return docMap;
} catch (error) {
logger.error('Failed to get documentation files:', error);
return docMap;
}
}
/**
* Clean up cloned repository
*/
async cleanup(): Promise<void> {
try {
await fs.rm(this.docsPath, { recursive: true, force: true });
this.cloned = false;
logger.info('Cleaned up documentation repository');
} catch (error) {
logger.error('Failed to cleanup docs repository:', error);
}
}
}

View File

@@ -0,0 +1,267 @@
import { logger } from './logger';
interface NodeExample {
nodes: any[];
connections: any;
pinData?: any;
meta?: any;
}
interface NodeParameter {
name: string;
type: string;
default?: any;
options?: any[];
displayOptions?: any;
}
export class ExampleGenerator {
/**
* Generate example workflow for a node
*/
static generateNodeExample(nodeType: string, nodeData: any): NodeExample {
const nodeName = this.getNodeName(nodeType);
const nodeId = this.generateNodeId();
// Base example structure
const example: NodeExample = {
nodes: [{
parameters: this.generateExampleParameters(nodeType, nodeData),
type: nodeType,
typeVersion: nodeData.typeVersion || 1,
position: [220, 120],
id: nodeId,
name: nodeName
}],
connections: {
[nodeName]: {
main: [[]]
}
},
pinData: {},
meta: {
templateCredsSetupCompleted: true,
instanceId: this.generateInstanceId()
}
};
// Add specific configurations based on node type
this.addNodeSpecificConfig(nodeType, example, nodeData);
return example;
}
/**
* Generate example parameters based on node type
*/
private static generateExampleParameters(nodeType: string, nodeData: any): any {
const params: any = {};
// Extract node name for specific handling
const nodeName = nodeType.split('.').pop()?.toLowerCase() || '';
// Common node examples
switch (nodeName) {
case 'if':
return {
conditions: {
options: {
caseSensitive: true,
leftValue: "",
typeValidation: "strict",
version: 2
},
conditions: [{
id: this.generateNodeId(),
leftValue: "={{ $json }}",
rightValue: "",
operator: {
type: "object",
operation: "notEmpty",
singleValue: true
}
}],
combinator: "and"
},
options: {}
};
case 'webhook':
return {
httpMethod: "POST",
path: "webhook-path",
responseMode: "onReceived",
responseData: "allEntries",
options: {}
};
case 'httprequest':
return {
method: "GET",
url: "https://api.example.com/data",
authentication: "none",
options: {},
headerParametersUi: {
parameter: []
}
};
case 'function':
return {
functionCode: "// Add your JavaScript code here\nreturn $input.all();"
};
case 'set':
return {
mode: "manual",
duplicateItem: false,
values: {
string: [{
name: "myField",
value: "myValue"
}]
}
};
case 'split':
return {
batchSize: 10,
options: {}
};
default:
// Generate generic parameters from node properties
return this.generateGenericParameters(nodeData);
}
}
/**
* Generate generic parameters from node properties
*/
private static generateGenericParameters(nodeData: any): any {
const params: any = {};
if (nodeData.properties) {
for (const prop of nodeData.properties) {
if (prop.default !== undefined) {
params[prop.name] = prop.default;
} else if (prop.type === 'string') {
params[prop.name] = '';
} else if (prop.type === 'number') {
params[prop.name] = 0;
} else if (prop.type === 'boolean') {
params[prop.name] = false;
} else if (prop.type === 'options' && prop.options?.length > 0) {
params[prop.name] = prop.options[0].value;
}
}
}
return params;
}
/**
* Add node-specific configurations
*/
private static addNodeSpecificConfig(nodeType: string, example: NodeExample, nodeData: any): void {
const nodeName = nodeType.split('.').pop()?.toLowerCase() || '';
// Add specific connection structures for different node types
switch (nodeName) {
case 'if':
// IF node has true/false outputs
example.connections[example.nodes[0].name] = {
main: [[], []] // Two outputs: true, false
};
break;
case 'switch':
// Switch node can have multiple outputs
const outputs = nodeData.outputs || 3;
example.connections[example.nodes[0].name] = {
main: Array(outputs).fill([])
};
break;
case 'merge':
// Merge node has multiple inputs
example.nodes[0].position = [400, 120];
// Add dummy input nodes
example.nodes.push({
parameters: {},
type: "n8n-nodes-base.noOp",
typeVersion: 1,
position: [200, 60],
id: this.generateNodeId(),
name: "Input 1"
});
example.nodes.push({
parameters: {},
type: "n8n-nodes-base.noOp",
typeVersion: 1,
position: [200, 180],
id: this.generateNodeId(),
name: "Input 2"
});
example.connections = {
"Input 1": { main: [[{ node: example.nodes[0].name, type: "main", index: 0 }]] },
"Input 2": { main: [[{ node: example.nodes[0].name, type: "main", index: 1 }]] },
[example.nodes[0].name]: { main: [[]] }
};
break;
}
// Add credentials if needed
if (nodeData.credentials?.length > 0) {
example.nodes[0].credentials = {};
for (const cred of nodeData.credentials) {
example.nodes[0].credentials[cred.name] = {
id: this.generateNodeId(),
name: `${cred.name} account`
};
}
}
}
/**
* Extract display name from node type
*/
private static getNodeName(nodeType: string): string {
const parts = nodeType.split('.');
const name = parts[parts.length - 1];
return name.charAt(0).toUpperCase() + name.slice(1);
}
/**
* Generate a random node ID
*/
private static generateNodeId(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* Generate instance ID
*/
private static generateInstanceId(): string {
return Array(64).fill(0).map(() => Math.floor(Math.random() * 16).toString(16)).join('');
}
/**
* Generate example from node definition
*/
static generateFromNodeDefinition(nodeDefinition: any): NodeExample {
const nodeType = nodeDefinition.description?.name || 'n8n-nodes-base.node';
const nodeData = {
typeVersion: nodeDefinition.description?.version || 1,
properties: nodeDefinition.description?.properties || [],
credentials: nodeDefinition.description?.credentials || [],
outputs: nodeDefinition.description?.outputs || ['main']
};
return this.generateNodeExample(nodeType, nodeData);
}
}