feat: add n8n workflow templates as MCP tools

- Add 4 new MCP tools for workflow templates
- Integrate with n8n.io API to fetch community templates
- Filter templates to last 6 months only
- Store templates in SQLite with full workflow JSON
- Manual fetch system (not part of regular rebuild)
- Support search by nodes, keywords, and task categories
- Add fetch:templates and test:templates npm scripts
- Update to v2.4.1

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-06-20 00:02:09 +02:00
parent 98b7e83739
commit 08f9d1ad30
13 changed files with 1068 additions and 33 deletions

View File

@@ -0,0 +1,149 @@
import axios from 'axios';
import { logger } from '../utils/logger';
export interface TemplateNode {
id: number;
name: string;
icon: string;
}
export interface TemplateUser {
id: number;
name: string;
username: string;
verified: boolean;
}
export interface TemplateWorkflow {
id: number;
name: string;
description: string;
totalViews: number;
createdAt: string;
user: TemplateUser;
nodes: TemplateNode[];
}
export interface TemplateDetail {
id: number;
name: string;
description: string;
views: number;
createdAt: string;
workflow: {
nodes: any[];
connections: any;
settings?: any;
};
}
export class TemplateFetcher {
private readonly baseUrl = 'https://api.n8n.io/api/templates';
private readonly pageSize = 100;
async fetchTemplates(progressCallback?: (current: number, total: number) => void): Promise<TemplateWorkflow[]> {
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
const allTemplates: TemplateWorkflow[] = [];
let page = 1;
let hasMore = true;
logger.info('Starting template fetch from n8n.io API');
while (hasMore) {
try {
const response = await axios.get(`${this.baseUrl}/search`, {
params: {
page,
rows: this.pageSize,
sort_by: 'last-updated'
}
});
const { workflows, totalWorkflows } = response.data;
// Filter templates by date
const recentTemplates = workflows.filter((w: TemplateWorkflow) => {
const createdDate = new Date(w.createdAt);
return createdDate >= sixMonthsAgo;
});
// If we hit templates older than 6 months, stop fetching
if (recentTemplates.length < workflows.length) {
hasMore = false;
logger.info(`Reached templates older than 6 months at page ${page}`);
}
allTemplates.push(...recentTemplates);
if (progressCallback) {
progressCallback(allTemplates.length, Math.min(totalWorkflows, allTemplates.length + 500));
}
// Check if there are more pages
if (workflows.length < this.pageSize || allTemplates.length >= totalWorkflows) {
hasMore = false;
}
page++;
// Rate limiting - be nice to the API
if (hasMore) {
await this.sleep(500); // 500ms between requests
}
} catch (error) {
logger.error(`Error fetching templates page ${page}:`, error);
throw error;
}
}
logger.info(`Fetched ${allTemplates.length} templates from last 6 months`);
return allTemplates;
}
async fetchTemplateDetail(workflowId: number): Promise<TemplateDetail> {
try {
const response = await axios.get(`${this.baseUrl}/workflows/${workflowId}`);
return response.data.workflow;
} catch (error) {
logger.error(`Error fetching template detail for ${workflowId}:`, error);
throw error;
}
}
async fetchAllTemplateDetails(
workflows: TemplateWorkflow[],
progressCallback?: (current: number, total: number) => void
): Promise<Map<number, TemplateDetail>> {
const details = new Map<number, TemplateDetail>();
logger.info(`Fetching details for ${workflows.length} templates`);
for (let i = 0; i < workflows.length; i++) {
const workflow = workflows[i];
try {
const detail = await this.fetchTemplateDetail(workflow.id);
details.set(workflow.id, detail);
if (progressCallback) {
progressCallback(i + 1, workflows.length);
}
// Rate limiting
await this.sleep(200); // 200ms between requests
} catch (error) {
logger.error(`Failed to fetch details for workflow ${workflow.id}:`, error);
// Continue with other templates
}
}
logger.info(`Successfully fetched ${details.size} template details`);
return details;
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}