mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-12 16:23:08 +00:00
feat: add n8n_deploy_template tool for one-click template deployment (v2.27.0) (#453)
* feat: add n8n_deploy_template tool for one-click template deployment (v2.27.0) Add new MCP tool that deploys n8n.io workflow templates directly to user's n8n instance in a single operation. Features: - Fetch template from local database - Extract and report required credentials - Optionally strip credentials (default: true) - Optionally auto-upgrade node typeVersions (default: true) - Optionally validate before deployment (default: true) - Return workflow ID, URL, and setup guidance Parameters: - templateId (required): Template ID from n8n.io - name (optional): Custom workflow name - autoUpgradeVersions (default: true) - validate (default: true) - stripCredentials (default: true) Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: address code review findings for n8n_deploy_template - Fix health check tool count (12 → 13) to include new tool - Add templateId validation (must be positive integer) - Use deep copy of workflow to prevent template mutation - Expand unit tests with negative/zero/decimal validation cases Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: update README with n8n_deploy_template tool - Update management tools count from 12 to 13 - Add n8n_deploy_template to the tools list Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: prevent workflow validator from mutating node types The validator was incorrectly mutating node types from full form (n8n-nodes-base.*) to short form (nodes-base.*) during validation. This caused deployed workflows to show "?" icons in n8n UI because the API requires full form node types. Also updated SplitInBatches detection to check both node type forms since workflows may contain either format. Conceived by Romuald Członkowski - www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test: update tool counts in handlers-n8n-manager test Update expected managementTools count from 12 to 13 and totalAvailable from 19 to 20 to account for the new n8n_deploy_template tool. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: pin MCP SDK and Zod versions to prevent Zod v4 resolution Fixes #440, #444, #446, #447, #450 Root cause: package.json declared `"@modelcontextprotocol/sdk": "^1.20.1"` which allowed npm to resolve to SDK 1.23.0. That version accepts `"zod": "^3.25 || ^4.0"`, causing npm to deduplicate to Zod v4. SDK 1.23.0's `isZ4Schema()` function crashes when called with undefined, which happens for plain JSON Schema objects. Changes: - Pin SDK to exact version 1.20.1 (removes ^ prefix) - Pin Zod to exact version 3.24.1 (removes ^ prefix) - Add CI workflow to verify fresh installs get compatible versions The new CI workflow: - Packs and installs package fresh (without lockfile) - Verifies SDK is exactly 1.20.1 - Verifies Zod is NOT v4 (blocks 4.x.x) - Runs weekly to catch upstream dependency changes Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: improve dependency-check workflow based on code review - Add workflow_dispatch for manual triggering/debugging - Add explicit "not found" handling for version detection failures - Use regex pattern for Zod v4 check to catch pre-release versions - Add Zod error pattern detection in functionality test - Capture stderr output for comprehensive error checking Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
c7e7bda505
commit
e7dd04b471
@@ -34,6 +34,7 @@ import { ExpressionFormatValidator, ExpressionFormatIssue } from '../services/ex
|
||||
import { WorkflowVersioningService } from '../services/workflow-versioning-service';
|
||||
import { handleUpdatePartialWorkflow } from './handlers-workflow-diff';
|
||||
import { telemetry } from '../telemetry';
|
||||
import { TemplateService } from '../templates/template-service';
|
||||
import {
|
||||
createCacheKey,
|
||||
createInstanceCache,
|
||||
@@ -1788,7 +1789,7 @@ export async function handleDiagnostic(request: any, context?: InstanceContext):
|
||||
|
||||
// Check which tools are available
|
||||
const documentationTools = 7; // Base documentation tools (after v2.26.0 consolidation)
|
||||
const managementTools = apiConfigured ? 12 : 0; // Management tools requiring API (after v2.26.0 consolidation)
|
||||
const managementTools = apiConfigured ? 13 : 0; // Management tools requiring API (includes n8n_deploy_template)
|
||||
const totalTools = documentationTools + managementTools;
|
||||
|
||||
// Check npm version
|
||||
@@ -2189,3 +2190,220 @@ export async function handleWorkflowVersions(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Template Deployment Handler
|
||||
// ========================================================================
|
||||
|
||||
const deployTemplateSchema = z.object({
|
||||
templateId: z.number().positive().int(),
|
||||
name: z.string().optional(),
|
||||
autoUpgradeVersions: z.boolean().default(true),
|
||||
validate: z.boolean().default(true),
|
||||
stripCredentials: z.boolean().default(true)
|
||||
});
|
||||
|
||||
interface RequiredCredential {
|
||||
nodeType: string;
|
||||
nodeName: string;
|
||||
credentialType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy a workflow template from n8n.io directly to the user's n8n instance.
|
||||
*
|
||||
* This handler:
|
||||
* 1. Fetches the template from the local template database
|
||||
* 2. Extracts credential requirements for user guidance
|
||||
* 3. Optionally strips credentials (for user to configure in n8n UI)
|
||||
* 4. Optionally upgrades node typeVersions to latest supported
|
||||
* 5. Optionally validates the workflow structure
|
||||
* 6. Creates the workflow in the n8n instance
|
||||
*/
|
||||
export async function handleDeployTemplate(
|
||||
args: unknown,
|
||||
templateService: TemplateService,
|
||||
repository: NodeRepository,
|
||||
context?: InstanceContext
|
||||
): Promise<McpToolResponse> {
|
||||
try {
|
||||
const client = ensureApiConfigured(context);
|
||||
const input = deployTemplateSchema.parse(args);
|
||||
|
||||
// Fetch template
|
||||
const template = await templateService.getTemplate(input.templateId, 'full');
|
||||
if (!template) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Template ${input.templateId} not found`,
|
||||
details: {
|
||||
hint: 'Use search_templates to find available templates',
|
||||
templateUrl: `https://n8n.io/workflows/${input.templateId}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Extract workflow from template (deep copy to avoid mutation)
|
||||
const workflow = JSON.parse(JSON.stringify(template.workflow));
|
||||
if (!workflow || !workflow.nodes) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Template has invalid workflow structure',
|
||||
details: { templateId: input.templateId }
|
||||
};
|
||||
}
|
||||
|
||||
// Set workflow name
|
||||
const workflowName = input.name || template.name;
|
||||
|
||||
// Collect required credentials before stripping
|
||||
const requiredCredentials: RequiredCredential[] = [];
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.credentials && typeof node.credentials === 'object') {
|
||||
for (const [credType] of Object.entries(node.credentials)) {
|
||||
requiredCredentials.push({
|
||||
nodeType: node.type,
|
||||
nodeName: node.name,
|
||||
credentialType: credType
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip credentials if requested
|
||||
if (input.stripCredentials) {
|
||||
workflow.nodes = workflow.nodes.map((node: any) => {
|
||||
const { credentials, ...rest } = node;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-upgrade typeVersions if requested
|
||||
if (input.autoUpgradeVersions) {
|
||||
const autoFixer = new WorkflowAutoFixer(repository);
|
||||
|
||||
// Run validation to get issues to fix
|
||||
const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
|
||||
const validationResult = await validator.validateWorkflow(workflow, {
|
||||
validateNodes: true,
|
||||
validateConnections: false,
|
||||
validateExpressions: false,
|
||||
profile: 'runtime'
|
||||
});
|
||||
|
||||
// Generate fixes focused on typeVersion upgrades
|
||||
const fixResult = await autoFixer.generateFixes(
|
||||
workflow,
|
||||
validationResult,
|
||||
[],
|
||||
{ fixTypes: ['typeversion-upgrade', 'typeversion-correction'] }
|
||||
);
|
||||
|
||||
// Apply fixes to workflow
|
||||
if (fixResult.operations.length > 0) {
|
||||
for (const op of fixResult.operations) {
|
||||
if (op.type === 'updateNode' && op.updates) {
|
||||
const node = workflow.nodes.find((n: any) =>
|
||||
n.id === op.nodeId || n.name === op.nodeName
|
||||
);
|
||||
if (node) {
|
||||
for (const [path, value] of Object.entries(op.updates)) {
|
||||
if (path === 'typeVersion') {
|
||||
node.typeVersion = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate workflow if requested
|
||||
if (input.validate) {
|
||||
const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
|
||||
const validationResult = await validator.validateWorkflow(workflow, {
|
||||
validateNodes: true,
|
||||
validateConnections: true,
|
||||
validateExpressions: true,
|
||||
profile: 'runtime'
|
||||
});
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Workflow validation failed',
|
||||
details: {
|
||||
errors: validationResult.errors.map(e => ({
|
||||
node: e.nodeName,
|
||||
message: e.message
|
||||
})),
|
||||
warnings: validationResult.warnings.length,
|
||||
hint: 'Use validate=false to skip validation, or fix the template issues'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Identify trigger type
|
||||
const triggerNode = workflow.nodes.find((n: any) =>
|
||||
n.type?.includes('Trigger') ||
|
||||
n.type?.includes('webhook') ||
|
||||
n.type === 'n8n-nodes-base.webhook'
|
||||
);
|
||||
const triggerType = triggerNode?.type?.split('.').pop() || 'manual';
|
||||
|
||||
// Create workflow via API (always creates inactive)
|
||||
const createdWorkflow = await client.createWorkflow({
|
||||
name: workflowName,
|
||||
nodes: workflow.nodes,
|
||||
connections: workflow.connections,
|
||||
settings: workflow.settings || { executionOrder: 'v1' }
|
||||
});
|
||||
|
||||
// Get base URL for workflow link
|
||||
const apiConfig = context ? getN8nApiConfigFromContext(context) : getN8nApiConfig();
|
||||
const baseUrl = apiConfig?.baseUrl?.replace('/api/v1', '') || '';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
workflowId: createdWorkflow.id,
|
||||
name: createdWorkflow.name,
|
||||
active: false,
|
||||
nodeCount: workflow.nodes.length,
|
||||
triggerType,
|
||||
requiredCredentials: requiredCredentials.length > 0 ? requiredCredentials : undefined,
|
||||
url: baseUrl ? `${baseUrl}/workflow/${createdWorkflow.id}` : undefined,
|
||||
templateId: input.templateId,
|
||||
templateUrl: template.url || `https://n8n.io/workflows/${input.templateId}`
|
||||
},
|
||||
message: `Workflow "${createdWorkflow.name}" deployed successfully from template ${input.templateId}. ${
|
||||
requiredCredentials.length > 0
|
||||
? `Configure ${requiredCredentials.length} credential(s) in n8n to activate.`
|
||||
: ''
|
||||
}`
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid input',
|
||||
details: { errors: error.errors }
|
||||
};
|
||||
}
|
||||
|
||||
if (error instanceof N8nApiError) {
|
||||
return {
|
||||
success: false,
|
||||
error: getUserFriendlyErrorMessage(error),
|
||||
code: error.code,
|
||||
details: error.details as Record<string, unknown> | undefined
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user