├── .prettierrc.json ├── src ├── index.ts ├── commands │ ├── clients.ts │ ├── servers.ts │ ├── remove.ts │ ├── list.ts │ ├── add.ts │ ├── tool.ts │ ├── tools.ts │ └── run.ts └── utils │ └── spawn.ts ├── bin ├── run.cmd ├── dev.cmd ├── run.js └── dev.js ├── test ├── tsconfig.json ├── integration.test.ts └── run.test.ts ├── .gitignore ├── tsconfig.tsbuildinfo ├── .mocharc.json ├── tsconfig.json ├── eslint.config.mjs ├── mcp-server-node.mjs ├── .github └── workflows │ ├── lint-workflows.yml │ ├── test.yml │ ├── onRelease.yml │ └── onPushToMain.yml ├── .vscode └── launch.json ├── LICENSE ├── mcp-server.py ├── package.json ├── README.md └── mcp-servers.json /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@oclif/prettier-config" 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {run} from '@oclif/core' 2 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [ 7 | {"path": ".."} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /bin/dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning 2 | 3 | import {execute} from '@oclif/core' 4 | 5 | await execute({development: true, dir: import.meta.url}) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | **/.DS_Store 4 | /.idea 5 | /dist 6 | /tmp 7 | /node_modules 8 | coverage 9 | oclif.manifest.json 10 | 11 | 12 | 13 | yarn.lock 14 | pnpm-lock.yaml 15 | 16 | -------------------------------------------------------------------------------- /tsconfig.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./src/index.ts","./src/commands/add.ts","./src/commands/clients.ts","./src/commands/list.ts","./src/commands/remove.ts","./src/commands/run.ts","./src/commands/servers.ts","./src/commands/tool.ts","./src/commands/tools.ts"],"version":"5.8.2"} -------------------------------------------------------------------------------- /.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 | "declaration": true, 4 | "module": "Node16", 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "strict": true, 8 | "target": "es2022", 9 | "moduleResolution": "node16" 10 | }, 11 | "include": ["./src/**/*"], 12 | "ts-node": { 13 | "esm": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/clients.ts: -------------------------------------------------------------------------------- 1 | import {Command} from '@oclif/core' 2 | 3 | export default class Clients extends Command { 4 | static description = 'List supported clients' 5 | 6 | async run() { 7 | const supportedClients = [ 8 | 'claude', 9 | // 'cursor' 10 | ] 11 | 12 | for (const client of supportedClients) { 13 | this.log(`${client}`) 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {includeIgnoreFile} from '@eslint/compat' 2 | import oclif from 'eslint-config-oclif' 3 | import prettier from 'eslint-config-prettier' 4 | import path from 'node:path' 5 | import {fileURLToPath} from 'node:url' 6 | 7 | const gitignorePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '.gitignore') 8 | 9 | export default [includeIgnoreFile(gitignorePath), ...oclif, prettier] 10 | -------------------------------------------------------------------------------- /mcp-server-node.mjs: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' 3 | import { z } from 'zod' 4 | 5 | const server = new McpServer({ name: 'EchoNode', version: '1.0.0' }) 6 | 7 | server.tool('echo', { message: z.string() }, async ({ message }) => ({ 8 | content: [{ text: message, type: 'text' }] 9 | })) 10 | 11 | const transport = new StdioServerTransport() 12 | await server.connect(transport) 13 | -------------------------------------------------------------------------------- /.github/workflows/lint-workflows.yml: -------------------------------------------------------------------------------- 1 | name: actionlint 2 | 3 | on: 4 | # run only when workflow files change 5 | pull_request: 6 | paths: ['.github/workflows/**'] 7 | push: 8 | branches: [main] 9 | paths: ['.github/workflows/**'] 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | # installs the actionlint binary and scans .github/workflows 18 | - name: Lint GitHub Actions 19 | uses: eifinger/actionlint-action@v1 20 | # with: 21 | # version: "v1.7.7" # ⇐ uncomment to pin an exact linter version 22 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Attach", 8 | "port": 9229, 9 | "skipFiles": ["/**"] 10 | }, 11 | { 12 | "type": "node", 13 | "request": "launch", 14 | "name": "Execute Command", 15 | "skipFiles": ["/**"], 16 | "runtimeExecutable": "node", 17 | "runtimeArgs": ["--loader", "ts-node/esm", "--no-warnings=ExperimentalWarning"], 18 | "program": "${workspaceFolder}/bin/dev.js", 19 | "args": ["hello", "world"] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/onRelease.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | release: 7 | types: [published] 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: latest 20 | - run: npm install 21 | - run: npm run build 22 | - run: npm run prepack 23 | - uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c 24 | with: 25 | token: ${{ secrets.NPM_TOKEN }} 26 | - run: npm run postpack 27 | -------------------------------------------------------------------------------- /src/utils/spawn.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | export function computeChildProcess(stringArgs: string[]): { 4 | childArgs: string[] 5 | childCommand: string 6 | } { 7 | const target = stringArgs[0] 8 | if (target.startsWith('@')) { 9 | const childCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx' 10 | const childArgs = ['-y', ...stringArgs] 11 | return {childArgs, childCommand} 12 | } 13 | 14 | const ext = path.extname(target) 15 | 16 | if (ext === '.mjs' || ext === '.js' || ext === '.cjs') { 17 | const childCommand = process.platform === 'win32' ? 'node.exe' : 'node' 18 | const childArgs = stringArgs 19 | return {childArgs, childCommand} 20 | } 21 | 22 | if (ext === '.py') { 23 | const childCommand = process.platform === 'win32' ? 'python' : 'python3' 24 | const childArgs = stringArgs 25 | return {childArgs, childCommand} 26 | } 27 | 28 | const childCommand = process.platform === 'win32' ? 'uvx.cmd' : 'uvx' 29 | const childArgs = stringArgs 30 | return {childArgs, childCommand} 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Gavin Uhma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/integration.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {spawnSync} from 'node:child_process' 3 | 4 | function runCli(args: string[]) { 5 | return spawnSync(process.execPath, ['--loader','ts-node/esm','--disable-warning=ExperimentalWarning', './bin/dev.js', ...args], {encoding: 'utf8', timeout: 15_000}) 6 | } 7 | 8 | describe('integration', () => { 9 | it('lists tools for python server', () => { 10 | const res = runCli(['tools', './mcp-server.py']) 11 | expect(res.status).to.equal(0) 12 | expect(res.stdout).to.contain('echo') 13 | }) 14 | 15 | it('calls tool on python server', () => { 16 | const res = runCli(['tool', './mcp-server.py', 'echo', 'message=hi']) 17 | expect(res.status).to.equal(0) 18 | expect(res.stdout).to.contain('hi') 19 | }) 20 | 21 | it('lists tools for node server', () => { 22 | const res = runCli(['tools', './mcp-server-node.mjs']) 23 | expect(res.status).to.equal(0) 24 | expect(res.stdout).to.contain('echo') 25 | }) 26 | 27 | it('calls tool on node server', () => { 28 | const res = runCli(['tool', './mcp-server-node.mjs', 'echo', 'message=hi']) 29 | expect(res.status).to.equal(0) 30 | expect(res.stdout).to.contain('hi') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/commands/servers.ts: -------------------------------------------------------------------------------- 1 | import {Command} from '@oclif/core' 2 | import {colorize, stdout} from '@oclif/core/ux' 3 | import { promises as fs } from 'node:fs'; 4 | import { dirname, join } from 'node:path'; 5 | import { fileURLToPath } from 'node:url'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | export default class Servers extends Command { 11 | static description = 'List servers' 12 | 13 | async run() { 14 | const filePath = join(__dirname, '..', '..', 'mcp-servers.json'); 15 | 16 | try { 17 | // Read the file asynchronously 18 | const fileData = await fs.readFile(filePath, 'utf8'); 19 | // Parse the JSON content into an array of servers 20 | const servers = JSON.parse(fileData) as Array<{description: string; name: string}>; 21 | 22 | // Print each server with spacing and colorize the server name 23 | for (const server of servers) { 24 | stdout( 25 | colorize('cyan', server.name.padEnd(50)) + 26 | ' ' + 27 | server.description 28 | ); 29 | } 30 | } catch (error: unknown) { 31 | if (error instanceof Error) { 32 | this.error(`Error reading servers file: ${error.message}`); 33 | } else { 34 | this.error('Error reading servers file'); 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /test/run.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | 3 | import {computeChildProcess} from '../src/utils/spawn.js' 4 | 5 | describe('run command', () => { 6 | it('uses node for .mjs files', () => { 7 | const {childArgs, childCommand} = computeChildProcess(['./server.mjs']) 8 | expect(childCommand).to.equal(process.platform === 'win32' ? 'node.exe' : 'node') 9 | expect(childArgs).to.deep.equal(['./server.mjs']) 10 | }) 11 | 12 | it('uses python for .py files', () => { 13 | const {childArgs, childCommand} = computeChildProcess(['./server.py']) 14 | expect(childCommand).to.equal(process.platform === 'win32' ? 'python' : 'python3') 15 | expect(childArgs).to.deep.equal(['./server.py']) 16 | }) 17 | 18 | it('uses npx for package names', () => { 19 | const {childArgs, childCommand} = computeChildProcess(['@scope/pkg']) 20 | expect(childCommand).to.equal(process.platform === 'win32' ? 'npx.cmd' : 'npx') 21 | expect(childArgs).to.deep.equal(['-y', '@scope/pkg']) 22 | }) 23 | 24 | it('uses node for .js files', () => { 25 | const {childArgs, childCommand} = computeChildProcess(['./server.js']) 26 | expect(childCommand).to.equal(process.platform === 'win32' ? 'node.exe' : 'node') 27 | expect(childArgs).to.deep.equal(['./server.js']) 28 | }) 29 | 30 | it('falls back to uvx for other files', () => { 31 | const {childArgs, childCommand} = computeChildProcess(['./server']) 32 | expect(childCommand).to.equal(process.platform === 'win32' ? 'uvx.cmd' : 'uvx') 33 | expect(childArgs).to.deep.equal(['./server']) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /mcp-server.py: -------------------------------------------------------------------------------- 1 | import sys, json 2 | 3 | echo_tool = { 4 | "name": "echo", 5 | "description": "Echo back a message", 6 | "inputSchema": { 7 | "type": "object", 8 | "properties": {"message": {"type": "string"}}, 9 | "required": ["message"], 10 | "additionalProperties": False, 11 | }, 12 | } 13 | 14 | for line in sys.stdin: 15 | line = line.strip() 16 | if not line: 17 | continue 18 | try: 19 | msg = json.loads(line) 20 | except json.JSONDecodeError: 21 | continue 22 | method = msg.get("method") 23 | if method == "initialize": 24 | resp = { 25 | "jsonrpc": "2.0", 26 | "id": msg.get("id"), 27 | "result": { 28 | "protocolVersion": msg.get("params", {}).get("protocolVersion"), 29 | "capabilities": {"tools": {}}, 30 | "serverInfo": {"name": "EchoPy", "version": "1.0.0"}, 31 | }, 32 | } 33 | sys.stdout.write(json.dumps(resp) + "\n") 34 | sys.stdout.flush() 35 | elif method == "tools/list": 36 | resp = {"jsonrpc": "2.0", "id": msg.get("id"), "result": {"tools": [echo_tool]}} 37 | sys.stdout.write(json.dumps(resp) + "\n") 38 | sys.stdout.flush() 39 | elif method == "tools/call": 40 | args = msg.get("params", {}).get("arguments", {}) 41 | text = args.get("message", "") 42 | resp = {"jsonrpc": "2.0", "id": msg.get("id"), "result": {"content": [{"type": "text", "text": text}]}} 43 | sys.stdout.write(json.dumps(resp) + "\n") 44 | sys.stdout.flush() 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcpgod", 3 | "description": "add, remove, run, and inspect mcp servers", 4 | "version": "0.1.1", 5 | "author": "Gavin Uhma", 6 | "bin": { 7 | "mcpgod": "./bin/run.js" 8 | }, 9 | "bugs": "https://github.com/mcpgod/cli/issues", 10 | "dependencies": { 11 | "@modelcontextprotocol/sdk": "^1.6.1", 12 | "@oclif/core": "^4", 13 | "strip-ansi": "^7", 14 | "winston": "^3.17.0", 15 | "zod": "^3" 16 | }, 17 | "devDependencies": { 18 | "@eslint/compat": "^1", 19 | "@oclif/prettier-config": "^0.2.1", 20 | "@oclif/test": "^4", 21 | "@types/chai": "^4", 22 | "@types/mocha": "^10", 23 | "@types/node": "^18", 24 | "c8": "^10.1.3", 25 | "chai": "^4", 26 | "eslint": "^9", 27 | "eslint-config-oclif": "^6", 28 | "eslint-config-prettier": "^10", 29 | "mocha": "^10", 30 | "oclif": "^4", 31 | "shx": "^0.3.3", 32 | "ts-node": "^10", 33 | "typescript": "^5", 34 | "node-actionlint": "^1.2.2" 35 | }, 36 | "engines": { 37 | "node": ">=18.0.0" 38 | }, 39 | "files": [ 40 | "./bin", 41 | "./dist", 42 | "./oclif.manifest.json", 43 | "./mcp-servers.json" 44 | ], 45 | "homepage": "https://github.com/mcpgod/cli", 46 | "keywords": [ 47 | "oclif" 48 | ], 49 | "license": "MIT", 50 | "main": "dist/index.js", 51 | "type": "module", 52 | "oclif": { 53 | "bin": "mcpgod", 54 | "dirname": "mcpgod", 55 | "commands": "./dist/commands" 56 | }, 57 | "repository": "mcpgod/cli", 58 | "scripts": { 59 | "build": "shx rm -rf dist && tsc -b", 60 | "lint": "eslint", 61 | "postpack": "shx rm -f oclif.manifest.json", 62 | "posttest": "npm run lint || true", 63 | "prepack": "oclif manifest && oclif readme", 64 | "test": "mocha --forbid-only \"test/**/*.test.ts\"", 65 | "coverage": "c8 --reporter=lcov --reporter=text npm test", 66 | "version": "oclif readme && git add README.md", 67 | "lint:workflows": "node-actionlint '.github/workflows/**/*.yml'" 68 | }, 69 | "types": "dist/index.d.ts" 70 | } 71 | -------------------------------------------------------------------------------- /src/commands/remove.ts: -------------------------------------------------------------------------------- 1 | import {Args, Command, Flags} from '@oclif/core' 2 | import * as fs from 'node:fs' 3 | import path from 'node:path' 4 | 5 | export default class Remove extends Command { 6 | static args = { 7 | mcpServer: Args.string({description: 'MCP server to remove', required: true}), 8 | } 9 | static description = 'Remove a server from a client' 10 | static flags = { 11 | client: Flags.string({char: 'c', description: 'client name', required: true}), 12 | } 13 | 14 | async run() { 15 | const {args, flags} = await this.parse(Remove) 16 | const {mcpServer} = args 17 | const {client} = flags 18 | 19 | // Determine the configuration file path based on the OS 20 | const configFilePath = process.platform === 'win32' 21 | ? path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json') 22 | : path.join(process.env.HOME || '', 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') 23 | 24 | // Load the configuration file 25 | let config 26 | try { 27 | const configFile = fs.readFileSync(configFilePath, 'utf8') 28 | config = JSON.parse(configFile) 29 | } catch (error: unknown) { 30 | if (error instanceof Error) { 31 | this.error(`Failed to read or parse configuration file: ${error.message}`) 32 | } else { 33 | this.error('Failed to read or parse configuration file') 34 | } 35 | 36 | return 37 | } 38 | 39 | // Check if the server exists 40 | if (!config.mcpServers[mcpServer]) { 41 | this.log(`Server ${mcpServer} does not exist in the configuration for client ${client}`) 42 | return 43 | } 44 | 45 | // Remove the mcp-server 46 | delete config.mcpServers[mcpServer] 47 | 48 | // Write the updated configuration back to the file 49 | try { 50 | fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2)) 51 | this.log(`Successfully removed ${mcpServer} from the configuration for client ${client}`) 52 | } catch (error: unknown) { 53 | if (error instanceof Error) { 54 | this.error(`Failed to write to configuration file: ${error.message}`) 55 | } else { 56 | this.error('Failed to write to configuration file') 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/commands/list.ts: -------------------------------------------------------------------------------- 1 | import {Command, Flags} from '@oclif/core' 2 | import {colorize, stdout} from '@oclif/core/ux' 3 | import * as fs from 'node:fs' 4 | import path from 'node:path' 5 | 6 | interface ServerDetails { 7 | args: string[]; 8 | command: string; 9 | } 10 | 11 | interface Config { 12 | mcpServers?: Record; 13 | } 14 | 15 | export default class List extends Command { 16 | static description = 'List all the servers for a client' 17 | static flags = { 18 | client: Flags.string({char: 'c', description: 'client name', required: true}), 19 | } 20 | 21 | async run() { 22 | const {flags} = await this.parse(List) 23 | const {client} = flags 24 | 25 | // Determine the configuration file path based on the OS 26 | const configFilePath = (() => { 27 | switch (client) { 28 | case 'claude': { 29 | return process.platform === 'win32' 30 | ? path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json') 31 | : path.join(process.env.HOME || '', 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'); 32 | } 33 | 34 | default: { 35 | this.log(`Unknown client ${client}`); 36 | return ''; 37 | } 38 | } 39 | })(); 40 | 41 | if (configFilePath === '') { 42 | return 43 | } 44 | 45 | // Load the configuration file 46 | let config: Config 47 | try { 48 | const configFile = fs.readFileSync(configFilePath, 'utf8') 49 | config = JSON.parse(configFile) 50 | } catch (error: unknown) { 51 | if (error instanceof Error) { 52 | this.error(`Failed to read or parse configuration file: ${error.message}`) 53 | } else { 54 | this.error('Failed to read or parse configuration file') 55 | } 56 | 57 | return 58 | } 59 | 60 | // List all servers for the specified client 61 | const servers = config.mcpServers || {} 62 | if (Object.keys(servers).length === 0) { 63 | this.log(`No servers found for client ${client}`) 64 | } else { 65 | for (const [serverName, serverDetails] of Object.entries(servers)) { 66 | const commandString = `${serverDetails.command} ${serverDetails.args.join(' ')}`; 67 | 68 | stdout(colorize('magenta', serverName) + `: ${commandString}`); 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/commands/add.ts: -------------------------------------------------------------------------------- 1 | import {Args, Command, Flags} from '@oclif/core' 2 | import * as fs from 'node:fs' 3 | import path from 'node:path' 4 | 5 | export default class Add extends Command { 6 | static args = { 7 | server: Args.string({description: 'The mcp server to add', required: true}), 8 | } 9 | static description = 'Add a server to a client' 10 | static flags = { 11 | client: Flags.string({char: 'c', description: 'Client name to add the server to', required: true}), 12 | tools: Flags.string({ 13 | char: 't', 14 | description: 'Comma separated list of approved tools' 15 | }) 16 | } 17 | 18 | async run() { 19 | const {args, flags} = await this.parse(Add) 20 | const {server} = args 21 | const {client} = flags 22 | 23 | let approvedTools = ''; 24 | 25 | if (flags.tools) { 26 | approvedTools += '--tools ' + flags.tools 27 | } 28 | 29 | // Determine the configuration file path based on the OS 30 | const configFilePath = process.platform === 'win32' 31 | ? path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json') 32 | : path.join(process.env.HOME || '', 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') 33 | 34 | // Load the configuration file 35 | let config 36 | try { 37 | const configFile = fs.readFileSync(configFilePath, 'utf8') 38 | config = JSON.parse(configFile) 39 | } catch (error: unknown) { 40 | if (error instanceof Error) { 41 | this.error(`Failed to read or parse configuration file: ${error.message}`) 42 | } else { 43 | this.error('Failed to read or parse configuration file') 44 | } 45 | 46 | return 47 | } 48 | 49 | // Check if the server already exists 50 | if (config.mcpServers[server]) { 51 | this.log(`Server ${server} already exists in the configuration for client ${client}. Remove and add again to make a change.`) 52 | return 53 | } 54 | 55 | // Add the new mcp-server 56 | config.mcpServers[server] = { 57 | args: [ 58 | "-y", 59 | "mcpgod", 60 | "run", 61 | server, 62 | approvedTools 63 | ], 64 | command: 'npx' 65 | } 66 | 67 | // Write the updated configuration back to the file 68 | try { 69 | fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2)) 70 | this.log(`Successfully added ${server} to the configuration for client ${client}`) 71 | } catch (error: unknown) { 72 | if (error instanceof Error) { 73 | this.error(`Failed to write to configuration file: ${error.message}`) 74 | } else { 75 | this.error('Failed to write to configuration file') 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/commands/tool.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js" 2 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" 3 | import { Args, Command, Flags } from '@oclif/core' 4 | 5 | import { computeChildProcess } from '../utils/spawn.js' 6 | 7 | // Define the expected shape for parsed arguments. 8 | 9 | export default class Tool extends Command { 10 | // Arguments must be declared in the order they are expected on the 11 | // command line. Required arguments cannot come after optional ones, 12 | // otherwise oclif will throw an "Invalid argument spec" error. 13 | /* eslint-disable perfectionist/sort-objects */ 14 | static args = { 15 | server: Args.string({ description: 'the MCP server', required: true }), 16 | tool: Args.string({ description: 'the tool to call', required: true }), 17 | properties: Args.string({ description: 'Tool properties as key=value pairs', multiple: true }), 18 | } 19 | /* eslint-enable perfectionist/sort-objects */ 20 | static description = 'Call a tool on a server' 21 | static flags = { 22 | json: Flags.boolean({ char: 'j', description: 'Output result in JSON format' }) 23 | } 24 | static strict = false 25 | 26 | async run() { 27 | const { argv, flags } = await this.parse(Tool) 28 | const [server, tool, ...properties] = argv as string[]; 29 | 30 | // console.log('server', server) 31 | // console.log('tool', tool) 32 | // console.log('properties', properties) 33 | 34 | const propsObject: Record = Object.fromEntries( 35 | properties 36 | .map((prop) => { 37 | const [key, ...valueParts] = prop.split('='); 38 | if (!key || valueParts.length === 0) { 39 | this.warn(`Skipping property "${prop}" (expected key=value)`); 40 | return; 41 | } 42 | 43 | const valueStr = valueParts.join('='); 44 | const num = Number(valueStr); 45 | // Only convert to a number if the trimmed value is non-empty and a valid number. 46 | const value = valueStr.trim() !== '' && !Number.isNaN(num) ? num : valueStr; 47 | return [key, value] as [string, number | string]; 48 | }) 49 | .filter((entry): entry is [string, number | string] => entry !== undefined) 50 | ); 51 | 52 | // console.log(propsObject); 53 | 54 | const { childArgs: args, childCommand: command } = computeChildProcess([server]) 55 | 56 | const transport = new StdioClientTransport({ 57 | args, 58 | command 59 | }); 60 | 61 | const client = new Client( 62 | { name: 'mcpgod', version: '1.0.0' }, 63 | { capabilities: { prompts: {}, resources: {}, tools: {} } } 64 | ) 65 | 66 | try { 67 | await client.connect(transport) 68 | } catch (error) { 69 | const {code} = (error as NodeJS.ErrnoException) 70 | if (code === 'ENOENT') { 71 | this.error(`${command} not found. Please install it and try again.`) 72 | } 73 | 74 | throw error 75 | } 76 | 77 | const result = await client.callTool({ 78 | arguments: propsObject, 79 | name: tool, 80 | }) 81 | 82 | if (flags.json) { 83 | console.log(JSON.stringify(result, null, 2) + '\n') 84 | } else { 85 | console.log('Tool call result:', result) 86 | } 87 | 88 | await client.close() 89 | 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/commands/tools.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 3 | import {Args, Command} from '@oclif/core' 4 | import {colorize, stdout} from '@oclif/core/ux' 5 | 6 | import { computeChildProcess } from '../utils/spawn.js' 7 | 8 | function printTool(tool: { description?: string; inputSchema: unknown; name: string; }): void { 9 | stdout( 10 | colorize('cyan', tool.name.padEnd(5)) + 11 | ' ' + 12 | (tool.description ?? '') 13 | ) 14 | 15 | if (tool.inputSchema && typeof tool.inputSchema === 'object') { 16 | const schema = tool.inputSchema as { 17 | properties?: Record 18 | required?: string[] 19 | } 20 | const requiredProps: string[] = Array.isArray(schema.required) ? schema.required : [] 21 | const properties = schema.properties || {} 22 | 23 | if (Object.keys(properties).length === 0) { 24 | console.log(' (no properties)') 25 | } else { 26 | for (const [propName, propDef] of Object.entries(properties)) { 27 | const def = propDef as { description?: string; type: string } 28 | const requiredTag = requiredProps.includes(propName) ? ', required' : '' 29 | stdout( 30 | ' - ' + colorize('magenta', propName) + ` (${def.type}${requiredTag})` + 31 | (def.description === undefined ? '' : ` - ${def.description}`) 32 | ) 33 | } 34 | } 35 | } else { 36 | console.log(' (no input schema)') 37 | } 38 | 39 | console.log('') 40 | } 41 | 42 | export default class Tools extends Command { 43 | static args = { 44 | server: Args.string({description: 'mcp server', required: true}), 45 | } 46 | static description = 'List the tools for a server' 47 | static strict = false 48 | 49 | async run() { 50 | // const {args} = await this.parse(Tools) 51 | // const server = args.server 52 | 53 | const { argv } = await this.parse(Tools) 54 | if (argv.length === 0) { 55 | this.error('Please specify a package to run') 56 | } 57 | 58 | // Assert that argv is a string array. 59 | const stringArgs = argv as string[] 60 | 61 | const { childArgs: args, childCommand: command } = computeChildProcess(stringArgs) 62 | 63 | const transport = new StdioClientTransport({ 64 | args, 65 | command 66 | }); 67 | 68 | const client = new Client( 69 | { 70 | name: "mcpgod", 71 | version: "1.0.0" 72 | }, 73 | { 74 | capabilities: { 75 | prompts: {}, 76 | resources: {}, 77 | tools: {} 78 | } 79 | } 80 | ); 81 | 82 | try { 83 | await client.connect(transport); 84 | } catch (error) { 85 | const {code} = (error as NodeJS.ErrnoException); 86 | if (code === 'ENOENT') { 87 | this.error(`${command} not found. Please install it and try again.`); 88 | } 89 | 90 | throw error; 91 | } 92 | 93 | const res = await client.listTools(); 94 | 95 | // Assuming res.tools is your array of tools 96 | const toolsArray = res.tools as { description?: string; inputSchema: unknown; name: string; }[]; 97 | for (const tool of toolsArray) { 98 | printTool(tool) 99 | } 100 | 101 | await client.close() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /.github/workflows/onPushToMain.yml: -------------------------------------------------------------------------------- 1 | # test 2 | name: version, tag and github release 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: latest 19 | - name: Check if version already exists 20 | id: version-check 21 | shell: bash 22 | run: | 23 | package_name="$(node -p "require('./package.json').name")" 24 | package_version="$(node -p "require('./package.json').version")" 25 | 26 | echo "tag=v${package_version}" >>"${GITHUB_OUTPUT}" 27 | 28 | gh_exists="$(gh api \ 29 | "repos/${{ github.repository }}/releases/tags/v${package_version}" \ 30 | >/dev/null 2>&1 && echo 'true' || echo '')" 31 | 32 | npm_exists="$(npm view "${package_name}@${package_version}" --json \ 33 | >/dev/null 2>&1 && echo 'true' || echo '')" 34 | 35 | if [[ -n "$gh_exists" || -n "$npm_exists" ]]; then 36 | echo "::warning file=package.json,line=1::Version v${package_version} already exists—skipping release." 37 | echo "skipped=true" >>"${GITHUB_OUTPUT}" 38 | else 39 | echo "skipped=false" >>"${GITHUB_OUTPUT}" 40 | fi 41 | env: 42 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | - name: Setup git 44 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 45 | run: | 46 | git config --global user.email ${{ secrets.GH_EMAIL }} 47 | git config --global user.name ${{ secrets.GH_USERNAME }} 48 | - name: Generate oclif README 49 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 50 | id: oclif-readme 51 | run: | 52 | npm install 53 | npm exec oclif readme 54 | if [ -n "$(git status --porcelain)" ]; then 55 | git add . 56 | git commit -am "chore: update README.md" 57 | git push -u origin ${{ github.ref_name }} 58 | fi 59 | - name: Create Github Release 60 | uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 61 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 62 | with: 63 | name: ${{ steps.version-check.outputs.tag }} 64 | tag: ${{ steps.version-check.outputs.tag }} 65 | commit: ${{ github.ref_name }} 66 | token: ${{ secrets.GITHUB_TOKEN }} 67 | skipIfReleaseExists: true 68 | - name: Install dependencies 69 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 70 | run: npm install 71 | - name: Build 72 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 73 | run: npm run build 74 | - name: Prepare package 75 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 76 | run: npm run prepack 77 | - name: Publish to npm 78 | uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c 79 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 80 | with: 81 | token: ${{ secrets.NPM_TOKEN }} 82 | - name: Cleanup package 83 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 84 | run: npm run postpack 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCPGod 2 | > Fine-grained control over model context protocol (MCP) clients, servers, and tools. Context is God. 3 | 4 | [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) 5 | [![Version](https://img.shields.io/npm/v/mcpgod.svg)](https://npmjs.org/package/mcpgod) 6 | [![Downloads/week](https://img.shields.io/npm/dw/mcpgod.svg)](https://npmjs.org/package/mcpgod) 7 | [![License](https://img.shields.io/npm/l/mcpgod.svg)](LICENSE) 8 | [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](coverage/lcov-report/index.html) 9 | 10 | ## Overview 11 | 12 | **MCPGod** is a CLI tool designed to help developers manage MCP servers with speed and ease. Whether you need to add, run, list, or remove servers—or even interact with server tools—**MCPGod** provides a streamlined interface to handle all these tasks on Windows, macOS, or Linux. 13 | 14 | ## Features 15 | 16 | - **Client Management** 17 | Add, remove, and list MCP servers for specific clients. 18 | - **Tool Discovery** 19 | List every tool on any MCP server. 20 | - **Tool Calling** 21 | Run any tool on any MCP server directly from the command line. 22 | - **Tool/Client Permissions** 23 | Allow or block specific tools for specific clients. 24 | - **Detailed Logging** 25 | Log every server run from every client, with timestamps and clean output for easy debugging. 26 | 27 | ## Installation 28 | 29 | Install **mcpgod** globally using `npm`: 30 | 31 | ```sh 32 | npm install -g mcpgod 33 | ``` 34 | 35 | Verify the installation: 36 | 37 | ```sh 38 | mcpgod --version 39 | ``` 40 | 41 | Or run directly with `npx`. 42 | 43 | ```sh 44 | npx -y mcpgod 45 | ``` 46 | 47 | ## Usage 48 | 49 | Access the CLI with the `mcpgod` command (or `npx -y mcpgod`). Below are some common examples: 50 | 51 | - **Add a Server to a Client** 52 | 53 | Add an MCP server to a client (e.g., Claude) with `mcpgod add -c `: 54 | 55 | ```sh 56 | mcpgod add @modelcontextprotocol/server-everything -c claude 57 | ``` 58 | 59 | - **Only Add Specific Tools to a Client** 60 | 61 | Only add specific tools to a client with `mcpgod add -c --tools=`: 62 | 63 | ```sh 64 | mcpgod add @modelcontextprotocol/server-everything -c claude --tools=echo,add 65 | ``` 66 | 67 | - **List Servers for a Client** 68 | 69 | List all configured servers for a specific client with `mcpgod list -c `: 70 | 71 | ```sh 72 | mcpgod list -c claude 73 | ``` 74 | 75 | - **Remove a Server** 76 | 77 | Remove an MCP server from your client's configuration with `mcpgod remove -c `: 78 | 79 | ```sh 80 | mcpgod remove @modelcontextprotocol/server-everything -c claude 81 | ``` 82 | 83 | - **Run a Server** 84 | 85 | Run a server process with detailed logging with `mcpgod run `: 86 | 87 | ```sh 88 | mcpgod run @modelcontextprotocol/server-everything 89 | mcpgod run ./mcp-server.py 90 | mcpgod run ./mcp-server-node.mjs 91 | ``` 92 | 93 | - **List Available Tools for a Server** 94 | 95 | Display the list of tools available on a server with `mcpgod tools `: 96 | 97 | ```sh 98 | mcpgod tools @modelcontextprotocol/server-everything 99 | mcpgod tools ./mcp-server.py 100 | mcpgod tools ./mcp-server-node.mjs 101 | ``` 102 | 103 | - **Call a Specific Tool on a Server** 104 | 105 | Interact with a tool by passing key-value properties with `mcpgod tool [optional parameters]`: 106 | 107 | ```sh 108 | mcpgod tool @modelcontextprotocol/server-everything add a=59 b=40 109 | mcpgod tool ./mcp-server.py echo message=hi 110 | mcpgod tool ./mcp-server-node.mjs echo message=hi 111 | ``` 112 | 113 | For a complete list of commands and options, simply run: 114 | 115 | ```sh 116 | mcpgod --help 117 | ``` 118 | 119 | 120 | ## Logging 121 | 122 | When running a server, **mcpgod** logs output to: 123 | 124 | ```plaintext 125 | ~/mcpgod/logs 126 | ``` 127 | 128 | Each log file is organized by server name and timestamped to help you trace and debug any issues that arise. 129 | 130 | ## Development 131 | 132 | **mcpgod** is built with the [Oclif](https://oclif.io) framework and uses the [Model Context Protocol SDK](https://modelcontextprotocol.org) for robust interactions with MCP servers. 133 | 134 | Clone the repository to get started with development: 135 | 136 | ```sh 137 | git clone https://github.com/mcpgod/cli.git 138 | cd mcpgod 139 | npm install 140 | ``` 141 | 142 | Run the CLI in development mode: 143 | 144 | ```sh 145 | ./bin/dev 146 | ``` 147 | 148 | ## Publishing 149 | 150 | Automatic publishing is handled by the `version, tag and github release` workflow. A new npm version is published whenever a version bump is merged into the `main` branch. The workflow can also be triggered manually from the **Actions** tab. 151 | 152 | To enable publishing, set the following repository secrets: 153 | 154 | - `NPM_TOKEN` – authentication token for npm. 155 | - `GH_EMAIL` and `GH_USERNAME` – used by the README update step. 156 | 157 | Once the secrets are configured, pushing a new version or running the workflow manually will build the project, create a GitHub release and publish the package to npm. 158 | 159 | ### Linting workflows 160 | 161 | Before committing changes to workflows, you can lint them locally: 162 | 163 | ```sh 164 | npm run lint:workflows 165 | ``` 166 | 167 | ## Contributing 168 | 169 | Contributions are always welcome! To contribute: 170 | 171 | 1. **Fork** the repository. 172 | 2. **Create a branch**: 173 | ```sh 174 | git checkout -b feature/your-feature 175 | ``` 176 | 3. **Make your changes**, and commit them: 177 | ```sh 178 | git commit -am 'Add new feature' 179 | ``` 180 | 4. **Push** your branch: 181 | ```sh 182 | git push origin feature/your-feature 183 | ``` 184 | 5. **Open a Pull Request** on GitHub. 185 | 186 | ## License 187 | 188 | This project is licensed under the [MIT License](LICENSE). 189 | 190 | --- 191 | 192 | ## Additional Resources 193 | 194 | - [Oclif CLI Framework](https://oclif.io) 195 | - [Model Context Protocol](https://modelcontextprotocol.org) 196 | - [npm Package mcpgod](https://npmjs.org/package/mcpgod) 197 | 198 | --- 199 | -------------------------------------------------------------------------------- /src/commands/run.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags } from '@oclif/core' 2 | import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process' 3 | import * as fs from 'node:fs' 4 | import path from 'node:path' 5 | import stripAnsi from 'strip-ansi' 6 | import * as winston from 'winston' 7 | 8 | import { computeChildProcess } from '../utils/spawn.js' 9 | 10 | // Helper: remove non-printable control characters except newline (\n), 11 | // carriage return (\r), and tab (\t). 12 | function removeControlChars(input: string): string { 13 | // eslint-disable-next-line no-control-regex 14 | return input.replaceAll(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '') 15 | } 16 | 17 | function sanitizeFilename(str: string): string { 18 | // Replace invalid characters with an underscore 19 | return str.replaceAll(/[/?%*:|"<>\\]/g, '_') 20 | } 21 | 22 | 23 | export default class Run extends Command { 24 | static description = 'Run a server' 25 | static examples = [ 26 | `<%= config.bin %> <%= command.id %> @user/package-name -x conf.json` 27 | ] 28 | static flags = { 29 | tools: Flags.string({ 30 | char: 't', 31 | default: '', 32 | description: 'Comma separated list of approved tools' 33 | }) 34 | }; 35 | static strict = false // Allow variable arguments 36 | 37 | async run(): Promise { 38 | const { argv, flags } = await this.parse(Run) 39 | if (argv.length === 0) { 40 | this.error('Please specify a package to run') 41 | } 42 | 43 | // Assert that argv is a string array. 44 | const stringArgs = argv as string[] 45 | 46 | const approvedTools = new Set(flags.tools.split(',').map(tool => tool.trim())); 47 | 48 | // Determine home directory for cross-platform compatibility. 49 | const homeDir = process.env.HOME || process.env.USERPROFILE; 50 | if (!homeDir) { 51 | throw new Error('Unable to determine home directory.'); 52 | } 53 | 54 | // Set the static log directory path: $HOME/mcpgod/logs 55 | const logsDir = path.join(homeDir, 'mcpgod', 'logs'); 56 | if (!fs.existsSync(logsDir)) { 57 | fs.mkdirSync(logsDir, { recursive: true }); // Ensure nested directories are created. 58 | } 59 | 60 | // Create log file with timestamp. 61 | const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-') 62 | const filename = sanitizeFilename(`run--${stringArgs.join(' ')}--${timestamp}.log`); 63 | const logFile = path.join(logsDir, filename) 64 | 65 | // Setup Winston logger. 66 | const logger = winston.createLogger({ 67 | format: winston.format.combine( 68 | winston.format.timestamp(), 69 | winston.format.printf(({ level, message, timestamp }) => `${timestamp} [${level.toUpperCase()}] ${message}`) 70 | ), 71 | level: 'info', 72 | transports: [new winston.transports.File({ filename: logFile })] 73 | }) 74 | 75 | logger.info('') 76 | logger.info(`Command: ${stringArgs.join(' ')}`) 77 | logger.info(`Started at: ${new Date().toISOString()}`) 78 | logger.info('') 79 | 80 | // Filter process.env so that all values are strings. 81 | const filteredEnv: {[key: string]: string} = {} 82 | for (const key of Object.keys(process.env)) { 83 | filteredEnv[key] = process.env[key] || '' 84 | } 85 | 86 | if (!filteredEnv.TERM) { 87 | filteredEnv.TERM = 'xterm-color' 88 | } 89 | 90 | function handleOutput(data: string, stream: NodeJS.WritableStream) { 91 | try { 92 | const parsed = JSON.parse(data); 93 | // Check if parsed JSON has the expected result.tools structure before filtering 94 | if (parsed && parsed.result && Array.isArray(parsed.result.tools)) { 95 | parsed.result.tools = parsed.result.tools.filter( 96 | (tool: { name: string }) => approvedTools.has(tool.name) 97 | ); 98 | const jsonString = JSON.stringify(parsed); 99 | // Apparently claude desktop expects a newline at the end. 100 | data = jsonString + "\n"; 101 | } 102 | } catch { 103 | // If JSON parsing fails, fall back to treating the data as plain text. 104 | } 105 | 106 | stream.write(data); 107 | const cleaned = removeControlChars(stripAnsi(data)); 108 | logger.info(cleaned); 109 | } 110 | 111 | // Helper to forward stdin to the child process. 112 | // writeFn should be a function accepting a string. 113 | function setupStdinForward(writeFn: (data: string) => void) { 114 | process.stdin.on('data', (data: Buffer) => { 115 | const str = data.toString() 116 | logger.info(`[stdin] ${str}`) 117 | writeFn(str) 118 | }) 119 | process.stdin.resume() 120 | } 121 | 122 | // Non-interactive mode using spawn. 123 | const { childArgs, childCommand } = computeChildProcess(stringArgs) 124 | const shell = true; // process.stdout.isTTY ? true : false; 125 | 126 | logger.info(`Spawn: ${childCommand} ${childArgs.join(' ')}`) 127 | logger.info(`Shell: ${shell}`) 128 | logger.info('') 129 | 130 | const child = spawn(childCommand, childArgs, { 131 | cwd: process.cwd(), 132 | env: filteredEnv, 133 | shell, 134 | stdio: ['pipe', 'pipe', 'pipe'] 135 | }) as ChildProcessWithoutNullStreams 136 | 137 | child.on('error', (err: NodeJS.ErrnoException) => { 138 | logger.error(`Failed to spawn ${childCommand}: ${err.message}`) 139 | if (err.code === 'ENOENT') { 140 | this.error(`${childCommand} not found. Please install it and try again.`) 141 | } else { 142 | this.error(`Failed to spawn ${childCommand}: ${err.message}`) 143 | } 144 | }) 145 | 146 | child.stdout.on('data', (data: Buffer) => { 147 | handleOutput(data.toString(), process.stdout) 148 | }) 149 | child.stderr.on('data', (data: Buffer) => { 150 | handleOutput(data.toString(), process.stderr) 151 | }) 152 | setupStdinForward(child.stdin.write.bind(child.stdin)) 153 | 154 | return new Promise((resolve, reject) => { 155 | child.on('exit', (code: number) => { 156 | logger.info(`Process exited with code ${code} at ${new Date().toISOString()}`) 157 | if (code === 0) { 158 | resolve() 159 | } else { 160 | reject(new Error(`${childCommand} exited with code ${code}`)) 161 | } 162 | }) 163 | }) 164 | } 165 | } -------------------------------------------------------------------------------- /mcp-servers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "@modelcontextprotocol/server-brave-search", 4 | "description": "MCP server for Brave Search API integration", 5 | "runtime": "node" 6 | }, 7 | { 8 | "name": "@modelcontextprotocol/server-everything", 9 | "description": "MCP server that exercises all the features of the MCP protocol", 10 | "runtime": "node" 11 | }, 12 | { 13 | "name": "@modelcontextprotocol/server-filesystem", 14 | "description": "MCP server for filesystem access", 15 | "runtime": "node" 16 | }, 17 | { 18 | "name": "@modelcontextprotocol/server-gdrive", 19 | "description": "MCP server for interacting with Google Drive", 20 | "runtime": "node" 21 | }, 22 | { 23 | "name": "@modelcontextprotocol/server-github", 24 | "description": "MCP server for using the GitHub API", 25 | "runtime": "node" 26 | }, 27 | { 28 | "name": "@modelcontextprotocol/server-gitlab", 29 | "description": "MCP server for using the GitLab API", 30 | "runtime": "node" 31 | }, 32 | { 33 | "name": "@modelcontextprotocol/server-google-maps", 34 | "description": "MCP server for using the Google Maps API", 35 | "runtime": "node" 36 | }, 37 | { 38 | "name": "@modelcontextprotocol/server-memory", 39 | "description": "MCP server for enabling memory for Claude through a knowledge graph", 40 | "runtime": "node" 41 | }, 42 | { 43 | "name": "@modelcontextprotocol/server-postgres", 44 | "description": "MCP server for interacting with PostgreSQL databases", 45 | "runtime": "node" 46 | }, 47 | { 48 | "name": "@modelcontextprotocol/server-puppeteer", 49 | "description": "MCP server for browser automation using Puppeteer", 50 | "runtime": "node" 51 | }, 52 | { 53 | "name": "@modelcontextprotocol/server-slack", 54 | "description": "MCP server for interacting with Slack", 55 | "runtime": "node" 56 | }, 57 | { 58 | "name": "@cloudflare/mcp-server-cloudflare", 59 | "description": "MCP server for interacting with Cloudflare API", 60 | "runtime": "node" 61 | }, 62 | { 63 | "name": "@raygun.io/mcp-server-raygun", 64 | "description": "MCP server for interacting with Raygun's API for crash reporting and real user monitoring metrics", 65 | "runtime": "node" 66 | }, 67 | { 68 | "name": "@kimtaeyoon83/mcp-server-youtube-transcript", 69 | "description": "This is an MCP server that allows you to directly download transcripts of YouTube videos.", 70 | "runtime": "node" 71 | }, 72 | { 73 | "name": "@kagi/mcp-server-kagi", 74 | "description": "MCP server for Kagi search API integration", 75 | "runtime": "node" 76 | }, 77 | { 78 | "name": "@exa/mcp-server", 79 | "description": "MCP server for Exa AI Search API integration", 80 | "runtime": "node" 81 | }, 82 | { 83 | "name": "@search1api/mcp-server", 84 | "description": "MCP server for Search1API integration", 85 | "runtime": "node" 86 | }, 87 | { 88 | "name": "@calclavia/mcp-obsidian", 89 | "description": "MCP server for reading and searching Markdown notes (like Obsidian vaults)", 90 | "runtime": "node" 91 | }, 92 | { 93 | "name": "@anaisbetts/mcp-youtube", 94 | "description": "MCP server for fetching YouTube subtitles", 95 | "runtime": "node" 96 | }, 97 | { 98 | "name": "@modelcontextprotocol/server-everart", 99 | "description": "MCP server for EverArt API integration", 100 | "runtime": "node" 101 | }, 102 | { 103 | "name": "@modelcontextprotocol/server-sequential-thinking", 104 | "description": "MCP server for sequential thinking and problem solving", 105 | "runtime": "node" 106 | }, 107 | { 108 | "name": "@automatalabs/mcp-server-playwright", 109 | "description": "MCP server for browser automation using Playwright", 110 | "runtime": "node" 111 | }, 112 | { 113 | "name": "@mcp-get-community/server-llm-txt", 114 | "description": "MCP server that extracts and serves context from llm.txt files, enabling AI models to understand file structure, dependencies, and code relationships in development environments", 115 | "runtime": "node" 116 | }, 117 | { 118 | "name": "@executeautomation/playwright-mcp-server", 119 | "description": "A Model Context Protocol server for Playwright for Browser Automation and Web Scraping.", 120 | "runtime": "node" 121 | }, 122 | { 123 | "name": "@mcp-get-community/server-curl", 124 | "description": "MCP server for making HTTP requests using a curl-like interface", 125 | "runtime": "node" 126 | }, 127 | { 128 | "name": "@mcp-get-community/server-macos", 129 | "description": "MCP server for macOS system operations", 130 | "runtime": "node" 131 | }, 132 | { 133 | "name": "@modelcontextprotocol/server-aws-kb-retrieval", 134 | "description": "MCP server for AWS Knowledge Base retrieval using Bedrock Agent Runtime", 135 | "runtime": "node" 136 | }, 137 | { 138 | "name": "mcp-mongo-server", 139 | "description": "A Model Context Protocol Server for MongoDB", 140 | "runtime": "node" 141 | }, 142 | { 143 | "name": "@llmindset/mcp-hfspace", 144 | "description": "MCP Server for using HuggingFace Spaces. Seamlessly use the latest Open Source Image, Audio and Text Models from within Claude Deskop.", 145 | "runtime": "node" 146 | }, 147 | { 148 | "name": "@llmindset/mcp-miro", 149 | "description": "A Model Context Protocol server to connect to the MIRO Whiteboard Application", 150 | "runtime": "node" 151 | }, 152 | { 153 | "name": "@strowk/mcp-k8s", 154 | "description": "MCP server connecting to Kubernetes", 155 | "runtime": "node" 156 | }, 157 | { 158 | "name": "mcp-shell", 159 | "description": "An MCP server for your shell", 160 | "runtime": "node" 161 | }, 162 | { 163 | "name": "@benborla29/mcp-server-mysql", 164 | "description": "An MCP server for interacting with MySQL databases", 165 | "runtime": "node" 166 | }, 167 | { 168 | "name": "airtable-mcp-server", 169 | "description": "Airtable database integration with schema inspection, read and write capabilities", 170 | "runtime": "node" 171 | }, 172 | { 173 | "name": "@enescinar/twitter-mcp", 174 | "description": "This MCP server allows Clients to interact with Twitter, enabling posting tweets and searching Twitter.", 175 | "runtime": "node" 176 | }, 177 | { 178 | "name": "mcp-server-commands", 179 | "description": "MCP server enabling LLMs to execute shell commands and run scripts through various interpreters with built-in safety controls.", 180 | "runtime": "node" 181 | }, 182 | { 183 | "name": "mcp-server-kubernetes", 184 | "description": "MCP server for managing Kubernetes clusters, enabling LLMs to interact with and control Kubernetes resources.", 185 | "runtime": "node" 186 | }, 187 | { 188 | "name": "@chanmeng666/google-news-server", 189 | "description": "MCP server for Google News search via SerpAPI", 190 | "runtime": "node" 191 | }, 192 | { 193 | "name": "mcp-server-stability-ai", 194 | "description": "Integrates Stability AI's image generation and manipulation capabilities for editing, upscaling, and more via Stable Diffusion models.", 195 | "runtime": "node" 196 | }, 197 | { 198 | "name": "@modelcontextprotocol/server-redis", 199 | "description": "", 200 | "runtime": "node" 201 | }, 202 | { 203 | "name": "kubernetes-mcp-server", 204 | "description": "Powerful and flexible Kubernetes MCP server implementation with additional features for OpenShift. Besides the typical CRUD operations on any Kubernetes resource, this implementation adds specialized features for Pods and other resources.", 205 | "runtime": "node" 206 | } 207 | ] --------------------------------------------------------------------------------