UNPKG

n8n-mcp

Version:

Integration between n8n workflow automation and Model Context Protocol (MCP)

251 lines (246 loc) 10.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DocumentationGenerator = exports.DocumentationSummarySchema = void 0; exports.createDocumentationGenerator = createDocumentationGenerator; const openai_1 = __importDefault(require("openai")); const zod_1 = require("zod"); const logger_1 = require("../utils/logger"); exports.DocumentationSummarySchema = zod_1.z.object({ purpose: zod_1.z.string().describe('What this node does in 1-2 sentences'), capabilities: zod_1.z.array(zod_1.z.string()).max(10).describe('Key features and operations'), authentication: zod_1.z.string().describe('How to authenticate (API key, OAuth, None, etc.)'), commonUseCases: zod_1.z.array(zod_1.z.string()).max(5).describe('Practical use case examples'), limitations: zod_1.z.array(zod_1.z.string()).max(5).describe('Known limitations or caveats'), relatedNodes: zod_1.z.array(zod_1.z.string()).max(5).describe('Related n8n nodes if mentioned'), }); const DEFAULT_CONFIG = { model: 'qwen3-4b-thinking-2507', apiKey: 'not-needed', timeout: 60000, maxTokens: 2000, sendThinkingKwargs: true, }; class DocumentationGenerator { constructor(config) { const fullConfig = { ...DEFAULT_CONFIG, ...config }; this.baseUrl = config.baseUrl; this.apiKey = fullConfig.apiKey; this.client = new openai_1.default({ baseURL: config.baseUrl, apiKey: fullConfig.apiKey, timeout: fullConfig.timeout, }); this.model = fullConfig.model; this.maxTokens = fullConfig.maxTokens; this.timeout = fullConfig.timeout; this.temperature = fullConfig.temperature; this.sendThinkingKwargs = fullConfig.sendThinkingKwargs; } async generateSummary(input) { try { const prompt = this.buildPrompt(input); const completion = await this.chatCompletion([ { role: 'system', content: this.getSystemPrompt() }, { role: 'user', content: prompt }, ], this.maxTokens); const content = completion.choices[0]?.message?.content; if (!content) { throw new Error('No content in LLM response'); } const jsonContent = this.extractJson(content); const parsed = JSON.parse(jsonContent); const truncated = this.truncateArrayFields(parsed); const validated = exports.DocumentationSummarySchema.parse(truncated); return { nodeType: input.nodeType, summary: validated, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger_1.logger.error(`Error generating documentation for ${input.nodeType}:`, error); return { nodeType: input.nodeType, summary: this.getDefaultSummary(input), error: errorMessage, }; } } async generateBatch(inputs, concurrency = 3, progressCallback) { const results = []; const total = inputs.length; logger_1.logger.info(`Generating documentation for ${total} nodes (concurrency: ${concurrency})...`); for (let i = 0; i < inputs.length; i += concurrency) { const batch = inputs.slice(i, i + concurrency); const batchPromises = batch.map((input) => this.generateSummary(input)); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); if (progressCallback) { progressCallback('Generating documentation', Math.min(i + concurrency, total), total); } if (i + concurrency < inputs.length) { await this.sleep(100); } } const successCount = results.filter((r) => !r.error).length; logger_1.logger.info(`Generated ${successCount}/${total} documentation summaries successfully`); return results; } buildPrompt(input) { const truncatedReadme = this.truncateReadme(input.readme, 6000); return ` Node Information: - Name: ${input.displayName} - Type: ${input.nodeType} - Package: ${input.npmPackageName || 'unknown'} - Description: ${input.description || 'No description provided'} README Content: ${truncatedReadme} Based on the README and node information above, generate a structured documentation summary. `.trim(); } getSystemPrompt() { return `You are analyzing an n8n community node to generate documentation for AI assistants. Your task: Extract key information from the README and create a structured JSON summary. Output format (JSON only, no markdown): { "purpose": "What this node does in 1-2 sentences", "capabilities": ["feature1", "feature2", "feature3"], "authentication": "How to authenticate (e.g., 'API key required', 'OAuth2', 'None')", "commonUseCases": ["use case 1", "use case 2"], "limitations": ["limitation 1"] or [] if none mentioned, "relatedNodes": ["related n8n node types"] or [] if none mentioned } Guidelines: - Focus on information useful for AI assistants configuring workflows - Be concise but comprehensive - For capabilities, list specific operations/actions supported - For authentication, identify the auth method from README - For limitations, note any mentioned constraints or missing features - Respond with valid JSON only, no additional text`; } extractJson(content) { const stripped = content.replace(/<think>[\s\S]*?<\/think>/g, '').trim(); const jsonBlockMatch = stripped.match(/```(?:json)?\s*([\s\S]*?)```/); if (jsonBlockMatch) { return jsonBlockMatch[1].trim(); } const jsonMatch = stripped.match(/\{[\s\S]*\}/); if (jsonMatch) { return jsonMatch[0]; } return stripped; } truncateArrayFields(parsed) { const limits = { capabilities: 10, commonUseCases: 5, limitations: 5, relatedNodes: 5, }; const result = { ...parsed }; for (const [field, maxLength] of Object.entries(limits)) { if (Array.isArray(result[field]) && result[field].length > maxLength) { result[field] = result[field].slice(0, maxLength); } } return result; } truncateReadme(readme, maxLength) { if (readme.length <= maxLength) { return readme; } const truncated = readme.slice(0, maxLength); const lastParagraph = truncated.lastIndexOf('\n\n'); if (lastParagraph > maxLength * 0.7) { return truncated.slice(0, lastParagraph) + '\n\n[README truncated...]'; } return truncated + '\n\n[README truncated...]'; } getDefaultSummary(input) { return { purpose: input.description || `Community node: ${input.displayName}`, capabilities: [], authentication: 'See README for authentication details', commonUseCases: [], limitations: ['Documentation could not be automatically generated'], relatedNodes: [], }; } async testConnection() { try { const completion = await this.chatCompletion([ { role: 'user', content: 'Hello' }, ], 200); if (completion.choices[0]?.message?.content) { return { success: true, message: `Connected to ${this.model}` }; } return { success: false, message: 'No response from LLM' }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; return { success: false, message: `Connection failed: ${message}` }; } } async chatCompletion(messages, maxTokens) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(this.apiKey !== 'not-needed' ? { Authorization: `Bearer ${this.apiKey}` } : {}), }, body: JSON.stringify({ model: this.model, messages, max_completion_tokens: maxTokens, ...(this.temperature !== undefined ? { temperature: this.temperature } : {}), ...(this.sendThinkingKwargs ? { chat_template_kwargs: { enable_thinking: false } } : {}), }), signal: controller.signal, }); if (!response.ok) { const text = await response.text(); throw new Error(`${response.status} ${text}`); } return (await response.json()); } finally { clearTimeout(timeoutId); } } sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } } exports.DocumentationGenerator = DocumentationGenerator; function createDocumentationGenerator() { const baseUrl = process.env.N8N_MCP_LLM_BASE_URL || 'http://localhost:1234/v1'; const model = process.env.N8N_MCP_LLM_MODEL || 'qwen3-4b-thinking-2507'; const timeout = parseInt(process.env.N8N_MCP_LLM_TIMEOUT || '60000', 10); const apiKey = process.env.N8N_MCP_LLM_API_KEY || process.env.OPENAI_API_KEY; let isLocalServer = true; try { const host = new URL(baseUrl).hostname; const isCloud = host === 'openai.com' || host.endsWith('.openai.com') || host.endsWith('.openai.azure.com') || host.endsWith('.azure.com') || host === 'anthropic.com' || host.endsWith('.anthropic.com'); isLocalServer = !isCloud; } catch { } return new DocumentationGenerator({ baseUrl, model, timeout, ...(apiKey ? { apiKey } : {}), ...(isLocalServer ? { temperature: 0.3 } : {}), sendThinkingKwargs: isLocalServer, }); } //# sourceMappingURL=documentation-generator.js.map