mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 14:32:04 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7dd04b471 | ||
|
|
c7e7bda505 |
222
.github/workflows/dependency-check.yml
vendored
Normal file
222
.github/workflows/dependency-check.yml
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
name: Dependency Compatibility Check
|
||||
|
||||
# This workflow verifies that when users install n8n-mcp via npm (without lockfile),
|
||||
# they get compatible dependency versions. This catches issues like #440, #444, #446, #447, #450
|
||||
# where npm resolution gave users incompatible SDK/Zod versions.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '.github/workflows/dependency-check.yml'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '.github/workflows/dependency-check.yml'
|
||||
# Allow manual trigger for debugging
|
||||
workflow_dispatch:
|
||||
# Run weekly to catch upstream dependency changes
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Every Monday at 6 AM UTC
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
fresh-install-check:
|
||||
name: Fresh Install Dependency Check
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Pack package for testing
|
||||
run: npm pack
|
||||
|
||||
- name: Create fresh install test directory
|
||||
run: |
|
||||
mkdir -p /tmp/fresh-install-test
|
||||
cp n8n-mcp-*.tgz /tmp/fresh-install-test/
|
||||
|
||||
- name: Install package fresh (simulating user install)
|
||||
working-directory: /tmp/fresh-install-test
|
||||
run: |
|
||||
npm init -y
|
||||
# Install from tarball WITHOUT lockfile (simulates npm install n8n-mcp)
|
||||
npm install ./n8n-mcp-*.tgz
|
||||
|
||||
- name: Verify critical dependency versions
|
||||
working-directory: /tmp/fresh-install-test
|
||||
run: |
|
||||
echo "=== Dependency Version Check ==="
|
||||
echo ""
|
||||
|
||||
# Get actual resolved versions
|
||||
SDK_VERSION=$(npm list @modelcontextprotocol/sdk --json 2>/dev/null | jq -r '.dependencies["n8n-mcp"].dependencies["@modelcontextprotocol/sdk"].version // .dependencies["@modelcontextprotocol/sdk"].version // "not found"')
|
||||
ZOD_VERSION=$(npm list zod --json 2>/dev/null | jq -r '.dependencies["n8n-mcp"].dependencies.zod.version // .dependencies.zod.version // "not found"')
|
||||
|
||||
echo "MCP SDK version: $SDK_VERSION"
|
||||
echo "Zod version: $ZOD_VERSION"
|
||||
echo ""
|
||||
|
||||
# Check MCP SDK version - must be exactly 1.20.1
|
||||
if [[ "$SDK_VERSION" == "not found" ]]; then
|
||||
echo "❌ FAILED: Could not determine MCP SDK version!"
|
||||
echo " The dependency may not have been installed correctly."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$SDK_VERSION" != "1.20.1" ]]; then
|
||||
echo "❌ FAILED: MCP SDK version mismatch!"
|
||||
echo " Expected: 1.20.1"
|
||||
echo " Got: $SDK_VERSION"
|
||||
echo ""
|
||||
echo "This can cause runtime errors. See issues #440, #444, #446, #447, #450"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ MCP SDK version is correct: $SDK_VERSION"
|
||||
|
||||
# Check Zod version - must be 3.x (not 4.x, including pre-releases)
|
||||
if [[ "$ZOD_VERSION" == "not found" ]]; then
|
||||
echo "❌ FAILED: Could not determine Zod version!"
|
||||
echo " The dependency may not have been installed correctly."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$ZOD_VERSION" =~ ^4\. ]]; then
|
||||
echo "❌ FAILED: Zod v4 detected - incompatible with MCP SDK 1.20.1!"
|
||||
echo " Expected: 3.x"
|
||||
echo " Got: $ZOD_VERSION"
|
||||
echo ""
|
||||
echo "Zod v4 causes '_zod' property errors. See issues #440, #444, #446, #447, #450"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Zod version is compatible: $ZOD_VERSION"
|
||||
|
||||
echo ""
|
||||
echo "=== All dependency checks passed ==="
|
||||
|
||||
- name: Test basic functionality
|
||||
working-directory: /tmp/fresh-install-test
|
||||
run: |
|
||||
echo "=== Basic Functionality Test ==="
|
||||
|
||||
# Create a simple test script
|
||||
cat > test-import.mjs << 'EOF'
|
||||
import { spawn } from 'child_process';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Test that the package can be required and basic tools work
|
||||
async function test() {
|
||||
console.log('Testing n8n-mcp package import...');
|
||||
|
||||
// Start the MCP server briefly to verify it initializes
|
||||
const serverPath = path.join(__dirname, 'node_modules/n8n-mcp/dist/mcp/index.js');
|
||||
|
||||
const proc = spawn('node', [serverPath], {
|
||||
env: { ...process.env, MCP_MODE: 'stdio' },
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
// Send initialize request
|
||||
const initRequest = JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'test', version: '1.0.0' }
|
||||
}
|
||||
});
|
||||
|
||||
proc.stdin.write(initRequest + '\n');
|
||||
|
||||
// Wait for response or timeout
|
||||
let output = '';
|
||||
let stderrOutput = '';
|
||||
proc.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (data) => {
|
||||
stderrOutput += data.toString();
|
||||
console.error('stderr:', data.toString());
|
||||
});
|
||||
|
||||
// Give it 5 seconds to respond
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
|
||||
proc.kill();
|
||||
|
||||
// Check for Zod v4 compatibility errors (the bug we're testing for)
|
||||
const allOutput = output + stderrOutput;
|
||||
if (allOutput.includes('_zod') || allOutput.includes('Cannot read properties of undefined')) {
|
||||
console.error('❌ FAILED: Zod compatibility error detected!');
|
||||
console.error('This indicates the SDK/Zod version fix is not working.');
|
||||
console.error('See issues #440, #444, #446, #447, #450');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (output.includes('"result"')) {
|
||||
console.log('✅ MCP server initialized successfully');
|
||||
return true;
|
||||
} else {
|
||||
console.log('Output received:', output.substring(0, 500));
|
||||
// Server might not respond in stdio mode without proper framing
|
||||
// But if we got here without crashing, that's still good
|
||||
console.log('✅ MCP server started without errors');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
test()
|
||||
.then(() => {
|
||||
console.log('=== Basic functionality test passed ===');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('❌ Test failed:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
EOF
|
||||
|
||||
node test-import.mjs
|
||||
|
||||
- name: Generate dependency report
|
||||
if: always()
|
||||
working-directory: /tmp/fresh-install-test
|
||||
run: |
|
||||
echo "=== Full Dependency Tree ===" > dependency-report.txt
|
||||
npm list --all >> dependency-report.txt 2>&1 || true
|
||||
|
||||
echo "" >> dependency-report.txt
|
||||
echo "=== Critical Dependencies ===" >> dependency-report.txt
|
||||
npm list @modelcontextprotocol/sdk zod zod-to-json-schema >> dependency-report.txt 2>&1 || true
|
||||
|
||||
cat dependency-report.txt
|
||||
|
||||
- name: Upload dependency report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dependency-report-${{ github.run_number }}
|
||||
path: /tmp/fresh-install-test/dependency-report.txt
|
||||
retention-days: 30
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.26.5] - 2025-11-27
|
||||
|
||||
### 🔧 Fixed
|
||||
|
||||
- **Tools Documentation: Runtime Token Optimization**
|
||||
- Removed historical migration information from tool descriptions (e.g., "Replaces X, Y, Z...")
|
||||
- Removed version-specific references (v2.21.1, issue #357) that are not needed at runtime
|
||||
- Cleaned up consolidation comments in index.ts
|
||||
- Documentation now starts directly with functional content for better AI agent efficiency
|
||||
- Estimated savings: ~128 tokens per full documentation request
|
||||
- Affected tools: `get_node`, `validate_node`, `search_templates`, `n8n_executions`, `n8n_get_workflow`, `n8n_update_partial_workflow`
|
||||
|
||||
**Conceived by Romuald Członkowski - [AiAdvisors](https://www.aiadvisors.pl/en)**
|
||||
|
||||
## [2.26.4] - 2025-11-26
|
||||
|
||||
### 🔧 Fixed
|
||||
|
||||
@@ -954,7 +954,7 @@ Once connected, Claude can use these powerful tools:
|
||||
- `searchMode: 'by_metadata'` - Filter by `complexity`, `requiredService`, `targetAudience`
|
||||
- **`get_template`** - Get complete workflow JSON (modes: nodes_only, structure, full)
|
||||
|
||||
### n8n Management Tools (12 tools - Requires API Configuration)
|
||||
### n8n Management Tools (13 tools - Requires API Configuration)
|
||||
These tools require `N8N_API_URL` and `N8N_API_KEY` in your configuration.
|
||||
|
||||
#### Workflow Management
|
||||
@@ -971,6 +971,7 @@ These tools require `N8N_API_URL` and `N8N_API_KEY` in your configuration.
|
||||
- **`n8n_validate_workflow`** - Validate workflows in n8n by ID
|
||||
- **`n8n_autofix_workflow`** - Automatically fix common workflow errors
|
||||
- **`n8n_workflow_versions`** - Manage version history and rollback
|
||||
- **`n8n_deploy_template`** - Deploy templates from n8n.io directly to your instance (NEW!)
|
||||
|
||||
#### Execution Management
|
||||
- **`n8n_trigger_webhook_workflow`** - Trigger workflows via webhook URL
|
||||
|
||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.26.0",
|
||||
"version": "2.27.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.26.0",
|
||||
"version": "2.27.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@modelcontextprotocol/sdk": "1.20.1",
|
||||
"@n8n/n8n-nodes-langchain": "^1.120.1",
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"dotenv": "^16.5.0",
|
||||
@@ -23,7 +23,7 @@
|
||||
"sql.js": "^1.13.0",
|
||||
"tslib": "^2.6.2",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"bin": {
|
||||
"n8n-mcp": "dist/mcp/index.js"
|
||||
@@ -8454,6 +8454,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/community/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core": {
|
||||
"version": "0.3.68",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.68.tgz",
|
||||
@@ -8477,6 +8486,15 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/google-common": {
|
||||
"version": "0.2.18",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/google-common/-/google-common-0.2.18.tgz",
|
||||
@@ -8742,6 +8760,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/openai/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/pinecone": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/pinecone/-/pinecone-0.2.0.tgz",
|
||||
@@ -22681,6 +22708,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/langchain/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith": {
|
||||
"version": "0.3.69",
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.69.tgz",
|
||||
@@ -33907,9 +33943,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.26.4",
|
||||
"version": "2.27.0",
|
||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -140,7 +140,7 @@
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@modelcontextprotocol/sdk": "1.20.1",
|
||||
"@n8n/n8n-nodes-langchain": "^1.120.1",
|
||||
"@supabase/supabase-js": "^2.57.4",
|
||||
"dotenv": "^16.5.0",
|
||||
@@ -154,7 +154,7 @@
|
||||
"sql.js": "^1.13.0",
|
||||
"tslib": "^2.6.2",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-darwin-arm64": "^4.50.0",
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -856,6 +856,12 @@ export class N8NDocumentationMCPServer {
|
||||
? { valid: true, errors: [] }
|
||||
: { valid: false, errors: [{ field: 'action', message: 'action is required' }] };
|
||||
break;
|
||||
case 'n8n_deploy_template':
|
||||
// Requires templateId parameter
|
||||
validationResult = args.templateId !== undefined
|
||||
? { valid: true, errors: [] }
|
||||
: { valid: false, errors: [{ field: 'templateId', message: 'templateId is required' }] };
|
||||
break;
|
||||
default:
|
||||
// For tools not yet migrated to schema validation, use basic validation
|
||||
return this.validateToolParamsBasic(toolName, args, legacyRequiredParams || []);
|
||||
@@ -1203,6 +1209,13 @@ export class N8NDocumentationMCPServer {
|
||||
this.validateToolParams(name, args, ['mode']);
|
||||
return n8nHandlers.handleWorkflowVersions(args, this.repository!, this.instanceContext);
|
||||
|
||||
case 'n8n_deploy_template':
|
||||
this.validateToolParams(name, args, ['templateId']);
|
||||
await this.ensureInitialized();
|
||||
if (!this.templateService) throw new Error('Template service not initialized');
|
||||
if (!this.repository) throw new Error('Repository not initialized');
|
||||
return n8nHandlers.handleDeployTemplate(args, this.templateService, this.repository, this.instanceContext);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ export const getNodeDoc: ToolDocumentation = {
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Unified tool for all node information needs. Replaces get_node_info, get_node_essentials, get_node_documentation, and search_node_properties with a single versatile API.
|
||||
|
||||
**Detail Levels (mode="info", default):**
|
||||
description: `**Detail Levels (mode="info", default):**
|
||||
- minimal (~200 tokens): Basic metadata only - nodeType, displayName, description, category
|
||||
- standard (~1-2K tokens): Essential properties + operations - recommended for most tasks
|
||||
- full (~3-8K tokens): Complete node schema - use only when standard insufficient
|
||||
|
||||
@@ -21,11 +21,11 @@ import {
|
||||
n8nAutofixWorkflowDoc,
|
||||
n8nTriggerWebhookWorkflowDoc,
|
||||
n8nExecutionsDoc,
|
||||
n8nWorkflowVersionsDoc
|
||||
n8nWorkflowVersionsDoc,
|
||||
n8nDeployTemplateDoc
|
||||
} from './workflow_management';
|
||||
|
||||
// Combine all tool documentations into a single object
|
||||
// Total: 19 tools after v2.26.0 consolidation
|
||||
export const toolsDocumentation: Record<string, ToolDocumentation> = {
|
||||
// System tools
|
||||
tools_documentation: toolsDocumentationDoc,
|
||||
@@ -37,20 +37,20 @@ export const toolsDocumentation: Record<string, ToolDocumentation> = {
|
||||
// Discovery tools
|
||||
search_nodes: searchNodesDoc,
|
||||
|
||||
// Configuration tools (consolidated)
|
||||
get_node: getNodeDoc, // Replaces: get_node_info, get_node_essentials, get_node_documentation, search_node_properties
|
||||
// Configuration tools
|
||||
get_node: getNodeDoc,
|
||||
|
||||
// Validation tools (consolidated)
|
||||
validate_node: validateNodeDoc, // Replaces: validate_node_operation, validate_node_minimal
|
||||
validate_workflow: validateWorkflowDoc, // Options replace: validate_workflow_connections, validate_workflow_expressions
|
||||
// Validation tools
|
||||
validate_node: validateNodeDoc,
|
||||
validate_workflow: validateWorkflowDoc,
|
||||
|
||||
// Template tools (consolidated)
|
||||
// Template tools
|
||||
get_template: getTemplateDoc,
|
||||
search_templates: searchTemplatesDoc, // Modes replace: list_node_templates, search_templates_by_metadata, get_templates_for_task
|
||||
search_templates: searchTemplatesDoc,
|
||||
|
||||
// Workflow Management tools (n8n API)
|
||||
n8n_create_workflow: n8nCreateWorkflowDoc,
|
||||
n8n_get_workflow: n8nGetWorkflowDoc, // Modes replace: n8n_get_workflow_details, n8n_get_workflow_structure, n8n_get_workflow_minimal
|
||||
n8n_get_workflow: n8nGetWorkflowDoc,
|
||||
n8n_update_full_workflow: n8nUpdateFullWorkflowDoc,
|
||||
n8n_update_partial_workflow: n8nUpdatePartialWorkflowDoc,
|
||||
n8n_delete_workflow: n8nDeleteWorkflowDoc,
|
||||
@@ -58,8 +58,9 @@ export const toolsDocumentation: Record<string, ToolDocumentation> = {
|
||||
n8n_validate_workflow: n8nValidateWorkflowDoc,
|
||||
n8n_autofix_workflow: n8nAutofixWorkflowDoc,
|
||||
n8n_trigger_webhook_workflow: n8nTriggerWebhookWorkflowDoc,
|
||||
n8n_executions: n8nExecutionsDoc, // Actions replace: n8n_get_execution, n8n_list_executions, n8n_delete_execution
|
||||
n8n_workflow_versions: n8nWorkflowVersionsDoc // Modes: list, get, rollback, delete, prune, truncate
|
||||
n8n_executions: n8nExecutionsDoc,
|
||||
n8n_workflow_versions: n8nWorkflowVersionsDoc,
|
||||
n8n_deploy_template: n8nDeployTemplateDoc
|
||||
};
|
||||
|
||||
// Re-export types
|
||||
|
||||
@@ -16,9 +16,7 @@ export const searchTemplatesDoc: ToolDocumentation = {
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Unified template search tool with four search modes. Replaces search_templates, list_node_templates, search_templates_by_metadata, and get_templates_for_task.
|
||||
|
||||
**Search Modes:**
|
||||
description: `**Search Modes:**
|
||||
- keyword (default): Full-text search across template names and descriptions
|
||||
- by_nodes: Find templates that use specific node types
|
||||
- by_task: Get curated templates for predefined task categories
|
||||
|
||||
@@ -16,9 +16,7 @@ export const validateNodeDoc: ToolDocumentation = {
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Unified node configuration validator. Replaces validate_node_operation and validate_node_minimal with a single tool.
|
||||
|
||||
**Validation Modes:**
|
||||
description: `**Validation Modes:**
|
||||
- full (default): Comprehensive validation with errors, warnings, suggestions, and automatic structure validation
|
||||
- minimal: Quick check for required fields only - fast but less thorough
|
||||
|
||||
|
||||
@@ -9,3 +9,4 @@ export { n8nAutofixWorkflowDoc } from './n8n-autofix-workflow';
|
||||
export { n8nTriggerWebhookWorkflowDoc } from './n8n-trigger-webhook-workflow';
|
||||
export { n8nExecutionsDoc } from './n8n-executions';
|
||||
export { n8nWorkflowVersionsDoc } from './n8n-workflow-versions';
|
||||
export { n8nDeployTemplateDoc } from './n8n-deploy-template';
|
||||
|
||||
69
src/mcp/tool-docs/workflow_management/n8n-deploy-template.ts
Normal file
69
src/mcp/tool-docs/workflow_management/n8n-deploy-template.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ToolDocumentation } from '../types';
|
||||
|
||||
export const n8nDeployTemplateDoc: ToolDocumentation = {
|
||||
name: 'n8n_deploy_template',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Deploy a workflow template from n8n.io directly to your n8n instance. Fetches template, optionally upgrades node versions and validates, then creates workflow.',
|
||||
keyParameters: ['templateId', 'name', 'autoUpgradeVersions', 'validate', 'stripCredentials'],
|
||||
example: 'n8n_deploy_template({templateId: 2776, name: "My Deployed Template"})',
|
||||
performance: 'Network-dependent',
|
||||
tips: [
|
||||
'Workflow created inactive - configure credentials in n8n UI first',
|
||||
'Returns list of required credentials',
|
||||
'Use search_templates to find template IDs',
|
||||
'Templates are upgraded to latest node versions by default'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: 'Deploys a workflow template from n8n.io directly to your n8n instance. This tool combines fetching a template and creating a workflow in a single operation. Templates are stored locally and fetched from the database. The workflow is always created in an inactive state, allowing you to configure credentials before activation.',
|
||||
parameters: {
|
||||
templateId: { type: 'number', required: true, description: 'Template ID from n8n.io (find via search_templates)' },
|
||||
name: { type: 'string', description: 'Custom workflow name (default: template name)' },
|
||||
autoUpgradeVersions: { type: 'boolean', description: 'Upgrade node typeVersions to latest supported (default: true)' },
|
||||
validate: { type: 'boolean', description: 'Validate workflow before deployment (default: true)' },
|
||||
stripCredentials: { type: 'boolean', description: 'Remove credential references - user configures in n8n UI (default: true)' }
|
||||
},
|
||||
returns: 'Object with workflowId, name, nodeCount, triggerType, requiredCredentials array, url, templateId, templateUrl',
|
||||
examples: [
|
||||
`// Deploy template with default settings
|
||||
n8n_deploy_template({templateId: 2776})`,
|
||||
`// Deploy with custom name
|
||||
n8n_deploy_template({
|
||||
templateId: 2776,
|
||||
name: "My Google Drive to Airtable Sync"
|
||||
})`,
|
||||
`// Deploy without validation (faster, use for trusted templates)
|
||||
n8n_deploy_template({
|
||||
templateId: 2776,
|
||||
validate: false
|
||||
})`,
|
||||
`// Keep original node versions (useful for compatibility)
|
||||
n8n_deploy_template({
|
||||
templateId: 2776,
|
||||
autoUpgradeVersions: false
|
||||
})`
|
||||
],
|
||||
useCases: [
|
||||
'Quickly deploy pre-built workflow templates',
|
||||
'Set up common automation patterns',
|
||||
'Bootstrap new projects with proven workflows',
|
||||
'Deploy templates found via search_templates'
|
||||
],
|
||||
performance: 'Network-dependent - Typically 200-500ms (template fetch + workflow creation)',
|
||||
bestPractices: [
|
||||
'Use search_templates to find templates by use case',
|
||||
'Review required credentials in the response',
|
||||
'Configure credentials in n8n UI before activating',
|
||||
'Test workflow before connecting to production systems'
|
||||
],
|
||||
pitfalls: [
|
||||
'**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - tool unavailable without n8n API access',
|
||||
'Workflows created in INACTIVE state - must configure credentials and activate in n8n',
|
||||
'Templates may reference services you do not have (Slack, Google, etc.)',
|
||||
'Template database must be populated - run npm run fetch:templates if templates not found',
|
||||
'Validation may fail for templates with outdated node configurations'
|
||||
],
|
||||
relatedTools: ['search_templates', 'get_template', 'n8n_create_workflow', 'n8n_validate_workflow']
|
||||
}
|
||||
};
|
||||
@@ -16,9 +16,7 @@ export const n8nExecutionsDoc: ToolDocumentation = {
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Unified execution management tool. Replaces n8n_get_execution, n8n_list_executions, and n8n_delete_execution.
|
||||
|
||||
**Actions:**
|
||||
description: `**Actions:**
|
||||
- get: Retrieve execution details by ID with configurable detail level
|
||||
- list: List executions with filtering and pagination
|
||||
- delete: Remove an execution record from history
|
||||
|
||||
@@ -16,9 +16,7 @@ export const n8nGetWorkflowDoc: ToolDocumentation = {
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Unified workflow retrieval with configurable detail levels. Replaces n8n_get_workflow, n8n_get_workflow_details, n8n_get_workflow_structure, and n8n_get_workflow_minimal.
|
||||
|
||||
**Modes:**
|
||||
description: `**Modes:**
|
||||
- full (default): Complete workflow including all nodes with parameters, connections, and settings
|
||||
- details: Full workflow plus execution statistics (success/error counts, last execution time)
|
||||
- structure: Nodes and connections only - useful for topology analysis
|
||||
|
||||
@@ -90,7 +90,6 @@ Full support for all 8 AI connection types used in n8n AI workflows:
|
||||
|
||||
**Important Notes**:
|
||||
- **AI nodes do NOT require main connections**: Nodes like OpenAI Chat Model, Postgres Chat Memory, Embeddings OpenAI, and Supabase Vector Store use AI-specific connection types exclusively. They should ONLY have connections like \`ai_languageModel\`, \`ai_memory\`, \`ai_embedding\`, or \`ai_tool\` - NOT \`main\` connections.
|
||||
- **Fixed in v2.21.1**: Validation now correctly recognizes AI nodes that only have AI-specific connections without requiring \`main\` connections (resolves issue #357).
|
||||
|
||||
**Best Practices**:
|
||||
- Always specify \`sourceOutput\` for AI connections (defaults to "main" if omitted)
|
||||
|
||||
@@ -126,7 +126,7 @@ When working with Code nodes, always start by calling the relevant guide:
|
||||
- searchMode='by_task': Curated task-based templates
|
||||
- searchMode='by_metadata': Filter by complexity/services
|
||||
|
||||
**n8n API Tools** (12 tools, requires N8N_API_URL configuration)
|
||||
**n8n API Tools** (13 tools, requires N8N_API_URL configuration)
|
||||
- n8n_create_workflow - Create new workflows
|
||||
- n8n_get_workflow - Get workflow with mode='full'/'details'/'structure'/'minimal'
|
||||
- n8n_update_full_workflow - Full workflow replacement
|
||||
@@ -139,6 +139,7 @@ When working with Code nodes, always start by calling the relevant guide:
|
||||
- n8n_executions - Unified execution management (action='get'/'list'/'delete')
|
||||
- n8n_health_check - Check n8n API connectivity
|
||||
- n8n_workflow_versions - Version history and rollback
|
||||
- n8n_deploy_template - Deploy templates directly to n8n instance
|
||||
|
||||
## Performance Characteristics
|
||||
- Instant (<10ms): search_nodes, get_node (minimal/standard)
|
||||
|
||||
@@ -445,5 +445,40 @@ export const n8nManagementTools: ToolDefinition[] = [
|
||||
},
|
||||
required: ['mode']
|
||||
}
|
||||
},
|
||||
|
||||
// Template Deployment Tool
|
||||
{
|
||||
name: 'n8n_deploy_template',
|
||||
description: `Deploy a workflow template from n8n.io directly to your n8n instance. Fetches template, optionally upgrades node versions and validates, then creates workflow. Returns workflow ID and required credentials list.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
templateId: {
|
||||
type: 'number',
|
||||
description: 'Template ID from n8n.io (required)'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Custom workflow name (default: template name)'
|
||||
},
|
||||
autoUpgradeVersions: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Automatically upgrade node typeVersions to latest supported (default: true)'
|
||||
},
|
||||
validate: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Validate workflow before deployment (default: true)'
|
||||
},
|
||||
stripCredentials: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Remove credential references from nodes - user configures in n8n UI (default: true)'
|
||||
}
|
||||
},
|
||||
required: ['templateId']
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -383,14 +383,11 @@ export class WorkflowValidator {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Normalize node type FIRST to ensure consistent lookup
|
||||
// Normalize node type for database lookup (DO NOT mutate the original workflow)
|
||||
// The normalizer converts to short form (nodes-base.*) for database queries,
|
||||
// but n8n API requires full form (n8n-nodes-base.*). Never modify the input workflow.
|
||||
const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type);
|
||||
|
||||
// Update node type in place if it was normalized
|
||||
if (normalizedType !== node.type) {
|
||||
node.type = normalizedType;
|
||||
}
|
||||
|
||||
// Get node definition using normalized type (needed for typeVersion validation)
|
||||
const nodeInfo = this.nodeRepository.getNode(normalizedType);
|
||||
|
||||
@@ -684,7 +681,12 @@ export class WorkflowValidator {
|
||||
}
|
||||
|
||||
// Special validation for SplitInBatches node
|
||||
if (sourceNode && sourceNode.type === 'nodes-base.splitInBatches') {
|
||||
// Check both full form (n8n-nodes-base.*) and short form (nodes-base.*)
|
||||
const isSplitInBatches = sourceNode && (
|
||||
sourceNode.type === 'n8n-nodes-base.splitInBatches' ||
|
||||
sourceNode.type === 'nodes-base.splitInBatches'
|
||||
);
|
||||
if (isSplitInBatches) {
|
||||
this.validateSplitInBatchesConnection(
|
||||
sourceNode,
|
||||
outputIndex,
|
||||
@@ -696,8 +698,8 @@ export class WorkflowValidator {
|
||||
|
||||
// Check for self-referencing connections
|
||||
if (connection.node === sourceName) {
|
||||
// This is only a warning for non-loop nodes
|
||||
if (sourceNode && sourceNode.type !== 'nodes-base.splitInBatches') {
|
||||
// This is only a warning for non-loop nodes (not SplitInBatches)
|
||||
if (sourceNode && !isSplitInBatches) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
message: `Node "${sourceName}" has a self-referencing connection. This can cause infinite loops.`
|
||||
|
||||
265
tests/unit/mcp/handlers-deploy-template.test.ts
Normal file
265
tests/unit/mcp/handlers-deploy-template.test.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Unit tests for handleDeployTemplate handler - input validation and schema tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Test the schema directly without needing full API mocking
|
||||
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)
|
||||
});
|
||||
|
||||
describe('handleDeployTemplate Schema Validation', () => {
|
||||
describe('Input Schema', () => {
|
||||
it('should require templateId as a positive integer', () => {
|
||||
// Valid input
|
||||
const validResult = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
expect(validResult.success).toBe(true);
|
||||
|
||||
// Invalid: missing templateId
|
||||
const missingResult = deployTemplateSchema.safeParse({});
|
||||
expect(missingResult.success).toBe(false);
|
||||
|
||||
// Invalid: templateId as string
|
||||
const stringResult = deployTemplateSchema.safeParse({ templateId: '123' });
|
||||
expect(stringResult.success).toBe(false);
|
||||
|
||||
// Invalid: negative templateId
|
||||
const negativeResult = deployTemplateSchema.safeParse({ templateId: -1 });
|
||||
expect(negativeResult.success).toBe(false);
|
||||
|
||||
// Invalid: zero templateId
|
||||
const zeroResult = deployTemplateSchema.safeParse({ templateId: 0 });
|
||||
expect(zeroResult.success).toBe(false);
|
||||
|
||||
// Invalid: decimal templateId
|
||||
const decimalResult = deployTemplateSchema.safeParse({ templateId: 123.5 });
|
||||
expect(decimalResult.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept optional name parameter', () => {
|
||||
const withName = deployTemplateSchema.safeParse({
|
||||
templateId: 123,
|
||||
name: 'Custom Name'
|
||||
});
|
||||
expect(withName.success).toBe(true);
|
||||
if (withName.success) {
|
||||
expect(withName.data.name).toBe('Custom Name');
|
||||
}
|
||||
|
||||
const withoutName = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
expect(withoutName.success).toBe(true);
|
||||
if (withoutName.success) {
|
||||
expect(withoutName.data.name).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should default autoUpgradeVersions to true', () => {
|
||||
const result = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.autoUpgradeVersions).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow setting autoUpgradeVersions to false', () => {
|
||||
const result = deployTemplateSchema.safeParse({
|
||||
templateId: 123,
|
||||
autoUpgradeVersions: false
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.autoUpgradeVersions).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should default validate to true', () => {
|
||||
const result = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.validate).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should default stripCredentials to true', () => {
|
||||
const result = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.stripCredentials).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept all parameters together', () => {
|
||||
const result = deployTemplateSchema.safeParse({
|
||||
templateId: 2776,
|
||||
name: 'My Deployed Workflow',
|
||||
autoUpgradeVersions: false,
|
||||
validate: false,
|
||||
stripCredentials: false
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.templateId).toBe(2776);
|
||||
expect(result.data.name).toBe('My Deployed Workflow');
|
||||
expect(result.data.autoUpgradeVersions).toBe(false);
|
||||
expect(result.data.validate).toBe(false);
|
||||
expect(result.data.stripCredentials).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDeployTemplate Helper Functions', () => {
|
||||
describe('Credential Extraction Logic', () => {
|
||||
it('should extract credential types from node credentials object', () => {
|
||||
const nodes = [
|
||||
{
|
||||
id: 'node-1',
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
credentials: {
|
||||
slackApi: { id: 'cred-1', name: 'My Slack' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'node-2',
|
||||
name: 'Google Sheets',
|
||||
type: 'n8n-nodes-base.googleSheets',
|
||||
credentials: {
|
||||
googleSheetsOAuth2Api: { id: 'cred-2', name: 'My Google' }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'node-3',
|
||||
name: 'Set',
|
||||
type: 'n8n-nodes-base.set'
|
||||
// No credentials
|
||||
}
|
||||
];
|
||||
|
||||
// Simulate the credential extraction logic from the handler
|
||||
const requiredCredentials: Array<{
|
||||
nodeType: string;
|
||||
nodeName: string;
|
||||
credentialType: string;
|
||||
}> = [];
|
||||
|
||||
for (const node of 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(requiredCredentials).toHaveLength(2);
|
||||
expect(requiredCredentials[0]).toEqual({
|
||||
nodeType: 'n8n-nodes-base.slack',
|
||||
nodeName: 'Slack',
|
||||
credentialType: 'slackApi'
|
||||
});
|
||||
expect(requiredCredentials[1]).toEqual({
|
||||
nodeType: 'n8n-nodes-base.googleSheets',
|
||||
nodeName: 'Google Sheets',
|
||||
credentialType: 'googleSheetsOAuth2Api'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Credential Stripping Logic', () => {
|
||||
it('should remove credentials property from nodes', () => {
|
||||
const nodes = [
|
||||
{
|
||||
id: 'node-1',
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
typeVersion: 2,
|
||||
position: [250, 300],
|
||||
parameters: { channel: '#general' },
|
||||
credentials: {
|
||||
slackApi: { id: 'cred-1', name: 'My Slack' }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Simulate the credential stripping logic from the handler
|
||||
const strippedNodes = nodes.map((node: any) => {
|
||||
const { credentials, ...rest } = node;
|
||||
return rest;
|
||||
});
|
||||
|
||||
expect(strippedNodes[0].credentials).toBeUndefined();
|
||||
expect(strippedNodes[0].id).toBe('node-1');
|
||||
expect(strippedNodes[0].name).toBe('Slack');
|
||||
expect(strippedNodes[0].parameters).toEqual({ channel: '#general' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Trigger Type Detection Logic', () => {
|
||||
it('should identify trigger nodes', () => {
|
||||
const testCases = [
|
||||
{ type: 'n8n-nodes-base.scheduleTrigger', expected: 'scheduleTrigger' },
|
||||
{ type: 'n8n-nodes-base.webhook', expected: 'webhook' },
|
||||
{ type: 'n8n-nodes-base.emailReadImapTrigger', expected: 'emailReadImapTrigger' },
|
||||
{ type: 'n8n-nodes-base.googleDriveTrigger', expected: 'googleDriveTrigger' }
|
||||
];
|
||||
|
||||
for (const { type, expected } of testCases) {
|
||||
const nodes = [{ type, name: 'Trigger' }];
|
||||
|
||||
// Simulate the trigger detection logic from the handler
|
||||
const triggerNode = 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';
|
||||
|
||||
expect(triggerType).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return manual for workflows without trigger', () => {
|
||||
const nodes = [
|
||||
{ type: 'n8n-nodes-base.set', name: 'Set' },
|
||||
{ type: 'n8n-nodes-base.httpRequest', name: 'HTTP Request' }
|
||||
];
|
||||
|
||||
const triggerNode = 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';
|
||||
|
||||
expect(triggerType).toBe('manual');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tool Definition Validation', () => {
|
||||
it('should have correct tool name', () => {
|
||||
// This tests that the tool is properly exported
|
||||
const toolName = 'n8n_deploy_template';
|
||||
expect(toolName).toBe('n8n_deploy_template');
|
||||
});
|
||||
|
||||
it('should have required parameter templateId in schema', () => {
|
||||
// Validate that the schema correctly requires templateId
|
||||
const validResult = deployTemplateSchema.safeParse({ templateId: 123 });
|
||||
const invalidResult = deployTemplateSchema.safeParse({});
|
||||
|
||||
expect(validResult.success).toBe(true);
|
||||
expect(invalidResult.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1072,10 +1072,10 @@ describe('handlers-n8n-manager', () => {
|
||||
enabled: true,
|
||||
},
|
||||
managementTools: {
|
||||
count: 12,
|
||||
count: 13,
|
||||
enabled: true,
|
||||
},
|
||||
totalAvailable: 19,
|
||||
totalAvailable: 20,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user