├── .eslintignore ├── .prettierrc.json ├── bin ├── run.cmd ├── dev.cmd ├── run.js └── dev.js ├── .eslintrc.json ├── .cursorignore ├── src ├── index.ts ├── data │ ├── types.ts │ └── servers │ │ └── index.ts └── commands │ ├── list.ts │ ├── uninstall.ts │ └── install.ts ├── test ├── tsconfig.json └── commands │ ├── install.test.ts │ ├── uninstall.test.ts │ └── list.test.ts ├── .gitignore ├── .mocharc.json ├── tsconfig.json ├── .github └── workflows │ ├── onRelease.yml │ ├── test.yml │ ├── indent_pr_review.yaml │ └── onPushToMain.yml ├── package.json ├── README.md └── .cursor └── rules └── derived-cursor-rules.mdc /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@oclif/prettier-config" 2 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["oclif", "oclif-typescript", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.cursorignore: -------------------------------------------------------------------------------- 1 | # Ignore SpecStory derived-cursor-rules.mdc backup files 2 | .specstory/cursor_rules_backups/* 3 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* 4 | -------------------------------------------------------------------------------- /bin/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {execute} from '@oclif/core' 4 | 5 | await execute({dir: import.meta.url}) 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data/servers' 2 | 3 | // Export types and data 4 | export * from './data/types' 5 | export {run} from '@oclif/core' 6 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [ 7 | {"path": ".."} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | **/.DS_Store 4 | /.idea 5 | /dist 6 | /tmp 7 | /node_modules 8 | oclif.manifest.json 9 | yarn.lock 10 | pnpm-lock.yaml 11 | *.tsbuildinfo 12 | .vscode/ 13 | .specstory 14 | -------------------------------------------------------------------------------- /bin/dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning 2 | 3 | // eslint-disable-next-line n/shebang 4 | import {execute} from '@oclif/core' 5 | 6 | await execute({development: true, dir: import.meta.url}) 7 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "ts-node/register" 4 | ], 5 | "watch-extensions": [ 6 | "ts" 7 | ], 8 | "recursive": true, 9 | "reporter": "spec", 10 | "timeout": 60000, 11 | "node-option": [ 12 | "loader=ts-node/esm", 13 | "experimental-specifier-resolution=node" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declaration": true, 5 | "module": "ESNext", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "target": "es2022", 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "declarationMap": true 14 | }, 15 | "include": ["./src/**/*"], 16 | "ts-node": { 17 | "esm": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/onRelease.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: latest 15 | - run: npm install 16 | - uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c 17 | with: 18 | token: ${{ secrets.NPM_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches-ignore: [main] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | unit-tests: 9 | strategy: 10 | matrix: 11 | os: ['ubuntu-latest', 'windows-latest'] 12 | node_version: [lts/-1, lts/*, latest] 13 | fail-fast: false 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node_version }} 20 | cache: npm 21 | - run: npm install 22 | - run: npm run build 23 | - run: npm run test 24 | -------------------------------------------------------------------------------- /test/commands/install.test.ts: -------------------------------------------------------------------------------- 1 | import {runCommand} from '@oclif/test' 2 | import {expect} from 'chai' 3 | 4 | describe('install', () => { 5 | it('fails when server name is not provided', async () => { 6 | try { 7 | await runCommand('install') 8 | expect.fail('Command should have failed without server name') 9 | } catch (error: unknown) { 10 | expect(error).to.exist 11 | } 12 | }) 13 | 14 | it('fails when server does not exist', async () => { 15 | try { 16 | await runCommand('install nonexistent-server') 17 | expect.fail('Command should have failed with nonexistent server') 18 | } catch (error: unknown) { 19 | expect(error).to.exist 20 | } 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/commands/uninstall.test.ts: -------------------------------------------------------------------------------- 1 | import {runCommand} from '@oclif/test' 2 | import {expect} from 'chai' 3 | 4 | describe('uninstall', () => { 5 | it('fails when server name is not provided', async () => { 6 | try { 7 | await runCommand('uninstall') 8 | expect.fail('Command should have failed without server name') 9 | } catch (error: unknown) { 10 | expect(error).to.exist 11 | } 12 | }) 13 | 14 | it('handles multiple server names', async () => { 15 | try { 16 | // This will fail because the servers don't exist, but we can test the argument parsing 17 | await runCommand('uninstall server1 server2') 18 | expect.fail('Command should have failed with nonexistent servers') 19 | } catch (error: unknown) { 20 | expect(error).to.exist 21 | } 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /.github/workflows/indent_pr_review.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: Indent PR Review 3 | 4 | on: 5 | pull_request: 6 | types: [opened, synchronize, reopened] 7 | 8 | jobs: 9 | indent: 10 | #### (Optional) Uncomment the `if` condition below to only run the workflow if the PR is labeled with 'indent'. 11 | #### It's recommended to also add 'labeled' to the `pull_request: types:` list above. 12 | # 13 | # if: contains(github.event.pull_request.labels.*.name, 'indent') 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 30 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.12.2" 24 | 25 | - name: Install uv 26 | uses: astral-sh/setup-uv@v5 27 | with: 28 | enable-cache: true 29 | cache-dependency-glob: | 30 | **/uv.lock 31 | **/pyproject.toml 32 | 33 | - name: Login to Indent 34 | run: uvx indent-ai login --key ${{ secrets.PR_REVIEW_INDENT_API_KEY }} 35 | 36 | - name: Generate dynamic prompt and run Indent 37 | env: 38 | EXPONENT_API_KEY: ${{ secrets.PR_REVIEW_INDENT_API_KEY }} 39 | PR_NUMBER: ${{ github.event.number }} 40 | run: | 41 | uvx indent-ai run --workflow-id indent_pr_review 42 | 43 | -------------------------------------------------------------------------------- /src/data/types.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | 3 | export const EnvVariable = z.object({ 4 | description: z.string(), 5 | required: z.boolean().default(true).optional(), 6 | }) 7 | 8 | export const MCPServerRuntimeArg = z.object({ 9 | default: z.any().optional(), 10 | description: z.string(), 11 | multiple: z.boolean().optional().default(false), 12 | }) 13 | 14 | export const MCPServerConfig = z.object({ 15 | args: z.array(z.string()), 16 | command: z.string(), 17 | env: z.record(z.string(), EnvVariable), 18 | runtimeArgs: MCPServerRuntimeArg.optional(), 19 | }) 20 | 21 | export const MCPServer = z.object({ 22 | config: MCPServerConfig, 23 | description: z.string(), 24 | distribution: z.object({ 25 | package: z.string().optional(), 26 | source: z.object({ 27 | binary: z.string(), 28 | path: z.string() 29 | }).optional(), 30 | type: z.enum(['npm', 'pip', 'source']), 31 | }), 32 | id: z.string().regex(/^[\da-z-]+$/), 33 | isOfficial: z.boolean().default(false), 34 | license: z.string().optional(), 35 | name: z.string(), 36 | publisher: z.object({ 37 | id: z.string().regex(/^[\da-z-]+$/), 38 | name: z.string(), 39 | url: z.string().url(), 40 | }), 41 | runtime: z.enum(['node', 'python', 'go', 'other']), 42 | sourceUrl: z.string().url(), 43 | }) 44 | 45 | // Infer types from schemas 46 | export type MCPServerType = z.infer 47 | -------------------------------------------------------------------------------- /test/commands/list.test.ts: -------------------------------------------------------------------------------- 1 | import {runCommand} from '@oclif/test' 2 | import {expect} from 'chai' 3 | import sinon from 'sinon' 4 | 5 | import List from '../../src/commands/list.js' 6 | 7 | describe('list', () => { 8 | let listClaudeServersStub: sinon.SinonStub 9 | let listContinueServersStub: sinon.SinonStub 10 | 11 | beforeEach(() => { 12 | // Create stubs for the private methods 13 | listClaudeServersStub = sinon.stub(List.prototype, 'listClaudeServers' as keyof typeof List.prototype) 14 | listContinueServersStub = sinon.stub(List.prototype, 'listContinueServers' as keyof typeof List.prototype) 15 | 16 | // Make both methods return false (no servers found) 17 | listClaudeServersStub.resolves(false) 18 | listContinueServersStub.resolves(false) 19 | }) 20 | 21 | afterEach(() => { 22 | // Restore the original methods after each test 23 | listClaudeServersStub.restore() 24 | listContinueServersStub.restore() 25 | }) 26 | 27 | it('runs list cmd with no servers found', async () => { 28 | const {stdout} = await runCommand('list') 29 | // When no servers are found, it should show the "No MCP servers" message 30 | expect(stdout).to.include('No MCP servers currently installed.') 31 | }) 32 | 33 | it('runs list with client flag and no servers found', async () => { 34 | const {stdout} = await runCommand('list --client=claude') 35 | // When filtering by client and no servers found, it should show client-specific message 36 | expect(stdout).to.include('No MCP servers currently installed on Claude Desktop.') 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opentools", 3 | "description": "The easiest way to install MCP servers", 4 | "version": "0.0.18", 5 | "author": "OpenTools Team", 6 | "bin": { 7 | "opentools": "./bin/run.js" 8 | }, 9 | "bugs": "https://github.com/opentoolsteam/cli/issues", 10 | "dependencies": { 11 | "@inquirer/prompts": "^7.2.1", 12 | "@oclif/core": "^4", 13 | "@oclif/plugin-help": "^6", 14 | "sinon": "^19.0.2", 15 | "zod": "^3.24.2" 16 | }, 17 | "devDependencies": { 18 | "@oclif/prettier-config": "^0.2.1", 19 | "@oclif/test": "^4", 20 | "@types/chai": "^4", 21 | "@types/mocha": "^10", 22 | "@types/node": "^18", 23 | "@types/sinon": "^17.0.4", 24 | "chai": "^4", 25 | "eslint": "^8", 26 | "eslint-config-oclif": "^5", 27 | "eslint-config-oclif-typescript": "^3", 28 | "eslint-config-prettier": "^9", 29 | "mocha": "^10", 30 | "oclif": "^4", 31 | "shx": "^0.3.3", 32 | "ts-node": "^10", 33 | "typescript": "~5.3.3" 34 | }, 35 | "engines": { 36 | "node": ">=18.0.0" 37 | }, 38 | "files": [ 39 | "/bin", 40 | "/dist", 41 | "/oclif.manifest.json" 42 | ], 43 | "homepage": "https://opentools.com", 44 | "keywords": [ 45 | "modelcontextprotocol", 46 | "mcp", 47 | "opentools" 48 | ], 49 | "license": "MIT", 50 | "main": "dist/index.js", 51 | "type": "module", 52 | "oclif": { 53 | "bin": "opentools", 54 | "dirname": "opentools", 55 | "commands": "./dist/commands", 56 | "plugins": [ 57 | "@oclif/plugin-help" 58 | ] 59 | }, 60 | "repository": "opentoolsteam/cli", 61 | "scripts": { 62 | "build": "shx rm -rf dist tsconfig.tsbuildinfo && tsc -b", 63 | "lint": "eslint . --ext .ts", 64 | "postpack": "shx rm -f oclif.manifest.json", 65 | "posttest": "npm run lint", 66 | "prepack": "oclif manifest && oclif readme", 67 | "test": "mocha --forbid-only \"test/**/*.test.ts\"", 68 | "version": "oclif readme && git add README.md" 69 | }, 70 | "types": "dist/index.d.ts", 71 | "exports": { 72 | "./data/servers": "./dist/data/servers/index.js", 73 | "./data/types": "./dist/data/types.js" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/onPushToMain.yml: -------------------------------------------------------------------------------- 1 | # test 2 | name: version, tag and github release 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | - name: Check if version already exists 15 | id: version-check 16 | run: | 17 | package_version=$(node -p "require('./package.json').version") 18 | exists=$(gh api repos/${{ github.repository }}/releases/tags/v$package_version >/dev/null 2>&1 && echo "true" || echo "") 19 | 20 | if [ -n "$exists" ]; 21 | then 22 | echo "Version v$package_version already exists" 23 | echo "::warning file=package.json,line=1::Version v$package_version already exists - no release will be created. If you want to create a new release, please update the version in package.json and push again." 24 | echo "skipped=true" >> $GITHUB_OUTPUT 25 | else 26 | echo "Version v$package_version does not exist. Creating release..." 27 | echo "skipped=false" >> $GITHUB_OUTPUT 28 | echo "tag=v$package_version" >> $GITHUB_OUTPUT 29 | fi 30 | env: 31 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 32 | - name: Setup git 33 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 34 | run: | 35 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 36 | git config --global user.name "github-actions[bot]" 37 | - name: Generate oclif README 38 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 39 | id: oclif-readme 40 | run: | 41 | npm install 42 | npm exec oclif readme 43 | if [ -n "$(git status --porcelain)" ]; then 44 | git add . 45 | git commit -am "chore: update README.md" 46 | git push -u origin ${{ github.ref_name }} 47 | fi 48 | - name: Create Github Release 49 | uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 50 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 51 | with: 52 | name: ${{ steps.version-check.outputs.tag }} 53 | tag: ${{ steps.version-check.outputs.tag }} 54 | commit: ${{ github.ref_name }} 55 | token: ${{ secrets.GH_TOKEN }} 56 | skipIfReleaseExists: true 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | opentools 2 | ================= 3 | 4 | A command-line interface (CLI) for managing Model Context Protocol (MCP) servers. This tool makes it easy to install, uninstall, and manage MCP servers for Claude Desktop and Continue clients. 5 | 6 | [![Version](https://img.shields.io/npm/v/opentools.svg)](https://npmjs.org/package/opentools) 7 | [![Downloads/week](https://img.shields.io/npm/dw/opentools.svg)](https://npmjs.org/package/opentools) 8 | 9 | # Installation 10 | 11 | ```sh-session 12 | $ npm install -g opentools 13 | ``` 14 | 15 | # Usage 16 | 17 | ```sh-session 18 | $ opentools COMMAND 19 | running command... 20 | $ opentools (--version) 21 | opentools/0.0.8 darwin-arm64 node-v22.10.0 22 | $ opentools --help [COMMAND] 23 | USAGE 24 | $ opentools COMMAND 25 | ``` 26 | 27 | # Commands 28 | 29 | * `opentools help [COMMAND]` - Display help for opentools 30 | * `opentools install SERVER` - Install an MCP server (alias: `i`) 31 | * `opentools uninstall SERVER` - Uninstall an MCP server (alias: `un`) 32 | * `opentools list` - List installed servers for the specified client 33 | 34 | ## Install a Server 35 | 36 | ```sh 37 | # Install for Claude Desktop (default) 38 | $ opentools install server-name 39 | 40 | # Install for Continue 41 | $ opentools install server-name --client continue 42 | ``` 43 | 44 | ## Uninstall a Server 45 | 46 | ```sh 47 | # Uninstall from Claude Desktop (default) 48 | $ opentools uninstall server-name 49 | 50 | # Uninstall from Continue 51 | $ opentools uninstall server-name --client continue 52 | ``` 53 | 54 | ## List Installed Servers 55 | 56 | ```sh 57 | # List servers installed on Claude Desktop (default) 58 | $ opentools list 59 | 60 | # List servers installed on Continue 61 | $ opentools list --client continue 62 | ``` 63 | 64 | # Supported Clients 65 | 66 | - Claude Desktop (default) 67 | - Continue 68 | - VS Code 69 | 70 | # Available Servers 71 | 72 | The CLI provides access to various MCP servers including: 73 | 74 | - AWS Knowledge Base 75 | - Axiom 76 | - Brave Search 77 | - Browserbase 78 | - EverArt 79 | - Filesystem 80 | - GitHub 81 | - GitLab 82 | - PostgreSQL 83 | - Puppeteer 84 | - Sentry 85 | - Slack 86 | - SQLite 87 | - Time 88 | - And more... 89 | 90 | Each server may require specific environment variables or configuration during installation. The CLI will guide you through the setup process. 91 | 92 | For more information about Model Context Protocol, visit [modelcontextprotocol.io](https://modelcontextprotocol.io/). 93 | -------------------------------------------------------------------------------- /.cursor/rules/derived-cursor-rules.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Cursor rules derived by SpecStory from the project AI interaction history 3 | globs: * 4 | --- 5 | 6 | ## PROJECT OVERVIEW 7 | This project is a Node.js based CLI tool. The source code is written in TypeScript. 8 | 9 | ## CODE STYLE 10 | Adhere to standard TypeScript coding conventions. Use Prettier for formatting. 11 | 12 | ## FOLDER ORGANIZATION 13 | The project follows a standard structure: 14 | - `src`: Source code. 15 | - `test`: Unit tests. 16 | - `cli`: Contains the CLI application's entry point and related files. 17 | 18 | 19 | ## TECH STACK 20 | - Node.js 21 | - TypeScript 22 | - npm 23 | - shx (used in build script) 24 | - OCLIF (used for CLI framework) 25 | - zod (added 2025-03-12) 26 | 27 | 28 | ## PROJECT-SPECIFIC STANDARDS 29 | - All commands should be defined in the `src/commands` directory. 30 | - Unit tests should be written for every command. Tests should accurately reflect command functionality. Avoid simple string matching in tests; instead verify behavior. 31 | - The `package.json` file should clearly define the entry point for the CLI. 32 | - Avoid using `any` type; explicitly define types for all variables to prevent runtime errors. (Added 2025-03-12) 33 | - Interface properties should be sorted alphabetically to satisfy `perfectionist/sort-object-types` linting rule. (Added 2025-03-12) 34 | 35 | 36 | ## WORKFLOW & RELEASE RULES 37 | - Before running the CLI, build the TypeScript code using `npm run build`. 38 | - Version updates will be managed using semantic versioning. (Further details on versioning strategy needed) 39 | - To resolve TypeScript errors and missing modules, follow the steps outlined in `2025-03-12_14-21-fixing-typescript-errors-and-missing-modules.md`. This includes installing `zod` and correctly typing variables to avoid `'value' is of type 'unknown'` errors. The `npm install zod` command will install the necessary dependency. Type assertions like `as [string, EnvVariable][]` may be necessary to resolve type errors related to 'unknown' types. 40 | - To run tests, use `npm run test`. Tests should be updated to reflect actual command behavior, not simple string matching. Remove tests for commands that do not exist. Use `npm run lint -- --fix` to automatically fix many linting errors. To run tests without linting, use `npm test --no-posttest`. 41 | - Address deprecation warnings related to `fs.Stats` constructor. 42 | 43 | 44 | ## REFERENCE EXAMPLES 45 | - Running the CLI from source: See instructions in `2025-03-11_12-54-running-a-package-from-source-instructions.md` 46 | - Fixing TypeScript errors and missing modules: See `2025-03-12_14-21-fixing-typescript-errors-and-missing-modules.md` 47 | - Fixing type issues in `uninstall.ts`: See `2025-03-12_15-33-fixing-type-issues-in-uninstall-ts.md` 48 | 49 | 50 | ## PROJECT DOCUMENTATION & CONTEXT SYSTEM 51 | Documentation will be maintained using markdown files and integrated into the repository. 52 | 53 | ## DEBUGGING 54 | - Ensure the TypeScript code is built (`npm run build`) before running the CLI. 55 | - The error "command i not found" indicates that the TypeScript code needs to be compiled. 56 | - `'value' is of type 'unknown'` errors in TypeScript indicate improperly typed variables. Refer to `2025-03-12_14-21-fixing-typescript-errors-and-missing-modules.md` for solutions. 57 | - Missing module errors (e.g., "Cannot find module 'zod'") indicate missing dependencies. Use `npm install` to install required packages. 58 | - Test failures often indicate a mismatch between test expectations and actual command output. Refactor tests to reflect actual command behavior. 59 | - Deprecation warnings related to `fs.Stats` constructor indicate outdated code. Update the relevant code sections. 60 | - Use `npm run lint -- --fix` to automatically fix many linting errors. 61 | - `any` type errors indicate a need for explicit type definitions. Properly type variables to resolve these errors. (Added 2025-03-12) 62 | - `perfectionist/sort-object-types` linting errors indicate improperly ordered object properties. Ensure properties are alphabetically ordered. (Added 2025-03-12) 63 | 64 | 65 | ## FINAL DOs AND DON'Ts 66 | - **DO** use TypeScript. 67 | - **DO** write unit tests that accurately reflect command functionality. 68 | - **DO** build the project using `npm run build` before running. 69 | - **DO** follow the folder organization guidelines. 70 | - **DO** install all necessary dependencies using `npm install`. 71 | - **DO** explicitly define types for all variables to avoid runtime errors. (Added 2025-03-12) 72 | - **DO** sort object properties alphabetically to satisfy linting rules. (Added 2025-03-12) 73 | - **DON'T** run the CLI without building the TypeScript code first. 74 | - **DON'T** leave variables untyped; ensure proper type declarations to avoid runtime errors. 75 | - **DON'T** write tests that rely on simple string matching of command output. Focus on verifying actual command behavior. 76 | - **DON'T** ignore deprecation warnings; address them promptly to maintain code quality and avoid future compatibility issues. 77 | - **DON'T** ignore linting errors; address them using `npm run lint -- --fix` or manually. 78 | - **DON'T** use `any` type unless absolutely necessary and with clear justification. (Added 2025-03-12) 79 | - **DON'T** leave object properties unsorted; maintain alphabetical order for consistency and to satisfy linting rules. (Added 2025-03-12) -------------------------------------------------------------------------------- /src/commands/list.ts: -------------------------------------------------------------------------------- 1 | import {Command, Flags} from '@oclif/core' 2 | import * as fs from 'node:fs/promises' 3 | import * as os from 'node:os' 4 | import * as path from 'node:path' 5 | 6 | import { servers } from '../data/servers/index.js' 7 | 8 | export default class List extends Command { 9 | static aliases = ['ls'] 10 | 11 | static override description = 'List installed servers across all clients' 12 | 13 | static override examples = [ 14 | '<%= config.bin %> list', 15 | '<%= config.bin %> list --client=claude', 16 | '<%= config.bin %> list --client=continue', 17 | ] 18 | 19 | static override flags = { 20 | client: Flags.string({ 21 | char: 'c', 22 | description: 'Only show servers for this client', 23 | options: ['claude', 'continue'], 24 | required: false, 25 | }), 26 | } 27 | 28 | private clientDisplayNames: Record = { 29 | 'claude': 'Claude Desktop', 30 | 'continue': 'Continue' 31 | } 32 | 33 | public async run(): Promise { 34 | const {flags} = await this.parse(List) 35 | 36 | let foundServers = false 37 | const trackServers = (found: boolean) => { 38 | foundServers = foundServers || found 39 | } 40 | 41 | if (flags.client === 'claude') { 42 | trackServers(await this.listClaudeServers()) 43 | } else if (flags.client === 'continue') { 44 | trackServers(await this.listContinueServers()) 45 | } else { 46 | // If no client specified, show all 47 | trackServers(await this.listClaudeServers().catch(() => false)) // Ignore errors 48 | trackServers(await this.listContinueServers().catch(() => false)) // Ignore errors 49 | } 50 | 51 | if (!foundServers) { 52 | if (flags.client) { 53 | const clientName = this.clientDisplayNames[flags.client] 54 | this.log(`No MCP servers currently installed on ${clientName}.`) 55 | } else { 56 | this.log('No MCP servers currently installed.') 57 | } 58 | } 59 | 60 | this.log('') // Add final newline 61 | } 62 | 63 | private async listClaudeServers(): Promise { 64 | const configPath = path.join( 65 | os.homedir(), 66 | 'Library', 67 | 'Application Support', 68 | 'Claude', 69 | 'claude_desktop_config.json' 70 | ) 71 | 72 | try { 73 | const configContent = await fs.readFile(configPath, 'utf8') 74 | const config = JSON.parse(configContent) 75 | const installedServers = Object.keys(config.mcpServers || {}) 76 | 77 | if (installedServers.length > 0) { 78 | this.log(`\n${this.clientDisplayNames.claude}`) 79 | 80 | // Sort servers into registered and unknown 81 | const registeredServers = installedServers.filter(id => servers.some(s => s.id === id)) 82 | const unknownServers = installedServers.filter(id => !servers.some(s => s.id === id)) 83 | 84 | // Display registered servers first 85 | for (const [index, serverId] of registeredServers.entries()) { 86 | const prefix = index === registeredServers.length - 1 && unknownServers.length === 0 ? '└── ' : '├── ' 87 | const link = `\u001B]8;;https://opentools.com/registry/${serverId}\u0007${serverId}\u001B]8;;\u0007` 88 | this.log(`${prefix}${link}`) 89 | } 90 | 91 | // Then display unknown servers 92 | for (const [index, serverId] of unknownServers.entries()) { 93 | const prefix = index === unknownServers.length - 1 ? '└── ' : '├── ' 94 | this.log(`\u001B[31m${prefix}${serverId} (unknown)\u001B[0m`) 95 | } 96 | 97 | return true 98 | } 99 | 100 | return false 101 | 102 | } catch (error) { 103 | if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { 104 | return false 105 | } 106 | 107 | throw error 108 | 109 | } 110 | } 111 | 112 | private async listContinueServers(): Promise { 113 | const configPath = path.join( 114 | os.homedir(), 115 | '.continue', 116 | 'config.json' 117 | ) 118 | 119 | try { 120 | const configContent = await fs.readFile(configPath, 'utf8') 121 | const config = JSON.parse(configContent) 122 | 123 | // Get installed servers from the experimental.modelContextProtocolServers array 124 | const installedServers = config.experimental?.modelContextProtocolServers || [] 125 | 126 | // Map installed servers back to their IDs by matching command and args 127 | const validServers = servers 128 | .filter(registryServer => 129 | installedServers.some((installed: { transport: { args: string[], command: string } }) => 130 | installed.transport.command === registryServer.config.command && 131 | JSON.stringify(installed.transport.args.slice(0, registryServer.config.args.length)) === JSON.stringify(registryServer.config.args) 132 | ) 133 | ) 134 | .map(s => s.id) 135 | 136 | // Only output if there are valid servers 137 | if (validServers.length > 0) { 138 | this.log(`\n${this.clientDisplayNames.continue}`) 139 | // Print servers in a tree-like format 140 | for (const [index, serverId] of validServers.entries()) { 141 | const prefix = index === validServers.length - 1 ? '└── ' : '├── ' 142 | const link = `\u001B]8;;https://opentools.com/registry/${serverId}\u0007${serverId}\u001B]8;;\u0007` 143 | this.log(`${prefix}${link}`) 144 | } 145 | 146 | return true 147 | } 148 | 149 | return false 150 | 151 | } catch (error) { 152 | if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { 153 | // Don't output anything if client not installed 154 | return false 155 | } 156 | 157 | throw error 158 | 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/commands/uninstall.ts: -------------------------------------------------------------------------------- 1 | import {Args, Command, Flags} from '@oclif/core' 2 | import * as fs from 'node:fs/promises' 3 | import * as os from 'node:os' 4 | import * as path from 'node:path' 5 | 6 | import { servers } from '../data/servers/index.js' 7 | interface MCPServerTransport { 8 | transport: { 9 | args: string[]; 10 | command: string; 11 | env?: Record; 12 | type?: string; 13 | }; 14 | } 15 | export default class Uninstall extends Command { 16 | static aliases = ['un'] 17 | 18 | static override args = { 19 | firstServer: Args.string({ 20 | description: 'name of the MCP server to uninstall', 21 | required: true, 22 | }), 23 | } // Allow variable length arguments 24 | 25 | static completion: (options: { argv: string[] }) => Promise = async ({ argv }) => { 26 | const input = argv.at(-1) || '' 27 | 28 | try { 29 | const claudeConfigPath = path.join( 30 | os.homedir(), 31 | 'Library', 32 | 'Application Support', 33 | 'Claude', 34 | 'claude_desktop_config.json' 35 | ) 36 | const continueConfigPath = path.join( 37 | os.homedir(), 38 | '.continue', 39 | 'config.json' 40 | ) 41 | 42 | const installedServers = new Set() 43 | 44 | try { 45 | const claudeConfig = JSON.parse(await fs.readFile(claudeConfigPath, 'utf8')) 46 | if (claudeConfig.mcpServers) { 47 | for (const id of Object.keys(claudeConfig.mcpServers) 48 | .filter(id => claudeConfig.mcpServers[id].status !== 'unknown')) { 49 | installedServers.add(id) 50 | } 51 | } 52 | } catch { 53 | // Ignore errors reading Claude config 54 | } 55 | 56 | try { 57 | const continueConfig = JSON.parse(await fs.readFile(continueConfigPath, 'utf8')) 58 | if (continueConfig.experimental?.modelContextProtocolServers) { 59 | for (const s of continueConfig.experimental.modelContextProtocolServers) { 60 | const server = servers.find(srv => 61 | s.transport.command === srv.config.command && 62 | JSON.stringify(s.transport.args.slice(0, srv.config.args.length)) === JSON.stringify(srv.config.args) 63 | ) 64 | if (server) { 65 | installedServers.add(server.id) 66 | } 67 | } 68 | } 69 | } catch { 70 | // Ignore errors reading Continue config 71 | } 72 | 73 | const matches = [...installedServers].filter(id => 74 | id.toLowerCase().startsWith(input.toLowerCase()) 75 | ) 76 | 77 | return matches 78 | } catch { 79 | return [] 80 | } 81 | } 82 | 83 | static override description = 'Uninstall one or more MCP servers' 84 | 85 | static override examples = [ 86 | '<%= config.bin %> <%= command.id %> server-name', 87 | '<%= config.bin %> <%= command.id %> server-name1 server-name2 --client claude', 88 | ] 89 | 90 | static override flags = { 91 | client: Flags.string({ 92 | char: 'c', 93 | default: 'claude', 94 | description: 'Uninstall the MCP servers from this client', 95 | options: ['claude', 'continue'], 96 | }), 97 | } 98 | 99 | static override strict = false 100 | 101 | public async run(): Promise { 102 | const {argv, flags} = await this.parse(Uninstall) 103 | const serverNames = argv as string[] 104 | 105 | const {platform} = process 106 | 107 | if (platform !== 'darwin' && platform !== 'win32') { 108 | this.error('This command is only supported on macOS and Windows') 109 | return 110 | } 111 | 112 | // First validate all server IDs exist in our known servers list 113 | const invalidServer = serverNames.find(serverName => !servers.some(server => server.id === serverName)) 114 | if (invalidServer) { 115 | this.error(`Server "${invalidServer}" is not a valid server ID`) 116 | return 117 | } 118 | 119 | // Then check if any servers are in an unknown state 120 | if (flags.client === 'claude') { 121 | try { 122 | const configPath = path.join( 123 | os.homedir(), 124 | 'Library', 125 | 'Application Support', 126 | 'Claude', 127 | 'claude_desktop_config.json' 128 | ) 129 | const configContent = await fs.readFile(configPath, 'utf8') 130 | const config = JSON.parse(configContent) as { mcpServers?: Record } 131 | 132 | // Process all servers at once to avoid await in loop 133 | const unknownServer = serverNames.find(serverName => 134 | config.mcpServers?.[serverName]?.status === 'unknown' 135 | ) 136 | 137 | if (unknownServer) { 138 | this.error(`Cannot uninstall "${unknownServer}" because it is in an unknown state. Please try reinstalling it first.`) 139 | return 140 | } 141 | } catch { 142 | // If we can't read the config, we'll let the actual uninstall handle the error 143 | } 144 | } 145 | 146 | this.log(`Platform: ${platform === 'darwin' ? 'macOS' : 'Windows'}`) 147 | this.log(`Client: ${flags.client}`) 148 | 149 | // Process servers sequentially 150 | const uninstallServer = async (serverName: string) => { 151 | this.log(`\nUninstalling MCP server: ${serverName}`) 152 | return platform === 'darwin' ? 153 | this.uninstallOnMacOS(serverName, flags.client) : 154 | this.uninstallOnWindows(serverName, flags.client) 155 | } 156 | 157 | try { 158 | await Promise.all(serverNames.map(name => uninstallServer(name))) 159 | } catch (error: unknown) { 160 | if (error instanceof Error) { 161 | this.error(`Failed to uninstall server: ${error.message}`) 162 | } else { 163 | this.error('An unknown error occurred during installation') 164 | } 165 | } 166 | } 167 | 168 | private async uninstallOnMacOS(serverName: string, client: string): Promise { 169 | if (client === 'claude') { 170 | const configPath = path.join( 171 | os.homedir(), 172 | 'Library', 173 | 'Application Support', 174 | 'Claude', 175 | 'claude_desktop_config.json' 176 | ) 177 | 178 | try { 179 | await fs.access(configPath) 180 | 181 | const configContent = await fs.readFile(configPath, 'utf8') 182 | const config = JSON.parse(configContent) as { mcpServers?: Record } 183 | 184 | if (!config.mcpServers || !config.mcpServers[serverName]) { 185 | this.error(`Server "${serverName}" is not installed`) 186 | return 187 | } 188 | 189 | delete config.mcpServers[serverName] 190 | 191 | await fs.writeFile(configPath, JSON.stringify(config, null, 2)) 192 | 193 | this.log(`🗑️ Successfully uninstalled ${serverName}`) 194 | 195 | } catch (error: unknown) { 196 | if (error instanceof Error) { 197 | if ('code' in error && error.code === 'ENOENT') { 198 | this.error(`Config file not found at ${configPath}. Is Claude Desktop installed?`) 199 | } else { 200 | this.error(`Error reading config: ${error.message}`) 201 | } 202 | } else { 203 | this.error('An unknown error occurred') 204 | } 205 | } 206 | } else if (client === 'continue') { 207 | const configPath = path.join( 208 | os.homedir(), 209 | '.continue', 210 | 'config.json' 211 | ) 212 | 213 | try { 214 | await fs.access(configPath) 215 | 216 | const configContent = await fs.readFile(configPath, 'utf8') 217 | const config = JSON.parse(configContent) 218 | 219 | if (!config.experimental?.modelContextProtocolServers?.length) { 220 | this.error(`No MCP servers are installed`) 221 | return 222 | } 223 | 224 | const serverIndex = config.experimental.modelContextProtocolServers.findIndex((s: MCPServerTransport) => { 225 | const server = servers.find(srv => srv.id === serverName) 226 | if (!server) return false 227 | 228 | return s.transport.command === server.config.command && 229 | JSON.stringify(s.transport.args.slice(0, server.config.args.length)) === JSON.stringify(server.config.args) 230 | }) 231 | 232 | if (serverIndex === -1) { 233 | this.error(`Server "${serverName}" is not installed`) 234 | return 235 | } 236 | 237 | config.experimental.modelContextProtocolServers.splice(serverIndex, 1) 238 | 239 | await fs.writeFile(configPath, JSON.stringify(config, null, 2)) 240 | 241 | this.log(`🗑️ Successfully uninstalled ${serverName}`) 242 | 243 | } catch (error: unknown) { 244 | if (error instanceof Error) { 245 | if ('code' in error && error.code === 'ENOENT') { 246 | this.error(`Config file not found at ${configPath}. Is Continue installed?`) 247 | } else { 248 | this.error(`Error reading config: ${error.message}`) 249 | } 250 | } else { 251 | this.error('An unknown error occurred') 252 | } 253 | } 254 | } 255 | } 256 | 257 | private async uninstallOnWindows(_serverName: string, _client: string): Promise { 258 | throw new Error('Windows uninstallation not implemented yet') 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/commands/install.ts: -------------------------------------------------------------------------------- 1 | import * as inquirer from '@inquirer/prompts' 2 | import {Args, Command, Flags} from '@oclif/core' 3 | import { exec, execFile } from 'node:child_process' 4 | import * as fs from 'node:fs/promises' 5 | import * as os from 'node:os' 6 | import * as path from 'node:path' 7 | import { promisify } from 'node:util' 8 | 9 | import type { MCPServerType } from '../data/types.js' 10 | 11 | import { servers } from '../data/servers/index.js' 12 | 13 | const execAsync = promisify(exec) 14 | 15 | interface ConfigServer { 16 | args: string[]; 17 | command: string; 18 | env: Record; 19 | } 20 | 21 | interface ConfigType { 22 | experimental?: { 23 | modelContextProtocolServers?: Array<{ 24 | transport: { type: string } & ConfigServer; 25 | }>; 26 | useTools?: boolean; 27 | }; 28 | mcpServers?: Record; 29 | } 30 | 31 | type ConfigTarget = { args: string[]; command: string; type: 'command' } | { path: string; type: 'file' }; 32 | 33 | export default class Install extends Command { 34 | static aliases = ['i'] 35 | 36 | static args = { 37 | server: Args.string({ 38 | description: 'name of the MCP server to install', 39 | required: true, 40 | }), 41 | } 42 | 43 | static description = 'Install an MCP server' 44 | 45 | static examples = [ 46 | '<%= config.bin %> <%= command.id %> server-name', 47 | '<%= config.bin %> <%= command.id %> server-name --client claude', 48 | '<%= config.bin %> <%= command.id %> server-name --client continue', 49 | '<%= config.bin %> <%= command.id %> server-name --client vscode', 50 | '<%= config.bin %> <%= command.id %> server-name --client vscode-insiders', 51 | ] 52 | 53 | static flags = { 54 | client: Flags.string({ 55 | char: 'c', 56 | default: 'claude', 57 | description: 'Install the MCP server to this client', 58 | options: ['claude', 'continue', 'vscode', 'vscode-insiders'], 59 | }), 60 | } 61 | 62 | private clientDisplayNames: Record = { 63 | 'claude': 'Claude Desktop', 64 | 'continue': 'Continue' 65 | } 66 | 67 | private clientProcessNames: Record = { 68 | 'claude': 'Claude', 69 | 'continue': 'Continue' 70 | } 71 | 72 | private CLIENTS_REQUIRING_RESTART: string[] = ['claude'] 73 | 74 | public async run(): Promise { 75 | const {args, flags} = await this.parse(Install) 76 | 77 | // Validate server exists in registry 78 | await this.validateServer(args.server) 79 | 80 | // Detect operating system 81 | const {platform} = process 82 | 83 | if (platform !== 'darwin' && platform !== 'win32') { 84 | this.error('This command is only supported on macOS and Windows') 85 | return 86 | } 87 | 88 | this.log(`Installing MCP server: ${args.server}`) 89 | this.log(`Platform: ${platform === 'darwin' ? 'macOS' : 'Windows'}`) 90 | this.log(`Client: ${flags.client}`) 91 | 92 | try { 93 | await (platform === 'darwin' ? this.installOnMacOS(args.server, flags.client) : this.installOnWindows(args.server, flags.client)); 94 | 95 | // After successful installation, prompt for restart 96 | if (this.CLIENTS_REQUIRING_RESTART.includes(flags.client)) { 97 | await this.promptForRestart(flags.client) 98 | } 99 | } catch (error: unknown) { 100 | if (error instanceof Error) { 101 | this.error(`Failed to install server: ${error.message}`) 102 | } else { 103 | this.error('An unknown error occurred during installation') 104 | } 105 | } 106 | } 107 | 108 | private addServerToConfig(client: string, serverName: string, config: ConfigType, serverConfig: ConfigServer): void { 109 | if (client === 'claude') { 110 | config.mcpServers = config.mcpServers || {} 111 | config.mcpServers[serverName] = serverConfig 112 | } else if (client === 'continue') { 113 | // Initialize experimental if it doesn't exist 114 | if (!config.experimental) { 115 | config.experimental = {} 116 | } 117 | 118 | // Always set useTools to true 119 | config.experimental.useTools = true 120 | 121 | // Initialize modelContextProtocolServers if it doesn't exist 122 | config.experimental.modelContextProtocolServers = config.experimental.modelContextProtocolServers || [] 123 | 124 | const serverTransport = { 125 | ...serverConfig, 126 | type: 'stdio' 127 | } 128 | 129 | // Find if server already exists in the array 130 | const existingServerIndex = config.experimental.modelContextProtocolServers.findIndex( 131 | (s) => s.transport.command === serverConfig.command && 132 | JSON.stringify(s.transport.args) === JSON.stringify(serverConfig.args) 133 | ) 134 | 135 | if (existingServerIndex >= 0) { 136 | config.experimental.modelContextProtocolServers[existingServerIndex].transport = serverTransport 137 | } else { 138 | config.experimental.modelContextProtocolServers.push({ 139 | transport: serverTransport 140 | }) 141 | } 142 | } 143 | } 144 | 145 | private async addServerViaCommand(command: string, args: string[], serverName: string, serverConfig: ConfigServer): Promise { 146 | const json = JSON.stringify({ ...serverConfig, name: serverName }) 147 | return new Promise((resolve, reject) => { 148 | const child = execFile(command, [...args, json], (err, stdout, stderr) => { 149 | if (err) { 150 | reject(err) 151 | } else if (child.exitCode === 0) { 152 | resolve() 153 | } else { 154 | reject(new Error(`running ${command}: ${stderr.toString() || stdout.toString()}`)) 155 | } 156 | }) 157 | }) 158 | } 159 | 160 | private getConfigTarget(client: string, platform: 'macos' | 'win'): ConfigTarget { 161 | switch (client) { 162 | case 'claude': { 163 | if (platform === 'win') { 164 | throw new Error('Windows installation not implemented yet') 165 | } 166 | 167 | return { 168 | path: path.join( 169 | os.homedir(), 170 | 'Library', 171 | 'Application Support', 172 | 'Claude', 173 | 'claude_desktop_config.json' 174 | ), 175 | type: 'file', 176 | } 177 | } 178 | 179 | case 'continue': { 180 | if (platform === 'win') { 181 | throw new Error('Windows installation not implemented yet') 182 | } 183 | 184 | return { 185 | path: path.join( 186 | os.homedir(), 187 | '.continue', 188 | 'config.json' 189 | ), 190 | type: 'file', 191 | } 192 | } 193 | 194 | case 'vscode': { 195 | return { args: ['--add-mcp'], command: platform === 'win' ? 'code.cmd' : 'code', type: 'command' } 196 | } 197 | 198 | case 'vscode-insiders': { 199 | return { args: ['--add-mcp'], command: platform === 'win' ? 'code-insiders.cmd' : 'code-insiders', type: 'command' } 200 | } 201 | 202 | default: { 203 | throw new Error(`Unsupported client: ${client}`) 204 | } 205 | } 206 | } 207 | 208 | private async installMCPServer(configTarget: ConfigTarget, serverName: string, client: string): Promise { 209 | 210 | let config: ConfigType = {} 211 | 212 | if (configTarget.type === 'file') { 213 | try { 214 | // Check if file exists 215 | await fs.access(configTarget.path) 216 | // Read and parse the config file 217 | const configContent = await fs.readFile(configTarget.path, 'utf8') 218 | config = JSON.parse(configContent) 219 | } catch (error: unknown) { 220 | if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { 221 | this.log('🆕 Initializing new configuration file...') 222 | // Create directory if it doesn't exist 223 | await fs.mkdir(path.dirname(configTarget.path), { recursive: true }) 224 | // Create empty config 225 | config = {} 226 | } else { 227 | throw error 228 | } 229 | } 230 | } 231 | 232 | // Get server configuration from registry 233 | const server = await this.validateServer(serverName) 234 | const serverConfig = server.config 235 | 236 | // Handle runtime arguments if they exist 237 | const finalArgs = [...serverConfig.args] 238 | if (serverConfig.runtimeArgs) { 239 | const runtimeArg = serverConfig.runtimeArgs 240 | let answer: string | string[] 241 | 242 | // Special case for filesystem-ref server 243 | let defaultValue = runtimeArg.default 244 | if (serverName === 'filesystem-ref' && Array.isArray(defaultValue)) { 245 | defaultValue = defaultValue.map(path => 246 | path.replace('username', os.userInfo().username) 247 | ) 248 | } 249 | 250 | if (runtimeArg.multiple) { 251 | // First get the default path 252 | const initialAnswer = await inquirer.input({ 253 | default: Array.isArray(defaultValue) ? defaultValue.join(', ') : defaultValue, 254 | message: runtimeArg.description, 255 | }) 256 | const paths = initialAnswer.split(',').map((s: string) => s.trim()) 257 | 258 | // Keep asking for additional paths until empty input 259 | const getAdditionalPaths = async (): Promise => { 260 | const additionalPaths: string[] = [] 261 | let input = await inquirer.input({ 262 | default: "", 263 | message: "Add another allowed directory path? (press Enter to finish)", 264 | }) 265 | 266 | while (input.trim()) { 267 | additionalPaths.push(input.trim()) 268 | // eslint-disable-next-line no-await-in-loop 269 | input = await inquirer.input({ 270 | default: "", 271 | message: "Add another allowed directory path? (press Enter to finish)", 272 | }) 273 | } 274 | 275 | return additionalPaths 276 | } 277 | 278 | const additionalPaths = await getAdditionalPaths() 279 | paths.push(...additionalPaths) 280 | 281 | answer = paths 282 | } else { 283 | answer = await inquirer.input({ 284 | default: defaultValue, 285 | message: runtimeArg.description, 286 | }) 287 | } 288 | 289 | // Add runtime arguments to args array 290 | if (Array.isArray(answer)) { 291 | finalArgs.push(...answer) 292 | } else { 293 | finalArgs.push(answer) 294 | } 295 | } 296 | 297 | // Collect environment variables 298 | const envVars = serverConfig.env 299 | const answers: Record = {} 300 | 301 | for (const [key, value] of Object.entries(envVars)) { 302 | // eslint-disable-next-line no-await-in-loop 303 | const answer = await inquirer.input({ 304 | message: value.description, 305 | validate(input: string) { 306 | if (value.required !== false && !input) { 307 | return `${key} is required` 308 | } 309 | 310 | return true 311 | } 312 | }) 313 | // Only add non-empty values to answers 314 | if (answer.trim()) { 315 | answers[key] = answer 316 | } 317 | } 318 | 319 | 320 | const finalConfig: ConfigServer = { 321 | args: finalArgs, 322 | command: serverConfig.command, 323 | env: answers, 324 | } 325 | 326 | if (configTarget.type === 'file') { 327 | // Update the config based on client type 328 | this.addServerToConfig(client, serverName, config, finalConfig) 329 | // Write the updated config back to file 330 | await fs.writeFile(configTarget.path, JSON.stringify(config, null, 2)) 331 | } else { 332 | await this.addServerViaCommand(configTarget.command, configTarget.args, serverName, finalConfig) 333 | } 334 | 335 | 336 | this.log(`🛠️ Successfully installed ${serverName}`) 337 | } 338 | 339 | private installOnMacOS(serverName: string, client: string): Promise { 340 | return this.installUsingConfig(this.getConfigTarget(client, 'macos'), serverName, client) 341 | } 342 | 343 | private async installOnWindows(serverName: string, client: string): Promise { 344 | return this.installUsingConfig(this.getConfigTarget(client, 'win'), serverName, client) 345 | } 346 | 347 | private async installUsingConfig(configTarget: ConfigTarget, serverName: string, client: string): Promise { 348 | try { 349 | await this.installMCPServer(configTarget, serverName, client) 350 | } catch (error: unknown) { 351 | if (error instanceof Error) { 352 | if ('code' in error && error.code === 'ENOENT') { 353 | if (configTarget.type === 'file') { 354 | this.error(`Config file not found at ${configTarget.path}. Is ${client} installed?`) 355 | } else { 356 | this.error(`Command not found '${configTarget.command}'. Is ${client} installed?`) 357 | } 358 | } else { 359 | this.error(`Error reading config: ${error.message}`) 360 | } 361 | } else { 362 | this.error('An unknown error occurred') 363 | } 364 | } 365 | } 366 | 367 | private async promptForRestart(client: string): Promise { 368 | const answer = await inquirer.confirm({ 369 | default: true, 370 | message: `Would you like to restart ${this.clientDisplayNames[client]} to apply changes?`, 371 | }) 372 | 373 | if (answer) { 374 | this.log(`Restarting ${this.clientDisplayNames[client]}...`) 375 | await this.restartClient(client) 376 | } 377 | } 378 | 379 | private async restartClient(client: string): Promise { 380 | const processName = this.clientProcessNames[client] 381 | if (!processName) { 382 | throw new Error(`Unknown client: ${client}`) 383 | } 384 | 385 | const sleep = (ms: number) => new Promise(resolve => { setTimeout(resolve, ms) }) 386 | 387 | try { 388 | const {platform} = process 389 | if (platform === 'darwin') { 390 | if (client === 'continue') { 391 | await this.restartContinueClient() 392 | } else { 393 | // For other clients like Claude, use the normal process 394 | await execAsync(`killall "${processName}"`) 395 | await sleep(2000) 396 | await execAsync(`open -a "${processName}"`) 397 | this.log(`✨ ${this.clientDisplayNames[client]} has been restarted`) 398 | } 399 | } else if (platform === 'win32') { 400 | if (client === 'continue') { 401 | await this.restartContinueClientWindows() 402 | } else { 403 | // For other clients 404 | await execAsync(`taskkill /F /IM "${processName}.exe" && start "" "${processName}.exe"`) 405 | this.log(`✨ ${this.clientDisplayNames[client]} has been restarted`) 406 | } 407 | } else { 408 | throw new Error('This command is only supported on macOS and Windows') 409 | } 410 | } catch (error: unknown) { 411 | if (error instanceof Error) { 412 | // Check if the error is just that no matching processes were found 413 | if (error.message.includes('No matching processes') || error.message.includes('not found')) { 414 | this.error(`${this.clientDisplayNames[client]} does not appear to be running`) 415 | } else { 416 | this.error(`Failed to restart ${this.clientDisplayNames[client]}: ${error.message}`) 417 | } 418 | } else { 419 | this.error(`Failed to restart ${this.clientDisplayNames[client]}`) 420 | } 421 | } 422 | } 423 | 424 | private async restartContinueClient(): Promise { 425 | const sleep = (ms: number) => new Promise(resolve => { setTimeout(resolve, ms) }) 426 | 427 | try { 428 | // First, find VS Code's installation location 429 | const findVSCode = await execAsync('mdfind "kMDItemCFBundleIdentifier == \'com.microsoft.VSCode\'" | head -n1') 430 | const vscodePath = findVSCode.stdout.trim() 431 | 432 | if (vscodePath) { 433 | const electronPath = path.join(vscodePath, 'Contents/MacOS/Electron') 434 | // Check if VS Code is running using the found path 435 | const vscodeProcesses = await execAsync(`pgrep -fl "${electronPath}"`) 436 | if (vscodeProcesses.stdout.trim().length > 0) { 437 | // Use pkill with full path to ensure we only kill VS Code's Electron 438 | await execAsync(`pkill -f "${electronPath}"`) 439 | await sleep(2000) 440 | await execAsync(`open -a "Visual Studio Code"`) 441 | this.log(`✨ Continue (VS Code) has been restarted`) 442 | return 443 | } 444 | } 445 | } catch { 446 | // VS Code not found or error in detection, try JetBrains 447 | try { 448 | const jetbrainsProcesses = await execAsync('pgrep -fl "IntelliJ IDEA.app"') 449 | if (jetbrainsProcesses.stdout.trim().length > 0) { 450 | await execAsync(`killall "idea"`) 451 | await sleep(2000) 452 | await execAsync(`open -a "IntelliJ IDEA"`) 453 | this.log(`✨ Continue (IntelliJ IDEA) has been restarted`) 454 | return 455 | } 456 | } catch { 457 | // JetBrains not found 458 | } 459 | } 460 | 461 | throw new Error('Could not detect running IDE (VS Code or JetBrains) for Continue') 462 | } 463 | 464 | private async restartContinueClientWindows(): Promise { 465 | try { 466 | const vscodeProcess = await execAsync('tasklist /FI "IMAGENAME eq Code.exe" /FO CSV /NH') 467 | if (vscodeProcess.stdout.includes('Code.exe')) { 468 | await execAsync('taskkill /F /IM "Code.exe" && start "" "Visual Studio Code"') 469 | this.log(`✨ VS Code has been restarted`) 470 | return 471 | } 472 | 473 | const jetbrainsProcess = await execAsync('tasklist /FI "IMAGENAME eq idea64.exe" /FO CSV /NH') 474 | if (jetbrainsProcess.stdout.includes('idea64.exe')) { 475 | await execAsync('taskkill /F /IM "idea64.exe" && start "" "IntelliJ IDEA"') 476 | this.log(`✨ IntelliJ IDEA has been restarted`) 477 | return 478 | } 479 | } catch { 480 | // Process detection failed 481 | } 482 | 483 | throw new Error('Could not detect running IDE (VS Code or JetBrains) for Continue') 484 | } 485 | 486 | private async validateServer(serverName: string): Promise { 487 | const server = servers.find(s => s.id === serverName) 488 | if (!server) { 489 | this.error(`Server "${serverName}" not found in registry`) 490 | } 491 | 492 | if (server.distribution?.type === 'source') { 493 | this.error(`Server "${serverName}" is a source distribution and cannot via OpenTools (for now). To install, please visit ${server.sourceUrl}`) 494 | } 495 | 496 | return server 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /src/data/servers/index.ts: -------------------------------------------------------------------------------- 1 | import type { MCPServerType } from '../types.js' 2 | 3 | export const servers: MCPServerType[] = [ 4 | { 5 | config: 6 | { 7 | args: ['artemis-mcp'], 8 | command: 'uvx', 9 | env: { 10 | ARTEMIS_API_KEY: { 11 | description: 'Your Artemis API key from https://app.artemis.xyz/settings.', 12 | } 13 | } 14 | }, 15 | description: 'Pull the latest fundamental crypto data from Artemis natively into your favorite chatbot interface.', 16 | distribution: { 17 | package: 'artemis-mcp', 18 | type: 'pip', 19 | }, 20 | id: 'artemis', 21 | isOfficial: true, 22 | license: 'MIT', 23 | name: 'Artemis Analytics', 24 | publisher: { 25 | id: 'Artemis-xyz', 26 | name: 'Artemis Analytics Inc.', 27 | url: 'https://www.artemis.xyz/', 28 | }, 29 | runtime: 'python', 30 | sourceUrl: 'https://github.com/Artemis-xyz/artemis-mcp' 31 | }, 32 | { 33 | config: { 34 | args: ["-y", "@modelcontextprotocol/server-aws-kb-retrieval"], 35 | command: "npx", 36 | env: { 37 | "AWS_ACCESS_KEY_ID": { 38 | description: "Your AWS access key ID.", 39 | }, 40 | "AWS_REGION": { 41 | description: "Your AWS region.", 42 | }, 43 | "AWS_SECRET_ACCESS_KEY": { 44 | description: "Your AWS secret access key.", 45 | }, 46 | } 47 | }, 48 | description: "Retrieval from AWS Knowledge Base using Bedrock Agent Runtime. A Model Context Protocol reference server.", 49 | distribution: { 50 | package: "@modelcontextprotocol/server-aws-kb-retrieval", 51 | type: "npm", 52 | }, 53 | id: "aws-kb-retrieval-server-ref", 54 | isOfficial: false, 55 | license: "MIT", 56 | name: "AWS Knowledge Base", 57 | publisher: { 58 | id: "modelcontextprotocol", 59 | name: "Anthropic, PBC", 60 | url: "https://modelcontextprotocol.io/", 61 | }, 62 | runtime: "node", 63 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/aws-kb-retrieval-server" 64 | }, 65 | { 66 | config: { 67 | args: [], 68 | command: "${HOME}/go/bin/axiom-mcp", // eslint-disable-line no-template-curly-in-string 69 | env: { 70 | "AXIOM_DATASETS_BURST": { 71 | description: "The burst limit for datasets.", 72 | required: false, 73 | }, 74 | "AXIOM_DATASETS_RATE": { 75 | description: "The rate limit for datasets.", 76 | required: false, 77 | }, 78 | "AXIOM_ORG_ID": { 79 | description: "Your Axiom organization ID.", 80 | }, 81 | "AXIOM_QUERY_BURST": { 82 | description: "The burst limit for queries.", 83 | required: false, 84 | }, 85 | "AXIOM_QUERY_RATE": { 86 | description: "The rate limit for queries.", 87 | required: false, 88 | }, 89 | "AXIOM_TOKEN": { 90 | description: "Your Axiom token.", 91 | }, 92 | "AXIOM_URL": { 93 | description: "Your Axiom URL.", 94 | }, 95 | } 96 | }, 97 | description: "Query and analyze your Axiom logs, traces, and all other event data in natural language", 98 | distribution: { 99 | source: { 100 | binary: "axiom-mcp", 101 | path: "github.com/axiomhq/axiom-mcp@latest" 102 | }, 103 | type: "source" 104 | }, 105 | id: "axiom", 106 | isOfficial: true, 107 | license: "MIT", 108 | name: "Axiom", 109 | publisher: { 110 | id: "axiomhq", 111 | name: "Axiom, Inc.", 112 | url: "https://axiom.co", 113 | }, 114 | runtime: "go", 115 | sourceUrl: "https://github.com/axiomhq/mcp-server-axiom" 116 | }, 117 | { 118 | config: { 119 | args: ["-y", "@modelcontextprotocol/server-brave-search"], 120 | command: "npx", 121 | env: { 122 | "BRAVE_API_KEY": { 123 | description: "Your Brave Search API key. See: https://brave.com/search/api", 124 | } 125 | } 126 | }, 127 | description: "Web and local search using Brave's Search API. A Model Context Protocol reference server.", 128 | distribution: { 129 | package: "@modelcontextprotocol/server-brave-search", 130 | type: "npm", 131 | }, 132 | id: "brave-search-ref", 133 | isOfficial: false, 134 | license: "MIT", 135 | name: "Brave Search", 136 | publisher: { 137 | id: "modelcontextprotocol", 138 | name: "Anthropic, PBC", 139 | url: "https://modelcontextprotocol.io/", 140 | }, 141 | runtime: "node", 142 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search" 143 | }, 144 | { 145 | config: { 146 | args: ["-y", "@browserbasehq/mcp-browserbase"], 147 | command: "npx", 148 | env: { 149 | "BROWSERBASE_API_KEY": { 150 | description: "Your Browserbase API key. Find it at: https://www.browserbase.com/settings", 151 | }, 152 | "BROWSERBASE_PROJECT_ID": { 153 | description: "Your Browserbase project ID. Find it at: https://www.browserbase.com/settings", 154 | }, 155 | } 156 | }, 157 | description: "Automate browser interactions in the cloud (e.g. web navigation, data extraction, form filling, and more)", 158 | distribution: { 159 | package: "@browserbasehq/mcp-browserbase", 160 | type: "npm", 161 | }, 162 | id: "browserbase", 163 | isOfficial: true, 164 | license: "MIT", 165 | name: "Browserbase", 166 | publisher: { 167 | id: "browserbase", 168 | name: "Browserbase Inc.", 169 | url: "https://www.browserbase.com/", 170 | }, 171 | runtime: "node", 172 | sourceUrl: "https://github.com/browserbase/mcp-server-browserbase/tree/main/browserbase" 173 | }, 174 | { 175 | config: { 176 | args: ["chakra-mcp"], 177 | command: "uvx", 178 | env: { 179 | "db_session_key": { 180 | description: "Your Chakra database session key. Find it at: https://console.chakra.dev/settings", 181 | } 182 | } 183 | }, 184 | description: "Integrate data from the open data marketplace and your organization natively into chat.", 185 | distribution: { 186 | package: "chakra-mcp", 187 | type: "pip", 188 | }, 189 | id: "chakra", 190 | isOfficial: true, 191 | license: "MIT", 192 | name: "Chakra", 193 | publisher: { 194 | id: "Chakra-Network", 195 | name: "Chakra Digital Labs, Inc.", 196 | url: "https://chakra.dev/", 197 | }, 198 | runtime: "python", 199 | sourceUrl: "https://github.com/Chakra-Network/mcp-server" 200 | }, 201 | { 202 | config: { 203 | args: ["-y", "@modelcontextprotocol/server-everart"], 204 | command: "npx", 205 | env: { 206 | "EVERART_API_KEY": { 207 | description: "Your EverArt API key. Find it at: https://www.everart.ai/api", 208 | } 209 | } 210 | }, 211 | description: "AI image generation using various models using EverArt. A Model Context Protocol reference server.", 212 | distribution: { 213 | package: "@modelcontextprotocol/server-everart", 214 | type: "npm", 215 | }, 216 | id: "everart-ref", 217 | isOfficial: false, 218 | license: "MIT", 219 | name: "EverArt", 220 | publisher: { 221 | id: "modelcontextprotocol", 222 | name: "Anthropic, PBC", 223 | url: "https://modelcontextprotocol.io/", 224 | }, 225 | runtime: "node", 226 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/everart" 227 | }, 228 | { 229 | config: { 230 | args: ["-y", "@modelcontextprotocol/server-everything"], 231 | command: "npx", 232 | env: {} 233 | }, 234 | description: "This MCP server attempts to exercise all the features of the MCP protocol. It is not intended to be a useful server, but rather a test server for builders of MCP clients. A Model Context Protocol reference server.", 235 | distribution: { 236 | package: "@modelcontextprotocol/server-everything", 237 | type: "npm", 238 | }, 239 | id: "everything-ref", 240 | isOfficial: false, 241 | license: "MIT", 242 | name: "Everything", 243 | publisher: { 244 | id: "modelcontextprotocol", 245 | name: "Anthropic, PBC", 246 | url: "https://modelcontextprotocol.io/", 247 | }, 248 | runtime: "node", 249 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/everything" 250 | }, 251 | { 252 | config: { 253 | args: ["-y", "exa-mcp-server"], 254 | command: "npx", 255 | env: { 256 | "EXA_API_KEY": { 257 | description: "Your Exa API key. Find it at: https://dashboard.exa.ai/api-keys", 258 | } 259 | } 260 | }, 261 | description: "This setup allows AI models to get real-time web information in a safe and controlled way.", 262 | distribution: { 263 | package: "exa-mcp-server", 264 | type: "npm", 265 | }, 266 | id: "exa", 267 | isOfficial: true, 268 | name: "Exa Search", 269 | publisher: { 270 | id: "exa-labs", 271 | name: "Exa Labs, Inc.", 272 | url: "https://exa.ai", 273 | }, 274 | runtime: "node", 275 | sourceUrl: "https://github.com/exa-labs/exa-mcp-server" 276 | }, 277 | { 278 | config: { 279 | args: ["mcp-server-fetch"], 280 | command: "uvx", 281 | env: {} 282 | }, 283 | description: "Web content fetching and conversion for efficient LLM usage. A Model Context Protocol reference server.", 284 | distribution: { 285 | package: "mcp-server-fetch", 286 | type: "pip", 287 | }, 288 | id: "fetch-ref", 289 | isOfficial: false, 290 | license: "MIT", 291 | name: "Fetch", 292 | publisher: { 293 | id: "modelcontextprotocol", 294 | name: "Anthropic, PBC", 295 | url: "https://modelcontextprotocol.io/", 296 | }, 297 | runtime: "python", 298 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/fetch" 299 | }, 300 | { 301 | config: { 302 | args: ["-y", "@modelcontextprotocol/server-filesystem"], 303 | command: "npx", 304 | env: {}, 305 | runtimeArgs: { 306 | default: ["/Users/username/Desktop"], 307 | description: "Directories that the server will be allowed to access", 308 | multiple: true 309 | } 310 | }, 311 | description: "Local filesystem access with configurable allowed paths. A Model Context Protocol reference server.", 312 | distribution: { 313 | package: "@modelcontextprotocol/server-filesystem", 314 | type: "npm", 315 | }, 316 | id: "filesystem-ref", 317 | isOfficial: false, 318 | license: "MIT", 319 | name: "Filesystem", 320 | publisher: { 321 | id: "modelcontextprotocol", 322 | name: "Anthropic, PBC", 323 | url: "https://modelcontextprotocol.io/", 324 | }, 325 | runtime: "node", 326 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem" 327 | }, 328 | { 329 | config: { 330 | args: ["-y", "@modelcontextprotocol/server-gdrive"], 331 | command: "npx", 332 | env: {} 333 | }, 334 | description: "File access and search capabilities for Google Drive. A Model Context Protocol reference server.", 335 | distribution: { 336 | package: "@modelcontextprotocol/server-gdrive", 337 | type: "npm", 338 | }, 339 | id: "gdrive-ref", 340 | isOfficial: false, 341 | license: "MIT", 342 | name: "Google Drive", 343 | publisher: { 344 | id: "modelcontextprotocol", 345 | name: "Anthropic, PBC", 346 | url: "https://modelcontextprotocol.io/", 347 | }, 348 | runtime: "node", 349 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/gdrive" 350 | }, 351 | { 352 | config: { 353 | args: ["mcp-server-git", "--repository"], 354 | command: "uvx", 355 | env: {}, 356 | runtimeArgs: { 357 | default: ["path/to/git/repo"], 358 | description: "Filepath to the Git repository", 359 | multiple: false 360 | } 361 | }, 362 | description: "Tools to read, search, and manipulate Git repositories. A Model Context Protocol reference server.", 363 | distribution: { 364 | package: "mcp-server-git", 365 | type: "pip", 366 | }, 367 | id: "git-ref", 368 | isOfficial: false, 369 | license: "MIT", 370 | name: "Git", 371 | publisher: { 372 | id: "modelcontextprotocol", 373 | name: "Anthropic, PBC", 374 | url: "https://modelcontextprotocol.io/", 375 | }, 376 | runtime: "python", 377 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/git" 378 | }, 379 | { 380 | config: { 381 | args: ["-y", "@modelcontextprotocol/server-github"], 382 | command: "npx", 383 | env: { 384 | "GITHUB_PERSONAL_ACCESS_TOKEN": { 385 | description: "Your GitHub Personal Access Token. Find it at: https://github.com/settings/tokens", 386 | } 387 | } 388 | }, 389 | description: "GitHub repository access and management. A Model Context Protocol reference server.", 390 | distribution: { 391 | package: "@modelcontextprotocol/server-github", 392 | type: "npm", 393 | }, 394 | id: "github-ref", 395 | isOfficial: false, 396 | license: "MIT", 397 | name: "GitHub", 398 | publisher: { 399 | id: "modelcontextprotocol", 400 | name: "Anthropic, PBC", 401 | url: "https://modelcontextprotocol.io/", 402 | }, 403 | runtime: "node", 404 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/github" 405 | }, 406 | { 407 | config: { 408 | args: ["-y", "@modelcontextprotocol/server-gitlab"], 409 | command: "npx", 410 | env: { 411 | "GITLAB_API_URL": { 412 | description: "GitLab API URL. Optional, defaults to gitlab.com, configure for self-hosted instances.", 413 | required: false 414 | }, 415 | "GITLAB_PERSONAL_ACCESS_TOKEN": { 416 | description: "Your GitLab Personal Access Token. See: https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html", 417 | } 418 | } 419 | }, 420 | description: "GitLab project access and management. A Model Context Protocol reference server.", 421 | distribution: { 422 | package: "@modelcontextprotocol/server-gitlab", 423 | type: "npm", 424 | }, 425 | id: "gitlab-ref", 426 | isOfficial: false, 427 | license: "MIT", 428 | name: "GitLab", 429 | publisher: { 430 | id: "modelcontextprotocol", 431 | name: "Anthropic, PBC", 432 | url: "https://modelcontextprotocol.io/", 433 | }, 434 | runtime: "node", 435 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/gitlab" 436 | }, 437 | { 438 | config: { 439 | args: ["-y", "@modelcontextprotocol/server-google-maps"], 440 | command: "npx", 441 | env: { 442 | "GOOGLE_MAPS_API_KEY": { 443 | description: "Your Google Maps API key. Find it at: https://console.cloud.google.com/google/maps-apis/credentials", 444 | } 445 | } 446 | }, 447 | description: "Google Maps location services, directions, and place details. A Model Context Protocol reference server.", 448 | distribution: { 449 | package: "@modelcontextprotocol/server-google-maps", 450 | type: "npm", 451 | }, 452 | id: "google-maps-ref", 453 | isOfficial: false, 454 | license: "MIT", 455 | name: "Google Maps", 456 | publisher: { 457 | id: "modelcontextprotocol", 458 | name: "Anthropic, PBC", 459 | url: "https://modelcontextprotocol.io/", 460 | }, 461 | runtime: "node", 462 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps" 463 | }, 464 | { 465 | config: { 466 | args: ["-y", "@modelcontextprotocol/server-memory"], 467 | command: "npx", 468 | env: {} 469 | }, 470 | description: "Knowledge graph-based persistent memory system. A Model Context Protocol reference server.", 471 | distribution: { 472 | package: "@modelcontextprotocol/server-memory", 473 | type: "npm", 474 | }, 475 | id: "memory-ref", 476 | isOfficial: false, 477 | license: "MIT", 478 | name: "Memory", 479 | publisher: { 480 | id: "modelcontextprotocol", 481 | name: "Anthropic, PBC", 482 | url: "https://modelcontextprotocol.io/", 483 | }, 484 | runtime: "node", 485 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/memory" 486 | }, 487 | { 488 | config: { 489 | args: ["-y", "@executeautomation/playwright-mcp-server"], 490 | command: "npx", 491 | env: {} 492 | }, 493 | description: "This server enables LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment using Playwright.", 494 | distribution: { 495 | package: "@executeautomation/playwright-mcp-server", 496 | type: "npm", 497 | }, 498 | id: "playwright-mcp-server", 499 | isOfficial: false, 500 | license: "MIT", 501 | name: "Playwright", 502 | publisher: { 503 | id: "executeautomation", 504 | name: "ExecuteAutomation", 505 | url: "https://github.com/executeautomation", 506 | }, 507 | runtime: "node", 508 | sourceUrl: "https://github.com/executeautomation/mcp-playwright" 509 | }, 510 | { 511 | config: { 512 | args: ["-y", "@modelcontextprotocol/server-postgres"], 513 | command: "npx", 514 | env: {}, 515 | runtimeArgs: { 516 | default: ["postgresql://localhost/mydb"], 517 | description: "PostgreSQL connection string (Replace /mydb with your database name)", 518 | multiple: false 519 | } 520 | }, 521 | description: "Read-only local PostgreSQL database access with schema inspection. A Model Context Protocol reference server.", 522 | distribution: { 523 | package: "@modelcontextprotocol/server-postgres", 524 | type: "npm", 525 | }, 526 | id: "postgres-ref", 527 | isOfficial: false, 528 | license: "MIT", 529 | name: "PostgreSQL", 530 | publisher: { 531 | id: "modelcontextprotocol", 532 | name: "Anthropic, PBC", 533 | url: "https://modelcontextprotocol.io/", 534 | }, 535 | runtime: "node", 536 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/postgres" 537 | }, 538 | { 539 | config: { 540 | args: ["-y", "@modelcontextprotocol/server-puppeteer"], 541 | command: "npx", 542 | env: {} 543 | }, 544 | description: "Browser automation and web scraping using Puppeteer. A Model Context Protocol reference server.", 545 | distribution: { 546 | package: "@modelcontextprotocol/server-puppeteer", 547 | type: "npm", 548 | }, 549 | id: "puppeteer-ref", 550 | isOfficial: false, 551 | license: "MIT", 552 | name: "Puppeteer", 553 | publisher: { 554 | id: "modelcontextprotocol", 555 | name: "Anthropic, PBC", 556 | url: "https://modelcontextprotocol.io/", 557 | }, 558 | runtime: "node", 559 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer" 560 | }, 561 | { 562 | config: { 563 | args: ["mcp-server-sentry", "--auth-token"], 564 | command: "uvx", 565 | env: {}, 566 | runtimeArgs: { 567 | default: ["YOUR_SENTRY_TOKEN"], 568 | description: "Your Sentry authentication token", 569 | multiple: false 570 | } 571 | }, 572 | description: "Retrieving and analyzing issues from Sentry.io. A Model Context Protocol reference server.", 573 | distribution: { 574 | package: "mcp-server-sentry", 575 | type: "pip", 576 | }, 577 | id: "sentry-ref", 578 | isOfficial: false, 579 | license: "MIT", 580 | name: "Sentry", 581 | publisher: { 582 | id: "modelcontextprotocol", 583 | name: "Anthropic, PBC", 584 | url: "https://modelcontextprotocol.io/", 585 | }, 586 | runtime: "python", 587 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/sentry" 588 | }, 589 | { 590 | config: { 591 | args: ["-y", "@modelcontextprotocol/server-sequential-thinking"], 592 | command: "npx", 593 | env: {} 594 | }, 595 | description: "Dynamic and reflective problem-solving through thought sequences. A Model Context Protocol reference server.", 596 | distribution: { 597 | package: "@modelcontextprotocol/server-sequential-thinking", 598 | type: "npm", 599 | }, 600 | id: "sequential-thinking-ref", 601 | isOfficial: false, 602 | license: "MIT", 603 | name: "Sequential Thinking", 604 | publisher: { 605 | id: "modelcontextprotocol", 606 | name: "Anthropic, PBC", 607 | url: "https://modelcontextprotocol.io/", 608 | }, 609 | runtime: "node", 610 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking" 611 | }, 612 | { 613 | config: { 614 | args: ["-y", "@modelcontextprotocol/server-slack"], 615 | command: "npx", 616 | env: { 617 | "SLACK_BOT_TOKEN": { 618 | description: "Your Slack bot token. Find it at: https://api.slack.com/apps", 619 | }, 620 | "SLACK_TEAM_ID": { 621 | description: "Your Slack team/workspace ID, See: https://slack.com/help/articles/221769328-Locate-your-Slack-URL-or-ID#find-your-workspace-or-org-id", 622 | } 623 | } 624 | }, 625 | description: "Slack channel management and messaging capabilities. A Model Context Protocol reference server.", 626 | distribution: { 627 | package: "@modelcontextprotocol/server-slack", 628 | type: "npm", 629 | }, 630 | id: "slack-ref", 631 | isOfficial: false, 632 | license: "MIT", 633 | name: "Slack", 634 | publisher: { 635 | id: "modelcontextprotocol", 636 | name: "Anthropic, PBC", 637 | url: "https://modelcontextprotocol.io/", 638 | }, 639 | runtime: "node", 640 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/slack" 641 | }, 642 | { 643 | config: { 644 | args: ["mcp-server-sqlite", "--db-path"], 645 | command: "uvx", 646 | env: {}, 647 | runtimeArgs: { 648 | default: ["~/test.db"], 649 | description: "Path to your SQLite database file", 650 | multiple: false 651 | } 652 | }, 653 | description: "Local SQLite database interaction and business intelligence capabilities. A Model Context Protocol reference server.", 654 | distribution: { 655 | package: "mcp-server-sqlite", 656 | type: "pip", 657 | }, 658 | id: "sqlite-ref", 659 | isOfficial: false, 660 | license: "MIT", 661 | name: "SQLite", 662 | publisher: { 663 | id: "modelcontextprotocol", 664 | name: "Anthropic, PBC", 665 | url: "https://modelcontextprotocol.io/", 666 | }, 667 | runtime: "python", 668 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite" 669 | }, 670 | { 671 | config: { 672 | args: ['-y', '@browserbasehq/mcp-stagehand'], 673 | command: 'npx', 674 | env: { 675 | 'BROWSERBASE_API_KEY': { 676 | description: 'Your Browserbase API key. Find it at: https://www.browserbase.com/settings', 677 | }, 678 | 'BROWSERBASE_PROJECT_ID': { 679 | description: 'Your Browserbase project ID. Find it at: https://www.browserbase.com/settings', 680 | }, 681 | 'OPENAI_API_KEY': { 682 | description: 'Your OpenAI API key. Find it at: https://platform.openai.com/api-keys', 683 | }, 684 | } 685 | }, 686 | description: "This server enables LLMs to interact with web pages, perform actions, extract data, and observe possible actions in a real browser environment", 687 | distribution: { 688 | package: '@browserbasehq/mcp-stagehand', 689 | type: 'npm', 690 | }, 691 | id: "stagehand", 692 | isOfficial: true, 693 | license: "MIT", 694 | name: "Stagehand by Browserbase", 695 | publisher: { 696 | id: "browserbase", 697 | name: "Browserbase Inc.", 698 | url: "https://www.browserbase.com/", 699 | }, 700 | runtime: "node", 701 | sourceUrl: "https://github.com/browserbase/mcp-server-browserbase/tree/main/stagehand" 702 | }, 703 | { 704 | config: { 705 | args: ["mcp-server-time"], 706 | command: "uvx", 707 | env: {} 708 | }, 709 | description: "Time and timezone conversion capabilities. A Model Context Protocol reference server.", 710 | distribution: { 711 | package: "mcp-server-time", 712 | type: "pip", 713 | }, 714 | id: "time-ref", 715 | isOfficial: false, 716 | license: "MIT", 717 | name: "Time", 718 | publisher: { 719 | id: "modelcontextprotocol", 720 | name: "Anthropic, PBC", 721 | url: "https://modelcontextprotocol.io/", 722 | }, 723 | runtime: "python", 724 | sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/time" 725 | }, 726 | { 727 | config: { 728 | args: ["-y", "@alanagoyal/mcp-server@latest"], 729 | command: "npx", 730 | env: {} 731 | }, 732 | description: "a model context protocol (mcp) server that provides ai assistants with information about alana goyal and basecase, based on alanagoyal.com and basecase.vc. the server integrates with popular ai development environments like windsurf and cursor.", 733 | distribution: { 734 | package: "@alanagoyal/mcp-server", 735 | type: "npm", 736 | }, 737 | id: "alanagoyal", 738 | isOfficial: false, 739 | license: "ISC", 740 | name: "Alana Goyal", 741 | publisher: { 742 | id: "alanagoyal", 743 | name: "Alana Goyal", 744 | url: "https://alanagoyal.com/", 745 | }, 746 | runtime: "node", 747 | sourceUrl: "https://github.com/alanagoyal/mcp-server" 748 | }, 749 | ] 750 | --------------------------------------------------------------------------------