This is page 2 of 2. Use http://codebase.md/leonardsellem/n8n-mcp-server?page={x} to view the full context.
# Directory Structure
```
├── .babelrc
├── .env.example
├── .eslintrc.json
├── .github
│ └── workflows
│ ├── claude-code-review.yml
│ ├── claude.yml
│ ├── docker-publish.yml
│ └── release-package.yml
├── .gitignore
├── AGENTS.md
├── babel.config.cjs
├── CLAUDE.md
├── Dockerfile
├── docs
│ ├── .gitkeep
│ ├── api
│ │ ├── dynamic-resources.md
│ │ ├── execution-tools.md
│ │ ├── index.md
│ │ ├── static-resources.md
│ │ └── workflow-tools.md
│ ├── development
│ │ ├── architecture.md
│ │ ├── extending.md
│ │ ├── index.md
│ │ └── testing.md
│ ├── examples
│ │ ├── advanced-scenarios.md
│ │ ├── basic-examples.md
│ │ ├── index.md
│ │ └── integration-examples.md
│ ├── images
│ │ ├── architecture.png.placeholder
│ │ └── n8n-api-key.png.placeholder
│ ├── index.md
│ └── setup
│ ├── configuration.md
│ ├── index.md
│ ├── installation.md
│ └── troubleshooting.md
├── jest.config.cjs
├── LICENSE
├── manual_verify_update.mjs
├── n8n-openapi.yml
├── package-lock.json
├── package.json
├── README.md
├── requirements.txt
├── run-tests.js
├── smithery.yaml
├── src
│ ├── .gitkeep
│ ├── api
│ │ ├── client.ts
│ │ └── n8n-client.ts
│ ├── config
│ │ ├── environment.ts
│ │ └── server.ts
│ ├── errors
│ │ ├── error-codes.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── resources
│ │ ├── dynamic
│ │ │ ├── execution.ts
│ │ │ └── workflow.ts
│ │ ├── index.ts
│ │ └── static
│ │ ├── execution-stats.ts
│ │ └── workflows.ts
│ ├── tools
│ │ ├── execution
│ │ │ ├── base-handler.ts
│ │ │ ├── delete.ts
│ │ │ ├── get.ts
│ │ │ ├── handler.ts
│ │ │ ├── index.ts
│ │ │ ├── list.ts
│ │ │ └── run.ts
│ │ └── workflow
│ │ ├── activate.ts
│ │ ├── base-handler.ts
│ │ ├── create.ts
│ │ ├── deactivate.ts
│ │ ├── delete.ts
│ │ ├── get.ts
│ │ ├── handler.ts
│ │ ├── index.ts
│ │ ├── list.ts
│ │ └── update.ts
│ ├── types
│ │ └── index.ts
│ └── utils
│ ├── execution-formatter.ts
│ └── resource-formatter.ts
├── tests
│ ├── jest-globals.d.ts
│ ├── mocks
│ │ ├── axios-mock.ts
│ │ └── n8n-fixtures.ts
│ ├── README.md
│ ├── test-setup.ts
│ ├── tsconfig.json
│ └── unit
│ ├── api
│ │ ├── client.test.ts.bak
│ │ └── simple-client.test.ts
│ ├── config
│ │ ├── environment.test.ts
│ │ ├── environment.test.ts.bak
│ │ └── simple-environment.test.ts
│ ├── resources
│ │ └── dynamic
│ │ └── workflow.test.ts
│ ├── tools
│ │ └── workflow
│ │ ├── list.test.ts.bak
│ │ └── simple-tool.test.ts
│ └── utils
│ ├── execution-formatter.test.ts
│ └── resource-formatter.test.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/tests/unit/config/environment.test.ts:
--------------------------------------------------------------------------------
```typescript
import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import findConfig from 'find-config'; // Use the actual find-config
import { loadEnvironmentVariables } from '../../../src/config/environment';
import { ENV_VARS } from '../../../src/config/environment'; // To access defined var names
// Determine project root for placing dummy .env file
let projectRootDir: string | null = null;
let dummyEnvPath: string | null = null;
try {
const packageJsonPath = findConfig('package.json');
if (packageJsonPath) {
projectRootDir = path.dirname(packageJsonPath);
dummyEnvPath = path.resolve(projectRootDir, '.env.test_dummy'); // Use a distinct name
} else {
console.error("Could not find project root (package.json). Tests involving .env file might fail or be skipped.");
}
} catch (e) {
console.error("Error finding project root:", e);
}
const originalEnv = { ...process.env };
const clearTestEnvVars = () => {
delete process.env[ENV_VARS.N8N_API_URL];
delete process.env[ENV_VARS.N8N_API_KEY];
delete process.env[ENV_VARS.N8N_WEBHOOK_USERNAME];
delete process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD];
};
const saveEnvState = () => {
return { ...process.env };
};
const DUMMY_ENV_CONTENT = `
${ENV_VARS.N8N_API_URL}=http://dummyapi.com
${ENV_VARS.N8N_API_KEY}=dummyapikey
${ENV_VARS.N8N_WEBHOOK_USERNAME}=dummyuser
${ENV_VARS.N8N_WEBHOOK_PASSWORD}=dummypassword
`;
describe('loadEnvironmentVariables', () => {
beforeEach(() => {
jest.resetModules(); // Reset module cache, critical for dotenv
process.env = { ...originalEnv }; // Restore original env
clearTestEnvVars(); // Clear our specific vars
// Ensure dummy .env is clean before each test that might create it
if (dummyEnvPath && fs.existsSync(dummyEnvPath)) {
fs.unlinkSync(dummyEnvPath);
}
});
afterEach(() => {
// Restore original env
process.env = { ...originalEnv };
// Clean up dummy .env file if it exists after a test
if (dummyEnvPath && fs.existsSync(dummyEnvPath)) {
try {
fs.unlinkSync(dummyEnvPath);
} catch (e) {
// In case the test itself deleted it, or it was never created.
}
}
});
// Test Case 1: All environment variables set
test('should not change process.env if all required env vars are already set', () => {
process.env[ENV_VARS.N8N_API_URL] = 'http://existingapi.com';
process.env[ENV_VARS.N8N_API_KEY] = 'existingapikey';
process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'existinguser';
process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'existingpassword';
const envStateBeforeLoad = saveEnvState();
loadEnvironmentVariables(); // Should not load .env because vars are present
expect(process.env).toEqual(envStateBeforeLoad);
});
// Test Case 2: No environment variables set, .env file exists
test('should load vars from .env file if no required env vars are set and .env exists', () => {
if (!projectRootDir || !dummyEnvPath) {
console.warn("Skipping test: Project root not found, cannot create dummy .env file.");
return; // or expect.hasAssertions() with a different path
}
// Pre-condition: specific vars are not set (cleared in beforeEach)
expect(process.env[ENV_VARS.N8N_API_URL]).toBeUndefined();
fs.writeFileSync(dummyEnvPath, DUMMY_ENV_CONTENT);
// Temporarily point findConfig's "idea" of .env to our dummy .env by hijacking process.env for dotenv
// This is tricky because loadEnvironmentVariables uses findConfig to locate package.json, then path.resolve for .env
// The most straightforward way is to ensure findConfig returns our projectRootDir, and then dotenv loads our dummyEnvPath.
// The current implementation of loadEnvironmentVariables is:
// const projectRoot = findConfig('package.json');
// if (projectRoot) {
// const envPath = path.resolve(path.dirname(projectRoot), '.env'); <--- This is the key
// dotenv.config({ path: envPath });
// }
// So, for this test, we need to make `path.resolve` point to `dummyEnvPath` OR make the actual `.env` the dummy.
// Let's rename the actual .env if it exists, place our dummy, then restore.
// A simpler approach for testing: the function loads ".env". So we make our dummy file THE ".env".
const actualEnvPath = path.resolve(projectRootDir, '.env');
let actualEnvRenamedPath: string | null = null;
if (fs.existsSync(actualEnvPath)) {
actualEnvRenamedPath = actualEnvPath + '.backup';
fs.renameSync(actualEnvPath, actualEnvRenamedPath);
}
fs.writeFileSync(actualEnvPath, DUMMY_ENV_CONTENT); // Write our dummy content to the actual .env path
try {
loadEnvironmentVariables();
expect(process.env[ENV_VARS.N8N_API_URL]).toBe('http://dummyapi.com');
expect(process.env[ENV_VARS.N8N_API_KEY]).toBe('dummyapikey');
expect(process.env[ENV_VARS.N8N_WEBHOOK_USERNAME]).toBe('dummyuser');
expect(process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD]).toBe('dummypassword');
} finally {
// Clean up: remove our dummy .env and restore original .env if it was renamed
if (fs.existsSync(actualEnvPath)) {
fs.unlinkSync(actualEnvPath);
}
if (actualEnvRenamedPath && fs.existsSync(actualEnvRenamedPath)) {
fs.renameSync(actualEnvRenamedPath, actualEnvPath);
}
}
});
// Test Case 3: No environment variables set, no .env file exists
test('should not change process.env if no required env vars are set and no .env file exists', () => {
if (!projectRootDir) {
console.warn("Skipping parts of test: Project root not found, .env file check might be unreliable.");
// We can still proceed as findConfig would return null or the .env file wouldn't be found
} else {
const actualEnvPath = path.resolve(projectRootDir, '.env');
if (fs.existsSync(actualEnvPath)) {
// This test requires no .env file, so if one exists (e.g. a real one for dev), this test is harder.
// For CI/isolated env, it should be fine. Here we assume it's okay if it doesn't exist.
// If it *does* exist, the test might reflect that it *was* loaded if not handled.
console.warn(`Warning: Test 'no .env file exists' running when an actual .env file is present at ${actualEnvPath}. This test assumes it won't be loaded or is empty.`);
// To be robust, we'd need to ensure it's not there, similar to Test Case 2's cleanup.
// For now, we assume `loadEnvironmentVariables` won't find one if `findConfig` fails or the file is empty/irrelevant.
}
}
// Vars are cleared in beforeEach
const envStateBeforeLoad = saveEnvState();
loadEnvironmentVariables(); // Should not find a .env file to load (or findConfig returns null)
expect(process.env[ENV_VARS.N8N_API_URL]).toBeUndefined();
expect(process.env[ENV_VARS.N8N_API_KEY]).toBeUndefined();
expect(process.env[ENV_VARS.N8N_WEBHOOK_USERNAME]).toBeUndefined();
expect(process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD]).toBeUndefined();
// Check if other env vars were not disturbed (more robust check)
expect(process.env).toEqual(envStateBeforeLoad);
});
// Test Case 4: Some environment variables set
test('should not change process.env if some (but not all) required env vars are set', () => {
process.env[ENV_VARS.N8N_API_URL] = 'http://partialapi.com';
process.env[ENV_VARS.N8N_API_KEY] = 'partialapikey';
// N8N_WEBHOOK_USERNAME and N8N_WEBHOOK_PASSWORD are not set (cleared by beforeEach)
const envStateBeforeLoad = saveEnvState();
loadEnvironmentVariables(); // Should not load .env because some vars are present
expect(process.env).toEqual(envStateBeforeLoad);
expect(process.env[ENV_VARS.N8N_API_URL]).toBe('http://partialapi.com');
expect(process.env[ENV_VARS.N8N_API_KEY]).toBe('partialapikey');
expect(process.env[ENV_VARS.N8N_WEBHOOK_USERNAME]).toBeUndefined();
expect(process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD]).toBeUndefined();
});
});
```
--------------------------------------------------------------------------------
/src/api/client.ts:
--------------------------------------------------------------------------------
```typescript
/**
* n8n API Client
*
* This module provides a client for interacting with the n8n API.
*/
import axios, { AxiosInstance } from 'axios';
import { EnvConfig } from '../config/environment.js';
import { handleAxiosError, N8nApiError } from '../errors/index.js';
/**
* n8n API Client class for making requests to the n8n API
*/
export class N8nApiClient {
private axiosInstance: AxiosInstance;
private config: EnvConfig;
/**
* Create a new n8n API client
*
* @param config Environment configuration
*/
constructor(config: EnvConfig) {
this.config = config;
this.axiosInstance = axios.create({
baseURL: config.n8nApiUrl,
headers: {
'X-N8N-API-KEY': config.n8nApiKey,
'Accept': 'application/json',
},
timeout: 10000, // 10 seconds
});
// Add request debugging if debug mode is enabled
if (config.debug) {
this.axiosInstance.interceptors.request.use(request => {
console.error(`[DEBUG] Request: ${request.method?.toUpperCase()} ${request.url}`);
return request;
});
this.axiosInstance.interceptors.response.use(response => {
console.error(`[DEBUG] Response: ${response.status} ${response.statusText}`);
return response;
});
}
}
/**
* Check connectivity to the n8n API
*
* @returns Promise that resolves if connectivity check succeeds
* @throws N8nApiError if connectivity check fails
*/
async checkConnectivity(): Promise<void> {
try {
// Try to fetch health endpoint or workflows
const response = await this.axiosInstance.get('/workflows');
if (response.status !== 200) {
throw new N8nApiError(
'n8n API connectivity check failed',
response.status
);
}
if (this.config.debug) {
console.error(`[DEBUG] Successfully connected to n8n API at ${this.config.n8nApiUrl}`);
console.error(`[DEBUG] Found ${response.data.data?.length || 0} workflows`);
}
} catch (error) {
throw handleAxiosError(error, 'Failed to connect to n8n API');
}
}
/**
* Get the axios instance for making custom requests
*
* @returns Axios instance
*/
getAxiosInstance(): AxiosInstance {
return this.axiosInstance;
}
/**
* Get all workflows from n8n
*
* @returns Array of workflow objects
*/
async getWorkflows(): Promise<any[]> {
try {
const response = await this.axiosInstance.get('/workflows');
return response.data.data || [];
} catch (error) {
throw handleAxiosError(error, 'Failed to fetch workflows');
}
}
/**
* Get a specific workflow by ID
*
* @param id Workflow ID
* @returns Workflow object
*/
async getWorkflow(id: string): Promise<any> {
try {
const response = await this.axiosInstance.get(`/workflows/${id}`);
return response.data;
} catch (error) {
throw handleAxiosError(error, `Failed to fetch workflow ${id}`);
}
}
/**
* Get all workflow executions
*
* @returns Array of execution objects
*/
async getExecutions(): Promise<any[]> {
try {
const response = await this.axiosInstance.get('/executions');
return response.data.data || [];
} catch (error) {
throw handleAxiosError(error, 'Failed to fetch executions');
}
}
/**
* Get a specific execution by ID
*
* @param id Execution ID
* @returns Execution object
*/
async getExecution(id: string): Promise<any> {
try {
const response = await this.axiosInstance.get(`/executions/${id}`);
return response.data;
} catch (error) {
throw handleAxiosError(error, `Failed to fetch execution ${id}`);
}
}
/**
* Execute a workflow by ID
*
* @param id Workflow ID
* @param data Optional data to pass to the workflow
* @returns Execution result
*/
async executeWorkflow(id: string, data?: Record<string, any>): Promise<any> {
try {
const response = await this.axiosInstance.post(`/workflows/${id}/execute`, data || {});
return response.data;
} catch (error) {
throw handleAxiosError(error, `Failed to execute workflow ${id}`);
}
}
/**
* Create a new workflow
*
* @param workflow Workflow object to create
* @returns Created workflow
*/
async createWorkflow(workflow: Record<string, any>): Promise<any> {
try {
// Make sure settings property is present
if (!workflow.settings) {
workflow.settings = {
saveExecutionProgress: true,
saveManualExecutions: true,
saveDataErrorExecution: "all",
saveDataSuccessExecution: "all",
executionTimeout: 3600,
timezone: "UTC"
};
}
// Remove read-only properties that cause issues
const workflowToCreate = { ...workflow };
delete workflowToCreate.active; // Remove active property as it's read-only
delete workflowToCreate.id; // Remove id property if it exists
delete workflowToCreate.createdAt; // Remove createdAt property if it exists
delete workflowToCreate.updatedAt; // Remove updatedAt property if it exists
delete workflowToCreate.tags; // Remove tags property as it's read-only
// Log request for debugging
console.error('[DEBUG] Creating workflow with data:', JSON.stringify(workflowToCreate, null, 2));
const response = await this.axiosInstance.post('/workflows', workflowToCreate);
return response.data;
} catch (error) {
console.error('[ERROR] Create workflow error:', error);
throw handleAxiosError(error, 'Failed to create workflow');
}
}
/**
* Update an existing workflow
*
* @param id Workflow ID
* @param workflow Updated workflow object
* @returns Updated workflow
*/
async updateWorkflow(id: string, workflow: Record<string, any>): Promise<any> {
try {
// Remove read-only properties that cause issues with n8n API v1
// According to n8n API schema, only name, nodes, connections, settings, and staticData are allowed
const workflowToUpdate = { ...workflow };
delete workflowToUpdate.id; // Remove id property as it's read-only
delete workflowToUpdate.active; // Remove active property as it's read-only
delete workflowToUpdate.createdAt; // Remove createdAt property as it's read-only
delete workflowToUpdate.updatedAt; // Remove updatedAt property as it's read-only
delete workflowToUpdate.tags; // Remove tags property as it's read-only
// Log request for debugging
if (this.config.debug) {
console.error('[DEBUG] Updating workflow with data:', JSON.stringify(workflowToUpdate, null, 2));
}
const response = await this.axiosInstance.put(`/workflows/${id}`, workflowToUpdate);
return response.data;
} catch (error) {
throw handleAxiosError(error, `Failed to update workflow ${id}`);
}
}
/**
* Delete a workflow
*
* @param id Workflow ID
* @returns Deleted workflow
*/
async deleteWorkflow(id: string): Promise<any> {
try {
const response = await this.axiosInstance.delete(`/workflows/${id}`);
return response.data;
} catch (error) {
throw handleAxiosError(error, `Failed to delete workflow ${id}`);
}
}
/**
* Activate a workflow
*
* @param id Workflow ID
* @returns Activated workflow
*/
async activateWorkflow(id: string): Promise<any> {
try {
const response = await this.axiosInstance.post(`/workflows/${id}/activate`);
return response.data;
} catch (error) {
throw handleAxiosError(error, `Failed to activate workflow ${id}`);
}
}
/**
* Deactivate a workflow
*
* @param id Workflow ID
* @returns Deactivated workflow
*/
async deactivateWorkflow(id: string): Promise<any> {
try {
const response = await this.axiosInstance.post(`/workflows/${id}/deactivate`);
return response.data;
} catch (error) {
throw handleAxiosError(error, `Failed to deactivate workflow ${id}`);
}
}
/**
* Delete an execution
*
* @param id Execution ID
* @returns Deleted execution or success message
*/
async deleteExecution(id: string): Promise<any> {
try {
const response = await this.axiosInstance.delete(`/executions/${id}`);
return response.data;
} catch (error) {
throw handleAxiosError(error, `Failed to delete execution ${id}`);
}
}
}
/**
* Create and return a configured n8n API client
*
* @param config Environment configuration
* @returns n8n API client instance
*/
export function createApiClient(config: EnvConfig): N8nApiClient {
return new N8nApiClient(config);
}
```
--------------------------------------------------------------------------------
/docs/development/testing.md:
--------------------------------------------------------------------------------
```markdown
# Testing
This document describes the testing approach for the n8n MCP Server and provides guidelines for writing effective tests.
## Overview
The n8n MCP Server uses Jest as its testing framework and follows a multi-level testing approach:
1. **Unit Tests**: Test individual components in isolation
2. **Integration Tests**: Test interactions between components
3. **End-to-End Tests**: Test the entire system as a whole
Tests are organized in the `tests/` directory, with a structure that mirrors the `src/` directory.
## Running Tests
### Running All Tests
To run all tests:
```bash
npm test
```
This command runs all tests and outputs a summary of the results.
### Running Tests with Coverage
To run tests with coverage reporting:
```bash
npm run test:coverage
```
This generates coverage reports in the `coverage/` directory, including HTML reports that you can view in a browser.
### Running Tests in Watch Mode
During development, you can run tests in watch mode, which will automatically rerun tests when files change:
```bash
npm run test:watch
```
### Running Specific Tests
To run tests in a specific file or directory:
```bash
npx jest path/to/test-file.test.ts
```
Or to run tests matching a specific pattern:
```bash
npx jest -t "test pattern"
```
## Test Structure
Tests are organized into the following directories:
- `tests/unit/`: Unit tests for individual components
- `tests/integration/`: Integration tests that test interactions between components
- `tests/e2e/`: End-to-end tests that test the entire system
- `tests/mocks/`: Shared test fixtures and mocks
### Unit Tests
Unit tests are organized in a structure that mirrors the `src/` directory. For example:
- `src/api/n8n-client.ts` has a corresponding test at `tests/unit/api/n8n-client.test.ts`
- `src/tools/workflow/list.ts` has a corresponding test at `tests/unit/tools/workflow/list.test.ts`
### Integration Tests
Integration tests focus on testing interactions between components, such as:
- Testing that tools correctly use the API client
- Testing that resources correctly format data from the API
### End-to-End Tests
End-to-end tests test the entire system, from the transport layer to the API client and back.
## Writing Effective Tests
### Unit Test Example
Here's an example of a unit test for a workflow tool:
```typescript
// tests/unit/tools/workflow/list.test.ts
import { describe, it, expect, jest } from '@jest/globals';
import { getListWorkflowsToolDefinition, handleListWorkflows } from '../../../../src/tools/workflow/list.js';
import { N8nClient } from '../../../../src/api/n8n-client.js';
// Mock data
const mockWorkflows = [
{
id: '1234abc',
name: 'Test Workflow 1',
active: true,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-02T14:30:00.000Z'
},
{
id: '5678def',
name: 'Test Workflow 2',
active: false,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-12T10:15:00.000Z'
}
];
describe('Workflow List Tool', () => {
describe('getListWorkflowsToolDefinition', () => {
it('should return the correct tool definition', () => {
const definition = getListWorkflowsToolDefinition();
expect(definition.name).toBe('workflow_list');
expect(definition.description).toBeTruthy();
expect(definition.inputSchema).toBeDefined();
expect(definition.inputSchema.properties).toHaveProperty('active');
expect(definition.inputSchema.required).toEqual([]);
});
});
describe('handleListWorkflows', () => {
it('should return all workflows when no filter is provided', async () => {
// Mock the API client
const mockClient = {
getWorkflows: jest.fn().mockResolvedValue(mockWorkflows)
};
const result = await handleListWorkflows(mockClient as unknown as N8nClient, {});
expect(mockClient.getWorkflows).toHaveBeenCalledWith(undefined);
expect(result.isError).toBeFalsy();
// Parse the JSON text to check the content
const content = JSON.parse(result.content[0].text);
expect(content).toHaveLength(2);
expect(content[0].id).toBe('1234abc');
expect(content[1].id).toBe('5678def');
});
it('should filter workflows by active status', async () => {
// Mock the API client
const mockClient = {
getWorkflows: jest.fn().mockResolvedValue(mockWorkflows)
};
const result = await handleListWorkflows(mockClient as unknown as N8nClient, { active: true });
expect(mockClient.getWorkflows).toHaveBeenCalledWith(true);
expect(result.isError).toBeFalsy();
// Parse the JSON text to check the content
const content = JSON.parse(result.content[0].text);
expect(content).toHaveLength(2);
});
it('should handle API errors', async () => {
// Mock the API client to throw an error
const mockClient = {
getWorkflows: jest.fn().mockRejectedValue(new Error('API error'))
};
const result = await handleListWorkflows(mockClient as unknown as N8nClient, {});
expect(result.isError).toBeTruthy();
expect(result.content[0].text).toContain('API error');
});
});
});
```
### Integration Test Example
Here's an example of an integration test that tests the interaction between a resource handler and the API client:
```typescript
// tests/integration/resources/static/workflows.test.ts
import { describe, it, expect, jest } from '@jest/globals';
import { handleWorkflowsRequest, WORKFLOWS_URI } from '../../../../src/resources/static/workflows.js';
import { N8nClient } from '../../../../src/api/n8n-client.js';
// Mock data
const mockWorkflows = [
{
id: '1234abc',
name: 'Test Workflow 1',
active: true,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-02T14:30:00.000Z'
},
{
id: '5678def',
name: 'Test Workflow 2',
active: false,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-12T10:15:00.000Z'
}
];
describe('Workflows Resource Handler', () => {
it('should return a properly formatted response', async () => {
// Mock the API client
const mockClient = {
getWorkflows: jest.fn().mockResolvedValue(mockWorkflows)
};
const response = await handleWorkflowsRequest(mockClient as unknown as N8nClient);
expect(mockClient.getWorkflows).toHaveBeenCalled();
expect(response.contents).toHaveLength(1);
expect(response.contents[0].uri).toBe(WORKFLOWS_URI);
expect(response.contents[0].mimeType).toBe('application/json');
// Parse the JSON text to check the content
const content = JSON.parse(response.contents[0].text);
expect(content).toHaveProperty('workflows');
expect(content.workflows).toHaveLength(2);
expect(content.count).toBe(2);
expect(content.workflows[0].id).toBe('1234abc');
});
it('should handle API errors', async () => {
// Mock the API client to throw an error
const mockClient = {
getWorkflows: jest.fn().mockRejectedValue(new Error('API error'))
};
await expect(handleWorkflowsRequest(mockClient as unknown as N8nClient))
.rejects
.toThrow('Failed to retrieve workflows');
});
});
```
### End-to-End Test Example
Here's an example of an end-to-end test that tests the entire system:
```typescript
// tests/e2e/workflow-operations.test.ts
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { MemoryServerTransport } from '@modelcontextprotocol/sdk/server/memory.js';
import { createServer } from '../../src/index.js';
describe('End-to-End Workflow Operations', () => {
let server: Server;
let transport: MemoryServerTransport;
beforeAll(async () => {
// Mock the environment
process.env.N8N_API_URL = 'http://localhost:5678/api/v1';
process.env.N8N_API_KEY = 'test-api-key';
// Create the server with a memory transport
transport = new MemoryServerTransport();
server = await createServer(transport);
});
afterAll(async () => {
await server.close();
});
it('should list workflows', async () => {
// Send a request to list workflows
const response = await transport.sendRequest({
jsonrpc: '2.0',
id: '1',
method: 'callTool',
params: {
name: 'workflow_list',
arguments: {}
}
});
expect(response.result).toBeDefined();
expect(response.result.content).toHaveLength(1);
expect(response.result.content[0].type).toBe('text');
// Parse the JSON text to check the content
const content = JSON.parse(response.result.content[0].text);
expect(Array.isArray(content)).toBe(true);
});
it('should retrieve a workflow by ID', async () => {
// Send a request to get a workflow
const response = await transport.sendRequest({
jsonrpc: '2.0',
id: '2',
method: 'callTool',
params: {
name: 'workflow_get',
arguments: {
id: '1234abc'
}
}
});
expect(response.result).toBeDefined();
expect(response.result.content).toHaveLength(1);
expect(response.result.content[0].type).toBe('text');
// Parse the JSON text to check the content
const content = JSON.parse(response.result.content[0].text);
expect(content).toHaveProperty('id');
expect(content.id).toBe('1234abc');
});
});
```
## Test Fixtures and Mocks
To avoid duplication and improve test maintainability, common test fixtures and mocks are stored in the `tests/mocks/` directory.
### Axios Mock
The Axios HTTP client is mocked using `axios-mock-adapter` to simulate HTTP responses without making actual API calls:
```typescript
// tests/mocks/axios-mock.ts
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
// Create a new instance of the mock adapter
export const axiosMock = new MockAdapter(axios);
// Helper function to reset the mock adapter before each test
export function resetAxiosMock() {
axiosMock.reset();
}
```
### n8n API Fixtures
Common fixtures for n8n API responses are stored in a shared file:
```typescript
// tests/mocks/n8n-fixtures.ts
export const mockWorkflows = [
{
id: '1234abc',
name: 'Test Workflow 1',
active: true,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-02T14:30:00.000Z',
nodes: [
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.start',
position: [100, 200],
parameters: {}
}
],
connections: {}
},
{
id: '5678def',
name: 'Test Workflow 2',
active: false,
createdAt: '2025-03-01T12:00:00.000Z',
updatedAt: '2025-03-12T10:15:00.000Z',
nodes: [],
connections: {}
}
];
export const mockExecutions = [
{
id: 'exec123',
workflowId: '1234abc',
workflowName: 'Test Workflow 1',
status: 'success',
startedAt: '2025-03-10T15:00:00.000Z',
finishedAt: '2025-03-10T15:01:00.000Z',
mode: 'manual'
},
{
id: 'exec456',
workflowId: '1234abc',
workflowName: 'Test Workflow 1',
status: 'error',
startedAt: '2025-03-09T12:00:00.000Z',
finishedAt: '2025-03-09T12:00:10.000Z',
mode: 'manual'
}
];
```
## Test Environment
The test environment is configured in `jest.config.js` and `babel.config.js`. Key configurations include:
- TypeScript support via Babel
- ES module support
- Coverage reporting
The `tests/test-setup.ts` file contains global setup code that runs before tests:
```typescript
// tests/test-setup.ts
import { jest } from '@jest/globals';
import { resetAxiosMock } from './mocks/axios-mock';
// Reset mocks before each test
beforeEach(() => {
jest.clearAllMocks();
resetAxiosMock();
});
```
## Best Practices
### General Testing Guidelines
1. **Write tests first**: Follow a test-driven development (TDD) approach when possible.
2. **Test behavior, not implementation**: Focus on what a component does, not how it's implemented.
3. **Keep tests simple**: Each test should test one behavior or aspect of functionality.
4. **Use descriptive test names**: Test names should describe the expected behavior.
5. **Follow the AAA pattern**: Arrange, Act, Assert (setup, execute, verify).
### Mocking Best Practices
1. **Mock dependencies, not the unit under test**: Only mock external dependencies, not the code you're testing.
2. **Use the minimum viable mock**: Only mock the methods and behavior needed for the test.
3. **Ensure mock behavior is realistic**: Mocks should behave similarly to the real implementation.
4. **Verify interactions with mocks**: Use `expect(mock).toHaveBeenCalled()` to verify interactions.
### Error Testing Best Practices
1. **Test error cases**: Don't just test the happy path; test error handling too.
2. **Simulate errors with mocks**: Use mocks to simulate error scenarios.
3. **Verify error messages**: Ensure error messages are helpful and descriptive.
### Performance Testing Considerations
1. **Monitor test performance**: Slow tests can slow down development.
2. **Use test timeout values wisely**: Set appropriate timeout values for async tests.
3. **Minimize redundant setup**: Use `beforeEach` and `beforeAll` to avoid redundant setup.
## Continuous Integration
Tests are run automatically in CI environments on pull requests and commits to the main branch. The CI configuration ensures tests pass before code can be merged.
### CI Test Requirements
- All tests must pass
- Test coverage must not decrease
- Linting checks must pass
## Debugging Tests
### Console Output
You can use `console.log()` statements in your tests to debug issues:
```typescript
it('should do something', () => {
const result = doSomething();
console.log('Result:', result);
expect(result).toBe(expectedValue);
});
```
When running tests with Jest, console output will be displayed for failing tests by default.
### Using the Debugger
You can also use the Node.js debugger with Jest:
```bash
node --inspect-brk node_modules/.bin/jest --runInBand path/to/test
```
Then connect to the debugger with Chrome DevTools or VS Code.
## Conclusion
Thorough testing is essential for maintaining a reliable and robust n8n MCP Server. By following these guidelines and examples, you can write effective tests that help ensure your code works as expected and catches issues early.
```
--------------------------------------------------------------------------------
/docs/development/extending.md:
--------------------------------------------------------------------------------
```markdown
# Extending the Server
This guide explains how to extend the n8n MCP Server with new functionality.
## Overview
The n8n MCP Server is designed to be extensible, allowing developers to add new tools and resources without modifying existing code. This extensibility makes it easy to support new n8n features or customize the server for specific use cases.
## Adding a New Tool
Tools in the MCP server represent executable operations that AI assistants can use. To add a new tool, follow these steps:
### 1. Define the Tool Interface
Create a new TypeScript interface that defines the input parameters for your tool:
```typescript
// src/types/tools/my-tool.ts
export interface MyToolParams {
param1: string;
param2?: number; // Optional parameter
}
```
### 2. Create the Tool Handler
Create a new file for your tool in the appropriate category under `src/tools/`:
```typescript
// src/tools/category/my-tool.ts
import { ToolCallResponse, ToolDefinition } from '@modelcontextprotocol/sdk/types.js';
import { N8nClient } from '../../api/n8n-client.js';
import { MyToolParams } from '../../types/tools/my-tool.js';
// Define the tool
export function getMyToolDefinition(): ToolDefinition {
return {
name: 'my_tool',
description: 'Description of what my tool does',
inputSchema: {
type: 'object',
properties: {
param1: {
type: 'string',
description: 'Description of param1'
},
param2: {
type: 'number',
description: 'Description of param2'
}
},
required: ['param1']
}
};
}
// Implement the tool handler
export async function handleMyTool(
client: N8nClient,
params: MyToolParams
): Promise<ToolCallResponse> {
try {
// Implement the tool logic here
// Use the N8nClient to interact with n8n
// Return the response
return {
content: [
{
type: 'text',
text: 'Result of the operation'
}
]
};
} catch (error) {
// Handle errors
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`
}
],
isError: true
};
}
}
```
### 3. Register the Tool in the Handler
Update the main handler file for your tool category (e.g., `src/tools/category/handler.ts`):
```typescript
// src/tools/category/handler.ts
import { getMyToolDefinition, handleMyTool } from './my-tool.js';
// Add your tool to the tools object
export const categoryTools = {
// ... existing tools
my_tool: {
definition: getMyToolDefinition,
handler: handleMyTool
}
};
```
### 4. Add Handler to Main Server
Update the main tool handler registration in `src/index.ts`:
```typescript
// src/index.ts
import { categoryTools } from './tools/category/handler.js';
// In the server initialization
const server = new Server(
{
name: 'n8n-mcp-server',
version: '0.1.0'
},
{
capabilities: {
tools: {
// ... existing categories
category: true
}
}
}
);
// Register tool handlers
Object.entries(categoryTools).forEach(([name, { definition, handler }]) => {
server.setToolHandler(definition(), async (request) => {
return await handler(client, request.params.arguments as any);
});
});
```
### 5. Add Unit Tests
Create unit tests for your new tool:
```typescript
// tests/unit/tools/category/my-tool.test.ts
import { describe, it, expect, jest } from '@jest/globals';
import { getMyToolDefinition, handleMyTool } from '../../../../src/tools/category/my-tool.js';
describe('My Tool', () => {
describe('getMyToolDefinition', () => {
it('should return the correct tool definition', () => {
const definition = getMyToolDefinition();
expect(definition.name).toBe('my_tool');
expect(definition.description).toBeTruthy();
expect(definition.inputSchema).toBeDefined();
expect(definition.inputSchema.properties).toHaveProperty('param1');
expect(definition.inputSchema.required).toEqual(['param1']);
});
});
describe('handleMyTool', () => {
it('should handle valid parameters', async () => {
const mockClient = {
// Mock the necessary client methods
};
const result = await handleMyTool(mockClient as any, {
param1: 'test value'
});
expect(result.isError).toBeFalsy();
expect(result.content[0].text).toBeTruthy();
});
it('should handle errors properly', async () => {
const mockClient = {
// Mock client that throws an error
someMethod: jest.fn().mockRejectedValue(new Error('Test error'))
};
const result = await handleMyTool(mockClient as any, {
param1: 'test value'
});
expect(result.isError).toBeTruthy();
expect(result.content[0].text).toContain('Error');
});
});
});
```
## Adding a New Resource
Resources in the MCP server provide data access through URI-based templates. To add a new resource, follow these steps:
### 1. Create a Static Resource (No Parameters)
For a resource that doesn't require parameters:
```typescript
// src/resources/static/my-resource.ts
import { McpError, ReadResourceResponse } from '@modelcontextprotocol/sdk/types.js';
import { ErrorCode } from '../../errors/error-codes.js';
import { N8nClient } from '../../api/n8n-client.js';
export const MY_RESOURCE_URI = 'n8n://my-resource';
export async function handleMyResourceRequest(
client: N8nClient
): Promise<ReadResourceResponse> {
try {
// Implement the resource logic
// Use the N8nClient to interact with n8n
// Return the response
return {
contents: [
{
uri: MY_RESOURCE_URI,
mimeType: 'application/json',
text: JSON.stringify(
{
// Resource data
property1: 'value1',
property2: 'value2'
},
null,
2
)
}
]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to retrieve resource: ${error.message}`
);
}
}
```
### 2. Create a Dynamic Resource (With Parameters)
For a resource that requires parameters:
```typescript
// src/resources/dynamic/my-resource.ts
import { McpError, ReadResourceResponse } from '@modelcontextprotocol/sdk/types.js';
import { ErrorCode } from '../../errors/error-codes.js';
import { N8nClient } from '../../api/n8n-client.js';
export const MY_RESOURCE_URI_TEMPLATE = 'n8n://my-resource/{id}';
export function matchMyResourceUri(uri: string): { id: string } | null {
const match = uri.match(/^n8n:\/\/my-resource\/([^/]+)$/);
if (!match) return null;
return {
id: decodeURIComponent(match[1])
};
}
export async function handleMyResourceRequest(
client: N8nClient,
uri: string
): Promise<ReadResourceResponse> {
const params = matchMyResourceUri(uri);
if (!params) {
throw new McpError(
ErrorCode.InvalidRequest,
`Invalid URI format: ${uri}`
);
}
try {
// Implement the resource logic using params.id
// Use the N8nClient to interact with n8n
// Return the response
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(
{
// Resource data with the specific ID
id: params.id,
property1: 'value1',
property2: 'value2'
},
null,
2
)
}
]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to retrieve resource: ${error.message}`
);
}
}
```
### 3. Register Resources in the Handler Files
Update the resource handler registration:
#### For Static Resources
```typescript
// src/resources/static/index.ts
import { MY_RESOURCE_URI, handleMyResourceRequest } from './my-resource.js';
export const staticResources = {
// ... existing static resources
[MY_RESOURCE_URI]: handleMyResourceRequest
};
```
#### For Dynamic Resources
```typescript
// src/resources/dynamic/index.ts
import { MY_RESOURCE_URI_TEMPLATE, matchMyResourceUri, handleMyResourceRequest } from './my-resource.js';
export const dynamicResourceMatchers = [
// ... existing dynamic resource matchers
{
uriTemplate: MY_RESOURCE_URI_TEMPLATE,
match: matchMyResourceUri,
handler: handleMyResourceRequest
}
];
```
### 4. Add Resource Listings
Update the resource listing functions:
```typescript
// src/resources/index.ts
// Update the resource templates listing
export function getResourceTemplates() {
return [
// ... existing templates
{
uriTemplate: MY_RESOURCE_URI_TEMPLATE,
name: 'My Resource',
description: 'Description of my resource'
}
];
}
// Update the static resources listing
export function getStaticResources() {
return [
// ... existing resources
{
uri: MY_RESOURCE_URI,
name: 'My Resource List',
description: 'List of all my resources'
}
];
}
```
### 5. Add Unit Tests
Create tests for your new resource:
```typescript
// tests/unit/resources/static/my-resource.test.ts
// or
// tests/unit/resources/dynamic/my-resource.test.ts
import { describe, it, expect, jest } from '@jest/globals';
import {
MY_RESOURCE_URI,
handleMyResourceRequest
} from '../../../../src/resources/static/my-resource.js';
describe('My Resource', () => {
it('should return resource data', async () => {
const mockClient = {
// Mock the necessary client methods
};
const response = await handleMyResourceRequest(mockClient as any);
expect(response.contents).toHaveLength(1);
expect(response.contents[0].uri).toBe(MY_RESOURCE_URI);
expect(response.contents[0].mimeType).toBe('application/json');
const data = JSON.parse(response.contents[0].text);
expect(data).toHaveProperty('property1');
expect(data).toHaveProperty('property2');
});
it('should handle errors properly', async () => {
const mockClient = {
// Mock client that throws an error
someMethod: jest.fn().mockRejectedValue(new Error('Test error'))
};
await expect(handleMyResourceRequest(mockClient as any))
.rejects
.toThrow('Failed to retrieve resource');
});
});
```
## Extending the API Client
If you need to add support for new n8n API features, extend the N8nClient class:
### 1. Add New Methods to the Client
```typescript
// src/api/n8n-client.ts
export class N8nClient {
// ... existing methods
// Add new methods
async myNewApiMethod(param1: string): Promise<any> {
try {
const response = await this.httpClient.get(`/endpoint/${param1}`);
return response.data;
} catch (error) {
this.handleApiError(error);
}
}
}
```
### 2. Add Type Definitions
```typescript
// src/types/api.ts
// Add types for API responses and requests
export interface MyApiResponse {
id: string;
name: string;
// Other properties
}
export interface MyApiRequest {
param1: string;
param2?: number;
}
```
### 3. Add Tests for the New API Methods
```typescript
// tests/unit/api/n8n-client.test.ts
describe('N8nClient', () => {
// ... existing tests
describe('myNewApiMethod', () => {
it('should call the correct API endpoint', async () => {
// Set up mock Axios
axiosMock.onGet('/endpoint/test').reply(200, {
id: '123',
name: 'Test'
});
const client = new N8nClient({
apiUrl: 'http://localhost:5678/api/v1',
apiKey: 'test-api-key'
});
const result = await client.myNewApiMethod('test');
expect(result).toEqual({
id: '123',
name: 'Test'
});
});
it('should handle errors correctly', async () => {
// Set up mock Axios
axiosMock.onGet('/endpoint/test').reply(404, {
message: 'Not found'
});
const client = new N8nClient({
apiUrl: 'http://localhost:5678/api/v1',
apiKey: 'test-api-key'
});
await expect(client.myNewApiMethod('test'))
.rejects
.toThrow('Resource not found');
});
});
});
```
## Best Practices for Extensions
1. **Follow the Existing Patterns**: Try to follow the patterns already established in the codebase.
2. **Type Safety**: Use TypeScript types and interfaces to ensure type safety.
3. **Error Handling**: Implement comprehensive error handling in all extensions.
4. **Testing**: Write thorough tests for all new functionality.
5. **Documentation**: Document your extensions, including JSDoc comments for all public methods.
6. **Backward Compatibility**: Ensure that your extensions don't break existing functionality.
## Example: Adding Support for n8n Tags
Here's a complete example of adding support for n8n tags:
### API Client Extension
```typescript
// src/api/n8n-client.ts
export class N8nClient {
// ... existing methods
// Add tag methods
async getTags(): Promise<Tag[]> {
try {
const response = await this.httpClient.get('/tags');
return response.data;
} catch (error) {
this.handleApiError(error);
}
}
async createTag(data: CreateTagRequest): Promise<Tag> {
try {
const response = await this.httpClient.post('/tags', data);
return response.data;
} catch (error) {
this.handleApiError(error);
}
}
async deleteTag(id: string): Promise<void> {
try {
await this.httpClient.delete(`/tags/${id}`);
} catch (error) {
this.handleApiError(error);
}
}
}
```
### Type Definitions
```typescript
// src/types/api.ts
export interface Tag {
id: string;
name: string;
createdAt: string;
updatedAt: string;
}
export interface CreateTagRequest {
name: string;
}
```
### Tool Implementations
```typescript
// src/tools/tag/list.ts
export function getTagListToolDefinition(): ToolDefinition {
return {
name: 'tag_list',
description: 'List all tags in n8n',
inputSchema: {
type: 'object',
properties: {},
required: []
}
};
}
export async function handleTagList(
client: N8nClient,
params: any
): Promise<ToolCallResponse> {
try {
const tags = await client.getTags();
return {
content: [
{
type: 'text',
text: JSON.stringify(tags, null, 2)
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error listing tags: ${error.message}`
}
],
isError: true
};
}
}
```
### Resource Implementation
```typescript
// src/resources/static/tags.ts
export const TAGS_URI = 'n8n://tags';
export async function handleTagsRequest(
client: N8nClient
): Promise<ReadResourceResponse> {
try {
const tags = await client.getTags();
return {
contents: [
{
uri: TAGS_URI,
mimeType: 'application/json',
text: JSON.stringify(
{
tags,
count: tags.length
},
null,
2
)
}
]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to retrieve tags: ${error.message}`
);
}
}
```
### Integration
Register the new tools and resources in the appropriate handler files, and update the main server initialization to include them.
By following these patterns, you can extend the n8n MCP Server to support any n8n feature or add custom functionality tailored to your specific needs.
```
--------------------------------------------------------------------------------
/docs/examples/advanced-scenarios.md:
--------------------------------------------------------------------------------
```markdown
# Advanced Scenarios
This page demonstrates more complex examples of using the n8n MCP Server with AI assistants.
## Workflow Analysis and Optimization
### User Prompt
"Analyze my workflows and suggest optimizations."
### Assistant Actions
```javascript
// Get all workflows
const workflows = await useMcpTool('n8n-mcp-server', 'workflow_list', {});
// Get execution statistics
const stats = await accessMcpResource('n8n-mcp-server', 'n8n://execution-stats');
// Analyze each workflow in detail
const analysisResults = [];
for (const workflow of workflows) {
// Get detailed workflow information
const details = await useMcpTool('n8n-mcp-server', 'workflow_get', {
id: workflow.id
});
// Get recent executions
const executions = await useMcpTool('n8n-mcp-server', 'execution_list', {
workflowId: workflow.id,
limit: 10
});
// Analyze workflow structure
const nodeCount = details.nodes.length;
const nodeTypes = new Set(details.nodes.map(node => node.type));
// Check for error patterns in executions
const errorRate = executions.length > 0
? executions.filter(e => e.status === "error").length / executions.length
: 0;
// Check execution times
const executionTimes = executions
.filter(e => e.status === "success" && e.finishedAt && e.startedAt)
.map(e => new Date(e.finishedAt) - new Date(e.startedAt));
const avgExecutionTime = executionTimes.length > 0
? executionTimes.reduce((sum, time) => sum + time, 0) / executionTimes.length / 1000
: 0;
// Generate optimization suggestions
const suggestions = [];
if (nodeCount > 15) {
suggestions.push("Consider splitting into multiple workflows for better maintainability");
}
if (errorRate > 0.2) {
suggestions.push("High error rate detected - review error handling and input validation");
}
if (avgExecutionTime > 30) {
suggestions.push("Long execution time - check for bottlenecks or inefficient operations");
}
if (nodeTypes.has("n8n-nodes-base.httpRequest") && !nodeTypes.has("n8n-nodes-base.errorTrigger")) {
suggestions.push("HTTP requests without error handling - consider adding error triggers");
}
analysisResults.push({
id: workflow.id,
name: workflow.name,
nodeCount,
errorRate,
avgExecutionTime,
suggestions
});
}
// Format and present the analysis
let response = "# Workflow Analysis and Optimization Suggestions\n\n";
analysisResults.forEach(result => {
response += `## ${result.name}\n\n`;
response += `**Nodes:** ${result.nodeCount}\n`;
response += `**Error Rate:** ${(result.errorRate * 100).toFixed(1)}%\n`;
response += `**Avg. Execution Time:** ${result.avgExecutionTime.toFixed(1)} seconds\n\n`;
if (result.suggestions.length > 0) {
response += "**Optimization Suggestions:**\n";
result.suggestions.forEach(suggestion => {
response += `- ${suggestion}\n`;
});
} else {
response += "No specific optimization suggestions for this workflow.\n";
}
response += "\n";
});
return response;
```
## Scheduled Workflow Management
### User Prompt
"Update all my scheduled workflows to run one hour later."
### Assistant Actions
```javascript
// Get all workflows
const workflows = await useMcpTool('n8n-mcp-server', 'workflow_list', {});
// Filter for scheduled workflows (those with a Schedule node)
const scheduledWorkflows = [];
for (const workflow of workflows) {
// Get detailed workflow information
const details = await useMcpTool('n8n-mcp-server', 'workflow_get', {
id: workflow.id
});
// Look for Schedule Trigger nodes
const scheduleNodes = details.nodes.filter(node =>
node.type === "n8n-nodes-base.scheduleTrigger" ||
node.type === "n8n-nodes-base.cron"
);
if (scheduleNodes.length > 0) {
scheduledWorkflows.push({
workflow: details,
scheduleNodes
});
}
}
if (scheduledWorkflows.length === 0) {
return "I couldn't find any workflows with scheduled triggers.";
}
// Process each scheduled workflow
const results = [];
for (const { workflow, scheduleNodes } of scheduledWorkflows) {
// Create a copy of the workflow for modification
const updatedWorkflow = { ...workflow };
// Update each schedule node
for (const scheduleNode of scheduleNodes) {
const nodeIndex = updatedWorkflow.nodes.findIndex(n => n.id === scheduleNode.id);
if (nodeIndex === -1) continue;
// Copy the node for modification
const updatedNode = { ...updatedWorkflow.nodes[nodeIndex] };
// Handle different types of schedule configurations
if (updatedNode.type === "n8n-nodes-base.scheduleTrigger") {
if (updatedNode.parameters.cronExpression) {
// Modify cron expression to run 1 hour later
const cronParts = updatedNode.parameters.cronExpression.split(' ');
if (cronParts.length === 5) {
// Standard cron format: minute hour day month dayOfWeek
const hour = parseInt(cronParts[1], 10);
cronParts[1] = ((hour + 1) % 24).toString();
updatedNode.parameters.cronExpression = cronParts.join(' ');
}
} else if (updatedNode.parameters.timeToRepeat) {
// Handle specific time scheduling
const time = updatedNode.parameters.timeToRepeat;
if (time && time.split(':').length === 2) {
const [hours, minutes] = time.split(':').map(part => parseInt(part, 10));
const newHours = (hours + 1) % 24;
updatedNode.parameters.timeToRepeat = `${newHours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
}
}
} else if (updatedNode.type === "n8n-nodes-base.cron") {
// Similar handling for cron node
if (updatedNode.parameters.cronExpression) {
const cronParts = updatedNode.parameters.cronExpression.split(' ');
if (cronParts.length === 5) {
const hour = parseInt(cronParts[1], 10);
cronParts[1] = ((hour + 1) % 24).toString();
updatedNode.parameters.cronExpression = cronParts.join(' ');
}
}
}
// Update the node in the workflow
updatedWorkflow.nodes[nodeIndex] = updatedNode;
}
// Save the updated workflow
const result = await useMcpTool('n8n-mcp-server', 'workflow_update', {
id: workflow.id,
nodes: updatedWorkflow.nodes,
connections: updatedWorkflow.connections
});
results.push({
name: workflow.name,
updated: result.updatedAt
});
}
// Format and present the results
let response = "# Updated Scheduled Workflows\n\n";
response += "I've updated the following workflows to run one hour later:\n\n";
results.forEach(result => {
response += `- **${result.name}** (Updated: ${new Date(result.updated).toLocaleString()})\n`;
});
return response;
```
## Error Handling and Notification System
### User Prompt
"Set up a notification system for any workflow that fails."
### Assistant Actions
```javascript
// This scenario demonstrates creating a monitoring workflow that checks for failures
// and sends notifications
// First, check if a monitoring workflow already exists
const workflows = await useMcpTool('n8n-mcp-server', 'workflow_list', {});
const existingMonitor = workflows.find(workflow =>
workflow.name.toLowerCase().includes('workflow monitor') ||
workflow.name.toLowerCase().includes('error notification')
);
if (existingMonitor) {
return `You already have a monitoring workflow: "${existingMonitor.name}" (ID: ${existingMonitor.id}). Would you like me to update it instead?`;
}
// Create a new monitoring workflow
const monitorWorkflow = await useMcpTool('n8n-mcp-server', 'workflow_create', {
name: "Workflow Error Notification System",
active: false, // Start inactive until configured
nodes: [
{
name: "Schedule Trigger",
type: "n8n-nodes-base.scheduleTrigger",
position: [100, 300],
parameters: {
cronExpression: "*/15 * * * *" // Run every 15 minutes
}
},
{
name: "Get Failed Executions",
type: "n8n-nodes-base.n8n",
position: [300, 300],
parameters: {
resource: "execution",
operation: "getAll",
filters: {
status: "error",
// Look for executions in the last 15 minutes
finished: {
$gt: "={{$now.minus({ minutes: 15 }).toISOString()}}"
}
}
}
},
{
name: "Filter Empty",
type: "n8n-nodes-base.filter",
position: [500, 300],
parameters: {
conditions: {
boolean: [
{
value1: "={{ $json.length > 0 }}",
operation: "equal",
value2: true
}
]
}
}
},
{
name: "Format Notification",
type: "n8n-nodes-base.function",
position: [700, 300],
parameters: {
functionCode: `
// Function to format error notifications
const executions = items;
const now = new Date();
// Group by workflow
const workflowErrors = {};
for (const execution of executions) {
const workflowId = execution.workflowId;
const workflowName = execution.workflowData.name;
if (!workflowErrors[workflowId]) {
workflowErrors[workflowId] = {
name: workflowName,
errors: []
};
}
workflowErrors[workflowId].errors.push({
id: execution.id,
time: execution.finished,
error: execution.error?.message || "Unknown error"
});
}
// Create notification text
let notificationText = "⚠️ Workflow Error Alert ⚠️\\n\\n";
notificationText += "The following workflows have failed:\\n\\n";
for (const [workflowId, data] of Object.entries(workflowErrors)) {
notificationText += \`👉 \${data.name} (ID: \${workflowId})\\n\`;
notificationText += \` Failed executions: \${data.errors.length}\\n\`;
// Add details about each failure
data.errors.forEach(error => {
const time = new Date(error.time).toLocaleString();
notificationText += \` - \${time}: \${error.error}\\n\`;
});
notificationText += "\\n";
}
notificationText += "Check your n8n dashboard for more details.";
return [{
json: {
text: notificationText,
subject: \`n8n Alert: \${Object.keys(workflowErrors).length} Workflow(s) Failed\`,
timestamp: now.toISOString()
}
}];
`
}
},
{
name: "Send Email",
type: "n8n-nodes-base.emailSend",
position: [900, 300],
parameters: {
to: "{{$env.EMAIL_RECIPIENT}}", // Will need to be configured
subject: "{{$json.subject}}",
text: "{{$json.text}}"
}
}
],
connections: {
"Schedule Trigger": {
main: [
[
{
node: "Get Failed Executions",
type: "main",
index: 0
}
]
]
},
"Get Failed Executions": {
main: [
[
{
node: "Filter Empty",
type: "main",
index: 0
}
]
]
},
"Filter Empty": {
main: [
[
{
node: "Format Notification",
type: "main",
index: 0
}
]
]
},
"Format Notification": {
main: [
[
{
node: "Send Email",
type: "main",
index: 0
}
]
]
}
}
});
return `
# Workflow Error Notification System Created
I've created a new workflow that will monitor for failed executions and send email notifications.
**Workflow ID:** ${monitorWorkflow.id}
## Configuration Needed:
1. The workflow is currently **inactive** - you'll need to activate it once configured
2. Set up the email configuration in n8n settings
3. Configure the "Send Email" node with your recipient email address
## How It Works:
- Runs every 15 minutes
- Checks for any workflows that failed in the last 15 minutes
- Groups failures by workflow and formats a notification
- Sends an email with details about the failures
You can customize the notification format, schedule, and notification method by editing the workflow in the n8n interface.
`;
```
## Data Migration Between Workflows
### User Prompt
"Help me move data from my 'Legacy CRM' workflow to my 'New CRM' workflow."
### Assistant Actions
```javascript
// Get all workflows
const workflows = await useMcpTool('n8n-mcp-server', 'workflow_list', {});
// Find the source and target workflows
const legacyWorkflow = workflows.find(w => w.name.toLowerCase().includes('legacy crm'));
const newWorkflow = workflows.find(w => w.name.toLowerCase().includes('new crm'));
if (!legacyWorkflow) {
return "I couldn't find a workflow with 'Legacy CRM' in the name. Please check the exact name of your source workflow.";
}
if (!newWorkflow) {
return "I couldn't find a workflow with 'New CRM' in the name. Please check the exact name of your target workflow.";
}
// Create a data migration workflow
const migrationWorkflow = await useMcpTool('n8n-mcp-server', 'workflow_create', {
name: "Data Migration: Legacy CRM to New CRM",
active: false, // Start inactive until confirmed
nodes: [
{
name: "Manual Trigger",
type: "n8n-nodes-base.manualTrigger",
position: [100, 300],
parameters: {}
},
{
name: "Execute Legacy Workflow",
type: "n8n-nodes-base.executeWorkflow",
position: [300, 300],
parameters: {
workflowId: legacyWorkflow.id,
options: {
includeData: true
}
}
},
{
name: "Transform Data",
type: "n8n-nodes-base.function",
position: [500, 300],
parameters: {
functionCode: `
// This is a placeholder transformation function that you'll need to customize
// based on the actual data structure of your workflows
const legacyData = items;
const transformedItems = [];
// Example transformation (modify based on your data structures)
for (const item of legacyData) {
transformedItems.push({
json: {
// Map legacy fields to new fields
customer_id: item.json.id,
customer_name: item.json.fullName || \`\${item.json.firstName || ''} \${item.json.lastName || ''}\`.trim(),
email: item.json.emailAddress || item.json.email,
phone: item.json.phoneNumber || item.json.phone,
notes: item.json.comments || item.json.notes || '',
// Add migration metadata
migrated_from_legacy: true,
migration_date: new Date().toISOString()
}
});
}
return transformedItems;
`
}
},
{
name: "Execute New Workflow",
type: "n8n-nodes-base.executeWorkflow",
position: [700, 300],
parameters: {
workflowId: newWorkflow.id,
options: {
includeData: true
}
}
},
{
name: "Migration Summary",
type: "n8n-nodes-base.function",
position: [900, 300],
parameters: {
functionCode: `
// Create a summary of the migration
const results = items;
const totalItems = items.length;
const successItems = items.filter(item => !item.json.error).length;
const errorItems = totalItems - successItems;
return [
{
json: {
summary: "Migration Complete",
total_records: totalItems,
successful_records: successItems,
failed_records: errorItems,
completion_time: new Date().toISOString()
}
}
];
`
}
}
],
connections: {
"Manual Trigger": {
main: [
[
{
node: "Execute Legacy Workflow",
type: "main",
index: 0
}
]
]
},
"Execute Legacy Workflow": {
main: [
[
{
node: "Transform Data",
type: "main",
index: 0
}
]
]
},
"Transform Data": {
main: [
[
{
node: "Execute New Workflow",
type: "main",
index: 0
}
]
]
},
"Execute New Workflow": {
main: [
[
{
node: "Migration Summary",
type: "main",
index: 0
}
]
]
}
}
});
return `
# Data Migration Workflow Created
I've created a new workflow to migrate data from "${legacyWorkflow.name}" to "${newWorkflow.name}".
**Migration Workflow ID:** ${migrationWorkflow.id}
## Important Notes:
1. The workflow is currently **inactive** - activate it only when you're ready to perform the migration
2. The data transformation is a placeholder - you'll need to edit the "Transform Data" function node to map fields correctly based on your specific data structures
3. This is a one-time migration workflow - run it manually when you're ready to migrate the data
## Next Steps:
1. Open the workflow in the n8n interface
2. Edit the "Transform Data" function to correctly map your data fields
3. Test the workflow with a small sample if possible
4. Activate and run the workflow to perform the migration
5. Check the migration summary for results
Would you like me to help you customize the data transformation based on the specific fields in your CRM workflows?
`;
```
These examples demonstrate more advanced usage patterns for the n8n MCP Server. For integration with other systems, see the [Integration Examples](./integration-examples.md) page.
```
--------------------------------------------------------------------------------
/docs/examples/integration-examples.md:
--------------------------------------------------------------------------------
```markdown
# Integration Examples
This page provides examples of integrating the n8n MCP Server with other systems and AI assistant platforms.
## AI Assistant Integration Examples
### Claude AI Assistant Integration
#### Example: Setting Up n8n MCP Server with Claude
```javascript
// Register the n8n MCP Server with Claude using the MCP Installer
const installationResult = await useMcpTool('mcp-installer', 'install_repo_mcp_server', {
name: 'n8n-mcp-server',
env: [
"N8N_API_URL=http://localhost:5678/api/v1",
"N8N_API_KEY=your_n8n_api_key_here",
"DEBUG=false"
]
});
// Once registered, Claude can interact with n8n
// Here's an example conversation:
// User: "Show me my active workflows in n8n"
// Claude: (uses workflow_list tool to retrieve and display active workflows)
// User: "Execute my 'Daily Report' workflow"
// Claude: (uses execution_run tool to start the workflow and provide status)
```
#### Example: Using n8n to Extend Claude's Capabilities
```javascript
// Using n8n as a bridge to external systems
// This example shows how Claude can access a database through n8n
// User: "Show me the top 5 customers from my database"
// Claude would:
// 1. Find the appropriate workflow
const workflows = await useMcpTool('n8n-mcp-server', 'workflow_list', {});
const dbQueryWorkflow = workflows.find(w =>
w.name.toLowerCase().includes('database query') ||
w.name.toLowerCase().includes('db query')
);
if (!dbQueryWorkflow) {
return "I couldn't find a database query workflow. Would you like me to help you create one?";
}
// 2. Execute the workflow with the appropriate parameters
const execution = await useMcpTool('n8n-mcp-server', 'execution_run', {
workflowId: dbQueryWorkflow.id,
data: {
query: "SELECT * FROM customers ORDER BY total_purchases DESC LIMIT 5",
format: "table"
},
waitForCompletion: true
});
// 3. Present the results to the user
if (execution.status !== "success") {
return "There was an error querying the database. Error: " + (execution.error || "Unknown error");
}
// Format the results as a table
const customers = execution.data.resultData.runData.lastNode[0].data.json;
let response = "# Top 5 Customers\n\n";
response += "| Customer Name | Email | Total Purchases |\n";
response += "|--------------|-------|----------------|\n";
customers.forEach(customer => {
response += `| ${customer.name} | ${customer.email} | $${customer.total_purchases.toFixed(2)} |\n`;
});
return response;
```
### OpenAI Assistant Integration
#### Example: Connecting n8n MCP Server to OpenAI Assistant
```javascript
// This is a conceptual example of how an OpenAI Assistant might interact with the n8n MCP Server
// In an OpenAI Assistant Function definition:
{
"name": "n8n_workflow_list",
"description": "List all workflows in n8n",
"parameters": {
"type": "object",
"properties": {
"active": {
"type": "boolean",
"description": "Filter by active status (optional)"
}
}
}
}
// The function would call the n8n MCP Server:
async function n8n_workflow_list(params) {
// Call the n8n MCP Server API
const response = await fetch('http://localhost:3000/n8n-mcp/tools/workflow_list', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_AUTH_TOKEN'
},
body: JSON.stringify(params)
});
return await response.json();
}
// The OpenAI Assistant would then use this function when asked about n8n workflows
```
## Integration with External Systems
### n8n to Git Integration
#### Example: Using n8n MCP Server to Manage Workflow Versioning in Git
```javascript
// User request: "Backup all my workflows to my Git repository"
// The assistant first creates a workflow for Git backup
const backupWorkflow = await useMcpTool('n8n-mcp-server', 'workflow_create', {
name: "Workflow Git Backup",
active: false,
nodes: [
{
name: "Manual Trigger",
type: "n8n-nodes-base.manualTrigger",
position: [100, 300],
parameters: {}
},
{
name: "Get All Workflows",
type: "n8n-nodes-base.n8n",
position: [300, 300],
parameters: {
resource: "workflow",
operation: "getAll"
}
},
{
name: "Format For Git",
type: "n8n-nodes-base.function",
position: [500, 300],
parameters: {
functionCode: `
// Convert workflows to individual JSON files
const workflows = items;
const outputItems = [];
for (const workflow of workflows) {
// Create a sanitized filename
const filename = workflow.json.name
.replace(/[^a-zA-Z0-9]/g, '_')
.toLowerCase() + '.json';
outputItems.push({
json: {
filename: filename,
content: JSON.stringify(workflow.json, null, 2),
commit_message: \`Backup workflow: \${workflow.json.name}\`
}
});
}
return outputItems;
`
}
},
{
name: "Git Commit",
type: "n8n-nodes-base.git",
position: [700, 300],
parameters: {
repository: "{{$env.GIT_REPO_PATH}}",
directory: "workflows",
operation: "commit",
message: "Automated workflow backup {{$now.format('YYYY-MM-DD HH:mm')}}",
filePath: "={{$json.filename}}",
fileContent: "={{$json.content}}"
}
},
{
name: "Git Push",
type: "n8n-nodes-base.git",
position: [900, 300],
parameters: {
repository: "{{$env.GIT_REPO_PATH}}",
operation: "push"
}
}
],
connections: {
"Manual Trigger": {
main: [
[
{
node: "Get All Workflows",
type: "main",
index: 0
}
]
]
},
"Get All Workflows": {
main: [
[
{
node: "Format For Git",
type: "main",
index: 0
}
]
]
},
"Format For Git": {
main: [
[
{
node: "Git Commit",
type: "main",
index: 0
}
]
]
},
"Git Commit": {
main: [
[
{
node: "Git Push",
type: "main",
index: 0
}
]
]
}
}
});
return `
# Git Backup Workflow Created
I've created a new workflow that will backup all your n8n workflows to Git.
**Workflow ID:** ${backupWorkflow.id}
## Configuration Needed:
1. The workflow is currently **inactive** - activate it once configured
2. Set the environment variable \`GIT_REPO_PATH\` to point to your Git repository
3. Ensure the Git repository is properly configured with credentials
## How It Works:
- When executed, this workflow retrieves all n8n workflows
- Converts each workflow to a JSON file
- Commits each file to the specified Git repository
- Pushes the changes to the remote repository
## Usage:
You can run this workflow manually whenever you want to backup your workflows,
or add a Schedule Trigger to automate regular backups.
`;
```
### CRM Integration
#### Example: Setting Up a Two-Way Sync Between n8n and a CRM
```javascript
// User request: "Help me set up a sync between n8n and my HubSpot CRM"
// First check for existing credentials
const workflows = await useMcpTool('n8n-mcp-server', 'workflow_list', {});
const hubspotWorkflows = workflows.filter(w =>
w.name.toLowerCase().includes('hubspot')
);
let credentialsMessage = "";
if (hubspotWorkflows.length > 0) {
credentialsMessage = "I noticed you already have HubSpot workflows. I'll assume you have HubSpot credentials set up.";
} else {
credentialsMessage = "You'll need to set up HubSpot credentials in n8n before activating this workflow.";
}
// Create the sync workflow
const syncWorkflow = await useMcpTool('n8n-mcp-server', 'workflow_create', {
name: "HubSpot Two-Way Sync",
active: false,
nodes: [
{
name: "Schedule Trigger",
type: "n8n-nodes-base.scheduleTrigger",
position: [100, 300],
parameters: {
cronExpression: "0 */2 * * *" // Every 2 hours
}
},
{
name: "Get HubSpot Contacts",
type: "n8n-nodes-base.hubspot",
position: [300, 200],
parameters: {
resource: "contact",
operation: "getAll",
returnAll: true,
additionalFields: {
formattedDate: true
}
}
},
{
name: "Get n8n Contacts",
type: "n8n-nodes-base.function",
position: [300, 400],
parameters: {
functionCode: `
// This function would retrieve contacts from your internal database
// For example purposes, we're using a placeholder
// In reality, you might use another n8n node to fetch from your database
return [
{
json: {
internalContacts: [
// Example internal contacts
// In a real scenario, these would come from your database
]
}
}
];
`
}
},
{
name: "Compare & Identify Changes",
type: "n8n-nodes-base.function",
position: [500, 300],
parameters: {
functionCode: `
const hubspotContacts = $input.first();
const internalContacts = $input.last().json.internalContacts;
// Identify new/updated contacts in HubSpot
const hubspotUpdates = hubspotContacts.json.map(contact => {
// Find matching internal contact by email
const internalMatch = internalContacts.find(
ic => ic.email === contact.properties.email
);
if (!internalMatch) {
return {
json: {
type: 'new_in_hubspot',
contact: contact,
action: 'create_in_internal'
}
};
}
// Check if HubSpot contact is newer
const hubspotUpdated = new Date(contact.properties.lastmodifieddate);
const internalUpdated = new Date(internalMatch.updated_at);
if (hubspotUpdated > internalUpdated) {
return {
json: {
type: 'updated_in_hubspot',
contact: contact,
action: 'update_in_internal'
}
};
}
return null;
}).filter(item => item !== null);
// Identify new/updated contacts in internal system
const internalUpdates = internalContacts.map(contact => {
// Find matching HubSpot contact by email
const hubspotMatch = hubspotContacts.json.find(
hc => hc.properties.email === contact.email
);
if (!hubspotMatch) {
return {
json: {
type: 'new_in_internal',
contact: contact,
action: 'create_in_hubspot'
}
};
}
// Check if internal contact is newer
const internalUpdated = new Date(contact.updated_at);
const hubspotUpdated = new Date(hubspotMatch.properties.lastmodifieddate);
if (internalUpdated > hubspotUpdated) {
return {
json: {
type: 'updated_in_internal',
contact: contact,
action: 'update_in_hubspot'
}
};
}
return null;
}).filter(item => item !== null);
// Combine all changes
return [...hubspotUpdates, ...internalUpdates];
`
}
},
{
name: "Route Updates",
type: "n8n-nodes-base.switch",
position: [700, 300],
parameters: {
rules: {
conditions: [
{
value1: "={{$json.action}}",
operation: "equal",
value2: "update_in_hubspot"
},
{
value1: "={{$json.action}}",
operation: "equal",
value2: "create_in_hubspot"
},
{
value1: "={{$json.action}}",
operation: "equal",
value2: "update_in_internal"
},
{
value1: "={{$json.action}}",
operation: "equal",
value2: "create_in_internal"
}
]
}
}
},
{
name: "Update HubSpot Contact",
type: "n8n-nodes-base.hubspot",
position: [900, 100],
parameters: {
resource: "contact",
operation: "update",
contactId: "={{$json.contact.hubspot_id || $json.hubspotId}}",
additionalFields: {
properties: {
firstname: "={{$json.contact.first_name}}",
lastname: "={{$json.contact.last_name}}",
email: "={{$json.contact.email}}",
phone: "={{$json.contact.phone}}",
// Add additional fields as needed
}
}
}
},
{
name: "Create HubSpot Contact",
type: "n8n-nodes-base.hubspot",
position: [900, 250],
parameters: {
resource: "contact",
operation: "create",
additionalFields: {
properties: {
firstname: "={{$json.contact.first_name}}",
lastname: "={{$json.contact.last_name}}",
email: "={{$json.contact.email}}",
phone: "={{$json.contact.phone}}",
// Add additional fields as needed
}
}
}
},
{
name: "Update Internal Contact",
type: "n8n-nodes-base.function",
position: [900, 400],
parameters: {
functionCode: `
// This function would update contacts in your internal database
// In reality, you might use another n8n node like a database connector
const contact = $input.first().json.contact;
// Process the HubSpot contact into internal format
const internalFormat = {
first_name: contact.properties.firstname,
last_name: contact.properties.lastname,
email: contact.properties.email,
phone: contact.properties.phone,
hubspot_id: contact.id,
updated_at: new Date().toISOString()
};
// In a real implementation, you would update your database
// For this example, we just return what would be updated
return [{
json: {
action: "updated_internal_contact",
contact: internalFormat
}
}];
`
}
},
{
name: "Create Internal Contact",
type: "n8n-nodes-base.function",
position: [900, 550],
parameters: {
functionCode: `
// This function would create new contacts in your internal database
// In reality, you might use another n8n node like a database connector
const contact = $input.first().json.contact;
// Process the HubSpot contact into internal format
const internalFormat = {
first_name: contact.properties.firstname,
last_name: contact.properties.lastname,
email: contact.properties.email,
phone: contact.properties.phone,
hubspot_id: contact.id,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
// In a real implementation, you would insert into your database
// For this example, we just return what would be created
return [{
json: {
action: "created_internal_contact",
contact: internalFormat
}
}];
`
}
},
{
name: "Sync Report",
type: "n8n-nodes-base.function",
position: [1100, 300],
parameters: {
functionCode: `
// Collect all results from previous nodes
const updateHubspot = $input.itemsFrom('Update HubSpot Contact');
const createHubspot = $input.itemsFrom('Create HubSpot Contact');
const updateInternal = $input.itemsFrom('Update Internal Contact');
const createInternal = $input.itemsFrom('Create Internal Contact');
// Generate sync summary
return [{
json: {
summary: "Sync Complete",
timestamp: new Date().toISOString(),
stats: {
hubspot_updated: updateHubspot.length,
hubspot_created: createHubspot.length,
internal_updated: updateInternal.length,
internal_created: createInternal.length,
total_changes: updateHubspot.length + createHubspot.length +
updateInternal.length + createInternal.length
}
}
}];
`
}
}
],
connections: {
"Schedule Trigger": {
main: [
[
{
node: "Get HubSpot Contacts",
type: "main",
index: 0
},
{
node: "Get n8n Contacts",
type: "main",
index: 0
}
]
]
},
"Get HubSpot Contacts": {
main: [
[
{
node: "Compare & Identify Changes",
type: "main",
index: 0
}
]
]
},
"Get n8n Contacts": {
main: [
[
{
node: "Compare & Identify Changes",
type: "main",
index: 1
}
]
]
},
"Compare & Identify Changes": {
main: [
[
{
node: "Route Updates",
type: "main",
index: 0
}
]
]
},
"Route Updates": {
main: [
[
{
node: "Update HubSpot Contact",
type: "main",
index: 0
}
],
[
{
node: "Create HubSpot Contact",
type: "main",
index: 0
}
],
[
{
node: "Update Internal Contact",
type: "main",
index: 0
}
],
[
{
node: "Create Internal Contact",
type: "main",
index: 0
}
]
]
},
"Update HubSpot Contact": {
main: [
[
{
node: "Sync Report",
type: "main",
index: 0
}
]
]
},
"Create HubSpot Contact": {
main: [
[
{
node: "Sync Report",
type: "main",
index: 0
}
]
]
},
"Update Internal Contact": {
main: [
[
{
node: "Sync Report",
type: "main",
index: 0
}
]
]
},
"Create Internal Contact": {
main: [
[
{
node: "Sync Report",
type: "main",
index: 0
}
]
]
}
}
});
return `
# HubSpot Two-Way Sync Workflow Created
I've created a new workflow that will synchronize contacts between HubSpot and your internal system.
**Workflow ID:** ${syncWorkflow.id}
## Configuration Needed:
1. The workflow is currently **inactive** - activate it once configured
2. ${credentialsMessage}
3. You'll need to customize the "Get n8n Contacts", "Update Internal Contact", and "Create Internal Contact" nodes to work with your specific database or storage system
## How It Works:
- Runs every 2 hours (configurable)
- Retrieves contacts from both HubSpot and your internal system
- Compares the data to identify new or updated contacts in either system
- Creates or updates contacts to keep both systems in sync
- Generates a sync report with statistics
## Customization:
You may need to modify the field mappings in the function nodes to match your specific data structure
and add any additional fields that need to be synchronized between the systems.
`;
```
### Slack Integration
#### Example: Creating a Workflow to Send n8n Notifications to Slack
```javascript
// User request: "Create a workflow that sends n8n execution notifications to Slack"
// Create the notification workflow
const notificationWorkflow = await useMcpTool('n8n-mcp-server', 'workflow_create', {
name: "n8n Execution Notifications to Slack",
active: false,
nodes: [
{
name: "Webhook",
type: "n8n-nodes-base.webhook",
position: [100, 300],
parameters: {
path: "n8n-notification",
responseMode: "onReceived",
options: {
responseData: "noData"
}
}
},
{
name: "Format Slack Message",
type: "n8n-nodes-base.function",
position: [300, 300],
parameters: {
functionCode: `
// Parse webhook data from n8n
const data = $input.first().json;
const workflow = data.workflow;
const execution = data.execution;
// Determine status color
let color = "#36a64f"; // Green for success
let icon = "✅";
if (execution.status === "error") {
color = "#ff0000"; // Red for error
icon = "❌";
} else if (execution.status === "warning") {
color = "#ffcc00"; // Yellow for warning
icon = "⚠️";
}
// Create Slack message blocks
const blocks = [
{
"type": "header",
"text": {
"type": "plain_text",
"text": \`\${icon} Workflow \${execution.status === "success" ? "Succeeded" : "Failed"}: \${workflow.name}\`,
"emoji": true
}
},
{
"type": "divider"
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": \`*Status:*\\n\${execution.status}\`
},
{
"type": "mrkdwn",
"text": \`*Execution Time:*\\n\${new Date(execution.startedAt).toLocaleString()}\`
},
{
"type": "mrkdwn",
"text": \`*Duration:*\\n\${Math.round((new Date(execution.finishedAt) - new Date(execution.startedAt)) / 1000)} seconds\`
},
{
"type": "mrkdwn",
"text": \`*Mode:*\\n\${execution.mode}\`
}
]
}
];
// Add error details if present
if (execution.status === "error" && execution.error) {
blocks.push(
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Error Details:*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": \`\\\`\\\`\${execution.error.message || "Unknown error"}\\\`\\\`\`
}
}
);
}
// Add link to n8n execution
blocks.push(
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": \`<\${data.n8nUrl}/workflow/\${workflow.id}|View Workflow> | <\${data.n8nUrl}/execution/\${execution.id}|View Execution>\`
}
}
);
return [{
json: {
blocks: blocks,
text: \`\${icon} Workflow \${execution.status === "success" ? "Succeeded" : "Failed"}: \${workflow.name}\`,
channel: "#n8n-notifications"
}
}];
`
}
},
{
name: "Send to Slack",
type: "n8n-nodes-base.slack",
position: [500, 300],
parameters: {
token: "{{$env.SLACK_TOKEN}}",
text: "={{$json.text}}",
channel: "={{$json.channel}}",
otherOptions: {
blocks: "={{$json.blocks}}"
}
}
}
],
connections: {
"Webhook": {
main: [
[
{
node: "Format Slack Message",
type: "main",
index: 0
}
]
]
},
"Format Slack Message": {
main: [
[
{
node: "Send to Slack",
type: "main",
index: 0
}
]
]
}
}
});
// Create workflow to set up webhook in n8n settings
const setupWorkflow = await useMcpTool('n8n-mcp-server', 'workflow_create', {
name: "Configure n8n Execution Notifications",
active: false,
nodes: [
{
name: "Manual Trigger",
type: "n8n-nodes-base.manualTrigger",
position: [100, 300],
parameters: {}
},
{
name: "n8n Settings",
type: "n8n-nodes-base.n8n",
position: [300, 300],
parameters: {
resource: "settings",
operation: "update",
values: {
executionTimeout: 120,
saveManualExecutions: true,
saveDataErrorExecution: "all",
saveDataSuccessExecution: "all",
saveExecutionProgress: true,
executeTimeout: 120,
maxExecutionTimeout: 300,
workflowCallerIdsAllowed: [],
timezone: "UTC",
executionMode: "regular",
pushBackend: "websocket",
webhookDebugEnabled: false,
webhookUrl: "={{$json.webhookUrl}}",
logLevel: "info"
}
}
},
{
name: "Create Instructions",
type: "n8n-nodes-base.function",
position: [500, 300],
parameters: {
functionCode: `
const webhookNode = $input.itemsFrom('Webhook')? $input.itemsFrom('Webhook')[0] : { json: { webhookUrl: "unknown" } };
const webhookUrl = webhookNode.json.webhookUrl;
return [{
json: {
instructions: \`
# n8n to Slack Notification Setup
I've created two workflows:
1. **n8n Execution Notifications to Slack** - This workflow receives execution notifications from n8n and sends them to Slack
- ID: ${notificationWorkflow.id}
- Webhook URL: \${webhookUrl}
2. **Configure n8n Execution Notifications** - This workflow configures n8n settings to send notifications
- ID: ${setupWorkflow.id}
## Next Steps:
1. Create a Slack app and obtain a token with the following permissions:
- chat:write
- chat:write.public
2. Set the environment variable \`SLACK_TOKEN\` with your Slack token
3. Run the "Configure n8n Execution Notifications" workflow after setting the \`webhookUrl\` parameter to:
\${webhookUrl}
4. Activate the "n8n Execution Notifications to Slack" workflow
5. Customize the channel in the "Format Slack Message" node if needed (default is #n8n-notifications)
Once completed, you'll receive Slack notifications whenever a workflow execution succeeds or fails!
\`
}
}];
`
}
}
],
connections: {
"Manual Trigger": {
main: [
[
{
node: "n8n Settings",
type: "main",
index: 0
}
]
]
},
"n8n Settings": {
main: [
[
{
node: "Create Instructions",
type: "main",
index: 0
}
]
]
}
}
});
// Execute the workflow to get the webhook URL
const execution = await useMcpTool('n8n-mcp-server', 'execution_run', {
workflowId: notificationWorkflow.id,
waitForCompletion: true
});
// Extract webhook URL from the execution
let webhookUrl = "undefined";
if (execution.status === "success" && execution.data?.resultData?.runData?.Webhook?.[0]?.data?.webhookUrl) {
webhookUrl = execution.data.resultData.runData.Webhook[0].data.webhookUrl;
}
return `
# n8n to Slack Notification Setup
I've created two workflows:
1. **n8n Execution Notifications to Slack** - This workflow receives execution notifications from n8n and sends them to Slack
- ID: ${notificationWorkflow.id}
- Webhook URL: ${webhookUrl}
2. **Configure n8n Execution Notifications** - This workflow configures n8n settings to send notifications
- ID: ${setupWorkflow.id}
## Next Steps:
1. Create a Slack app and obtain a token with the following permissions:
- chat:write
- chat:write.public
2. Set the environment variable \`SLACK_TOKEN\` with your Slack token
3. Run the "Configure n8n Execution Notifications" workflow after setting the \`webhookUrl\` parameter to:
${webhookUrl}
4. Activate the "n8n Execution Notifications to Slack" workflow
5. Customize the channel in the "Format Slack Message" node if needed (default is #n8n-notifications)
Once completed, you'll receive Slack notifications whenever a workflow execution completes!
```
# Integration Examples
This page provides examples of integrating the n8n MCP Server with other systems and AI assistant platforms.
## AI Assistant Integration Examples
### Claude AI Assistant Integration
#### Example: Setting Up n8n MCP Server with Claude
```javascript
// Register the n8n MCP Server with Claude using the MCP Installer
const installationResult = await useMcpTool('mcp-installer', 'install_repo_mcp_server', {
name: 'n8n-mcp-server',
env: [
"N8N_API_URL=http://localhost:5678/api/v1",
"N8N_API_KEY=your_n8n_api_key_here",
"DEBUG=false"
]
});
// Once registered, Claude can interact with n8n
// Here's an example conversation:
// User: "Show me my active workflows in n8n"
// Claude: (uses workflow_list tool to retrieve and display active workflows)
// User: "Execute my 'Daily Report' workflow"
// Claude: (uses execution_run tool to start the workflow and provide status)
```
#### Example: Using n8n to Extend Claude's Capabilities
```javascript
// Using n8n as a bridge to external systems
// This example shows how Claude can access a database through n8n
// User: "Show me the top 5 customers from my database"
// Claude would:
// 1. Find the appropriate workflow
const workflows = await useMcpTool('n8n-mcp-server', 'workflow_list', {});
const dbQueryWorkflow = workflows.find(w =>
w.name.toLowerCase().includes('database query') ||
w.name.toLowerCase().includes('db query')
);
if (!dbQueryWorkflow) {
return "I couldn't find a database query workflow. Would you like me to help you create one?";
}
// 2. Execute the workflow with the appropriate parameters
const execution = await useMcpTool('n8n-mcp-server', 'execution_run', {
workflowId: dbQueryWorkflow.id,
data: {
query: "SELECT * FROM customers ORDER BY total_purchases DESC LIMIT 5",
format: "table"
},
waitForCompletion: true
});
// 3. Present the results to the user
if (execution.status !== "success") {
return "There was an error querying the database. Error: " + (execution.error || "Unknown error");
}
// Format the results as a table
const customers = execution.data.resultData.runData.lastNode[0].data.json;
let response = "# Top 5 Customers\n\n";
response += "| Customer Name | Email | Total Purchases |\n";
response += "|--------------|-------|----------------|\n";
customers.forEach(customer => {
response += `| ${customer.name} | ${customer.email} | $${customer.total_purchases.toFixed(2)} |\n`;
});
return response;
```
### OpenAI Assistant Integration
#### Example: Connecting n8n MCP Server to OpenAI Assistant
```javascript
// This is a conceptual example of how an OpenAI Assistant might interact with the n8n MCP Server
// In an OpenAI Assistant Function definition:
{
"name": "n8n_workflow_list",
"description": "List all workflows in n8n",
"parameters": {
"type": "object",
"properties": {
"active": {
"type": "boolean",
"description": "Filter by active status (optional)"
}
}
}
}
// The function would call the n8n MCP Server:
async function n8n_workflow_list(params) {
// Call the n8n MCP Server API
const response = await fetch('http://localhost:3000/n8n-mcp/tools/workflow_list', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_AUTH_TOKEN'
},
body: JSON.stringify(params)
});
return await response.json();
}
// The OpenAI Assistant would then use this function when asked about n8n workflows
```
## Integration with External Systems
### n8n to Git Integration
#### Example: Using n8n MCP Server to Manage Workflow Versioning in Git
```javascript
// User request: "Backup all my workflows to my Git repository"
// The assistant first creates a workflow for Git backup
const backupWorkflow = await useMcpTool('n8n-mcp-server', 'workflow_create', {
name: "Workflow Git Backup",
active: false,
nodes: [
{
name: "Manual Trigger",
type: "n8n-nodes-base.manualTrigger",
position: [100, 300],
parameters: {}
},
{
name: "Get All Workflows",
type: "n8n-nodes-base.n8n",
position: [300, 300],
parameters: {
resource: "workflow",
operation: "getAll"
}
},
{
name: "Format For Git",
type: "n8n-nodes-base.function",
position: [500, 300],
parameters: {
functionCode: `
// Convert workflows to individual JSON files
const workflows = items;
const outputItems = [];
for (const workflow of workflows) {
// Create a sanitized filename
const filename = workflow.json.name
.replace(/[^a-zA-Z0-9]/g, '_')
.toLowerCase() + '.json';
outputItems.push({
json: {
filename: filename,
content: JSON.stringify(workflow.json, null, 2),
commit_message: \`Backup workflow: \${workflow.json.name}\`
}
});
}
return outputItems;
`
}
},
{
name: "Git Commit",
type: "n8n-nodes-base.git",
position: [700, 300],
parameters: {
repository: "{{$env.GIT_REPO_PATH}}",
directory: "workflows",
operation: "commit",
message: "Automated workflow backup {{$now.format('YYYY-MM-DD HH:mm')}}",
filePath: "={{$json.filename}}",
fileContent: "={{$json.content}}"
}
},
{
name: "Git Push",
type: "n8n-nodes-base.git",
position: [900, 300],
parameters: {
repository: "{{$env.GIT_REPO_PATH}}",
operation: "push"
}
}
],
connections: {
"Manual Trigger": {
main: [
[
{
node: "Get All Workflows",
type: "main",
index: 0
}
]
]
},
"Get All Workflows": {
main: [
[
{
node: "Format For Git",
type: "main",
index: 0
}
]
]
},
"Format For Git": {
main: [
[
{
node: "Git Commit",
type: "main",
index: 0
}
]
]
},
"Git Commit": {
main: [
[
{
node: "Git Push",
type: "main",
index: 0
}
]
]
}
}
});
return `
# Git Backup Workflow Created
I've created a new workflow that will backup all your n8n workflows to Git.
**Workflow ID:** ${backupWorkflow.id}
## Configuration Needed:
1. The workflow is currently **inactive** - activate it once configured
2. Set the environment variable \`GIT_REPO_PATH\` to point to your Git repository
3. Ensure the Git repository is properly configured with credentials
## How It Works:
- When executed, this workflow retrieves all n8n workflows
- Converts each workflow to a JSON file
- Commits each file to the specified Git repository
- Pushes the changes to the remote repository
## Usage:
You can run this workflow manually whenever you want to backup your workflows,
or add a Schedule Trigger to automate regular backups.
`;
```
### CRM Integration
#### Example: Setting Up a Two-Way Sync Between n8n and a CRM
```javascript
// User request: "Help me set up a sync between n8n and my HubSpot CRM"
// First check for existing credentials
const workflows = await useMcpTool('n8n-mcp-server', 'workflow_list', {});
const hubspotWorkflows = workflows.filter(w =>
w.name.toLowerCase().includes('hubspot')
);
let credentialsMessage = "";
if (hubspotWorkflows.length > 0) {
credentialsMessage = "I noticed you already have HubSpot workflows. I'll assume you have HubSpot credentials set up.";
} else {
credentialsMessage = "You'll need to set up HubSpot credentials in n8n before activating this workflow.";
}
// Create the sync workflow
const syncWorkflow = await useMcpTool('n8n-mcp-server', 'workflow_create', {
name: "HubSpot Two-Way Sync",
active: false,
nodes: [
{
name: "Schedule Trigger",
type: "n8n-nodes-base.scheduleTrigger",
position: [100, 300],
parameters: {
cronExpression: "0 */2 * * *" // Every 2 hours
}
},
{
name: "Get HubSpot Contacts",
type: "n8n-nodes-base.hubspot",
position: [300, 200],
parameters: {
resource: "contact",
operation: "getAll",
returnAll: true,
additionalFields: {
formattedDate: true
}
}
},
{
name: "Get n8n Contacts",
type: "n8n-nodes-base.function",
position: [300, 400],
parameters: {
functionCode: `
// This function would retrieve contacts from your internal database
// For example purposes, we're using a placeholder
// In reality, you might use another n8n node to fetch from your database
return [
{
json: {
internalContacts: [
// Example internal contacts
// In a real scenario, these would come from your database
]
}
}
];
`
}
},
{
name: "Compare & Identify Changes",
type: "n8n-nodes-base.function",
position: [500, 300],
parameters: {
functionCode: `
const hubspotContacts = $input.first();
const internalContacts = $input.last().json.internalContacts;
// Identify new/updated contacts in HubSpot
const hubspotUpdates = hubspotContacts.json.map(contact => {
// Find matching internal contact by email
const internalMatch = internalContacts.find(
ic => ic.email === contact.properties.email
);
if (!internalMatch) {
return {
json: {
type: 'new_in_hubspot',
contact: contact,
action: 'create_in_internal'
}
};
}
// Check if HubSpot contact is newer
const hubspotUpdated = new Date(contact.properties.lastmodifieddate);
const internalUpdated = new Date(internalMatch.updated_at);
if (hubspotUpdated > internalUpdated) {
return {
json: {
type: 'updated_in_hubspot',
contact: contact,
action: 'update_in_internal'
}
};
}
return null;
}).filter(item => item !== null);
// Identify new/updated contacts in internal system
const internalUpdates = internalContacts.map(contact => {
// Find matching HubSpot contact by email
const hubspotMatch = hubspotContacts.json.find(
hc => hc.properties.email === contact.email
);
if (!hubspotMatch) {
return {
json: {
type: 'new_in_internal',
contact: contact,
action: 'create_in_hubspot'
}
};
}
// Check if internal contact is newer
const internalUpdated = new Date(contact.updated_at);
const hubspotUpdated = new Date(hubspotMatch.properties.lastmodifieddate);
if (internalUpdated > hubspotUpdated) {
return {
json: {
type: 'updated_in_internal',
contact: contact,
action: 'update_in_hubspot'
}
};
}
return null;
}).filter(item => item !== null);
// Combine all changes
return [...hubspotUpdates, ...internalUpdates];
`
}
},
{
name: "Route Updates",
type: "n8n-nodes-base.switch",
position: [700, 300],
parameters: {
rules: {
conditions: [
{
value1: "={{$json.action}}",
operation: "equal",
value2: "update_in_hubspot"
},
{
value1: "={{$json.action}}",
operation: "equal",
value2: "create_in_hubspot"
},
{
value1: "={{$json.action}}",
operation: "equal",
value2: "update_in_internal"
},
{
value1: "={{$json.action}}",
operation: "equal",
value2: "create_in_internal"
}
]
}
}
},
{
name: "Update HubSpot Contact",
type: "n8n-nodes-base.hubspot",
position: [900, 100],
parameters: {
resource: "contact",
operation: "update",
contactId: "={{$json.contact.hubspot_id || $json.hubspotId}}",
additionalFields: {
properties: {
firstname: "={{$json.contact.first_name}}",
lastname: "={{$json.contact.last_name}}",
email: "={{$json.contact.email}}",
phone: "={{$json.contact.phone}}",
// Add additional fields as needed
}
}
}
},
{
name: "Create HubSpot Contact",
type: "n8n-nodes-base.hubspot",
position: [900, 250],
parameters: {
resource: "contact",
operation: "create",
additionalFields: {
properties: {
firstname: "={{$json.contact.first_name}}",
lastname: "={{$json.contact.last_name}}",
email: "={{$json.contact.email}}",
phone: "={{$json.contact.phone}}",
// Add additional fields as needed
}
}
}
},
{
name: "Update Internal Contact",
type: "n8n-nodes-base.function",
position: [900, 400],
parameters: {
functionCode: `
// This function would update contacts in your internal database
// In reality, you might use another n8n node like a database connector
const contact = $input.first().json.contact;
// Process the HubSpot contact into internal format
const internalFormat = {
first_name: contact.properties.firstname,
last_name: contact.properties.lastname,
email: contact.properties.email,
phone: contact.properties.phone,
hubspot_id: contact.id,
updated_at: new Date().toISOString()
};
// In a real implementation, you would update your database
// For this example, we just return what would be updated
return [{
json: {
action: "updated_internal_contact",
contact: internalFormat
}
}];
`
}
},
{
name: "Create Internal Contact",
type: "n8n-nodes-base.function",
position: [900, 550],
parameters: {
functionCode: `
// This function would create new contacts in your internal database
// In reality, you might use another n8n node like a database connector
const contact = $input.first().json.contact;
// Process the HubSpot contact into internal format
const internalFormat = {
first_name: contact.properties.firstname,
last_name: contact.properties.lastname,
email: contact.properties.email,
phone: contact.properties.phone,
hubspot_id: contact.id,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
// In a real implementation, you would insert into your database
// For this example, we just return what would be created
return [{
json: {
action: "created_internal_contact",
contact: internalFormat
}
}];
`
}
},
{
name: "Sync Report",
type: "n8n-nodes-base.function",
position: [1100, 300],
parameters: {
functionCode: `
// Collect all results from previous nodes
const updateHubspot = $input.itemsFrom('Update HubSpot Contact');
const createHubspot = $input.itemsFrom('Create HubSpot Contact');
const updateInternal = $input.itemsFrom('Update Internal Contact');
const createInternal = $input.itemsFrom('Create Internal Contact');
// Generate sync summary
return [{
json: {
summary: "Sync Complete",
timestamp: new Date().toISOString(),
stats: {
hubspot_updated: updateHubspot.length,
hubspot_created: createHubspot.length,
internal_updated: updateInternal.length,
internal_created: createInternal.length,
total_changes: updateHubspot.length + createHubspot.length +
updateInternal.length + createInternal.length
}
}
}];
`
}
}
],
connections: {
"Schedule Trigger": {
main: [
[
{
node: "Get HubSpot Contacts",
type: "main",
index: 0
},
{
node: "Get n8n Contacts",
type: "main",
index: 0
}
]
]
},
"Get HubSpot Contacts": {
main: [
[
{
node: "Compare & Identify Changes",
type: "main",
index: 0
}
]
]
},
"Get n8n Contacts": {
main: [
[
{
node: "Compare & Identify Changes",
type: "main",
index: 1
}
]
]
},
"Compare & Identify Changes": {
main: [
[
{
node: "Route Updates",
type: "main",
index: 0
}
]
]
},
"Route Updates": {
main: [
[
{
node: "Update HubSpot Contact",
type: "main",
index: 0
}
],
[
{
node: "Create HubSpot Contact",
type: "main",
index: 0
}
],
[
{
node: "Update Internal Contact",
type: "main",
index: 0
}
],
[
{
node: "Create Internal Contact",
type: "main",
index: 0
}
]
]
},
"Update HubSpot Contact": {
main: [
[
{
node: "Sync Report",
type: "main",
index: 0
}
]
]
},
"Create HubSpot Contact": {
main: [
[
{
node: "Sync Report",
type: "main",
index: 0
}
]
]
},
"Update Internal Contact": {
main: [
[
{
node: "Sync Report",
type: "main",
index: 0
}
]
]
},
"Create Internal Contact": {
main: [
[
{
node: "Sync Report",
type: "main",
index: 0
}
]
]
}
}
});
return `
# HubSpot Two-Way Sync Workflow Created
I've created a new workflow that will synchronize contacts between HubSpot and your internal system.
**Workflow ID:** ${syncWorkflow.id}
## Configuration Needed:
1. The workflow is currently **inactive** - activate it once configured
2. ${credentialsMessage}
3. You'll need to customize the "Get n8n Contacts", "Update Internal Contact", and "Create Internal Contact" nodes to work with your specific database or storage system
## How It Works:
- Runs every 2 hours (configurable)
- Retrieves contacts from both HubSpot and your internal system
- Compares the data to identify new or updated contacts in either system
- Creates or updates contacts to keep both systems in sync
- Generates a sync report with statistics
## Customization:
You may need to modify the field mappings in the function nodes to match your specific data structure
and add any additional fields that need to be synchronized between the systems.
`;
```
### Slack Integration
#### Example: Creating a Workflow to Send n8n Notifications to Slack
```javascript
// User request: "Create a workflow that sends n8n execution notifications to Slack"
// Create the notification workflow
const notificationWorkflow = await useMcpTool('n8n-mcp-server', 'workflow_create', {
name: "n8n Execution Notifications to Slack",
active: false,
nodes: [
{
name: "Webhook",
type: "n8n-nodes-base.webhook",
position: [100, 300],
parameters: {
path: "n8n-notification",
responseMode: "onReceived",
options: {
responseData: "noData"
}
}
},
{
name: "Format Slack Message",
type: "n8n-nodes-base.function",
position: [300, 300],
parameters: {
functionCode: `
// Parse webhook data from n8n
const data = $input.first().json;
const workflow = data.workflow;
const execution = data.execution;
// Determine status color
let color = "#36a64f"; // Green for success
let icon = "✅";
if (execution.status === "error") {
color = "#ff0000"; // Red for error
icon = "❌";
} else if (execution.status === "warning") {
color = "#ffcc00"; // Yellow for warning
icon = "⚠️";
}
// Create Slack message blocks
const blocks = [
{
"type": "header",
"text": {
"type": "plain_text",
"text": \`\${icon} Workflow \${execution.status === "success" ? "Succeeded" : "Failed"}: \${workflow.name}\`,
"emoji": true
}
},
{
"type": "divider"
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": \`*Status:*\\n\${execution.status}\`
},
{
"type": "mrkdwn",
"text": \`*Execution Time:*\\n\${new Date(execution.startedAt).toLocaleString()}\`
},
{
"type": "mrkdwn",
"text": \`*Duration:*\\n\${Math.round((new Date(execution.finishedAt) - new Date(execution.startedAt)) / 1000)} seconds\`
},
{
"type": "mrkdwn",
"text": \`*Mode:*\\n\${execution.mode}\`
}
]
}
];
// Add error details if present
if (execution.status === "error" && execution.error) {
blocks.push(
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Error Details:*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": \`\\\`\\\`\${execution.error.message || "Unknown error"}\\\`\\\`\`
}
}
);
}
// Add link to n8n execution
blocks.push(
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": \`<\${data.n8nUrl}/workflow/\${workflow.id}|View Workflow> | <\${data.n8nUrl}/execution/\${execution.id}|View Execution>\`
}
}
);
return [{
json: {
blocks: blocks,
text: \`\${icon} Workflow \${execution.status === "success" ? "Succeeded" : "Failed"}: \${workflow.name}\`,
channel: "#n8n-notifications"
}
}];
`
}
},
{
name: "Send to Slack",
type: "n8n-nodes-base.slack",
position: [500, 300],
parameters: {
token: "{{$env.SLACK_TOKEN}}",
text: "={{$json.text}}",
channel: "={{$json.channel}}",
otherOptions: {
blocks: "={{$json.blocks}}"
}
}
}
],
connections: {
"Webhook": {
main: [
[
{
node: "Format Slack Message",
type: "main",
index: 0
}
]
]
},
"Format Slack Message": {
main: [
[
{
node: "Send to Slack",
type: "main",
index: 0
}
]
]
}
}
});
// Create workflow to set up webhook in n8n settings
const setupWorkflow = await useMcpTool('n8n-mcp-server', 'workflow_create', {
name: "Configure n8n Execution Notifications",
active: false,
nodes: [
{
name: "Manual Trigger",
type: "n8n-nodes-base.manualTrigger",
position: [100, 300],
parameters: {}
},
{
name: "n8n Settings",
type: "n8n-nodes-base.n8n",
position: [300, 300],
parameters: {
resource: "settings",
operation: "update",
values: {
executionTimeout: 120,
saveManualExecutions: true,
saveDataErrorExecution: "all",
saveDataSuccessExecution: "all",
saveExecutionProgress: true,
executeTimeout: 120,
maxExecutionTimeout: 300,
workflowCallerIdsAllowed: [],
timezone: "UTC",
executionMode: "regular",
pushBackend: "websocket",
webhookDebugEnabled: false,
webhookUrl: "={{$json.webhookUrl}}",
logLevel: "info"
}
}
},
{
name: "Create Instructions",
type: "n8n-nodes-base.function",
position: [500, 300],
parameters: {
functionCode: `
const webhookNode = $input.itemsFrom('Webhook')? $input.itemsFrom('Webhook')[0] : { json: { webhookUrl: "unknown" } };
const webhookUrl = webhookNode.json.webhookUrl;
return [{
json: {
instructions: \`
# n8n to Slack Notification Setup
I've created two workflows:
1. **n8n Execution Notifications to Slack** - This workflow receives execution notifications from n8n and sends them to Slack
- ID: ${notificationWorkflow.id}
- Webhook URL: \${webhookUrl}
2. **Configure n8n Execution Notifications** - This workflow configures n8n settings to send notifications
- ID: ${setupWorkflow.id}
## Next Steps:
1. Create a Slack app and obtain a token with the following permissions:
- chat:write
- chat:write.public
2. Set the environment variable \`SLACK_TOKEN\` with your Slack token
3. Run the "Configure n8n Execution Notifications" workflow after setting the \`webhookUrl\` parameter to:
\${webhookUrl}
4. Activate the "n8n Execution Notifications to Slack" workflow
5. Customize the channel in the "Format Slack Message" node if needed (default is #n8n-notifications)
Once completed, you'll receive Slack notifications whenever a workflow execution succeeds or fails!
\`
}
}];
`
}
}
],
connections: {
"Manual Trigger": {
main: [
[
{
node: "n8n Settings",
type: "main",
index: 0
}
]
]
},
"n8n Settings": {
main: [
[
{
node: "Create Instructions",
type: "main",
index: 0
}
]
]
}
}
});
// Execute the workflow to get the webhook URL
const execution = await useMcpTool('n8n-mcp-server', 'execution_run', {
workflowId: notificationWorkflow.id,
waitForCompletion: true
});
// Extract webhook URL from the execution
let webhookUrl = "undefined";
if (execution.status === "success" && execution.data?.resultData?.runData?.Webhook?.[0]?.data?.webhookUrl) {
webhookUrl = execution.data.resultData.runData.Webhook[0].data.webhookUrl;
}
return `
# n8n to Slack Notification Setup
I've created two workflows:
1. **n8n Execution Notifications to Slack** - This workflow receives execution notifications from n8n and sends them to Slack
- ID: ${notificationWorkflow.id}
- Webhook URL: ${webhookUrl}
2. **Configure n8n Execution Notifications** - This workflow configures n8n settings to send notifications
- ID: ${setupWorkflow.id}
## Next Steps:
1. Create a Slack app and obtain a token with the following permissions:
- chat:write
- chat:write.public
2. Set the environment variable \`SLACK_TOKEN\` with your Slack token
3. Run the "Configure n8n Execution Notifications" workflow after setting the \`webhookUrl\` parameter to:
${webhookUrl}
4. Activate the "n8n Execution Notifications to Slack" workflow
5. Customize the channel in the "Format Slack Message" node if needed (default is #n8n-notifications)
Once completed, you
```
--------------------------------------------------------------------------------
/n8n-openapi.yml:
--------------------------------------------------------------------------------
```yaml
openapi: 3.0.0
info:
title: n8n Public API
description: n8n Public API
termsOfService: https://n8n.io/legal/terms
contact:
email: [email protected]
license:
name: Sustainable Use License
url: https://github.com/n8n-io/n8n/blob/master/LICENSE.md
version: 1.1.1
servers:
- url: /api/v1
security:
- ApiKeyAuth: []
tags:
- name: User
description: Operations about users
- name: Audit
description: Operations about security audit
- name: Execution
description: Operations about executions
- name: Workflow
description: Operations about workflows
- name: Credential
description: Operations about credentials
- name: Tags
description: Operations about tags
- name: SourceControl
description: Operations about source control
- name: Variables
description: Operations about variables
- name: Projects
description: Operations about projects
externalDocs:
description: n8n API documentation
url: https://docs.n8n.io/api/
paths:
/audit:
post:
x-eov-operation-id: generateAudit
x-eov-operation-handler: v1/handlers/audit/audit.handler
tags:
- Audit
summary: Generate an audit
description: Generate a security audit for your n8n instance.
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
additionalOptions:
type: object
properties:
daysAbandonedWorkflow:
type: integer
description: Days for a workflow to be considered abandoned if not executed
categories:
type: array
items:
type: string
enum:
- credentials
- database
- nodes
- filesystem
- instance
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/audit'
'401':
$ref: '#/components/responses/unauthorized'
'500':
description: Internal server error.
/credentials:
post:
x-eov-operation-id: createCredential
x-eov-operation-handler: v1/handlers/credentials/credentials.handler
tags:
- Credential
summary: Create a credential
description: Creates a credential that can be used by nodes of the specified type.
requestBody:
description: Credential to be created.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/credential'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/create-credential-response'
'401':
$ref: '#/components/responses/unauthorized'
'415':
description: Unsupported media type.
/credentials/{id}:
delete:
x-eov-operation-id: deleteCredential
x-eov-operation-handler: v1/handlers/credentials/credentials.handler
tags:
- Credential
summary: Delete credential by ID
description: Deletes a credential from your instance. You must be the owner of the credentials
operationId: deleteCredential
parameters:
- name: id
in: path
description: The credential ID that needs to be deleted
required: true
schema:
type: string
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/credential'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
/credentials/schema/{credentialTypeName}:
get:
x-eov-operation-id: getCredentialType
x-eov-operation-handler: v1/handlers/credentials/credentials.handler
tags:
- Credential
summary: Show credential data schema
parameters:
- name: credentialTypeName
in: path
description: The credential type name that you want to get the schema for
required: true
schema:
type: string
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
type: object
examples:
freshdeskApi:
value:
additionalProperties: false
type: object
properties:
apiKey:
type: string
domain:
type: string
required:
- apiKey
- domain
slackOAuth2Api:
value:
additionalProperties: false
type: object
properties:
clientId:
type: string
clientSecret:
type: string
required:
- clientId
- clientSecret
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
/executions:
get:
x-eov-operation-id: getExecutions
x-eov-operation-handler: v1/handlers/executions/executions.handler
tags:
- Execution
summary: Retrieve all executions
description: Retrieve all executions from your instance.
parameters:
- $ref: '#/components/parameters/includeData'
- name: status
in: query
description: Status to filter the executions by.
required: false
schema:
type: string
enum:
- error
- success
- waiting
- name: workflowId
in: query
description: Workflow to filter the executions by.
required: false
schema:
type: string
example: '1000'
- name: projectId
in: query
required: false
explode: false
allowReserved: true
schema:
type: string
example: VmwOO9HeTEj20kxM
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/cursor'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/executionList'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
/executions/{id}:
get:
x-eov-operation-id: getExecution
x-eov-operation-handler: v1/handlers/executions/executions.handler
tags:
- Execution
summary: Retrieve an execution
description: Retrieve an execution from your instance.
parameters:
- $ref: '#/components/parameters/executionId'
- $ref: '#/components/parameters/includeData'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/execution'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
delete:
x-eov-operation-id: deleteExecution
x-eov-operation-handler: v1/handlers/executions/executions.handler
tags:
- Execution
summary: Delete an execution
description: Deletes an execution from your instance.
parameters:
- $ref: '#/components/parameters/executionId'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/execution'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
/tags:
post:
x-eov-operation-id: createTag
x-eov-operation-handler: v1/handlers/tags/tags.handler
tags:
- Tags
summary: Create a tag
description: Create a tag in your instance.
requestBody:
description: Created tag object.
content:
application/json:
schema:
$ref: '#/components/schemas/tag'
required: true
responses:
'201':
description: A tag object
content:
application/json:
schema:
$ref: '#/components/schemas/tag'
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
'409':
$ref: '#/components/responses/conflict'
get:
x-eov-operation-id: getTags
x-eov-operation-handler: v1/handlers/tags/tags.handler
tags:
- Tags
summary: Retrieve all tags
description: Retrieve all tags from your instance.
parameters:
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/cursor'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/tagList'
'401':
$ref: '#/components/responses/unauthorized'
/tags/{id}:
get:
x-eov-operation-id: getTag
x-eov-operation-handler: v1/handlers/tags/tags.handler
tags:
- Tags
summary: Retrieves a tag
description: Retrieves a tag.
parameters:
- $ref: '#/components/parameters/tagId'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/tag'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
delete:
x-eov-operation-id: deleteTag
x-eov-operation-handler: v1/handlers/tags/tags.handler
tags:
- Tags
summary: Delete a tag
description: Deletes a tag.
parameters:
- $ref: '#/components/parameters/tagId'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/tag'
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
'404':
$ref: '#/components/responses/notFound'
put:
x-eov-operation-id: updateTag
x-eov-operation-handler: v1/handlers/tags/tags.handler
tags:
- Tags
summary: Update a tag
description: Update a tag.
parameters:
- $ref: '#/components/parameters/tagId'
requestBody:
description: Updated tag object.
content:
application/json:
schema:
$ref: '#/components/schemas/tag'
required: true
responses:
'200':
description: Tag object
content:
application/json:
schema:
$ref: '#/components/schemas/tag'
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
'409':
$ref: '#/components/responses/conflict'
/workflows:
post:
x-eov-operation-id: createWorkflow
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
tags:
- Workflow
summary: Create a workflow
description: Create a workflow in your instance.
requestBody:
description: Created workflow object.
content:
application/json:
schema:
$ref: '#/components/schemas/workflow'
required: true
responses:
'200':
description: A workflow object
content:
application/json:
schema:
$ref: '#/components/schemas/workflow'
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
get:
x-eov-operation-id: getWorkflows
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
tags:
- Workflow
summary: Retrieve all workflows
description: Retrieve all workflows from your instance.
parameters:
- name: active
in: query
schema:
type: boolean
example: true
- name: tags
in: query
required: false
explode: false
allowReserved: true
schema:
type: string
example: test,production
- name: name
in: query
required: false
explode: false
allowReserved: true
schema:
type: string
example: My Workflow
- name: projectId
in: query
required: false
explode: false
allowReserved: true
schema:
type: string
example: VmwOO9HeTEj20kxM
- name: excludePinnedData
in: query
required: false
description: Set this to avoid retrieving pinned data
schema:
type: boolean
example: true
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/cursor'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/workflowList'
'401':
$ref: '#/components/responses/unauthorized'
/workflows/{id}:
get:
x-eov-operation-id: getWorkflow
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
tags:
- Workflow
summary: Retrieves a workflow
description: Retrieves a workflow.
parameters:
- name: excludePinnedData
in: query
required: false
description: Set this to avoid retrieving pinned data
schema:
type: boolean
example: true
- $ref: '#/components/parameters/workflowId'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/workflow'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
delete:
x-eov-operation-id: deleteWorkflow
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
tags:
- Workflow
summary: Delete a workflow
description: Deletes a workflow.
parameters:
- $ref: '#/components/parameters/workflowId'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/workflow'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
put:
x-eov-operation-id: updateWorkflow
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
tags:
- Workflow
summary: Update a workflow
description: Update a workflow.
parameters:
- $ref: '#/components/parameters/workflowId'
requestBody:
description: Updated workflow object.
content:
application/json:
schema:
$ref: '#/components/schemas/workflow'
required: true
responses:
'200':
description: Workflow object
content:
application/json:
schema:
$ref: '#/components/schemas/workflow'
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
/workflows/{id}/activate:
post:
x-eov-operation-id: activateWorkflow
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
tags:
- Workflow
summary: Activate a workflow
description: Active a workflow.
parameters:
- $ref: '#/components/parameters/workflowId'
responses:
'200':
description: Workflow object
content:
application/json:
schema:
$ref: '#/components/schemas/workflow'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
/workflows/{id}/deactivate:
post:
x-eov-operation-id: deactivateWorkflow
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
tags:
- Workflow
summary: Deactivate a workflow
description: Deactivate a workflow.
parameters:
- $ref: '#/components/parameters/workflowId'
responses:
'200':
description: Workflow object
content:
application/json:
schema:
$ref: '#/components/schemas/workflow'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
/workflows/{id}/transfer:
put:
x-eov-operation-id: transferWorkflow
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
tags:
- Workflow
summary: Transfer a workflow to another project.
description: Transfer a workflow to another project.
parameters:
- $ref: '#/components/parameters/workflowId'
requestBody:
description: Destination project information for the workflow transfer.
content:
application/json:
schema:
type: object
properties:
destinationProjectId:
type: string
description: The ID of the project to transfer the workflow to.
required:
- destinationProjectId
required: true
responses:
'200':
description: Operation successful.
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
/credentials/{id}/transfer:
put:
x-eov-operation-id: transferCredential
x-eov-operation-handler: v1/handlers/credentials/credentials.handler
tags:
- Workflow
summary: Transfer a credential to another project.
description: Transfer a credential to another project.
parameters:
- $ref: '#/components/parameters/credentialId'
requestBody:
description: Destination project for the credential transfer.
content:
application/json:
schema:
type: object
properties:
destinationProjectId:
type: string
description: The ID of the project to transfer the credential to.
required:
- destinationProjectId
required: true
responses:
'200':
description: Operation successful.
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
/workflows/{id}/tags:
get:
x-eov-operation-id: getWorkflowTags
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
tags:
- Workflow
summary: Get workflow tags
description: Get workflow tags.
parameters:
- $ref: '#/components/parameters/workflowId'
responses:
'200':
description: List of tags
content:
application/json:
schema:
$ref: '#/components/schemas/workflowTags'
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
put:
x-eov-operation-id: updateWorkflowTags
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
tags:
- Workflow
summary: Update tags of a workflow
description: Update tags of a workflow.
parameters:
- $ref: '#/components/parameters/workflowId'
requestBody:
description: List of tags
content:
application/json:
schema:
$ref: '#/components/schemas/tagIds'
required: true
responses:
'200':
description: List of tags after add the tag
content:
application/json:
schema:
$ref: '#/components/schemas/workflowTags'
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
/users:
get:
x-eov-operation-id: getUsers
x-eov-operation-handler: v1/handlers/users/users.handler.ee
tags:
- User
summary: Retrieve all users
description: Retrieve all users from your instance. Only available for the instance owner.
parameters:
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/cursor'
- $ref: '#/components/parameters/includeRole'
- name: projectId
in: query
required: false
explode: false
allowReserved: true
schema:
type: string
example: VmwOO9HeTEj20kxM
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/userList'
'401':
$ref: '#/components/responses/unauthorized'
post:
x-eov-operation-id: createUser
x-eov-operation-handler: v1/handlers/users/users.handler.ee
tags:
- User
summary: Create multiple users
description: Create one or more users.
requestBody:
description: Array of users to be created.
required: true
content:
application/json:
schema:
type: array
items:
type: object
properties:
email:
type: string
format: email
role:
type: string
enum:
- global:admin
- global:member
required:
- email
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
type: object
properties:
user:
type: object
properties:
id:
type: string
email:
type: string
inviteAcceptUrl:
type: string
emailSent:
type: boolean
error:
type: string
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
/users/{id}:
get:
x-eov-operation-id: getUser
x-eov-operation-handler: v1/handlers/users/users.handler.ee
tags:
- User
summary: Get user by ID/Email
description: Retrieve a user from your instance. Only available for the instance owner.
parameters:
- $ref: '#/components/parameters/userIdentifier'
- $ref: '#/components/parameters/includeRole'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/user'
'401':
$ref: '#/components/responses/unauthorized'
delete:
x-eov-operation-id: deleteUser
x-eov-operation-handler: v1/handlers/users/users.handler.ee
tags:
- User
summary: Delete a user
description: Delete a user from your instance.
parameters:
- $ref: '#/components/parameters/userIdentifier'
responses:
'204':
description: Operation successful.
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
'404':
$ref: '#/components/responses/notFound'
/users/{id}/role:
patch:
x-eov-operation-id: changeRole
x-eov-operation-handler: v1/handlers/users/users.handler.ee
tags:
- User
summary: Change a user's global role
description: Change a user's global role
parameters:
- $ref: '#/components/parameters/userIdentifier'
requestBody:
description: New role for the user
required: true
content:
application/json:
schema:
type: object
properties:
newRoleName:
type: string
enum:
- global:admin
- global:member
required:
- newRoleName
responses:
'200':
description: Operation successful.
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
'404':
$ref: '#/components/responses/notFound'
/source-control/pull:
post:
x-eov-operation-id: pull
x-eov-operation-handler: v1/handlers/source-control/source-control.handler
tags:
- SourceControl
summary: Pull changes from the remote repository
description: Requires the Source Control feature to be licensed and connected to a repository.
requestBody:
description: Pull options
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/pull'
responses:
'200':
description: Import result
content:
application/json:
schema:
$ref: '#/components/schemas/importResult'
'400':
$ref: '#/components/responses/badRequest'
'409':
$ref: '#/components/responses/conflict'
/variables:
post:
x-eov-operation-id: createVariable
x-eov-operation-handler: v1/handlers/variables/variables.handler
tags:
- Variables
summary: Create a variable
description: Create a variable in your instance.
requestBody:
description: Payload for variable to create.
content:
application/json:
schema:
$ref: '#/components/schemas/variable'
required: true
responses:
'201':
description: Operation successful.
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
get:
x-eov-operation-id: getVariables
x-eov-operation-handler: v1/handlers/variables/variables.handler
tags:
- Variables
summary: Retrieve variables
description: Retrieve variables from your instance.
parameters:
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/cursor'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/variableList'
'401':
$ref: '#/components/responses/unauthorized'
/variables/{id}:
delete:
x-eov-operation-id: deleteVariable
x-eov-operation-handler: v1/handlers/variables/variables.handler
tags:
- Variables
summary: Delete a variable
description: Delete a variable from your instance.
parameters:
- $ref: '#/components/parameters/variableId'
responses:
'204':
description: Operation successful.
'401':
$ref: '#/components/responses/unauthorized'
'404':
$ref: '#/components/responses/notFound'
/projects:
post:
x-eov-operation-id: createProject
x-eov-operation-handler: v1/handlers/projects/projects.handler
tags:
- Projects
summary: Create a project
description: Create a project in your instance.
requestBody:
description: Payload for project to create.
content:
application/json:
schema:
$ref: '#/components/schemas/project'
required: true
responses:
'201':
description: Operation successful.
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
get:
x-eov-operation-id: getProjects
x-eov-operation-handler: v1/handlers/projects/projects.handler
tags:
- Projects
summary: Retrieve projects
description: Retrieve projects from your instance.
parameters:
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/cursor'
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
$ref: '#/components/schemas/projectList'
'401':
$ref: '#/components/responses/unauthorized'
/projects/{projectId}:
delete:
x-eov-operation-id: deleteProject
x-eov-operation-handler: v1/handlers/projects/projects.handler
tags:
- Projects
summary: Delete a project
description: Delete a project from your instance.
parameters:
- $ref: '#/components/parameters/projectId'
responses:
'204':
description: Operation successful.
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
'404':
$ref: '#/components/responses/notFound'
put:
x-eov-operation-id: updateProject
x-eov-operation-handler: v1/handlers/projects/projects.handler
tags:
- Project
summary: Update a project
description: Update a project.
requestBody:
description: Updated project object.
content:
application/json:
schema:
$ref: '#/components/schemas/project'
required: true
responses:
'204':
description: Operation successful.
'400':
$ref: '#/components/responses/badRequest'
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
'404':
$ref: '#/components/responses/notFound'
components:
schemas:
audit:
type: object
properties:
Credentials Risk Report:
type: object
example:
risk: credentials
sections:
- title: Credentials not used in any workflow
description: These credentials are not used in any workflow. Keeping unused credentials in your instance is an unneeded security risk.
recommendation: Consider deleting these credentials if you no longer need them.
location:
- kind: credential
id: '1'
name: My Test Account
Database Risk Report:
type: object
example:
risk: database
sections:
- title: Expressions in "Execute Query" fields in SQL nodes
description: This SQL node has an expression in the "Query" field of an "Execute Query" operation. Building a SQL query with an expression may lead to a SQL injection attack.
recommendation: Consider using the "Query Parameters" field to pass parameters to the query
or validating the input of the expression in the "Query" field.: null
location:
- kind: node
workflowId: '1'
workflowName: My Workflow
nodeId: 51eb5852-ce0b-4806-b4ff-e41322a4041a
nodeName: MySQL
nodeType: n8n-nodes-base.mySql
Filesystem Risk Report:
type: object
example:
risk: filesystem
sections:
- title: Nodes that interact with the filesystem
description: This node reads from and writes to any accessible file in the host filesystem. Sensitive file content may be manipulated through a node operation.
recommendation: Consider protecting any sensitive files in the host filesystem
or refactoring the workflow so that it does not require host filesystem interaction.: null
location:
- kind: node
workflowId: '1'
workflowName: My Workflow
nodeId: 51eb5852-ce0b-4806-b4ff-e41322a4041a
nodeName: Ready Binary file
nodeType: n8n-nodes-base.readBinaryFile
Nodes Risk Report:
type: object
example:
risk: nodes
sections:
- title: Community nodes
description: This node is sourced from the community. Community nodes are not vetted by the n8n team and have full access to the host system.
recommendation: Consider reviewing the source code in any community nodes installed in this n8n instance
and uninstalling any community nodes no longer used.: null
location:
- kind: community
nodeType: n8n-nodes-test.test
packageUrl: https://www.npmjs.com/package/n8n-nodes-test
Instance Risk Report:
type: object
example:
risk: execution
sections:
- title: Unprotected webhooks in instance
description: These webhook nodes have the "Authentication" field set to "None" and are not directly connected to a node to validate the payload. Every unprotected webhook allows your workflow to be called by any third party who knows the webhook URL.
recommendation: Consider setting the "Authentication" field to an option other than "None"
or validating the payload with one of the following nodes.: null
location:
- kind: community
nodeType: n8n-nodes-test.test
packageUrl: https://www.npmjs.com/package/n8n-nodes-test
credential:
required:
- name
- type
- data
type: object
properties:
id:
type: string
readOnly: true
example: R2DjclaysHbqn778
name:
type: string
example: Joe's Github Credentials
type:
type: string
example: github
data:
type: object
writeOnly: true
example:
token: ada612vad6fa5df4adf5a5dsf4389adsf76da7s
createdAt:
type: string
format: date-time
readOnly: true
example: '2022-04-29T11:02:29.842Z'
updatedAt:
type: string
format: date-time
readOnly: true
example: '2022-04-29T11:02:29.842Z'
create-credential-response:
required:
- id
- name
- type
- createdAt
- updatedAt
type: object
properties:
id:
type: string
readOnly: true
example: vHxaz5UaCghVYl9C
name:
type: string
example: John's Github account
type:
type: string
example: github
createdAt:
type: string
format: date-time
readOnly: true
example: '2022-04-29T11:02:29.842Z'
updatedAt:
type: string
format: date-time
readOnly: true
example: '2022-04-29T11:02:29.842Z'
execution:
type: object
properties:
id:
type: number
example: 1000
data:
type: object
finished:
type: boolean
example: true
mode:
type: string
enum:
- cli
- error
- integrated
- internal
- manual
- retry
- trigger
- webhook
retryOf:
type: number
nullable: true
retrySuccessId:
type: number
nullable: true
example: '2'
startedAt:
type: string
format: date-time
stoppedAt:
type: string
format: date-time
workflowId:
type: number
example: '1000'
waitTill:
type: string
nullable: true
format: date-time
customData:
type: object
executionList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/execution'
nextCursor:
type: string
description: Paginate through executions by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
nullable: true
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
tag:
type: object
additionalProperties: false
required:
- name
properties:
id:
type: string
readOnly: true
example: 2tUt1wbLX592XDdX
name:
type: string
example: Production
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true
tagList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/tag'
nextCursor:
type: string
description: Paginate through tags by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
nullable: true
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
node:
type: object
additionalProperties: false
properties:
id:
type: string
example: 0f5532f9-36ba-4bef-86c7-30d607400b15
name:
type: string
example: Jira
webhookId:
type: string
disabled:
type: boolean
notesInFlow:
type: boolean
notes:
type: string
type:
type: string
example: n8n-nodes-base.Jira
typeVersion:
type: number
example: 1
executeOnce:
type: boolean
example: false
alwaysOutputData:
type: boolean
example: false
retryOnFail:
type: boolean
example: false
maxTries:
type: number
waitBetweenTries:
type: number
continueOnFail:
type: boolean
example: false
description: use onError instead
deprecated: true
onError:
type: string
example: stopWorkflow
position:
type: array
items:
type: number
example:
- -100
- 80
parameters:
type: object
example:
additionalProperties: {}
credentials:
type: object
example:
jiraSoftwareCloudApi:
id: '35'
name: jiraApi
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true
workflowSettings:
type: object
additionalProperties: false
properties:
saveExecutionProgress:
type: boolean
saveManualExecutions:
type: boolean
saveDataErrorExecution:
type: string
enum:
- all
- none
saveDataSuccessExecution:
type: string
enum:
- all
- none
executionTimeout:
type: number
example: 3600
maxLength: 3600
errorWorkflow:
type: string
example: VzqKEW0ShTXA5vPj
description: The ID of the workflow that contains the error trigger node.
timezone:
type: string
example: America/New_York
executionOrder:
type: string
example: v1
workflow:
type: object
additionalProperties: false
required:
- name
- nodes
- connections
- settings
properties:
id:
type: string
readOnly: true
example: 2tUt1wbLX592XDdX
name:
type: string
example: Workflow 1
active:
type: boolean
readOnly: true
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true
nodes:
type: array
items:
$ref: '#/components/schemas/node'
connections:
type: object
example:
main:
- node: Jira
type: main
index: 0
settings:
$ref: '#/components/schemas/workflowSettings'
staticData:
example:
lastId: 1
anyOf:
- type: string
format: jsonString
nullable: true
- type: object
nullable: true
tags:
type: array
items:
$ref: '#/components/schemas/tag'
readOnly: true
workflowList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/workflow'
nextCursor:
type: string
description: Paginate through workflows by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
nullable: true
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
workflowTags:
type: array
items:
$ref: '#/components/schemas/tag'
tagIds:
type: array
items:
type: object
additionalProperties: false
required:
- id
properties:
id:
type: string
example: 2tUt1wbLX592XDdX
user:
required:
- email
type: object
properties:
id:
type: string
readOnly: true
example: 123e4567-e89b-12d3-a456-426614174000
email:
type: string
format: email
example: [email protected]
firstName:
maxLength: 32
type: string
description: User's first name
readOnly: true
example: john
lastName:
maxLength: 32
type: string
description: User's last name
readOnly: true
example: Doe
isPending:
type: boolean
description: Whether the user finished setting up their account in response to the invitation (true) or not (false).
readOnly: true
createdAt:
type: string
description: Time the user was created.
format: date-time
readOnly: true
updatedAt:
type: string
description: Last time the user was updated.
format: date-time
readOnly: true
role:
type: string
example: owner
readOnly: true
userList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/user'
nextCursor:
type: string
description: Paginate through users by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
nullable: true
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
pull:
type: object
properties:
force:
type: boolean
example: true
variables:
type: object
example:
foo: bar
importResult:
type: object
additionalProperties: true
properties:
variables:
type: object
properties:
added:
type: array
items:
type: string
changed:
type: array
items:
type: string
credentials:
type: array
items:
type: object
properties:
id:
type: string
name:
type: string
type:
type: string
workflows:
type: array
items:
type: object
properties:
id:
type: string
name:
type: string
tags:
type: object
properties:
tags:
type: array
items:
type: object
properties:
id:
type: string
name:
type: string
mappings:
type: array
items:
type: object
properties:
workflowId:
type: string
tagId:
type: string
variable:
type: object
additionalProperties: false
required:
- key
- value
properties:
id:
type: string
readOnly: true
key:
type: string
value:
type: string
example: test
type:
type: string
readOnly: true
variableList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/variable'
nextCursor:
type: string
description: Paginate through variables by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
nullable: true
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
project:
type: object
additionalProperties: false
required:
- name
properties:
id:
type: string
readOnly: true
name:
type: string
type:
type: string
readOnly: true
projectList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/project'
nextCursor:
type: string
description: Paginate through projects by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
nullable: true
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
error:
required:
- message
type: object
properties:
code:
type: string
message:
type: string
description:
type: string
role:
readOnly: true
type: object
properties:
id:
type: number
readOnly: true
example: 1
name:
type: string
example: owner
readOnly: true
scope:
type: string
readOnly: true
example: global
createdAt:
type: string
description: Time the role was created.
format: date-time
readOnly: true
updatedAt:
type: string
description: Last time the role was updated.
format: date-time
readOnly: true
credentialType:
type: object
properties:
displayName:
type: string
readOnly: true
example: Email
name:
type: string
readOnly: true
example: email
type:
type: string
readOnly: true
example: string
default:
type: string
readOnly: true
example: string
Error:
$ref: '#/components/schemas/error'
Role:
$ref: '#/components/schemas/role'
Execution:
$ref: '#/components/schemas/execution'
Node:
$ref: '#/components/schemas/node'
Tag:
$ref: '#/components/schemas/tag'
Workflow:
$ref: '#/components/schemas/workflow'
WorkflowSettings:
$ref: '#/components/schemas/workflowSettings'
ExecutionList:
$ref: '#/components/schemas/executionList'
WorkflowList:
$ref: '#/components/schemas/workflowList'
Credential:
$ref: '#/components/schemas/credential'
CredentialType:
$ref: '#/components/schemas/credentialType'
Audit:
$ref: '#/components/schemas/audit'
Pull:
$ref: '#/components/schemas/pull'
ImportResult:
$ref: '#/components/schemas/importResult'
UserList:
$ref: '#/components/schemas/userList'
User:
$ref: '#/components/schemas/user'
responses:
unauthorized:
description: Unauthorized
notFound:
description: The specified resource was not found.
badRequest:
description: The request is invalid or provides malformed data.
conflict:
description: Conflict
forbidden:
description: Forbidden
NotFound:
$ref: '#/components/responses/notFound'
Unauthorized:
$ref: '#/components/responses/unauthorized'
BadRequest:
$ref: '#/components/responses/badRequest'
Conflict:
$ref: '#/components/responses/conflict'
Forbidden:
$ref: '#/components/responses/forbidden'
parameters:
includeData:
name: includeData
in: query
description: Whether or not to include the execution's detailed data.
required: false
schema:
type: boolean
limit:
name: limit
in: query
description: The maximum number of items to return.
required: false
schema:
type: number
example: 100
default: 100
maximum: 250
cursor:
name: cursor
in: query
description: Paginate by setting the cursor parameter to the nextCursor attribute returned by the previous request's response. Default value fetches the first "page" of the collection. See pagination for more detail.
required: false
style: form
schema:
type: string
executionId:
name: id
in: path
description: The ID of the execution.
required: true
schema:
type: number
tagId:
name: id
in: path
description: The ID of the tag.
required: true
schema:
type: string
workflowId:
name: id
in: path
description: The ID of the workflow.
required: true
schema:
type: string
credentialId:
name: id
in: path
description: The ID of the credential.
required: true
schema:
type: string
includeRole:
name: includeRole
in: query
description: Whether to include the user's role or not.
required: false
schema:
type: boolean
example: true
default: false
userIdentifier:
name: id
in: path
description: The ID or email of the user.
required: true
schema:
type: string
format: identifier
variableId:
name: id
in: path
description: The ID of the variable.
required: true
schema:
type: string
projectId:
name: projectId
in: path
description: The ID of the project.
required: true
schema:
type: string
Cursor:
$ref: '#/components/parameters/cursor'
Limit:
$ref: '#/components/parameters/limit'
ExecutionId:
$ref: '#/components/parameters/executionId'
WorkflowId:
$ref: '#/components/parameters/workflowId'
TagId:
$ref: '#/components/parameters/tagId'
IncludeData:
$ref: '#/components/parameters/includeData'
UserIdentifier:
$ref: '#/components/parameters/userIdentifier'
IncludeRole:
$ref: '#/components/parameters/includeRole'
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-N8N-API-KEY
```