├── .editorconfig ├── .eslintrc.js ├── .eslintrc.prepublish.js ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci-cd.yml │ └── pr-validation.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc.js ├── .vscode └── extensions.json ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── __tests__ └── McpClient.test.ts ├── assets ├── brave-search-example.png ├── credentials-envs.png ├── credentials.png ├── execute-tool-result.png ├── execute-tool.png ├── list-tools.png ├── mcp-n8n.png ├── multi-server-example.png ├── operations.png ├── sse-credentials.png └── sse-example.png ├── credentials ├── McpClientApi.credentials.ts ├── McpClientHttpApi.credentials.ts ├── McpClientSseApi.credentials.ts └── mcpClient.svg ├── gulpfile.js ├── index.js ├── jest.config.js ├── nodes └── McpClient │ ├── McpClient.node.test.ts │ ├── McpClient.node.ts │ └── mcpClient.svg ├── package-lock.json ├── package.json ├── pnpm-lock.yaml └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [*.yml] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').ESLint.ConfigData} 3 | */ 4 | module.exports = { 5 | root: true, 6 | 7 | env: { 8 | browser: true, 9 | es6: true, 10 | node: true, 11 | }, 12 | 13 | parser: '@typescript-eslint/parser', 14 | 15 | parserOptions: { 16 | project: ['./tsconfig.json'], 17 | sourceType: 'module', 18 | extraFileExtensions: ['.json'], 19 | }, 20 | 21 | ignorePatterns: ['.eslintrc.js', '**/*.js', '**/node_modules/**', '**/dist/**'], 22 | 23 | overrides: [ 24 | { 25 | files: ['package.json'], 26 | plugins: ['eslint-plugin-n8n-nodes-base'], 27 | extends: ['plugin:n8n-nodes-base/community'], 28 | rules: { 29 | 'n8n-nodes-base/community-package-json-name-still-default': 'off', 30 | }, 31 | }, 32 | { 33 | files: ['./credentials/**/*.ts'], 34 | plugins: ['eslint-plugin-n8n-nodes-base'], 35 | extends: ['plugin:n8n-nodes-base/credentials'], 36 | rules: { 37 | 'n8n-nodes-base/cred-class-field-documentation-url-missing': 'off', 38 | 'n8n-nodes-base/cred-class-field-documentation-url-miscased': 'off', 39 | }, 40 | }, 41 | { 42 | files: ['./nodes/**/*.ts'], 43 | plugins: ['eslint-plugin-n8n-nodes-base'], 44 | extends: ['plugin:n8n-nodes-base/nodes'], 45 | rules: { 46 | 'n8n-nodes-base/node-execute-block-missing-continue-on-fail': 'off', 47 | 'n8n-nodes-base/node-resource-description-filename-against-convention': 'off', 48 | 'n8n-nodes-base/node-param-fixed-collection-type-unsorted-items': 'off', 49 | 'n8n-nodes-base/node-class-description-inputs-wrong-regular-node': 'off', 50 | 'n8n-nodes-base/node-class-description-outputs-wrong': 'off', 51 | }, 52 | }, 53 | ], 54 | }; 55 | -------------------------------------------------------------------------------- /.eslintrc.prepublish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').ESLint.ConfigData} 3 | */ 4 | module.exports = { 5 | extends: "./.eslintrc.js", 6 | 7 | overrides: [ 8 | { 9 | files: ['package.json'], 10 | plugins: ['eslint-plugin-n8n-nodes-base'], 11 | rules: { 12 | 'n8n-nodes-base/community-package-json-name-still-default': 'error', 13 | }, 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 10 8 | versioning-strategy: increase 9 | 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | open-pull-requests-limit: 5 15 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## Related Issue 5 | 6 | Closes # 7 | 8 | ## Type of Change 9 | 10 | - [ ] Bug fix (non-breaking change that fixes an issue) 11 | - [ ] New feature (non-breaking change that adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | - [ ] Documentation update 14 | - [ ] Other (please describe): 15 | 16 | ## How Has This Been Tested? 17 | 18 | 19 | ## Checklist 20 | - [ ] My code follows the code style of this project 21 | - [ ] I have updated the documentation accordingly 22 | - [ ] I have added tests to cover my changes 23 | - [ ] All new and existing tests passed 24 | 25 | ## Release Notes 26 | 27 | 28 | ## Screenshots (if applicable) 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD Pipeline 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | inputs: 10 | release_type: 11 | description: 'Release type (patch, minor, major)' 12 | required: true 13 | default: 'patch' 14 | type: choice 15 | options: 16 | - patch 17 | - minor 18 | - major 19 | 20 | jobs: 21 | build-and-test: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Setup Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: '18' 30 | cache: 'npm' 31 | 32 | - name: Install dependencies 33 | run: npm ci 34 | 35 | - name: Lint 36 | run: npm run lint --if-present || echo "Linting failed but continuing" 37 | 38 | - name: Build 39 | run: npm run build --if-present || echo "Build script not found or build failed but continuing" 40 | 41 | - name: Test 42 | run: npm run test --if-present || echo "No tests found or tests failed but continuing" 43 | 44 | - name: Upload build artifacts 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: dist 48 | path: dist/ 49 | 50 | release: 51 | needs: build-and-test 52 | if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') 53 | runs-on: ubuntu-latest 54 | permissions: 55 | contents: write 56 | packages: write 57 | steps: 58 | - uses: actions/checkout@v4 59 | with: 60 | fetch-depth: 0 61 | token: ${{ secrets.CICD_TOKEN }} 62 | 63 | - name: Setup Node.js 64 | uses: actions/setup-node@v4 65 | with: 66 | node-version: '18' 67 | registry-url: 'https://registry.npmjs.org' 68 | 69 | - name: Install dependencies 70 | run: npm ci 71 | 72 | - name: Download build artifacts 73 | uses: actions/download-artifact@v4 74 | with: 75 | name: dist 76 | path: dist/ 77 | 78 | - name: Configure Git 79 | run: | 80 | git config user.name "GitHub Actions" 81 | git config user.email "actions@github.com" 82 | 83 | - name: Bump version 84 | id: bump_version 85 | if: github.event_name == 'workflow_dispatch' 86 | run: | 87 | RELEASE_TYPE="${{ github.event.inputs.release_type }}" 88 | npm version $RELEASE_TYPE -m "Bump version to %s [skip ci]" 89 | echo "new_version=$(npm pkg get version | tr -d '"')" >> $GITHUB_OUTPUT 90 | 91 | - name: Auto bump patch version 92 | id: auto_bump 93 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 94 | run: | 95 | npm version patch -m "Bump version to %s [skip ci]" 96 | echo "new_version=$(npm pkg get version | tr -d '"')" >> $GITHUB_OUTPUT 97 | 98 | - name: Push changes 99 | run: | 100 | VERSION=$(npm pkg get version | tr -d '"') 101 | git push 102 | git push origin v$VERSION 103 | 104 | - name: Create GitHub Release 105 | uses: softprops/action-gh-release@v1 106 | with: 107 | tag_name: v${{ steps.bump_version.outputs.new_version || steps.auto_bump.outputs.new_version }} 108 | name: Release v${{ steps.bump_version.outputs.new_version || steps.auto_bump.outputs.new_version }} 109 | draft: false 110 | generate_release_notes: true 111 | 112 | - name: Publish to npm 113 | run: npm publish --access public 114 | env: 115 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 116 | -------------------------------------------------------------------------------- /.github/workflows/pr-validation.yml: -------------------------------------------------------------------------------- 1 | name: PR Validation 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | types: [opened, synchronize, reopened] 7 | 8 | jobs: 9 | validate: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: '18' 17 | cache: 'npm' 18 | 19 | - name: Install dependencies 20 | run: npm ci 21 | 22 | - name: Lint 23 | run: npm run lint 24 | 25 | - name: Build 26 | run: npm run build 27 | 28 | - name: Test 29 | run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .tmp 4 | tmp 5 | dist 6 | npm-debug.log* 7 | yarn.lock 8 | .vscode/launch.json 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Git, GitHub and CI/CD 2 | .git 3 | .github 4 | .gitignore 5 | 6 | # Source and dev files 7 | src 8 | tests 9 | .eslintrc 10 | .prettierrc 11 | tsconfig.json 12 | jest.config.js 13 | 14 | # Documentation 15 | docs 16 | *.md 17 | !README.md 18 | !LICENSE.md 19 | 20 | # Misc 21 | .DS_Store 22 | .vscode 23 | coverage 24 | node_modules 25 | 26 | # Assets not needed in package 27 | assets 28 | screenshots 29 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.0 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * https://prettier.io/docs/en/options.html#semicolons 4 | */ 5 | semi: true, 6 | 7 | /** 8 | * https://prettier.io/docs/en/options.html#trailing-commas 9 | */ 10 | trailingComma: 'all', 11 | 12 | /** 13 | * https://prettier.io/docs/en/options.html#bracket-spacing 14 | */ 15 | bracketSpacing: true, 16 | 17 | /** 18 | * https://prettier.io/docs/en/options.html#tabs 19 | */ 20 | useTabs: true, 21 | 22 | /** 23 | * https://prettier.io/docs/en/options.html#tab-width 24 | */ 25 | tabWidth: 2, 26 | 27 | /** 28 | * https://prettier.io/docs/en/options.html#arrow-function-parentheses 29 | */ 30 | arrowParens: 'always', 31 | 32 | /** 33 | * https://prettier.io/docs/en/options.html#quotes 34 | */ 35 | singleQuote: true, 36 | 37 | /** 38 | * https://prettier.io/docs/en/options.html#quote-props 39 | */ 40 | quoteProps: 'as-needed', 41 | 42 | /** 43 | * https://prettier.io/docs/en/options.html#end-of-line 44 | */ 45 | endOfLine: 'lf', 46 | 47 | /** 48 | * https://prettier.io/docs/en/options.html#print-width 49 | */ 50 | printWidth: 100, 51 | }; 52 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "EditorConfig.EditorConfig", 5 | "esbenp.prettier-vscode", 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jan@n8n.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 n8n 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/bd76f121-1c8f-4f5d-9c65-1eac5d81b6af) 2 | 3 | # n8n-nodes-mcp-client 4 | 5 | > **Important Note:** 6 | > The Server-Sent Events (SSE) transport is deprecated and replaced by the new HTTP Streamable transport. SSE remains available for legacy compatibility, but HTTP Streamable is now the recommended method for all new implementations. 7 | 8 | This is an n8n community node that lets you interact with Model Context Protocol (MCP) servers in your n8n workflows. 9 | 10 | MCP is a protocol that enables AI models to interact with external tools and data sources in a standardized way. This node allows you to connect to MCP servers, access resources, execute tools, and use prompts. 11 | 12 | [n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/reference/license/) workflow automation platform. 13 | 14 | [Installation](#installation) 15 | [Credentials](#credentials) 16 | [Environment Variables](#environment-variables) 17 | [Operations](#operations) 18 | [Using as a Tool](#using-as-a-tool) 19 | [Compatibility](#compatibility) 20 | [Resources](#resources) 21 | 22 | ## Getting Started 23 | 24 | Official Quickstart Video: 25 | 26 | [![MCP Client Node Quickstart](/assets/mcp-n8n.png)](https://youtu.be/1t8DQL-jUJk) 27 | 28 | 29 | 30 | ### Community Videos 31 | 32 | Shoutout to all the creators of the following n8n community videos that are great resources for learning how to use this node: 33 | 34 | - [Is MCP the Future of N8N AI Agents? (Fully Tested!)](https://youtu.be/sb5hCcFYPIE) 35 | - [Connect N8N AI Agents to EVERYTHING using MCP?](https://youtu.be/tTDRgkD-120) 36 | - [Build an AI Agent That Can Use Any Tool (MCP in n8n Tutorial)](https://www.youtube.com/watch?v=SVZe2rdhYmA) 37 | - [The NEW N8N MCP is an Absolute Game-Changer (Brave Search MCP)](https://youtu.be/RxXS_FpJyGM) 38 | - [MCP & n8n Automation: The Ultimate Guide for MCP AI Agents (2025)](https://www.youtube.com/watch?v=mbQsnrxHPwE) 39 | - [REVOLUÇÃO na criação de AGENTES no N8N com o MCP Server!!!](https://www.youtube.com/watch?v=zgH85dJcs5c) (Portuguese) 40 | 41 | If you have a great video that you'd like to share, please let me know and I'll add it to the list! 42 | 43 | #### Interested a deeper dive into MCP? 44 | 45 | Check out my YouTube Series [MCP Explained](https://www.youtube.com/playlist?list=PLjOCx_PNfJ4S_oOSqrMi6t9_x1GllvQZO) for more information about the Model Context Protocol. 46 | 47 | ### Security Assessment 48 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/nerding-io-n8n-nodes-mcp-badge.png)](https://mseep.ai/app/nerding-io-n8n-nodes-mcp) 49 | 50 | 51 | ## Installation 52 | 53 | Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community nodes documentation. 54 | 55 | Also pay attention to Environment Variables for [using tools in AI Agents](#using-as-a-tool). It's mandatory to set the `N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE` environment variable to `true` if you want to use the MCP Client node as a tool in AI Agents. 56 | 57 | ## Credentials 58 | 59 | The MCP Client node supports three types of credentials to connect to an MCP server: 60 | 61 | ### Command-line Based Transport (STDIO) 62 | 63 | ![MCP Client STDIO Credentials](./assets/credentials.png) 64 | 65 | - **Command**: The command to start the MCP server 66 | - **Arguments**: Optional arguments to pass to the server command 67 | - **Environment Variables**: Variables to pass to the server in NAME=VALUE format 68 | 69 | ### HTTP Streamable Transport (Recommended) 70 | 71 | - **HTTP Streamable URL**: The HTTP endpoint that supports streaming responses (e.g., http://localhost:3001/stream) 72 | - **Additional Headers**: Optional headers to send with requests (format: name:value, one per line) 73 | 74 | HTTP Streamable is the recommended and modern method for all new integrations, providing better efficiency and flexibility compared to SSE. 75 | 76 | #### Example: Using a Local MCP Server with HTTP Streamable 77 | 78 | This example shows how to connect to a locally running MCP server using HTTP Streamable: 79 | 80 | 1. Start a local MCP server that supports HTTP Streamable: 81 | ```bash 82 | npx @modelcontextprotocol/server-example-streamable 83 | ``` 84 | 85 | 2. Configure MCP Client credentials: 86 | - In the node settings, select **Connection Type**: `HTTP Streamable` 87 | - Create new credentials of type **MCP Client (HTTP Streamable) API** 88 | - Set **HTTP Streamable URL**: `http://localhost:3001/stream` 89 | - Add any required headers for authentication 90 | 91 | 3. Create a workflow using the MCP Client node: 92 | - Add an MCP Client node 93 | - Set the Connection Type to `HTTP Streamable` 94 | - Select your HTTP Streamable credentials 95 | - Execute the workflow to see the results 96 | 97 | ### Server-Sent Events (SSE) Transport (Deprecated, still available for legacy use) 98 | 99 | - **SSE URL**: The URL of the SSE endpoint (default: http://localhost:3001/sse) 100 | - **Messages Post Endpoint**: Optional custom endpoint for posting messages if different from the SSE URL 101 | - **Additional Headers**: Optional headers to send with requests (format: name:value, one per line) 102 | 103 | > **Deprecated:** SSE is deprecated and will not receive further updates, but remains available for legacy compatibility. For new projects, use HTTP Streamable. 104 | 105 | #### Example: Using a Local MCP Server with SSE (legacy) 106 | 107 | This example shows how to connect to a locally running MCP server using Server-Sent Events (SSE): 108 | 109 | 1. Start a local MCP server that supports SSE: 110 | ```bash 111 | npx @modelcontextprotocol/server-example-sse 112 | ``` 113 | 114 | 2. Configure MCP Client credentials: 115 | - In the node settings, select **Connection Type**: `Server-Sent Events (SSE)` 116 | - Create new credentials of type **MCP Client (SSE) API** 117 | - Set **SSE URL**: `http://localhost:3001/sse` 118 | - Add any required headers for authentication 119 | 120 | 3. Create a workflow using the MCP Client node: 121 | - Add an MCP Client node 122 | - Set the Connection Type to `Server-Sent Events (SSE)` 123 | - Select your SSE credentials 124 | - Execute the workflow to see the results 125 | 126 | > **Note:** For new projects, HTTP Streamable is strongly recommended. 127 | 128 | ## Environment Variables 129 | 130 | The MCP Client node supports passing environment variables to MCP servers using the command-line based transport in two ways: 131 | 132 | ### 1. Using the Credentials UI 133 | 134 | You can add environment variables directly in the credentials configuration: 135 | 136 | ![Environment Variables in Credentials](./assets/credentials-envs.png) 137 | 138 | This method is useful for individual setups and testing. The values are stored securely as credentials in n8n. 139 | 140 | ### 2. Using Docker Environment Variables 141 | 142 | For Docker deployments, you can pass environment variables directly to your MCP servers by prefixing them with `MCP_`: 143 | 144 | ```yaml 145 | version: '3' 146 | 147 | services: 148 | n8n: 149 | image: n8nio/n8n 150 | environment: 151 | - MCP_BRAVE_API_KEY=your-api-key-here 152 | - MCP_OPENAI_API_KEY=your-openai-key-here 153 | - MCP_CUSTOM_SETTING=some-value 154 | # other configuration... 155 | ``` 156 | 157 | These environment variables will be automatically passed to your MCP servers when they are executed. 158 | 159 | ### Example: Using Brave Search MCP Server 160 | 161 | This example shows how to set up and use the Brave Search MCP server: 162 | 163 | 1. Install the Brave Search MCP server: 164 | ```bash 165 | npm install -g @modelcontextprotocol/server-brave-search 166 | ``` 167 | 168 | 2. Configure MCP Client credentials: 169 | - **Command**: `npx` 170 | - **Arguments**: `-y @modelcontextprotocol/server-brave-search` 171 | - **Environment Variables**: `BRAVE_API_KEY=your-api-key` Add a variables (space comma or newline separated) 172 | 173 | 3. Create a workflow that uses the MCP Client node: 174 | - Add an MCP Client node 175 | - Select the "List Tools" operation to see available search tools 176 | - Add another MCP Client node 177 | - Select the "Execute Tool" operation 178 | - Choose the "brave_search" tool 179 | - Set Parameters to: `{"query": "latest AI news"}` 180 | 181 | ![Brave Search Example](./assets/brave-search-example.png) 182 | 183 | The node will execute the search and return the results in the output. 184 | 185 | ### Example: Multi-Server Setup with AI Agent 186 | 187 | This example demonstrates how to set up multiple MCP servers in a production environment and use them with an AI agent: 188 | 189 | 1. Configure your docker-compose.yml file: 190 | 191 | ```yaml 192 | version: '3' 193 | 194 | services: 195 | n8n: 196 | image: n8nio/n8n 197 | environment: 198 | # MCP server environment variables 199 | - MCP_BRAVE_API_KEY=your-brave-api-key 200 | - MCP_OPENAI_API_KEY=your-openai-key 201 | - MCP_SERPER_API_KEY=your-serper-key 202 | - MCP_WEATHER_API_KEY=your-weather-api-key 203 | 204 | # Enable community nodes as tools 205 | - N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true 206 | ports: 207 | - "5678:5678" 208 | volumes: 209 | - ~/.n8n:/home/node/.n8n 210 | ``` 211 | 212 | 2. Create multiple MCP Client credentials in n8n: 213 | 214 | **Brave Search Credentials**: 215 | - Command: `npx` 216 | - Arguments: `-y @modelcontextprotocol/server-brave-search` 217 | 218 | **OpenAI Tools Credentials**: 219 | - Command: `npx` 220 | - Arguments: `-y @modelcontextprotocol/server-openai` 221 | 222 | **Web Search Credentials**: 223 | - Command: `npx` 224 | - Arguments: `-y @modelcontextprotocol/server-serper` 225 | 226 | **Weather API Credentials**: 227 | - Command: `npx` 228 | - Arguments: `-y @modelcontextprotocol/server-weather` 229 | 230 | 3. Create an AI Agent workflow: 231 | - Add an AI Agent node 232 | - Enable MCP Client as a tool 233 | - Configure different MCP Client nodes with different credentials 234 | - Create a prompt that uses multiple data sources 235 | 236 | ![Multi-Server Setup](./assets/multi-server-example.png) 237 | 238 | Example AI Agent prompt: 239 | ``` 240 | I need you to help me plan a trip. First, search for popular destinations in {destination_country}. 241 | Then, check the current weather in the top 3 cities. 242 | Finally, find some recent news about travel restrictions for these places. 243 | ``` 244 | 245 | With this setup, the AI agent can use multiple MCP tools across different servers, all using environment variables configured in your Docker deployment. 246 | 247 | ## Operations 248 | 249 | The MCP Client node supports the following operations: 250 | 251 | ![MCP Client Operations](./assets/operations.png) 252 | 253 | - **Execute Tool** - Execute a specific tool with parameters 254 | - **Get Prompt** - Get a specific prompt template 255 | - **List Prompts** - Get a list of available prompts 256 | - **List Resources** - Get a list of available resources from the MCP server 257 | - **List Tools** - Get a list of available tools 258 | - **Read Resource** - Read a specific resource by URI 259 | 260 | ### Example: List Tools Operation 261 | 262 | ![List Tools Example](./assets/list-tools.png) 263 | 264 | The List Tools operation returns all available tools from the MCP server, including their names, descriptions, and parameter schemas. 265 | 266 | ### Example: Execute Tool Operation 267 | 268 | ![Execute Tool Example](./assets/execute-tool.png) 269 | 270 | The Execute Tool operation allows you to execute a specific tool with parameters. Make sure to select the tool you want to execute from the dropdown menu. 271 | 272 | ## Using as a Tool 273 | 274 | This node can be used as a tool in n8n AI Agents. To enable community nodes as tools, you need to set the `N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE` environment variable to `true`. 275 | 276 | ### Setting the Environment Variable 277 | 278 | **If you're using a bash/zsh shell:** 279 | ```bash 280 | export N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true 281 | n8n start 282 | ``` 283 | 284 | **If you're using Docker:** 285 | Add to your docker-compose.yml file: 286 | ```yaml 287 | environment: 288 | - N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true 289 | ``` 290 | 291 | **If you're using the desktop app:** 292 | Create a `.env` file in the n8n directory: 293 | ``` 294 | N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true 295 | ``` 296 | 297 | **If you want to set it permanently on Mac/Linux:** 298 | Add to your `~/.zshrc` or `~/.bash_profile`: 299 | ```bash 300 | export N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true 301 | ``` 302 | 303 | Example of an AI Agent workflow results: 304 | 305 | ![AI Agent Example](./assets/execute-tool-result.png) 306 | 307 | After setting this environment variable and restarting n8n, your MCP Client node will be available as a tool in AI Agent nodes. 308 | 309 | ## Compatibility 310 | 311 | - Requires n8n version 1.0.0 or later 312 | - Compatible with MCP Protocol version 1.0.0 or later 313 | - Supports both STDIO and SSE transports for connecting to MCP servers 314 | - SSE transport requires a server that implements the MCP Server-Sent Events specification 315 | 316 | ## Resources 317 | 318 | * [n8n community nodes documentation](https://docs.n8n.io/integrations/community-nodes/) 319 | * [Model Context Protocol Documentation](https://modelcontextprotocol.io/docs/) 320 | * [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) 321 | * [MCP Transports Overview](https://modelcontextprotocol.io/docs/concepts/transports) 322 | * [Using SSE in MCP](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/sse.ts) 323 | 324 | 325 | -------------------------------------------------------------------------------- /__tests__/McpClient.test.ts: -------------------------------------------------------------------------------- 1 | import { McpClient } from '../nodes/McpClient/McpClient.node'; 2 | 3 | describe('McpClient Node', () => { 4 | let mcpClient: McpClient; 5 | 6 | beforeEach(() => { 7 | mcpClient = new McpClient(); 8 | }); 9 | 10 | it('should have the correct node type', () => { 11 | expect(mcpClient.description.name).toBe('mcpClient'); 12 | }); 13 | 14 | it('should have properties defined', () => { 15 | expect(mcpClient.description.properties).toBeDefined(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /assets/brave-search-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerding-io/n8n-nodes-mcp/03581616cdab8a61f9946d4cb4bedb5851e806b1/assets/brave-search-example.png -------------------------------------------------------------------------------- /assets/credentials-envs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerding-io/n8n-nodes-mcp/03581616cdab8a61f9946d4cb4bedb5851e806b1/assets/credentials-envs.png -------------------------------------------------------------------------------- /assets/credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerding-io/n8n-nodes-mcp/03581616cdab8a61f9946d4cb4bedb5851e806b1/assets/credentials.png -------------------------------------------------------------------------------- /assets/execute-tool-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerding-io/n8n-nodes-mcp/03581616cdab8a61f9946d4cb4bedb5851e806b1/assets/execute-tool-result.png -------------------------------------------------------------------------------- /assets/execute-tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerding-io/n8n-nodes-mcp/03581616cdab8a61f9946d4cb4bedb5851e806b1/assets/execute-tool.png -------------------------------------------------------------------------------- /assets/list-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerding-io/n8n-nodes-mcp/03581616cdab8a61f9946d4cb4bedb5851e806b1/assets/list-tools.png -------------------------------------------------------------------------------- /assets/mcp-n8n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerding-io/n8n-nodes-mcp/03581616cdab8a61f9946d4cb4bedb5851e806b1/assets/mcp-n8n.png -------------------------------------------------------------------------------- /assets/multi-server-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerding-io/n8n-nodes-mcp/03581616cdab8a61f9946d4cb4bedb5851e806b1/assets/multi-server-example.png -------------------------------------------------------------------------------- /assets/operations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerding-io/n8n-nodes-mcp/03581616cdab8a61f9946d4cb4bedb5851e806b1/assets/operations.png -------------------------------------------------------------------------------- /assets/sse-credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerding-io/n8n-nodes-mcp/03581616cdab8a61f9946d4cb4bedb5851e806b1/assets/sse-credentials.png -------------------------------------------------------------------------------- /assets/sse-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerding-io/n8n-nodes-mcp/03581616cdab8a61f9946d4cb4bedb5851e806b1/assets/sse-example.png -------------------------------------------------------------------------------- /credentials/McpClientApi.credentials.ts: -------------------------------------------------------------------------------- 1 | import { ICredentialType, INodeProperties } from 'n8n-workflow'; 2 | 3 | export class McpClientApi implements ICredentialType { 4 | name = 'mcpClientApi'; 5 | displayName = 'MCP Client (STDIO) API'; 6 | 7 | // Cast the icon to the correct type for n8n 8 | icon = 'file:mcpClient.svg' as const; 9 | 10 | properties: INodeProperties[] = [ 11 | { 12 | displayName: 'Command', 13 | name: 'command', 14 | type: 'string', 15 | default: '', 16 | required: true, 17 | description: 'Command to execute (e.g., npx @modelcontextprotocol/client, python script.py)', 18 | }, 19 | { 20 | displayName: 'Arguments', 21 | name: 'args', 22 | type: 'string', 23 | default: '', 24 | description: 25 | 'Command line arguments (space-separated). Do not include API keys or sensitive information here - use Environments instead.', 26 | }, 27 | { 28 | displayName: 'Environments', 29 | name: 'environments', 30 | type: 'string', 31 | default: '', 32 | typeOptions: { 33 | password: true, 34 | }, 35 | description: 36 | 'Environment variables in NAME=VALUE format, separated by commas (e.g., BRAVE_API_KEY=xyz,OPENAI_API_KEY=abc)', 37 | }, 38 | ]; 39 | } 40 | -------------------------------------------------------------------------------- /credentials/McpClientHttpApi.credentials.ts: -------------------------------------------------------------------------------- 1 | import { ICredentialType, INodeProperties } from 'n8n-workflow'; 2 | 3 | export class McpClientHttpApi implements ICredentialType { 4 | name = 'mcpClientHttpApi'; 5 | displayName = 'MCP Client (HTTP Streamable) API'; 6 | 7 | // Cast the icon to the correct type for n8n 8 | icon = 'file:mcpClient.svg' as const; 9 | 10 | properties: INodeProperties[] = [ 11 | { 12 | displayName: 'HTTP Stream URL', 13 | name: 'httpStreamUrl', 14 | type: 'string', 15 | default: 'http://localhost:3001/stream', 16 | required: true, 17 | description: 'URL of the HTTP stream endpoint for the MCP server', 18 | }, 19 | { 20 | displayName: 'HTTP Connection Timeout', 21 | name: 'httpTimeout', 22 | type: 'number', 23 | default: 60000, 24 | required: false, 25 | description: 'Timeout for the HTTP stream connection in milliseconds. Default is 60 seconds.', 26 | }, 27 | { 28 | displayName: 'Messages Post Endpoint', 29 | name: 'messagesPostEndpoint', 30 | type: 'string', 31 | default: '', 32 | description: 'Optional custom endpoint for posting messages (if different from HTTP stream URL)', 33 | }, 34 | { 35 | displayName: 'Additional Headers', 36 | name: 'headers', 37 | type: 'string', 38 | default: '', 39 | description: 'Additional headers to send in the request in NAME=VALUE format, separated by commas (e.g., BRAVE_API_KEY=xyz,OPENAI_API_KEY=abc)', 40 | }, 41 | ]; 42 | } -------------------------------------------------------------------------------- /credentials/McpClientSseApi.credentials.ts: -------------------------------------------------------------------------------- 1 | import { ICredentialType, INodeProperties } from 'n8n-workflow'; 2 | 3 | export class McpClientSseApi implements ICredentialType { 4 | name = 'mcpClientSseApi'; 5 | displayName = 'MCP Client (SSE) API'; 6 | 7 | // Cast the icon to the correct type for n8n 8 | icon = 'file:mcpClient.svg' as const; 9 | 10 | properties: INodeProperties[] = [ 11 | { 12 | displayName: 'SSE URL', 13 | name: 'sseUrl', 14 | type: 'string', 15 | default: 'http://localhost:3001/sse', 16 | required: true, 17 | description: 'URL of the SSE endpoint for the MCP server', 18 | }, 19 | { 20 | displayName: 'SSE Connection Timeout', 21 | name: 'sseTimeout', 22 | type: 'number', 23 | default: 60000, 24 | required: false, 25 | description: 'Timeout for the SSE connection in milliseconds. Default is 60 seconds.', 26 | }, 27 | { 28 | displayName: 'Messages Post Endpoint', 29 | name: 'messagesPostEndpoint', 30 | type: 'string', 31 | default: '', 32 | description: 'Optional custom endpoint for posting messages (if different from SSE URL)', 33 | }, 34 | { 35 | displayName: 'Additional Headers', 36 | name: 'headers', 37 | type: 'string', 38 | default: '', 39 | description: 'Additional headers to send in the request in NAME=VALUE format, separated by commas (e.g., BRAVE_API_KEY=xyz,OPENAI_API_KEY=abc)', 40 | }, 41 | ]; 42 | } 43 | -------------------------------------------------------------------------------- /credentials/mcpClient.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { task, src, dest } = require('gulp'); 3 | 4 | task('build:icons', copyIcons); 5 | 6 | function copyIcons() { 7 | const nodeSource = path.resolve('nodes', '**', '*.{png,svg}'); 8 | const nodeDestination = path.resolve('dist', 'nodes'); 9 | 10 | src(nodeSource).pipe(dest(nodeDestination)); 11 | 12 | const credSource = path.resolve('credentials', '**', '*.{png,svg}'); 13 | const credDestination = path.resolve('dist', 'credentials'); 14 | 15 | return src(credSource).pipe(dest(credDestination)); 16 | } 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // This file ensures n8n can find and load your nodes and credentials 2 | const { McpClient } = require('./dist/nodes/McpClient/McpClient.node.js'); 3 | 4 | module.exports = { 5 | nodeTypes: { 6 | mcpClient: McpClient, 7 | }, 8 | credentialTypes: { 9 | mcpClientApi: require('./dist/credentials/McpClientApi.credentials.js').McpClientApi, 10 | mcpClientSseApi: require('./dist/credentials/McpClientSseApi.credentials.js').McpClientSseApi, 11 | mcpClientHttpApi: require('./dist/credentials/McpClientHttpApi.credentials.js').McpClientHttpApi, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testMatch: ['**/__tests__/**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': [ 7 | 'ts-jest', 8 | { 9 | tsconfig: 'tsconfig.json', 10 | }, 11 | ], 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'json'], 14 | }; 15 | -------------------------------------------------------------------------------- /nodes/McpClient/McpClient.node.test.ts: -------------------------------------------------------------------------------- 1 | // Mock a minimal McpClient class or relevant parts for testing the parsing logic. 2 | // The actual McpClient class has many dependencies that are not needed for these unit tests. 3 | 4 | // Helper function to simulate the core parsing logic for environment variables 5 | const parseEnvVars = (envString: string | undefined): Record => { 6 | const env: Record = {}; 7 | if (envString) { 8 | const envLines = envString.split('\n'); 9 | for (const line of envLines) { 10 | const equalsIndex = line.indexOf('='); 11 | if (equalsIndex > 0) { // Ensure '=' is present and not the first character 12 | const name = line.substring(0, equalsIndex).trim(); 13 | const value = line.substring(equalsIndex + 1).trim(); 14 | if (name && value !== undefined) { 15 | env[name] = value; 16 | } 17 | } 18 | } 19 | } 20 | return env; 21 | }; 22 | 23 | // Helper function to simulate the core parsing logic for headers 24 | const parseHeaders = (headersString: string | undefined): Record => { 25 | const headers: Record = {}; 26 | if (headersString) { 27 | const headerLines = headersString.split('\n'); 28 | for (const line of headerLines) { 29 | const equalsIndex = line.indexOf('='); 30 | if (equalsIndex > 0) { // Ensure '=' is present and not the first character 31 | const name = line.substring(0, equalsIndex).trim(); 32 | const value = line.substring(equalsIndex + 1).trim(); 33 | if (name && value !== undefined) { 34 | headers[name] = value; 35 | } 36 | } 37 | } 38 | } 39 | return headers; 40 | }; 41 | 42 | describe('McpClient Node - Credentials Parsing', () => { 43 | describe('cmdCredentials.environments parsing', () => { 44 | const testCases = [ 45 | { 46 | name: 'Basic Test', 47 | input: 'FOO=bar\nBAZ=qux', 48 | expected: { FOO: 'bar', BAZ: 'qux' }, 49 | }, 50 | { 51 | name: 'Value with Spaces', 52 | input: 'MY_VAR=hello world', 53 | expected: { MY_VAR: 'hello world' }, 54 | }, 55 | { 56 | name: 'Value with Commas', 57 | input: 'LIST_OF_THINGS=apple,banana,orange', 58 | expected: { LIST_OF_THINGS: 'apple,banana,orange' }, 59 | }, 60 | { 61 | name: 'JSON Value', 62 | input: 'JSON_DATA={"key": "value", "number": 123, "nested": {"foo": "bar"}}', 63 | expected: { JSON_DATA: '{"key": "value", "number": 123, "nested": {"foo": "bar"}}' }, 64 | }, 65 | { 66 | name: 'Equals Sign in Value', 67 | input: 'FORMULA=E=mc^2', 68 | expected: { FORMULA: 'E=mc^2' }, 69 | }, 70 | { 71 | name: 'Whitespace Trimming', 72 | input: ' KEY_WITH_SPACES = value with spaces \nANOTHER=value', 73 | expected: { KEY_WITH_SPACES: 'value with spaces', ANOTHER: 'value' }, 74 | }, 75 | { 76 | name: 'Empty Lines and Lines without Equals', 77 | input: 'VALID=true\n\nINVALID_LINE\nANOTHER_VALID=false', 78 | expected: { VALID: 'true', ANOTHER_VALID: 'false' }, 79 | }, 80 | { 81 | name: 'Empty Input', 82 | input: '', 83 | expected: {}, 84 | }, 85 | { 86 | name: 'Line with only Equals Sign', 87 | input: '=', 88 | expected: {}, 89 | }, 90 | { 91 | name: 'Key starting with Equals', 92 | input: '=INVALID_KEY=value', 93 | expected: {}, 94 | }, 95 | { 96 | name: 'Value is just whitespace', 97 | input: 'KEY_WITH_WHITESPACE_VALUE= ', 98 | expected: { KEY_WITH_WHITESPACE_VALUE: '' }, 99 | }, 100 | { 101 | name: 'Value can be empty after key', 102 | input: 'EMPTY_VALUE=', 103 | expected: { EMPTY_VALUE: '' }, 104 | }, 105 | { 106 | name: 'Mixed valid and invalid lines', 107 | input: 'A=1\nB=\n C = 2 \nINVALID\n=startswithEquals\n D = 3 ', 108 | expected: { A: '1', B: '', C: '2', D: '3' }, 109 | }, 110 | ]; 111 | 112 | testCases.forEach(tc => { 113 | it(`should correctly parse: ${tc.name}`, () => { 114 | // In a real test, you would mock this.getCredentials('mcpClientApi') 115 | // and then call a method that uses this parsing logic. 116 | // For this outline, we'll call the helper directly. 117 | const result = parseEnvVars(tc.input); 118 | expect(result).toEqual(tc.expected); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('httpCredentials.headers parsing', () => { 124 | const testCases = [ 125 | { 126 | name: 'Basic Header', 127 | input: 'Content-Type=application/json\nAuthorization=Bearer token123', 128 | expected: { 'Content-Type': 'application/json', Authorization: 'Bearer token123' }, 129 | }, 130 | { 131 | name: 'Header Value with Spaces', 132 | input: 'X-Custom-Header=some value with spaces', 133 | expected: { 'X-Custom-Header': 'some value with spaces' }, 134 | }, 135 | { 136 | name: 'Header Value with Commas', 137 | input: 'Accept-Language=en-US,en;q=0.9,fr;q=0.8', 138 | expected: { 'Accept-Language': 'en-US,en;q=0.9,fr;q=0.8' }, 139 | }, 140 | { 141 | name: 'JSON in Header Value', 142 | input: 'X-Json-Data={"id": 1, "name": "test", "tags": ["a", "b"]}', 143 | expected: { 'X-Json-Data': '{"id": 1, "name": "test", "tags": ["a", "b"]}' }, 144 | }, 145 | { 146 | name: 'Equals Sign in Header Value', 147 | input: 'X-Query-Param=filter=some_value', 148 | expected: { 'X-Query-Param': 'filter=some_value' }, 149 | }, 150 | { 151 | name: 'Whitespace Trimming for Headers', 152 | input: ' X-Padded-Header = padded value \nUser-Agent=N8N', 153 | expected: { 'X-Padded-Header': 'padded value', 'User-Agent': 'N8N' }, 154 | }, 155 | { 156 | name: 'Empty Lines and Invalid Header Lines', 157 | input: 'Valid-Header=valid\n\nNotAValidHeaderLine\nAnother-Header=true', 158 | expected: { 'Valid-Header': 'valid', 'Another-Header': 'true' }, 159 | }, 160 | { 161 | name: 'Empty Input for Headers', 162 | input: '', 163 | expected: {}, 164 | }, 165 | { 166 | name: 'Header value can be empty', 167 | input: 'X-Empty-Value=', 168 | expected: { 'X-Empty-Value': ''} 169 | }, 170 | { 171 | name: 'Mixed valid and invalid header lines', 172 | input: 'Header-One=value1\nHeader-Two=\n Invalid Line \n Header-Three = value3 ', 173 | expected: { 'Header-One': 'value1', 'Header-Two': '', 'Header-Three': 'value3' }, 174 | }, 175 | ]; 176 | 177 | testCases.forEach(tc => { 178 | it(`should correctly parse: ${tc.name}`, () => { 179 | // Similar to above, this is a direct call to the helper for the outline. 180 | const result = parseHeaders(tc.input); 181 | expect(result).toEqual(tc.expected); 182 | }); 183 | }); 184 | }); 185 | 186 | // sseCredentials.headers parsing uses the same logic as httpCredentials.headers 187 | // So, we can reuse the same test cases, just imagining they come from 'mcpClientSseApi' 188 | describe('sseCredentials.headers parsing', () => { 189 | const testCases = [ 190 | { 191 | name: 'Basic Header (SSE)', 192 | input: 'Content-Type=application/json\nAuthorization=Bearer token456', 193 | expected: { 'Content-Type': 'application/json', Authorization: 'Bearer token456' }, 194 | }, 195 | { 196 | name: 'Header Value with Spaces (SSE)', 197 | input: 'X-Sse-Custom-Header=sse value with spaces', 198 | expected: { 'X-Sse-Custom-Header': 'sse value with spaces' }, 199 | }, 200 | { 201 | name: 'JSON in Header Value (SSE)', 202 | input: 'X-Sse-Json-Data={"event": "update", "data": {"value": 42}}', 203 | expected: { 'X-Sse-Json-Data': '{"event": "update", "data": {"value": 42}}' }, 204 | }, 205 | { 206 | name: 'Empty Input for Headers (SSE)', 207 | input: '', 208 | expected: {}, 209 | }, 210 | { 211 | name: 'Header value can be empty (SSE)', 212 | input: 'X-Sse-Empty-Value=', 213 | expected: { 'X-Sse-Empty-Value': ''} 214 | }, 215 | ]; 216 | 217 | testCases.forEach(tc => { 218 | it(`should correctly parse: ${tc.name}`, () => { 219 | // Direct call for the outline. 220 | const result = parseHeaders(tc.input); 221 | expect(result).toEqual(tc.expected); 222 | }); 223 | }); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /nodes/McpClient/McpClient.node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IExecuteFunctions, 3 | INodeExecutionData, 4 | INodeType, 5 | INodeTypeDescription, 6 | NodeConnectionType, 7 | NodeOperationError, 8 | } from 'n8n-workflow'; 9 | import { DynamicStructuredTool } from '@langchain/core/tools'; 10 | import { z } from 'zod'; 11 | import { zodToJsonSchema } from 'zod-to-json-schema'; 12 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; 13 | import { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol'; 14 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; 15 | import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; 16 | import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; 17 | 18 | // Add Node.js process type declaration 19 | declare const process: { 20 | env: Record; 21 | }; 22 | 23 | export class McpClient implements INodeType { 24 | description: INodeTypeDescription = { 25 | displayName: 'MCP Client', 26 | name: 'mcpClient', 27 | icon: 'file:mcpClient.svg', 28 | group: ['transform'], 29 | version: 1, 30 | subtitle: '={{$parameter["operation"]}}', 31 | description: 'Use MCP client', 32 | defaults: { 33 | name: 'MCP Client', 34 | }, 35 | // @ts-ignore - node-class-description-outputs-wrong 36 | inputs: [{ type: NodeConnectionType.Main }], 37 | // @ts-ignore - node-class-description-outputs-wrong 38 | outputs: [{ type: NodeConnectionType.Main }], 39 | usableAsTool: true, 40 | credentials: [ 41 | { 42 | name: 'mcpClientApi', 43 | required: false, 44 | displayOptions: { 45 | show: { 46 | connectionType: ['cmd'], 47 | }, 48 | }, 49 | }, 50 | { 51 | name: 'mcpClientSseApi', 52 | required: false, 53 | displayOptions: { 54 | show: { 55 | connectionType: ['sse'], 56 | }, 57 | }, 58 | }, 59 | { 60 | name: 'mcpClientHttpApi', 61 | required: false, 62 | displayOptions: { 63 | show: { 64 | connectionType: ['http'], 65 | }, 66 | }, 67 | }, 68 | ], 69 | properties: [ 70 | { 71 | displayName: 'Connection Type', 72 | name: 'connectionType', 73 | type: 'options', 74 | options: [ 75 | { 76 | name: 'Command Line (STDIO)', 77 | value: 'cmd', 78 | }, 79 | { 80 | name: 'Server-Sent Events (SSE)', 81 | value: 'sse', 82 | description: 'Deprecated: Use HTTP Streamable instead', 83 | }, 84 | { 85 | name: 'HTTP Streamable', 86 | value: 'http', 87 | description: 'Use HTTP streamable protocol for real-time communication', 88 | }, 89 | ], 90 | default: 'cmd', 91 | description: 'Choose the transport type to connect to MCP server', 92 | }, 93 | { 94 | displayName: 'Operation', 95 | name: 'operation', 96 | type: 'options', 97 | noDataExpression: true, 98 | options: [ 99 | { 100 | name: 'Execute Tool', 101 | value: 'executeTool', 102 | description: 'Execute a specific tool', 103 | action: 'Execute a tool', 104 | }, 105 | { 106 | name: 'Get Prompt', 107 | value: 'getPrompt', 108 | description: 'Get a specific prompt template', 109 | action: 'Get a prompt template', 110 | }, 111 | { 112 | name: 'List Prompts', 113 | value: 'listPrompts', 114 | description: 'Get available prompts', 115 | action: 'List available prompts', 116 | }, 117 | { 118 | name: 'List Resource Templates', 119 | value: 'listResourceTemplates', 120 | description: 'Get a list of available resource templates', 121 | action: 'List available resource templates', 122 | }, 123 | { 124 | name: 'List Resources', 125 | value: 'listResources', 126 | description: 'Get a list of available resources', 127 | action: 'List available resources', 128 | }, 129 | { 130 | name: 'List Tools', 131 | value: 'listTools', 132 | description: 'Get available tools', 133 | action: 'List available tools', 134 | }, 135 | { 136 | name: 'Read Resource', 137 | value: 'readResource', 138 | description: 'Read a specific resource by URI', 139 | action: 'Read a resource', 140 | }, 141 | ], 142 | default: 'listTools', 143 | required: true, 144 | }, 145 | { 146 | displayName: 'Resource URI', 147 | name: 'resourceUri', 148 | type: 'string', 149 | required: true, 150 | displayOptions: { 151 | show: { 152 | operation: ['readResource'], 153 | }, 154 | }, 155 | default: '', 156 | description: 'URI of the resource to read', 157 | }, 158 | { 159 | displayName: 'Tool Name', 160 | name: 'toolName', 161 | type: 'string', 162 | required: true, 163 | displayOptions: { 164 | show: { 165 | operation: ['executeTool'], 166 | }, 167 | }, 168 | default: '', 169 | description: 'Name of the tool to execute', 170 | }, 171 | { 172 | displayName: 'Tool Parameters', 173 | name: 'toolParameters', 174 | type: 'json', 175 | required: true, 176 | displayOptions: { 177 | show: { 178 | operation: ['executeTool'], 179 | }, 180 | }, 181 | default: '{}', 182 | description: 'Parameters to pass to the tool in JSON format', 183 | }, 184 | { 185 | displayName: 'Prompt Name', 186 | name: 'promptName', 187 | type: 'string', 188 | required: true, 189 | displayOptions: { 190 | show: { 191 | operation: ['getPrompt'], 192 | }, 193 | }, 194 | default: '', 195 | description: 'Name of the prompt template to get', 196 | }, 197 | ], 198 | }; 199 | 200 | async execute(this: IExecuteFunctions): Promise { 201 | const returnData: INodeExecutionData[] = []; 202 | const operation = this.getNodeParameter('operation', 0) as string; 203 | let transport: Transport | undefined; 204 | 205 | // For backward compatibility - if connectionType isn't set, default to 'cmd' 206 | let connectionType = 'cmd'; 207 | try { 208 | connectionType = this.getNodeParameter('connectionType', 0) as string; 209 | } catch (error) { 210 | // If connectionType parameter doesn't exist, keep default 'cmd' 211 | this.logger.debug('ConnectionType parameter not found, using default "cmd" transport'); 212 | } 213 | let timeout = 600000; 214 | 215 | try { 216 | if (connectionType === 'http') { 217 | // Use HTTP Streamable transport 218 | const httpCredentials = await this.getCredentials('mcpClientHttpApi'); 219 | 220 | // Dynamically import the HTTP client 221 | const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js'); 222 | 223 | const httpStreamUrl = httpCredentials.httpStreamUrl as string; 224 | const messagesPostEndpoint = (httpCredentials.messagesPostEndpoint as string) || ''; 225 | timeout = httpCredentials.httpTimeout as number || 60000; 226 | 227 | // Parse headers 228 | let headers: Record = {}; 229 | if (httpCredentials.headers) { 230 | const headerLines = (httpCredentials.headers as string).split('\n'); 231 | for (const line of headerLines) { 232 | const equalsIndex = line.indexOf('='); 233 | // Ensure '=' is present and not the first character of the line 234 | if (equalsIndex > 0) { 235 | const name = line.substring(0, equalsIndex).trim(); 236 | const value = line.substring(equalsIndex + 1).trim(); 237 | // Add to headers object if key is not empty and value is defined 238 | if (name && value !== undefined) { 239 | headers[name] = value; 240 | } 241 | } 242 | } 243 | } 244 | 245 | const requestInit: RequestInit = { headers }; 246 | if (messagesPostEndpoint) { 247 | (requestInit as any).endpoint = new URL(messagesPostEndpoint); 248 | } 249 | 250 | transport = new StreamableHTTPClientTransport( 251 | new URL(httpStreamUrl), 252 | { requestInit } 253 | ); 254 | } else if (connectionType === 'sse') { 255 | // Use SSE transport 256 | const sseCredentials = await this.getCredentials('mcpClientSseApi'); 257 | 258 | // Dynamically import the SSE client to avoid TypeScript errors 259 | const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js'); 260 | 261 | const sseUrl = sseCredentials.sseUrl as string; 262 | const messagesPostEndpoint = (sseCredentials.messagesPostEndpoint as string) || ''; 263 | timeout = sseCredentials.sseTimeout as number || 60000; 264 | 265 | // Parse headers 266 | let headers: Record = {}; 267 | if (sseCredentials.headers) { 268 | const headerLines = (sseCredentials.headers as string).split('\n'); 269 | for (const line of headerLines) { 270 | const equalsIndex = line.indexOf('='); 271 | // Ensure '=' is present and not the first character of the line 272 | if (equalsIndex > 0) { 273 | const name = line.substring(0, equalsIndex).trim(); 274 | const value = line.substring(equalsIndex + 1).trim(); 275 | // Add to headers object if key is not empty and value is defined 276 | if (name && value !== undefined) { 277 | headers[name] = value; 278 | } 279 | } 280 | } 281 | } 282 | 283 | // Create SSE transport with dynamic import to avoid TypeScript errors 284 | transport = new SSEClientTransport( 285 | // @ts-ignore 286 | new URL(sseUrl), 287 | { 288 | // @ts-ignore 289 | eventSourceInit: { headers }, 290 | // @ts-ignore 291 | requestInit: { 292 | headers, 293 | ...(messagesPostEndpoint 294 | ? { 295 | // @ts-ignore 296 | endpoint: new URL(messagesPostEndpoint), 297 | } 298 | : {}), 299 | }, 300 | }, 301 | ); 302 | 303 | this.logger.debug(`Created SSE transport for MCP client URL: ${sseUrl}`); 304 | if (messagesPostEndpoint) { 305 | this.logger.debug(`Using custom POST endpoint: ${messagesPostEndpoint}`); 306 | } 307 | } else { 308 | // Use stdio transport (default) 309 | const cmdCredentials = await this.getCredentials('mcpClientApi'); 310 | 311 | // Build environment variables object for MCP servers 312 | const env: Record = { 313 | // Preserve the PATH environment variable to ensure commands can be found 314 | PATH: process.env.PATH || '', 315 | }; 316 | 317 | this.logger.debug(`Original PATH: ${process.env.PATH}`); 318 | 319 | // Parse newline-separated environment variables from credentials 320 | if (cmdCredentials.environments) { 321 | const envLines = (cmdCredentials.environments as string).split('\n'); 322 | for (const line of envLines) { 323 | const equalsIndex = line.indexOf('='); 324 | // Ensure '=' is present and not the first character of the line 325 | if (equalsIndex > 0) { 326 | const name = line.substring(0, equalsIndex).trim(); 327 | const value = line.substring(equalsIndex + 1).trim(); 328 | // Add to env object if key is not empty and value is defined 329 | if (name && value !== undefined) { 330 | env[name] = value; 331 | } 332 | } 333 | } 334 | } 335 | 336 | // Process environment variables from Node.js 337 | // This allows Docker environment variables to override credentials 338 | for (const key in process.env) { 339 | // Only pass through MCP-related environment variables 340 | if (key.startsWith('MCP_') && process.env[key]) { 341 | // Strip off the MCP_ prefix when passing to the MCP server 342 | const envName = key.substring(4); // Remove 'MCP_' 343 | env[envName] = process.env[key] as string; 344 | } 345 | } 346 | 347 | transport = new StdioClientTransport({ 348 | command: cmdCredentials.command as string, 349 | args: (cmdCredentials.args as string)?.split(' ') || [], 350 | env: env, // Always pass the env with PATH preserved 351 | }); 352 | 353 | // Use n8n's logger instead of console.log 354 | this.logger.debug( 355 | `Transport created for MCP client command: ${cmdCredentials.command}, PATH: ${env.PATH}`, 356 | ); 357 | } 358 | 359 | // Add error handling to transport 360 | if (transport) { 361 | transport.onerror = (error: Error) => { 362 | throw new NodeOperationError(this.getNode(), `Transport error: ${error.message}`); 363 | }; 364 | } 365 | 366 | const client = new Client( 367 | { 368 | name: `${McpClient.name}-client`, 369 | version: '1.0.0', 370 | }, 371 | { 372 | capabilities: { 373 | prompts: {}, 374 | resources: {}, 375 | tools: {}, 376 | }, 377 | }, 378 | ); 379 | 380 | try { 381 | if (!transport) { 382 | throw new NodeOperationError(this.getNode(), 'No transport available'); 383 | } 384 | await client.connect(transport); 385 | this.logger.debug('Client connected to MCP server'); 386 | } catch (connectionError) { 387 | this.logger.error(`MCP client connection error: ${(connectionError as Error).message}`); 388 | throw new NodeOperationError( 389 | this.getNode(), 390 | `Failed to connect to MCP server: ${(connectionError as Error).message}`, 391 | ); 392 | } 393 | 394 | // Create a RequestOptions object from environment variables 395 | const requestOptions: RequestOptions = {}; 396 | requestOptions.timeout = timeout; 397 | 398 | switch (operation) { 399 | case 'listResources': { 400 | const resources = await client.listResources(); 401 | returnData.push({ 402 | json: { resources }, 403 | }); 404 | break; 405 | } 406 | 407 | case 'listResourceTemplates': { 408 | const resourceTemplates = await client.listResourceTemplates(); 409 | returnData.push({ 410 | json: { resourceTemplates }, 411 | }); 412 | break; 413 | } 414 | 415 | case 'readResource': { 416 | const uri = this.getNodeParameter('resourceUri', 0) as string; 417 | const resource = await client.readResource({ 418 | uri, 419 | }); 420 | returnData.push({ 421 | json: { resource }, 422 | }); 423 | break; 424 | } 425 | 426 | case 'listTools': { 427 | const rawTools = await client.listTools(); 428 | const tools = Array.isArray(rawTools) 429 | ? rawTools 430 | : Array.isArray(rawTools?.tools) 431 | ? rawTools.tools 432 | : typeof rawTools?.tools === 'object' && rawTools.tools !== null 433 | ? Object.values(rawTools.tools) 434 | : []; 435 | 436 | if (!tools.length) { 437 | this.logger.warn('No tools found from MCP client response.'); 438 | throw new NodeOperationError(this.getNode(), 'No tools found from MCP client'); 439 | } 440 | 441 | const aiTools = tools.map((tool: any) => { 442 | const paramSchema = tool.inputSchema?.properties 443 | ? z.object( 444 | Object.entries(tool.inputSchema.properties).reduce( 445 | (acc: any, [key, prop]: [string, any]) => { 446 | let zodType: z.ZodType; 447 | 448 | switch (prop.type) { 449 | case 'string': 450 | zodType = z.string(); 451 | break; 452 | case 'number': 453 | zodType = z.number(); 454 | break; 455 | case 'integer': 456 | zodType = z.number().int(); 457 | break; 458 | case 'boolean': 459 | zodType = z.boolean(); 460 | break; 461 | case 'array': 462 | if (prop.items?.type === 'string') { 463 | zodType = z.array(z.string()); 464 | } else if (prop.items?.type === 'number') { 465 | zodType = z.array(z.number()); 466 | } else if (prop.items?.type === 'boolean') { 467 | zodType = z.array(z.boolean()); 468 | } else { 469 | zodType = z.array(z.any()); 470 | } 471 | break; 472 | case 'object': 473 | zodType = z.record(z.string(), z.any()); 474 | break; 475 | default: 476 | zodType = z.any(); 477 | } 478 | 479 | if (prop.description) { 480 | zodType = zodType.describe(prop.description); 481 | } 482 | 483 | if (!tool.inputSchema?.required?.includes(key)) { 484 | zodType = zodType.optional(); 485 | } 486 | 487 | return { 488 | ...acc, 489 | [key]: zodType, 490 | }; 491 | }, 492 | {}, 493 | ), 494 | ) 495 | : z.object({}); 496 | 497 | return new DynamicStructuredTool({ 498 | name: tool.name, 499 | description: tool.description || `Execute the ${tool.name} tool`, 500 | schema: paramSchema, 501 | func: async (params) => { 502 | try { 503 | const result = await client.callTool({ 504 | name: tool.name, 505 | arguments: params, 506 | }, CallToolResultSchema, requestOptions); 507 | 508 | return typeof result === 'object' ? JSON.stringify(result) : String(result); 509 | } catch (error) { 510 | throw new NodeOperationError( 511 | this.getNode(), 512 | `Failed to execute ${tool.name}: ${(error as Error).message}`, 513 | ); 514 | } 515 | }, 516 | }); 517 | }); 518 | 519 | returnData.push({ 520 | json: { 521 | tools: aiTools.map((t: DynamicStructuredTool) => ({ 522 | name: t.name, 523 | description: t.description, 524 | schema: zodToJsonSchema(t.schema as z.ZodTypeAny || z.object({})), 525 | })), 526 | }, 527 | }); 528 | break; 529 | } 530 | 531 | case 'executeTool': { 532 | const toolName = this.getNodeParameter('toolName', 0) as string; 533 | let toolParams; 534 | 535 | try { 536 | const rawParams = this.getNodeParameter('toolParameters', 0); 537 | this.logger.debug(`Raw tool parameters: ${JSON.stringify(rawParams)}`); 538 | 539 | // Handle different parameter types 540 | if (rawParams === undefined || rawParams === null) { 541 | // Handle null/undefined case 542 | toolParams = {}; 543 | } else if (typeof rawParams === 'string') { 544 | // Handle string input (typical direct node usage) 545 | if (!rawParams || rawParams.trim() === '') { 546 | toolParams = {}; 547 | } else { 548 | toolParams = JSON.parse(rawParams); 549 | } 550 | } else if (typeof rawParams === 'object') { 551 | // Handle object input (when used as a tool in AI Agent) 552 | toolParams = rawParams; 553 | } else { 554 | // Try to convert other types to object 555 | try { 556 | toolParams = JSON.parse(JSON.stringify(rawParams)); 557 | } catch (parseError) { 558 | throw new NodeOperationError( 559 | this.getNode(), 560 | `Invalid parameter type: ${typeof rawParams}`, 561 | ); 562 | } 563 | } 564 | 565 | // Ensure toolParams is an object 566 | if ( 567 | typeof toolParams !== 'object' || 568 | toolParams === null || 569 | Array.isArray(toolParams) 570 | ) { 571 | throw new NodeOperationError(this.getNode(), 'Tool parameters must be a JSON object'); 572 | } 573 | } catch (error) { 574 | throw new NodeOperationError( 575 | this.getNode(), 576 | `Failed to parse tool parameters: ${(error as Error).message 577 | }. Make sure the parameters are valid JSON.`, 578 | ); 579 | } 580 | 581 | // Validate tool exists before executing 582 | try { 583 | const availableTools = await client.listTools(); 584 | const toolsList = Array.isArray(availableTools) 585 | ? availableTools 586 | : Array.isArray(availableTools?.tools) 587 | ? availableTools.tools 588 | : Object.values(availableTools?.tools || {}); 589 | 590 | const toolExists = toolsList.some((tool: any) => tool.name === toolName); 591 | 592 | if (!toolExists) { 593 | const availableToolNames = toolsList.map((t: any) => t.name).join(', '); 594 | throw new NodeOperationError( 595 | this.getNode(), 596 | `Tool '${toolName}' does not exist. Available tools: ${availableToolNames}`, 597 | ); 598 | } 599 | 600 | this.logger.debug( 601 | `Executing tool: ${toolName} with params: ${JSON.stringify(toolParams)}`, 602 | ); 603 | 604 | const result = await client.callTool({ 605 | name: toolName, 606 | arguments: toolParams, 607 | }, CallToolResultSchema, requestOptions); 608 | 609 | this.logger.debug(`Tool executed successfully: ${JSON.stringify(result)}`); 610 | 611 | returnData.push({ 612 | json: { result }, 613 | }); 614 | } catch (error) { 615 | throw new NodeOperationError( 616 | this.getNode(), 617 | `Failed to execute tool '${toolName}': ${(error as Error).message}`, 618 | ); 619 | } 620 | break; 621 | } 622 | 623 | case 'listPrompts': { 624 | const prompts = await client.listPrompts(); 625 | returnData.push({ 626 | json: { prompts }, 627 | }); 628 | break; 629 | } 630 | 631 | case 'getPrompt': { 632 | const promptName = this.getNodeParameter('promptName', 0) as string; 633 | const prompt = await client.getPrompt({ 634 | name: promptName, 635 | }); 636 | returnData.push({ 637 | json: { prompt }, 638 | }); 639 | break; 640 | } 641 | 642 | default: 643 | throw new NodeOperationError(this.getNode(), `Operation ${operation} not supported`); 644 | } 645 | 646 | return [returnData]; 647 | } catch (error) { 648 | throw new NodeOperationError( 649 | this.getNode(), 650 | `Failed to execute operation: ${(error as Error).message}`, 651 | ); 652 | } finally { 653 | if (transport) { 654 | await transport.close(); 655 | } 656 | } 657 | } 658 | } 659 | -------------------------------------------------------------------------------- /nodes/McpClient/mcpClient.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "n8n-nodes-mcp", 3 | "version": "0.1.28", 4 | "description": "MCP nodes for n8n ", 5 | "keywords": [ 6 | "n8n-community-node-package", 7 | "mcp", 8 | "mcp-client", 9 | "mcp-client-node", 10 | "mcp-client-n8n", 11 | "mcp-client-n8n-node", 12 | "mcp-client-n8n-node-package" 13 | ], 14 | "license": "MIT", 15 | "homepage": "", 16 | "author": { 17 | "name": "Jd Fiscus", 18 | "email": "jd@nerding.io" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/nerding-io/n8n-nodes-mcp.git" 23 | }, 24 | "main": "index.js", 25 | "scripts": { 26 | "build": "tsc && gulp build:icons", 27 | "dev": "tsc --watch", 28 | "format": "prettier nodes credentials --write", 29 | "lint": "eslint nodes credentials package.json", 30 | "lintfix": "eslint nodes credentials package.json --fix", 31 | "prepublishOnly": "npm run build && npm run lint -c .eslintrc.prepublish.js nodes credentials package.json", 32 | "test": "jest" 33 | }, 34 | "files": [ 35 | "dist" 36 | ], 37 | "n8n": { 38 | "n8nNodesApiVersion": 1, 39 | "credentials": [ 40 | "dist/credentials/McpClientApi.credentials.js", 41 | "dist/credentials/McpClientSseApi.credentials.js", 42 | "dist/credentials/McpClientHttpApi.credentials.js" 43 | ], 44 | "nodes": [ 45 | "dist/nodes/McpClient/McpClient.node.js" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@types/jest": "^29.5.14", 50 | "@types/node": "^22.15.21", 51 | "@typescript-eslint/parser": "~5.45", 52 | "eslint": "^8.57.1", 53 | "eslint-plugin-n8n-nodes-base": "^1.11.0", 54 | "gulp": "^4.0.2", 55 | "jest": "^29.7.0", 56 | "n8n-workflow": "*", 57 | "prettier": "^2.7.1", 58 | "ts-jest": "^29.2.6", 59 | "typescript": "~4.8.4" 60 | }, 61 | "dependencies": { 62 | "@langchain/core": "^0.3.43", 63 | "@modelcontextprotocol/sdk": "^1.10.2", 64 | "zod": "^3.24.0", 65 | "zod-to-json-schema": "^3.24.0" 66 | }, 67 | "overrides": { 68 | "pkce-challenge": "3.0.0" 69 | }, 70 | "peerDependencies": { 71 | "n8n-workflow": "*" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "es2019", 7 | "lib": ["es2019", "es2020", "es2022.error"], 8 | "removeComments": true, 9 | "useUnknownInCatchVariables": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "preserveConstEnums": true, 16 | "esModuleInterop": true, 17 | "resolveJsonModule": true, 18 | "incremental": true, 19 | "declaration": true, 20 | "sourceMap": true, 21 | "skipLibCheck": true, 22 | "outDir": "./dist/", 23 | "types": ["node", "jest"] 24 | }, 25 | "include": [ 26 | "credentials/**/*", 27 | "nodes/**/*", 28 | "nodes/**/*.json", 29 | "package.json", 30 | "__tests__/**/*" 31 | ], 32 | } 33 | --------------------------------------------------------------------------------