├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .releaserc.json ├── LICENSE ├── README.md ├── bin ├── run └── run.ts ├── commitlint.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── client-config.ts ├── commands │ ├── index.ts │ └── install.ts ├── index.test.ts ├── index.ts └── logger.ts ├── tsconfig.json └── tsup.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,py,md}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supermemoryai/install-mcp/58775c984ba0b3ad1368913e98d9392a01cbaa97/.env.example -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/github/gitignore/blob/main/Node.gitignore 2 | # and added custom ignores 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional stylelint cache 61 | .stylelintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.development.local 81 | .env.test.local 82 | .env.production.local 83 | .env.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | .parcel-cache 88 | 89 | # Next.js build output 90 | .next 91 | out 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and not Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # vuepress v2.x temp and cache directory 107 | .temp 108 | .cache 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | # custom added ignores below 136 | bundle 137 | dist 138 | node_modules 139 | TODO 140 | # eslintignore specific 141 | .yarn -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | root: true, 4 | parserOptions: { 5 | sourceType: 'module', 6 | }, 7 | parser: '@typescript-eslint/parser', 8 | plugins: ['@typescript-eslint', 'prettier', 'jest', 'unused-imports'], 9 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 10 | rules: { 11 | 'no-console': 1, 12 | 'prettier/prettier': ['error', { endOfLine: 'auto' }], 13 | '@typescript-eslint/no-unused-vars': 'off', 14 | 'unused-imports/no-unused-imports': 'error', 15 | }, 16 | env: { 17 | node: true, 18 | 'jest/globals': true, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: read # for checkout 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write # to be able to publish a GitHub release 16 | issues: write # to be able to comment on released issues 17 | pull-requests: write # to be able to comment on released pull requests 18 | id-token: write # to enable use of OIDC for npm provenance 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 'lts/*' 29 | 30 | - name: Install pnpm 31 | run: npm i pnpm -g 32 | 33 | - name: Install dependencies 34 | run: pnpm install 35 | 36 | - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies 37 | run: pnpm audit signatures 38 | 39 | - name: Lint 40 | run: pnpm lint 41 | 42 | - name: Test 43 | run: pnpm test 44 | 45 | - name: Build 46 | run: pnpm build 47 | 48 | - name: Release 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 52 | run: pnpm release 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/github/gitignore/blob/main/Node.gitignore 2 | # and added custom ignores 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | .fleet 13 | .idea 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Optional stylelint cache 63 | .stylelintcache 64 | 65 | # Microbundle cache 66 | .rpt2_cache/ 67 | .rts2_cache_cjs/ 68 | .rts2_cache_es/ 69 | .rts2_cache_umd/ 70 | 71 | # Optional REPL history 72 | .node_repl_history 73 | 74 | # Output of 'npm pack' 75 | *.tgz 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # dotenv environment variable files 81 | .env 82 | .env.development.local 83 | .env.test.local 84 | .env.production.local 85 | .env.local 86 | 87 | # parcel-bundler cache (https://parceljs.org/) 88 | .cache 89 | .parcel-cache 90 | 91 | # Next.js build output 92 | .next 93 | out 94 | 95 | # Nuxt.js build / generate output 96 | .nuxt 97 | dist 98 | 99 | # Gatsby files 100 | .cache/ 101 | # Comment in the public line in if your project uses Gatsby and not Next.js 102 | # https://nextjs.org/blog/next-9-1#public-directory-support 103 | # public 104 | 105 | # vuepress build output 106 | .vuepress/dist 107 | 108 | # vuepress v2.x temp and cache directory 109 | .temp 110 | .cache 111 | 112 | # Docusaurus cache and generated files 113 | .docusaurus 114 | 115 | 116 | # custom added ignores below 117 | bundle 118 | node_modules 119 | TODO -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm commitlint ${1} 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint:fix 2 | pnpm format:fix 3 | pnpm test 4 | pnpm build 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.json 3 | .prettierrc 4 | .eslintignore 5 | .eslint.cjs 6 | .env.example 7 | .releaserc.json 8 | commitlint.config.js 9 | jest.config.js 10 | tsup.config.ts 11 | coverage 12 | .idea 13 | .editorconfig 14 | .fleet -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/github/gitignore/blob/main/Node.gitignore 2 | # and added custom ignores 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | pnpm-lock.yaml 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | .cache 110 | 111 | # Docusaurus cache and generated files 112 | .docusaurus 113 | 114 | # Serverless directories 115 | .serverless/ 116 | 117 | # FuseBox cache 118 | .fusebox/ 119 | 120 | # DynamoDB Local files 121 | .dynamodb/ 122 | 123 | # TernJS port file 124 | .tern-port 125 | 126 | # Stores VSCode versions used for testing VSCode extensions 127 | .vscode-test 128 | 129 | # yarn v2 130 | .yarn/cache 131 | .yarn/unplugged 132 | .yarn/build-state.yml 133 | .yarn/install-state.gz 134 | .pnp.* 135 | 136 | # custom added ignores below 137 | bundle 138 | dist 139 | node_modules 140 | TODO 141 | # eslintignore specific 142 | .yarn -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 120, 4 | "singleQuote": true, 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "+([0-9])?(.{+([0-9]),x}).x", 4 | "main", 5 | "next", 6 | "next-major", 7 | { 8 | "name": "beta", 9 | "prerelease": true 10 | }, 11 | { 12 | "name": "alpha", 13 | "prerelease": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Dhravya Shah 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Install MCP CLI 2 | 3 | ### A CLI tool to install and manage MCP servers. 4 | 5 | Installing MCPs is a huge pain, so I made a CLI tool to make it easier. 6 | 7 | ## Usage 8 | 9 | Just run 10 | `npx install-mcp '' --client ` 11 | 12 | Also works with SSE URLs 13 | `npx install-mcp '' --client ` 14 | 15 | where `` is one of the following: 16 | 17 | - `claude` 18 | - `cline` 19 | - `roo-cline` 20 | - `windsurf` 21 | - `witsy` 22 | - `enconvo` 23 | - `cursor` 24 | 25 | ## License 26 | 27 | MIT 28 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/run') 4 | -------------------------------------------------------------------------------- /bin/run.ts: -------------------------------------------------------------------------------- 1 | // biome-ignore lint/style/useImportType: 2 | import yargs, { ArgumentsCamelCase, Argv } from 'yargs' 3 | import { config } from 'dotenv' 4 | import { builder, handler, type InstallArgv } from '../src/commands/install' 5 | 6 | config() 7 | 8 | const run = yargs(process.argv.slice(2)).command({ 9 | command: '$0 [target]', 10 | describe: 'Install MCP server', 11 | builder: (yargs) => builder(yargs as unknown as Argv), 12 | handler: (argv) => handler(argv as ArgumentsCamelCase), 13 | }) 14 | 15 | run.help().argv 16 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | modulePathIgnorePatterns: ['/dist/'], 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "install-mcp", 3 | "version": "1.0.6", 4 | "description": "A CLI tool to install and manage MCP servers.", 5 | "bin": { 6 | "install-mcp": "./bin/run" 7 | }, 8 | "directories": { 9 | "lib": "src", 10 | "bin": "bin" 11 | }, 12 | "files": [ 13 | "dist", 14 | "bin" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+ssh://git@github.com/dhravya/install-mcp.git" 19 | }, 20 | "scripts": { 21 | "build": "tsup-node", 22 | "build:watch": "tsup-node --watch", 23 | "clean": "rimraf dist", 24 | "commit": "cz", 25 | "commitlint": "commitlint --edit", 26 | "compile": "tsc", 27 | "format": "prettier . --check", 28 | "format:fix": "prettier . --write", 29 | "lint": "eslint .", 30 | "lint:fix": "eslint . --fix", 31 | "start": "ts-node ./bin/run.ts", 32 | "start:node": "node ./bin/run", 33 | "test": "jest", 34 | "test:watch": "jest --watchAll", 35 | "prepare": "husky", 36 | "release": "semantic-release" 37 | }, 38 | "keywords": [ 39 | "typescript", 40 | "starter", 41 | "cli", 42 | "mcp" 43 | ], 44 | "author": "Dhravya Shah ", 45 | "license": "MIT", 46 | "devDependencies": { 47 | "@commitlint/cli": "^18.6.1", 48 | "@commitlint/config-conventional": "^18.6.3", 49 | "@jest/globals": "^29.7.0", 50 | "@tsconfig/node20": "^20.1.4", 51 | "@types/jest": "^29.5.12", 52 | "@types/node": "^20.12.12", 53 | "@types/prompts": "^2.4.9", 54 | "@types/signale": "^1.4.7", 55 | "@types/yargs": "^17.0.32", 56 | "@typescript-eslint/eslint-plugin": "^6.21.0", 57 | "@typescript-eslint/parser": "^6.21.0", 58 | "commitizen": "^4.3.0", 59 | "cz-conventional-changelog": "^3.3.0", 60 | "eslint": "^8.57.0", 61 | "eslint-config-prettier": "^9.1.0", 62 | "eslint-plugin-jest": "^27.9.0", 63 | "eslint-plugin-prettier": "^5.1.3", 64 | "eslint-plugin-unused-imports": "^3.2.0", 65 | "husky": "^9.0.11", 66 | "jest": "^29.7.0", 67 | "prettier": "^3.2.5", 68 | "rimraf": "^5.0.7", 69 | "semantic-release": "^23.1.1", 70 | "ts-jest": "^29.1.4", 71 | "ts-node": "^10.9.2", 72 | "tsup": "^8.0.2", 73 | "typescript": "^5.4.5" 74 | }, 75 | "dependencies": { 76 | "consola": "^3.2.3", 77 | "dotenv": "^16.4.5", 78 | "giget": "^1.2.3", 79 | "picocolors": "^1.0.1", 80 | "yargs": "^17.7.2" 81 | }, 82 | "config": { 83 | "commitizen": { 84 | "path": "cz-conventional-changelog" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/client-config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import os from 'node:os' 3 | import path from 'node:path' 4 | import process from 'node:process' 5 | 6 | import { verbose } from './logger' 7 | // import { execFileSync } from "node:child_process" 8 | 9 | export interface ClientConfig { 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | [key: string]: any 12 | } 13 | 14 | interface ClientFileTarget { 15 | type: 'file' 16 | path: string 17 | localPath?: string 18 | } 19 | type ClientInstallTarget = ClientFileTarget 20 | 21 | // Initialize platform-specific paths 22 | const homeDir = os.homedir() 23 | 24 | const platformPaths = { 25 | win32: { 26 | baseDir: process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), 27 | vscodePath: path.join('Code', 'User', 'globalStorage'), 28 | }, 29 | darwin: { 30 | baseDir: path.join(homeDir, 'Library', 'Application Support'), 31 | vscodePath: path.join('Code', 'User', 'globalStorage'), 32 | }, 33 | linux: { 34 | baseDir: process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config'), 35 | vscodePath: path.join('Code/User/globalStorage'), 36 | }, 37 | } 38 | 39 | const platform = process.platform as keyof typeof platformPaths 40 | const { baseDir, vscodePath } = platformPaths[platform] 41 | const defaultClaudePath = path.join(baseDir, 'Claude', 'claude_desktop_config.json') 42 | 43 | // Define client paths using the platform-specific base directories 44 | const clientPaths: { [key: string]: ClientInstallTarget } = { 45 | claude: { type: 'file', path: defaultClaudePath }, 46 | cline: { 47 | type: 'file', 48 | path: path.join(baseDir, vscodePath, 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), 49 | }, 50 | 'roo-cline': { 51 | type: 'file', 52 | path: path.join(baseDir, vscodePath, 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json'), 53 | }, 54 | windsurf: { 55 | type: 'file', 56 | path: path.join(homeDir, '.codeium', 'windsurf', 'mcp_config.json'), 57 | }, 58 | witsy: { type: 'file', path: path.join(baseDir, 'Witsy', 'settings.json') }, 59 | enconvo: { 60 | type: 'file', 61 | path: path.join(homeDir, '.config', 'enconvo', 'mcp_config.json'), 62 | }, 63 | cursor: { 64 | type: 'file', 65 | path: path.join(homeDir, '.cursor', 'mcp.json'), 66 | localPath: path.join(process.cwd(), '.cursor', 'mcp.json'), 67 | }, 68 | } 69 | 70 | export const clientNames = Object.keys(clientPaths) 71 | 72 | export function getConfigPath(client?: string, local?: boolean): ClientInstallTarget { 73 | const normalizedClient = client?.toLowerCase() || 'claude' 74 | verbose(`Getting config path for client: ${normalizedClient}${local ? ' (local)' : ''}`) 75 | 76 | const configTarget = clientPaths[normalizedClient] 77 | if (!configTarget) { 78 | return { 79 | type: 'file', 80 | path: path.join(path.dirname(defaultClaudePath), '..', client || 'claude', `${normalizedClient}_config.json`), 81 | } 82 | } 83 | 84 | if (local && configTarget.localPath) { 85 | verbose(`Using local config path for ${normalizedClient}: ${configTarget.localPath}`) 86 | return { ...configTarget, path: configTarget.localPath } 87 | } 88 | 89 | verbose(`Using default config path for ${normalizedClient}: ${configTarget.path}`) 90 | return configTarget 91 | } 92 | 93 | export function readConfig(client: string, local?: boolean): ClientConfig { 94 | verbose(`Reading config for client: ${client}${local ? ' (local)' : ''}`) 95 | try { 96 | const configPath = getConfigPath(client, local) 97 | 98 | verbose(`Checking if config file exists at: ${configPath.path}`) 99 | if (!fs.existsSync(configPath.path)) { 100 | verbose('Config file not found, returning default empty config') 101 | return { mcpServers: {} } 102 | } 103 | 104 | verbose('Reading config file content') 105 | const rawConfig = JSON.parse(fs.readFileSync(configPath.path, 'utf8')) 106 | verbose(`Config loaded successfully: ${JSON.stringify(rawConfig, null, 2)}`) 107 | 108 | return { 109 | ...rawConfig, 110 | mcpServers: rawConfig.mcpServers || {}, 111 | } 112 | } catch (error) { 113 | verbose(`Error reading config: ${error instanceof Error ? error.stack : JSON.stringify(error)}`) 114 | return { mcpServers: {} } 115 | } 116 | } 117 | 118 | export function writeConfig(config: ClientConfig, client?: string, local?: boolean): void { 119 | verbose(`Writing config for client: ${client || 'default'}${local ? ' (local)' : ''}`) 120 | verbose(`Config data: ${JSON.stringify(config, null, 2)}`) 121 | 122 | if (!config.mcpServers || typeof config.mcpServers !== 'object') { 123 | verbose('Invalid mcpServers structure in config') 124 | throw new Error('Invalid mcpServers structure') 125 | } 126 | 127 | const configPath = getConfigPath(client, local) 128 | 129 | writeConfigFile(config, configPath) 130 | } 131 | 132 | function writeConfigFile(config: ClientConfig, target: ClientFileTarget): void { 133 | const configDir = path.dirname(target.path) 134 | 135 | verbose(`Ensuring config directory exists: ${configDir}`) 136 | if (!fs.existsSync(configDir)) { 137 | verbose(`Creating directory: ${configDir}`) 138 | fs.mkdirSync(configDir, { recursive: true }) 139 | } 140 | 141 | let existingConfig: ClientConfig = { mcpServers: {} } 142 | try { 143 | if (fs.existsSync(target.path)) { 144 | verbose('Reading existing config file for merging') 145 | existingConfig = JSON.parse(fs.readFileSync(target.path, 'utf8')) 146 | verbose(`Existing config loaded: ${JSON.stringify(existingConfig, null, 2)}`) 147 | } 148 | } catch (error) { 149 | verbose(`Error reading existing config for merge: ${error instanceof Error ? error.message : String(error)}`) 150 | // If reading fails, continue with empty existing config 151 | } 152 | 153 | verbose('Merging configs') 154 | const mergedConfig = { 155 | ...existingConfig, 156 | ...config, 157 | } 158 | verbose(`Merged config: ${JSON.stringify(mergedConfig, null, 2)}`) 159 | 160 | verbose(`Writing config to file: ${target.path}`) 161 | fs.writeFileSync(target.path, JSON.stringify(mergedConfig, null, 2)) 162 | verbose('Config successfully written') 163 | } 164 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | import * as install from './install' 2 | 3 | export const commands = [install] 4 | -------------------------------------------------------------------------------- /src/commands/install.ts: -------------------------------------------------------------------------------- 1 | import type { ArgumentsCamelCase, Argv } from 'yargs' 2 | import { logger } from '../logger' 3 | import { green, red } from 'picocolors' 4 | import { clientNames, readConfig, writeConfig } from '../client-config' 5 | 6 | export interface InstallArgv { 7 | target?: string 8 | name?: string 9 | client: string 10 | local?: boolean 11 | yes?: boolean 12 | } 13 | 14 | export const command = 'install [target]' 15 | export const describe = 'Install MCP server' 16 | export const aliases = ['i'] 17 | 18 | export function builder(yargs: Argv): Argv { 19 | return yargs 20 | .positional('target', { 21 | type: 'string', 22 | description: 'Installation target (URL or command)', 23 | }) 24 | .option('name', { 25 | type: 'string', 26 | description: 'Name of the server (auto-extracted from target if not provided)', 27 | }) 28 | .option('client', { 29 | type: 'string', 30 | description: 'Client to use for installation', 31 | demandOption: true, 32 | }) 33 | .option('local', { 34 | type: 'boolean', 35 | description: 'Install to the local directory instead of the default location', 36 | default: false, 37 | }) 38 | .option('yes', { 39 | type: 'boolean', 40 | alias: 'y', 41 | description: 'Skip confirmation prompt', 42 | default: false, 43 | }) 44 | } 45 | 46 | export async function handler(argv: ArgumentsCamelCase) { 47 | if (!argv.client || !clientNames.includes(argv.client)) { 48 | logger.error(`Invalid client: ${argv.client}. Available clients: ${clientNames.join(', ')}`) 49 | return 50 | } 51 | 52 | let target = argv.target 53 | if (!target) { 54 | target = (await logger.prompt('Enter the installation target (URL or command):', { 55 | type: 'text', 56 | })) as string 57 | } 58 | 59 | let name = argv.name 60 | if (!name) { 61 | // Auto-extract name from target 62 | if (target.startsWith('http') || target.startsWith('https')) { 63 | // For URLs, try to extract from the last part of the path 64 | const urlParts = target.split('/') 65 | name = urlParts[urlParts.length - 1] || 'server' 66 | } else { 67 | // For commands, try to extract package name 68 | const parts = target.split(' ') 69 | if (parts[0] === 'npx' && parts.length > 1) { 70 | // Skip flags like -y and get the package name 71 | const packageIndex = parts.findIndex((part, index) => index > 0 && !part.startsWith('-')) 72 | if (packageIndex !== -1) { 73 | name = parts[packageIndex] 74 | } else { 75 | name = parts[parts.length - 1] 76 | } 77 | } else { 78 | name = parts[0] 79 | } 80 | } 81 | 82 | // If we still don't have a name, prompt for it 83 | if (!name || name === '') { 84 | name = (await logger.prompt('Enter the name of the server:', { 85 | type: 'text', 86 | })) as string 87 | } 88 | } 89 | 90 | logger.info(`Installing MCP server ${argv.client} with target ${argv.target} and name ${name}`) 91 | 92 | let ready = argv.yes 93 | if (!ready) { 94 | ready = await logger.prompt( 95 | green( 96 | `Are you ready to install MCP server "${name}" (${target}) in ${argv.client}${argv.local ? ' (locally)' : ''}?`, 97 | ), 98 | { 99 | type: 'confirm', 100 | }, 101 | ) 102 | } 103 | 104 | if (ready) { 105 | try { 106 | const config = readConfig(argv.client, argv.local) 107 | 108 | // if it is a URL, add it to config 109 | if (target.startsWith('http') || target.startsWith('https')) { 110 | config.mcpServers[name] = { 111 | command: 'npx', 112 | args: ['-y', 'supergateway', '--sse', target], 113 | } 114 | writeConfig(config, argv.client, argv.local) 115 | } 116 | 117 | // if it is a command, add it to config 118 | else { 119 | config.mcpServers[name] = { 120 | command: target.split(' ')[0], 121 | args: target.split(' ').slice(1), 122 | } 123 | writeConfig(config, argv.client, argv.local) 124 | } 125 | 126 | logger.box( 127 | green( 128 | `Successfully installed MCP server "${name}" (${target}) in ${argv.client}${argv.local ? ' (locally)' : ''}.`, 129 | ), 130 | ) 131 | } catch (e) { 132 | logger.error(red((e as Error).message)) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from '@jest/globals' 2 | 3 | describe('First Test Case', () => { 4 | it('should pass', () => { 5 | expect(true).toBeTruthy() 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './commands' 2 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import { createConsola } from 'consola' 2 | 3 | export const logger = createConsola({}) 4 | 5 | export const verbose = (msg: string) => logger.verbose(msg) 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist/src", 5 | "baseUrl": ".", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "allowUnusedLabels": false, 9 | "allowUnreachableCode": false, 10 | "noFallthroughCasesInSwitch": true, 11 | "noUncheckedIndexedAccess": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "declaration": true, 16 | "sourceMap": true, 17 | "resolveJsonModule": true 18 | }, 19 | "include": ["src"], 20 | "exclude": ["dist", "bundle", "node_modules"], 21 | "ts-node": { 22 | // these options are overrides used only by ts-node 23 | // same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable 24 | "compilerOptions": { 25 | "module": "commonjs" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['bin/run.ts'], 5 | splitting: false, 6 | sourcemap: false, 7 | clean: true, 8 | }) 9 | --------------------------------------------------------------------------------