├── .babelrc ├── .env.example ├── .eslintrc.json ├── .github └── workflows │ └── release-package.yml ├── .gitignore ├── AGENTS.md ├── CLAUDE.md ├── Dockerfile ├── LICENSE ├── README.md ├── babel.config.cjs ├── 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 ├── manual_verify_update.mjs ├── n8n-openapi.yml ├── package-lock.json ├── package.json ├── requirements.txt ├── run-tests.js ├── 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 ├── README.md ├── jest-globals.d.ts ├── mocks │ ├── axios-mock.ts │ └── n8n-fixtures.ts ├── 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 /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": "current" } }], 4 | "@babel/preset-typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # n8n MCP Server Environment Variables 2 | 3 | # Required: URL of the n8n API (e.g., http://localhost:5678/api/v1) 4 | N8N_API_URL=http://localhost:5678/api/v1 5 | 6 | # Required: API key for authenticating with n8n 7 | # Generate this in the n8n UI under Settings > API > API Keys 8 | N8N_API_KEY=your_n8n_api_key_here 9 | 10 | # Optional: Set to 'true' to enable debug logging 11 | DEBUG=false 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended" 5 | ], 6 | "parser": "@typescript-eslint/parser", 7 | "plugins": [ 8 | "@typescript-eslint" 9 | ], 10 | "root": true, 11 | "rules": { 12 | "@typescript-eslint/no-explicit-any": "off", 13 | "@typescript-eslint/no-unused-vars": [ 14 | "error", 15 | { 16 | "argsIgnorePattern": "^_", 17 | "varsIgnorePattern": "^_" 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/release-package.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build-and-publish: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write # Allow workflow to push to the repository 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | token: ${{ github.token }} # Uses the default GITHUB_TOKEN 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: '20' # Specify your desired Node.js version 23 | registry-url: 'https://registry.npmjs.org' # Point to npmjs.com 24 | - run: npm ci 25 | - run: npm run build # Add your build script here if you have one, otherwise remove this line 26 | - name: Bump version, commit, and push 27 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 28 | run: | 29 | git config --global user.name 'github-actions[bot]' 30 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 31 | npm version patch -m "chore: release %s" 32 | git push 33 | git push --tags 34 | env: 35 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} # NPM_TOKEN might be needed for npm version if it interacts with registry 36 | - name: Publish to npmjs.com 37 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 38 | run: npm publish 39 | env: 40 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} # Use the NPM_TOKEN secret 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | coverage/ 4 | 5 | # Environment variables 6 | .env 7 | .env.local 8 | .env.development.local 9 | .env.test.local 10 | .env.production.local 11 | 12 | # Build outputs 13 | dist/ 14 | build/ 15 | 16 | # Logs 17 | logs 18 | *.log 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # macOS 31 | .DS_Store 32 | 33 | # Misc 34 | .npm 35 | .eslintcache 36 | .yarn-integrity 37 | 38 | # CRCT System 39 | cline_docs/ 40 | strategy_tasks/ 41 | Previous_versions/ 42 | --output 43 | test-output-summary.md 44 | 45 | # Python 46 | __pycache__/ 47 | *.py[cod] 48 | *$py.class 49 | *.so 50 | .Python 51 | venv/ 52 | ENV/ 53 | env/ 54 | .env/ 55 | .venv/ 56 | *.egg-info/ 57 | dist/ 58 | build/ 59 | 60 | # Embeddings 61 | *.embedding 62 | embeddings/ 63 | mcp-config.json -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Guidelines for Coding Agents 2 | 3 | These rules apply to all automated contributors working on this project. 4 | 5 | ## Development 6 | - Use **TypeScript** and ES modules. Source files live under `src/`. 7 | - Place tests in the `tests/` directory mirroring the source structure. Test files must end with `.test.ts`. 8 | - Install dependencies with `npm install` and build with `npm run build` when required. 9 | - Format code using `npm run lint` and ensure all tests pass via `npm test` before committing. 10 | - Do **not** commit files ignored by `.gitignore` (e.g. `node_modules/`, `build/`, `.env`). 11 | - Follow existing patterns when adding new tools or resources as described in `docs/development`. 12 | 13 | ## Commit Messages 14 | - Use short messages in the form `type: description` (e.g. `feat: add webhook tool`, `fix: handle null id`). 15 | 16 | ## Pull Requests 17 | - Provide a concise summary of changes and reference related issues when opening a PR. 18 | - CI must pass before requesting review. 19 | 20 | ## Environment 21 | - Node.js 20 or later is required. 22 | 23 | ## Continuous Improvement 24 | - After completing a task, review this guide and update it with any lessons 25 | learned about the codebase, coding principles, or user preferences. 26 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Status 6 | 7 | This appears to be a new/empty repository for an n8n MCP (Model Context Protocol) server project. 8 | 9 | ## Getting Started 10 | 11 | When this project is initialized, common commands to look for: 12 | - `npm install` - Install dependencies 13 | - `npm run build` - Build the project 14 | - `npm run dev` - Run in development mode 15 | - `npm run test` - Run tests 16 | - `npm run lint` - Run linting 17 | 18 | ## Architecture Notes 19 | 20 | This will be populated once the project structure is established. Key areas to document: 21 | - MCP server implementation details 22 | - n8n integration patterns 23 | - Configuration management 24 | - API endpoints and protocols -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image using Node.js 20 (LTS) 2 | FROM node:20-slim 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Install Python and pip for Python dependencies 8 | RUN apt-get update && \ 9 | apt-get install -y python3 python3-full python3-venv && \ 10 | apt-get clean && \ 11 | rm -rf /var/lib/apt/lists/* 12 | 13 | # Copy all source files 14 | COPY . . 15 | 16 | # Install Node.js dependencies and build the project 17 | RUN npm install && npm run build 18 | 19 | # Set up Python virtual environment and install dependencies 20 | RUN python3 -m venv /app/venv && \ 21 | /app/venv/bin/pip install --upgrade pip && \ 22 | /app/venv/bin/pip install --no-cache-dir -r requirements.txt 23 | 24 | # Set executable permissions for the binary 25 | RUN chmod +x build/index.js 26 | 27 | # Create a volume for environment configuration 28 | VOLUME /app/config 29 | 30 | # Set environment variables (these will need to be provided when running the container) 31 | ENV N8N_API_URL=http://host.docker.internal:5678/api/v1 32 | ENV N8N_API_KEY=your_n8n_api_key_here 33 | ENV N8N_WEBHOOK_USERNAME=username 34 | ENV N8N_WEBHOOK_PASSWORD=password 35 | ENV DEBUG=false 36 | ENV PATH="/app/venv/bin:$PATH" 37 | 38 | # Create a healthcheck script 39 | RUN echo '#!/bin/sh\necho "n8n-mcp-server is ready"\nexit 0' > /app/healthcheck.sh && \ 40 | chmod +x /app/healthcheck.sh 41 | 42 | # Add healthcheck 43 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 44 | CMD ["/app/healthcheck.sh"] 45 | 46 | # Set the entrypoint to run the MCP server 47 | ENTRYPOINT ["node", "build/index.js"] 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Leonard Sellem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' }, modules: false }], 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: [ 7 | // No explicit CJS transform plugin 8 | ] 9 | }; -------------------------------------------------------------------------------- /docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonardsellem/n8n-mcp-server/d274146fb0a0de0fe15dcfb6d4c4d2f06e4b496a/docs/.gitkeep -------------------------------------------------------------------------------- /docs/api/dynamic-resources.md: -------------------------------------------------------------------------------- 1 | # Dynamic Resources 2 | 3 | This page documents the dynamic resources available in the n8n MCP Server. 4 | 5 | ## Overview 6 | 7 | Dynamic resources are parameterized URIs that allow access to specific n8n data based on identifiers such as workflow IDs or execution IDs. These resources follow the URI template format defined in RFC 6570, with parameters enclosed in curly braces. 8 | 9 | ## Available Resource Templates 10 | 11 | ### n8n://workflow/{id} 12 | 13 | Provides detailed information about a specific workflow. 14 | 15 | **URI Template:** `n8n://workflow/{id}` 16 | 17 | **Parameters:** 18 | - `id` (required): The ID of the workflow to retrieve 19 | 20 | **Description:** Returns comprehensive information about a specific workflow, including its nodes, connections, and settings. 21 | 22 | **Example Usage:** 23 | 24 | ```javascript 25 | const resource = await accessMcpResource('n8n-mcp-server', 'n8n://workflow/1234abc'); 26 | ``` 27 | 28 | **Response:** 29 | 30 | ```javascript 31 | { 32 | "workflow": { 33 | "id": "1234abc", 34 | "name": "Email Processing Workflow", 35 | "active": true, 36 | "createdAt": "2025-03-01T12:00:00.000Z", 37 | "updatedAt": "2025-03-02T14:30:00.000Z", 38 | "nodes": [ 39 | { 40 | "id": "node1", 41 | "name": "Start", 42 | "type": "n8n-nodes-base.start", 43 | "position": [100, 200], 44 | "parameters": {} 45 | }, 46 | { 47 | "id": "node2", 48 | "name": "Email Trigger", 49 | "type": "n8n-nodes-base.emailTrigger", 50 | "position": [300, 200], 51 | "parameters": { 52 | "inbox": "support", 53 | "domain": "example.com" 54 | } 55 | } 56 | ], 57 | "connections": { 58 | "node1": { 59 | "main": [ 60 | [ 61 | { 62 | "node": "node2", 63 | "type": "main", 64 | "index": 0 65 | } 66 | ] 67 | ] 68 | } 69 | }, 70 | "settings": { 71 | "saveExecutionProgress": true, 72 | "saveManualExecutions": true, 73 | "timezone": "America/New_York" 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | ### n8n://executions/{workflowId} 80 | 81 | Provides a list of executions for a specific workflow. 82 | 83 | **URI Template:** `n8n://executions/{workflowId}` 84 | 85 | **Parameters:** 86 | - `workflowId` (required): The ID of the workflow whose executions to retrieve 87 | 88 | **Description:** Returns a list of execution records for the specified workflow, sorted by most recent first. 89 | 90 | **Example Usage:** 91 | 92 | ```javascript 93 | const resource = await accessMcpResource('n8n-mcp-server', 'n8n://executions/1234abc'); 94 | ``` 95 | 96 | **Response:** 97 | 98 | ```javascript 99 | { 100 | "executions": [ 101 | { 102 | "id": "exec789", 103 | "workflowId": "1234abc", 104 | "status": "success", 105 | "startedAt": "2025-03-12T16:30:00.000Z", 106 | "finishedAt": "2025-03-12T16:30:05.000Z", 107 | "mode": "manual" 108 | }, 109 | { 110 | "id": "exec456", 111 | "workflowId": "1234abc", 112 | "status": "error", 113 | "startedAt": "2025-03-11T14:20:00.000Z", 114 | "finishedAt": "2025-03-11T14:20:10.000Z", 115 | "mode": "manual" 116 | } 117 | ], 118 | "count": 2, 119 | "pagination": { 120 | "hasMore": false 121 | } 122 | } 123 | ``` 124 | 125 | ### n8n://execution/{id} 126 | 127 | Provides detailed information about a specific execution. 128 | 129 | **URI Template:** `n8n://execution/{id}` 130 | 131 | **Parameters:** 132 | - `id` (required): The ID of the execution to retrieve 133 | 134 | **Description:** Returns comprehensive information about a specific execution, including its status, inputs, outputs, and execution path. 135 | 136 | **Example Usage:** 137 | 138 | ```javascript 139 | const resource = await accessMcpResource('n8n-mcp-server', 'n8n://execution/exec789'); 140 | ``` 141 | 142 | **Response:** 143 | 144 | ```javascript 145 | { 146 | "execution": { 147 | "id": "exec789", 148 | "workflowId": "1234abc", 149 | "workflowName": "Email Processing Workflow", 150 | "status": "success", 151 | "startedAt": "2025-03-12T16:30:00.000Z", 152 | "finishedAt": "2025-03-12T16:30:05.000Z", 153 | "mode": "manual", 154 | "data": { 155 | "resultData": { 156 | "runData": { 157 | "node1": [ 158 | { 159 | "startTime": "2025-03-12T16:30:00.000Z", 160 | "endTime": "2025-03-12T16:30:01.000Z", 161 | "executionStatus": "success", 162 | "data": { 163 | "json": { 164 | "started": true 165 | } 166 | } 167 | } 168 | ], 169 | "node2": [ 170 | { 171 | "startTime": "2025-03-12T16:30:01.000Z", 172 | "endTime": "2025-03-12T16:30:05.000Z", 173 | "executionStatus": "success", 174 | "data": { 175 | "json": { 176 | "subject": "Test Email", 177 | "body": "This is a test", 178 | "from": "sender@example.com" 179 | } 180 | } 181 | } 182 | ] 183 | } 184 | }, 185 | "executionData": { 186 | "nodeExecutionOrder": ["node1", "node2"], 187 | "waitingNodes": [], 188 | "waitingExecutionData": [] 189 | } 190 | } 191 | } 192 | } 193 | ``` 194 | 195 | ### n8n://workflow/{id}/active 196 | 197 | Provides information about whether a specific workflow is active. 198 | 199 | **URI Template:** `n8n://workflow/{id}/active` 200 | 201 | **Parameters:** 202 | - `id` (required): The ID of the workflow to check 203 | 204 | **Description:** Returns the active status of a specific workflow. 205 | 206 | **Example Usage:** 207 | 208 | ```javascript 209 | const resource = await accessMcpResource('n8n-mcp-server', 'n8n://workflow/1234abc/active'); 210 | ``` 211 | 212 | **Response:** 213 | 214 | ```javascript 215 | { 216 | "workflowId": "1234abc", 217 | "active": true 218 | } 219 | ``` 220 | 221 | ## Content Types 222 | 223 | All dynamic resources return JSON content with the MIME type `application/json`. 224 | 225 | ## Error Handling 226 | 227 | Dynamic resources can return the following errors: 228 | 229 | | HTTP Status | Description | 230 | |-------------|-------------| 231 | | 400 | Bad Request - Invalid parameter in URI | 232 | | 401 | Unauthorized - Invalid or missing API key | 233 | | 403 | Forbidden - API key does not have permission to access this resource | 234 | | 404 | Not Found - The requested resource does not exist | 235 | | 500 | Internal Server Error - An unexpected error occurred on the n8n server | 236 | 237 | ## Parameter Format 238 | 239 | When using dynamic resources, parameters must be properly formatted: 240 | 241 | 1. **Workflow IDs**: Must be valid n8n workflow IDs (typically alphanumeric) 242 | 2. **Execution IDs**: Must be valid n8n execution IDs (typically alphanumeric) 243 | 244 | ## Best Practices 245 | 246 | - Validate resource URIs before accessing them 247 | - Handle possible 404 errors when accessing resources by ID 248 | - Cache resource data when appropriate to reduce API calls 249 | - Use specific resources (like `n8n://workflow/{id}/active`) for single properties when you don't need the entire resource 250 | - Check workflow status before performing operations that require an active workflow 251 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | This section provides a comprehensive reference for the n8n MCP Server API, including all available tools and resources. 4 | 5 | ## Overview 6 | 7 | The n8n MCP Server implements the Model Context Protocol (MCP) to provide AI assistants with access to n8n workflows and executions. The API is divided into two main categories: 8 | 9 | 1. **Tools**: Executable functions that can perform operations on n8n, such as creating workflows or starting executions. 10 | 2. **Resources**: Data sources that provide information about workflows and executions. 11 | 12 | ## API Architecture 13 | 14 | The n8n MCP Server follows a clean separation of concerns: 15 | 16 | - **Client Layer**: Handles communication with the n8n API 17 | - **Transport Layer**: Implements the MCP protocol for communication with AI assistants 18 | - **Tools Layer**: Exposes executable operations to AI assistants 19 | - **Resources Layer**: Provides data access through URI-based resources 20 | 21 | All API interactions are authenticated using the n8n API key configured in your environment. 22 | 23 | ## Available Tools 24 | 25 | The server provides tools for managing workflows and executions: 26 | 27 | - [Workflow Tools](./workflow-tools.md): Create, list, update, and delete workflows 28 | - [Execution Tools](./execution-tools.md): Execute workflows and manage workflow executions 29 | 30 | ## Available Resources 31 | 32 | The server provides resources for accessing workflow and execution data: 33 | 34 | - [Static Resources](./static-resources.md): Fixed resources like workflow listings or execution statistics 35 | - [Dynamic Resources](./dynamic-resources.md): Parameterized resources for specific workflows or executions 36 | 37 | ## Understanding Input Schemas 38 | 39 | Each tool has an input schema that defines the expected parameters. These schemas follow the JSON Schema format and are automatically provided to AI assistants to enable proper parameter validation and suggestion. 40 | 41 | Example input schema for the `workflow_get` tool: 42 | 43 | ```json 44 | { 45 | "type": "object", 46 | "properties": { 47 | "id": { 48 | "type": "string", 49 | "description": "The ID of the workflow to retrieve" 50 | } 51 | }, 52 | "required": ["id"] 53 | } 54 | ``` 55 | 56 | ## Error Handling 57 | 58 | All API operations can return errors in a standardized format. Common error scenarios include: 59 | 60 | - Authentication failures (invalid or missing API key) 61 | - Resource not found (workflow or execution doesn't exist) 62 | - Permission issues (API key doesn't have required permissions) 63 | - Input validation errors (missing or invalid parameters) 64 | 65 | Error responses include detailed messages to help troubleshoot issues. 66 | 67 | ## Next Steps 68 | 69 | Explore the detailed documentation for each category: 70 | 71 | - [Workflow Tools](./workflow-tools.md) 72 | - [Execution Tools](./execution-tools.md) 73 | - [Static Resources](./static-resources.md) 74 | - [Dynamic Resources](./dynamic-resources.md) 75 | -------------------------------------------------------------------------------- /docs/api/static-resources.md: -------------------------------------------------------------------------------- 1 | # Static Resources 2 | 3 | This page documents the static resources available in the n8n MCP Server. 4 | 5 | ## Overview 6 | 7 | Static resources provide access to fixed n8n data sources without requiring parameters in the URI. These resources are ideal for retrieving collections of data or summary information. 8 | 9 | ## Available Resources 10 | 11 | ### n8n://workflows/list 12 | 13 | Provides a list of all workflows in the n8n instance. 14 | 15 | **URI:** `n8n://workflows/list` 16 | 17 | **Description:** Returns a comprehensive list of all workflows with their basic metadata. 18 | 19 | **Example Usage:** 20 | 21 | ```javascript 22 | const resource = await accessMcpResource('n8n-mcp-server', 'n8n://workflows/list'); 23 | ``` 24 | 25 | **Response:** 26 | 27 | ```javascript 28 | { 29 | "workflows": [ 30 | { 31 | "id": "1234abc", 32 | "name": "Email Processing Workflow", 33 | "active": true, 34 | "createdAt": "2025-03-01T12:00:00.000Z", 35 | "updatedAt": "2025-03-02T14:30:00.000Z" 36 | }, 37 | { 38 | "id": "5678def", 39 | "name": "Data Sync Workflow", 40 | "active": false, 41 | "createdAt": "2025-03-01T12:00:00.000Z", 42 | "updatedAt": "2025-03-12T10:15:00.000Z" 43 | } 44 | ], 45 | "count": 2, 46 | "pagination": { 47 | "hasMore": false 48 | } 49 | } 50 | ``` 51 | 52 | ### n8n://execution-stats 53 | 54 | Provides aggregated statistics about workflow executions. 55 | 56 | **URI:** `n8n://execution-stats` 57 | 58 | **Description:** Returns summary statistics about workflow executions, including counts by status, average execution times, and recent trends. 59 | 60 | **Example Usage:** 61 | 62 | ```javascript 63 | const resource = await accessMcpResource('n8n-mcp-server', 'n8n://execution-stats'); 64 | ``` 65 | 66 | **Response:** 67 | 68 | ```javascript 69 | { 70 | "totalExecutions": 1250, 71 | "statusCounts": { 72 | "success": 1050, 73 | "error": 180, 74 | "cancelled": 20 75 | }, 76 | "averageExecutionTime": 3.5, // seconds 77 | "recentActivity": { 78 | "last24Hours": 125, 79 | "last7Days": 450 80 | }, 81 | "topWorkflows": [ 82 | { 83 | "id": "1234abc", 84 | "name": "Email Processing Workflow", 85 | "executionCount": 256 86 | }, 87 | { 88 | "id": "5678def", 89 | "name": "Data Sync Workflow", 90 | "executionCount": 198 91 | } 92 | ] 93 | } 94 | ``` 95 | 96 | ### n8n://health 97 | 98 | Provides health information about the n8n instance. 99 | 100 | **URI:** `n8n://health` 101 | 102 | **Description:** Returns health status information about the n8n instance including connection status, version, and basic metrics. 103 | 104 | **Example Usage:** 105 | 106 | ```javascript 107 | const resource = await accessMcpResource('n8n-mcp-server', 'n8n://health'); 108 | ``` 109 | 110 | **Response:** 111 | 112 | ```javascript 113 | { 114 | "status": "healthy", 115 | "n8nVersion": "1.5.0", 116 | "uptime": 259200, // seconds (3 days) 117 | "databaseStatus": "connected", 118 | "apiStatus": "operational", 119 | "memoryUsage": { 120 | "rss": "156MB", 121 | "heapTotal": "85MB", 122 | "heapUsed": "72MB" 123 | } 124 | } 125 | ``` 126 | 127 | ## Content Types 128 | 129 | All static resources return JSON content with the MIME type `application/json`. 130 | 131 | ## Authentication 132 | 133 | Access to static resources requires the same authentication as tools, using the configured n8n API key. If authentication fails, the resource will return an error. 134 | 135 | ## Error Handling 136 | 137 | Static resources can return the following errors: 138 | 139 | | HTTP Status | Description | 140 | |-------------|-------------| 141 | | 401 | Unauthorized - Invalid or missing API key | 142 | | 403 | Forbidden - API key does not have permission to access this resource | 143 | | 500 | Internal Server Error - An unexpected error occurred on the n8n server | 144 | 145 | ## Pagination 146 | 147 | Some resources that return large collections (like `n8n://workflows/list`) support pagination. The response includes a `pagination` object with information about whether more results are available. 148 | 149 | ## Best Practices 150 | 151 | - Use static resources for getting an overview of what's available in the n8n instance 152 | - Prefer static resources over tools when you only need to read data 153 | - Check the health resource before performing operations to ensure the n8n instance is operational 154 | - Use execution statistics to monitor the performance and reliability of your workflows 155 | -------------------------------------------------------------------------------- /docs/api/workflow-tools.md: -------------------------------------------------------------------------------- 1 | # Workflow Tools 2 | 3 | This page documents the tools available for managing n8n workflows. 4 | 5 | ## Overview 6 | 7 | Workflow tools allow AI assistants to manage n8n workflows, including creating, retrieving, updating, deleting, activating, and deactivating workflows. These tools provide a natural language interface to n8n's workflow management capabilities. 8 | 9 | ## Available Tools 10 | 11 | ### workflow_list 12 | 13 | Lists all workflows with optional filtering. 14 | 15 | **Input Schema:** 16 | 17 | ```json 18 | { 19 | "type": "object", 20 | "properties": { 21 | "active": { 22 | "type": "boolean", 23 | "description": "Filter workflows by active status" 24 | } 25 | }, 26 | "required": [] 27 | } 28 | ``` 29 | 30 | **Example Usage:** 31 | 32 | ```javascript 33 | // List all workflows 34 | const result = await useWorkflowList({}); 35 | 36 | // List only active workflows 37 | const activeWorkflows = await useWorkflowList({ active: true }); 38 | 39 | // List only inactive workflows 40 | const inactiveWorkflows = await useWorkflowList({ active: false }); 41 | ``` 42 | 43 | **Response:** 44 | 45 | ```javascript 46 | [ 47 | { 48 | "id": "1234abc", 49 | "name": "Test Workflow 1", 50 | "active": true, 51 | "createdAt": "2025-03-01T12:00:00.000Z", 52 | "updatedAt": "2025-03-02T14:30:00.000Z" 53 | }, 54 | { 55 | "id": "5678def", 56 | "name": "Test Workflow 2", 57 | "active": false, 58 | "createdAt": "2025-03-01T12:00:00.000Z", 59 | "updatedAt": "2025-03-12T10:15:00.000Z" 60 | } 61 | ] 62 | ``` 63 | 64 | ### workflow_get 65 | 66 | Retrieves a specific workflow by ID. 67 | 68 | **Input Schema:** 69 | 70 | ```json 71 | { 72 | "type": "object", 73 | "properties": { 74 | "id": { 75 | "type": "string", 76 | "description": "The ID of the workflow to retrieve" 77 | } 78 | }, 79 | "required": ["id"] 80 | } 81 | ``` 82 | 83 | **Example Usage:** 84 | 85 | ```javascript 86 | const workflow = await useWorkflowGet({ id: "1234abc" }); 87 | ``` 88 | 89 | **Response:** 90 | 91 | ```javascript 92 | { 93 | "id": "1234abc", 94 | "name": "Test Workflow 1", 95 | "active": true, 96 | "createdAt": "2025-03-01T12:00:00.000Z", 97 | "updatedAt": "2025-03-02T14:30:00.000Z", 98 | "nodes": [ 99 | // Detailed node configuration 100 | ], 101 | "connections": { 102 | // Connection configuration 103 | }, 104 | "settings": { 105 | // Workflow settings 106 | } 107 | } 108 | ``` 109 | 110 | ### workflow_create 111 | 112 | Creates a new workflow. 113 | 114 | **Input Schema:** 115 | 116 | ```json 117 | { 118 | "type": "object", 119 | "properties": { 120 | "name": { 121 | "type": "string", 122 | "description": "Name of the workflow" 123 | }, 124 | "nodes": { 125 | "type": "array", 126 | "description": "Array of node configurations" 127 | }, 128 | "connections": { 129 | "type": "object", 130 | "description": "Connection configuration" 131 | }, 132 | "active": { 133 | "type": "boolean", 134 | "description": "Whether the workflow should be active" 135 | }, 136 | "settings": { 137 | "type": "object", 138 | "description": "Workflow settings" 139 | } 140 | }, 141 | "required": ["name"] 142 | } 143 | ``` 144 | 145 | **Example Usage:** 146 | 147 | ```javascript 148 | const newWorkflow = await useWorkflowCreate({ 149 | name: "New Workflow", 150 | active: true, 151 | nodes: [ 152 | { 153 | "name": "Start", 154 | "type": "n8n-nodes-base.start", 155 | "position": [100, 200], 156 | "parameters": {} 157 | } 158 | ], 159 | connections: {} 160 | }); 161 | ``` 162 | 163 | **Response:** 164 | 165 | ```javascript 166 | { 167 | "id": "new123", 168 | "name": "New Workflow", 169 | "active": true, 170 | "createdAt": "2025-03-12T15:30:00.000Z", 171 | "updatedAt": "2025-03-12T15:30:00.000Z", 172 | "nodes": [ 173 | { 174 | "name": "Start", 175 | "type": "n8n-nodes-base.start", 176 | "position": [100, 200], 177 | "parameters": {} 178 | } 179 | ], 180 | "connections": {} 181 | } 182 | ``` 183 | 184 | ### workflow_update 185 | 186 | Updates an existing workflow. 187 | 188 | **Input Schema:** 189 | 190 | ```json 191 | { 192 | "type": "object", 193 | "properties": { 194 | "id": { 195 | "type": "string", 196 | "description": "ID of the workflow to update" 197 | }, 198 | "name": { 199 | "type": "string", 200 | "description": "New name for the workflow" 201 | }, 202 | "nodes": { 203 | "type": "array", 204 | "description": "Updated array of node configurations" 205 | }, 206 | "connections": { 207 | "type": "object", 208 | "description": "Updated connection configuration" 209 | }, 210 | "active": { 211 | "type": "boolean", 212 | "description": "Whether the workflow should be active" 213 | }, 214 | "settings": { 215 | "type": "object", 216 | "description": "Updated workflow settings" 217 | } 218 | }, 219 | "required": ["id"] 220 | } 221 | ``` 222 | 223 | **Example Usage:** 224 | 225 | ```javascript 226 | const updatedWorkflow = await useWorkflowUpdate({ 227 | id: "1234abc", 228 | name: "Updated Workflow Name", 229 | active: false 230 | }); 231 | ``` 232 | 233 | **Response:** 234 | 235 | ```javascript 236 | { 237 | "id": "1234abc", 238 | "name": "Updated Workflow Name", 239 | "active": false, 240 | "createdAt": "2025-03-01T12:00:00.000Z", 241 | "updatedAt": "2025-03-12T15:45:00.000Z", 242 | "nodes": [ 243 | // Existing node configuration 244 | ], 245 | "connections": { 246 | // Existing connection configuration 247 | } 248 | } 249 | ``` 250 | 251 | ### workflow_delete 252 | 253 | Deletes a workflow. 254 | 255 | **Input Schema:** 256 | 257 | ```json 258 | { 259 | "type": "object", 260 | "properties": { 261 | "id": { 262 | "type": "string", 263 | "description": "ID of the workflow to delete" 264 | } 265 | }, 266 | "required": ["id"] 267 | } 268 | ``` 269 | 270 | **Example Usage:** 271 | 272 | ```javascript 273 | await useWorkflowDelete({ id: "1234abc" }); 274 | ``` 275 | 276 | **Response:** 277 | 278 | ```javascript 279 | { 280 | "success": true 281 | } 282 | ``` 283 | 284 | ### workflow_activate 285 | 286 | Activates a workflow. 287 | 288 | **Input Schema:** 289 | 290 | ```json 291 | { 292 | "type": "object", 293 | "properties": { 294 | "id": { 295 | "type": "string", 296 | "description": "ID of the workflow to activate" 297 | } 298 | }, 299 | "required": ["id"] 300 | } 301 | ``` 302 | 303 | **Example Usage:** 304 | 305 | ```javascript 306 | const activatedWorkflow = await useWorkflowActivate({ id: "1234abc" }); 307 | ``` 308 | 309 | **Response:** 310 | 311 | ```javascript 312 | { 313 | "id": "1234abc", 314 | "name": "Test Workflow 1", 315 | "active": true, 316 | "createdAt": "2025-03-01T12:00:00.000Z", 317 | "updatedAt": "2025-03-12T16:00:00.000Z" 318 | } 319 | ``` 320 | 321 | ### workflow_deactivate 322 | 323 | Deactivates a workflow. 324 | 325 | **Input Schema:** 326 | 327 | ```json 328 | { 329 | "type": "object", 330 | "properties": { 331 | "id": { 332 | "type": "string", 333 | "description": "ID of the workflow to deactivate" 334 | } 335 | }, 336 | "required": ["id"] 337 | } 338 | ``` 339 | 340 | **Example Usage:** 341 | 342 | ```javascript 343 | const deactivatedWorkflow = await useWorkflowDeactivate({ id: "1234abc" }); 344 | ``` 345 | 346 | **Response:** 347 | 348 | ```javascript 349 | { 350 | "id": "1234abc", 351 | "name": "Test Workflow 1", 352 | "active": false, 353 | "createdAt": "2025-03-01T12:00:00.000Z", 354 | "updatedAt": "2025-03-12T16:15:00.000Z" 355 | } 356 | ``` 357 | 358 | ## Error Handling 359 | 360 | All workflow tools can return the following errors: 361 | 362 | | Error | Description | 363 | |-------|-------------| 364 | | Authentication Error | The provided API key is invalid or missing | 365 | | Not Found Error | The requested workflow does not exist | 366 | | Validation Error | The input parameters are invalid or incomplete | 367 | | Permission Error | The API key does not have permission to perform the operation | 368 | | Server Error | An unexpected error occurred on the n8n server | 369 | 370 | ## Best Practices 371 | 372 | - Use `workflow_list` to discover available workflows before performing operations 373 | - Validate workflow IDs before attempting to update or delete workflows 374 | - Check workflow status (active/inactive) before attempting activation/deactivation 375 | - Include only the necessary fields when updating workflows to avoid unintended changes 376 | -------------------------------------------------------------------------------- /docs/development/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | This document describes the architectural design of the n8n MCP Server. 4 | 5 | ## Overview 6 | 7 | The n8n MCP Server follows a layered architecture pattern that separates concerns and promotes maintainability. The main architectural layers are: 8 | 9 | 1. **Transport Layer**: Handles communication with AI assistants via the Model Context Protocol 10 | 2. **API Client Layer**: Interacts with the n8n API 11 | 3. **Tools Layer**: Implements executable operations as MCP tools 12 | 4. **Resources Layer**: Provides data access through URI-based resources 13 | 5. **Configuration Layer**: Manages environment variables and server settings 14 | 6. **Error Handling Layer**: Provides consistent error management and reporting 15 | 16 | ## System Components 17 | 18 | ![Architecture Diagram](../images/architecture.png.placeholder) 19 | 20 | ### Entry Point 21 | 22 | The server entry point is defined in `src/index.ts`. This file: 23 | 24 | 1. Initializes the configuration from environment variables 25 | 2. Creates and configures the MCP server instance 26 | 3. Registers tool and resource handlers 27 | 4. Connects to the transport layer (typically stdio) 28 | 29 | ### Configuration 30 | 31 | The configuration layer (`src/config/`) handles: 32 | 33 | - Loading environment variables 34 | - Validating required configuration 35 | - Providing typed access to configuration values 36 | 37 | The main configuration component is the `Environment` class, which validates and manages environment variables like `N8N_API_URL` and `N8N_API_KEY`. 38 | 39 | ### API Client 40 | 41 | The API client layer (`src/api/`) provides a clean interface for interacting with the n8n API. It includes: 42 | 43 | - `N8nClient`: The main client that encapsulates communication with n8n 44 | - API-specific functionality divided by resource type (workflows, executions) 45 | - Authentication handling using the n8n API key 46 | 47 | The client uses Axios for HTTP requests and includes error handling specific to the n8n API responses. 48 | 49 | ### MCP Tools 50 | 51 | The tools layer (`src/tools/`) implements the executable operations exposed to AI assistants. Each tool follows a common pattern: 52 | 53 | 1. A tool definition that specifies name, description, and input schema 54 | 2. A handler function that processes input parameters and executes the operation 55 | 3. Error handling for validation and execution errors 56 | 57 | Tools are categorized by resource type: 58 | 59 | - Workflow tools: Create, list, update, delete, activate, and deactivate workflows 60 | - Execution tools: Run, list, and manage workflow executions 61 | 62 | Each tool is designed to be independently testable and maintains a clean separation of concerns. 63 | 64 | ### MCP Resources 65 | 66 | The resources layer (`src/resources/`) provides data access through URI-based templates. Resources are divided into two categories: 67 | 68 | 1. **Static Resources** (`src/resources/static/`): Fixed resources like workflow listings 69 | 2. **Dynamic Resources** (`src/resources/dynamic/`): Parameterized resources like specific workflow details 70 | 71 | Each resource implements: 72 | - URI pattern matching 73 | - Content retrieval 74 | - Error handling 75 | - Response formatting 76 | 77 | ### Error Handling 78 | 79 | The error handling layer (`src/errors/`) provides consistent error management across the server. It includes: 80 | 81 | - Custom error types that map to MCP error codes 82 | - Error translation functions to convert n8n API errors to MCP errors 83 | - Common error patterns and handling strategies 84 | 85 | ## Data Flow 86 | 87 | A typical data flow through the system: 88 | 89 | 1. AI assistant sends a request via stdin to the MCP server 90 | 2. Server routes the request to the appropriate handler based on the request type 91 | 3. Handler validates input and delegates to the appropriate tool or resource 92 | 4. Tool/resource uses the n8n API client to interact with n8n 93 | 5. Response is processed, formatted, and returned via stdout 94 | 6. AI assistant receives and processes the response 95 | 96 | ## Key Design Principles 97 | 98 | ### 1. Separation of Concerns 99 | 100 | Each component has a single responsibility, making the codebase easier to understand, test, and extend. 101 | 102 | ### 2. Type Safety 103 | 104 | TypeScript interfaces and types are used extensively to ensure type safety and provide better developer experience. 105 | 106 | ### 3. Error Handling 107 | 108 | Comprehensive error handling ensures that errors are caught at the appropriate level and translated into meaningful messages for AI assistants. 109 | 110 | ### 4. Testability 111 | 112 | The architecture supports unit testing by keeping components loosely coupled and maintaining clear boundaries between layers. 113 | 114 | ### 5. Extensibility 115 | 116 | New tools and resources can be added without modifying existing code, following the open-closed principle. 117 | 118 | ## Implementation Patterns 119 | 120 | ### Factory Pattern 121 | 122 | Used for creating client instances and tool handlers based on configuration. 123 | 124 | ### Adapter Pattern 125 | 126 | The n8n API client adapts the n8n API to the internal representation used by the server. 127 | 128 | ### Strategy Pattern 129 | 130 | Different resource handlers implement a common interface but provide different strategies for retrieving and formatting data. 131 | 132 | ### Decorator Pattern 133 | 134 | Used to add cross-cutting concerns like logging and error handling to base functionality. 135 | 136 | ## Core Files and Their Purposes 137 | 138 | | File | Purpose | 139 | |------|---------| 140 | | `src/index.ts` | Main entry point, initializes and configures the server | 141 | | `src/config/environment.ts` | Manages environment variables and configuration | 142 | | `src/api/n8n-client.ts` | Main client for interacting with the n8n API | 143 | | `src/tools/workflow/handler.ts` | Handles workflow-related tool requests | 144 | | `src/tools/execution/handler.ts` | Handles execution-related tool requests | 145 | | `src/resources/index.ts` | Registers and manages resource handlers | 146 | | `src/resources/dynamic/workflow.ts` | Provides access to specific workflow resources | 147 | | `src/resources/static/workflows.ts` | Provides access to workflow listings | 148 | | `src/errors/index.ts` | Defines and manages error types and handling | 149 | 150 | ## Extension Points 151 | 152 | To extend the server with new capabilities: 153 | 154 | 1. **Adding a new tool**: Create a new handler in the appropriate category under `src/tools/` and register it in the main server setup 155 | 2. **Adding a new resource**: Create a new resource handler in `src/resources/` and register it in the resource manager 156 | 3. **Supporting new n8n API features**: Extend the API client in `src/api/` to support new API endpoints or features 157 | 158 | For detailed instructions on extending the server, see [Extending the Server](./extending.md). 159 | -------------------------------------------------------------------------------- /docs/development/index.md: -------------------------------------------------------------------------------- 1 | # Development Guide 2 | 3 | This section provides information for developers who want to understand, maintain, or extend the n8n MCP Server. 4 | 5 | ## Overview 6 | 7 | The n8n MCP Server is built with TypeScript and implements the Model Context Protocol (MCP) to provide AI assistants with access to n8n workflows and executions. This development guide covers the architecture, extension points, and testing procedures. 8 | 9 | ## Topics 10 | 11 | - [Architecture](./architecture.md): Overview of the codebase organization and design patterns 12 | - [Extending the Server](./extending.md): Guide to adding new tools and resources 13 | - [Testing](./testing.md): Information on testing procedures and writing tests 14 | 15 | ## Development Setup 16 | 17 | To set up a development environment: 18 | 19 | 1. Clone the repository: 20 | ```bash 21 | git clone https://github.com/yourusername/n8n-mcp-server.git 22 | cd n8n-mcp-server 23 | ``` 24 | 25 | 2. Install dependencies: 26 | ```bash 27 | npm install 28 | ``` 29 | 30 | 3. Create a `.env` file for local development: 31 | ```bash 32 | cp .env.example .env 33 | # Edit the .env file with your n8n API credentials 34 | ``` 35 | 36 | 4. Start the development server: 37 | ```bash 38 | npm run dev 39 | ``` 40 | 41 | This will compile the TypeScript code in watch mode, allowing you to make changes and see them take effect immediately. 42 | 43 | ## Project Structure 44 | 45 | The project follows a modular structure: 46 | 47 | ``` 48 | n8n-mcp-server/ 49 | ├── src/ # Source code 50 | │ ├── api/ # API client for n8n 51 | │ ├── config/ # Configuration and environment settings 52 | │ ├── errors/ # Error handling 53 | │ ├── resources/ # MCP resources implementation 54 | │ │ ├── static/ # Static resources 55 | │ │ └── dynamic/ # Dynamic (parameterized) resources 56 | │ ├── tools/ # MCP tools implementation 57 | │ │ ├── workflow/ # Workflow management tools 58 | │ │ └── execution/ # Execution management tools 59 | │ ├── types/ # TypeScript type definitions 60 | │ └── utils/ # Utility functions 61 | ├── tests/ # Test files 62 | │ ├── unit/ # Unit tests 63 | │ ├── integration/ # Integration tests 64 | │ └── e2e/ # End-to-end tests 65 | └── build/ # Compiled output 66 | ``` 67 | 68 | ## Build and Distribution 69 | 70 | To build the project for distribution: 71 | 72 | ```bash 73 | npm run build 74 | ``` 75 | 76 | This will compile the TypeScript code to JavaScript in the `build` directory and make the executable script file. 77 | 78 | ## Development Workflow 79 | 80 | 1. Create a feature branch for your changes 81 | 2. Make your changes and ensure tests pass 82 | 3. Update documentation as needed 83 | 4. Submit a pull request 84 | 85 | For more detailed instructions on specific development tasks, see the linked guides. 86 | -------------------------------------------------------------------------------- /docs/examples/basic-examples.md: -------------------------------------------------------------------------------- 1 | # Basic Examples 2 | 3 | This page provides basic examples of using the n8n MCP Server with AI assistants. 4 | 5 | ## Listing Workflows 6 | 7 | ### User Prompt 8 | 9 | "Show me all the workflows in my n8n instance." 10 | 11 | ### Assistant Actions 12 | 13 | ```javascript 14 | // The assistant uses the workflow_list tool 15 | const result = await useMcpTool('n8n-mcp-server', 'workflow_list', {}); 16 | 17 | // The assistant formats and presents the results 18 | if (result.length === 0) { 19 | return "You don't have any workflows in your n8n instance yet."; 20 | } else { 21 | let response = "Here are your workflows:\n\n"; 22 | result.forEach(workflow => { 23 | response += `- ${workflow.name} (ID: ${workflow.id}) - ${workflow.active ? 'Active' : 'Inactive'}\n`; 24 | }); 25 | return response; 26 | } 27 | ``` 28 | 29 | ## Getting Workflow Details 30 | 31 | ### User Prompt 32 | 33 | "Tell me about my 'Email Processing' workflow." 34 | 35 | ### Assistant Actions 36 | 37 | ```javascript 38 | // First list all workflows to find the one with matching name 39 | const workflows = await useMcpTool('n8n-mcp-server', 'workflow_list', {}); 40 | 41 | // Find the workflow with a name containing "Email Processing" 42 | const emailWorkflow = workflows.find(workflow => 43 | workflow.name.toLowerCase().includes('email processing') 44 | ); 45 | 46 | if (!emailWorkflow) { 47 | return "I couldn't find a workflow named 'Email Processing'. Would you like to see a list of all your workflows?"; 48 | } 49 | 50 | // Get detailed information about the workflow 51 | const details = await useMcpTool('n8n-mcp-server', 'workflow_get', { 52 | id: emailWorkflow.id 53 | }); 54 | 55 | // Format and present the workflow details 56 | return ` 57 | # Email Processing Workflow 58 | 59 | **Status:** ${details.active ? 'Active' : 'Inactive'} 60 | **Created:** ${new Date(details.createdAt).toLocaleString()} 61 | **Last Updated:** ${new Date(details.updatedAt).toLocaleString()} 62 | 63 | ## Nodes (${details.nodes.length}) 64 | ${details.nodes.map(node => `- ${node.name} (${node.type})`).join('\n')} 65 | 66 | ## Settings 67 | ${Object.entries(details.settings || {}).map(([key, value]) => `- ${key}: ${value}`).join('\n')} 68 | `; 69 | ``` 70 | 71 | ## Executing a Workflow 72 | 73 | ### User Prompt 74 | 75 | "Run my 'Data Sync' workflow." 76 | 77 | ### Assistant Actions 78 | 79 | ```javascript 80 | // First list all workflows to find the one with matching name 81 | const workflows = await useMcpTool('n8n-mcp-server', 'workflow_list', {}); 82 | 83 | // Find the workflow with a name containing "Data Sync" 84 | const syncWorkflow = workflows.find(workflow => 85 | workflow.name.toLowerCase().includes('data sync') 86 | ); 87 | 88 | if (!syncWorkflow) { 89 | return "I couldn't find a workflow named 'Data Sync'. Would you like to see a list of all your workflows?"; 90 | } 91 | 92 | // Check if the workflow is active 93 | if (!syncWorkflow.active) { 94 | return "The 'Data Sync' workflow is currently inactive. Would you like me to activate it before running it?"; 95 | } 96 | 97 | // Execute the workflow 98 | const execution = await useMcpTool('n8n-mcp-server', 'execution_run', { 99 | workflowId: syncWorkflow.id 100 | }); 101 | 102 | return ` 103 | I've started the 'Data Sync' workflow. 104 | 105 | **Execution ID:** ${execution.executionId} 106 | **Status:** ${execution.status} 107 | **Started At:** ${new Date(execution.startedAt).toLocaleString()} 108 | 109 | You can check the execution status later by asking me about it. 110 | `; 111 | ``` 112 | 113 | ## Checking Execution Status 114 | 115 | ### User Prompt 116 | 117 | "Check the status of my last workflow execution." 118 | 119 | ### Assistant Actions 120 | 121 | ```javascript 122 | // Get recent executions (this example assumes we've stored the last execution ID in context) 123 | // In a real scenario, you might need to list recent executions first 124 | const lastExecutionId = "exec123"; // In practice, this would come from context or a previous response 125 | 126 | // Get execution details 127 | const execution = await useMcpTool('n8n-mcp-server', 'execution_get', { 128 | executionId: lastExecutionId 129 | }); 130 | 131 | // Format and present the execution status 132 | let response = ` 133 | # Execution Status for ${execution.workflowName} 134 | 135 | **Status:** ${execution.status} 136 | **Started:** ${new Date(execution.startedAt).toLocaleString()} 137 | `; 138 | 139 | if (execution.finishedAt) { 140 | response += `**Finished:** ${new Date(execution.finishedAt).toLocaleString()}\n`; 141 | response += `**Duration:** ${Math.round((new Date(execution.finishedAt) - new Date(execution.startedAt)) / 1000)} seconds\n`; 142 | } 143 | 144 | response += "\n## Execution Path\n"; 145 | execution.data.executionData.nodeExecutionOrder.forEach(nodeName => { 146 | response += `- ${nodeName}\n`; 147 | }); 148 | 149 | return response; 150 | ``` 151 | 152 | ## Activating/Deactivating a Workflow 153 | 154 | ### User Prompt 155 | 156 | "Deactivate my 'Daily Report' workflow." 157 | 158 | ### Assistant Actions 159 | 160 | ```javascript 161 | // First list all workflows to find the one with matching name 162 | const workflows = await useMcpTool('n8n-mcp-server', 'workflow_list', {}); 163 | 164 | // Find the workflow with a name containing "Daily Report" 165 | const reportWorkflow = workflows.find(workflow => 166 | workflow.name.toLowerCase().includes('daily report') 167 | ); 168 | 169 | if (!reportWorkflow) { 170 | return "I couldn't find a workflow named 'Daily Report'. Would you like to see a list of all your workflows?"; 171 | } 172 | 173 | // Check if the workflow is already inactive 174 | if (!reportWorkflow.active) { 175 | return "The 'Daily Report' workflow is already inactive."; 176 | } 177 | 178 | // Deactivate the workflow 179 | const result = await useMcpTool('n8n-mcp-server', 'workflow_deactivate', { 180 | id: reportWorkflow.id 181 | }); 182 | 183 | return ` 184 | I've deactivated the 'Daily Report' workflow. 185 | 186 | It was last updated on ${new Date(result.updatedAt).toLocaleString()}. 187 | 188 | You can reactivate it at any time by asking me to activate it. 189 | `; 190 | ``` 191 | 192 | ## Creating a Simple Workflow 193 | 194 | ### User Prompt 195 | 196 | "Create a new workflow that sends me a daily email reminder." 197 | 198 | ### Assistant Actions 199 | 200 | ```javascript 201 | // Create a new workflow 202 | const newWorkflow = await useMcpTool('n8n-mcp-server', 'workflow_create', { 203 | name: "Daily Email Reminder", 204 | active: false, // Start inactive until it's configured properly 205 | nodes: [ 206 | { 207 | name: "Schedule Trigger", 208 | type: "n8n-nodes-base.scheduleTrigger", 209 | position: [100, 300], 210 | parameters: { 211 | rule: { 212 | interval: [ 213 | { 214 | field: "hours", 215 | minuteInterval: 24 216 | } 217 | ] 218 | } 219 | } 220 | }, 221 | { 222 | name: "Send Email", 223 | type: "n8n-nodes-base.emailSend", 224 | position: [300, 300], 225 | parameters: { 226 | to: "{{$json.email}}", 227 | subject: "Daily Reminder", 228 | text: "This is your daily reminder!" 229 | } 230 | } 231 | ], 232 | connections: { 233 | "Schedule Trigger": { 234 | main: [ 235 | [ 236 | { 237 | node: "Send Email", 238 | type: "main", 239 | index: 0 240 | } 241 | ] 242 | ] 243 | } 244 | } 245 | }); 246 | 247 | return ` 248 | I've created a new workflow called "Daily Email Reminder". 249 | 250 | This workflow is currently **inactive** and needs configuration: 251 | 1. You'll need to enter your email address in the "Send Email" node 252 | 2. You might want to customize the schedule and email content 253 | 254 | You can view and edit this workflow in the n8n interface (ID: ${newWorkflow.id}), and then ask me to activate it when you're ready. 255 | `; 256 | ``` 257 | 258 | These examples demonstrate the basic operations you can perform with the n8n MCP Server. For more complex scenarios, see the [Advanced Scenarios](./advanced-scenarios.md) page. 259 | -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | # Usage Examples 2 | 3 | This section provides practical examples of using the n8n MCP Server with AI assistants. 4 | 5 | ## Overview 6 | 7 | The examples in this section demonstrate how AI assistants can interact with n8n workflows through the MCP server. They range from basic operations to complex integration scenarios. 8 | 9 | ## Examples Categories 10 | 11 | - [Basic Examples](./basic-examples.md): Simple examples covering fundamental operations like listing workflows, retrieving workflow details, and executing workflows. 12 | - [Advanced Scenarios](./advanced-scenarios.md): More complex examples showing how to chain operations, handle errors, and implement common workflow patterns. 13 | - [Integration Examples](./integration-examples.md): Examples of integrating the n8n MCP Server with different AI assistant platforms and other tools. 14 | 15 | ## How to Use These Examples 16 | 17 | The examples in this section show both: 18 | 19 | 1. **User Prompts**: What a user might ask an AI assistant to do 20 | 2. **Assistant Actions**: How the assistant would use the MCP tools and resources to accomplish the task 21 | 22 | You can use these examples as inspiration for your own interactions with the n8n MCP Server or as templates for building more complex workflows. 23 | -------------------------------------------------------------------------------- /docs/images/architecture.png.placeholder: -------------------------------------------------------------------------------- 1 | This is a placeholder for an architecture diagram. 2 | Replace this with an actual diagram showing the layered architecture of the n8n MCP Server, 3 | including the Transport Layer, API Client Layer, Tools Layer, Resources Layer, 4 | Configuration Layer, and Error Handling Layer. 5 | -------------------------------------------------------------------------------- /docs/images/n8n-api-key.png.placeholder: -------------------------------------------------------------------------------- 1 | This is a placeholder for a screenshot showing the n8n API Key creation process. 2 | When documenting the actual project, replace this with a real screenshot. 3 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # n8n MCP Server Documentation 2 | 3 | Welcome to the n8n MCP Server documentation. This documentation provides comprehensive information about setting up, configuring, and using the n8n MCP Server. 4 | 5 | ## Table of Contents 6 | 7 | - [Setup and Configuration](./setup/index.md) 8 | - [Installation](./setup/installation.md) 9 | - [Configuration](./setup/configuration.md) 10 | - [Troubleshooting](./setup/troubleshooting.md) 11 | 12 | - [API Reference](./api/index.md) 13 | - [Tools](./api/tools.md) 14 | - [Workflow Tools](./api/workflow-tools.md) 15 | - [Execution Tools](./api/execution-tools.md) 16 | - [Resources](./api/resources.md) 17 | - [Static Resources](./api/static-resources.md) 18 | - [Dynamic Resources](./api/dynamic-resources.md) 19 | 20 | - [Usage Examples](./examples/index.md) 21 | - [Basic Examples](./examples/basic-examples.md) 22 | - [Advanced Scenarios](./examples/advanced-scenarios.md) 23 | - [Integration Examples](./examples/integration-examples.md) 24 | 25 | - [Development](./development/index.md) 26 | - [Architecture](./development/architecture.md) 27 | - [Extending the Server](./development/extending.md) 28 | - [Testing](./development/testing.md) 29 | 30 | ## Quick Links 31 | 32 | - [GitHub Repository](https://github.com/yourusername/n8n-mcp-server) 33 | - [n8n Documentation](https://docs.n8n.io/) 34 | - [Model Context Protocol Documentation](https://modelcontextprotocol.github.io/) 35 | -------------------------------------------------------------------------------- /docs/setup/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration Guide 2 | 3 | This guide provides detailed information on configuring the n8n MCP Server. 4 | 5 | ## Environment Variables 6 | 7 | The n8n MCP Server is configured using environment variables, which can be set in a `.env` file or directly in your environment. 8 | 9 | ### Required Variables 10 | 11 | | Variable | Description | Example | 12 | |----------|-------------|---------| 13 | | `N8N_API_URL` | URL of the n8n API | `http://localhost:5678/api/v1` | 14 | | `N8N_API_KEY` | API key for authenticating with n8n | `n8n_api_...` | 15 | 16 | ### Optional Variables 17 | 18 | | Variable | Description | Default | Example | 19 | |----------|-------------|---------|---------| 20 | | `DEBUG` | Enable debug logging | `false` | `true` or `false` | 21 | 22 | ## Creating a .env File 23 | 24 | The simplest way to configure the server is to create a `.env` file in the directory where you'll run the server: 25 | 26 | ```bash 27 | # Copy the example .env file 28 | cp .env.example .env 29 | 30 | # Edit the .env file with your settings 31 | nano .env # or use any text editor 32 | ``` 33 | 34 | Example `.env` file: 35 | 36 | ```env 37 | # n8n MCP Server Environment Variables 38 | 39 | # Required: URL of the n8n API 40 | N8N_API_URL=http://localhost:5678/api/v1 41 | 42 | # Required: API key for authenticating with n8n 43 | N8N_API_KEY=your_n8n_api_key_here 44 | 45 | # Optional: Set to 'true' to enable debug logging 46 | DEBUG=false 47 | ``` 48 | 49 | ## Generating an n8n API Key 50 | 51 | To use the n8n MCP Server, you need an API key from your n8n instance: 52 | 53 | 1. Open your n8n instance in a browser 54 | 2. Go to **Settings** > **API** > **API Keys** 55 | 3. Click **Create** to create a new API key 56 | 4. Set appropriate **Scope** (recommended: `workflow:read workflow:write workflow:execute`) 57 | 5. Copy the key to your `.env` file 58 | 59 | ![Creating an n8n API Key](../images/n8n-api-key.png) 60 | 61 | ## Server Connection Options 62 | 63 | By default, the n8n MCP Server listens on `stdin` and `stdout` for Model Context Protocol communications. This is the format expected by AI assistants using the MCP protocol. 64 | 65 | ## Configuring AI Assistants 66 | 67 | To use the n8n MCP Server with AI assistants, you need to register it with your AI assistant platform. The exact method depends on the platform you're using. 68 | 69 | ### Using the MCP Installer 70 | 71 | If you're using Claude or another assistant that supports the MCP Installer, you can register the server with: 72 | 73 | ```bash 74 | # Install the MCP Installer 75 | npx @anaisbetts/mcp-installer 76 | 77 | # Register the server (if installed globally) 78 | install_repo_mcp_server n8n-mcp-server 79 | 80 | # Or register from a local installation 81 | install_local_mcp_server path/to/n8n-mcp-server 82 | ``` 83 | 84 | ### Manual Configuration 85 | 86 | For platforms without an installer, you'll need to configure the connection according to the platform's documentation. Typically, this involves: 87 | 88 | 1. Specifying the path to the executable 89 | 2. Setting environment variables for the server 90 | 3. Configuring response formatting 91 | 92 | ## Verifying Configuration 93 | 94 | To verify your configuration: 95 | 96 | 1. Start the server 97 | 2. Open your AI assistant 98 | 3. Try a simple command like "List all workflows in n8n" 99 | 100 | If configured correctly, the assistant should be able to retrieve and display your workflows. 101 | 102 | ## Troubleshooting 103 | 104 | If you encounter issues with your configuration, check: 105 | 106 | - The `.env` file is in the correct location 107 | - The n8n API URL is accessible from where the server is running 108 | - The API key has the correct permissions 109 | - Any firewalls or network restrictions that might block connections 110 | 111 | For more specific issues, see the [Troubleshooting](./troubleshooting.md) guide. 112 | -------------------------------------------------------------------------------- /docs/setup/index.md: -------------------------------------------------------------------------------- 1 | # Setup and Configuration 2 | 3 | This section covers everything you need to know to set up and configure the n8n MCP Server. 4 | 5 | ## Topics 6 | 7 | - [Installation](./installation.md): Instructions for installing the n8n MCP Server from npm or from source. 8 | - [Configuration](./configuration.md): Information on configuring the server, including environment variables and n8n API setup. 9 | - [Troubleshooting](./troubleshooting.md): Solutions to common issues you might encounter. 10 | 11 | ## Quick Start 12 | 13 | For a quick start, follow these steps: 14 | 15 | 1. Install the server: `npm install -g @leonardsellem/n8n-mcp-server` 16 | 2. Create a `.env` file with your n8n API URL and API key 17 | 3. Run the server: `n8n-mcp-server` 18 | 4. Register the server with your AI assistant platform 19 | -------------------------------------------------------------------------------- /docs/setup/installation.md: -------------------------------------------------------------------------------- 1 | # Installation Guide 2 | 3 | This guide covers the installation process for the n8n MCP Server. 4 | 5 | ## Prerequisites 6 | 7 | - Before installing the n8n MCP Server, ensure you have: 8 | 9 | - Node.js 20 or later installed 10 | - An n8n instance running and accessible via HTTP/HTTPS 11 | - API access enabled on your n8n instance 12 | - An API key with appropriate permissions (see [Configuration](./configuration.md)) 13 | 14 | ## Option 1: Install from npm (Recommended) 15 | 16 | The easiest way to install the n8n MCP Server is from npm: 17 | 18 | ```bash 19 | npm install -g n8n-mcp-server 20 | ``` 21 | 22 | This will install the server globally, making the `n8n-mcp-server` command available in your terminal. 23 | 24 | ## Option 2: Install from Source 25 | 26 | For development purposes or to use the latest features, you can install from source: 27 | 28 | ```bash 29 | # Clone the repository 30 | git clone https://github.com/yourusername/n8n-mcp-server.git 31 | cd n8n-mcp-server 32 | 33 | # Install dependencies 34 | npm install 35 | 36 | # Build the project 37 | npm run build 38 | 39 | # Optional: Install globally 40 | npm install -g . 41 | ``` 42 | 43 | ## Verifying Installation 44 | 45 | Once installed, you can verify the installation by running: 46 | 47 | ```bash 48 | n8n-mcp-server --version 49 | ``` 50 | 51 | This should display the version number of the installed n8n MCP Server. 52 | 53 | ## Next Steps 54 | 55 | After installation, you'll need to: 56 | 57 | 1. [Configure the server](./configuration.md) by setting up environment variables 58 | 2. Run the server 59 | 3. Register the server with your AI assistant platform 60 | 61 | ## Upgrading 62 | 63 | To upgrade a global installation from npm: 64 | 65 | ```bash 66 | npm update -g n8n-mcp-server 67 | ``` 68 | 69 | To upgrade a source installation: 70 | 71 | ```bash 72 | # Navigate to the repository directory 73 | cd n8n-mcp-server 74 | 75 | # Pull the latest changes 76 | git pull 77 | 78 | # Install dependencies and rebuild 79 | npm install 80 | npm run build 81 | 82 | # If installed globally, reinstall 83 | npm install -g . 84 | -------------------------------------------------------------------------------- /docs/setup/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting Guide 2 | 3 | This guide addresses common issues you might encounter when setting up and using the n8n MCP Server. 4 | 5 | ## Connection Issues 6 | 7 | ### Cannot Connect to n8n API 8 | 9 | **Symptoms:** 10 | - Error messages mentioning "Connection refused" or "Cannot connect to n8n API" 11 | - Timeout errors when trying to use MCP tools 12 | 13 | **Possible Solutions:** 14 | 1. **Verify n8n is running:** 15 | - Ensure your n8n instance is running and accessible 16 | - Try accessing the n8n URL in a browser 17 | 18 | 2. **Check n8n API URL:** 19 | - Verify the `N8N_API_URL` in your `.env` file 20 | - Make sure it includes the full path (e.g., `http://localhost:5678/api/v1`) 21 | - Check for typos or incorrect protocol (http vs https) 22 | 23 | 3. **Network Configuration:** 24 | - If running on a different machine, ensure there are no firewall rules blocking access 25 | - Check if n8n is configured to allow remote connections 26 | 27 | 4. **HTTPS/SSL Issues:** 28 | - If using HTTPS, ensure certificates are valid 29 | - For self-signed certificates, you may need to set up additional configuration 30 | 31 | ### Authentication Failures 32 | 33 | **Symptoms:** 34 | - "Authentication failed" or "Invalid API key" messages 35 | - 401 or 403 HTTP status codes 36 | 37 | **Possible Solutions:** 38 | 1. **Verify API Key:** 39 | - Check that the `N8N_API_KEY` in your `.env` file matches the one in n8n 40 | - Create a new API key if necessary 41 | 42 | 2. **Check API Key Permissions:** 43 | - Ensure the API key has appropriate scopes/permissions 44 | - Required scopes: `workflow:read workflow:write workflow:execute` 45 | 46 | 3. **n8n API Settings:** 47 | - Verify that API access is enabled in n8n settings 48 | - Check if there are IP restrictions on API access 49 | 50 | ## MCP Server Issues 51 | 52 | ### Server Crashes or Exits Unexpectedly 53 | 54 | **Symptoms:** 55 | - The MCP server stops running unexpectedly 56 | - Error messages in logs or console output 57 | 58 | **Possible Solutions:** 59 | 1. **Check Node.js Version:** 60 | - Ensure you're using Node.js 20 or later 61 | - Check with `node --version` 62 | 63 | 2. **Check Environment Variables:** 64 | - Ensure all required environment variables are set 65 | - Verify the format of the environment variables 66 | 67 | 3. **View Debug Logs:** 68 | - Set `DEBUG=true` in your `.env` file 69 | - Check the console output for detailed error messages 70 | 71 | 4. **Memory Issues:** 72 | - If running on a system with limited memory, increase available memory 73 | - Check for memory leaks or high consumption patterns 74 | 75 | ### AI Assistant Cannot Communicate with MCP Server 76 | 77 | **Symptoms:** 78 | - AI assistant reports it cannot connect to the MCP server 79 | - Tools are not available in the assistant interface 80 | 81 | **Possible Solutions:** 82 | 1. **Verify Server Registration:** 83 | - Ensure the server is properly registered with your AI assistant platform 84 | - Check the configuration settings for the MCP server in your assistant 85 | 86 | 2. **Server Running Check:** 87 | - Verify the MCP server is running 88 | - Check that it was started with the correct environment 89 | 90 | 3. **Restart Components:** 91 | - Restart the MCP server 92 | - Refresh the AI assistant interface 93 | - If using a managed AI assistant, check platform status 94 | 95 | ## Tool-Specific Issues 96 | 97 | ### Workflow Operations Fail 98 | 99 | **Symptoms:** 100 | - Cannot list, create, or update workflows 101 | - Error messages about missing permissions 102 | 103 | **Possible Solutions:** 104 | 1. **API Key Scope:** 105 | - Ensure your API key has `workflow:read` and `workflow:write` permissions 106 | - Create a new key with appropriate permissions if needed 107 | 108 | 2. **n8n Version:** 109 | - Check if your n8n version supports all the API endpoints being used 110 | - Update n8n to the latest version if possible 111 | 112 | 3. **Workflow Complexity:** 113 | - Complex workflows with custom nodes may not work correctly 114 | - Try with simpler workflows to isolate the issue 115 | 116 | ### Execution Operations Fail 117 | 118 | **Symptoms:** 119 | - Cannot execute workflows or retrieve execution data 120 | - Execution starts but fails to complete 121 | 122 | **Possible Solutions:** 123 | 1. **API Key Scope:** 124 | - Ensure your API key has the `workflow:execute` permission 125 | - Create a new key with appropriate permissions if needed 126 | 127 | 2. **Workflow Status:** 128 | - Check if the target workflow is active 129 | - Verify the workflow executes correctly in the n8n interface 130 | 131 | 3. **Workflow Inputs:** 132 | - Ensure all required inputs for workflow execution are provided 133 | - Check the format of input data 134 | 135 | ## Getting More Help 136 | 137 | If you're still experiencing issues after trying these troubleshooting steps: 138 | 139 | 1. **Check GitHub Issues:** 140 | - Look for similar issues in the [GitHub repository](https://github.com/yourusername/n8n-mcp-server/issues) 141 | - Create a new issue with detailed information about your problem 142 | 143 | 2. **Submit Logs:** 144 | - Enable debug logging with `DEBUG=true` 145 | - Include relevant logs when seeking help 146 | 147 | 3. **Community Support:** 148 | - Ask in the n8n community forums 149 | - Check MCP-related discussion groups 150 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Use commonjs style export 3 | preset: 'ts-jest/presets/default-esm', // Using ESM preset for ts-jest 4 | testEnvironment: 'node', 5 | // transform: { // Removed to let ts-jest handle transformation via preset 6 | // '^.+\\.tsx?$': 'babel-jest', 7 | // }, 8 | // globals: { // Deprecated way to configure ts-jest 9 | // 'ts-jest': { 10 | // useESM: true, 11 | // } 12 | // }, 13 | transform: { 14 | '^.+\\.tsx?$': ['ts-jest', { 15 | useESM: true, 16 | tsconfig: 'tests/tsconfig.json', // Point to the tsconfig in the tests directory 17 | // babelConfig: true, // If babel.config.cjs is still needed for some features not in ts-jest 18 | }], 19 | }, 20 | // Allow src and test folders to resolve imports properly 21 | moduleNameMapper: { 22 | '^(\\.{1,2}/.*)\\.js$': '$1', 23 | // For ESM, Jest might need help resolving module paths if they are not fully specified 24 | // or if there are conditions in package.json exports. 25 | // Example: '(@modelcontextprotocol/sdk)(.*)': '/node_modules/$1/dist$2.js' 26 | // This line below is a guess, might need adjustment or might not be needed. 27 | // '^(@modelcontextprotocol/sdk/.*)\\.js$': '/node_modules/$1.js', 28 | }, 29 | // Handle the modelcontextprotocol SDK 30 | // Default is /node_modules/, so we want to NOT transform anything in node_modules 31 | transformIgnorePatterns: [ 32 | "node_modules/" 33 | ], 34 | collectCoverage: true, 35 | coverageDirectory: 'coverage', 36 | coverageReporters: ['text', 'lcov'], 37 | testMatch: ['**/tests/**/*.test.ts'], 38 | verbose: true, 39 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 40 | setupFilesAfterEnv: ['/tests/test-setup.ts'] 41 | }; 42 | -------------------------------------------------------------------------------- /manual_verify_update.mjs: -------------------------------------------------------------------------------- 1 | import { N8nApiClient } from './build/api/client.js'; 2 | import { getEnvConfig, loadEnvironmentVariables } from './build/config/environment.js'; 3 | import { N8nApiError } from './build/errors/index.js'; 4 | 5 | async function main() { 6 | console.log('Attempting to load environment configuration...'); 7 | loadEnvironmentVariables(); // Load .env file if present and needed 8 | const config = getEnvConfig(); // Get validated config object 9 | 10 | if (!config.n8nApiUrl || !config.n8nApiKey) { 11 | console.error('Error: N8N_API_URL and/or N8N_API_KEY are not set.'); 12 | console.error('Please set these environment variables to run this verification.'); 13 | process.exit(1); 14 | } 15 | 16 | console.log(`N8N API URL: ${config.n8nApiUrl}`); 17 | console.log('N8N API Key: EXISTS (not printing value)'); 18 | config.debug = true; // Enable debug logging for the client 19 | 20 | const client = new N8nApiClient(config); 21 | 22 | try { 23 | console.log('Checking connectivity...'); 24 | await client.checkConnectivity(); 25 | console.log('Connectivity check successful.'); 26 | 27 | console.log('Fetching workflows to find one to update...'); 28 | let workflows = await client.getWorkflows(); 29 | 30 | if (!workflows || workflows.length === 0) { 31 | console.log('No workflows found. Cannot test update.'); 32 | // If no workflows, try to create one, then update it. 33 | console.log('Attempting to create a dummy workflow for testing update...'); 34 | const newWorkflowData = { 35 | name: 'Test Workflow for Update Verification', 36 | nodes: [ 37 | { 38 | parameters: {}, 39 | id: '0743771a-291a-4763-ab03-570546a05f70', 40 | name: 'When Webhook Called', 41 | type: 'n8n-nodes-base.webhook', 42 | typeVersion: 1, 43 | position: [480, 300], 44 | webhookId: 'test-webhook-id' 45 | } 46 | ], 47 | connections: {}, 48 | active: false, 49 | settings: { executionOrder: 'v1' }, 50 | tags: [] // Intentionally include tags to see if they are filtered 51 | }; 52 | let createdWorkflow; 53 | try { 54 | createdWorkflow = await client.createWorkflow(newWorkflowData); 55 | console.log(`Successfully created workflow with ID: ${createdWorkflow.id}, Name: ${createdWorkflow.name}`); 56 | workflows.push(createdWorkflow); // Add to list to proceed with update 57 | } catch (createError) { 58 | console.error('Failed to create a dummy workflow:', createError); 59 | if (createError instanceof N8nApiError) { 60 | console.error(`N8nApiError Details: Status ${createError.status}, Message: ${createError.message}`); 61 | if (createError.cause) console.error('Cause:', createError.cause); 62 | } 63 | process.exit(1); 64 | } 65 | } 66 | 67 | if (!workflows || workflows.length === 0) { 68 | console.log('Still no workflows found after attempting creation. Cannot test update.'); 69 | process.exit(0); // Exit gracefully, can't test. 70 | } 71 | 72 | const workflowToUpdate = workflows[0]; 73 | const originalName = workflowToUpdate.name; 74 | const newName = `Updated - ${originalName} - ${Date.now()}`; 75 | 76 | console.log(`Attempting to update workflow ID: ${workflowToUpdate.id}, Original Name: "${originalName}"`); 77 | console.log(`New Name will be: "${newName}"`); 78 | 79 | // Construct the update payload. Include fields that should be stripped. 80 | const updatePayload = { 81 | ...workflowToUpdate, // Spread the existing workflow 82 | name: newName, // Change the name 83 | // Explicitly include fields that should be removed by updateWorkflow 84 | id: workflowToUpdate.id, 85 | createdAt: workflowToUpdate.createdAt || '2023-01-01T00:00:00Z', 86 | updatedAt: workflowToUpdate.updatedAt || '2023-01-01T00:00:00Z', 87 | tags: workflowToUpdate.tags || [{ id: 'testtag', name: 'Test Tag' }] 88 | }; 89 | 90 | // Remove nodes and connections if they are very large to avoid log clutter 91 | // and potential issues if they are not meant to be sent in full for simple updates. 92 | // The PR is about filtering metadata, not changing core workflow structure via update. 93 | delete updatePayload.nodes; 94 | delete updatePayload.connections; 95 | 96 | 97 | console.log('Workflow object before sending to updateWorkflow (with fields that should be stripped):', JSON.stringify(updatePayload, null, 2)); 98 | 99 | const updatedWorkflow = await client.updateWorkflow(workflowToUpdate.id, updatePayload); 100 | 101 | if (updatedWorkflow.name === newName) { 102 | console.log(`SUCCESS: Workflow updated successfully! New name: "${updatedWorkflow.name}"`); 103 | console.log('Received updated workflow object:', JSON.stringify(updatedWorkflow, null, 2)); 104 | 105 | // Optional: try to revert the name 106 | try { 107 | console.log(`Attempting to revert name for workflow ID: ${workflowToUpdate.id} to "${originalName}"`); 108 | await client.updateWorkflow(workflowToUpdate.id, { name: originalName }); 109 | console.log(`Successfully reverted name to "${originalName}"`); 110 | } catch (revertError) { 111 | console.error('Failed to revert workflow name, but the main update test passed:', revertError); 112 | } 113 | 114 | } else { 115 | console.error(`FAILURE: Workflow name was not updated as expected. Expected: "${newName}", Got: "${updatedWorkflow.name}"`); 116 | process.exit(1); 117 | } 118 | 119 | } catch (error) { 120 | console.error('Manual verification script failed:'); 121 | if (error instanceof N8nApiError) { 122 | console.error(`N8nApiError Details: Status ${error.status}, Message: ${error.message}`); 123 | if (error.cause) console.error('Cause:', error.cause); 124 | } else if (error.isAxiosError) { 125 | console.error('Axios Error:', error.message); 126 | if (error.response) { 127 | console.error('Response Status:', error.response.status); 128 | console.error('Response Data:', JSON.stringify(error.response.data, null, 2)); 129 | } 130 | } else { 131 | console.error(error); 132 | } 133 | process.exit(1); 134 | } 135 | } 136 | 137 | main(); 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@leonardsellem/n8n-mcp-server", 3 | "version": "0.1.3", 4 | "description": "Model Context Protocol (MCP) server for n8n workflow automation", 5 | "main": "build/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc && chmod +x build/index.js", 9 | "start": "node build/index.js", 10 | "dev": "tsc -w", 11 | "lint": "eslint --ext .ts src/", 12 | "test": "node --experimental-vm-modules run-tests.js", 13 | "test:watch": "node --experimental-vm-modules run-tests.js --watch", 14 | "test:coverage": "node --experimental-vm-modules run-tests.js --coverage", 15 | "prepare": "npm run build" 16 | }, 17 | "bin": { 18 | "n8n-mcp-server": "build/index.js" 19 | }, 20 | "keywords": [ 21 | "mcp", 22 | "n8n", 23 | "workflow", 24 | "automation", 25 | "ai" 26 | ], 27 | "author": "Leonard Sellem (https://sellem.me)", 28 | "license": "MIT", 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/leonardsellem/n8n-mcp-server.git" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/leonardsellem/n8n-mcp-server/issues" 35 | }, 36 | "homepage": "https://github.com/leonardsellem/n8n-mcp-server#readme", 37 | "files": [ 38 | "build", 39 | "README.md", 40 | "LICENSE", 41 | "package.json" 42 | ], 43 | "dependencies": { 44 | "@modelcontextprotocol/sdk": "^0.7.0", 45 | "axios": "^1.6.2", 46 | "dotenv": "^16.3.1", 47 | "find-config": "^1.0.0" 48 | }, 49 | "devDependencies": { 50 | "@babel/core": "^7.26.10", 51 | "@babel/plugin-transform-modules-commonjs": "^7.26.3", 52 | "@babel/preset-env": "^7.26.9", 53 | "@babel/preset-typescript": "^7.26.0", 54 | "@types/find-config": "^1.0.4", 55 | "@types/jest": "^29.5.14", 56 | "@types/node": "^20.10.0", 57 | "@typescript-eslint/eslint-plugin": "^6.13.1", 58 | "@typescript-eslint/parser": "^6.13.1", 59 | "babel-jest": "^29.7.0", 60 | "eslint": "^8.54.0", 61 | "jest": "^29.7.0", 62 | "ts-jest": "^29.1.1", 63 | "typescript": "^5.3.2" 64 | }, 65 | "engines": { 66 | "node": ">=18.0.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==3.3.8 2 | certifi==2025.1.31 3 | charset-normalizer==3.4.1 4 | colorama==0.4.6 5 | dill==0.3.9 6 | filelock==3.17.0 7 | fsspec==2025.2.0 8 | huggingface-hub==0.29.2 9 | idna==3.10 10 | isort==6.0.1 11 | Jinja2==3.1.6 12 | joblib==1.4.2 13 | MarkupSafe==3.0.2 14 | mccabe==0.7.0 15 | mpmath==1.3.0 16 | networkx==3.4.2 17 | numpy==2.2.3 18 | packaging==24.2 19 | pillow==11.1.0 20 | platformdirs==4.3.6 21 | pylint==3.3.4 22 | PyYAML==6.0.2 23 | regex==2024.11.6 24 | requests==2.32.3 25 | safetensors==0.5.3 26 | scikit-learn==1.6.1 27 | scipy==1.15.2 28 | sentence-transformers==3.4.1 29 | setuptools==75.8.2 30 | sympy==1.13.1 31 | threadpoolctl==3.5.0 32 | tokenizers==0.21.0 33 | tomlkit==0.13.2 34 | torch==2.6.0 35 | tqdm==4.67.1 36 | transformers==4.49.0 37 | typing_extensions==4.12.2 38 | urllib3==2.3.0 39 | -------------------------------------------------------------------------------- /run-tests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Runner Script 3 | * 4 | * This script provides a more reliable way to run Jest tests with proper 5 | * ESM support and error handling. 6 | */ 7 | 8 | import { spawn } from 'child_process'; 9 | import { fileURLToPath } from 'url'; 10 | import { dirname, resolve } from 'path'; 11 | 12 | // Get the directory of the current module 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = dirname(__filename); 15 | 16 | // Set NODE_OPTIONS to ensure proper ESM support 17 | process.env.NODE_OPTIONS = '--experimental-vm-modules'; 18 | 19 | console.log('🧪 Running tests for n8n MCP Server...'); 20 | 21 | // Get command line arguments to pass to Jest 22 | const args = process.argv.slice(2); 23 | const jestArgs = ['--config', './jest.config.cjs', ...args]; 24 | 25 | // Spawn Jest process 26 | const jestProcess = spawn('node_modules/.bin/jest', jestArgs, { 27 | stdio: 'inherit', 28 | cwd: __dirname, 29 | env: { ...process.env, NODE_ENV: 'test' } 30 | }); 31 | 32 | // Handle process events 33 | jestProcess.on('error', (error) => { 34 | console.error('Error running tests:', error); 35 | process.exit(1); 36 | }); 37 | 38 | jestProcess.on('close', (code) => { 39 | if (code !== 0) { 40 | console.error(`Test process exited with code ${code}`); 41 | process.exit(code); 42 | } 43 | console.log('✅ Tests completed successfully'); 44 | }); 45 | -------------------------------------------------------------------------------- /src/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonardsellem/n8n-mcp-server/d274146fb0a0de0fe15dcfb6d4c4d2f06e4b496a/src/.gitkeep -------------------------------------------------------------------------------- /src/api/n8n-client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * n8n API Client Interface 3 | * 4 | * This module defines interfaces and types for the n8n API client. 5 | */ 6 | 7 | import { N8nApiClient } from './client.js'; 8 | import { EnvConfig } from '../config/environment.js'; 9 | import { Workflow, Execution } from '../types/index.js'; 10 | 11 | /** 12 | * n8n API service - provides functions for interacting with n8n API 13 | */ 14 | export class N8nApiService { 15 | private client: N8nApiClient; 16 | 17 | /** 18 | * Create a new n8n API service 19 | * 20 | * @param config Environment configuration 21 | */ 22 | constructor(config: EnvConfig) { 23 | this.client = new N8nApiClient(config); 24 | } 25 | 26 | /** 27 | * Check connectivity to the n8n API 28 | */ 29 | async checkConnectivity(): Promise { 30 | return this.client.checkConnectivity(); 31 | } 32 | 33 | /** 34 | * Get all workflows from n8n 35 | * 36 | * @returns Array of workflow objects 37 | */ 38 | async getWorkflows(): Promise { 39 | return this.client.getWorkflows(); 40 | } 41 | 42 | /** 43 | * Get a specific workflow by ID 44 | * 45 | * @param id Workflow ID 46 | * @returns Workflow object 47 | */ 48 | async getWorkflow(id: string): Promise { 49 | return this.client.getWorkflow(id); 50 | } 51 | 52 | /** 53 | * Execute a workflow by ID 54 | * 55 | * @param id Workflow ID 56 | * @param data Optional data to pass to the workflow 57 | * @returns Execution result 58 | */ 59 | async executeWorkflow(id: string, data?: Record): Promise { 60 | return this.client.executeWorkflow(id, data); 61 | } 62 | 63 | /** 64 | * Create a new workflow 65 | * 66 | * @param workflow Workflow object to create 67 | * @returns Created workflow 68 | */ 69 | async createWorkflow(workflow: Record): Promise { 70 | return this.client.createWorkflow(workflow); 71 | } 72 | 73 | /** 74 | * Update an existing workflow 75 | * 76 | * @param id Workflow ID 77 | * @param workflow Updated workflow object 78 | * @returns Updated workflow 79 | */ 80 | async updateWorkflow(id: string, workflow: Record): Promise { 81 | return this.client.updateWorkflow(id, workflow); 82 | } 83 | 84 | /** 85 | * Delete a workflow 86 | * 87 | * @param id Workflow ID 88 | * @returns Deleted workflow or success message 89 | */ 90 | async deleteWorkflow(id: string): Promise { 91 | return this.client.deleteWorkflow(id); 92 | } 93 | 94 | /** 95 | * Activate a workflow 96 | * 97 | * @param id Workflow ID 98 | * @returns Activated workflow 99 | */ 100 | async activateWorkflow(id: string): Promise { 101 | return this.client.activateWorkflow(id); 102 | } 103 | 104 | /** 105 | * Deactivate a workflow 106 | * 107 | * @param id Workflow ID 108 | * @returns Deactivated workflow 109 | */ 110 | async deactivateWorkflow(id: string): Promise { 111 | return this.client.deactivateWorkflow(id); 112 | } 113 | 114 | /** 115 | * Get all workflow executions 116 | * 117 | * @returns Array of execution objects 118 | */ 119 | async getExecutions(): Promise { 120 | return this.client.getExecutions(); 121 | } 122 | 123 | /** 124 | * Get a specific execution by ID 125 | * 126 | * @param id Execution ID 127 | * @returns Execution object 128 | */ 129 | async getExecution(id: string): Promise { 130 | return this.client.getExecution(id); 131 | } 132 | 133 | /** 134 | * Delete an execution 135 | * 136 | * @param id Execution ID 137 | * @returns Deleted execution or success message 138 | */ 139 | async deleteExecution(id: string): Promise { 140 | return this.client.deleteExecution(id); 141 | } 142 | } 143 | 144 | /** 145 | * Create a new n8n API service 146 | * 147 | * @param config Environment configuration 148 | * @returns n8n API service 149 | */ 150 | export function createApiService(config: EnvConfig): N8nApiService { 151 | return new N8nApiService(config); 152 | } 153 | -------------------------------------------------------------------------------- /src/config/environment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Environment Configuration 3 | * 4 | * This module handles loading and validating environment variables 5 | * required for connecting to the n8n API. 6 | */ 7 | 8 | import dotenv from 'dotenv'; 9 | import findConfig from 'find-config'; 10 | import path from 'path'; 11 | import { McpError } from '@modelcontextprotocol/sdk/types.js'; 12 | import { ErrorCode } from '../errors/error-codes.js'; 13 | 14 | // Environment variable names 15 | export const ENV_VARS = { 16 | N8N_API_URL: 'N8N_API_URL', 17 | N8N_API_KEY: 'N8N_API_KEY', 18 | N8N_WEBHOOK_USERNAME: 'N8N_WEBHOOK_USERNAME', 19 | N8N_WEBHOOK_PASSWORD: 'N8N_WEBHOOK_PASSWORD', 20 | DEBUG: 'DEBUG', 21 | }; 22 | 23 | // Interface for validated environment variables 24 | export interface EnvConfig { 25 | n8nApiUrl: string; 26 | n8nApiKey: string; 27 | n8nWebhookUsername?: string; // Made optional 28 | n8nWebhookPassword?: string; // Made optional 29 | debug: boolean; 30 | } 31 | 32 | /** 33 | * Load environment variables from .env file if present 34 | */ 35 | export function loadEnvironmentVariables(): void { 36 | const { 37 | N8N_API_URL, 38 | N8N_API_KEY, 39 | N8N_WEBHOOK_USERNAME, 40 | N8N_WEBHOOK_PASSWORD 41 | } = process.env; 42 | 43 | if ( 44 | !N8N_API_URL && 45 | !N8N_API_KEY && 46 | !N8N_WEBHOOK_USERNAME && 47 | !N8N_WEBHOOK_PASSWORD 48 | ) { 49 | const projectRoot = findConfig('package.json'); 50 | if (projectRoot) { 51 | const envPath = path.resolve(path.dirname(projectRoot), '.env'); 52 | dotenv.config({ path: envPath }); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Validate and retrieve required environment variables 59 | * 60 | * @returns Validated environment configuration 61 | * @throws {McpError} If required environment variables are missing 62 | */ 63 | export function getEnvConfig(): EnvConfig { 64 | const n8nApiUrl = process.env[ENV_VARS.N8N_API_URL]; 65 | const n8nApiKey = process.env[ENV_VARS.N8N_API_KEY]; 66 | const n8nWebhookUsername = process.env[ENV_VARS.N8N_WEBHOOK_USERNAME]; 67 | const n8nWebhookPassword = process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD]; 68 | const debug = process.env[ENV_VARS.DEBUG]?.toLowerCase() === 'true'; 69 | 70 | // Validate required core environment variables 71 | if (!n8nApiUrl) { 72 | throw new McpError( 73 | ErrorCode.InitializationError, 74 | `Missing required environment variable: ${ENV_VARS.N8N_API_URL}` 75 | ); 76 | } 77 | 78 | if (!n8nApiKey) { 79 | throw new McpError( 80 | ErrorCode.InitializationError, 81 | `Missing required environment variable: ${ENV_VARS.N8N_API_KEY}` 82 | ); 83 | } 84 | 85 | // N8N_WEBHOOK_USERNAME and N8N_WEBHOOK_PASSWORD are now optional at startup. 86 | // Tools requiring them should perform checks at the point of use. 87 | 88 | // Validate URL format 89 | try { 90 | new URL(n8nApiUrl); 91 | } catch (error) { 92 | throw new McpError( 93 | ErrorCode.InitializationError, 94 | `Invalid URL format for ${ENV_VARS.N8N_API_URL}: ${n8nApiUrl}` 95 | ); 96 | } 97 | 98 | return { 99 | n8nApiUrl, 100 | n8nApiKey, 101 | n8nWebhookUsername: n8nWebhookUsername || undefined, // Ensure undefined if empty 102 | n8nWebhookPassword: n8nWebhookPassword || undefined, // Ensure undefined if empty 103 | debug, 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /src/config/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Server Configuration 3 | * 4 | * This module configures the MCP server with tools and resources 5 | * for n8n workflow management. 6 | */ 7 | 8 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 9 | import { 10 | CallToolRequestSchema, 11 | ListToolsRequestSchema 12 | } from '@modelcontextprotocol/sdk/types.js'; 13 | import { getEnvConfig } from './environment.js'; 14 | import { setupWorkflowTools } from '../tools/workflow/index.js'; 15 | import { setupExecutionTools } from '../tools/execution/index.js'; 16 | import { setupResourceHandlers } from '../resources/index.js'; 17 | import { createApiService } from '../api/n8n-client.js'; 18 | 19 | // Import types 20 | import { ToolCallResult } from '../types/index.js'; 21 | 22 | /** 23 | * Configure and return an MCP server instance with all tools and resources 24 | * 25 | * @returns Configured MCP server instance 26 | */ 27 | export async function configureServer(): Promise { 28 | // Get validated environment configuration 29 | const envConfig = getEnvConfig(); 30 | 31 | // Create n8n API service 32 | const apiService = createApiService(envConfig); 33 | 34 | // Verify n8n API connectivity 35 | try { 36 | console.error('Verifying n8n API connectivity...'); 37 | await apiService.checkConnectivity(); 38 | console.error(`Successfully connected to n8n API at ${envConfig.n8nApiUrl}`); 39 | } catch (error) { 40 | console.error('ERROR: Failed to connect to n8n API:', error instanceof Error ? error.message : error); 41 | throw error; 42 | } 43 | 44 | // Create server instance 45 | const server = new Server( 46 | { 47 | name: 'n8n-mcp-server', 48 | version: '0.1.0', 49 | }, 50 | { 51 | capabilities: { 52 | resources: {}, 53 | tools: {}, 54 | }, 55 | } 56 | ); 57 | 58 | // Set up all request handlers 59 | setupToolListRequestHandler(server); 60 | setupToolCallRequestHandler(server); 61 | setupResourceHandlers(server, envConfig); 62 | 63 | return server; 64 | } 65 | 66 | /** 67 | * Set up the tool list request handler for the server 68 | * 69 | * @param server MCP server instance 70 | */ 71 | function setupToolListRequestHandler(server: Server): void { 72 | server.setRequestHandler(ListToolsRequestSchema, async () => { 73 | // Combine tools from workflow and execution modules 74 | const workflowTools = await setupWorkflowTools(); 75 | const executionTools = await setupExecutionTools(); 76 | 77 | return { 78 | tools: [...workflowTools, ...executionTools], 79 | }; 80 | }); 81 | } 82 | 83 | /** 84 | * Set up the tool call request handler for the server 85 | * 86 | * @param server MCP server instance 87 | */ 88 | function setupToolCallRequestHandler(server: Server): void { 89 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 90 | const toolName = request.params.name; 91 | const args = request.params.arguments || {}; 92 | 93 | let result: ToolCallResult; 94 | 95 | try { 96 | // Handle "prompts/list" as a special case, returning an empty success response 97 | // This is to address client calls for a method not central to n8n-mcp-server's direct n8n integration. 98 | if (toolName === 'prompts/list') { 99 | return { 100 | content: [{ type: 'text', text: 'Prompts list acknowledged.' }], // Or an empty array: content: [] 101 | isError: false, 102 | }; 103 | } 104 | 105 | // Import handlers 106 | const { 107 | ListWorkflowsHandler, 108 | GetWorkflowHandler, 109 | CreateWorkflowHandler, 110 | UpdateWorkflowHandler, 111 | DeleteWorkflowHandler, 112 | ActivateWorkflowHandler, 113 | DeactivateWorkflowHandler 114 | } = await import('../tools/workflow/index.js'); 115 | 116 | const { 117 | ListExecutionsHandler, 118 | GetExecutionHandler, 119 | DeleteExecutionHandler, 120 | RunWebhookHandler 121 | } = await import('../tools/execution/index.js'); 122 | 123 | // Route the tool call to the appropriate handler 124 | if (toolName === 'list_workflows') { 125 | const handler = new ListWorkflowsHandler(); 126 | result = await handler.execute(args); 127 | } else if (toolName === 'get_workflow') { 128 | const handler = new GetWorkflowHandler(); 129 | result = await handler.execute(args); 130 | } else if (toolName === 'create_workflow') { 131 | const handler = new CreateWorkflowHandler(); 132 | result = await handler.execute(args); 133 | } else if (toolName === 'update_workflow') { 134 | const handler = new UpdateWorkflowHandler(); 135 | result = await handler.execute(args); 136 | } else if (toolName === 'delete_workflow') { 137 | const handler = new DeleteWorkflowHandler(); 138 | result = await handler.execute(args); 139 | } else if (toolName === 'activate_workflow') { 140 | const handler = new ActivateWorkflowHandler(); 141 | result = await handler.execute(args); 142 | } else if (toolName === 'deactivate_workflow') { 143 | const handler = new DeactivateWorkflowHandler(); 144 | result = await handler.execute(args); 145 | } else if (toolName === 'list_executions') { 146 | const handler = new ListExecutionsHandler(); 147 | result = await handler.execute(args); 148 | } else if (toolName === 'get_execution') { 149 | const handler = new GetExecutionHandler(); 150 | result = await handler.execute(args); 151 | } else if (toolName === 'delete_execution') { 152 | const handler = new DeleteExecutionHandler(); 153 | result = await handler.execute(args); 154 | } else if (toolName === 'run_webhook') { 155 | const handler = new RunWebhookHandler(); 156 | result = await handler.execute(args); 157 | } else { 158 | throw new Error(`Unknown tool: ${toolName}`); 159 | } 160 | 161 | // Converting to MCP SDK expected format 162 | return { 163 | content: result.content, 164 | isError: result.isError, 165 | }; 166 | } catch (error) { 167 | console.error(`Error handling tool call to ${toolName}:`, error); 168 | return { 169 | content: [ 170 | { 171 | type: 'text', 172 | text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, 173 | }, 174 | ], 175 | isError: true, 176 | }; 177 | } 178 | }); 179 | } 180 | -------------------------------------------------------------------------------- /src/errors/error-codes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error Codes Module 3 | * 4 | * This module defines error codes used throughout the application. 5 | * These codes are compatible with the MCP SDK error handling system. 6 | */ 7 | 8 | // Numeric error codes for McpError 9 | export enum ErrorCode { 10 | InitializationError = 1000, 11 | AuthenticationError = 1001, 12 | NotFoundError = 1002, 13 | InvalidRequest = 1003, 14 | InternalError = 1004, 15 | NotImplemented = 1005, 16 | } 17 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error Handling Module 3 | * 4 | * This module provides custom error classes and error handling utilities 5 | * for the n8n MCP Server. 6 | */ 7 | 8 | import { McpError as SdkMcpError } from '@modelcontextprotocol/sdk/types.js'; 9 | import { ErrorCode } from './error-codes.js'; 10 | 11 | // Re-export McpError from SDK 12 | export { McpError } from '@modelcontextprotocol/sdk/types.js'; 13 | // Re-export ErrorCode enum 14 | export { ErrorCode } from './error-codes.js'; 15 | 16 | /** 17 | * n8n API Error class for handling errors from the n8n API 18 | */ 19 | export class N8nApiError extends SdkMcpError { 20 | constructor(message: string, statusCode?: number, details?: unknown) { 21 | // Map HTTP status codes to appropriate MCP error codes 22 | let errorCode = ErrorCode.InternalError; 23 | 24 | if (statusCode) { 25 | if (statusCode === 401 || statusCode === 403) { 26 | errorCode = ErrorCode.AuthenticationError; 27 | } else if (statusCode === 404) { 28 | errorCode = ErrorCode.NotFoundError; 29 | } else if (statusCode >= 400 && statusCode < 500) { 30 | errorCode = ErrorCode.InvalidRequest; 31 | } 32 | } 33 | 34 | super(errorCode, formatErrorMessage(message, statusCode, details)); 35 | } 36 | } 37 | 38 | /** 39 | * Format an error message with status code and details 40 | */ 41 | function formatErrorMessage(message: string, statusCode?: number, details?: unknown): string { 42 | let formattedMessage = message; 43 | 44 | if (statusCode) { 45 | formattedMessage += ` (Status: ${statusCode})`; 46 | } 47 | 48 | if (details) { 49 | try { 50 | const detailsStr = typeof details === 'string' 51 | ? details 52 | : JSON.stringify(details, null, 2); 53 | formattedMessage += `\nDetails: ${detailsStr}`; 54 | } catch (error) { 55 | // Ignore JSON stringification errors 56 | } 57 | } 58 | 59 | return formattedMessage; 60 | } 61 | 62 | /** 63 | * Safely parse JSON response from n8n API 64 | * 65 | * @param text Text to parse as JSON 66 | * @returns Parsed JSON object or null if parsing fails 67 | */ 68 | export function safeJsonParse(text: string): any { 69 | try { 70 | return JSON.parse(text); 71 | } catch (error) { 72 | return null; 73 | } 74 | } 75 | 76 | /** 77 | * Handle axios errors and convert them to N8nApiError 78 | * 79 | * @param error Error object from axios 80 | * @param defaultMessage Default error message 81 | * @returns N8nApiError with appropriate details 82 | */ 83 | export function handleAxiosError(error: any, defaultMessage = 'n8n API request failed'): N8nApiError { 84 | // Handle axios error responses 85 | if (error.response) { 86 | const statusCode = error.response.status; 87 | const responseData = error.response.data; 88 | 89 | let errorMessage = defaultMessage; 90 | if (responseData && responseData.message) { 91 | errorMessage = responseData.message; 92 | } 93 | 94 | return new N8nApiError(errorMessage, statusCode, responseData); 95 | } 96 | 97 | // Handle request errors (e.g., network issues) 98 | if (error.request) { 99 | return new N8nApiError( 100 | 'Network error connecting to n8n API', 101 | undefined, 102 | error.message 103 | ); 104 | } 105 | 106 | // Handle other errors 107 | return new N8nApiError(error.message || defaultMessage); 108 | } 109 | 110 | /** 111 | * Extract a readable error message from an error object 112 | * 113 | * @param error Error object 114 | * @returns Readable error message 115 | */ 116 | export function getErrorMessage(error: unknown): string { 117 | if (error instanceof Error) { 118 | return error.message; 119 | } 120 | 121 | if (typeof error === 'string') { 122 | return error; 123 | } 124 | 125 | try { 126 | return JSON.stringify(error); 127 | } catch { 128 | return 'Unknown error'; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * n8n MCP Server - Main Entry Point 4 | * 5 | * This file serves as the entry point for the n8n MCP Server, 6 | * which allows AI assistants to interact with n8n workflows through the MCP protocol. 7 | */ 8 | 9 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 10 | import { loadEnvironmentVariables } from './config/environment.js'; 11 | import { configureServer } from './config/server.js'; 12 | 13 | // Load environment variables 14 | loadEnvironmentVariables(); 15 | 16 | /** 17 | * Main function to start the n8n MCP Server 18 | */ 19 | async function main() { 20 | try { 21 | console.error('Starting n8n MCP Server...'); 22 | 23 | // Create and configure the MCP server 24 | const server = await configureServer(); 25 | 26 | // Set up error handling 27 | server.onerror = (error: unknown) => console.error('[MCP Error]', error); 28 | 29 | // Set up clean shutdown 30 | process.on('SIGINT', async () => { 31 | console.error('Shutting down n8n MCP Server...'); 32 | await server.close(); 33 | process.exit(0); 34 | }); 35 | 36 | // Connect to the server transport (stdio) 37 | const transport = new StdioServerTransport(); 38 | await server.connect(transport); 39 | 40 | console.error('n8n MCP Server running on stdio'); 41 | } catch (error) { 42 | console.error('Failed to start n8n MCP Server:', error); 43 | process.exit(1); 44 | } 45 | } 46 | 47 | // Start the server 48 | main().catch(console.error); 49 | -------------------------------------------------------------------------------- /src/resources/dynamic/execution.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Dynamic Execution Resource Handler 3 | * 4 | * This module provides the MCP resource implementation for retrieving 5 | * detailed execution information by ID. 6 | */ 7 | 8 | import { N8nApiService } from '../../api/n8n-client.js'; 9 | import { formatExecutionDetails } from '../../utils/execution-formatter.js'; 10 | import { formatResourceUri } from '../../utils/resource-formatter.js'; 11 | import { McpError, ErrorCode } from '../../errors/index.js'; 12 | 13 | /** 14 | * Get execution resource data by ID 15 | * 16 | * @param apiService n8n API service 17 | * @param executionId Execution ID 18 | * @returns Formatted execution resource data 19 | */ 20 | export async function getExecutionResource(apiService: N8nApiService, executionId: string): Promise { 21 | try { 22 | // Get the specific execution from the API 23 | const execution = await apiService.getExecution(executionId); 24 | 25 | // Format the execution for resource consumption 26 | const formattedExecution = formatExecutionDetails(execution); 27 | 28 | // Add metadata about the resource 29 | const result = { 30 | resourceType: 'execution', 31 | id: executionId, 32 | ...formattedExecution, 33 | _links: { 34 | self: formatResourceUri('execution', executionId), 35 | // Include link to related workflow 36 | workflow: `n8n://workflows/${execution.workflowId}`, 37 | }, 38 | lastUpdated: new Date().toISOString(), 39 | }; 40 | 41 | return JSON.stringify(result, null, 2); 42 | } catch (error) { 43 | console.error(`Error fetching execution resource (ID: ${executionId}):`, error); 44 | 45 | // Handle not found errors specifically 46 | if (error instanceof McpError && error.code === ErrorCode.NotFoundError) { 47 | throw error; 48 | } 49 | 50 | throw new McpError( 51 | ErrorCode.InternalError, 52 | `Failed to retrieve execution (ID: ${executionId}): ${error instanceof Error ? error.message : 'Unknown error'}` 53 | ); 54 | } 55 | } 56 | 57 | /** 58 | * Get execution resource template URI 59 | * 60 | * @returns Formatted resource template URI 61 | */ 62 | export function getExecutionResourceTemplateUri(): string { 63 | return 'n8n://executions/{id}'; 64 | } 65 | 66 | /** 67 | * Get execution resource template metadata 68 | * 69 | * @returns Resource template metadata object 70 | */ 71 | export function getExecutionResourceTemplateMetadata(): Record { 72 | return { 73 | uriTemplate: getExecutionResourceTemplateUri(), 74 | name: 'n8n Execution Details', 75 | mimeType: 'application/json', 76 | description: 'Detailed information about a specific n8n workflow execution including node results and error information', 77 | }; 78 | } 79 | 80 | /** 81 | * Extract execution ID from resource URI 82 | * 83 | * @param uri Resource URI 84 | * @returns Execution ID or null if URI format is invalid 85 | */ 86 | export function extractExecutionIdFromUri(uri: string): string | null { 87 | const match = uri.match(/^n8n:\/\/executions\/([^/]+)$/); 88 | return match ? match[1] : null; 89 | } 90 | -------------------------------------------------------------------------------- /src/resources/dynamic/workflow.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Dynamic Workflow Resource Handler 3 | * 4 | * This module provides the MCP resource implementation for retrieving 5 | * detailed workflow information by ID. 6 | */ 7 | 8 | import { N8nApiService } from '../../api/n8n-client.js'; 9 | import { formatWorkflowDetails, formatResourceUri } from '../../utils/resource-formatter.js'; 10 | import { McpError, ErrorCode } from '../../errors/index.js'; 11 | 12 | /** 13 | * Get workflow resource data by ID 14 | * 15 | * @param apiService n8n API service 16 | * @param workflowId Workflow ID 17 | * @returns Formatted workflow resource data 18 | */ 19 | export async function getWorkflowResource(apiService: N8nApiService, workflowId: string): Promise { 20 | try { 21 | // Get the specific workflow from the API 22 | const workflow = await apiService.getWorkflow(workflowId); 23 | 24 | // Format the workflow for resource consumption 25 | const formattedWorkflow = formatWorkflowDetails(workflow); 26 | 27 | // Add metadata about the resource 28 | const result = { 29 | resourceType: 'workflow', 30 | id: workflowId, 31 | ...formattedWorkflow, 32 | _links: { 33 | self: formatResourceUri('workflow', workflowId), 34 | // Include links to related resources 35 | executions: `n8n://executions?workflowId=${workflowId}`, 36 | }, 37 | lastUpdated: new Date().toISOString(), 38 | }; 39 | 40 | return JSON.stringify(result, null, 2); 41 | } catch (error) { 42 | console.error(`Error fetching workflow resource (ID: ${workflowId}):`, error); 43 | 44 | // Handle not found errors specifically 45 | if (error instanceof McpError && error.code === ErrorCode.NotFoundError) { 46 | throw error; 47 | } 48 | 49 | throw new McpError( 50 | ErrorCode.InternalError, 51 | `Failed to retrieve workflow (ID: ${workflowId}): ${error instanceof Error ? error.message : 'Unknown error'}` 52 | ); 53 | } 54 | } 55 | 56 | /** 57 | * Get workflow resource template URI 58 | * 59 | * @returns Formatted resource template URI 60 | */ 61 | export function getWorkflowResourceTemplateUri(): string { 62 | return 'n8n://workflows/{id}'; 63 | } 64 | 65 | /** 66 | * Get workflow resource template metadata 67 | * 68 | * @returns Resource template metadata object 69 | */ 70 | export function getWorkflowResourceTemplateMetadata(): Record { 71 | return { 72 | uriTemplate: getWorkflowResourceTemplateUri(), 73 | name: 'n8n Workflow Details', 74 | mimeType: 'application/json', 75 | description: 'Detailed information about a specific n8n workflow including all nodes, connections, and settings', 76 | }; 77 | } 78 | 79 | /** 80 | * Extract workflow ID from resource URI 81 | * 82 | * @param uri Resource URI 83 | * @returns Workflow ID or null if URI format is invalid 84 | */ 85 | export function extractWorkflowIdFromUri(uri: string): string | null { 86 | const match = uri.match(/^n8n:\/\/workflows\/([^/]+)$/); 87 | return match ? match[1] : null; 88 | } 89 | -------------------------------------------------------------------------------- /src/resources/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Resources Module 3 | * 4 | * This module provides MCP resource handlers for n8n workflows and executions. 5 | */ 6 | 7 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 8 | import { 9 | ListResourcesRequestSchema, 10 | ListResourceTemplatesRequestSchema, 11 | ReadResourceRequestSchema, 12 | } from '@modelcontextprotocol/sdk/types.js'; 13 | import { EnvConfig } from '../config/environment.js'; 14 | import { createApiService } from '../api/n8n-client.js'; 15 | import { McpError, ErrorCode } from '../errors/index.js'; 16 | 17 | // Import static resource handlers 18 | import { 19 | getWorkflowsResource, 20 | getWorkflowsResourceMetadata, 21 | getWorkflowsResourceUri, 22 | } from './static/workflows.js'; 23 | import { 24 | getExecutionStatsResource, 25 | getExecutionStatsResourceMetadata, 26 | getExecutionStatsResourceUri, 27 | } from './static/execution-stats.js'; 28 | 29 | // Import dynamic resource handlers 30 | import { 31 | getWorkflowResource, 32 | getWorkflowResourceTemplateMetadata, 33 | extractWorkflowIdFromUri, 34 | } from './dynamic/workflow.js'; 35 | import { 36 | getExecutionResource, 37 | getExecutionResourceTemplateMetadata, 38 | extractExecutionIdFromUri, 39 | } from './dynamic/execution.js'; 40 | 41 | /** 42 | * Set up resource handlers for the MCP server 43 | * 44 | * @param server MCP server instance 45 | * @param envConfig Environment configuration 46 | */ 47 | export function setupResourceHandlers(server: Server, envConfig: EnvConfig): void { 48 | // Set up static resources 49 | setupStaticResources(server, envConfig); 50 | 51 | // Set up dynamic resources 52 | setupDynamicResources(server, envConfig); 53 | } 54 | 55 | /** 56 | * Set up static resource handlers 57 | * 58 | * @param server MCP server instance 59 | * @param envConfig Environment configuration 60 | */ 61 | function setupStaticResources(server: Server, _envConfig: EnvConfig): void { 62 | server.setRequestHandler(ListResourcesRequestSchema, async () => { 63 | // Return all available static resources 64 | return { 65 | resources: [ 66 | getWorkflowsResourceMetadata(), 67 | getExecutionStatsResourceMetadata(), 68 | ], 69 | }; 70 | }); 71 | } 72 | 73 | /** 74 | * Set up dynamic resource handlers 75 | * 76 | * @param server MCP server instance 77 | * @param envConfig Environment configuration 78 | */ 79 | function setupDynamicResources(server: Server, envConfig: EnvConfig): void { 80 | const apiService = createApiService(envConfig); 81 | 82 | server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { 83 | // Return all available dynamic resource templates 84 | return { 85 | resourceTemplates: [ 86 | getWorkflowResourceTemplateMetadata(), 87 | getExecutionResourceTemplateMetadata(), 88 | ], 89 | }; 90 | }); 91 | 92 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => { 93 | const uri = request.params.uri; 94 | console.log(`Resource requested: ${uri}`); 95 | 96 | try { 97 | // Handle static resources 98 | if (uri === getWorkflowsResourceUri()) { 99 | const content = await getWorkflowsResource(apiService); 100 | return { 101 | contents: [ 102 | { 103 | uri, 104 | mimeType: 'application/json', 105 | text: content, 106 | }, 107 | ], 108 | }; 109 | } 110 | 111 | if (uri === getExecutionStatsResourceUri()) { 112 | const content = await getExecutionStatsResource(apiService); 113 | return { 114 | contents: [ 115 | { 116 | uri, 117 | mimeType: 'application/json', 118 | text: content, 119 | }, 120 | ], 121 | }; 122 | } 123 | 124 | // Handle dynamic resources 125 | const workflowId = extractWorkflowIdFromUri(uri); 126 | if (workflowId) { 127 | const content = await getWorkflowResource(apiService, workflowId); 128 | return { 129 | contents: [ 130 | { 131 | uri, 132 | mimeType: 'application/json', 133 | text: content, 134 | }, 135 | ], 136 | }; 137 | } 138 | 139 | const executionId = extractExecutionIdFromUri(uri); 140 | if (executionId) { 141 | const content = await getExecutionResource(apiService, executionId); 142 | return { 143 | contents: [ 144 | { 145 | uri, 146 | mimeType: 'application/json', 147 | text: content, 148 | }, 149 | ], 150 | }; 151 | } 152 | 153 | // If we get here, the URI isn't recognized 154 | throw new McpError( 155 | ErrorCode.NotFoundError, 156 | `Resource not found: ${uri}` 157 | ); 158 | } catch (error) { 159 | console.error(`Error retrieving resource (${uri}):`, error); 160 | 161 | // Pass through McpErrors 162 | if (error instanceof McpError) { 163 | throw error; 164 | } 165 | 166 | // Convert other errors to McpError 167 | throw new McpError( 168 | ErrorCode.InternalError, 169 | `Error retrieving resource: ${error instanceof Error ? error.message : 'Unknown error'}` 170 | ); 171 | } 172 | }); 173 | } 174 | -------------------------------------------------------------------------------- /src/resources/static/execution-stats.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Static Execution Statistics Resource Handler 3 | * 4 | * This module provides the MCP resource implementation for execution statistics. 5 | */ 6 | 7 | import { N8nApiService } from '../../api/n8n-client.js'; 8 | import { formatExecutionStats, formatResourceUri } from '../../utils/resource-formatter.js'; 9 | import { McpError, ErrorCode } from '../../errors/index.js'; 10 | 11 | /** 12 | * Get execution statistics resource data 13 | * 14 | * @param apiService n8n API service 15 | * @returns Formatted execution statistics resource data 16 | */ 17 | export async function getExecutionStatsResource(apiService: N8nApiService): Promise { 18 | try { 19 | // Get executions from the API 20 | const executions = await apiService.getExecutions(); 21 | 22 | // Format the execution statistics 23 | const stats = formatExecutionStats(executions); 24 | 25 | // Add metadata about the resource 26 | const result = { 27 | resourceType: 'execution-stats', 28 | ...stats, 29 | _links: { 30 | self: formatResourceUri('execution-stats'), 31 | } 32 | }; 33 | 34 | return JSON.stringify(result, null, 2); 35 | } catch (error) { 36 | console.error('Error fetching execution statistics resource:', error); 37 | throw new McpError( 38 | ErrorCode.InternalError, 39 | `Failed to retrieve execution statistics: ${error instanceof Error ? error.message : 'Unknown error'}` 40 | ); 41 | } 42 | } 43 | 44 | /** 45 | * Get execution statistics resource URI 46 | * 47 | * @returns Formatted resource URI 48 | */ 49 | export function getExecutionStatsResourceUri(): string { 50 | return formatResourceUri('execution-stats'); 51 | } 52 | 53 | /** 54 | * Get execution statistics resource metadata 55 | * 56 | * @returns Resource metadata object 57 | */ 58 | export function getExecutionStatsResourceMetadata(): Record { 59 | return { 60 | uri: getExecutionStatsResourceUri(), 61 | name: 'n8n Execution Statistics', 62 | mimeType: 'application/json', 63 | description: 'Summary statistics of workflow executions including success rates, average duration, and trends', 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/resources/static/workflows.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Static Workflows Resource Handler 3 | * 4 | * This module provides the MCP resource implementation for listing all workflows. 5 | */ 6 | 7 | import { N8nApiService } from '../../api/n8n-client.js'; 8 | import { formatWorkflowSummary, formatResourceUri } from '../../utils/resource-formatter.js'; 9 | import { McpError, ErrorCode } from '../../errors/index.js'; 10 | 11 | /** 12 | * Get workflows resource data 13 | * 14 | * @param apiService n8n API service 15 | * @returns Formatted workflows resource data 16 | */ 17 | export async function getWorkflowsResource(apiService: N8nApiService): Promise { 18 | try { 19 | // Get all workflows from the API 20 | const workflows = await apiService.getWorkflows(); 21 | 22 | // Format the workflows for resource consumption 23 | const formattedWorkflows = workflows.map(workflow => formatWorkflowSummary(workflow)); 24 | 25 | // Add metadata about the resource 26 | const result = { 27 | resourceType: 'workflows', 28 | count: formattedWorkflows.length, 29 | workflows: formattedWorkflows, 30 | _links: { 31 | self: formatResourceUri('workflows'), 32 | }, 33 | lastUpdated: new Date().toISOString(), 34 | }; 35 | 36 | return JSON.stringify(result, null, 2); 37 | } catch (error) { 38 | console.error('Error fetching workflows resource:', error); 39 | throw new McpError( 40 | ErrorCode.InternalError, 41 | `Failed to retrieve workflows: ${error instanceof Error ? error.message : 'Unknown error'}` 42 | ); 43 | } 44 | } 45 | 46 | /** 47 | * Get workflows resource URI 48 | * 49 | * @returns Formatted resource URI 50 | */ 51 | export function getWorkflowsResourceUri(): string { 52 | return formatResourceUri('workflows'); 53 | } 54 | 55 | /** 56 | * Get workflows resource metadata 57 | * 58 | * @returns Resource metadata object 59 | */ 60 | export function getWorkflowsResourceMetadata(): Record { 61 | return { 62 | uri: getWorkflowsResourceUri(), 63 | name: 'n8n Workflows', 64 | mimeType: 'application/json', 65 | description: 'List of all workflows in the n8n instance with their basic information', 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /src/tools/execution/base-handler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base Execution Tool Handler 3 | * 4 | * This module provides a base handler for execution-related tools. 5 | */ 6 | 7 | import { ToolCallResult } from '../../types/index.js'; 8 | import { N8nApiError } from '../../errors/index.js'; 9 | import { createApiService } from '../../api/n8n-client.js'; 10 | import { getEnvConfig } from '../../config/environment.js'; 11 | 12 | /** 13 | * Base class for execution tool handlers 14 | */ 15 | export abstract class BaseExecutionToolHandler { 16 | protected apiService = createApiService(getEnvConfig()); 17 | 18 | /** 19 | * Validate and execute the tool 20 | * 21 | * @param args Arguments passed to the tool 22 | * @returns Tool call result 23 | */ 24 | abstract execute(args: Record): Promise; 25 | 26 | /** 27 | * Format a successful response 28 | * 29 | * @param data Response data 30 | * @param message Optional success message 31 | * @returns Formatted success response 32 | */ 33 | protected formatSuccess(data: any, message?: string): ToolCallResult { 34 | const formattedData = typeof data === 'object' 35 | ? JSON.stringify(data, null, 2) 36 | : String(data); 37 | 38 | return { 39 | content: [ 40 | { 41 | type: 'text', 42 | text: message ? `${message}\n\n${formattedData}` : formattedData, 43 | }, 44 | ], 45 | }; 46 | } 47 | 48 | /** 49 | * Format an error response 50 | * 51 | * @param error Error object or message 52 | * @returns Formatted error response 53 | */ 54 | protected formatError(error: Error | string): ToolCallResult { 55 | const errorMessage = error instanceof Error ? error.message : error; 56 | 57 | return { 58 | content: [ 59 | { 60 | type: 'text', 61 | text: errorMessage, 62 | }, 63 | ], 64 | isError: true, 65 | }; 66 | } 67 | 68 | /** 69 | * Handle tool execution errors 70 | * 71 | * @param handler Function to execute 72 | * @param args Arguments to pass to the handler 73 | * @returns Tool call result 74 | */ 75 | protected async handleExecution( 76 | handler: (args: Record) => Promise, 77 | args: Record 78 | ): Promise { 79 | try { 80 | return await handler(args); 81 | } catch (error) { 82 | if (error instanceof N8nApiError) { 83 | return this.formatError(error.message); 84 | } 85 | 86 | const errorMessage = error instanceof Error 87 | ? error.message 88 | : 'Unknown error occurred'; 89 | 90 | return this.formatError(`Error executing execution tool: ${errorMessage}`); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/tools/execution/delete.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Delete Execution Tool 3 | * 4 | * This tool deletes a specific workflow execution from n8n. 5 | */ 6 | 7 | import { BaseExecutionToolHandler } from './base-handler.js'; 8 | import { ToolCallResult, ToolDefinition } from '../../types/index.js'; 9 | import { McpError } from '@modelcontextprotocol/sdk/types.js'; 10 | import { ErrorCode } from '../../errors/error-codes.js'; 11 | 12 | /** 13 | * Handler for the delete_execution tool 14 | */ 15 | export class DeleteExecutionHandler extends BaseExecutionToolHandler { 16 | /** 17 | * Execute the tool 18 | * 19 | * @param args Tool arguments (executionId) 20 | * @returns Result of the deletion operation 21 | */ 22 | async execute(args: Record): Promise { 23 | return this.handleExecution(async () => { 24 | // Validate required parameters 25 | if (!args.executionId) { 26 | throw new McpError( 27 | ErrorCode.InvalidRequest, 28 | 'Missing required parameter: executionId' 29 | ); 30 | } 31 | 32 | // Store execution ID for response message 33 | const executionId = args.executionId; 34 | 35 | // Delete the execution 36 | await this.apiService.deleteExecution(executionId); 37 | 38 | return this.formatSuccess( 39 | { id: executionId, deleted: true }, 40 | `Successfully deleted execution with ID: ${executionId}` 41 | ); 42 | }, args); 43 | } 44 | } 45 | 46 | /** 47 | * Get tool definition for the delete_execution tool 48 | * 49 | * @returns Tool definition 50 | */ 51 | export function getDeleteExecutionToolDefinition(): ToolDefinition { 52 | return { 53 | name: 'delete_execution', 54 | description: 'Delete a specific workflow execution from n8n', 55 | inputSchema: { 56 | type: 'object', 57 | properties: { 58 | executionId: { 59 | type: 'string', 60 | description: 'ID of the execution to delete', 61 | }, 62 | }, 63 | required: ['executionId'], 64 | }, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /src/tools/execution/get.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get Execution Tool 3 | * 4 | * This tool retrieves detailed information about a specific workflow execution. 5 | */ 6 | 7 | import { BaseExecutionToolHandler } from './base-handler.js'; 8 | import { ToolCallResult, ToolDefinition } from '../../types/index.js'; 9 | import { McpError } from '@modelcontextprotocol/sdk/types.js'; 10 | import { ErrorCode } from '../../errors/error-codes.js'; 11 | import { formatExecutionDetails } from '../../utils/execution-formatter.js'; 12 | 13 | /** 14 | * Handler for the get_execution tool 15 | */ 16 | export class GetExecutionHandler extends BaseExecutionToolHandler { 17 | /** 18 | * Execute the tool 19 | * 20 | * @param args Tool arguments (executionId) 21 | * @returns Execution details 22 | */ 23 | async execute(args: Record): Promise { 24 | return this.handleExecution(async () => { 25 | // Validate required parameters 26 | if (!args.executionId) { 27 | throw new McpError( 28 | ErrorCode.InvalidRequest, 29 | 'Missing required parameter: executionId' 30 | ); 31 | } 32 | 33 | // Get execution details 34 | const execution = await this.apiService.getExecution(args.executionId); 35 | 36 | // Format the execution for display 37 | const formattedExecution = formatExecutionDetails(execution); 38 | 39 | return this.formatSuccess( 40 | formattedExecution, 41 | `Execution Details for ID: ${args.executionId}` 42 | ); 43 | }, args); 44 | } 45 | 46 | } 47 | 48 | /** 49 | * Get tool definition for the get_execution tool 50 | * 51 | * @returns Tool definition 52 | */ 53 | export function getGetExecutionToolDefinition(): ToolDefinition { 54 | return { 55 | name: 'get_execution', 56 | description: 'Retrieve detailed information about a specific workflow execution', 57 | inputSchema: { 58 | type: 'object', 59 | properties: { 60 | executionId: { 61 | type: 'string', 62 | description: 'ID of the execution to retrieve', 63 | }, 64 | }, 65 | required: ['executionId'], 66 | }, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/tools/execution/handler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Execution Tools Handler 3 | * 4 | * This module handles calls to execution-related tools. 5 | */ 6 | 7 | import { ToolCallResult } from '../../types/index.js'; 8 | import { McpError } from '@modelcontextprotocol/sdk/types.js'; 9 | import { ErrorCode } from '../../errors/error-codes.js'; 10 | import { getErrorMessage } from '../../errors/index.js'; 11 | import { 12 | ListExecutionsHandler, 13 | GetExecutionHandler, 14 | DeleteExecutionHandler, 15 | RunWebhookHandler 16 | } from './index.js'; 17 | 18 | /** 19 | * Handle execution tool calls 20 | * 21 | * @param toolName Name of the tool being called 22 | * @param args Arguments passed to the tool 23 | * @returns Tool call result 24 | */ 25 | export default async function executionHandler( 26 | toolName: string, 27 | args: Record 28 | ): Promise { 29 | try { 30 | // Route to the appropriate handler based on tool name 31 | switch (toolName) { 32 | case 'list_executions': 33 | return await new ListExecutionsHandler().execute(args); 34 | 35 | case 'get_execution': 36 | return await new GetExecutionHandler().execute(args); 37 | 38 | case 'delete_execution': 39 | return await new DeleteExecutionHandler().execute(args); 40 | 41 | case 'run_webhook': 42 | return await new RunWebhookHandler().execute(args); 43 | 44 | default: 45 | throw new McpError( 46 | ErrorCode.NotImplemented, 47 | `Unknown execution tool: '${toolName}'` 48 | ); 49 | } 50 | } catch (error) { 51 | // Get appropriate error message 52 | const errorMessage = getErrorMessage(error); 53 | 54 | return { 55 | content: [ 56 | { 57 | type: 'text', 58 | text: `Error executing execution tool: ${errorMessage}`, 59 | }, 60 | ], 61 | isError: true, 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/tools/execution/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Execution Tools Module 3 | * 4 | * This module provides MCP tools for interacting with n8n workflow executions. 5 | */ 6 | 7 | import { ToolDefinition } from '../../types/index.js'; 8 | import { getListExecutionsToolDefinition } from './list.js'; 9 | import { getGetExecutionToolDefinition } from './get.js'; 10 | import { getDeleteExecutionToolDefinition } from './delete.js'; 11 | import { getRunWebhookToolDefinition } from './run.js'; 12 | 13 | /** 14 | * Set up execution management tools 15 | * 16 | * @returns Array of execution tool definitions 17 | */ 18 | export async function setupExecutionTools(): Promise { 19 | return [ 20 | getListExecutionsToolDefinition(), 21 | getGetExecutionToolDefinition(), 22 | getDeleteExecutionToolDefinition(), 23 | getRunWebhookToolDefinition() 24 | ]; 25 | } 26 | 27 | // Export execution tool handlers for use in the handler 28 | export { ListExecutionsHandler } from './list.js'; 29 | export { GetExecutionHandler } from './get.js'; 30 | export { DeleteExecutionHandler } from './delete.js'; 31 | export { RunWebhookHandler } from './run.js'; 32 | -------------------------------------------------------------------------------- /src/tools/execution/list.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * List Executions Tool 3 | * 4 | * This tool retrieves a list of workflow executions from n8n. 5 | */ 6 | 7 | import { BaseExecutionToolHandler } from './base-handler.js'; 8 | import { ToolCallResult, ToolDefinition, Execution } from '../../types/index.js'; 9 | import { formatExecutionSummary, summarizeExecutions } from '../../utils/execution-formatter.js'; 10 | 11 | /** 12 | * Handler for the list_executions tool 13 | */ 14 | export class ListExecutionsHandler extends BaseExecutionToolHandler { 15 | /** 16 | * Execute the tool 17 | * 18 | * @param args Tool arguments (workflowId, status, limit, lastId) 19 | * @returns List of executions 20 | */ 21 | async execute(args: Record): Promise { 22 | return this.handleExecution(async () => { 23 | const executions = await this.apiService.getExecutions(); 24 | 25 | // Apply filters if provided 26 | let filteredExecutions = executions; 27 | 28 | // Filter by workflow ID if provided 29 | if (args.workflowId) { 30 | filteredExecutions = filteredExecutions.filter( 31 | (execution: Execution) => execution.workflowId === args.workflowId 32 | ); 33 | } 34 | 35 | // Filter by status if provided 36 | if (args.status) { 37 | filteredExecutions = filteredExecutions.filter( 38 | (execution: Execution) => execution.status === args.status 39 | ); 40 | } 41 | 42 | // Apply limit if provided 43 | const limit = args.limit && args.limit > 0 ? args.limit : filteredExecutions.length; 44 | filteredExecutions = filteredExecutions.slice(0, limit); 45 | 46 | // Format the executions for display 47 | const formattedExecutions = filteredExecutions.map((execution: Execution) => 48 | formatExecutionSummary(execution) 49 | ); 50 | 51 | // Generate summary if requested 52 | let summary = undefined; 53 | if (args.includeSummary) { 54 | summary = summarizeExecutions(executions); 55 | } 56 | 57 | // Prepare response data 58 | const responseData = { 59 | executions: formattedExecutions, 60 | summary: summary, 61 | total: formattedExecutions.length, 62 | filtered: args.workflowId || args.status ? true : false 63 | }; 64 | 65 | return this.formatSuccess( 66 | responseData, 67 | `Found ${formattedExecutions.length} execution(s)` 68 | ); 69 | }, args); 70 | } 71 | } 72 | 73 | /** 74 | * Get tool definition for the list_executions tool 75 | * 76 | * @returns Tool definition 77 | */ 78 | export function getListExecutionsToolDefinition(): ToolDefinition { 79 | return { 80 | name: 'list_executions', 81 | description: 'Retrieve a list of workflow executions from n8n', 82 | inputSchema: { 83 | type: 'object', 84 | properties: { 85 | workflowId: { 86 | type: 'string', 87 | description: 'Optional ID of workflow to filter executions by', 88 | }, 89 | status: { 90 | type: 'string', 91 | description: 'Optional status to filter by (success, error, waiting, or canceled)', 92 | }, 93 | limit: { 94 | type: 'number', 95 | description: 'Maximum number of executions to return', 96 | }, 97 | lastId: { 98 | type: 'string', 99 | description: 'ID of the last execution for pagination', 100 | }, 101 | includeSummary: { 102 | type: 'boolean', 103 | description: 'Include summary statistics about executions', 104 | }, 105 | }, 106 | required: [], 107 | }, 108 | }; 109 | } 110 | -------------------------------------------------------------------------------- /src/tools/execution/run.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Run Execution via Webhook Tool Handler 3 | * 4 | * This module provides a tool for running n8n workflows via webhooks. 5 | */ 6 | 7 | import axios from 'axios'; 8 | import { z } from 'zod'; 9 | import { ToolCallResult } from '../../types/index.js'; 10 | import { BaseExecutionToolHandler } from './base-handler.js'; 11 | import { N8nApiError } from '../../errors/index.js'; 12 | import { getEnvConfig } from '../../config/environment.js'; 13 | import { URL } from 'url'; 14 | 15 | /** 16 | * Webhook execution input schema 17 | */ 18 | const runWebhookSchema = z.object({ 19 | workflowName: z.string().describe('Name of the workflow to execute (e.g., "hello-world")'), 20 | data: z.record(z.any()).optional().describe('Input data to pass to the webhook'), 21 | headers: z.record(z.string()).optional().describe('Additional headers to send with the request') 22 | }); 23 | 24 | /** 25 | * Handler for the run_webhook tool 26 | */ 27 | export class RunWebhookHandler extends BaseExecutionToolHandler { 28 | /** 29 | * Tool definition for execution via webhook 30 | */ 31 | public static readonly inputSchema = runWebhookSchema; 32 | 33 | /** 34 | * Extract N8N base URL from N8N API URL by removing /api/v1 35 | * @returns N8N base URL 36 | */ 37 | private getN8nBaseUrl(): string { 38 | const config = getEnvConfig(); 39 | const apiUrl = new URL(config.n8nApiUrl); 40 | 41 | // Remove /api/v1 if it exists in the path 42 | let path = apiUrl.pathname; 43 | if (path.endsWith('/api/v1') || path.endsWith('/api/v1/')) { 44 | path = path.replace(/\/api\/v1\/?$/, ''); 45 | } 46 | 47 | // Create a new URL with the base path 48 | apiUrl.pathname = path; 49 | return apiUrl.toString(); 50 | } 51 | 52 | /** 53 | * Validate and execute webhook call 54 | * 55 | * @param args Tool arguments 56 | * @returns Tool call result 57 | */ 58 | async execute(args: Record): Promise { 59 | return this.handleExecution(async (args) => { 60 | // Parse and validate arguments 61 | const params = runWebhookSchema.parse(args); 62 | 63 | // Get environment config for auth credentials 64 | const config = getEnvConfig(); 65 | 66 | // Check if webhook credentials are provided, as they are required for this tool 67 | if (!config.n8nWebhookUsername || !config.n8nWebhookPassword) { 68 | throw new N8nApiError( 69 | 'Webhook username and password are required for run_webhook tool. ' + 70 | 'Please set N8N_WEBHOOK_USERNAME and N8N_WEBHOOK_PASSWORD environment variables.', 71 | 400 // Bad Request, as it's a client-side configuration issue for this specific tool 72 | ); 73 | } 74 | 75 | try { 76 | // Get the webhook URL with the proper prefix 77 | const baseUrl = this.getN8nBaseUrl(); 78 | const webhookPath = `webhook/${params.workflowName}`; 79 | const webhookUrl = new URL(webhookPath, baseUrl).toString(); 80 | 81 | // Prepare request config with basic auth from environment 82 | const requestConfig: any = { 83 | headers: { 84 | 'Content-Type': 'application/json', 85 | ...(params.headers || {}) 86 | }, 87 | auth: { 88 | username: config.n8nWebhookUsername, 89 | password: config.n8nWebhookPassword 90 | } 91 | }; 92 | 93 | // Make the request to the webhook 94 | const response = await axios.post( 95 | webhookUrl, 96 | params.data || {}, 97 | requestConfig 98 | ); 99 | 100 | // Return the webhook response 101 | return this.formatSuccess({ 102 | status: response.status, 103 | statusText: response.statusText, 104 | data: response.data 105 | }, 'Webhook executed successfully'); 106 | } catch (error) { 107 | // Handle error from the webhook request 108 | if (axios.isAxiosError(error)) { 109 | let errorMessage = `Webhook execution failed: ${error.message}`; 110 | 111 | if (error.response) { 112 | errorMessage = `Webhook execution failed with status ${error.response.status}: ${error.response.statusText}`; 113 | if (error.response.data) { 114 | return this.formatError(new N8nApiError( 115 | `${errorMessage}\n\n${JSON.stringify(error.response.data, null, 2)}`, 116 | error.response.status 117 | )); 118 | } 119 | } 120 | 121 | return this.formatError(new N8nApiError(errorMessage, error.response?.status || 500)); 122 | } 123 | 124 | throw error; // Re-throw non-axios errors for the handler to catch 125 | } 126 | }, args); 127 | } 128 | } 129 | 130 | /** 131 | * Get the tool definition for run_webhook 132 | * 133 | * @returns Tool definition object 134 | */ 135 | export function getRunWebhookToolDefinition() { 136 | return { 137 | name: 'run_webhook', 138 | description: 'Execute a workflow via webhook with optional input data', 139 | inputSchema: { 140 | type: 'object', 141 | properties: { 142 | workflowName: { 143 | type: 'string', 144 | description: 'Name of the workflow to execute (e.g., "hello-world")' 145 | }, 146 | data: { 147 | type: 'object', 148 | description: 'Input data to pass to the webhook' 149 | }, 150 | headers: { 151 | type: 'object', 152 | description: 'Additional headers to send with the request' 153 | } 154 | }, 155 | required: ['workflowName'] 156 | } 157 | }; 158 | } -------------------------------------------------------------------------------- /src/tools/workflow/activate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Activate Workflow Tool 3 | * 4 | * This tool activates an existing workflow in n8n. 5 | */ 6 | 7 | import { BaseWorkflowToolHandler } from './base-handler.js'; 8 | import { ToolCallResult, ToolDefinition } from '../../types/index.js'; 9 | import { N8nApiError } from '../../errors/index.js'; 10 | 11 | /** 12 | * Handler for the activate_workflow tool 13 | */ 14 | export class ActivateWorkflowHandler extends BaseWorkflowToolHandler { 15 | /** 16 | * Execute the tool 17 | * 18 | * @param args Tool arguments containing workflowId 19 | * @returns Activation confirmation 20 | */ 21 | async execute(args: Record): Promise { 22 | return this.handleExecution(async (args) => { 23 | const { workflowId } = args; 24 | 25 | if (!workflowId) { 26 | throw new N8nApiError('Missing required parameter: workflowId'); 27 | } 28 | 29 | // Activate the workflow 30 | const workflow = await this.apiService.activateWorkflow(workflowId); 31 | 32 | return this.formatSuccess( 33 | { 34 | id: workflow.id, 35 | name: workflow.name, 36 | active: workflow.active 37 | }, 38 | `Workflow "${workflow.name}" (ID: ${workflowId}) has been successfully activated` 39 | ); 40 | }, args); 41 | } 42 | } 43 | 44 | /** 45 | * Get tool definition for the activate_workflow tool 46 | * 47 | * @returns Tool definition 48 | */ 49 | export function getActivateWorkflowToolDefinition(): ToolDefinition { 50 | return { 51 | name: 'activate_workflow', 52 | description: 'Activate a workflow in n8n', 53 | inputSchema: { 54 | type: 'object', 55 | properties: { 56 | workflowId: { 57 | type: 'string', 58 | description: 'ID of the workflow to activate', 59 | }, 60 | }, 61 | required: ['workflowId'], 62 | }, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/tools/workflow/base-handler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base Workflow Tool Handler 3 | * 4 | * This module provides a base handler for workflow-related tools. 5 | */ 6 | 7 | import { ToolCallResult } from '../../types/index.js'; 8 | import { N8nApiError } from '../../errors/index.js'; 9 | import { createApiService } from '../../api/n8n-client.js'; 10 | import { getEnvConfig } from '../../config/environment.js'; 11 | 12 | /** 13 | * Base class for workflow tool handlers 14 | */ 15 | export abstract class BaseWorkflowToolHandler { 16 | protected apiService = createApiService(getEnvConfig()); 17 | 18 | /** 19 | * Validate and execute the tool 20 | * 21 | * @param args Arguments passed to the tool 22 | * @returns Tool call result 23 | */ 24 | abstract execute(args: Record): Promise; 25 | 26 | /** 27 | * Format a successful response 28 | * 29 | * @param data Response data 30 | * @param message Optional success message 31 | * @returns Formatted success response 32 | */ 33 | protected formatSuccess(data: any, message?: string): ToolCallResult { 34 | const formattedData = typeof data === 'object' 35 | ? JSON.stringify(data, null, 2) 36 | : String(data); 37 | 38 | return { 39 | content: [ 40 | { 41 | type: 'text', 42 | text: message ? `${message}\n\n${formattedData}` : formattedData, 43 | }, 44 | ], 45 | }; 46 | } 47 | 48 | /** 49 | * Format an error response 50 | * 51 | * @param error Error object or message 52 | * @returns Formatted error response 53 | */ 54 | protected formatError(error: Error | string): ToolCallResult { 55 | const errorMessage = error instanceof Error ? error.message : error; 56 | 57 | return { 58 | content: [ 59 | { 60 | type: 'text', 61 | text: errorMessage, 62 | }, 63 | ], 64 | isError: true, 65 | }; 66 | } 67 | 68 | /** 69 | * Handle tool execution errors 70 | * 71 | * @param handler Function to execute 72 | * @param args Arguments to pass to the handler 73 | * @returns Tool call result 74 | */ 75 | protected async handleExecution( 76 | handler: (args: Record) => Promise, 77 | args: Record 78 | ): Promise { 79 | try { 80 | return await handler(args); 81 | } catch (error) { 82 | if (error instanceof N8nApiError) { 83 | return this.formatError(error.message); 84 | } 85 | 86 | const errorMessage = error instanceof Error 87 | ? error.message 88 | : 'Unknown error occurred'; 89 | 90 | return this.formatError(`Error executing workflow tool: ${errorMessage}`); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/tools/workflow/create.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Workflow Tool 3 | * 4 | * This tool creates a new workflow in n8n. 5 | */ 6 | 7 | import { BaseWorkflowToolHandler } from './base-handler.js'; 8 | import { ToolCallResult, ToolDefinition } from '../../types/index.js'; 9 | import { N8nApiError } from '../../errors/index.js'; 10 | 11 | /** 12 | * Handler for the create_workflow tool 13 | */ 14 | export class CreateWorkflowHandler extends BaseWorkflowToolHandler { 15 | /** 16 | * Execute the tool 17 | * 18 | * @param args Tool arguments containing workflow details 19 | * @returns Created workflow information 20 | */ 21 | async execute(args: Record): Promise { 22 | return this.handleExecution(async (args) => { 23 | const { name, nodes, connections, active, tags } = args; 24 | 25 | if (!name) { 26 | throw new N8nApiError('Missing required parameter: name'); 27 | } 28 | 29 | // Validate nodes if provided 30 | if (nodes && !Array.isArray(nodes)) { 31 | throw new N8nApiError('Parameter "nodes" must be an array'); 32 | } 33 | 34 | // Validate connections if provided 35 | if (connections && typeof connections !== 'object') { 36 | throw new N8nApiError('Parameter "connections" must be an object'); 37 | } 38 | 39 | // Prepare workflow object 40 | const workflowData: Record = { 41 | name, 42 | active: active === true, // Default to false if not specified 43 | }; 44 | 45 | // Add optional fields if provided 46 | if (nodes) workflowData.nodes = nodes; 47 | if (connections) workflowData.connections = connections; 48 | if (tags) workflowData.tags = tags; 49 | 50 | // Create the workflow 51 | const workflow = await this.apiService.createWorkflow(workflowData); 52 | 53 | return this.formatSuccess( 54 | { 55 | id: workflow.id, 56 | name: workflow.name, 57 | active: workflow.active 58 | }, 59 | `Workflow created successfully` 60 | ); 61 | }, args); 62 | } 63 | } 64 | 65 | /** 66 | * Get tool definition for the create_workflow tool 67 | * 68 | * @returns Tool definition 69 | */ 70 | export function getCreateWorkflowToolDefinition(): ToolDefinition { 71 | return { 72 | name: 'create_workflow', 73 | description: 'Create a new workflow in n8n', 74 | inputSchema: { 75 | type: 'object', 76 | properties: { 77 | name: { 78 | type: 'string', 79 | description: 'Name of the workflow', 80 | }, 81 | nodes: { 82 | type: 'array', 83 | description: 'Array of node objects that define the workflow', 84 | items: { 85 | type: 'object', 86 | }, 87 | }, 88 | connections: { 89 | type: 'object', 90 | description: 'Connection mappings between nodes', 91 | }, 92 | active: { 93 | type: 'boolean', 94 | description: 'Whether the workflow should be active upon creation', 95 | }, 96 | tags: { 97 | type: 'array', 98 | description: 'Tags to associate with the workflow', 99 | items: { 100 | type: 'string', 101 | }, 102 | }, 103 | }, 104 | required: ['name'], 105 | }, 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /src/tools/workflow/deactivate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Deactivate Workflow Tool 3 | * 4 | * This tool deactivates an existing workflow in n8n. 5 | */ 6 | 7 | import { BaseWorkflowToolHandler } from './base-handler.js'; 8 | import { ToolCallResult, ToolDefinition } from '../../types/index.js'; 9 | import { N8nApiError } from '../../errors/index.js'; 10 | 11 | /** 12 | * Handler for the deactivate_workflow tool 13 | */ 14 | export class DeactivateWorkflowHandler extends BaseWorkflowToolHandler { 15 | /** 16 | * Execute the tool 17 | * 18 | * @param args Tool arguments containing workflowId 19 | * @returns Deactivation confirmation 20 | */ 21 | async execute(args: Record): Promise { 22 | return this.handleExecution(async (args) => { 23 | const { workflowId } = args; 24 | 25 | if (!workflowId) { 26 | throw new N8nApiError('Missing required parameter: workflowId'); 27 | } 28 | 29 | // Deactivate the workflow 30 | const workflow = await this.apiService.deactivateWorkflow(workflowId); 31 | 32 | return this.formatSuccess( 33 | { 34 | id: workflow.id, 35 | name: workflow.name, 36 | active: workflow.active 37 | }, 38 | `Workflow "${workflow.name}" (ID: ${workflowId}) has been successfully deactivated` 39 | ); 40 | }, args); 41 | } 42 | } 43 | 44 | /** 45 | * Get tool definition for the deactivate_workflow tool 46 | * 47 | * @returns Tool definition 48 | */ 49 | export function getDeactivateWorkflowToolDefinition(): ToolDefinition { 50 | return { 51 | name: 'deactivate_workflow', 52 | description: 'Deactivate a workflow in n8n', 53 | inputSchema: { 54 | type: 'object', 55 | properties: { 56 | workflowId: { 57 | type: 'string', 58 | description: 'ID of the workflow to deactivate', 59 | }, 60 | }, 61 | required: ['workflowId'], 62 | }, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/tools/workflow/delete.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Delete Workflow Tool 3 | * 4 | * This tool deletes an existing workflow from n8n. 5 | */ 6 | 7 | import { BaseWorkflowToolHandler } from './base-handler.js'; 8 | import { ToolCallResult, ToolDefinition } from '../../types/index.js'; 9 | import { N8nApiError } from '../../errors/index.js'; 10 | 11 | /** 12 | * Handler for the delete_workflow tool 13 | */ 14 | export class DeleteWorkflowHandler extends BaseWorkflowToolHandler { 15 | /** 16 | * Execute the tool 17 | * 18 | * @param args Tool arguments containing workflowId 19 | * @returns Deletion confirmation 20 | */ 21 | async execute(args: Record): Promise { 22 | return this.handleExecution(async (args) => { 23 | const { workflowId } = args; 24 | 25 | if (!workflowId) { 26 | throw new N8nApiError('Missing required parameter: workflowId'); 27 | } 28 | 29 | // Get the workflow info first for the confirmation message 30 | const workflow = await this.apiService.getWorkflow(workflowId); 31 | const workflowName = workflow.name; 32 | 33 | // Delete the workflow 34 | await this.apiService.deleteWorkflow(workflowId); 35 | 36 | return this.formatSuccess( 37 | { id: workflowId }, 38 | `Workflow "${workflowName}" (ID: ${workflowId}) has been successfully deleted` 39 | ); 40 | }, args); 41 | } 42 | } 43 | 44 | /** 45 | * Get tool definition for the delete_workflow tool 46 | * 47 | * @returns Tool definition 48 | */ 49 | export function getDeleteWorkflowToolDefinition(): ToolDefinition { 50 | return { 51 | name: 'delete_workflow', 52 | description: 'Delete a workflow from n8n', 53 | inputSchema: { 54 | type: 'object', 55 | properties: { 56 | workflowId: { 57 | type: 'string', 58 | description: 'ID of the workflow to delete', 59 | }, 60 | }, 61 | required: ['workflowId'], 62 | }, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/tools/workflow/get.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get Workflow Tool 3 | * 4 | * This tool retrieves a specific workflow from n8n by ID. 5 | */ 6 | 7 | import { BaseWorkflowToolHandler } from './base-handler.js'; 8 | import { ToolCallResult, ToolDefinition } from '../../types/index.js'; 9 | import { N8nApiError } from '../../errors/index.js'; 10 | 11 | /** 12 | * Handler for the get_workflow tool 13 | */ 14 | export class GetWorkflowHandler extends BaseWorkflowToolHandler { 15 | /** 16 | * Execute the tool 17 | * 18 | * @param args Tool arguments containing workflowId 19 | * @returns Workflow details 20 | */ 21 | async execute(args: Record): Promise { 22 | return this.handleExecution(async (args) => { 23 | const { workflowId } = args; 24 | 25 | if (!workflowId) { 26 | throw new N8nApiError('Missing required parameter: workflowId'); 27 | } 28 | 29 | const workflow = await this.apiService.getWorkflow(workflowId); 30 | 31 | return this.formatSuccess(workflow, `Retrieved workflow: ${workflow.name}`); 32 | }, args); 33 | } 34 | } 35 | 36 | /** 37 | * Get tool definition for the get_workflow tool 38 | * 39 | * @returns Tool definition 40 | */ 41 | export function getGetWorkflowToolDefinition(): ToolDefinition { 42 | return { 43 | name: 'get_workflow', 44 | description: 'Retrieve a specific workflow by ID', 45 | inputSchema: { 46 | type: 'object', 47 | properties: { 48 | workflowId: { 49 | type: 'string', 50 | description: 'ID of the workflow to retrieve', 51 | }, 52 | }, 53 | required: ['workflowId'], 54 | }, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/tools/workflow/handler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Workflow Tools Handler 3 | * 4 | * This module handles calls to workflow-related tools. 5 | */ 6 | 7 | import { ToolCallResult } from '../../types/index.js'; 8 | import { N8nApiError } from '../../errors/index.js'; 9 | import { 10 | ListWorkflowsHandler, 11 | GetWorkflowHandler, 12 | CreateWorkflowHandler, 13 | UpdateWorkflowHandler, 14 | DeleteWorkflowHandler, 15 | ActivateWorkflowHandler, 16 | DeactivateWorkflowHandler, 17 | } from './index.js'; 18 | 19 | /** 20 | * Handle workflow tool calls 21 | * 22 | * @param toolName Name of the tool being called 23 | * @param args Arguments passed to the tool 24 | * @returns Tool call result 25 | */ 26 | export default async function workflowHandler( 27 | toolName: string, 28 | args: Record 29 | ): Promise { 30 | try { 31 | // Route to the appropriate handler based on the tool name 32 | switch (toolName) { 33 | case 'list_workflows': 34 | return await new ListWorkflowsHandler().execute(args); 35 | 36 | case 'get_workflow': 37 | return await new GetWorkflowHandler().execute(args); 38 | 39 | case 'create_workflow': 40 | return await new CreateWorkflowHandler().execute(args); 41 | 42 | case 'update_workflow': 43 | return await new UpdateWorkflowHandler().execute(args); 44 | 45 | case 'delete_workflow': 46 | return await new DeleteWorkflowHandler().execute(args); 47 | 48 | case 'activate_workflow': 49 | return await new ActivateWorkflowHandler().execute(args); 50 | 51 | case 'deactivate_workflow': 52 | return await new DeactivateWorkflowHandler().execute(args); 53 | 54 | default: 55 | throw new N8nApiError(`Unknown workflow tool: ${toolName}`); 56 | } 57 | } catch (error) { 58 | if (error instanceof N8nApiError) { 59 | return { 60 | content: [ 61 | { 62 | type: 'text', 63 | text: error.message, 64 | }, 65 | ], 66 | isError: true, 67 | }; 68 | } 69 | 70 | // Handle unexpected errors 71 | const errorMessage = error instanceof Error 72 | ? error.message 73 | : 'Unknown error occurred'; 74 | 75 | return { 76 | content: [ 77 | { 78 | type: 'text', 79 | text: `Error executing workflow tool: ${errorMessage}`, 80 | }, 81 | ], 82 | isError: true, 83 | }; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/tools/workflow/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Workflow Tools Module 3 | * 4 | * This module provides MCP tools for interacting with n8n workflows. 5 | */ 6 | 7 | import { ToolDefinition } from '../../types/index.js'; 8 | 9 | // Import tool definitions 10 | import { getListWorkflowsToolDefinition, ListWorkflowsHandler } from './list.js'; 11 | import { getGetWorkflowToolDefinition, GetWorkflowHandler } from './get.js'; 12 | import { getCreateWorkflowToolDefinition, CreateWorkflowHandler } from './create.js'; 13 | import { getUpdateWorkflowToolDefinition, UpdateWorkflowHandler } from './update.js'; 14 | import { getDeleteWorkflowToolDefinition, DeleteWorkflowHandler } from './delete.js'; 15 | import { getActivateWorkflowToolDefinition, ActivateWorkflowHandler } from './activate.js'; 16 | import { getDeactivateWorkflowToolDefinition, DeactivateWorkflowHandler } from './deactivate.js'; 17 | 18 | // Export handlers 19 | export { 20 | ListWorkflowsHandler, 21 | GetWorkflowHandler, 22 | CreateWorkflowHandler, 23 | UpdateWorkflowHandler, 24 | DeleteWorkflowHandler, 25 | ActivateWorkflowHandler, 26 | DeactivateWorkflowHandler, 27 | }; 28 | 29 | /** 30 | * Set up workflow management tools 31 | * 32 | * @returns Array of workflow tool definitions 33 | */ 34 | export async function setupWorkflowTools(): Promise { 35 | return [ 36 | getListWorkflowsToolDefinition(), 37 | getGetWorkflowToolDefinition(), 38 | getCreateWorkflowToolDefinition(), 39 | getUpdateWorkflowToolDefinition(), 40 | getDeleteWorkflowToolDefinition(), 41 | getActivateWorkflowToolDefinition(), 42 | getDeactivateWorkflowToolDefinition(), 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /src/tools/workflow/list.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * List Workflows Tool 3 | * 4 | * This tool retrieves a list of workflows from n8n. 5 | */ 6 | 7 | import { BaseWorkflowToolHandler } from './base-handler.js'; 8 | import { ToolCallResult, ToolDefinition, Workflow } from '../../types/index.js'; 9 | 10 | /** 11 | * Handler for the list_workflows tool 12 | */ 13 | export class ListWorkflowsHandler extends BaseWorkflowToolHandler { 14 | /** 15 | * Execute the tool 16 | * 17 | * @param args Tool arguments 18 | * @returns List of workflows 19 | */ 20 | async execute(args: Record): Promise { 21 | return this.handleExecution(async () => { 22 | const workflows = await this.apiService.getWorkflows(); 23 | 24 | // Format the workflows for display 25 | const formattedWorkflows = workflows.map((workflow: Workflow) => ({ 26 | id: workflow.id, 27 | name: workflow.name, 28 | active: workflow.active, 29 | updatedAt: workflow.updatedAt, 30 | })); 31 | 32 | return this.formatSuccess( 33 | formattedWorkflows, 34 | `Found ${formattedWorkflows.length} workflow(s)` 35 | ); 36 | }, args); 37 | } 38 | } 39 | 40 | /** 41 | * Get tool definition for the list_workflows tool 42 | * 43 | * @returns Tool definition 44 | */ 45 | export function getListWorkflowsToolDefinition(): ToolDefinition { 46 | return { 47 | name: 'list_workflows', 48 | description: 'Retrieve a list of all workflows available in n8n', 49 | inputSchema: { 50 | type: 'object', 51 | properties: { 52 | active: { 53 | type: 'boolean', 54 | description: 'Optional filter to show only active or inactive workflows', 55 | }, 56 | }, 57 | required: [], 58 | }, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/tools/workflow/update.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Update Workflow Tool 3 | * 4 | * This tool updates an existing workflow in n8n. 5 | */ 6 | 7 | import { BaseWorkflowToolHandler } from './base-handler.js'; 8 | import { ToolCallResult, ToolDefinition } from '../../types/index.js'; 9 | import { N8nApiError } from '../../errors/index.js'; 10 | 11 | /** 12 | * Handler for the update_workflow tool 13 | */ 14 | export class UpdateWorkflowHandler extends BaseWorkflowToolHandler { 15 | /** 16 | * Execute the tool 17 | * 18 | * @param args Tool arguments containing workflow updates 19 | * @returns Updated workflow information 20 | */ 21 | async execute(args: Record): Promise { 22 | return this.handleExecution(async (args) => { 23 | const { workflowId, name, nodes, connections, active, tags } = args; 24 | 25 | if (!workflowId) { 26 | throw new N8nApiError('Missing required parameter: workflowId'); 27 | } 28 | 29 | // Validate nodes if provided 30 | if (nodes && !Array.isArray(nodes)) { 31 | throw new N8nApiError('Parameter "nodes" must be an array'); 32 | } 33 | 34 | // Validate connections if provided 35 | if (connections && typeof connections !== 'object') { 36 | throw new N8nApiError('Parameter "connections" must be an object'); 37 | } 38 | 39 | // Get the current workflow to update 40 | const currentWorkflow = await this.apiService.getWorkflow(workflowId); 41 | 42 | // Prepare update object with changes 43 | const workflowData: Record = { ...currentWorkflow }; 44 | 45 | // Update fields if provided 46 | if (name !== undefined) workflowData.name = name; 47 | if (nodes !== undefined) workflowData.nodes = nodes; 48 | if (connections !== undefined) workflowData.connections = connections; 49 | if (active !== undefined) workflowData.active = active; 50 | if (tags !== undefined) workflowData.tags = tags; 51 | 52 | // Update the workflow 53 | const updatedWorkflow = await this.apiService.updateWorkflow(workflowId, workflowData); 54 | 55 | // Build a summary of changes 56 | const changesArray = []; 57 | if (name !== undefined && name !== currentWorkflow.name) changesArray.push(`name: "${currentWorkflow.name}" → "${name}"`); 58 | if (active !== undefined && active !== currentWorkflow.active) changesArray.push(`active: ${currentWorkflow.active} → ${active}`); 59 | if (nodes !== undefined) changesArray.push('nodes updated'); 60 | if (connections !== undefined) changesArray.push('connections updated'); 61 | if (tags !== undefined) changesArray.push('tags updated'); 62 | 63 | const changesSummary = changesArray.length > 0 64 | ? `Changes: ${changesArray.join(', ')}` 65 | : 'No changes were made'; 66 | 67 | return this.formatSuccess( 68 | { 69 | id: updatedWorkflow.id, 70 | name: updatedWorkflow.name, 71 | active: updatedWorkflow.active 72 | }, 73 | `Workflow updated successfully. ${changesSummary}` 74 | ); 75 | }, args); 76 | } 77 | } 78 | 79 | /** 80 | * Get tool definition for the update_workflow tool 81 | * 82 | * @returns Tool definition 83 | */ 84 | export function getUpdateWorkflowToolDefinition(): ToolDefinition { 85 | return { 86 | name: 'update_workflow', 87 | description: 'Update an existing workflow in n8n', 88 | inputSchema: { 89 | type: 'object', 90 | properties: { 91 | workflowId: { 92 | type: 'string', 93 | description: 'ID of the workflow to update', 94 | }, 95 | name: { 96 | type: 'string', 97 | description: 'New name for the workflow', 98 | }, 99 | nodes: { 100 | type: 'array', 101 | description: 'Updated array of node objects that define the workflow', 102 | items: { 103 | type: 'object', 104 | }, 105 | }, 106 | connections: { 107 | type: 'object', 108 | description: 'Updated connection mappings between nodes', 109 | }, 110 | active: { 111 | type: 'boolean', 112 | description: 'Whether the workflow should be active', 113 | }, 114 | tags: { 115 | type: 'array', 116 | description: 'Updated tags to associate with the workflow', 117 | items: { 118 | type: 'string', 119 | }, 120 | }, 121 | }, 122 | required: ['workflowId'], 123 | }, 124 | }; 125 | } 126 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Core Types Module 3 | * 4 | * This module provides type definitions used throughout the application 5 | * and bridges compatibility with the MCP SDK. 6 | */ 7 | 8 | // Tool definition for MCP tools 9 | export interface ToolDefinition { 10 | name: string; 11 | description: string; 12 | inputSchema: { 13 | type: string; 14 | properties: Record; 15 | required?: string[]; 16 | }; 17 | } 18 | 19 | // Tool call result for MCP tool responses 20 | export interface ToolCallResult { 21 | content: Array<{ 22 | type: string; 23 | text: string; 24 | }>; 25 | isError?: boolean; 26 | } 27 | 28 | // Type for n8n workflow object 29 | export interface Workflow { 30 | id: string; 31 | name: string; 32 | active: boolean; 33 | nodes: any[]; 34 | connections: any; 35 | createdAt: string; 36 | updatedAt: string; 37 | [key: string]: any; 38 | } 39 | 40 | // Type for n8n execution object 41 | export interface Execution { 42 | id: string; 43 | workflowId: string; 44 | finished: boolean; 45 | mode: string; 46 | startedAt: string; 47 | stoppedAt: string; 48 | status: string; 49 | data: { 50 | resultData: { 51 | runData: any; 52 | }; 53 | }; 54 | [key: string]: any; 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/execution-formatter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Execution Formatter Utilities 3 | * 4 | * This module provides utility functions for formatting execution data 5 | * in a consistent, user-friendly manner. 6 | */ 7 | 8 | import { Execution } from '../types/index.js'; 9 | 10 | /** 11 | * Format basic execution information for display 12 | * 13 | * @param execution Execution object 14 | * @returns Formatted execution summary 15 | */ 16 | export function formatExecutionSummary(execution: Execution): Record { 17 | // Calculate duration 18 | const startedAt = new Date(execution.startedAt); 19 | const stoppedAt = execution.stoppedAt ? new Date(execution.stoppedAt) : new Date(); 20 | const durationMs = stoppedAt.getTime() - startedAt.getTime(); 21 | const durationSeconds = Math.round(durationMs / 1000); 22 | 23 | // Create status indicator emoji 24 | const statusIndicator = getStatusIndicator(execution.status); 25 | 26 | return { 27 | id: execution.id, 28 | workflowId: execution.workflowId, 29 | status: `${statusIndicator} ${execution.status}`, 30 | startedAt: execution.startedAt, 31 | stoppedAt: execution.stoppedAt || 'In progress', 32 | duration: `${durationSeconds}s`, 33 | finished: execution.finished 34 | }; 35 | } 36 | 37 | /** 38 | * Format detailed execution information including node results 39 | * 40 | * @param execution Execution object 41 | * @returns Formatted execution details 42 | */ 43 | export function formatExecutionDetails(execution: Execution): Record { 44 | const summary = formatExecutionSummary(execution); 45 | 46 | // Extract node results 47 | const nodeResults: Record = {}; 48 | if (execution.data?.resultData?.runData) { 49 | for (const [nodeName, nodeData] of Object.entries(execution.data.resultData.runData)) { 50 | try { 51 | // Get the last output 52 | const lastOutput = Array.isArray(nodeData) && nodeData.length > 0 53 | ? nodeData[nodeData.length - 1] 54 | : null; 55 | 56 | if (lastOutput && lastOutput.data && Array.isArray(lastOutput.data.main)) { 57 | // Extract the output data 58 | const outputData = lastOutput.data.main.length > 0 59 | ? lastOutput.data.main[0] 60 | : []; 61 | 62 | nodeResults[nodeName] = { 63 | status: lastOutput.status, 64 | items: outputData.length, 65 | data: outputData.slice(0, 3), // Limit to first 3 items to avoid overwhelming response 66 | }; 67 | } 68 | } catch (error) { 69 | nodeResults[nodeName] = { error: 'Failed to parse node output' }; 70 | } 71 | } 72 | } 73 | 74 | // Add node results and error information to the summary 75 | return { 76 | ...summary, 77 | mode: execution.mode, 78 | nodeResults: nodeResults, 79 | // Include error information if present 80 | error: execution.data?.resultData && 'error' in execution.data.resultData 81 | ? { 82 | message: (execution.data.resultData as any).error?.message, 83 | stack: (execution.data.resultData as any).error?.stack, 84 | } 85 | : undefined, 86 | }; 87 | } 88 | 89 | /** 90 | * Get appropriate status indicator emoji based on execution status 91 | * 92 | * @param status Execution status string 93 | * @returns Status indicator emoji 94 | */ 95 | export function getStatusIndicator(status: string): string { 96 | switch (status) { 97 | case 'success': 98 | return '✅'; // Success 99 | case 'error': 100 | return '❌'; // Error 101 | case 'waiting': 102 | return '⏳'; // Waiting 103 | case 'canceled': 104 | return '🛑'; // Canceled 105 | default: 106 | return '⏱️'; // In progress or unknown 107 | } 108 | } 109 | 110 | /** 111 | * Summarize execution results for more compact display 112 | * 113 | * @param executions Array of execution objects 114 | * @param limit Maximum number of executions to include 115 | * @returns Summary of execution results 116 | */ 117 | export function summarizeExecutions(executions: Execution[], limit: number = 10): Record { 118 | const limitedExecutions = executions.slice(0, limit); 119 | 120 | // Group executions by status 121 | const byStatus: Record = {}; 122 | limitedExecutions.forEach(execution => { 123 | const status = execution.status || 'unknown'; 124 | byStatus[status] = (byStatus[status] || 0) + 1; 125 | }); 126 | 127 | // Calculate success rate 128 | const totalCount = limitedExecutions.length; 129 | const successCount = byStatus.success || 0; 130 | const successRate = totalCount > 0 ? Math.round((successCount / totalCount) * 100) : 0; 131 | 132 | return { 133 | total: totalCount, 134 | byStatus: Object.entries(byStatus).map(([status, count]) => ({ 135 | status: `${getStatusIndicator(status)} ${status}`, 136 | count, 137 | percentage: totalCount > 0 ? Math.round((count / totalCount) * 100) : 0 138 | })), 139 | successRate: `${successRate}%`, 140 | displayed: limitedExecutions.length, 141 | totalAvailable: executions.length 142 | }; 143 | } 144 | -------------------------------------------------------------------------------- /src/utils/resource-formatter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Resource Formatter Utilities 3 | * 4 | * This module provides utility functions for formatting resource data 5 | * in a consistent, user-friendly manner for MCP resources. 6 | */ 7 | 8 | import { Workflow, Execution } from '../types/index.js'; 9 | 10 | /** 11 | * Format workflow summary for static resource listing 12 | * 13 | * @param workflow Workflow object 14 | * @returns Formatted workflow summary 15 | */ 16 | export function formatWorkflowSummary(workflow: Workflow): Record { 17 | return { 18 | id: workflow.id, 19 | name: workflow.name, 20 | active: workflow.active, 21 | status: workflow.active ? '🟢 Active' : '⚪ Inactive', 22 | updatedAt: workflow.updatedAt, 23 | createdAt: workflow.createdAt, 24 | }; 25 | } 26 | 27 | /** 28 | * Format detailed workflow information for dynamic resources 29 | * 30 | * @param workflow Workflow object 31 | * @returns Formatted workflow details 32 | */ 33 | export function formatWorkflowDetails(workflow: Workflow): Record { 34 | const summary = formatWorkflowSummary(workflow); 35 | 36 | // Add additional details 37 | return { 38 | ...summary, 39 | nodes: workflow.nodes.map(node => ({ 40 | id: node.id, 41 | name: node.name, 42 | type: node.type, 43 | position: node.position, 44 | parameters: node.parameters, 45 | })), 46 | connections: workflow.connections, 47 | staticData: workflow.staticData, 48 | settings: workflow.settings, 49 | tags: workflow.tags, 50 | // Exclude potentially sensitive or unnecessary information 51 | // like pinData or other internal fields 52 | }; 53 | } 54 | 55 | /** 56 | * Format execution statistics summary 57 | * 58 | * @param executions Array of execution objects 59 | * @returns Formatted execution statistics 60 | */ 61 | export function formatExecutionStats(executions: Execution[]): Record { 62 | // Group executions by status 63 | const statusCounts: Record = {}; 64 | executions.forEach(execution => { 65 | const status = execution.status || 'unknown'; 66 | statusCounts[status] = (statusCounts[status] || 0) + 1; 67 | }); 68 | 69 | // Calculate success rate 70 | const totalCount = executions.length; 71 | const successCount = statusCounts.success || 0; 72 | const successRate = totalCount > 0 ? Math.round((successCount / totalCount) * 100) : 0; 73 | 74 | // Calculate average execution time 75 | let totalDuration = 0; 76 | let completedCount = 0; 77 | 78 | executions.forEach(execution => { 79 | if (execution.startedAt && execution.stoppedAt) { 80 | const startedAt = new Date(execution.startedAt); 81 | const stoppedAt = new Date(execution.stoppedAt); 82 | const durationMs = stoppedAt.getTime() - startedAt.getTime(); 83 | 84 | totalDuration += durationMs; 85 | completedCount++; 86 | } 87 | }); 88 | 89 | const avgDurationMs = completedCount > 0 ? Math.round(totalDuration / completedCount) : 0; 90 | const avgDurationSec = Math.round(avgDurationMs / 1000); 91 | 92 | // Group executions by workflow 93 | const workflowExecutions: Record = {}; 94 | executions.forEach(execution => { 95 | const workflowId = execution.workflowId; 96 | workflowExecutions[workflowId] = (workflowExecutions[workflowId] || 0) + 1; 97 | }); 98 | 99 | // Get top workflows by execution count 100 | const topWorkflows = Object.entries(workflowExecutions) 101 | .sort((a, b) => b[1] - a[1]) 102 | .slice(0, 5) 103 | .map(([workflowId, count]) => ({ 104 | workflowId, 105 | executionCount: count, 106 | percentage: totalCount > 0 ? Math.round((count / totalCount) * 100) : 0 107 | })); 108 | 109 | return { 110 | total: totalCount, 111 | byStatus: Object.entries(statusCounts).map(([status, count]) => ({ 112 | status, 113 | count, 114 | percentage: totalCount > 0 ? Math.round((count / totalCount) * 100) : 0 115 | })), 116 | successRate: `${successRate}%`, 117 | averageExecutionTime: completedCount > 0 ? `${avgDurationSec}s` : 'N/A', 118 | recentTrend: { 119 | // Recent executions trend - last 24 hours vs previous 24 hours 120 | // This is a placeholder - would need timestamp filtering logic 121 | changePercent: '0%', 122 | description: 'Stable execution rate' 123 | }, 124 | topWorkflows: topWorkflows, 125 | timeUpdated: new Date().toISOString() 126 | }; 127 | } 128 | 129 | /** 130 | * Format resource URI for n8n resources 131 | * 132 | * @param resourceType Type of resource (workflow or execution) 133 | * @param id Optional resource ID for specific resources 134 | * @returns Formatted resource URI 135 | */ 136 | export function formatResourceUri( 137 | resourceType: 'workflow' | 'execution' | 'workflows' | 'execution-stats', 138 | id?: string, 139 | ): string { 140 | if (id) { 141 | const base = ['workflow', 'execution'].includes(resourceType) 142 | ? `${resourceType}s` 143 | : resourceType; 144 | return `n8n://${base}/${id}`; 145 | } 146 | return `n8n://${resourceType}`; 147 | } 148 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Testing System for n8n MCP Server 2 | 3 | This directory contains the testing framework and tests for the n8n MCP Server project. The tests are organized in a hierarchical structure to match the project's architecture. 4 | 5 | ## Test Structure 6 | 7 | - **unit/**: Unit tests for individual components 8 | - **api/**: Tests for API clients and services 9 | - **config/**: Tests for configuration handling 10 | - **errors/**: Tests for error handling 11 | - **resources/**: Tests for MCP resource handlers 12 | - **dynamic/**: Tests for dynamic resource handlers 13 | - **static/**: Tests for static resource handlers 14 | - **tools/**: Tests for MCP tool handlers 15 | - **workflow/**: Tests for workflow-related tools 16 | - **execution/**: Tests for execution-related tools 17 | - **utils/**: Tests for utility functions 18 | 19 | - **integration/**: Integration tests for component interactions 20 | - Tests that verify multiple components work together correctly 21 | 22 | - **e2e/**: End-to-end tests for full server functionality 23 | - Tests that simulate real-world usage scenarios 24 | 25 | - **mocks/**: Mock data and utilities for testing 26 | - Reusable mock data and functions shared across tests 27 | 28 | ## Running Tests 29 | 30 | The project uses Jest as the test runner with ESM support. The following npm scripts are available: 31 | 32 | ```bash 33 | # Run all tests 34 | npm test 35 | 36 | # Run tests in watch mode (useful during development) 37 | npm run test:watch 38 | 39 | # Run tests with coverage report 40 | npm run test:coverage 41 | 42 | # Run specific test file(s) 43 | npm test -- tests/unit/api/client.test.ts 44 | 45 | # Run tests matching a specific pattern 46 | npm test -- -t "should format and return workflows" 47 | ``` 48 | 49 | ## Writing Tests 50 | 51 | ### Test File Naming Convention 52 | 53 | - All test files should end with `.test.ts` 54 | - Test files should be placed in the same directory structure as the source files they test 55 | 56 | ### Test Organization 57 | 58 | Each test file should follow this structure: 59 | 60 | ```typescript 61 | /** 62 | * Description of what's being tested 63 | */ 64 | 65 | import '@jest/globals'; 66 | import { ComponentToTest } from '../../../src/path/to/component.js'; 67 | // Import other dependencies and mocks 68 | 69 | // Mock dependencies 70 | jest.mock('../../../src/path/to/dependency.js'); 71 | 72 | describe('ComponentName', () => { 73 | // Setup and teardown 74 | beforeEach(() => { 75 | // Common setup 76 | }); 77 | 78 | afterEach(() => { 79 | // Common cleanup 80 | }); 81 | 82 | describe('methodName', () => { 83 | it('should do something specific', () => { 84 | // Arrange 85 | // ... 86 | 87 | // Act 88 | // ... 89 | 90 | // Assert 91 | expect(result).toBe(expectedValue); 92 | }); 93 | 94 | // More test cases... 95 | }); 96 | 97 | // More method tests... 98 | }); 99 | ``` 100 | 101 | ### Testing Utilities 102 | 103 | The project provides several testing utilities: 104 | 105 | - **test-setup.ts**: Common setup for all tests 106 | - **mocks/axios-mock.ts**: Utilities for mocking Axios HTTP requests 107 | - **mocks/n8n-fixtures.ts**: Mock data for n8n API responses 108 | 109 | ## Best Practices 110 | 111 | 1. **Isolation**: Each test should be independent and not rely on other tests 112 | 2. **Mock Dependencies**: External dependencies should be mocked 113 | 3. **Descriptive Names**: Use descriptive test and describe names 114 | 4. **Arrange-Act-Assert**: Structure your tests with clear sections 115 | 5. **Coverage**: Aim for high test coverage, especially for critical paths 116 | 6. **Readability**: Write clear, readable tests that serve as documentation 117 | 118 | ## Extending the Test Suite 119 | 120 | When adding new functionality to the project: 121 | 122 | 1. Create corresponding test files in the appropriate directory 123 | 2. Use existing mocks and utilities when possible 124 | 3. Create new mock data in `mocks/` for reusability 125 | 4. Update this README if you add new testing patterns or utilities 126 | 127 | ## Troubleshooting 128 | 129 | If you encounter issues running the tests: 130 | 131 | - Ensure you're using Node.js 20 or later 132 | - Run `npm install` to ensure all dependencies are installed 133 | - Check for ESM compatibility issues if importing CommonJS modules 134 | - Use `console.log` or `console.error` for debugging (removed in production) 135 | -------------------------------------------------------------------------------- /tests/jest-globals.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Jest global type declarations 3 | * This file adds typings for Jest globals to reduce TypeScript errors in test files 4 | */ 5 | 6 | import '@jest/globals'; 7 | 8 | // Declare global Jest types explicitly to help TypeScript 9 | declare global { 10 | // Jest testing functions 11 | const describe: typeof import('@jest/globals').describe; 12 | const it: typeof import('@jest/globals').it; 13 | const test: typeof import('@jest/globals').test; 14 | const expect: typeof import('@jest/globals').expect; 15 | const beforeAll: typeof import('@jest/globals').beforeAll; 16 | const beforeEach: typeof import('@jest/globals').beforeEach; 17 | const afterAll: typeof import('@jest/globals').afterAll; 18 | const afterEach: typeof import('@jest/globals').afterEach; 19 | 20 | // Jest mock functionality 21 | const jest: typeof import('@jest/globals').jest; 22 | 23 | // Additional common helpers 24 | namespace jest { 25 | interface Mock extends Function { 26 | new (...args: Y): T; 27 | (...args: Y): T; 28 | mockImplementation(fn: (...args: Y) => T): this; 29 | mockImplementationOnce(fn: (...args: Y) => T): this; 30 | mockReturnValue(value: T): this; 31 | mockReturnValueOnce(value: T): this; 32 | mockResolvedValue(value: T): this; 33 | mockResolvedValueOnce(value: T): this; 34 | mockRejectedValue(value: any): this; 35 | mockRejectedValueOnce(value: any): this; 36 | mockClear(): this; 37 | mockReset(): this; 38 | mockRestore(): this; 39 | mockName(name: string): this; 40 | getMockName(): string; 41 | mock: { 42 | calls: Y[]; 43 | instances: T[]; 44 | contexts: any[]; 45 | lastCall: Y; 46 | results: Array<{ type: string; value: T }>; 47 | }; 48 | } 49 | 50 | function fn(): Mock; 51 | function fn(implementation: (...args: Y) => T): Mock; 52 | 53 | function spyOn( 54 | object: T, 55 | method: M & string 56 | ): Mock[M]>; 57 | 58 | function mocked(item: T, deep?: boolean): jest.Mocked; 59 | } 60 | } 61 | 62 | export {}; 63 | -------------------------------------------------------------------------------- /tests/mocks/axios-mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Axios mock utilities for n8n MCP Server tests 3 | */ 4 | 5 | import { AxiosRequestConfig, AxiosResponse } from 'axios'; 6 | 7 | export interface MockResponse { 8 | data: any; 9 | status: number; 10 | statusText: string; 11 | headers?: Record; 12 | config?: AxiosRequestConfig; 13 | } 14 | 15 | export const createMockAxiosResponse = (options: Partial = {}): AxiosResponse => { 16 | return { 17 | data: options.data ?? {}, 18 | status: options.status ?? 200, 19 | statusText: options.statusText ?? 'OK', 20 | headers: options.headers ?? {}, 21 | config: options.config ?? {}, 22 | } as AxiosResponse; 23 | }; 24 | 25 | /** 26 | * Create a mock axios instance for testing 27 | */ 28 | export const createMockAxiosInstance = () => { 29 | const mockRequests: Record = {}; 30 | const mockResponses: Record = {}; 31 | 32 | const mockInstance = { 33 | get: jest.fn(), 34 | post: jest.fn(), 35 | put: jest.fn(), 36 | delete: jest.fn(), 37 | interceptors: { 38 | request: { 39 | use: jest.fn(), 40 | }, 41 | response: { 42 | use: jest.fn(), 43 | }, 44 | }, 45 | defaults: {}, 46 | 47 | // Helper method to add mock response 48 | addMockResponse(method: string, url: string, response: MockResponse | Error) { 49 | if (!mockResponses[`${method}:${url}`]) { 50 | mockResponses[`${method}:${url}`] = []; 51 | } 52 | 53 | if (response instanceof Error) { 54 | mockResponses[`${method}:${url}`].push(response as any); 55 | } else { 56 | mockResponses[`${method}:${url}`].push(response); 57 | } 58 | }, 59 | 60 | // Helper method to get request history 61 | getRequestHistory(method: string, url: string) { 62 | return mockRequests[`${method}:${url}`] || []; 63 | }, 64 | 65 | // Reset all mocks 66 | reset() { 67 | Object.keys(mockRequests).forEach(key => { 68 | delete mockRequests[key]; 69 | }); 70 | 71 | Object.keys(mockResponses).forEach(key => { 72 | delete mockResponses[key]; 73 | }); 74 | 75 | mockInstance.get.mockReset(); 76 | mockInstance.post.mockReset(); 77 | mockInstance.put.mockReset(); 78 | mockInstance.delete.mockReset(); 79 | } 80 | }; 81 | 82 | // Setup method implementations 83 | ['get', 'post', 'put', 'delete'].forEach(method => { 84 | mockInstance[method].mockImplementation(async (url: string, data?: any) => { 85 | const requestKey = `${method}:${url}`; 86 | 87 | if (!mockRequests[requestKey]) { 88 | mockRequests[requestKey] = []; 89 | } 90 | 91 | mockRequests[requestKey].push(data); 92 | 93 | if (mockResponses[requestKey] && mockResponses[requestKey].length > 0) { 94 | const response = mockResponses[requestKey].shift(); 95 | 96 | if (response instanceof Error) { 97 | throw response; 98 | } 99 | 100 | return createMockAxiosResponse(response); 101 | } 102 | 103 | throw new Error(`No mock response defined for ${method.toUpperCase()} ${url}`); 104 | }); 105 | }); 106 | 107 | return mockInstance; 108 | }; 109 | 110 | export default { 111 | createMockAxiosResponse, 112 | createMockAxiosInstance, 113 | }; 114 | -------------------------------------------------------------------------------- /tests/mocks/n8n-fixtures.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock fixtures for n8n API responses 3 | */ 4 | 5 | import { Workflow, Execution } from '../../src/types/index.js'; 6 | 7 | /** 8 | * Create a mock workflow for testing 9 | */ 10 | export const createMockWorkflow = (overrides: Partial = {}): Workflow => { 11 | const id = overrides.id ?? 'mock-workflow-1'; 12 | 13 | return { 14 | id, 15 | name: overrides.name ?? `Mock Workflow ${id}`, 16 | active: overrides.active ?? false, 17 | createdAt: overrides.createdAt ?? new Date().toISOString(), 18 | updatedAt: overrides.updatedAt ?? new Date().toISOString(), 19 | nodes: overrides.nodes ?? [ 20 | { 21 | id: 'start', 22 | name: 'Start', 23 | type: 'n8n-nodes-base.start', 24 | parameters: {}, 25 | position: [100, 300], 26 | }, 27 | ], 28 | connections: overrides.connections ?? {}, 29 | settings: overrides.settings ?? {}, 30 | staticData: overrides.staticData ?? null, 31 | pinData: overrides.pinData ?? {}, 32 | ...overrides, 33 | }; 34 | }; 35 | 36 | /** 37 | * Create multiple mock workflows 38 | */ 39 | export const createMockWorkflows = (count: number = 3): Workflow[] => { 40 | return Array.from({ length: count }, (_, i) => 41 | createMockWorkflow({ 42 | id: `mock-workflow-${i + 1}`, 43 | name: `Mock Workflow ${i + 1}`, 44 | active: i % 2 === 0, // Alternate active status 45 | }) 46 | ); 47 | }; 48 | 49 | /** 50 | * Create a mock execution for testing 51 | */ 52 | export const createMockExecution = (overrides: Partial = {}): Execution => { 53 | const id = overrides.id ?? 'mock-execution-1'; 54 | const workflowId = overrides.workflowId ?? 'mock-workflow-1'; 55 | 56 | return { 57 | id, 58 | workflowId, 59 | finished: overrides.finished ?? true, 60 | mode: overrides.mode ?? 'manual', 61 | waitTill: overrides.waitTill ?? null, 62 | startedAt: overrides.startedAt ?? new Date().toISOString(), 63 | stoppedAt: overrides.stoppedAt ?? new Date().toISOString(), 64 | status: overrides.status ?? 'success', 65 | data: overrides.data ?? { 66 | resultData: { 67 | runData: {}, 68 | }, 69 | }, 70 | workflowData: overrides.workflowData ?? createMockWorkflow({ id: workflowId }), 71 | ...overrides, 72 | }; 73 | }; 74 | 75 | /** 76 | * Create multiple mock executions 77 | */ 78 | export const createMockExecutions = (count: number = 3): Execution[] => { 79 | return Array.from({ length: count }, (_, i) => 80 | createMockExecution({ 81 | id: `mock-execution-${i + 1}`, 82 | workflowId: `mock-workflow-${(i % 2) + 1}`, // Alternate between two workflows 83 | status: i % 3 === 0 ? 'success' : i % 3 === 1 ? 'error' : 'waiting', 84 | }) 85 | ); 86 | }; 87 | 88 | /** 89 | * Create mock n8n API responses 90 | */ 91 | export const mockApiResponses = { 92 | workflows: { 93 | list: { 94 | data: createMockWorkflows(), 95 | }, 96 | single: (id: string = 'mock-workflow-1') => createMockWorkflow({ id }), 97 | create: (workflow: Partial = {}) => createMockWorkflow(workflow), 98 | update: (id: string = 'mock-workflow-1', workflow: Partial = {}) => 99 | createMockWorkflow({ ...workflow, id }), 100 | delete: { success: true }, 101 | activate: (id: string = 'mock-workflow-1') => createMockWorkflow({ id, active: true }), 102 | deactivate: (id: string = 'mock-workflow-1') => createMockWorkflow({ id, active: false }), 103 | }, 104 | 105 | executions: { 106 | list: { 107 | data: createMockExecutions(), 108 | }, 109 | single: (id: string = 'mock-execution-1') => createMockExecution({ id }), 110 | delete: { success: true }, 111 | }, 112 | }; 113 | 114 | export default { 115 | createMockWorkflow, 116 | createMockWorkflows, 117 | createMockExecution, 118 | createMockExecutions, 119 | mockApiResponses, 120 | }; 121 | -------------------------------------------------------------------------------- /tests/test-setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Global test setup for n8n MCP Server tests 3 | */ 4 | import { beforeEach, afterEach, jest } from '@jest/globals'; 5 | 6 | // Reset environment variables before each test 7 | beforeEach(() => { 8 | process.env = { 9 | ...process.env, 10 | NODE_ENV: 'test' 11 | }; 12 | }); 13 | 14 | // Clean up after each test 15 | afterEach(() => { 16 | jest.resetAllMocks(); 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | export const mockEnv = (envVars: Record) => { 21 | const originalEnv = process.env; 22 | 23 | beforeEach(() => { 24 | process.env = { 25 | ...originalEnv, 26 | ...envVars 27 | }; 28 | }); 29 | 30 | afterEach(() => { 31 | process.env = originalEnv; 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"], 5 | "esModuleInterop": true, 6 | "rootDir": ".." 7 | }, 8 | "include": [ 9 | "**/*.ts", 10 | "**/*.tsx" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tests/unit/api/simple-client.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple HTTP client tests without complex dependencies 3 | */ 4 | 5 | import { describe, it, expect } from '@jest/globals'; 6 | 7 | // Create a simple HTTP client class to test 8 | class SimpleHttpClient { 9 | constructor(private baseUrl: string, private apiKey: string) {} 10 | 11 | getBaseUrl(): string { 12 | return this.baseUrl; 13 | } 14 | 15 | getApiKey(): string { 16 | return this.apiKey; 17 | } 18 | 19 | buildAuthHeader(): Record { 20 | return { 21 | 'X-N8N-API-KEY': this.apiKey 22 | }; 23 | } 24 | 25 | formatUrl(path: string): string { 26 | return `${this.baseUrl}${path.startsWith('/') ? path : '/' + path}`; 27 | } 28 | } 29 | 30 | describe('SimpleHttpClient', () => { 31 | it('should store baseUrl and apiKey properly', () => { 32 | const baseUrl = 'https://n8n.example.com/api/v1'; 33 | const apiKey = 'test-api-key'; 34 | const client = new SimpleHttpClient(baseUrl, apiKey); 35 | 36 | expect(client.getBaseUrl()).toBe(baseUrl); 37 | expect(client.getApiKey()).toBe(apiKey); 38 | }); 39 | 40 | it('should create proper auth headers', () => { 41 | const client = new SimpleHttpClient('https://n8n.example.com/api/v1', 'test-api-key'); 42 | const headers = client.buildAuthHeader(); 43 | 44 | expect(headers).toEqual({ 'X-N8N-API-KEY': 'test-api-key' }); 45 | }); 46 | 47 | it('should format URLs correctly', () => { 48 | const baseUrl = 'https://n8n.example.com/api/v1'; 49 | const client = new SimpleHttpClient(baseUrl, 'test-api-key'); 50 | 51 | expect(client.formatUrl('workflows')).toBe(`${baseUrl}/workflows`); 52 | expect(client.formatUrl('/workflows')).toBe(`${baseUrl}/workflows`); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/unit/config/environment.test.ts: -------------------------------------------------------------------------------- 1 | import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import findConfig from 'find-config'; // Use the actual find-config 5 | 6 | import { loadEnvironmentVariables } from '../../../src/config/environment'; 7 | import { ENV_VARS } from '../../../src/config/environment'; // To access defined var names 8 | 9 | // Determine project root for placing dummy .env file 10 | let projectRootDir: string | null = null; 11 | let dummyEnvPath: string | null = null; 12 | 13 | try { 14 | const packageJsonPath = findConfig('package.json'); 15 | if (packageJsonPath) { 16 | projectRootDir = path.dirname(packageJsonPath); 17 | dummyEnvPath = path.resolve(projectRootDir, '.env.test_dummy'); // Use a distinct name 18 | } else { 19 | console.error("Could not find project root (package.json). Tests involving .env file might fail or be skipped."); 20 | } 21 | } catch (e) { 22 | console.error("Error finding project root:", e); 23 | } 24 | 25 | 26 | const originalEnv = { ...process.env }; 27 | 28 | const clearTestEnvVars = () => { 29 | delete process.env[ENV_VARS.N8N_API_URL]; 30 | delete process.env[ENV_VARS.N8N_API_KEY]; 31 | delete process.env[ENV_VARS.N8N_WEBHOOK_USERNAME]; 32 | delete process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD]; 33 | }; 34 | 35 | const saveEnvState = () => { 36 | return { ...process.env }; 37 | }; 38 | 39 | const DUMMY_ENV_CONTENT = ` 40 | ${ENV_VARS.N8N_API_URL}=http://dummyapi.com 41 | ${ENV_VARS.N8N_API_KEY}=dummyapikey 42 | ${ENV_VARS.N8N_WEBHOOK_USERNAME}=dummyuser 43 | ${ENV_VARS.N8N_WEBHOOK_PASSWORD}=dummypassword 44 | `; 45 | 46 | describe('loadEnvironmentVariables', () => { 47 | beforeEach(() => { 48 | jest.resetModules(); // Reset module cache, critical for dotenv 49 | process.env = { ...originalEnv }; // Restore original env 50 | clearTestEnvVars(); // Clear our specific vars 51 | 52 | // Ensure dummy .env is clean before each test that might create it 53 | if (dummyEnvPath && fs.existsSync(dummyEnvPath)) { 54 | fs.unlinkSync(dummyEnvPath); 55 | } 56 | }); 57 | 58 | afterEach(() => { 59 | // Restore original env 60 | process.env = { ...originalEnv }; 61 | // Clean up dummy .env file if it exists after a test 62 | if (dummyEnvPath && fs.existsSync(dummyEnvPath)) { 63 | try { 64 | fs.unlinkSync(dummyEnvPath); 65 | } catch (e) { 66 | // In case the test itself deleted it, or it was never created. 67 | } 68 | } 69 | }); 70 | 71 | // Test Case 1: All environment variables set 72 | test('should not change process.env if all required env vars are already set', () => { 73 | process.env[ENV_VARS.N8N_API_URL] = 'http://existingapi.com'; 74 | process.env[ENV_VARS.N8N_API_KEY] = 'existingapikey'; 75 | process.env[ENV_VARS.N8N_WEBHOOK_USERNAME] = 'existinguser'; 76 | process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD] = 'existingpassword'; 77 | 78 | const envStateBeforeLoad = saveEnvState(); 79 | loadEnvironmentVariables(); // Should not load .env because vars are present 80 | 81 | expect(process.env).toEqual(envStateBeforeLoad); 82 | }); 83 | 84 | // Test Case 2: No environment variables set, .env file exists 85 | test('should load vars from .env file if no required env vars are set and .env exists', () => { 86 | if (!projectRootDir || !dummyEnvPath) { 87 | console.warn("Skipping test: Project root not found, cannot create dummy .env file."); 88 | return; // or expect.hasAssertions() with a different path 89 | } 90 | 91 | // Pre-condition: specific vars are not set (cleared in beforeEach) 92 | expect(process.env[ENV_VARS.N8N_API_URL]).toBeUndefined(); 93 | 94 | fs.writeFileSync(dummyEnvPath, DUMMY_ENV_CONTENT); 95 | 96 | // Temporarily point findConfig's "idea" of .env to our dummy .env by hijacking process.env for dotenv 97 | // This is tricky because loadEnvironmentVariables uses findConfig to locate package.json, then path.resolve for .env 98 | // The most straightforward way is to ensure findConfig returns our projectRootDir, and then dotenv loads our dummyEnvPath. 99 | // The current implementation of loadEnvironmentVariables is: 100 | // const projectRoot = findConfig('package.json'); 101 | // if (projectRoot) { 102 | // const envPath = path.resolve(path.dirname(projectRoot), '.env'); <--- This is the key 103 | // dotenv.config({ path: envPath }); 104 | // } 105 | // So, for this test, we need to make `path.resolve` point to `dummyEnvPath` OR make the actual `.env` the dummy. 106 | // Let's rename the actual .env if it exists, place our dummy, then restore. 107 | // A simpler approach for testing: the function loads ".env". So we make our dummy file THE ".env". 108 | 109 | const actualEnvPath = path.resolve(projectRootDir, '.env'); 110 | let actualEnvRenamedPath: string | null = null; 111 | if (fs.existsSync(actualEnvPath)) { 112 | actualEnvRenamedPath = actualEnvPath + '.backup'; 113 | fs.renameSync(actualEnvPath, actualEnvRenamedPath); 114 | } 115 | 116 | fs.writeFileSync(actualEnvPath, DUMMY_ENV_CONTENT); // Write our dummy content to the actual .env path 117 | 118 | try { 119 | loadEnvironmentVariables(); 120 | 121 | expect(process.env[ENV_VARS.N8N_API_URL]).toBe('http://dummyapi.com'); 122 | expect(process.env[ENV_VARS.N8N_API_KEY]).toBe('dummyapikey'); 123 | expect(process.env[ENV_VARS.N8N_WEBHOOK_USERNAME]).toBe('dummyuser'); 124 | expect(process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD]).toBe('dummypassword'); 125 | } finally { 126 | // Clean up: remove our dummy .env and restore original .env if it was renamed 127 | if (fs.existsSync(actualEnvPath)) { 128 | fs.unlinkSync(actualEnvPath); 129 | } 130 | if (actualEnvRenamedPath && fs.existsSync(actualEnvRenamedPath)) { 131 | fs.renameSync(actualEnvRenamedPath, actualEnvPath); 132 | } 133 | } 134 | }); 135 | 136 | // Test Case 3: No environment variables set, no .env file exists 137 | test('should not change process.env if no required env vars are set and no .env file exists', () => { 138 | if (!projectRootDir) { 139 | console.warn("Skipping parts of test: Project root not found, .env file check might be unreliable."); 140 | // We can still proceed as findConfig would return null or the .env file wouldn't be found 141 | } else { 142 | const actualEnvPath = path.resolve(projectRootDir, '.env'); 143 | if (fs.existsSync(actualEnvPath)) { 144 | // This test requires no .env file, so if one exists (e.g. a real one for dev), this test is harder. 145 | // For CI/isolated env, it should be fine. Here we assume it's okay if it doesn't exist. 146 | // If it *does* exist, the test might reflect that it *was* loaded if not handled. 147 | 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.`); 148 | // To be robust, we'd need to ensure it's not there, similar to Test Case 2's cleanup. 149 | // For now, we assume `loadEnvironmentVariables` won't find one if `findConfig` fails or the file is empty/irrelevant. 150 | } 151 | } 152 | 153 | // Vars are cleared in beforeEach 154 | const envStateBeforeLoad = saveEnvState(); 155 | loadEnvironmentVariables(); // Should not find a .env file to load (or findConfig returns null) 156 | 157 | expect(process.env[ENV_VARS.N8N_API_URL]).toBeUndefined(); 158 | expect(process.env[ENV_VARS.N8N_API_KEY]).toBeUndefined(); 159 | expect(process.env[ENV_VARS.N8N_WEBHOOK_USERNAME]).toBeUndefined(); 160 | expect(process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD]).toBeUndefined(); 161 | // Check if other env vars were not disturbed (more robust check) 162 | expect(process.env).toEqual(envStateBeforeLoad); 163 | }); 164 | 165 | // Test Case 4: Some environment variables set 166 | test('should not change process.env if some (but not all) required env vars are set', () => { 167 | process.env[ENV_VARS.N8N_API_URL] = 'http://partialapi.com'; 168 | process.env[ENV_VARS.N8N_API_KEY] = 'partialapikey'; 169 | // N8N_WEBHOOK_USERNAME and N8N_WEBHOOK_PASSWORD are not set (cleared by beforeEach) 170 | 171 | const envStateBeforeLoad = saveEnvState(); 172 | loadEnvironmentVariables(); // Should not load .env because some vars are present 173 | 174 | expect(process.env).toEqual(envStateBeforeLoad); 175 | expect(process.env[ENV_VARS.N8N_API_URL]).toBe('http://partialapi.com'); 176 | expect(process.env[ENV_VARS.N8N_API_KEY]).toBe('partialapikey'); 177 | expect(process.env[ENV_VARS.N8N_WEBHOOK_USERNAME]).toBeUndefined(); 178 | expect(process.env[ENV_VARS.N8N_WEBHOOK_PASSWORD]).toBeUndefined(); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /tests/unit/config/environment.test.ts.bak: -------------------------------------------------------------------------------- 1 | /** 2 | * Environment configuration unit tests 3 | */ 4 | 5 | import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; 6 | import { getEnvConfig, loadEnvironmentVariables, ENV_VARS } from '../../../src/config/environment.js'; 7 | import { McpError } from '@modelcontextprotocol/sdk/types.js'; 8 | import { ErrorCode } from '../../../src/errors/error-codes.js'; 9 | import { mockEnv } from '../../test-setup.js'; 10 | 11 | describe('Environment Configuration', () => { 12 | const originalEnv = process.env; 13 | 14 | beforeEach(() => { 15 | jest.resetModules(); 16 | process.env = { ...originalEnv }; 17 | // Clear environment variables that might interfere with tests 18 | delete process.env[ENV_VARS.N8N_API_URL]; 19 | delete process.env[ENV_VARS.N8N_API_KEY]; 20 | delete process.env[ENV_VARS.DEBUG]; 21 | }); 22 | 23 | afterEach(() => { 24 | process.env = originalEnv; 25 | }); 26 | 27 | describe('loadEnvironmentVariables', () => { 28 | it('should load environment variables from .env file', () => { 29 | // This is mostly a coverage test, as we can't easily verify dotenv behavior 30 | expect(() => loadEnvironmentVariables()).not.toThrow(); 31 | }); 32 | }); 33 | 34 | describe('getEnvConfig', () => { 35 | it('should return a valid config when all required variables are present', () => { 36 | // Setup 37 | process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1'; 38 | process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; 39 | 40 | // Execute 41 | const config = getEnvConfig(); 42 | 43 | // Assert 44 | expect(config).toEqual({ 45 | n8nApiUrl: 'https://n8n.example.com/api/v1', 46 | n8nApiKey: 'test-api-key', 47 | debug: false, 48 | }); 49 | }); 50 | 51 | it('should set debug to true when DEBUG=true', () => { 52 | // Setup 53 | process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1'; 54 | process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; 55 | process.env[ENV_VARS.DEBUG] = 'true'; 56 | 57 | // Execute 58 | const config = getEnvConfig(); 59 | 60 | // Assert 61 | expect(config.debug).toBe(true); 62 | }); 63 | 64 | it('should handle uppercase true for DEBUG', () => { 65 | // Setup 66 | process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1'; 67 | process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; 68 | process.env[ENV_VARS.DEBUG] = 'TRUE'; 69 | 70 | // Execute 71 | const config = getEnvConfig(); 72 | 73 | // Assert 74 | expect(config.debug).toBe(true); 75 | }); 76 | 77 | it('should set debug to false for non-true values', () => { 78 | // Setup 79 | process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1'; 80 | process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; 81 | process.env[ENV_VARS.DEBUG] = 'yes'; 82 | 83 | // Execute 84 | const config = getEnvConfig(); 85 | 86 | // Assert 87 | expect(config.debug).toBe(false); 88 | }); 89 | 90 | it('should throw an error when N8N_API_URL is missing', () => { 91 | // Setup 92 | process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; 93 | 94 | // Execute & Assert 95 | expect(() => getEnvConfig()).toThrow( 96 | new McpError( 97 | ErrorCode.InitializationError, 98 | `Missing required environment variable: ${ENV_VARS.N8N_API_URL}` 99 | ) 100 | ); 101 | }); 102 | 103 | it('should throw an error when N8N_API_KEY is missing', () => { 104 | // Setup 105 | process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1'; 106 | 107 | // Execute & Assert 108 | expect(() => getEnvConfig()).toThrow( 109 | new McpError( 110 | ErrorCode.InitializationError, 111 | `Missing required environment variable: ${ENV_VARS.N8N_API_KEY}` 112 | ) 113 | ); 114 | }); 115 | 116 | it('should throw an error when N8N_API_URL is not a valid URL', () => { 117 | // Setup 118 | process.env[ENV_VARS.N8N_API_URL] = 'invalid-url'; 119 | process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; 120 | 121 | // Execute & Assert 122 | expect(() => getEnvConfig()).toThrow( 123 | new McpError( 124 | ErrorCode.InitializationError, 125 | `Invalid URL format for ${ENV_VARS.N8N_API_URL}: invalid-url` 126 | ) 127 | ); 128 | }); 129 | 130 | it('should allow localhost URLs', () => { 131 | // Setup 132 | process.env[ENV_VARS.N8N_API_URL] = 'http://localhost:5678/api/v1'; 133 | process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; 134 | 135 | // Execute 136 | const config = getEnvConfig(); 137 | 138 | // Assert 139 | expect(config.n8nApiUrl).toBe('http://localhost:5678/api/v1'); 140 | }); 141 | 142 | it('should accept URLs with trailing slashes', () => { 143 | // Setup 144 | process.env[ENV_VARS.N8N_API_URL] = 'https://n8n.example.com/api/v1/'; 145 | process.env[ENV_VARS.N8N_API_KEY] = 'test-api-key'; 146 | 147 | // Execute 148 | const config = getEnvConfig(); 149 | 150 | // Assert 151 | expect(config.n8nApiUrl).toBe('https://n8n.example.com/api/v1/'); 152 | }); 153 | }); 154 | 155 | describe('with mockEnv helper', () => { 156 | // Using the mockEnv helper from test-setup 157 | mockEnv({ 158 | [ENV_VARS.N8N_API_URL]: 'https://n8n.example.com/api/v1', 159 | [ENV_VARS.N8N_API_KEY]: 'test-api-key', 160 | }); 161 | 162 | it('should use the mocked environment variables', () => { 163 | const config = getEnvConfig(); 164 | expect(config.n8nApiUrl).toBe('https://n8n.example.com/api/v1'); 165 | expect(config.n8nApiKey).toBe('test-api-key'); 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /tests/unit/config/simple-environment.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple environment configuration tests 3 | */ 4 | 5 | import { describe, it, expect } from '@jest/globals'; 6 | 7 | // Simple environment validation function to test 8 | function validateEnvironment(env: Record): { 9 | n8nApiUrl: string; 10 | n8nApiKey: string; 11 | debug: boolean; 12 | } { 13 | // Check required variables 14 | if (!env.N8N_API_URL) { 15 | throw new Error('Missing required environment variable: N8N_API_URL'); 16 | } 17 | 18 | if (!env.N8N_API_KEY) { 19 | throw new Error('Missing required environment variable: N8N_API_KEY'); 20 | } 21 | 22 | // Validate URL format 23 | try { 24 | new URL(env.N8N_API_URL); 25 | } catch (error) { 26 | throw new Error(`Invalid URL format for N8N_API_URL: ${env.N8N_API_URL}`); 27 | } 28 | 29 | // Return parsed config 30 | return { 31 | n8nApiUrl: env.N8N_API_URL, 32 | n8nApiKey: env.N8N_API_KEY, 33 | debug: env.DEBUG?.toLowerCase() === 'true' 34 | }; 35 | } 36 | 37 | describe('Environment Configuration', () => { 38 | describe('validateEnvironment', () => { 39 | it('should return a valid config when all required variables are present', () => { 40 | const env = { 41 | N8N_API_URL: 'https://n8n.example.com/api/v1', 42 | N8N_API_KEY: 'test-api-key' 43 | }; 44 | 45 | const config = validateEnvironment(env); 46 | 47 | expect(config).toEqual({ 48 | n8nApiUrl: 'https://n8n.example.com/api/v1', 49 | n8nApiKey: 'test-api-key', 50 | debug: false 51 | }); 52 | }); 53 | 54 | it('should set debug to true when DEBUG=true', () => { 55 | const env = { 56 | N8N_API_URL: 'https://n8n.example.com/api/v1', 57 | N8N_API_KEY: 'test-api-key', 58 | DEBUG: 'true' 59 | }; 60 | 61 | const config = validateEnvironment(env); 62 | 63 | expect(config.debug).toBe(true); 64 | }); 65 | 66 | it('should throw an error when N8N_API_URL is missing', () => { 67 | const env = { 68 | N8N_API_KEY: 'test-api-key' 69 | }; 70 | 71 | expect(() => validateEnvironment(env)).toThrow( 72 | 'Missing required environment variable: N8N_API_URL' 73 | ); 74 | }); 75 | 76 | it('should throw an error when N8N_API_KEY is missing', () => { 77 | const env = { 78 | N8N_API_URL: 'https://n8n.example.com/api/v1' 79 | }; 80 | 81 | expect(() => validateEnvironment(env)).toThrow( 82 | 'Missing required environment variable: N8N_API_KEY' 83 | ); 84 | }); 85 | 86 | it('should throw an error when N8N_API_URL is not a valid URL', () => { 87 | const env = { 88 | N8N_API_URL: 'invalid-url', 89 | N8N_API_KEY: 'test-api-key' 90 | }; 91 | 92 | expect(() => validateEnvironment(env)).toThrow( 93 | 'Invalid URL format for N8N_API_URL: invalid-url' 94 | ); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /tests/unit/resources/dynamic/workflow.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple test for URI Template functionality 3 | */ 4 | 5 | import { describe, it, expect } from '@jest/globals'; 6 | 7 | // Simple functions to test without complex imports 8 | function getWorkflowResourceTemplateUri() { 9 | return 'n8n://workflows/{id}'; 10 | } 11 | 12 | function extractWorkflowIdFromUri(uri: string): string | null { 13 | const regex = /^n8n:\/\/workflows\/([^/]+)$/; 14 | const match = uri.match(regex); 15 | return match ? match[1] : null; 16 | } 17 | 18 | describe('Workflow Resource URI Functions', () => { 19 | describe('getWorkflowResourceTemplateUri', () => { 20 | it('should return the correct URI template', () => { 21 | expect(getWorkflowResourceTemplateUri()).toBe('n8n://workflows/{id}'); 22 | }); 23 | }); 24 | 25 | describe('extractWorkflowIdFromUri', () => { 26 | it('should extract workflow ID from valid URI', () => { 27 | expect(extractWorkflowIdFromUri('n8n://workflows/123abc')).toBe('123abc'); 28 | expect(extractWorkflowIdFromUri('n8n://workflows/workflow-name-with-dashes')).toBe('workflow-name-with-dashes'); 29 | }); 30 | 31 | it('should return null for invalid URI formats', () => { 32 | expect(extractWorkflowIdFromUri('n8n://workflows/')).toBeNull(); 33 | expect(extractWorkflowIdFromUri('n8n://workflows')).toBeNull(); 34 | expect(extractWorkflowIdFromUri('n8n://workflow/123')).toBeNull(); 35 | expect(extractWorkflowIdFromUri('invalid://workflows/123')).toBeNull(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/unit/tools/workflow/list.test.ts.bak: -------------------------------------------------------------------------------- 1 | /** 2 | * ListWorkflowsHandler unit tests 3 | */ 4 | 5 | import { describe, it, expect, jest } from '@jest/globals'; 6 | import { getListWorkflowsToolDefinition } from '../../../../src/tools/workflow/list.js'; 7 | import { mockApiResponses } from '../../../mocks/n8n-fixtures.js'; 8 | 9 | // Since this is an integration test, we'll test the definition directly 10 | // rather than mocking the complex handler implementation 11 | jest.mock('../../../../src/tools/workflow/base-handler.js'); 12 | 13 | describe('getListWorkflowsToolDefinition', () => { 14 | it('should return the correct tool definition', () => { 15 | // Execute 16 | const definition = getListWorkflowsToolDefinition(); 17 | 18 | // Assert 19 | expect(definition.name).toBe('list_workflows'); 20 | expect(definition.description).toBeTruthy(); 21 | expect(definition.inputSchema).toBeDefined(); 22 | expect(definition.inputSchema.properties).toHaveProperty('active'); 23 | expect(definition.inputSchema.required).toEqual([]); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/unit/tools/workflow/simple-tool.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple workflow tool tests without complex dependencies 3 | */ 4 | 5 | import { describe, it, expect } from '@jest/globals'; 6 | 7 | interface MockWorkflow { 8 | id: string; 9 | name: string; 10 | active: boolean; 11 | createdAt: string; 12 | updatedAt: string; 13 | nodes: any[]; 14 | } 15 | 16 | interface WorkflowFilter { 17 | active?: boolean; 18 | } 19 | 20 | // Mock workflow data 21 | const mockWorkflows: MockWorkflow[] = [ 22 | { 23 | id: '1234abc', 24 | name: 'Test Workflow 1', 25 | active: true, 26 | createdAt: '2025-03-01T12:00:00.000Z', 27 | updatedAt: '2025-03-02T14:30:00.000Z', 28 | nodes: [] 29 | }, 30 | { 31 | id: '5678def', 32 | name: 'Test Workflow 2', 33 | active: false, 34 | createdAt: '2025-03-01T12:00:00.000Z', 35 | updatedAt: '2025-03-12T10:15:00.000Z', 36 | nodes: [] 37 | } 38 | ]; 39 | 40 | // Simple function to test tool definition 41 | function getListWorkflowsToolDefinition() { 42 | return { 43 | name: 'list_workflows', 44 | description: 'List all workflows with optional filtering by status', 45 | inputSchema: { 46 | type: 'object', 47 | properties: { 48 | active: { 49 | type: 'boolean', 50 | description: 'Filter workflows by active status' 51 | } 52 | }, 53 | required: [] 54 | } 55 | }; 56 | } 57 | 58 | // Simple function to test workflow filtering 59 | function filterWorkflows(workflows: MockWorkflow[], filter: WorkflowFilter): MockWorkflow[] { 60 | if (filter && typeof filter.active === 'boolean') { 61 | return workflows.filter(workflow => workflow.active === filter.active); 62 | } 63 | return workflows; 64 | } 65 | 66 | describe('Workflow Tools', () => { 67 | describe('getListWorkflowsToolDefinition', () => { 68 | it('should return the correct tool definition', () => { 69 | const definition = getListWorkflowsToolDefinition(); 70 | 71 | expect(definition.name).toBe('list_workflows'); 72 | expect(definition.description).toBeTruthy(); 73 | expect(definition.inputSchema).toBeDefined(); 74 | expect(definition.inputSchema.properties).toHaveProperty('active'); 75 | expect(definition.inputSchema.required).toEqual([]); 76 | }); 77 | }); 78 | 79 | describe('filterWorkflows', () => { 80 | it('should return all workflows when no filter is provided', () => { 81 | const result = filterWorkflows(mockWorkflows, {}); 82 | 83 | expect(result).toHaveLength(2); 84 | expect(result).toEqual(mockWorkflows); 85 | }); 86 | 87 | it('should filter workflows by active status when active is true', () => { 88 | const result = filterWorkflows(mockWorkflows, { active: true }); 89 | 90 | expect(result).toHaveLength(1); 91 | expect(result[0].id).toBe('1234abc'); 92 | expect(result[0].active).toBe(true); 93 | }); 94 | 95 | it('should filter workflows by active status when active is false', () => { 96 | const result = filterWorkflows(mockWorkflows, { active: false }); 97 | 98 | expect(result).toHaveLength(1); 99 | expect(result[0].id).toBe('5678def'); 100 | expect(result[0].active).toBe(false); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /tests/unit/utils/execution-formatter.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from '@jest/globals'; 2 | import { 3 | formatExecutionSummary, 4 | formatExecutionDetails, 5 | getStatusIndicator, 6 | summarizeExecutions 7 | } from '../../../src/utils/execution-formatter.js'; 8 | import { 9 | createMockExecution, 10 | createMockExecutions 11 | } from '../../mocks/n8n-fixtures.js'; 12 | 13 | 14 | describe('Execution Formatter Utilities', () => { 15 | describe('getStatusIndicator', () => { 16 | it('returns the correct emoji for known statuses', () => { 17 | expect(getStatusIndicator('success')).toBe('✅'); 18 | expect(getStatusIndicator('error')).toBe('❌'); 19 | expect(getStatusIndicator('waiting')).toBe('⏳'); 20 | expect(getStatusIndicator('canceled')).toBe('🛑'); 21 | }); 22 | 23 | it('returns a default emoji for unknown status', () => { 24 | expect(getStatusIndicator('unknown')).toBe('⏱️'); 25 | }); 26 | }); 27 | 28 | describe('formatExecutionSummary', () => { 29 | it('formats execution data into a summary', () => { 30 | const execution = createMockExecution({ 31 | id: 'exec1', 32 | workflowId: 'wf1', 33 | status: 'success', 34 | startedAt: '2025-01-01T00:00:00.000Z', 35 | stoppedAt: '2025-01-01T00:00:05.000Z' 36 | }); 37 | 38 | const summary = formatExecutionSummary(execution); 39 | 40 | expect(summary).toMatchObject({ 41 | id: 'exec1', 42 | workflowId: 'wf1', 43 | status: '✅ success', 44 | startedAt: '2025-01-01T00:00:00.000Z', 45 | stoppedAt: '2025-01-01T00:00:05.000Z', 46 | finished: true 47 | }); 48 | expect(summary.duration).toBe('5s'); 49 | }); 50 | 51 | it('marks execution as in progress when stoppedAt is missing', () => { 52 | const execution = createMockExecution({ 53 | stoppedAt: undefined as any, 54 | status: 'waiting' 55 | }); 56 | 57 | const summary = formatExecutionSummary(execution); 58 | expect(summary.stoppedAt).toBe('In progress'); 59 | }); 60 | }); 61 | 62 | describe('formatExecutionDetails', () => { 63 | it('includes node results when present', () => { 64 | const execution = createMockExecution({ 65 | data: { 66 | resultData: { 67 | runData: { 68 | MyNode: [ 69 | { 70 | status: 'success', 71 | data: { main: [[{ foo: 'bar' }]] } 72 | } 73 | ] 74 | } 75 | } 76 | }, 77 | status: 'success' 78 | }); 79 | 80 | const details = formatExecutionDetails(execution); 81 | expect(details.nodeResults.MyNode).toEqual({ 82 | status: 'success', 83 | items: 1, 84 | data: [{ foo: 'bar' }] 85 | }); 86 | }); 87 | 88 | it('adds error information when present', () => { 89 | const execution = createMockExecution({ 90 | data: { 91 | resultData: { 92 | runData: {}, 93 | error: { message: 'boom', stack: 'trace' } 94 | } as any 95 | }, 96 | status: 'error' 97 | }); 98 | 99 | const details = formatExecutionDetails(execution); 100 | expect(details.error).toEqual({ message: 'boom', stack: 'trace' }); 101 | }); 102 | }); 103 | 104 | describe('summarizeExecutions', () => { 105 | it('summarizes counts and percentages', () => { 106 | const executions = [ 107 | createMockExecution({ status: 'success' }), 108 | createMockExecution({ status: 'error' }), 109 | createMockExecution({ status: 'waiting' }), 110 | createMockExecution({ status: 'success' }) 111 | ]; 112 | 113 | const summary = summarizeExecutions(executions); 114 | 115 | expect(summary.total).toBe(4); 116 | const success = summary.byStatus.find((s: any) => s.status.includes('success')); 117 | const error = summary.byStatus.find((s: any) => s.status.includes('error')); 118 | expect(success.count).toBe(2); 119 | expect(error.count).toBe(1); 120 | expect(summary.successRate).toBe('50%'); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /tests/unit/utils/resource-formatter.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Resource formatter utility tests 3 | */ 4 | 5 | import { describe, it, expect } from '@jest/globals'; 6 | import { formatResourceUri } from '../../../src/utils/resource-formatter.js'; 7 | 8 | describe('formatResourceUri', () => { 9 | it('appends "s" for singular resource types', () => { 10 | expect(formatResourceUri('workflow', '1')).toBe('n8n://workflows/1'); 11 | expect(formatResourceUri('execution', '2')).toBe('n8n://executions/2'); 12 | }); 13 | 14 | it('does not append "s" for already plural resource types', () => { 15 | expect(formatResourceUri('workflows', '3')).toBe('n8n://workflows/3'); 16 | expect(formatResourceUri('execution-stats', '4')).toBe('n8n://execution-stats/4'); 17 | }); 18 | 19 | it('returns URI without id when none is provided', () => { 20 | expect(formatResourceUri('workflow')).toBe('n8n://workflow'); 21 | expect(formatResourceUri('workflows')).toBe('n8n://workflows'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "build", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "rootDir": "src", 15 | "lib": [ 16 | "ES2020", 17 | "DOM" 18 | ], 19 | "types": [ 20 | "node" 21 | ] 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ], 26 | "exclude": [ 27 | "node_modules", 28 | "build", 29 | "**/*.test.ts" 30 | ] 31 | } 32 | --------------------------------------------------------------------------------