diff --git a/.cursor/rules/derived-cursor-rules.mdc b/.cursor/rules/derived-cursor-rules.mdc new file mode 100644 index 0000000..7865a55 --- /dev/null +++ b/.cursor/rules/derived-cursor-rules.mdc @@ -0,0 +1,79 @@ +--- +description: Cursor rules derived by SpecStory from the project AI interaction history +globs: * +--- + +## PROJECT OVERVIEW +This project is a Node.js based CLI tool. The source code is written in TypeScript. + +## CODE STYLE +Adhere to standard TypeScript coding conventions. Use Prettier for formatting. + +## FOLDER ORGANIZATION +The project follows a standard structure: +- `src`: Source code. +- `test`: Unit tests. +- `cli`: Contains the CLI application's entry point and related files. + + +## TECH STACK +- Node.js +- TypeScript +- npm +- shx (used in build script) +- OCLIF (used for CLI framework) +- zod (added 2025-03-12) + + +## PROJECT-SPECIFIC STANDARDS +- All commands should be defined in the `src/commands` directory. +- Unit tests should be written for every command. Tests should accurately reflect command functionality. Avoid simple string matching in tests; instead verify behavior. +- The `package.json` file should clearly define the entry point for the CLI. +- Avoid using `any` type; explicitly define types for all variables to prevent runtime errors. (Added 2025-03-12) +- Interface properties should be sorted alphabetically to satisfy `perfectionist/sort-object-types` linting rule. (Added 2025-03-12) + + +## WORKFLOW & RELEASE RULES +- Before running the CLI, build the TypeScript code using `npm run build`. +- Version updates will be managed using semantic versioning. (Further details on versioning strategy needed) +- 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. +- 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`. +- Address deprecation warnings related to `fs.Stats` constructor. + + +## REFERENCE EXAMPLES +- Running the CLI from source: See instructions in `2025-03-11_12-54-running-a-package-from-source-instructions.md` +- Fixing TypeScript errors and missing modules: See `2025-03-12_14-21-fixing-typescript-errors-and-missing-modules.md` +- Fixing type issues in `uninstall.ts`: See `2025-03-12_15-33-fixing-type-issues-in-uninstall-ts.md` + + +## PROJECT DOCUMENTATION & CONTEXT SYSTEM +Documentation will be maintained using markdown files and integrated into the repository. + +## DEBUGGING +- Ensure the TypeScript code is built (`npm run build`) before running the CLI. +- The error "command i not found" indicates that the TypeScript code needs to be compiled. +- `'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. +- Missing module errors (e.g., "Cannot find module 'zod'") indicate missing dependencies. Use `npm install` to install required packages. +- Test failures often indicate a mismatch between test expectations and actual command output. Refactor tests to reflect actual command behavior. +- Deprecation warnings related to `fs.Stats` constructor indicate outdated code. Update the relevant code sections. +- Use `npm run lint -- --fix` to automatically fix many linting errors. +- `any` type errors indicate a need for explicit type definitions. Properly type variables to resolve these errors. (Added 2025-03-12) +- `perfectionist/sort-object-types` linting errors indicate improperly ordered object properties. Ensure properties are alphabetically ordered. (Added 2025-03-12) + + +## FINAL DOs AND DON'Ts +- **DO** use TypeScript. +- **DO** write unit tests that accurately reflect command functionality. +- **DO** build the project using `npm run build` before running. +- **DO** follow the folder organization guidelines. +- **DO** install all necessary dependencies using `npm install`. +- **DO** explicitly define types for all variables to avoid runtime errors. (Added 2025-03-12) +- **DO** sort object properties alphabetically to satisfy linting rules. (Added 2025-03-12) +- **DON'T** run the CLI without building the TypeScript code first. +- **DON'T** leave variables untyped; ensure proper type declarations to avoid runtime errors. +- **DON'T** write tests that rely on simple string matching of command output. Focus on verifying actual command behavior. +- **DON'T** ignore deprecation warnings; address them promptly to maintain code quality and avoid future compatibility issues. +- **DON'T** ignore linting errors; address them using `npm run lint -- --fix` or manually. +- **DON'T** use `any` type unless absolutely necessary and with clear justification. (Added 2025-03-12) +- **DON'T** leave object properties unsorted; maintain alphabetical order for consistency and to satisfy linting rules. (Added 2025-03-12) \ No newline at end of file diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..f5d4281 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,2 @@ +# Ignore SpecStory derived-cursor-rules.mdc backup files +.specstory/cursor_rules_backups/* diff --git a/.gitignore b/.gitignore index ed8d894..d95ab6e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ yarn.lock pnpm-lock.yaml *.tsbuildinfo .vscode/ +.specstory diff --git a/package-lock.json b/package-lock.json index 3b372c8..c6b5039 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "dependencies": { "@inquirer/prompts": "^7.2.1", "@oclif/core": "^4", - "@oclif/plugin-help": "^6" + "@oclif/plugin-help": "^6", + "sinon": "^19.0.2", + "zod": "^3.24.2" }, "bin": { "opentools": "bin/run.js" @@ -22,6 +24,7 @@ "@types/chai": "^4", "@types/mocha": "^10", "@types/node": "^18", + "@types/sinon": "^17.0.4", "chai": "^4", "eslint": "^8", "eslint-config-oclif": "^5", @@ -31,7 +34,7 @@ "oclif": "^4", "shx": "^0.3.3", "ts-node": "^10", - "typescript": "^5" + "typescript": "~5.3.3" }, "engines": { "node": ">=18.0.0" @@ -2250,6 +2253,50 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/commons/node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@smithy/abort-controller": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.9.tgz", @@ -3099,6 +3146,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/sinon": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", @@ -6811,6 +6875,12 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6877,6 +6947,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7107,6 +7184,19 @@ "dev": true, "license": "MIT" }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -7482,6 +7572,15 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -8403,6 +8502,45 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sinon": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -8855,7 +8993,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -8952,9 +9089,9 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -9317,6 +9454,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index c53bab1..f6be98e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "dependencies": { "@inquirer/prompts": "^7.2.1", "@oclif/core": "^4", - "@oclif/plugin-help": "^6" + "@oclif/plugin-help": "^6", + "sinon": "^19.0.2", + "zod": "^3.24.2" }, "devDependencies": { "@oclif/prettier-config": "^0.2.1", @@ -18,6 +20,7 @@ "@types/chai": "^4", "@types/mocha": "^10", "@types/node": "^18", + "@types/sinon": "^17.0.4", "chai": "^4", "eslint": "^8", "eslint-config-oclif": "^5", @@ -27,7 +30,7 @@ "oclif": "^4", "shx": "^0.3.3", "ts-node": "^10", - "typescript": "^5" + "typescript": "~5.3.3" }, "engines": { "node": ">=18.0.0" diff --git a/src/commands/install.ts b/src/commands/install.ts index 786198b..99d795c 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -1,38 +1,19 @@ -import {Args, Command, Flags} from '@oclif/core' -import * as fs from 'fs/promises' -import * as path from 'path' -import * as os from 'os' import * as inquirer from '@inquirer/prompts' -import { servers } from '../data/servers/index.js' +import {Args, Command, Flags} from '@oclif/core' +import { exec } from 'node:child_process' +import * as fs from 'node:fs/promises' +import * as os from 'node:os' +import * as path from 'node:path' +import { promisify } from 'node:util' + import type { MCPServerType } from '../data/types.js' -import { promisify } from 'util' -import { exec } from 'child_process' + +import { servers } from '../data/servers/index.js' const execAsync = promisify(exec) export default class Install extends Command { - private CLIENTS_REQUIRING_RESTART: string[] = ['claude'] - - private clientDisplayNames: Record = { - 'claude': 'Claude Desktop', - 'continue': 'Continue' - } - - private clientProcessNames: Record = { - 'claude': 'Claude', - 'continue': 'Continue' - } - - private async validateServer(serverName: string): Promise { - const server = servers.find(s => s.id === serverName) - if (!server) { - this.error(`Server "${serverName}" not found in registry`) - } - if (server.distribution?.type === 'source') { - this.error(`Server "${serverName}" is a source distribution and cannot via OpenTools (for now). To install, please visit ${server.sourceUrl}`) - } - return server - } + static aliases = ['i'] static args = { server: Args.string({ @@ -52,17 +33,61 @@ export default class Install extends Command { static flags = { client: Flags.string({ char: 'c', + default: 'claude', description: 'Install the MCP server to this client', options: ['claude', 'continue'], - default: 'claude', }), } - static aliases = ['i'] + private clientDisplayNames: Record = { + 'claude': 'Claude Desktop', + 'continue': 'Continue' + } + + private clientProcessNames: Record = { + 'claude': 'Claude', + 'continue': 'Continue' + } + + private CLIENTS_REQUIRING_RESTART: string[] = ['claude'] + + public async run(): Promise { + const {args, flags} = await this.parse(Install) + + // Validate server exists in registry + await this.validateServer(args.server) + + // Detect operating system + const {platform} = process + + if (platform !== 'darwin' && platform !== 'win32') { + this.error('This command is only supported on macOS and Windows') + return + } + + this.log(`Installing MCP server: ${args.server}`) + this.log(`Platform: ${platform === 'darwin' ? 'macOS' : 'Windows'}`) + this.log(`Client: ${flags.client}`) + + try { + await (platform === 'darwin' ? this.installOnMacOS(args.server, flags.client) : this.installOnWindows(args.server, flags.client)); + + // After successful installation, prompt for restart + if (this.CLIENTS_REQUIRING_RESTART.includes(flags.client)) { + await this.promptForRestart(flags.client) + } + } catch (error: unknown) { + if (error instanceof Error) { + this.error(`Failed to install server: ${error.message}`) + } else { + this.error('An unknown error occurred during installation') + } + } + } private getConfigPath(client: string): string { switch (client) { - case 'claude': + case 'claude': { return path.join( os.homedir(), 'Library', @@ -70,25 +95,45 @@ export default class Install extends Command { 'Claude', 'claude_desktop_config.json' ) - case 'continue': + } + + case 'continue': { return path.join( os.homedir(), '.continue', 'config.json' ) - default: + } + + default: { throw new Error(`Unsupported client: ${client}`) + } } } private async installMCPServer(configPath: string, serverName: string, client: string): Promise { - let config: any = {} + interface ConfigType { + experimental?: { + modelContextProtocolServers?: Array<{ + transport: { + args: string[]; + command: string; + env: Record; + type: string; + }; + }>; + useTools?: boolean; + }; + mcpServers?: Record; + } + + let config: ConfigType = {} try { // Check if file exists await fs.access(configPath) // Read and parse the config file - const configContent = await fs.readFile(configPath, 'utf-8') + const configContent = await fs.readFile(configPath, 'utf8') config = JSON.parse(configContent) } catch (error: unknown) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { @@ -98,7 +143,7 @@ export default class Install extends Command { // Create empty config config = {} } else { - throw error // Re-throw if it's not a missing file error + throw error } } @@ -107,10 +152,10 @@ export default class Install extends Command { const serverConfig = server.config // Handle runtime arguments if they exist - let finalArgs = [...serverConfig.args] + const finalArgs = [...serverConfig.args] if (serverConfig.runtimeArgs) { const runtimeArg = serverConfig.runtimeArgs - let answer: any + let answer: string | string[] // Special case for filesystem-ref server let defaultValue = runtimeArg.default @@ -122,31 +167,40 @@ export default class Install extends Command { if (runtimeArg.multiple) { // First get the default path - answer = await inquirer.input({ - message: runtimeArg.description, + const initialAnswer = await inquirer.input({ default: Array.isArray(defaultValue) ? defaultValue.join(', ') : defaultValue, + message: runtimeArg.description, }) - let paths = answer.split(',').map((s: string) => s.trim()) + const paths = initialAnswer.split(',').map((s: string) => s.trim()) - // Keep asking for additional paths - while (true) { - const additionalPath = await inquirer.input({ - message: "Add another allowed directory path? (press Enter to finish)", + // Keep asking for additional paths until empty input + const getAdditionalPaths = async (): Promise => { + const additionalPaths: string[] = [] + let input = await inquirer.input({ default: "", + message: "Add another allowed directory path? (press Enter to finish)", }) - if (!additionalPath.trim()) { - break + while (input.trim()) { + additionalPaths.push(input.trim()) + // eslint-disable-next-line no-await-in-loop + input = await inquirer.input({ + default: "", + message: "Add another allowed directory path? (press Enter to finish)", + }) } - paths.push(additionalPath.trim()) + return additionalPaths } + const additionalPaths = await getAdditionalPaths() + paths.push(...additionalPaths) + answer = paths } else { answer = await inquirer.input({ - message: runtimeArg.description, default: defaultValue, + message: runtimeArg.description, }) } @@ -163,12 +217,14 @@ export default class Install extends Command { const answers: Record = {} for (const [key, value] of Object.entries(envVars)) { + // eslint-disable-next-line no-await-in-loop const answer = await inquirer.input({ message: value.description, - validate: (input: string) => { + validate(input: string) { if (value.required !== false && !input) { return `${key} is required` } + return true } }) @@ -182,8 +238,8 @@ export default class Install extends Command { if (client === 'claude') { config.mcpServers = config.mcpServers || {} config.mcpServers[serverName] = { - command: serverConfig.command, args: finalArgs, + command: serverConfig.command, env: answers } } else if (client === 'continue') { @@ -199,16 +255,16 @@ export default class Install extends Command { config.experimental.modelContextProtocolServers = config.experimental.modelContextProtocolServers || [] const serverTransport = { - type: 'stdio', - command: serverConfig.command, args: finalArgs, - env: answers + command: serverConfig.command, + env: answers, + type: 'stdio' } // Find if server already exists in the array const existingServerIndex = config.experimental.modelContextProtocolServers.findIndex( - (s: any) => s.transport.command === serverConfig.command && - JSON.stringify(s.transport.args) === JSON.stringify(finalArgs) + (s) => s.transport.command === serverConfig.command && + JSON.stringify(s.transport.args) === JSON.stringify(finalArgs) ) if (existingServerIndex >= 0) { @@ -243,15 +299,15 @@ export default class Install extends Command { } } - private async installOnWindows(serverName: string, client: string): Promise { + private async installOnWindows(_serverName: string, _client: string): Promise { // TODO: Implement Windows-specific installation logic throw new Error('Windows installation not implemented yet') } private async promptForRestart(client: string): Promise { const answer = await inquirer.confirm({ - message: `Would you like to restart ${this.clientDisplayNames[client]} to apply changes?`, default: true, + message: `Would you like to restart ${this.clientDisplayNames[client]} to apply changes?`, }) if (answer) { @@ -266,73 +322,23 @@ export default class Install extends Command { throw new Error(`Unknown client: ${client}`) } + const sleep = (ms: number) => new Promise(resolve => { setTimeout(resolve, ms) }) + try { - const platform = process.platform + const {platform} = process if (platform === 'darwin') { if (client === 'continue') { - try { - // First, find VS Code's installation location - const findVSCode = await execAsync('mdfind "kMDItemCFBundleIdentifier == \'com.microsoft.VSCode\'" | head -n1') - const vscodePath = findVSCode.stdout.trim() - - if (vscodePath) { - const electronPath = path.join(vscodePath, 'Contents/MacOS/Electron') - // Check if VS Code is running using the found path - const vscodeProcesses = await execAsync(`pgrep -fl "${electronPath}"`) - if (vscodeProcesses.stdout.trim().length > 0) { - // Use pkill with full path to ensure we only kill VS Code's Electron - await execAsync(`pkill -f "${electronPath}"`) - await new Promise(resolve => setTimeout(resolve, 2000)) - await execAsync(`open -a "Visual Studio Code"`) - this.log(`✨ Continue (VS Code) has been restarted`) - return - } - } - } catch (error) { - // VS Code not found or error in detection, try JetBrains - try { - const jetbrainsProcesses = await execAsync('pgrep -fl "IntelliJ IDEA.app"') - if (jetbrainsProcesses.stdout.trim().length > 0) { - await execAsync(`killall "idea"`) - await new Promise(resolve => setTimeout(resolve, 2000)) - await execAsync(`open -a "IntelliJ IDEA"`) - this.log(`✨ Continue (IntelliJ IDEA) has been restarted`) - return - } - } catch { - // JetBrains not found - } - } - - throw new Error('Could not detect running IDE (VS Code or JetBrains) for Continue') + await this.restartContinueClient() } else { // For other clients like Claude, use the normal process await execAsync(`killall "${processName}"`) - await new Promise(resolve => setTimeout(resolve, 2000)) + await sleep(2000) await execAsync(`open -a "${processName}"`) this.log(`✨ ${this.clientDisplayNames[client]} has been restarted`) } } else if (platform === 'win32') { if (client === 'continue') { - try { - const vscodeProcess = await execAsync('tasklist /FI "IMAGENAME eq Code.exe" /FO CSV /NH') - if (vscodeProcess.stdout.includes('Code.exe')) { - await execAsync('taskkill /F /IM "Code.exe" && start "" "Visual Studio Code"') - this.log(`✨ VS Code has been restarted`) - return - } - - const jetbrainsProcess = await execAsync('tasklist /FI "IMAGENAME eq idea64.exe" /FO CSV /NH') - if (jetbrainsProcess.stdout.includes('idea64.exe')) { - await execAsync('taskkill /F /IM "idea64.exe" && start "" "IntelliJ IDEA"') - this.log(`✨ IntelliJ IDEA has been restarted`) - return - } - } catch (error) { - // Process detection failed - } - - throw new Error('Could not detect running IDE (VS Code or JetBrains) for Continue') + await this.restartContinueClientWindows() } else { // For other clients await execAsync(`taskkill /F /IM "${processName}.exe" && start "" "${processName}.exe"`) @@ -355,41 +361,78 @@ export default class Install extends Command { } } - public async run(): Promise { - const {args, flags} = await this.parse(Install) + private async restartContinueClient(): Promise { + const sleep = (ms: number) => new Promise(resolve => { setTimeout(resolve, ms) }) - // Validate server exists in registry - const server = await this.validateServer(args.server) - - // Detect operating system - const platform = process.platform - - if (platform !== 'darwin' && platform !== 'win32') { - this.error('This command is only supported on macOS and Windows') - return + try { + // First, find VS Code's installation location + const findVSCode = await execAsync('mdfind "kMDItemCFBundleIdentifier == \'com.microsoft.VSCode\'" | head -n1') + const vscodePath = findVSCode.stdout.trim() + + if (vscodePath) { + const electronPath = path.join(vscodePath, 'Contents/MacOS/Electron') + // Check if VS Code is running using the found path + const vscodeProcesses = await execAsync(`pgrep -fl "${electronPath}"`) + if (vscodeProcesses.stdout.trim().length > 0) { + // Use pkill with full path to ensure we only kill VS Code's Electron + await execAsync(`pkill -f "${electronPath}"`) + await sleep(2000) + await execAsync(`open -a "Visual Studio Code"`) + this.log(`✨ Continue (VS Code) has been restarted`) + return + } + } + } catch { + // VS Code not found or error in detection, try JetBrains + try { + const jetbrainsProcesses = await execAsync('pgrep -fl "IntelliJ IDEA.app"') + if (jetbrainsProcesses.stdout.trim().length > 0) { + await execAsync(`killall "idea"`) + await sleep(2000) + await execAsync(`open -a "IntelliJ IDEA"`) + this.log(`✨ Continue (IntelliJ IDEA) has been restarted`) + return + } + } catch { + // JetBrains not found + } } - this.log(`Installing MCP server: ${args.server}`) - this.log(`Platform: ${platform === 'darwin' ? 'macOS' : 'Windows'}`) - this.log(`Client: ${flags.client}`) + throw new Error('Could not detect running IDE (VS Code or JetBrains) for Continue') + } + private async restartContinueClientWindows(): Promise { try { - if (platform === 'darwin') { - await this.installOnMacOS(args.server, flags.client) - } else { - await this.installOnWindows(args.server, flags.client) + const vscodeProcess = await execAsync('tasklist /FI "IMAGENAME eq Code.exe" /FO CSV /NH') + if (vscodeProcess.stdout.includes('Code.exe')) { + await execAsync('taskkill /F /IM "Code.exe" && start "" "Visual Studio Code"') + this.log(`✨ VS Code has been restarted`) + return } - // After successful installation, prompt for restart - if (this.CLIENTS_REQUIRING_RESTART.includes(flags.client)) { - await this.promptForRestart(flags.client) - } - } catch (error: unknown) { - if (error instanceof Error) { - this.error(`Failed to install server: ${error.message}`) - } else { - this.error('An unknown error occurred during installation') + const jetbrainsProcess = await execAsync('tasklist /FI "IMAGENAME eq idea64.exe" /FO CSV /NH') + if (jetbrainsProcess.stdout.includes('idea64.exe')) { + await execAsync('taskkill /F /IM "idea64.exe" && start "" "IntelliJ IDEA"') + this.log(`✨ IntelliJ IDEA has been restarted`) + return } + } catch { + // Process detection failed + } + + throw new Error('Could not detect running IDE (VS Code or JetBrains) for Continue') + } + + private async validateServer(serverName: string): Promise { + const server = servers.find(s => s.id === serverName) + if (!server) { + this.error(`Server "${serverName}" not found in registry`) } + + if (server.distribution?.type === 'source') { + this.error(`Server "${serverName}" is a source distribution and cannot via OpenTools (for now). To install, please visit ${server.sourceUrl}`) + } + + return server } } diff --git a/src/commands/list.ts b/src/commands/list.ts index 03fb540..d118048 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -1,14 +1,12 @@ import {Command, Flags} from '@oclif/core' -import * as fs from 'fs/promises' -import * as path from 'path' -import * as os from 'os' +import * as fs from 'node:fs/promises' +import * as os from 'node:os' +import * as path from 'node:path' + import { servers } from '../data/servers/index.js' export default class List extends Command { - private clientDisplayNames: Record = { - 'claude': 'Claude Desktop', - 'continue': 'Continue' - } + static aliases = ['ls'] static override description = 'List installed servers across all clients' @@ -27,7 +25,10 @@ export default class List extends Command { }), } - static aliases = ['ls'] + private clientDisplayNames: Record = { + 'claude': 'Claude Desktop', + 'continue': 'Continue' + } public async run(): Promise { const {flags} = await this.parse(List) @@ -69,40 +70,42 @@ export default class List extends Command { ) try { - const configContent = await fs.readFile(configPath, 'utf-8') + const configContent = await fs.readFile(configPath, 'utf8') const config = JSON.parse(configContent) const installedServers = Object.keys(config.mcpServers || {}) if (installedServers.length > 0) { - this.log(`\n${this.clientDisplayNames['claude']}`) + this.log(`\n${this.clientDisplayNames.claude}`) // Sort servers into registered and unknown const registeredServers = installedServers.filter(id => servers.some(s => s.id === id)) const unknownServers = installedServers.filter(id => !servers.some(s => s.id === id)) // Display registered servers first - registeredServers.forEach((serverId, index) => { + for (const [index, serverId] of registeredServers.entries()) { const prefix = index === registeredServers.length - 1 && unknownServers.length === 0 ? '└── ' : '├── ' const link = `\u001B]8;;https://opentools.com/registry/${serverId}\u0007${serverId}\u001B]8;;\u0007` this.log(`${prefix}${link}`) - }) + } // Then display unknown servers - unknownServers.forEach((serverId, index) => { + for (const [index, serverId] of unknownServers.entries()) { const prefix = index === unknownServers.length - 1 ? '└── ' : '├── ' - this.log(`\x1b[31m${prefix}${serverId} (unknown)\x1b[0m`) - }) + this.log(`\u001B[31m${prefix}${serverId} (unknown)\u001B[0m`) + } return true } + return false } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { return false - } else { - throw error } + + throw error + } } @@ -114,7 +117,7 @@ export default class List extends Command { ) try { - const configContent = await fs.readFile(configPath, 'utf-8') + const configContent = await fs.readFile(configPath, 'utf8') const config = JSON.parse(configContent) // Get installed servers from the experimental.modelContextProtocolServers array @@ -123,7 +126,7 @@ export default class List extends Command { // Map installed servers back to their IDs by matching command and args const validServers = servers .filter(registryServer => - installedServers.some((installed: { transport: { command: string, args: string[] } }) => + installedServers.some((installed: { transport: { args: string[], command: string } }) => installed.transport.command === registryServer.config.command && JSON.stringify(installed.transport.args.slice(0, registryServer.config.args.length)) === JSON.stringify(registryServer.config.args) ) @@ -132,24 +135,27 @@ export default class List extends Command { // Only output if there are valid servers if (validServers.length > 0) { - this.log(`\n${this.clientDisplayNames['continue']}`) + this.log(`\n${this.clientDisplayNames.continue}`) // Print servers in a tree-like format - validServers.forEach((serverId, index) => { + for (const [index, serverId] of validServers.entries()) { const prefix = index === validServers.length - 1 ? '└── ' : '├── ' const link = `\u001B]8;;https://opentools.com/registry/${serverId}\u0007${serverId}\u001B]8;;\u0007` this.log(`${prefix}${link}`) - }) + } + return true } + return false } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { // Don't output anything if client not installed return false - } else { - throw error } + + throw error + } } } diff --git a/src/commands/uninstall.ts b/src/commands/uninstall.ts index fe9e357..c6e3da1 100644 --- a/src/commands/uninstall.ts +++ b/src/commands/uninstall.ts @@ -1,41 +1,29 @@ import {Args, Command, Flags} from '@oclif/core' -import * as fs from 'fs/promises' -import * as path from 'path' -import * as os from 'os' -import { servers } from '../data/servers/index.js' -import type { MCPServerType } from '../data/types.js' +import * as fs from 'node:fs/promises' +import * as os from 'node:os' +import * as path from 'node:path' +import { servers } from '../data/servers/index.js' +interface MCPServerTransport { + transport: { + args: string[]; + command: string; + env?: Record; + type?: string; + }; +} export default class Uninstall extends Command { + static aliases = ['un'] + static override args = { firstServer: Args.string({ description: 'name of the MCP server to uninstall', required: true, }), - } - - static override strict = false // Allow variable length arguments - - static override description = 'Uninstall one or more MCP servers' - - static override examples = [ - '<%= config.bin %> <%= command.id %> server-name', - '<%= config.bin %> <%= command.id %> server-name1 server-name2 --client claude', - ] - - static override flags = { - client: Flags.string({ - char: 'c', - description: 'Uninstall the MCP servers from this client', - options: ['claude', 'continue'], - default: 'claude', - }), - } - - static aliases = ['un'] + } // Allow variable length arguments - // Add completion support static completion: (options: { argv: string[] }) => Promise = async ({ argv }) => { - const input = argv[argv.length - 1] || '' + const input = argv.at(-1) || '' try { const claudeConfigPath = path.join( @@ -53,53 +41,68 @@ export default class Uninstall extends Command { const installedServers = new Set() - // Get Claude Desktop servers try { - const claudeConfig = JSON.parse(await fs.readFile(claudeConfigPath, 'utf-8')) + const claudeConfig = JSON.parse(await fs.readFile(claudeConfigPath, 'utf8')) if (claudeConfig.mcpServers) { - Object.keys(claudeConfig.mcpServers) - .filter(id => claudeConfig.mcpServers[id].status !== 'unknown') - .forEach(id => installedServers.add(id)) + for (const id of Object.keys(claudeConfig.mcpServers) + .filter(id => claudeConfig.mcpServers[id].status !== 'unknown')) { + installedServers.add(id) + } } - } catch (error) { + } catch { // Ignore errors reading Claude config } - // Get Continue servers try { - const continueConfig = JSON.parse(await fs.readFile(continueConfigPath, 'utf-8')) + const continueConfig = JSON.parse(await fs.readFile(continueConfigPath, 'utf8')) if (continueConfig.experimental?.modelContextProtocolServers) { - continueConfig.experimental.modelContextProtocolServers.forEach((s: any) => { - // Find matching server by command and args - const server: MCPServerType | undefined = servers.find(srv => + for (const s of continueConfig.experimental.modelContextProtocolServers) { + const server = servers.find(srv => s.transport.command === srv.config.command && JSON.stringify(s.transport.args.slice(0, srv.config.args.length)) === JSON.stringify(srv.config.args) ) if (server) { installedServers.add(server.id) } - }) + } } - } catch (error) { + } catch { // Ignore errors reading Continue config } - // Filter by input prefix if provided - const matches = Array.from(installedServers).filter(id => + const matches = [...installedServers].filter(id => id.toLowerCase().startsWith(input.toLowerCase()) ) return matches - } catch (error) { + } catch { return [] } } + static override description = 'Uninstall one or more MCP servers' + + static override examples = [ + '<%= config.bin %> <%= command.id %> server-name', + '<%= config.bin %> <%= command.id %> server-name1 server-name2 --client claude', + ] + + static override flags = { + client: Flags.string({ + char: 'c', + default: 'claude', + description: 'Uninstall the MCP servers from this client', + options: ['claude', 'continue'], + }), + } + + static override strict = false + public async run(): Promise { const {argv, flags} = await this.parse(Uninstall) const serverNames = argv as string[] - const platform = process.platform + const {platform} = process if (platform !== 'darwin' && platform !== 'win32') { this.error('This command is only supported on macOS and Windows') @@ -107,12 +110,10 @@ export default class Uninstall extends Command { } // First validate all server IDs exist in our known servers list - for (const serverName of serverNames) { - const serverExists = servers.some(server => server.id === serverName) - if (!serverExists) { - this.error(`Server "${serverName}" is not a valid server ID`) - return - } + const invalidServer = serverNames.find(serverName => !servers.some(server => server.id === serverName)) + if (invalidServer) { + this.error(`Server "${invalidServer}" is not a valid server ID`) + return } // Then check if any servers are in an unknown state @@ -125,16 +126,19 @@ export default class Uninstall extends Command { 'Claude', 'claude_desktop_config.json' ) - const configContent = await fs.readFile(configPath, 'utf-8') - const config = JSON.parse(configContent) + const configContent = await fs.readFile(configPath, 'utf8') + const config = JSON.parse(configContent) as { mcpServers?: Record } - for (const serverName of serverNames) { - if (config.mcpServers?.[serverName]?.status === 'unknown') { - this.error(`Cannot uninstall "${serverName}" because it is in an unknown state. Please try reinstalling it first.`) - return - } + // Process all servers at once to avoid await in loop + const unknownServer = serverNames.find(serverName => + config.mcpServers?.[serverName]?.status === 'unknown' + ) + + if (unknownServer) { + this.error(`Cannot uninstall "${unknownServer}" because it is in an unknown state. Please try reinstalling it first.`) + return } - } catch (error) { + } catch { // If we can't read the config, we'll let the actual uninstall handle the error } } @@ -143,24 +147,20 @@ export default class Uninstall extends Command { this.log(`Client: ${flags.client}`) // Process servers sequentially - for (const serverName of serverNames) { + const uninstallServer = async (serverName: string) => { this.log(`\nUninstalling MCP server: ${serverName}`) + return platform === 'darwin' ? + this.uninstallOnMacOS(serverName, flags.client) : + this.uninstallOnWindows(serverName, flags.client) + } - try { - if (platform === 'darwin') { - await this.uninstallOnMacOS(serverName, flags.client) - } else { - await this.uninstallOnWindows(serverName, flags.client) - } - } catch (error: unknown) { - if (error instanceof Error) { - this.error(`Failed to uninstall server "${serverName}": ${error.message}`) - // Stop processing remaining servers on first failure - return - } else { - this.error(`An unknown error occurred during uninstallation of "${serverName}"`) - return - } + try { + await Promise.all(serverNames.map(name => uninstallServer(name))) + } catch (error: unknown) { + if (error instanceof Error) { + this.error(`Failed to uninstall server: ${error.message}`) + } else { + this.error('An unknown error occurred during installation') } } } @@ -176,23 +176,18 @@ export default class Uninstall extends Command { ) try { - // Check if file exists await fs.access(configPath) - // Read and parse the config file - const configContent = await fs.readFile(configPath, 'utf-8') - const config = JSON.parse(configContent) + const configContent = await fs.readFile(configPath, 'utf8') + const config = JSON.parse(configContent) as { mcpServers?: Record } - // Check if mcpServers exists and has the server if (!config.mcpServers || !config.mcpServers[serverName]) { this.error(`Server "${serverName}" is not installed`) return } - // Remove the server from config delete config.mcpServers[serverName] - // Write the updated config back to file await fs.writeFile(configPath, JSON.stringify(config, null, 2)) this.log(`🗑️ Successfully uninstalled ${serverName}`) @@ -216,21 +211,17 @@ export default class Uninstall extends Command { ) try { - // Check if file exists await fs.access(configPath) - // Read and parse the config file - const configContent = await fs.readFile(configPath, 'utf-8') + const configContent = await fs.readFile(configPath, 'utf8') const config = JSON.parse(configContent) - // Check if experimental and modelContextProtocolServers exist if (!config.experimental?.modelContextProtocolServers?.length) { this.error(`No MCP servers are installed`) return } - // Find the server in the array by matching command and args that would have been used during installation - const serverIndex = config.experimental.modelContextProtocolServers.findIndex((s: any) => { + const serverIndex = config.experimental.modelContextProtocolServers.findIndex((s: MCPServerTransport) => { const server = servers.find(srv => srv.id === serverName) if (!server) return false @@ -243,10 +234,8 @@ export default class Uninstall extends Command { return } - // Remove the server from the array config.experimental.modelContextProtocolServers.splice(serverIndex, 1) - // Write the updated config back to file await fs.writeFile(configPath, JSON.stringify(config, null, 2)) this.log(`🗑️ Successfully uninstalled ${serverName}`) @@ -265,8 +254,7 @@ export default class Uninstall extends Command { } } - private async uninstallOnWindows(serverName: string, client: string): Promise { - // TODO: Implement Windows-specific uninstallation logic + private async uninstallOnWindows(_serverName: string, _client: string): Promise { throw new Error('Windows uninstallation not implemented yet') } } diff --git a/src/data/servers/index.ts b/src/data/servers/index.ts index 793c71e..7326eff 100644 --- a/src/data/servers/index.ts +++ b/src/data/servers/index.ts @@ -2,165 +2,149 @@ import type { MCPServerType } from '../types.js' export const servers: MCPServerType[] = [ { - id: 'artemis', - name: 'Artemis Analytics', - description: 'Pull the latest fundamental crypto data from Artemis natively into you favorite chatbot interface.', - publisher: { - id: 'Artemis-xyz', - name: 'Artemis Analytics Inc.', - url: 'https://www.artemis.xyz/', - }, - isOfficial: true, - sourceUrl: 'https://github.com/Artemis-xyz/artemis-mcp', - distribution: { - type: 'pip', - package: 'artemis-mcp', - }, - license: 'MIT', - runtime: 'python', config: { - command: 'uvx', args: ['artemis-mcp'], + command: 'uvx', env: { ARTEMIS_API_KEY: { description: 'Your Artemis API key from https://app.artemis.xyz/settings.', } } - } - }, - { - id: "aws-kb-retrieval-server-ref", - name: "AWS Knowledge Base", - description: "Retrieval from AWS Knowledge Base using Bedrock Agent Runtime. A Model Context Protocol reference server.", - publisher: { - id: "modelcontextprotocol", - name: "Anthropic, PBC", - url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/aws-kb-retrieval-server", + description: 'Pull the latest fundamental crypto data from Artemis natively into you favorite chatbot interface.', distribution: { - type: "npm", - package: "@modelcontextprotocol/server-aws-kb-retrieval", + package: 'artemis-mcp', + type: 'pip', }, - license: "MIT", - runtime: "node", + id: 'artemis', + isOfficial: true, + license: 'MIT', + name: 'Artemis Analytics', + publisher: { + id: 'Artemis-xyz', + name: 'Artemis Analytics Inc.', + url: 'https://www.artemis.xyz/', + }, + runtime: 'python', + sourceUrl: 'https://github.com/Artemis-xyz/artemis-mcp' + }, + { config: { - command: "npx", args: ["-y", "@modelcontextprotocol/server-aws-kb-retrieval"], + command: "npx", env: { "AWS_ACCESS_KEY_ID": { description: "Your AWS access key ID.", }, - "AWS_SECRET_ACCESS_KEY": { - description: "Your AWS secret access key.", - }, "AWS_REGION": { description: "Your AWS region.", }, + "AWS_SECRET_ACCESS_KEY": { + description: "Your AWS secret access key.", + }, } - } - }, - { - id: "axiom", - name: "Axiom", - description: "Query and analyze your Axiom logs, traces, and all other event data in natural language", - publisher: { - id: "axiomhq", - name: "Axiom, Inc.", - url: "https://axiom.co", }, - isOfficial: true, - sourceUrl: "https://github.com/axiomhq/mcp-server-axiom", + description: "Retrieval from AWS Knowledge Base using Bedrock Agent Runtime. A Model Context Protocol reference server.", distribution: { - type: "source", - source: { - path: "github.com/axiomhq/axiom-mcp@latest", - binary: "axiom-mcp" - } + package: "@modelcontextprotocol/server-aws-kb-retrieval", + type: "npm", }, + id: "aws-kb-retrieval-server-ref", + isOfficial: false, license: "MIT", - runtime: "go", + name: "AWS Knowledge Base", + publisher: { + id: "modelcontextprotocol", + name: "Anthropic, PBC", + url: "https://modelcontextprotocol.io/", + }, + runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/aws-kb-retrieval-server" + }, + { config: { - command: "${HOME}/go/bin/axiom-mcp", args: [], + command: "${HOME}/go/bin/axiom-mcp", // eslint-disable-line no-template-curly-in-string env: { - "AXIOM_TOKEN": { - description: "Your Axiom token.", + "AXIOM_DATASETS_BURST": { + description: "The burst limit for datasets.", + required: false, }, - "AXIOM_URL": { - description: "Your Axiom URL.", + "AXIOM_DATASETS_RATE": { + description: "The rate limit for datasets.", + required: false, }, "AXIOM_ORG_ID": { description: "Your Axiom organization ID.", }, - "AXIOM_QUERY_RATE": { - description: "The rate limit for queries.", - required: false, - }, "AXIOM_QUERY_BURST": { description: "The burst limit for queries.", required: false, }, - "AXIOM_DATASETS_RATE": { - description: "The rate limit for datasets.", + "AXIOM_QUERY_RATE": { + description: "The rate limit for queries.", required: false, }, - "AXIOM_DATASETS_BURST": { - description: "The burst limit for datasets.", - required: false, + "AXIOM_TOKEN": { + description: "Your Axiom token.", + }, + "AXIOM_URL": { + description: "Your Axiom URL.", }, } - } - }, - { - id: "brave-search-ref", - name: "Brave Search", - description: "Web and local search using Brave's Search API. A Model Context Protocol reference server.", - publisher: { - id: "modelcontextprotocol", - name: "Anthropic, PBC", - url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search", + description: "Query and analyze your Axiom logs, traces, and all other event data in natural language", distribution: { - type: "npm", - package: "@modelcontextprotocol/server-brave-search", + source: { + binary: "axiom-mcp", + path: "github.com/axiomhq/axiom-mcp@latest" + }, + type: "source" }, + id: "axiom", + isOfficial: true, license: "MIT", - runtime: "node", + name: "Axiom", + publisher: { + id: "axiomhq", + name: "Axiom, Inc.", + url: "https://axiom.co", + }, + runtime: "go", + sourceUrl: "https://github.com/axiomhq/mcp-server-axiom" + }, + { config: { - command: "npx", args: ["-y", "@modelcontextprotocol/server-brave-search"], + command: "npx", env: { "BRAVE_API_KEY": { description: "Your Brave Search API key. See: https://brave.com/search/api", } } - } - }, - { - id: "browserbase", - name: "Browserbase", - description: "Automate browser interactions in the cloud (e.g. web navigation, data extraction, form filling, and more)", - publisher: { - id: "browserbase", - name: "Browserbase Inc.", - url: "https://www.browserbase.com/", }, - isOfficial: true, - sourceUrl: "https://github.com/browserbase/mcp-server-browserbase/tree/main/browserbase", + description: "Web and local search using Brave's Search API. A Model Context Protocol reference server.", distribution: { + package: "@modelcontextprotocol/server-brave-search", type: "npm", - package: "@browserbasehq/mcp-browserbase", }, + id: "brave-search-ref", + isOfficial: false, license: "MIT", + name: "Brave Search", + publisher: { + id: "modelcontextprotocol", + name: "Anthropic, PBC", + url: "https://modelcontextprotocol.io/", + }, runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search" + }, + { config: { - command: "npx", args: ["-y", "@browserbasehq/mcp-browserbase"], + command: "npx", env: { "BROWSERBASE_API_KEY": { description: "Your Browserbase API key. Find it at: https://www.browserbase.com/settings", @@ -169,466 +153,466 @@ export const servers: MCPServerType[] = [ description: "Your Browserbase project ID. Find it at: https://www.browserbase.com/settings", }, } - } - }, - { - id: "chakra", - name: "Chakra", - description: "Integrate data from the open data marketplace and your organization natively into chat.", - publisher: { - id: "Chakra-Network", - name: "Chakra Digital Labs, Inc.", - url: "https://chakra.dev/", }, - isOfficial: true, - sourceUrl: "https://github.com/Chakra-Network/mcp-server", + description: "Automate browser interactions in the cloud (e.g. web navigation, data extraction, form filling, and more)", distribution: { - type: "pip", - package: "chakra-mcp", + package: "@browserbasehq/mcp-browserbase", + type: "npm", }, + id: "browserbase", + isOfficial: true, license: "MIT", - runtime: "python", + name: "Browserbase", + publisher: { + id: "browserbase", + name: "Browserbase Inc.", + url: "https://www.browserbase.com/", + }, + runtime: "node", + sourceUrl: "https://github.com/browserbase/mcp-server-browserbase/tree/main/browserbase" + }, + { config: { - command: "uvx", args: ["chakra-mcp"], + command: "uvx", env: { "db_session_key": { description: "Your Chakra database session key. Find it at: https://console.chakra.dev/settings", } } - } - }, - { - id: "everart-ref", - name: "EverArt", - description: "AI image generation using various models using EverArt. A Model Context Protocol reference server.", - publisher: { - id: "modelcontextprotocol", - name: "Anthropic, PBC", - url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/everart", + description: "Integrate data from the open data marketplace and your organization natively into chat.", distribution: { - type: "npm", - package: "@modelcontextprotocol/server-everart", + package: "chakra-mcp", + type: "pip", }, + id: "chakra", + isOfficial: true, license: "MIT", - runtime: "node", + name: "Chakra", + publisher: { + id: "Chakra-Network", + name: "Chakra Digital Labs, Inc.", + url: "https://chakra.dev/", + }, + runtime: "python", + sourceUrl: "https://github.com/Chakra-Network/mcp-server" + }, + { config: { - command: "npx", args: ["-y", "@modelcontextprotocol/server-everart"], + command: "npx", env: { "EVERART_API_KEY": { description: "Your EverArt API key. Find it at: https://www.everart.ai/api", } } - } - }, - { - id: "everything-ref", - name: "Everything", - 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.", - publisher: { - id: "modelcontextprotocol", - name: "Anthropic, PBC", - url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/everything", + description: "AI image generation using various models using EverArt. A Model Context Protocol reference server.", distribution: { + package: "@modelcontextprotocol/server-everart", type: "npm", - package: "@modelcontextprotocol/server-everything", }, + id: "everart-ref", + isOfficial: false, license: "MIT", + name: "EverArt", + publisher: { + id: "modelcontextprotocol", + name: "Anthropic, PBC", + url: "https://modelcontextprotocol.io/", + }, runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/everart" + }, + { config: { - command: "npx", args: ["-y", "@modelcontextprotocol/server-everything"], + command: "npx", env: {} - } - }, - { - id: "exa", - name: "Exa Search", - description: "This setup allows AI models to get real-time web information in a safe and controlled way.", - publisher: { - id: "exa-labs", - name: "Exa Labs, Inc.", - url: "https://exa.ai", }, - isOfficial: true, - sourceUrl: "https://github.com/exa-labs/exa-mcp-server", + 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.", distribution: { + package: "@modelcontextprotocol/server-everything", type: "npm", - package: "exa-mcp-server", + }, + id: "everything-ref", + isOfficial: false, + license: "MIT", + name: "Everything", + publisher: { + id: "modelcontextprotocol", + name: "Anthropic, PBC", + url: "https://modelcontextprotocol.io/", }, runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/everything" + }, + { config: { - command: "npx", args: ["-y", "exa-mcp-server"], + command: "npx", env: { "EXA_API_KEY": { description: "Your Exa API key. Find it at: https://dashboard.exa.ai/api-keys", } } - } + }, + description: "This setup allows AI models to get real-time web information in a safe and controlled way.", + distribution: { + package: "exa-mcp-server", + type: "npm", + }, + id: "exa", + isOfficial: true, + name: "Exa Search", + publisher: { + id: "exa-labs", + name: "Exa Labs, Inc.", + url: "https://exa.ai", + }, + runtime: "node", + sourceUrl: "https://github.com/exa-labs/exa-mcp-server" }, { + config: { + args: ["mcp-server-fetch"], + command: "uvx", + env: {} + }, + description: "Web content fetching and conversion for efficient LLM usage. A Model Context Protocol reference server.", + distribution: { + package: "mcp-server-fetch", + type: "pip", + }, id: "fetch-ref", + isOfficial: false, + license: "MIT", name: "Fetch", - description: "Web content fetching and conversion for efficient LLM usage. A Model Context Protocol reference server.", publisher: { id: "modelcontextprotocol", name: "Anthropic, PBC", url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/fetch", - distribution: { - type: "pip", - package: "mcp-server-fetch", - }, - license: "MIT", runtime: "python", - config: { - command: "uvx", - args: ["mcp-server-fetch"], - env: {} - } + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/fetch" }, { + config: { + args: ["-y", "@modelcontextprotocol/server-filesystem"], + command: "npx", + env: {}, + runtimeArgs: { + default: ["/Users/username/Desktop"], + description: "Directories that the server will be allowed to access", + multiple: true + } + }, + description: "Local filesystem access with configurable allowed paths. A Model Context Protocol reference server.", + distribution: { + package: "@modelcontextprotocol/server-filesystem", + type: "npm", + }, id: "filesystem-ref", + isOfficial: false, + license: "MIT", name: "Filesystem", - description: "Local filesystem access with configurable allowed paths. A Model Context Protocol reference server.", publisher: { id: "modelcontextprotocol", name: "Anthropic, PBC", url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem", - distribution: { - type: "npm", - package: "@modelcontextprotocol/server-filesystem", - }, - license: "MIT", runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem" + }, + { config: { + args: ["-y", "@modelcontextprotocol/server-gdrive"], command: "npx", - args: ["-y", "@modelcontextprotocol/server-filesystem"], - runtimeArgs: { - description: "Directories that the server will be allowed to access", - default: ["/Users/username/Desktop"], - multiple: true - }, env: {} - } - }, - { - id: "gdrive-ref", - name: "Google Drive", - description: "File access and search capabilities for Google Drive. A Model Context Protocol reference server.", - publisher: { - id: "modelcontextprotocol", - name: "Anthropic, PBC", - url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/gdrive", + description: "File access and search capabilities for Google Drive. A Model Context Protocol reference server.", distribution: { - type: "npm", package: "@modelcontextprotocol/server-gdrive", + type: "npm", }, + id: "gdrive-ref", + isOfficial: false, license: "MIT", - runtime: "node", - config: { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-gdrive"], - env: {} - } - }, - { - id: "git-ref", - name: "Git", - description: "Tools to read, search, and manipulate Git repositories. A Model Context Protocol reference server.", + name: "Google Drive", publisher: { id: "modelcontextprotocol", name: "Anthropic, PBC", url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/git", - distribution: { - type: "pip", - package: "mcp-server-git", - }, - license: "MIT", - runtime: "python", + runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/gdrive" + }, + { config: { - command: "uvx", args: ["mcp-server-git", "--repository"], + command: "uvx", + env: {}, runtimeArgs: { - description: "Filepath to the Git repository", default: ["path/to/git/repo"], + description: "Filepath to the Git repository", multiple: false - }, - env: {} - } - }, - { - id: "github-ref", - name: "GitHub", - description: "GitHub repository access and management. A Model Context Protocol reference server.", + } + }, + description: "Tools to read, search, and manipulate Git repositories. A Model Context Protocol reference server.", + distribution: { + package: "mcp-server-git", + type: "pip", + }, + id: "git-ref", + isOfficial: false, + license: "MIT", + name: "Git", publisher: { id: "modelcontextprotocol", name: "Anthropic, PBC", url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/github", - distribution: { - type: "npm", - package: "@modelcontextprotocol/server-github", - }, - license: "MIT", - runtime: "node", + runtime: "python", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/git" + }, + { config: { - command: "npx", args: ["-y", "@modelcontextprotocol/server-github"], + command: "npx", env: { "GITHUB_PERSONAL_ACCESS_TOKEN": { description: "Your GitHub Personal Access Token. Find it at: https://github.com/settings/tokens", } } - } - }, - { - id: "gitlab-ref", - name: "GitLab", - description: "GitLab project access and management. A Model Context Protocol reference server.", - publisher: { - id: "modelcontextprotocol", - name: "Anthropic, PBC", - url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/gitlab", + description: "GitHub repository access and management. A Model Context Protocol reference server.", distribution: { + package: "@modelcontextprotocol/server-github", type: "npm", - package: "@modelcontextprotocol/server-gitlab", }, + id: "github-ref", + isOfficial: false, license: "MIT", + name: "GitHub", + publisher: { + id: "modelcontextprotocol", + name: "Anthropic, PBC", + url: "https://modelcontextprotocol.io/", + }, runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/github" + }, + { config: { - command: "npx", args: ["-y", "@modelcontextprotocol/server-gitlab"], + command: "npx", env: { - "GITLAB_PERSONAL_ACCESS_TOKEN": { - description: "Your GitLab Personal Access Token. See: https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html", - }, "GITLAB_API_URL": { description: "GitLab API URL. Optional, defaults to gitlab.com, configure for self-hosted instances.", required: false + }, + "GITLAB_PERSONAL_ACCESS_TOKEN": { + description: "Your GitLab Personal Access Token. See: https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html", } } - } - }, - { - id: "google-maps-ref", - name: "Google Maps", - description: "Google Maps location services, directions, and place details. A Model Context Protocol reference server.", - publisher: { - id: "modelcontextprotocol", - name: "Anthropic, PBC", - url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps", + description: "GitLab project access and management. A Model Context Protocol reference server.", distribution: { + package: "@modelcontextprotocol/server-gitlab", type: "npm", - package: "@modelcontextprotocol/server-google-maps", }, + id: "gitlab-ref", + isOfficial: false, license: "MIT", + name: "GitLab", + publisher: { + id: "modelcontextprotocol", + name: "Anthropic, PBC", + url: "https://modelcontextprotocol.io/", + }, runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/gitlab" + }, + { config: { - command: "npx", args: ["-y", "@modelcontextprotocol/server-google-maps"], + command: "npx", env: { "GOOGLE_MAPS_API_KEY": { description: "Your Google Maps API key. Find it at: https://console.cloud.google.com/google/maps-apis/credentials", } } - } + }, + description: "Google Maps location services, directions, and place details. A Model Context Protocol reference server.", + distribution: { + package: "@modelcontextprotocol/server-google-maps", + type: "npm", + }, + id: "google-maps-ref", + isOfficial: false, + license: "MIT", + name: "Google Maps", + publisher: { + id: "modelcontextprotocol", + name: "Anthropic, PBC", + url: "https://modelcontextprotocol.io/", + }, + runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps" }, { + config: { + args: ["-y", "@modelcontextprotocol/server-memory"], + command: "npx", + env: {} + }, + description: "Knowledge graph-based persistent memory system. A Model Context Protocol reference server.", + distribution: { + package: "@modelcontextprotocol/server-memory", + type: "npm", + }, id: "memory-ref", + isOfficial: false, + license: "MIT", name: "Memory", - description: "Knowledge graph-based persistent memory system. A Model Context Protocol reference server.", publisher: { id: "modelcontextprotocol", name: "Anthropic, PBC", url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/memory", - distribution: { - type: "npm", - package: "@modelcontextprotocol/server-memory", - }, - license: "MIT", runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/memory" + }, + { config: { + args: ["-y", "@executeautomation/playwright-mcp-server"], command: "npx", - args: ["-y", "@modelcontextprotocol/server-memory"], env: {} - } - }, - { + }, + description: "This server enables LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment using Playwright.", + distribution: { + package: "@executeautomation/playwright-mcp-server", + type: "npm", + }, id: "playwright-mcp-server", + isOfficial: false, + license: "MIT", name: "Playwright", - description: "This server enables LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment using Playwright.", publisher: { id: "executeautomation", name: "ExecuteAutomation", url: "https://github.com/executeautomation", }, - isOfficial: false, - sourceUrl: "https://github.com/executeautomation/mcp-playwright", - distribution: { - type: "npm", - package: "@executeautomation/playwright-mcp-server", - }, - license: "MIT", runtime: "node", - config: { - command: "npx", - args: ["-y", "@executeautomation/playwright-mcp-server"], - env: {} - } + sourceUrl: "https://github.com/executeautomation/mcp-playwright" }, { + config: { + args: ["-y", "@modelcontextprotocol/server-postgres"], + command: "npx", + env: {}, + runtimeArgs: { + default: ["postgresql://localhost/mydb"], + description: "PostgreSQL connection string (Replace /mydb with your database name)", + multiple: false + } + }, + description: "Read-only local PostgreSQL database access with schema inspection. A Model Context Protocol reference server.", + distribution: { + package: "@modelcontextprotocol/server-postgres", + type: "npm", + }, id: "postgres-ref", + isOfficial: false, + license: "MIT", name: "PostgreSQL", - description: "Read-only local PostgreSQL database access with schema inspection. A Model Context Protocol reference server.", publisher: { id: "modelcontextprotocol", name: "Anthropic, PBC", url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/postgres", - distribution: { - type: "npm", - package: "@modelcontextprotocol/server-postgres", - }, - license: "MIT", runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/postgres" + }, + { config: { + args: ["-y", "@modelcontextprotocol/server-puppeteer"], command: "npx", - args: ["-y", "@modelcontextprotocol/server-postgres"], - runtimeArgs: { - description: "PostgreSQL connection string (Replace /mydb with your database name)", - default: ["postgresql://localhost/mydb"], - multiple: false - }, env: {} - } - }, - { + }, + description: "Browser automation and web scraping using Puppeteer. A Model Context Protocol reference server.", + distribution: { + package: "@modelcontextprotocol/server-puppeteer", + type: "npm", + }, id: "puppeteer-ref", + isOfficial: false, + license: "MIT", name: "Puppeteer", - description: "Browser automation and web scraping using Puppeteer. A Model Context Protocol reference server.", publisher: { id: "modelcontextprotocol", name: "Anthropic, PBC", url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer", - distribution: { - type: "npm", - package: "@modelcontextprotocol/server-puppeteer", - }, - license: "MIT", runtime: "node", - config: { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-puppeteer"], - env: {} - } + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer" }, { + config: { + args: ["mcp-server-sentry", "--auth-token"], + command: "uvx", + env: {}, + runtimeArgs: { + default: ["YOUR_SENTRY_TOKEN"], + description: "Your Sentry authentication token", + multiple: false + } + }, + description: "Retrieving and analyzing issues from Sentry.io. A Model Context Protocol reference server.", + distribution: { + package: "mcp-server-sentry", + type: "pip", + }, id: "sentry-ref", + isOfficial: false, + license: "MIT", name: "Sentry", - description: "Retrieving and analyzing issues from Sentry.io. A Model Context Protocol reference server.", publisher: { id: "modelcontextprotocol", name: "Anthropic, PBC", url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/sentry", - distribution: { - type: "pip", - package: "mcp-server-sentry", - }, - license: "MIT", runtime: "python", - config: { - command: "uvx", - args: ["mcp-server-sentry", "--auth-token"], - runtimeArgs: { - description: "Your Sentry authentication token", - default: ["YOUR_SENTRY_TOKEN"], - multiple: false - }, - env: {} - } + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/sentry" }, { - id: "sequential-thinking-ref", - name: "Sequential Thinking", - description: "Dynamic and reflective problem-solving through thought sequences. A Model Context Protocol reference server.", - publisher: { - id: "modelcontextprotocol", - name: "Anthropic, PBC", - url: "https://modelcontextprotocol.io/", + config: { + args: ["-y", "@modelcontextprotocol/server-sequential-thinking"], + command: "npx", + env: {} }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking", + description: "Dynamic and reflective problem-solving through thought sequences. A Model Context Protocol reference server.", distribution: { - type: "npm", package: "@modelcontextprotocol/server-sequential-thinking", + type: "npm", }, + id: "sequential-thinking-ref", + isOfficial: false, license: "MIT", - runtime: "node", - config: { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-sequential-thinking"], - env: {} - } - }, - { - id: "slack-ref", - name: "Slack", - description: "Slack channel management and messaging capabilities. A Model Context Protocol reference server.", + name: "Sequential Thinking", publisher: { id: "modelcontextprotocol", name: "Anthropic, PBC", url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/slack", - distribution: { - type: "npm", - package: "@modelcontextprotocol/server-slack", - }, - license: "MIT", runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking" + }, + { config: { - command: "npx", args: ["-y", "@modelcontextprotocol/server-slack"], + command: "npx", env: { "SLACK_BOT_TOKEN": { description: "Your Slack bot token. Find it at: https://api.slack.com/apps", @@ -637,56 +621,56 @@ export const servers: MCPServerType[] = [ 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", } } - } - }, - { - id: "sqlite-ref", - name: "SQLite", - description: "Local SQLite database interaction and business intelligence capabilities. A Model Context Protocol reference server.", + }, + description: "Slack channel management and messaging capabilities. A Model Context Protocol reference server.", + distribution: { + package: "@modelcontextprotocol/server-slack", + type: "npm", + }, + id: "slack-ref", + isOfficial: false, + license: "MIT", + name: "Slack", publisher: { id: "modelcontextprotocol", name: "Anthropic, PBC", url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite", - distribution: { - type: "pip", - package: "mcp-server-sqlite", - }, - license: "MIT", - runtime: "python", + runtime: "node", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/slack" + }, + { config: { - command: "uvx", args: ["mcp-server-sqlite", "--db-path"], + command: "uvx", + env: {}, runtimeArgs: { - description: "Path to your SQLite database file", default: ["~/test.db"], + description: "Path to your SQLite database file", multiple: false - }, - env: {} - } - }, - { - id: "stagehand", - name: "Stagehand by Browserbase", - description: "This server enables LLMs to interact with web pages, perform actions, extract data, and observe possible actions in a real browser environment", - publisher: { - id: "browserbase", - name: "Browserbase Inc.", - url: "https://www.browserbase.com/", + } }, - isOfficial: true, - sourceUrl: "https://github.com/browserbase/mcp-server-browserbase/tree/main/stagehand", + description: "Local SQLite database interaction and business intelligence capabilities. A Model Context Protocol reference server.", distribution: { - type: 'npm', - package: '@browserbasehq/mcp-stagehand', + package: "mcp-server-sqlite", + type: "pip", }, + id: "sqlite-ref", + isOfficial: false, license: "MIT", - runtime: "node", + name: "SQLite", + publisher: { + id: "modelcontextprotocol", + name: "Anthropic, PBC", + url: "https://modelcontextprotocol.io/", + }, + runtime: "python", + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite" + }, + { config: { - command: 'npx', args: ['-y', '@browserbasehq/mcp-stagehand'], + command: 'npx', env: { 'BROWSERBASE_API_KEY': { description: 'Your Browserbase API key. Find it at: https://www.browserbase.com/settings', @@ -698,29 +682,45 @@ export const servers: MCPServerType[] = [ description: 'Your OpenAI API key. Find it at: https://platform.openai.com/api-keys', }, } - } + }, + description: "This server enables LLMs to interact with web pages, perform actions, extract data, and observe possible actions in a real browser environment", + distribution: { + package: '@browserbasehq/mcp-stagehand', + type: 'npm', + }, + id: "stagehand", + isOfficial: true, + license: "MIT", + name: "Stagehand by Browserbase", + publisher: { + id: "browserbase", + name: "Browserbase Inc.", + url: "https://www.browserbase.com/", + }, + runtime: "node", + sourceUrl: "https://github.com/browserbase/mcp-server-browserbase/tree/main/stagehand" }, { + config: { + args: ["mcp-server-time"], + command: "uvx", + env: {} + }, + description: "Time and timezone conversion capabilities. A Model Context Protocol reference server.", + distribution: { + package: "mcp-server-time", + type: "pip", + }, id: "time-ref", + isOfficial: false, + license: "MIT", name: "Time", - description: "Time and timezone conversion capabilities. A Model Context Protocol reference server.", publisher: { id: "modelcontextprotocol", name: "Anthropic, PBC", url: "https://modelcontextprotocol.io/", }, - isOfficial: false, - sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/time", - distribution: { - type: "pip", - package: "mcp-server-time", - }, - license: "MIT", runtime: "python", - config: { - command: "uvx", - args: ["mcp-server-time"], - env: {} - } + sourceUrl: "https://github.com/modelcontextprotocol/servers/tree/main/src/time" }, ] diff --git a/src/data/types.ts b/src/data/types.ts index f59698f..70d279b 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -6,40 +6,40 @@ export const EnvVariable = z.object({ }) export const MCPServerRuntimeArg = z.object({ - description: z.string(), default: z.any().optional(), + description: z.string(), multiple: z.boolean().optional().default(false), }) export const MCPServerConfig = z.object({ - command: z.string(), args: z.array(z.string()), - runtimeArgs: MCPServerRuntimeArg.optional(), + command: z.string(), env: z.record(z.string(), EnvVariable), + runtimeArgs: MCPServerRuntimeArg.optional(), }) export const MCPServer = z.object({ - id: z.string().regex(/^[a-z0-9-]+$/), - name: z.string(), + config: MCPServerConfig, description: z.string(), - publisher: z.object({ - id: z.string().regex(/^[a-z0-9-]+$/), - name: z.string(), - url: z.string().url(), - }), - isOfficial: z.boolean().default(false), - sourceUrl: z.string().url(), distribution: z.object({ - type: z.enum(['npm', 'pip', 'source']), package: z.string().optional(), source: z.object({ - path: z.string(), - binary: z.string() + binary: z.string(), + path: z.string() }).optional(), + type: z.enum(['npm', 'pip', 'source']), }), + id: z.string().regex(/^[\da-z-]+$/), + isOfficial: z.boolean().default(false), license: z.string().optional(), + name: z.string(), + publisher: z.object({ + id: z.string().regex(/^[\da-z-]+$/), + name: z.string(), + url: z.string().url(), + }), runtime: z.enum(['node', 'python', 'go', 'other']), - config: MCPServerConfig, + sourceUrl: z.string().url(), }) // Infer types from schemas diff --git a/src/index.ts b/src/index.ts index b7f081b..a7bb280 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -export {run} from '@oclif/core' +export * from './data/servers' // Export types and data export * from './data/types' -export * from './data/servers' +export {run} from '@oclif/core' diff --git a/test/commands/hello/index.test.ts b/test/commands/hello/index.test.ts deleted file mode 100644 index dad0ac3..0000000 --- a/test/commands/hello/index.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {runCommand} from '@oclif/test' -import {expect} from 'chai' - -describe('hello', () => { - it('runs hello', async () => { - const {stdout} = await runCommand('hello friend --from oclif') - expect(stdout).to.contain('hello friend from oclif!') - }) -}) diff --git a/test/commands/hello/world.test.ts b/test/commands/hello/world.test.ts deleted file mode 100644 index 0f5e90f..0000000 --- a/test/commands/hello/world.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {runCommand} from '@oclif/test' -import {expect} from 'chai' - -describe('hello world', () => { - it('runs hello world cmd', async () => { - const {stdout} = await runCommand('hello world') - expect(stdout).to.contain('hello world!') - }) -}) diff --git a/test/commands/install.test.ts b/test/commands/install.test.ts index b67eeec..6a04548 100644 --- a/test/commands/install.test.ts +++ b/test/commands/install.test.ts @@ -2,13 +2,21 @@ import {runCommand} from '@oclif/test' import {expect} from 'chai' describe('install', () => { - it('runs install cmd', async () => { - const {stdout} = await runCommand('install') - expect(stdout).to.contain('hello world') + it('fails when server name is not provided', async () => { + try { + await runCommand('install') + expect.fail('Command should have failed without server name') + } catch (error: unknown) { + expect(error).to.exist + } }) - it('runs install --name oclif', async () => { - const {stdout} = await runCommand('install --name oclif') - expect(stdout).to.contain('hello oclif') + it('fails when server does not exist', async () => { + try { + await runCommand('install nonexistent-server') + expect.fail('Command should have failed with nonexistent server') + } catch (error: unknown) { + expect(error).to.exist + } }) }) diff --git a/test/commands/list.test.ts b/test/commands/list.test.ts index c46b32e..2de3d5e 100644 --- a/test/commands/list.test.ts +++ b/test/commands/list.test.ts @@ -1,14 +1,38 @@ import {runCommand} from '@oclif/test' import {expect} from 'chai' +import sinon from 'sinon' + +import List from '../../src/commands/list.js' describe('list', () => { - it('runs list cmd', async () => { + let listClaudeServersStub: sinon.SinonStub + let listContinueServersStub: sinon.SinonStub + + beforeEach(() => { + // Create stubs for the private methods + listClaudeServersStub = sinon.stub(List.prototype, 'listClaudeServers' as keyof typeof List.prototype) + listContinueServersStub = sinon.stub(List.prototype, 'listContinueServers' as keyof typeof List.prototype) + + // Make both methods return false (no servers found) + listClaudeServersStub.resolves(false) + listContinueServersStub.resolves(false) + }) + + afterEach(() => { + // Restore the original methods after each test + listClaudeServersStub.restore() + listContinueServersStub.restore() + }) + + it('runs list cmd with no servers found', async () => { const {stdout} = await runCommand('list') - expect(stdout).to.contain('hello world') + // When no servers are found, it should show the "No MCP servers" message + expect(stdout).to.include('No MCP servers currently installed.') }) - it('runs list --name oclif', async () => { - const {stdout} = await runCommand('list --name oclif') - expect(stdout).to.contain('hello oclif') + it('runs list with client flag and no servers found', async () => { + const {stdout} = await runCommand('list --client=claude') + // When filtering by client and no servers found, it should show client-specific message + expect(stdout).to.include('No MCP servers currently installed on Claude Desktop.') }) }) diff --git a/test/commands/uninstall.test.ts b/test/commands/uninstall.test.ts index 69db142..23c8f03 100644 --- a/test/commands/uninstall.test.ts +++ b/test/commands/uninstall.test.ts @@ -2,13 +2,22 @@ import {runCommand} from '@oclif/test' import {expect} from 'chai' describe('uninstall', () => { - it('runs uninstall cmd', async () => { - const {stdout} = await runCommand('uninstall') - expect(stdout).to.contain('hello world') + it('fails when server name is not provided', async () => { + try { + await runCommand('uninstall') + expect.fail('Command should have failed without server name') + } catch (error: unknown) { + expect(error).to.exist + } }) - it('runs uninstall --name oclif', async () => { - const {stdout} = await runCommand('uninstall --name oclif') - expect(stdout).to.contain('hello oclif') + it('handles multiple server names', async () => { + try { + // This will fail because the servers don't exist, but we can test the argument parsing + await runCommand('uninstall server1 server2') + expect.fail('Command should have failed with nonexistent servers') + } catch (error: unknown) { + expect(error).to.exist + } }) })