feat: enhance template tooling with pagination and flexible retrieval

- Add pagination support to all template search/list tools
  - Consistent response format with total, limit, offset, hasMore
  - Support for customizable limits (1-100) and offsets

- Add new list_templates tool for browsing all templates
  - Returns minimal data (id, name, views, node count)
  - Supports sorting by views, created_at, or name
  - Efficient for discovering available templates

- Enhance get_template with flexible response modes
  - nodes_only: Just list of node types (minimal tokens)
  - structure: Nodes with positions and connections
  - full: Complete workflow JSON (default)

- Update database_statistics to show template count
  - Shows total templates, average/min/max views
  - Provides complete database overview

- Add count methods to repository for pagination
  - getSearchCount, getNodeTemplatesCount, getTaskTemplatesCount
  - Enables accurate pagination info

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-09-14 15:04:17 +02:00
parent f35097ed46
commit e7895d2e01
5 changed files with 339 additions and 65 deletions

View File

@@ -725,21 +725,32 @@ export class N8NDocumentationMCPServer {
case 'get_node_as_tool_info':
this.validateToolParams(name, args, ['nodeType']);
return this.getNodeAsToolInfo(args.nodeType);
case 'list_templates':
// No required params
const listLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
const listOffset = Math.max(Number(args.offset) || 0, 0);
const sortBy = args.sortBy || 'views';
return this.listTemplates(listLimit, listOffset, sortBy);
case 'list_node_templates':
this.validateToolParams(name, args, ['nodeTypes']);
const templateLimit = args.limit !== undefined ? Number(args.limit) || 10 : 10;
return this.listNodeTemplates(args.nodeTypes, templateLimit);
const templateLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
const templateOffset = Math.max(Number(args.offset) || 0, 0);
return this.listNodeTemplates(args.nodeTypes, templateLimit, templateOffset);
case 'get_template':
this.validateToolParams(name, args, ['templateId']);
const templateId = Number(args.templateId);
return this.getTemplate(templateId);
const mode = args.mode || 'full';
return this.getTemplate(templateId, mode);
case 'search_templates':
this.validateToolParams(name, args, ['query']);
const searchLimit = args.limit !== undefined ? Number(args.limit) || 20 : 20;
return this.searchTemplates(args.query, searchLimit);
const searchLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
const searchOffset = Math.max(Number(args.offset) || 0, 0);
return this.searchTemplates(args.query, searchLimit, searchOffset);
case 'get_templates_for_task':
this.validateToolParams(name, args, ['task']);
return this.getTemplatesForTask(args.task);
const taskLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
const taskOffset = Math.max(Number(args.offset) || 0, 0);
return this.getTemplatesForTask(args.task, taskLimit, taskOffset);
case 'validate_workflow':
this.validateToolParams(name, args, ['workflow']);
return this.validateWorkflow(args.workflow, args.options);
@@ -1594,8 +1605,19 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
GROUP BY package_name
`).all() as any[];
// Get template statistics
const templateStats = this.db!.prepare(`
SELECT
COUNT(*) as total_templates,
AVG(views) as avg_views,
MIN(views) as min_views,
MAX(views) as max_views
FROM templates
`).get() as any;
return {
totalNodes: stats.total,
totalTemplates: templateStats.total_templates || 0,
statistics: {
aiTools: stats.ai_tools,
triggers: stats.triggers,
@@ -1604,6 +1626,12 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
documentationCoverage: Math.round((stats.with_docs / stats.total) * 100) + '%',
uniquePackages: stats.packages,
uniqueCategories: stats.categories,
templates: {
total: templateStats.total_templates || 0,
avgViews: Math.round(templateStats.avg_views || 0),
minViews: templateStats.min_views || 0,
maxViews: templateStats.max_views || 0
}
},
packageBreakdown: packages.map(pkg => ({
package: pkg.package_name,
@@ -2300,76 +2328,95 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
}
// Template-related methods
private async listNodeTemplates(nodeTypes: string[], limit: number = 10): Promise<any> {
private async listTemplates(limit: number = 10, offset: number = 0, sortBy: 'views' | 'created_at' | 'name' = 'views'): Promise<any> {
await this.ensureInitialized();
if (!this.templateService) throw new Error('Template service not initialized');
const templates = await this.templateService.listNodeTemplates(nodeTypes, limit);
const result = await this.templateService.listTemplates(limit, offset, sortBy);
if (templates.length === 0) {
return {
...result,
tip: result.items.length > 0 ?
`Use get_template(templateId) to get full workflow details. Total: ${result.total} templates available.` :
"No templates found. Run 'npm run fetch:templates' to update template database"
};
}
private async listNodeTemplates(nodeTypes: string[], limit: number = 10, offset: number = 0): Promise<any> {
await this.ensureInitialized();
if (!this.templateService) throw new Error('Template service not initialized');
const result = await this.templateService.listNodeTemplates(nodeTypes, limit, offset);
if (result.items.length === 0 && offset === 0) {
return {
...result,
message: `No templates found using nodes: ${nodeTypes.join(', ')}`,
tip: "Try searching with more common nodes or run 'npm run fetch:templates' to update template database",
templates: []
tip: "Try searching with more common nodes or run 'npm run fetch:templates' to update template database"
};
}
return {
templates,
count: templates.length,
tip: `Use get_template(templateId) to get the full workflow JSON for any template`
...result,
tip: `Showing ${result.items.length} of ${result.total} templates. Use offset for pagination.`
};
}
private async getTemplate(templateId: number): Promise<any> {
private async getTemplate(templateId: number, mode: 'nodes_only' | 'structure' | 'full' = 'full'): Promise<any> {
await this.ensureInitialized();
if (!this.templateService) throw new Error('Template service not initialized');
const template = await this.templateService.getTemplate(templateId);
const template = await this.templateService.getTemplate(templateId, mode);
if (!template) {
return {
error: `Template ${templateId} not found`,
tip: "Use list_node_templates or search_templates to find available templates"
tip: "Use list_templates, list_node_templates or search_templates to find available templates"
};
}
const usage = mode === 'nodes_only' ? "Node list for quick overview" :
mode === 'structure' ? "Workflow structure without full details" :
"Complete workflow JSON ready to import into n8n";
return {
mode,
template,
usage: "Import this workflow JSON directly into n8n or use it as a reference for building workflows"
usage
};
}
private async searchTemplates(query: string, limit: number = 20): Promise<any> {
private async searchTemplates(query: string, limit: number = 20, offset: number = 0): Promise<any> {
await this.ensureInitialized();
if (!this.templateService) throw new Error('Template service not initialized');
const templates = await this.templateService.searchTemplates(query, limit);
const result = await this.templateService.searchTemplates(query, limit, offset);
if (templates.length === 0) {
if (result.items.length === 0 && offset === 0) {
return {
...result,
message: `No templates found matching: "${query}"`,
tip: "Try different keywords or run 'npm run fetch:templates' to update template database",
templates: []
tip: "Try different keywords or run 'npm run fetch:templates' to update template database"
};
}
return {
templates,
count: templates.length,
query
...result,
query,
tip: `Found ${result.total} templates matching "${query}". Showing ${result.items.length}.`
};
}
private async getTemplatesForTask(task: string): Promise<any> {
private async getTemplatesForTask(task: string, limit: number = 10, offset: number = 0): Promise<any> {
await this.ensureInitialized();
if (!this.templateService) throw new Error('Template service not initialized');
const templates = await this.templateService.getTemplatesForTask(task);
const result = await this.templateService.getTemplatesForTask(task, limit, offset);
const availableTasks = this.templateService.listAvailableTasks();
if (templates.length === 0) {
if (result.items.length === 0 && offset === 0) {
return {
...result,
message: `No templates found for task: ${task}`,
availableTasks,
tip: "Try a different task or use search_templates for custom searches"
@@ -2377,10 +2424,10 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
}
return {
...result,
task,
templates,
count: templates.length,
description: this.getTaskDescription(task)
description: this.getTaskDescription(task),
tip: `${result.total} templates available for ${task}. Showing ${result.items.length}.`
};
}