├── packages ├── package-list.json ├── mcp-solver.json ├── mcp-shell.json ├── modelcontextprotocol-server-filesystem.json ├── mcp-server-mysql.json ├── strowk--mcp-k8s.json ├── graphlit-mcp-server.json ├── mcp-mongo-server.json ├── mcp-server-giphy.json ├── chargebee--mcp.json ├── gongrzhe--server-gmail-autoauth-mcp.json ├── anaisbetts--mcp-youtube.json ├── web3-research-mcp.json ├── mcp-server-sqlite.json ├── qanon_mcp.json ├── mcp-server-sentry.json ├── pinkpixel--taskflow-mcp.json ├── modelcontextprotocol--server-everart.json ├── modelcontextprotocol--server-filesystem.json ├── modelcontextprotocol--server-gdrive.json ├── cloudflare--mcp-server-cloudflare.json ├── modelcontextprotocol--server-postgres.json ├── modelcontextprotocol--server-puppeteer.json ├── chriswhiterocks--sushimcp.json ├── mcp-get-community--server-macos.json ├── docker-mcp.json ├── modelcontextprotocol--server-memory.json ├── mcp-server-commands.json ├── mcp-server-time.json ├── modelcontextprotocol--server-everything.json ├── automatalabs--mcp-server-playwright.json ├── mcp-server-fetch.json ├── mcp-server-kubernetes.json ├── mcp-get-community--server-curl.json ├── llmindset--mcp-hfspace.json ├── mcp-server-git.json ├── modelcontextprotocol--server-sequential-thinking.json ├── modelcontextprotocol--server-aws-kb-retrieval.json ├── pollinations--model-context-protocol.json ├── mcp-openapi-schema-explorer.json ├── mcp-server-perplexity.json ├── executeautomation--playwright-mcp-server.json ├── sirmews--mcp-upbank.json ├── kimtaeyoon83--mcp-server-youtube-transcript.json ├── mcp-server-make.json ├── chanmeng666--google-news-server.json ├── armor-crypto-mcp.json ├── mcp-server-tree-sitter.json ├── ragieai--mcp-server.json ├── webflow-mcp-server.json ├── mcp-server-flomo.json ├── airtable-mcp-server.json ├── modelcontextprotocol--server-brave-search.json ├── raygun.io--mcp-server-raygun.json ├── mcp-rememberizer-vectordb.json ├── modelcontextprotocol--server-github.json ├── modelcontextprotocol--server-google-maps.json ├── mcp-get-community--server-llm-txt.json ├── kubernetes-mcp-server.json ├── anilist-mcp.json ├── kocierik--mcp-nomad.json ├── hyperbrowser-mcp.json ├── llmindset--mcp-miro.json ├── videodb-director-mcp.json ├── skydeckai-code.json ├── mcp-server-aidd.json ├── mcp-server-rememberizer.json ├── modelcontextprotocol--server-slack.json ├── mcp-tinybird.json ├── hackmd-mcp.json ├── niledatabase--nile-mcp-server.json ├── modelcontextprotocol--server-gitlab.json ├── awslabs.nova-canvas-mcp-server.json ├── last9-mcp-server ├── mcp-server-stability-ai.json ├── flamedeck--flamechart-mcp.json ├── enescinar--twitter-mcp.json ├── benborla29--mcp-server-mysql.json └── discogs-mcp-server.json ├── src ├── types │ ├── index.ts │ ├── inquirer-autocomplete-prompt.d.ts │ └── package.ts ├── mcp-get.code-workspace ├── types.ts ├── utils │ ├── runtime-utils.ts │ ├── display.ts │ ├── ui.ts │ ├── package-actions.ts │ ├── config.ts │ ├── __tests__ │ │ ├── package-registry.test.ts │ │ └── package-validation.test.ts │ ├── package-resolver.ts │ ├── config-manager.ts │ └── package-registry.ts ├── commands │ ├── list.ts │ ├── installed.ts │ ├── uninstall.ts │ └── install.ts ├── __tests__ │ ├── pr-check.test.js │ ├── auto-update.test.ts │ ├── package-publication.test.ts │ ├── uninstall.test.ts │ ├── installed.test.ts │ ├── list.test.ts │ └── install.test.ts ├── index.ts ├── install.ts ├── auto-update.ts ├── scripts │ ├── pr-check.test.js │ ├── convert-packages.test.js │ ├── add-package.js │ ├── migrate-env-vars.js │ └── convert-packages.js └── helpers │ └── index.ts ├── mcp-get.code-workspace ├── tsconfig.json ├── .github └── workflows │ ├── jest-tests.yml │ ├── eslint.yml │ ├── publish.yml │ ├── pr-check.yml │ └── update-packages.yml ├── jest.config.mjs ├── eslint.config.js ├── .gitignore ├── LICENSE ├── package.json ├── CLAUDE.md ├── scripts └── gather-mcp-server-info.js └── README.md /packages/package-list.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './package.js'; 2 | -------------------------------------------------------------------------------- /src/types/inquirer-autocomplete-prompt.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'inquirer-autocomplete-prompt'; -------------------------------------------------------------------------------- /mcp-get.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".." 5 | } 6 | ] 7 | } -------------------------------------------------------------------------------- /src/mcp-get.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".." 5 | } 6 | ] 7 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Package { 2 | name: string; 3 | description: string; 4 | vendor: string; 5 | sourceUrl: string; 6 | homepage: string; 7 | license: string; 8 | } -------------------------------------------------------------------------------- /packages/mcp-solver.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-solver", 3 | "description": "MCP server for Constraint Solving and Optimization", 4 | "vendor": "Stefan Szeider", 5 | "sourceUrl": "https://github.com/szeider/mcp-solver", 6 | "homepage": "https://github.com/szeider/mcp-solver", 7 | "license": "MIT", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/mcp-shell.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-shell", 3 | "description": "An MCP server for your shell", 4 | "vendor": "High Dimensional Research (https://hdr.is)", 5 | "sourceUrl": "https://github.com/hdresearch/mcp-shell", 6 | "homepage": "https://github.com/hdresearch/mcp-shell", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol-server-filesystem.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-filesystem", 3 | "description": "MCP server for filesystem access", 4 | "runtime": "node", 5 | "vendor": "Anthropic", 6 | "sourceUrl": "https://github.com/modelcontextprotocol/servers", 7 | "homepage": "https://modelcontextprotocol.io", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /packages/mcp-server-mysql.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@benborla29/mcp-server-mysql", 3 | "description": "MCP server for interacting with MySQL databases based on Node", 4 | "runtime": "node", 5 | "vendor": "benborla", 6 | "sourceUrl": "https://github.com/benborla/mcp-server-mysql", 7 | "homepage": "https://github.com/benborla/mcp-server-mysql", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /packages/strowk--mcp-k8s.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@strowk/mcp-k8s", 3 | "description": "MCP server connecting to Kubernetes", 4 | "vendor": "Timur Sultanaev (https://str4.io/about-me)", 5 | "sourceUrl": "https://github.com/strowk/mcp-k8s-go", 6 | "homepage": "https://github.com/strowk/mcp-k8s-go", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/graphlit-mcp-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphlit-mcp-server", 3 | "description": "Graphlit MCP Server for AI, RAG, OpenAI, PDF parsing and preprocessing", 4 | "runtime": "node", 5 | "vendor": "kirkmarple", 6 | "sourceUrl": "https://github.com/graphlit/graphlit-mcp-server", 7 | "homepage": "https://github.com/graphlit/graphlit-mcp-server", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /packages/mcp-mongo-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-mongo-server", 3 | "description": "A Model Context Protocol Server for MongoDB", 4 | "vendor": "Muhammed Kılıç ", 5 | "sourceUrl": "https://github.com/kiliczsh/mcp-mongo-server", 6 | "homepage": "https://github.com/kiliczsh/mcp-mongo-server", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/mcp-server-giphy.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-giphy", 3 | "description": "MCP Server for the Giphy API, enabling AI models to search, retrieve, and utilize GIFs from Giphy", 4 | "runtime": "node", 5 | "vendor": "magarcia", 6 | "sourceUrl": "https://github.com/magarcia/mcp-server-giphy", 7 | "homepage": "https://github.com/magarcia/mcp-server-giphy", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /packages/chargebee--mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chargebee/mcp", 3 | "description": "MCP Server that connects AI agents to Chargebee platform.", 4 | "vendor": "Chargebee (https://chargebee.com)", 5 | "sourceUrl": "https://github.com/chargebee/agentkit/tree/main/modelcontextprotocol", 6 | "homepage": "https://chargebee.com", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/gongrzhe--server-gmail-autoauth-mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gongrzhe/server-gmail-autoauth-mcp", 3 | "description": "Gmail MCP server with auto authentication support", 4 | "runtime": "node", 5 | "vendor": "gongrzhe", 6 | "sourceUrl": "https://github.com/gongrzhe/server-gmail-autoauth-mcp", 7 | "homepage": "https://github.com/gongrzhe/server-gmail-autoauth-mcp", 8 | "license": "ISC" 9 | } 10 | -------------------------------------------------------------------------------- /packages/anaisbetts--mcp-youtube.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@anaisbetts/mcp-youtube", 3 | "description": "MCP server for fetching YouTube subtitles", 4 | "vendor": "Anaïs Betts (https://github.com/anaisbetts)", 5 | "sourceUrl": "https://github.com/anaisbetts/mcp-youtube", 6 | "homepage": "https://github.com/anaisbetts/mcp-youtube", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/web3-research-mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web3-research-mcp", 3 | "description": "Deep Research for crypto - free & fully local 🧠", 4 | "vendor": "Aaron Elijah Mars", 5 | "sourceUrl": "https://github.com/aaronjmars/web3-research-mcp", 6 | "homepage": "https://smithery.ai/server/@aaronjmars/web3-research-mcp", 7 | "license": "Apache-2.0", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/mcp-server-sqlite.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-sqlite", 3 | "description": "A simple SQLite MCP server", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/sqlite", 6 | "homepage": "https://github.com/modelcontextprotocol/servers", 7 | "license": "MIT", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/qanon_mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qanon_mcp", 3 | "description": "Enables search, exploration, and analysis of all QAnon posts/drops for sociological study", 4 | "vendor": "Jack Kingsman", 5 | "sourceUrl": "https://github.com/jkingsman/qanon-mcp-server", 6 | "homepage": "https://github.com/jkingsman/qanon-mcp-server", 7 | "license": "MIT", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } 11 | -------------------------------------------------------------------------------- /packages/mcp-server-sentry.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-sentry", 3 | "description": "MCP server for retrieving issues from sentry.io", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/sentry", 6 | "homepage": "https://github.com/modelcontextprotocol/servers", 7 | "license": "MIT", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/pinkpixel--taskflow-mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pinkpixel/taskflow-mcp", 3 | "description": "Task manager Model Context Protocol (MCP) server for planning and executing tasks.", 4 | "vendor": "Pink Pixel", 5 | "sourceUrl": "https://github.com/pinkpixel-dev/taskflow-mcp", 6 | "homepage": "https://github.com/pinkpixel-dev/taskflow-mcp", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } 11 | -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-everart.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-everart", 3 | "description": "MCP server for EverArt API integration", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/everart", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-filesystem.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-filesystem", 3 | "description": "MCP server for filesystem access", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-gdrive.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-gdrive", 3 | "description": "MCP server for interacting with Google Drive", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/gdrive", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/cloudflare--mcp-server-cloudflare.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/mcp-server-cloudflare", 3 | "description": "MCP server for interacting with Cloudflare API", 4 | "vendor": "Cloudflare, Inc. (https://cloudflare.com)", 5 | "sourceUrl": "https://github.com/cloudflare/mcp-server-cloudflare", 6 | "homepage": "https://github.com/cloudflare/mcp-server-cloudflare", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "types": ["node", "jest"] 13 | }, 14 | "include": ["src/**/*", "test/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-postgres.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-postgres", 3 | "description": "MCP server for interacting with PostgreSQL databases", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/postgres", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-puppeteer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-puppeteer", 3 | "description": "MCP server for browser automation using Puppeteer", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/puppeteer", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/chriswhiterocks--sushimcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chriswhiterocks/sushimcp", 3 | "description": "SushiMCP assists developers with delivering context and up to date docs to their AI IDE's.", 4 | "vendor": "Chris White (https://chriswhite.rocks)", 5 | "sourceUrl": "https://github.com/maverickg59/sushimcp", 6 | "homepage": "https://sushimcp.com", 7 | "license": "AGPLv3", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } 11 | -------------------------------------------------------------------------------- /packages/mcp-get-community--server-macos.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mcp-get-community/server-macos", 3 | "description": "MCP server for macOS system operations", 4 | "vendor": "Michael Latman ", 5 | "sourceUrl": "https://github.com/mcp-get/community-servers/blob/main/src/server-macos", 6 | "homepage": "https://github.com/mcp-get-community/server-macos#readme", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/docker-mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-mcp", 3 | "description": "A powerful Model Context Protocol (MCP) server for Docker operations, enabling seamless container and compose stack management through Claude AI", 4 | "vendor": "QuantGeekDev & md-archive", 5 | "sourceUrl": "https://github.com/QuantGeekDev/docker-mcp", 6 | "homepage": "https://github.com/QuantGeekDev/docker-mcp", 7 | "license": "MIT", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-memory.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-memory", 3 | "description": "MCP server for enabling memory for Claude through a knowledge graph", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/memory", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/mcp-server-commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-commands", 3 | "description": "MCP server enabling LLMs to execute shell commands and run scripts through various interpreters with built-in safety controls.", 4 | "vendor": "g0t4 (https://github.com/g0t4)", 5 | "sourceUrl": "https://github.com/g0t4/mcp-server-commands", 6 | "homepage": "https://github.com/g0t4/mcp-server-commands", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/mcp-server-time.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-time", 3 | "description": "A Model Context Protocol server providing tools for time queries and timezone conversions for LLMs", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/time", 6 | "homepage": "https://github.com/modelcontextprotocol/servers", 7 | "license": "MIT", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-everything.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-everything", 3 | "description": "MCP server that exercises all the features of the MCP protocol", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/everything", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/automatalabs--mcp-server-playwright.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@automatalabs/mcp-server-playwright", 3 | "description": "MCP server for browser automation using Playwright", 4 | "vendor": "Automata Labs (https://automatalabs.io)", 5 | "sourceUrl": "https://github.com/Automata-Labs-team/MCP-Server-Playwright/tree/main", 6 | "homepage": "https://github.com/Automata-Labs-team/MCP-Server-Playwright", 7 | "runtime": "node", 8 | "license": "MIT", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/mcp-server-fetch.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-fetch", 3 | "description": "A Model Context Protocol server providing tools to fetch and convert web content for usage by LLMs", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/fetch", 6 | "homepage": "https://github.com/modelcontextprotocol/servers", 7 | "license": "MIT", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/mcp-server-kubernetes.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-kubernetes", 3 | "description": "MCP server for managing Kubernetes clusters, enabling LLMs to interact with and control Kubernetes resources.", 4 | "vendor": "Flux159 (https://github.com/Flux159)", 5 | "sourceUrl": "https://github.com/Flux159/mcp-server-kubernetes", 6 | "homepage": "https://github.com/Flux159/mcp-server-kubernetes", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/mcp-get-community--server-curl.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mcp-get-community/server-curl", 3 | "description": "MCP server for making HTTP requests using a curl-like interface", 4 | "vendor": "Michael Latman ", 5 | "sourceUrl": "https://github.com/mcp-get/community-servers/blob/main/src/server-curl", 6 | "homepage": "https://github.com/mcp-get-community/server-curl#readme", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /.github/workflows/jest-tests.yml: -------------------------------------------------------------------------------- 1 | name: Jest Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: '18' 18 | 19 | - name: Install dependencies 20 | run: npm ci 21 | 22 | - name: Run Jest tests 23 | run: npm test 24 | -------------------------------------------------------------------------------- /packages/llmindset--mcp-hfspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@llmindset/mcp-hfspace", 3 | "description": "MCP Server for using HuggingFace Spaces. Seamlessly use the latest Open Source Image, Audio and Text Models from within Claude Desktop.", 4 | "vendor": "llmindset.co.uk", 5 | "sourceUrl": "https://github.com/evalstate/mcp-hfspace/", 6 | "homepage": "https://llmindset.co.uk/resources/hfspace-connector/", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } 11 | -------------------------------------------------------------------------------- /packages/mcp-server-git.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-git", 3 | "description": "A Model Context Protocol server providing tools to read, search, and manipulate Git repositories programmatically via LLMs", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/git", 6 | "homepage": "https://github.com/modelcontextprotocol/servers", 7 | "license": "MIT", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-sequential-thinking.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-sequential-thinking", 3 | "description": "MCP server for sequential thinking and problem solving", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/sequentialthinking", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-aws-kb-retrieval.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-aws-kb-retrieval", 3 | "description": "MCP server for AWS Knowledge Base retrieval using Bedrock Agent Runtime", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/aws-kb-retrieval-server", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/pollinations--model-context-protocol.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pollinations/model-context-protocol", 3 | "description": "MCP server for image, audio, and text generation using Pollinations APIs.", 4 | "vendor": "Pollinations.AI", 5 | "sourceUrl": "https://github.com/pollinations/pollinations/tree/main/model-context-protocol", 6 | "homepage": "https://github.com/pollinations/pollinations/tree/main/model-context-protocol", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/mcp-openapi-schema-explorer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-openapi-schema-explorer", 3 | "description": "MCP server providing token-efficient access to OpenAPI/Swagger specs via MCP Resources for client-side exploration.", 4 | "vendor": "Aleksandr Kadykov (https://www.kadykov.com/)", 5 | "sourceUrl": "https://github.com/kadykov/mcp-openapi-schema-explorer", 6 | "homepage": "https://github.com/kadykov/mcp-openapi-schema-explorer", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/mcp-server-perplexity.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-perplexity", 3 | "description": "MCP Server for the Perplexity API", 4 | "vendor": "tanigami", 5 | "sourceUrl": "https://github.com/tanigami/mcp-server-perplexity", 6 | "homepage": "https://github.com/tanigami/mcp-server-perplexity", 7 | "license": "MIT", 8 | "runtime": "python", 9 | "environmentVariables": { 10 | "PERPLEXITY_API_KEY": { 11 | "description": "API key for Perplexity API access", 12 | "required": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/executeautomation--playwright-mcp-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@executeautomation/playwright-mcp-server", 3 | "description": "A Model Context Protocol server for Playwright for Browser Automation and Web Scraping.", 4 | "vendor": "ExecuteAutomation, Ltd (https://executeautomation.com)", 5 | "sourceUrl": "https://github.com/executeautomation/mcp-playwright/tree/main/src", 6 | "homepage": "https://github.com/executeautomation/mcp-playwright", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/sirmews--mcp-upbank.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sirmews/mcp-upbank", 3 | "description": "An MCP Server for interacting with the Up Bank API", 4 | "vendor": "Navishkar Rao (https://github.com/sirmews)", 5 | "sourceUrl": "https://github.com/sirmews/mcp-upbank", 6 | "homepage": "https://github.com/sirmews/mcp-upbank", 7 | "license": "Unlicense", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "UP_API_TOKEN": { 11 | "description": "Your Up Bank Personal Access Token", 12 | "required": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/kimtaeyoon83--mcp-server-youtube-transcript.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kimtaeyoon83/mcp-server-youtube-transcript", 3 | "description": "This is an MCP server that allows you to directly download transcripts of YouTube videos.", 4 | "vendor": "Freddie (https://github.com/kimtaeyoon83)", 5 | "sourceUrl": "https://github.com/kimtaeyoon83/mcp-server-youtube-transcript", 6 | "homepage": "https://github.com/kimtaeyoon83/mcp-server-youtube-transcript", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/mcp-server-make.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-make", 3 | "description": "A Model Context Protocol server that provides make target calling functionality. This server enables LLMs to execute make targets from a specified Makefile within a specified working directory.", 4 | "vendor": "Wrale LTD (https://wrale.com)", 5 | "sourceUrl": "https://github.com/wrale/mcp-server-make", 6 | "homepage": "https://github.com/wrale/mcp-server-make", 7 | "license": "MIT", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: '18' 18 | 19 | - name: Install dependencies 20 | run: npm ci 21 | 22 | - name: Run ESLint 23 | run: npm run lint || true # Don't fail the build for now, just report issues 24 | -------------------------------------------------------------------------------- /packages/chanmeng666--google-news-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chanmeng666/google-news-server", 3 | "description": "MCP server for Google News search via SerpAPI", 4 | "vendor": "Chan Meng", 5 | "sourceUrl": "https://github.com/ChanMeng666/server-google-news", 6 | "homepage": "https://github.com/ChanMeng666/server-google-news", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "SERP_API_KEY": { 11 | "description": "API key for Google News search", 12 | "required": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/armor-crypto-mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "armor-crypto-mcp", 3 | "description": "MCP server for full access to the blockchain functionality of Armor Wallet.", 4 | "vendor": "Armor Wallet", 5 | "sourceUrl": "https://github.com/armorwallet/armor-crypto-mcp.git", 6 | "homepage": "https://github.com/armorwallet/armor-crypto-mcp.git", 7 | "license": "GNU GPL-3.0", 8 | "runtime": "python", 9 | "environmentVariables": { 10 | "ARMOR_API_KEY": { 11 | "description": "API for Armor Wallet", 12 | "required": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | const config = { 3 | preset: 'ts-jest/presets/default-esm', 4 | testEnvironment: 'node', 5 | extensionsToTreatAsEsm: ['.ts'], 6 | moduleNameMapper: { 7 | '^(\\.{1,2}/.*)\\.js$': '$1', 8 | }, 9 | transform: { 10 | '^.+\\.tsx?$': [ 11 | 'ts-jest', 12 | { 13 | useESM: true, 14 | }, 15 | ], 16 | }, 17 | testMatch: ['**/__tests__/**/*.test.ts'], 18 | testPathIgnorePatterns: ['/node_modules/', '/loaders/'], 19 | }; 20 | 21 | export default config; -------------------------------------------------------------------------------- /packages/mcp-server-tree-sitter.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-tree-sitter", 3 | "description": "A Model Context Protocol server that provides code analysis capabilities using tree-sitter. This server enables LLMs to explore, search, and analyze code with appropriate context management.", 4 | "vendor": "Wrale LTD (https://wrale.com)", 5 | "sourceUrl": "https://github.com/wrale/mcp-server-tree-sitter", 6 | "homepage": "https://github.com/wrale/mcp-server-tree-sitter", 7 | "license": "MIT", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } 11 | -------------------------------------------------------------------------------- /packages/ragieai--mcp-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ragieai/mcp-server", 3 | "description": "Retrieve context from your Ragie knowledge base connected to integrations like Google Drive, Notion, JIRA and more.", 4 | "runtime": "node", 5 | "vendor": "Ragie", 6 | "sourceUrl": "https://github.com/ragieai/ragie-mcp-server", 7 | "homepage": "https://github.com/ragieai/ragie-mcp-server", 8 | "license": "MIT", 9 | "environmentVariables": { 10 | "RAGIE_API_KEY": { 11 | "description": "API key for Ragie", 12 | "required": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/webflow-mcp-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webflow-mcp-server", 3 | "description": "Webflow MCP server to help users easily interact with Webflow sites, pages, and collections.", 4 | "vendor": "Webflow Inc. (https://webflow.com)", 5 | "sourceUrl": "https://github.com/webflow/mcp-server", 6 | "homepage": "https://github.com/webflow/mcp-server", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "WEBFLOW_TOKEN": { 11 | "description": "API key for Webflow API", 12 | "required": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/mcp-server-flomo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-flomo", 3 | "description": "A MCP server for Flomo", 4 | "vendor": "@xianminx", 5 | "sourceUrl": "https://github.com/xianminx/mcp-server-flomo", 6 | "homepage": "https://github.com/xianminx/mcp-server-flomo", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "FLOMO_API_URL": { 11 | "description": "Get your Flomo API URL from [Flomo API Settings](https://v.flomoapp.com/mine)", 12 | "required": true, 13 | "argName": "FLOMO_API_URL" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/airtable-mcp-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airtable-mcp-server", 3 | "description": "Airtable database integration with schema inspection, read and write capabilities", 4 | "vendor": "Adam Jones (https://github.com/domdomegg/)", 5 | "sourceUrl": "https://github.com/domdomegg/airtable-mcp-server", 6 | "homepage": "https://github.com/domdomegg/airtable-mcp-server", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "AIRTABLE_API_KEY": { 11 | "description": "API key for Airtable API", 12 | "required": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-brave-search.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-brave-search", 3 | "description": "MCP server for Brave Search API integration", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/brave-search", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "BRAVE_API_KEY": { 11 | "description": "API key for Brave Search", 12 | "required": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/raygun.io--mcp-server-raygun.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@raygun.io/mcp-server-raygun", 3 | "description": "MCP server for interacting with Raygun's API for crash reporting and real user monitoring metrics", 4 | "vendor": "Raygun (https://raygun.com)", 5 | "sourceUrl": "https://github.com/MindscapeHQ/mcp-server-raygun", 6 | "homepage": "https://raygun.com", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "RAYGUN_PAT_TOKEN": { 11 | "description": "Personal access token for Raygun API access", 12 | "required": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/mcp-rememberizer-vectordb.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-rememberizer-vectordb", 3 | "description": "A Model Context Protocol server for LLMs to interact with Rememberizer Vector Store.", 4 | "vendor": "Rememberizer®", 5 | "sourceUrl": "https://github.com/skydeckai/mcp-rememberizer-vectordb", 6 | "homepage": "https://rememberizer.ai/", 7 | "license": "Apache 2.0", 8 | "runtime": "python", 9 | "environmentVariables": { 10 | "REMEMBERIZER_VECTOR_STORE_API_KEY": { 11 | "description": "API token for Rememberizer Vector Store", 12 | "required": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-github.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-github", 3 | "description": "MCP server for using the GitHub API", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/github", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "GITHUB_PERSONAL_ACCESS_TOKEN": { 11 | "description": "Personal access token for GitHub API access", 12 | "required": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-google-maps.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-google-maps", 3 | "description": "MCP server for using the Google Maps API", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/google-maps", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "GOOGLE_MAPS_API_KEY": { 11 | "description": "API key for Google Maps services", 12 | "required": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/mcp-get-community--server-llm-txt.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mcp-get-community/server-llm-txt", 3 | "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", 4 | "vendor": "Michael Latman (https://michaellatman.com)", 5 | "sourceUrl": "https://github.com/mcp-get/community-servers/blob/main/src/server-llm-txt", 6 | "homepage": "https://github.com/mcp-get/community-servers#readme", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/kubernetes-mcp-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kubernetes-mcp-server", 3 | "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.", 4 | "vendor": "Marc Nuri (https://www.marcnuri.com)", 5 | "sourceUrl": "https://github.com/manusa/kubernetes-mcp-server", 6 | "homepage": "https://github.com/manusa/kubernetes-mcp-server", 7 | "license": "Apache-2.0", 8 | "runtime": "node", 9 | "environmentVariables": {} 10 | } -------------------------------------------------------------------------------- /packages/anilist-mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anilist-mcp", 3 | "description": "MCP server that interfaces with the AniList API, allowing LLM clients to access and interact with anime, manga, character, staff, and user data from AniList", 4 | "vendor": "yuna0x0 (https://github.com/yuna0x0)", 5 | "sourceUrl": "https://github.com/yuna0x0/anilist-mcp", 6 | "homepage": "https://github.com/yuna0x0/anilist-mcp", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "ANILIST_TOKEN": { 11 | "description": "Optional API token for AniList", 12 | "required": false 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/kocierik--mcp-nomad.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kocierik/mcp-nomad", 3 | "description": "MCP server connecting to Nomad", 4 | "vendor": "Erik Koci (https://teapot.ovh/@erik)", 5 | "sourceUrl": "https://github.com/kocierik/mcp-nomad", 6 | "homepage": "https://github.com/kocierik/mcp-nomad", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "NOMAD_TOKEN": { 11 | "description": "Nomad token if ACL are enabled", 12 | "required": false 13 | }, 14 | "NOMAD_ADDR": { 15 | "description": "Nomad URL address", 16 | "required": false 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/hyperbrowser-mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperbrowser-mcp", 3 | "description": "An MCP server for Hyperbrowser - Hyperbrowser is the next-generation platform empowering AI agents and enabling effortless, scalable browser automation", 4 | "vendor": "Hyperbrowser (https://www.hyperbrowser.ai/)", 5 | "sourceUrl": "https://github.com/hyperbrowserai/mcp/tree/main", 6 | "homepage": "https://www.hyperbrowser.ai/", 7 | "runtime": "node", 8 | "license": "MIT", 9 | "environmentVariables": { 10 | "HYPERBROWSER_API_KEY": { 11 | "description": "API KEY for Hyperbrowser", 12 | "required": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/llmindset--mcp-miro.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@llmindset/mcp-miro", 3 | "description": "A Model Context Protocol server to connect to the MIRO Whiteboard Application", 4 | "vendor": "llmindset.co.uk", 5 | "sourceUrl": "https://github.com/evalstate/mcp-miro", 6 | "homepage": "https://github.com/evalstate/mcp-miro#readme", 7 | "license": "Apache-2.0", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "MIRO-OAUTH-KEY": { 11 | "description": "Authentication token for Miro API access (can also be provided via --token argument)", 12 | "required": true, 13 | "argName": "token" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /packages/videodb-director-mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videodb-director-mcp", 3 | "description": "Unlock Intelligent Video Processing with VideoDB MCP", 4 | "vendor": "VideoDB", 5 | "sourceUrl": "https://github.com/video-db/agent-toolkit/tree/main/modelcontextprotocol", 6 | "homepage": "https://videodb.io/mcp-users", 7 | "license": "Apache 2.0", 8 | "runtime": "python", 9 | "environmentVariables": { 10 | "VIDEODB_API_KEY": { 11 | "description": "The VideoDB API key required to connect to the VideoDB service. You can get one by logging into https://console.videodb.io", 12 | "required": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import tseslint from '@typescript-eslint/eslint-plugin'; 2 | import tsParser from '@typescript-eslint/parser'; 3 | 4 | export default [ 5 | { 6 | ignores: ['dist/**', 'node_modules/**', 'coverage/**'], 7 | }, 8 | { 9 | files: ['**/*.ts'], 10 | languageOptions: { 11 | parser: tsParser, 12 | parserOptions: { 13 | ecmaVersion: 2022, 14 | sourceType: 'module', 15 | }, 16 | globals: { 17 | }, 18 | }, 19 | plugins: { 20 | '@typescript-eslint': tseslint, 21 | }, 22 | rules: { 23 | ...tseslint.configs.recommended.rules, 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /packages/skydeckai-code.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skydeckai-code", 3 | "description": "An MCP server that provides a comprehensive set of tools for AI-driven development workflows. Features include file system operations, code analysis using tree-sitter for multiple programming languages, Git operations, code execution, and system information retrieval. Designed to enhance AI's capability to assist in software development tasks.", 4 | "vendor": "SkyDeck.ai®", 5 | "sourceUrl": "https://github.com/skydeckai/skydeckai-code", 6 | "homepage": "https://skydeck.ai/", 7 | "license": "Apache 2.0", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } 11 | -------------------------------------------------------------------------------- /packages/mcp-server-aidd.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-aidd", 3 | "description": "An MCP server that provides a comprehensive set of tools for AI-driven development workflows. Features include file system operations, code analysis using tree-sitter for multiple programming languages, Git operations, code execution, and system information retrieval. Designed to enhance AI's capability to assist in software development tasks.", 4 | "vendor": "SkyDeck.ai®", 5 | "sourceUrl": "https://github.com/skydeckai/mcp-server-aidd", 6 | "homepage": "https://skydeck.ai/", 7 | "license": "Apache 2.0", 8 | "runtime": "python", 9 | "environmentVariables": {} 10 | } 11 | -------------------------------------------------------------------------------- /packages/mcp-server-rememberizer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-rememberizer", 3 | "description": "An MCP server for interacting with Rememberizer's document and knowledge management API. This server enables Large Language Models to search, retrieve, and manage documents and integrations through Rememberizer.", 4 | "vendor": "Rememberizer®", 5 | "sourceUrl": "https://github.com/skydeckai/mcp-server-rememberizer", 6 | "homepage": "https://rememberizer.ai/", 7 | "license": "Apache 2.0", 8 | "runtime": "python", 9 | "environmentVariables": { 10 | "REMEMBERIZER_API_TOKEN": { 11 | "description": "API token for Rememberizer", 12 | "required": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-slack.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-slack", 3 | "description": "MCP server for interacting with Slack", 4 | "vendor": "Anthropic, PBC (https://anthropic.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/slack", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "SLACK_BOT_TOKEN": { 11 | "description": "Slack Bot User OAuth Token (starts with xoxb-)", 12 | "required": true 13 | }, 14 | "SLACK_TEAM_ID": { 15 | "description": "Slack Team/Workspace ID", 16 | "required": true 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /packages/mcp-tinybird.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-tinybird", 3 | "description": "A Model Context Protocol server that lets you interact with a Tinybird Workspace from any MCP client.", 4 | "vendor": "Tinybird (https://tinybird.co)", 5 | "sourceUrl": "https://github.com/tinybirdco/mcp-tinybird/tree/main/src/mcp-tinybird", 6 | "homepage": "https://github.com/tinybirdco/mcp-tinybird", 7 | "license": "Apache 2.0", 8 | "runtime": "python", 9 | "environmentVariables": { 10 | "TB_API_URL": { 11 | "description": "API URL for Tinybird", 12 | "required": true 13 | }, 14 | "TB_ADMIN_TOKEN": { 15 | "description": "Admin token for Tinybird", 16 | "required": true 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /packages/hackmd-mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackmd-mcp", 3 | "description": "A Model Context Protocol server for integrating HackMD's note-taking platform with AI assistants", 4 | "vendor": "yuna0x0 (https://github.com/yuna0x0)", 5 | "sourceUrl": "https://github.com/yuna0x0/hackmd-mcp", 6 | "homepage": "https://github.com/yuna0x0/hackmd-mcp", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "HACKMD_API_TOKEN": { 11 | "description": "API token for HackMD", 12 | "required": true 13 | }, 14 | "HACKMD_API_URL": { 15 | "description": "HackMD API Endpoint URL (optional, defaults to https://api.hackmd.io/v1)", 16 | "required": false 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/niledatabase--nile-mcp-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@niledatabase/nile-mcp-server", 3 | "description": "MCP server for Nile Database - Manage and query databases, tenants, users, auth using LLMs", 4 | "vendor": "Nile (https://www.thenile.dev/)", 5 | "sourceUrl": "https://github.com/niledatabase/nile-mcp-server/tree/main", 6 | "homepage": "https://github.com/niledatabase/nile-mcp-server/tree/main", 7 | "runtime": "node", 8 | "license": "MIT", 9 | "environmentVariables": { 10 | "NILE_API_KEY": { 11 | "description": "API KEY for Nile", 12 | "required": true 13 | }, 14 | "NILE_WORKSPACE_SLUG": { 15 | "description": "Nile workspace name", 16 | "required": true 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /packages/modelcontextprotocol--server-gitlab.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modelcontextprotocol/server-gitlab", 3 | "description": "MCP server for using the GitLab API", 4 | "vendor": "GitLab, PBC (https://gitlab.com)", 5 | "sourceUrl": "https://github.com/modelcontextprotocol/servers/blob/main/src/gitlab", 6 | "homepage": "https://modelcontextprotocol.io", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "GITLAB_PERSONAL_ACCESS_TOKEN": { 11 | "description": "Personal access token for GitLab API access", 12 | "required": true 13 | }, 14 | "GITLAB_API_URL": { 15 | "description": "GitLab API URL (optional, for self-hosted instances)", 16 | "required": false 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /packages/awslabs.nova-canvas-mcp-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awslabs.nova-canvas-mcp-server", 3 | "description": "A Model Context Protocol server that lets you interact with a Nova Canvas from any MCP client.", 4 | "vendor": "AWS (https://aws.amazon.com)", 5 | "sourceUrl": "https://github.com/awslabs/mcp/tree/main/src/nova-canvas-mcp-server", 6 | "homepage": "https://github.com/awslabs/mcp/tree/main/src/nova-canvas-mcp-server", 7 | "license": "Apache 2.0", 8 | "runtime": "python", 9 | "environmentVariables": { 10 | "AWS_PROFILE": { 11 | "description": "AWS profile to use", 12 | "required": true 13 | }, 14 | "AWS_REGION": { 15 | "description": "AWS region to use", 16 | "required": true 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build output 8 | dist/ 9 | build/ 10 | *.tsbuildinfo 11 | 12 | # Environment variables 13 | .env 14 | .env.local 15 | .env.*.local 16 | 17 | # IDE and editor files 18 | .idea/ 19 | .vscode/ 20 | *.swp 21 | *.swo 22 | .DS_Store 23 | Thumbs.db 24 | 25 | # Temporary files 26 | temp/ 27 | tmp/ 28 | *.tmp 29 | *.temp 30 | extract_ref/ 31 | 32 | # Logs 33 | logs/ 34 | *.log 35 | 36 | # Test coverage 37 | coverage/ 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional eslint cache 43 | .eslintcache 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | # Output of 'npm pack' 49 | *.tgz 50 | 51 | # Yarn Integrity file 52 | .yarn-integrity 53 | 54 | # Ignore loaders directory 55 | loaders/ -------------------------------------------------------------------------------- /packages/last9-mcp-server: -------------------------------------------------------------------------------- 1 | { 2 | "name": "last9-mcp-server", 3 | "description": "Seamlessly bring real-time production context—logs, metrics, and traces—into your local environment to auto-fix code faster.", 4 | "vendor": "Last9", 5 | "sourceUrl": "https://github.com/last9/last9-mcp-server", 6 | "homepage": "https://last9.io/docs/mcp/", 7 | "license": "MIT", 8 | "runtime": "go", 9 | "environmentVariables": { 10 | "LAST9_AUTH_TOKEN": { 11 | "description": "Authentication token for Last9 MCP server", 12 | "required": true 13 | }, 14 | "LAST9_BASE_URL": { 15 | "description": "Last9 API URL", 16 | "required": true 17 | }, 18 | "LAST9_REFRESH_TOKEN": { 19 | "description": "Refresh Token with Write permissions. Needed for accessing control plane APIs", 20 | "required": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/mcp-server-stability-ai.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-stability-ai", 3 | "description": "Integrates Stability AI's image generation and manipulation capabilities for editing, upscaling, and more via Stable Diffusion models.", 4 | "vendor": "Tadas Antanavicius (https://github.com/tadasant)", 5 | "sourceUrl": "https://github.com/tadasant/mcp-server-stability-ai", 6 | "homepage": "https://www.pulsemcp.com/servers/tadasant-stability-ai", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "STABILITY_AI_API_KEY": { 11 | "description": "API key for Stability AI; get it from https://platform.stability.ai/account/keys.", 12 | "required": true 13 | }, 14 | "IMAGE_STORAGE_DIRECTORY": { 15 | "description": "Absolute path to a directory on filesystem to store output images.", 16 | "required": true 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /packages/flamedeck--flamechart-mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flamedeck/flamechart-mcp", 3 | "description": "MCP server for debugging and analyzing a wide range of performance profiles (go, javascript, python, etc) using flamegraphs. Use with local traces or with FlameDeck's hosted trace storage.", 4 | "vendor": "Flamedeck Team", 5 | "sourceUrl": "https://github.com/flamedeck-org/flamedeck/tree/main/packages/flamechart-mcp", 6 | "homepage": "https://github.com/flamedeck-org/flamedeck/tree/main/packages/flamechart-mcp#readme", 7 | "license": "ISC", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "FLAMEDECK_API_KEY": { 11 | "description": "API key for analyzing remote Flamedeck traces. Only required for remote Flamedeck URL traces - works completely offline for local trace files without this key.", 12 | "required": false 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /packages/enescinar--twitter-mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@enescinar/twitter-mcp", 3 | "description": "This MCP server allows Clients to interact with Twitter, enabling posting tweets and searching Twitter.", 4 | "vendor": "Enes Çınar", 5 | "sourceUrl": "https://github.com/EnesCinr/twitter-mcp", 6 | "homepage": "https://github.com/EnesCinr/twitter-mcp", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "API_KEY": { 11 | "description": "API key for X API", 12 | "required": true 13 | }, 14 | "API_SECRET_KEY": { 15 | "description": "API secret key for X API", 16 | "required": true 17 | }, 18 | "ACCESS_TOKEN": { 19 | "description": "API access token for X API", 20 | "required": true 21 | }, 22 | "ACCESS_TOKEN_SECRET": { 23 | "description": "API access token secret for X API", 24 | "required": true 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /packages/benborla29--mcp-server-mysql.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@benborla29/mcp-server-mysql", 3 | "description": "An MCP server for interacting with MySQL databases", 4 | "vendor": "Ben Borla (https://benborla.dev)", 5 | "sourceUrl": "https://github.com/benborla/mcp-server-mysql", 6 | "homepage": "https://github.com/benborla/mcp-server-mysql", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "MYSQL_HOST": { 11 | "description": "MySQL Host address", 12 | "required": true 13 | }, 14 | "MYSQL_PORT": { 15 | "description": "MySQL port defaults to 3306", 16 | "required": false 17 | }, 18 | "MYSQL_USER": { 19 | "description": "MySQL username", 20 | "required": true 21 | }, 22 | "MYSQL_PASS": { 23 | "description": "MySQL password", 24 | "required": true 25 | }, 26 | "MYSQL_DB": { 27 | "description": "MySQL database to use", 28 | "required": false 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/types/package.ts: -------------------------------------------------------------------------------- 1 | export interface Package { 2 | name: string; 3 | description: string; 4 | runtime: 'node' | 'python' | 'go'; 5 | vendor: string; 6 | sourceUrl: string; 7 | homepage: string; 8 | license: string; 9 | version?: string; // Optional version field to specify package version 10 | environmentVariables?: { 11 | [key: string]: { 12 | description: string; 13 | required: boolean; 14 | argName?: string; 15 | } 16 | }; 17 | } 18 | 19 | export interface ResolvedPackage extends Package { 20 | isInstalled: boolean; 21 | isVerified: boolean; 22 | } 23 | 24 | export interface PackageHelper { 25 | requiredEnvVars?: { 26 | [key: string]: { 27 | description: string; 28 | required: boolean; 29 | argName?: string; 30 | } 31 | }; 32 | configureEnv?: (config: any) => Promise; 33 | runtime?: 'node' | 'python' | 'go'; 34 | } 35 | 36 | export interface PackageHelpers { 37 | [packageName: string]: PackageHelper; 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Michael Latman 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. -------------------------------------------------------------------------------- /packages/discogs-mcp-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discogs-mcp-server", 3 | "description": "A MCP server for Discogs API integration", 4 | "vendor": "Christopher Kim (https://github.com/cswkim)", 5 | "sourceUrl": "https://github.com/cswkim/discogs-mcp-server", 6 | "homepage": "https://github.com/cswkim/discogs-mcp-server", 7 | "license": "MIT", 8 | "runtime": "node", 9 | "environmentVariables": { 10 | "DISCOGS_PERSONAL_ACCESS_TOKEN": { 11 | "description": "Personal access token for Discogs API", 12 | "required": true 13 | }, 14 | "DISCOGS_API_URL": { 15 | "description": "API URL for Discogs API", 16 | "required": false 17 | }, 18 | "DISCOGS_MEDIA_TYPE": { 19 | "description": "Accept header for Discogs API", 20 | "required": false 21 | }, 22 | "DISCOGS_USER_AGENT": { 23 | "description": "User agent header for Discogs API", 24 | "required": false 25 | }, 26 | "PORT": { 27 | "description": "Port for the MCP server SSE endpoint", 28 | "required": false 29 | }, 30 | "SERVER_NAME": { 31 | "description": "Name of the MCP server", 32 | "required": false 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: # Allows manual triggering 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Check out repository 18 | uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Set up Node.js 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: '18' 26 | registry-url: 'https://registry.npmjs.org' 27 | 28 | - name: Install dependencies 29 | run: npm ci 30 | 31 | - name: Build project 32 | run: npm run build 33 | 34 | - name: Configure Git 35 | run: | 36 | git config --local user.email "action@github.com" 37 | git config --local user.name "GitHub Action" 38 | 39 | - name: Bump version 40 | run: | 41 | npm version patch -m "chore: bump version to %s [skip ci]" 42 | git push 43 | git push --tags 44 | 45 | - name: Publish to npm 46 | run: npm publish --access public 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/pr-check.yml: -------------------------------------------------------------------------------- 1 | name: PR Check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | pr-check: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check out repository 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: '18' 25 | 26 | - name: Set up Python 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: '3.12' 30 | 31 | - name: Configure pip for uvx 32 | run: | 33 | pip config set global.index-url https://uvx.org/pypi/simple/ 34 | pip config set global.trusted-host uvx.org 35 | 36 | - name: Install dependencies 37 | run: npm ci 38 | 39 | - name: Run PR check script 40 | run: npm run pr-check 41 | env: 42 | DEBUG: 'true' 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | GITHUB_REPOSITORY: ${{ github.repository }} 45 | GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} 46 | GITHUB_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} 47 | -------------------------------------------------------------------------------- /src/utils/runtime-utils.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import inquirer from 'inquirer'; 4 | import chalk from 'chalk'; 5 | 6 | const execAsync = promisify(exec); 7 | 8 | export async function checkUVInstalled(): Promise { 9 | try { 10 | await execAsync('uvx --version'); 11 | return true; 12 | } catch (error) { 13 | return false; 14 | } 15 | } 16 | 17 | export async function promptForUVInstall(inquirerInstance: typeof inquirer): Promise { 18 | const { shouldInstall } = await inquirerInstance.prompt<{ shouldInstall: boolean }>([{ 19 | type: 'confirm', 20 | name: 'shouldInstall', 21 | message: 'UV package manager is required for Python MCP servers. Would you like to install it?', 22 | default: true 23 | }]); 24 | 25 | if (!shouldInstall) { 26 | console.warn(chalk.yellow('UV installation was declined. You can install it manually from https://astral.sh/uv')); 27 | return false; 28 | } 29 | 30 | console.log('Installing uv package manager...'); 31 | try { 32 | await execAsync('curl -LsSf https://astral.sh/uv/install.sh | sh'); 33 | console.log(chalk.green('✓ UV installed successfully')); 34 | return true; 35 | } catch (error) { 36 | console.warn(chalk.yellow('Failed to install UV. You can install it manually from https://astral.sh/uv')); 37 | return false; 38 | } 39 | } -------------------------------------------------------------------------------- /src/commands/list.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import inquirer from 'inquirer'; 3 | import { displayPackageDetailsWithActions } from '../utils/display.js'; 4 | import { resolvePackages } from '../utils/package-resolver.js'; 5 | import { ResolvedPackage } from '../types/package.js'; 6 | import AutocompletePrompt from 'inquirer-autocomplete-prompt'; 7 | import { createPackagePrompt, printPackageListHeader } from '../utils/ui.js'; 8 | import { handlePackageAction } from '../utils/package-actions.js'; 9 | 10 | // Register the autocomplete prompt 11 | inquirer.registerPrompt('autocomplete', AutocompletePrompt); 12 | 13 | export async function list() { 14 | try { 15 | const packages = resolvePackages(); 16 | printPackageListHeader(packages.length); 17 | 18 | const prompt = createPackagePrompt(packages, { showInstallStatus: true }); 19 | const answer = await inquirer.prompt<{ selectedPackage: ResolvedPackage }>([prompt]); 20 | 21 | if (!answer.selectedPackage) { 22 | return; 23 | } 24 | 25 | const action = await displayPackageDetailsWithActions(answer.selectedPackage); 26 | await handlePackageAction(answer.selectedPackage, action, { 27 | onBack: list 28 | }); 29 | } catch (error) { 30 | console.error(chalk.red('Error loading package list:')); 31 | console.error(chalk.red(error instanceof Error ? error.message : String(error))); 32 | process.exit(1); 33 | } 34 | } -------------------------------------------------------------------------------- /src/commands/installed.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import chalk from 'chalk'; 3 | import { displayPackageDetailsWithActions } from '../utils/display.js'; 4 | import { resolvePackages } from '../utils/package-resolver.js'; 5 | import { ResolvedPackage } from '../types/package.js'; 6 | import AutocompletePrompt from 'inquirer-autocomplete-prompt'; 7 | import { createPackagePrompt, printPackageListHeader } from '../utils/ui.js'; 8 | import { handlePackageAction } from '../utils/package-actions.js'; 9 | 10 | inquirer.registerPrompt('autocomplete', AutocompletePrompt); 11 | 12 | export async function listInstalledPackages(): Promise { 13 | // Get all packages with their resolved status 14 | const allPackages = resolvePackages(); 15 | 16 | // Filter for only installed packages 17 | const installedPackages = allPackages.filter(pkg => pkg.isInstalled); 18 | 19 | if (installedPackages.length === 0) { 20 | console.log(chalk.yellow('\nNo MCP servers are currently installed.')); 21 | return; 22 | } 23 | 24 | printPackageListHeader(installedPackages.length, 'installed'); 25 | 26 | const prompt = createPackagePrompt(installedPackages, { 27 | message: 'Search and select a package:' 28 | }); 29 | const answer = await inquirer.prompt<{ selectedPackage: ResolvedPackage }>([prompt]); 30 | 31 | if (!answer.selectedPackage) { 32 | return; 33 | } 34 | 35 | const action = await displayPackageDetailsWithActions(answer.selectedPackage); 36 | await handlePackageAction(answer.selectedPackage, action, { 37 | onUninstall: () => listInstalledPackages(), 38 | onBack: listInstalledPackages 39 | }); 40 | } -------------------------------------------------------------------------------- /src/commands/uninstall.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import inquirer from 'inquirer'; 3 | import { resolvePackage } from '../utils/package-resolver.js'; 4 | import { uninstallPackage } from '../utils/package-management.js'; 5 | 6 | export async function uninstall(packageName?: string): Promise { 7 | console.error("!"); 8 | try { 9 | // If no package name provided, show error 10 | if (!packageName) { 11 | console.error(chalk.red('Error: Package name is required')); 12 | console.log('Usage: mcp-get uninstall '); 13 | process.exit(1); 14 | } 15 | 16 | // Resolve the package 17 | const pkg = resolvePackage(packageName); 18 | if (!pkg) { 19 | console.log(chalk.yellow(`Package ${packageName} not found.`)); 20 | return; 21 | } 22 | 23 | if (!pkg.isInstalled) { 24 | console.log(chalk.yellow(`Package ${packageName} is not installed.`)); 25 | return; 26 | } 27 | 28 | // Confirm uninstallation 29 | const { confirmUninstall } = await inquirer.prompt<{ confirmUninstall: boolean }>([{ 30 | type: 'confirm', 31 | name: 'confirmUninstall', 32 | message: `Are you sure you want to uninstall ${packageName}?`, 33 | default: false 34 | }]); 35 | 36 | if (!confirmUninstall) { 37 | console.log('Uninstallation cancelled.'); 38 | return; 39 | } 40 | 41 | // Perform uninstallation 42 | await uninstallPackage(packageName); 43 | console.log(chalk.green(`\nSuccessfully uninstalled ${packageName}`)); 44 | console.log(chalk.yellow('\nNote: Please restart Claude for the changes to take effect.')); 45 | 46 | } catch (error) { 47 | console.error(chalk.red('Failed to uninstall package:')); 48 | console.error(chalk.red(error instanceof Error ? error.message : String(error))); 49 | process.exit(1); 50 | } 51 | } -------------------------------------------------------------------------------- /src/__tests__/pr-check.test.js: -------------------------------------------------------------------------------- 1 | import { jest, describe, it, expect } from '@jest/globals'; 2 | import { fileURLToPath } from 'url'; 3 | import path from 'path'; 4 | 5 | const prCheckPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../scripts/pr-check.js'); 6 | 7 | jest.mock(prCheckPath, () => { 8 | const originalModule = jest.requireActual(prCheckPath); 9 | return { 10 | ...originalModule, 11 | default: jest.fn(), 12 | }; 13 | }); 14 | 15 | const { normalizePackageName, getPackageFilename } = await import(prCheckPath); 16 | 17 | describe('PR Check Utilities', () => { 18 | describe('normalizePackageName', () => { 19 | it('should normalize npm package names correctly', () => { 20 | expect(normalizePackageName('Test Package', 'node')).toBe('test-package'); 21 | expect(normalizePackageName('Test_Package', 'node')).toBe('test_package'); 22 | expect(normalizePackageName('Test-Package', 'node')).toBe('test-package'); 23 | expect(normalizePackageName('@scope/package', 'node')).toBe('scope-package'); 24 | expect(normalizePackageName('Bazi MCP', 'node')).toBe('bazi-mcp'); 25 | }); 26 | 27 | it('should normalize Python package names correctly', () => { 28 | expect(normalizePackageName('Test Package', 'python')).toBe('test-package'); 29 | expect(normalizePackageName('Test_Package', 'python')).toBe('test-package'); 30 | expect(normalizePackageName('Test-Package', 'python')).toBe('test-package'); 31 | }); 32 | 33 | it('should handle empty or undefined names', () => { 34 | expect(normalizePackageName('', 'node')).toBe(''); 35 | expect(normalizePackageName(undefined, 'node')).toBe(''); 36 | }); 37 | }); 38 | 39 | describe('getPackageFilename', () => { 40 | it('should convert package names to safe filenames', () => { 41 | expect(getPackageFilename('test-package')).toBe('test-package'); 42 | expect(getPackageFilename('@scope/package')).toBe('scope--package'); 43 | expect(getPackageFilename('org/package')).toBe('org--package'); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /.github/workflows/update-packages.yml: -------------------------------------------------------------------------------- 1 | name: Update Package List 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' # Runs daily at midnight 6 | workflow_dispatch: # Allows manual triggering 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | update-packages: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Check out repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '18' 24 | 25 | - name: Install dependencies 26 | run: npm install 27 | 28 | - name: Run package extractor 29 | run: npm run extract 30 | 31 | - name: Check for changes 32 | id: git-check 33 | run: | 34 | git diff --exit-code packages/package-list.json || echo "changes=true" >> $GITHUB_OUTPUT 35 | 36 | - name: Read commit message 37 | id: commit-msg 38 | if: steps.git-check.outputs.changes == 'true' 39 | run: | 40 | if [ -f "temp/commit-msg.txt" ]; then 41 | MSG=$(cat temp/commit-msg.txt | sed 's/\\n/\n/g') 42 | echo "message<> $GITHUB_OUTPUT 43 | echo "$MSG" >> $GITHUB_OUTPUT 44 | echo "EOF" >> $GITHUB_OUTPUT 45 | else 46 | echo "message=chore(packages): update MCP package list" >> $GITHUB_OUTPUT 47 | fi 48 | 49 | - name: Remove temp files 50 | run: rm -rf temp 51 | 52 | - name: Create Pull Request 53 | if: steps.git-check.outputs.changes == 'true' 54 | uses: peter-evans/create-pull-request@v5 55 | with: 56 | token: ${{ secrets.GITHUB_TOKEN }} 57 | commit-message: | 58 | ${{ steps.commit-msg.outputs.message }} 59 | title: 'Update MCP Package List' 60 | body: | 61 | ${{ steps.commit-msg.outputs.message }} 62 | 63 | This PR was automatically generated by the package update workflow. 64 | branch: update-package-list 65 | base: main 66 | delete-branch: true 67 | labels: | 68 | automated pr 69 | automerge 70 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { list } from './commands/list.js'; 4 | import { install } from './commands/install.js'; 5 | import { uninstall } from './commands/uninstall.js'; 6 | import { listInstalledPackages } from './commands/installed.js'; 7 | import { updatePackage } from './auto-update.js'; 8 | import chalk from 'chalk'; 9 | 10 | const command = process.argv[2]; 11 | const packageName = process.argv[3]; 12 | const version = process.argv[4]; // Add support for version parameter 13 | 14 | async function main() { 15 | // Show deprecation notice 16 | console.log(chalk.yellow('⚠️ NOTICE: mcp-get is deprecated and no longer actively maintained.')); 17 | console.log(chalk.cyan('We recommend using Smithery for MCP server management: https://smithery.ai')); 18 | console.log(chalk.gray('This tool will continue to work but will not receive updates.\n')); 19 | 20 | switch (command) { 21 | case 'list': 22 | await list(); 23 | break; 24 | case 'install': 25 | if (!packageName) { 26 | console.error('Please provide a package name to install'); 27 | process.exit(1); 28 | } 29 | await install(packageName, version); // Pass version to install function 30 | break; 31 | case 'uninstall': 32 | await uninstall(packageName); 33 | break; 34 | case 'installed': 35 | await listInstalledPackages(); 36 | break; 37 | case 'update': 38 | await updatePackage(); 39 | break; 40 | default: 41 | console.log('Available commands:'); 42 | console.log(' list List all available packages'); 43 | console.log(' install [version] Install a package with optional version'); 44 | console.log(' uninstall [package] Uninstall a package'); 45 | console.log(' installed List installed packages'); 46 | console.log(' update Update mcp-get to latest version'); 47 | console.log(''); 48 | console.log('Options:'); 49 | console.log(' --ci Skip interactive prompts (for CI environments)'); 50 | console.log(' --restart-claude Automatically restart Claude without prompting'); 51 | process.exit(1); 52 | } 53 | } 54 | 55 | main(); 56 | -------------------------------------------------------------------------------- /src/utils/display.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { ResolvedPackage } from '../types/package.js'; 3 | import inquirer from 'inquirer'; 4 | 5 | export async function displayPackageDetailsWithActions(pkg: ResolvedPackage): Promise<'install' | 'uninstall' | 'open' | 'back' | 'exit'> { 6 | console.log('\n' + chalk.bold.cyan('Package Details:')); 7 | console.log(chalk.bold('Name: ') + pkg.name); 8 | console.log(chalk.bold('Description: ') + pkg.description); 9 | console.log(chalk.bold('Vendor: ') + pkg.vendor); 10 | console.log(chalk.bold('License: ') + pkg.license); 11 | console.log(chalk.bold('Runtime: ') + (pkg.runtime || 'node')); 12 | console.log(chalk.bold('Source: ') + (pkg.sourceUrl || 'Not available')); 13 | console.log(chalk.bold('Homepage: ') + (pkg.homepage || 'Not available')); 14 | console.log(chalk.bold('Status: ') + (pkg.isInstalled ? chalk.green('Installed') : 'Not installed') + 15 | (pkg.isVerified ? '' : chalk.yellow(' (Unverified package)'))); 16 | 17 | // Display environment variables if available 18 | if (pkg.environmentVariables && Object.keys(pkg.environmentVariables).length > 0) { 19 | console.log(chalk.bold('\nEnvironment Variables:')); 20 | for (const [key, value] of Object.entries(pkg.environmentVariables)) { 21 | console.log(chalk.bold(` ${key}: `) + 22 | value.description + 23 | (value.required ? chalk.red(' (Required)') : chalk.gray(' (Optional)'))); 24 | } 25 | console.log(''); // Add an extra line after environment variables 26 | } 27 | 28 | const choices = [ 29 | { name: pkg.isInstalled ? '🔄 Reinstall this package' : '📦 Install this package', value: 'install' }, 30 | ...(pkg.isInstalled ? [{ name: '🗑️ Uninstall this package', value: 'uninstall' }] : []), 31 | ...(pkg.sourceUrl ? [{ name: '🔗 Open source URL', value: 'open' }] : []), 32 | { name: '⬅️ Back to list', value: 'back' }, 33 | { name: '❌ Exit', value: 'exit' } 34 | ]; 35 | 36 | const { action } = await inquirer.prompt<{ action: 'install' | 'uninstall' | 'open' | 'back' | 'exit' }>([ 37 | { 38 | type: 'list', 39 | name: 'action', 40 | message: 'What would you like to do?', 41 | choices 42 | } 43 | ]); 44 | 45 | return action; 46 | } -------------------------------------------------------------------------------- /src/install.ts: -------------------------------------------------------------------------------- 1 | import { Package } from './types/index.js'; 2 | import { installPackage as installPkg } from './utils/package-management.js'; 3 | import inquirer from 'inquirer'; 4 | import chalk from 'chalk'; 5 | import { loadPackage } from './utils/package-registry.js'; 6 | 7 | async function promptForRuntime(): Promise<'node' | 'python' | 'go'> { 8 | const { runtime } = await inquirer.prompt<{ runtime: 'node' | 'python' | 'go' }>([ 9 | { 10 | type: 'list', 11 | name: 'runtime', 12 | message: 'What runtime does this package use?', 13 | choices: [ 14 | { name: 'Node.js', value: 'node' }, 15 | { name: 'Python', value: 'python' }, 16 | { name: 'Go', value: 'go' } 17 | ] 18 | } 19 | ]); 20 | return runtime; 21 | } 22 | 23 | function createUnknownPackage(packageName: string, runtime: 'node' | 'python' | 'go'): Package { 24 | return { 25 | name: packageName, 26 | description: 'Unverified package', 27 | runtime, 28 | vendor: '', 29 | sourceUrl: '', 30 | homepage: '', 31 | license: '' 32 | }; 33 | } 34 | 35 | export async function installPackage(pkg: Package): Promise { 36 | return installPkg(pkg); 37 | } 38 | 39 | export async function install(packageName: string): Promise { 40 | const pkg = loadPackage(packageName); 41 | 42 | if (!pkg) { 43 | console.warn(chalk.yellow(`Package ${packageName} not found in the curated list.`)); 44 | 45 | const { proceedWithInstall } = await inquirer.prompt<{ proceedWithInstall: boolean }>([ 46 | { 47 | type: 'confirm', 48 | name: 'proceedWithInstall', 49 | message: `Would you like to try installing ${packageName} anyway? This package hasn't been verified.`, 50 | default: false 51 | } 52 | ]); 53 | 54 | if (proceedWithInstall) { 55 | console.log(chalk.cyan(`Proceeding with installation of ${packageName}...`)); 56 | 57 | // Prompt for runtime for unverified packages 58 | const runtime = await promptForRuntime(); 59 | 60 | // Create a basic package object for unverified packages 61 | const unknownPkg = createUnknownPackage(packageName, runtime); 62 | await installPkg(unknownPkg); 63 | } else { 64 | console.log('Installation cancelled.'); 65 | process.exit(1); 66 | } 67 | return; 68 | } 69 | 70 | await installPkg(pkg); 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/ui.ts: -------------------------------------------------------------------------------- 1 | import { ResolvedPackage } from '../types/package.js'; 2 | import inquirer from 'inquirer'; 3 | import fuzzy from 'fuzzy'; 4 | import chalk from 'chalk'; 5 | 6 | export interface PackageChoice { 7 | name: string; 8 | value: ResolvedPackage; 9 | short: string; 10 | } 11 | 12 | export function formatPackageChoice(pkg: ResolvedPackage, showInstallStatus = false): PackageChoice { 13 | const prefix = showInstallStatus ? (pkg.isInstalled ? '✓ ' : ' ') : ''; 14 | return { 15 | name: `${prefix}${pkg.name.padEnd(showInstallStatus ? 22 : 24)} │ ${ 16 | pkg.description.length > 47 ? `${pkg.description.slice(0, 44)}...` : pkg.description.padEnd(49) 17 | } │ ${pkg.vendor.padEnd(19)} │ ${pkg.license.padEnd(14)}`, 18 | value: pkg, 19 | short: pkg.name 20 | }; 21 | } 22 | 23 | export function createPackagePrompt(packages: ResolvedPackage[], options: { 24 | message?: string; 25 | showInstallStatus?: boolean; 26 | } = {}) { 27 | const choices = packages.map(pkg => formatPackageChoice(pkg, options.showInstallStatus)); 28 | 29 | return { 30 | type: 'autocomplete', 31 | name: 'selectedPackage', 32 | message: options.message || 'Search and select a package:', 33 | source: async (_answersSoFar: any, input: string) => { 34 | if (!input) return choices; 35 | 36 | return fuzzy 37 | .filter(input.toLowerCase(), choices, { 38 | extract: (choice) => `${choice.value.name} ${choice.value.description} ${choice.value.vendor}`.toLowerCase() 39 | }) 40 | .map(result => result.original); 41 | }, 42 | pageSize: 10 43 | }; 44 | } 45 | 46 | export async function confirmUninstall(packageName: string): Promise { 47 | const { confirmUninstall } = await inquirer.prompt<{ confirmUninstall: boolean }>([ 48 | { 49 | type: 'confirm', 50 | name: 'confirmUninstall', 51 | message: `Are you sure you want to uninstall ${packageName}?`, 52 | default: false 53 | } 54 | ]); 55 | return confirmUninstall; 56 | } 57 | 58 | export function printPackageListHeader(count: number, type: 'all' | 'installed' = 'all') { 59 | console.log(chalk.bold.cyan('\n📦 ' + (type === 'installed' ? 'Installed Packages' : 'Available Packages'))); 60 | console.log(chalk.gray(`Found ${count} ${type === 'installed' ? 'installed ' : ''}packages\n`)); 61 | } -------------------------------------------------------------------------------- /src/utils/package-actions.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { ResolvedPackage } from '../types/package.js'; 3 | import { displayPackageDetailsWithActions } from './display.js'; 4 | import { installPackage, uninstallPackage } from './package-management.js'; 5 | import { confirmUninstall } from './ui.js'; 6 | 7 | export type ActionHandler = { 8 | onInstall?: (pkg: ResolvedPackage) => Promise; 9 | onUninstall?: (pkg: ResolvedPackage) => Promise; 10 | onBack?: () => Promise; 11 | }; 12 | 13 | export async function handlePackageAction( 14 | pkg: ResolvedPackage, 15 | action: string, 16 | handlers: ActionHandler, 17 | showActionsAfter = true 18 | ): Promise { 19 | switch (action) { 20 | case 'install': 21 | console.log(chalk.cyan(`\nPreparing to install ${pkg.name}...`)); 22 | await installPackage(pkg); 23 | pkg.isInstalled = true; 24 | if (handlers.onInstall) { 25 | await handlers.onInstall(pkg); 26 | } 27 | break; 28 | case 'uninstall': 29 | if (await confirmUninstall(pkg.name)) { 30 | await uninstallPackage(pkg.name); 31 | console.log(chalk.green(`Successfully uninstalled ${pkg.name}`)); 32 | pkg.isInstalled = false; 33 | if (handlers.onUninstall) { 34 | await handlers.onUninstall(pkg); 35 | return; // Don't show actions after uninstall if handler provided 36 | } 37 | } else { 38 | console.log('Uninstallation cancelled.'); 39 | } 40 | break; 41 | case 'open': 42 | if (pkg.sourceUrl) { 43 | const open = (await import('open')).default; 44 | await open(pkg.sourceUrl); 45 | console.log(chalk.green(`\nOpened ${pkg.sourceUrl} in your browser`)); 46 | } else { 47 | console.log(chalk.yellow('\nNo source URL available for this package')); 48 | } 49 | break; 50 | case 'back': 51 | if (handlers.onBack) { 52 | await handlers.onBack(); 53 | } 54 | return; 55 | case 'exit': 56 | process.exit(0); 57 | } 58 | 59 | // Show actions again after completing an action (except for exit/back) 60 | if (showActionsAfter) { 61 | const nextAction = await displayPackageDetailsWithActions(pkg); 62 | await handlePackageAction(pkg, nextAction, handlers); 63 | } 64 | } -------------------------------------------------------------------------------- /src/auto-update.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import { readFileSync } from 'fs'; 4 | import { resolve, dirname } from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | import chalk from 'chalk'; 7 | 8 | const execAsync = promisify(exec); 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | 13 | async function getCurrentVersion(): Promise { 14 | const packageJsonPath = resolve(__dirname, '../package.json'); 15 | const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); 16 | return packageJson.version; 17 | } 18 | 19 | async function getLatestVersion(): Promise { 20 | const { stdout } = await execAsync('npm show @michaellatman/mcp-get version'); 21 | return stdout.trim(); 22 | } 23 | 24 | export async function updatePackage(silent: boolean = false): Promise { 25 | try { 26 | const currentVersion = await getCurrentVersion(); 27 | const latestVersion = await getLatestVersion(); 28 | 29 | if (currentVersion !== latestVersion) { 30 | if (!silent) { 31 | console.log(chalk.yellow(`\nA new version of mcp-get is available: ${latestVersion} (current: ${currentVersion})`)); 32 | console.log(chalk.cyan('Installing update...')); 33 | } 34 | 35 | try { 36 | const { stdout, stderr } = await execAsync('npm install -g @michaellatman/mcp-get@latest'); 37 | if (!silent) { 38 | if (stdout) console.log(stdout); 39 | if (stderr) console.error(chalk.yellow('Update process output:'), stderr); 40 | console.log(chalk.green('✓ Update complete\n')); 41 | } 42 | } catch (installError: any) { 43 | if (!silent) { 44 | console.error(chalk.red('Failed to install update:'), installError.message); 45 | if (installError.stdout) console.log('stdout:', installError.stdout); 46 | if (installError.stderr) console.error('stderr:', installError.stderr); 47 | console.error(chalk.yellow('Try running the update manually with sudo:')); 48 | console.error(chalk.cyan(' sudo npm install -g @michaellatman/mcp-get@latest')); 49 | } 50 | return; 51 | } 52 | } else { 53 | if (!silent) console.log(chalk.green('✓ mcp-get is already up to date\n')); 54 | } 55 | } catch (error: any) { 56 | if (!silent) { 57 | console.error(chalk.red('Failed to check for updates:'), error.message); 58 | if (error.stdout) console.log('stdout:', error.stdout); 59 | if (error.stderr) console.error('stderr:', error.stderr); 60 | } 61 | return; 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/commands/install.ts: -------------------------------------------------------------------------------- 1 | import { Package } from '../types/package.js'; 2 | import { installPackage as installPkg } from '../utils/package-management.js'; 3 | import inquirer from 'inquirer'; 4 | import chalk from 'chalk'; 5 | import { resolvePackages } from '../utils/package-resolver.js'; 6 | 7 | async function promptForRuntime(): Promise<'node' | 'python' | 'go'> { 8 | const { runtime } = await inquirer.prompt<{ runtime: 'node' | 'python' | 'go' }>([ 9 | { 10 | type: 'list', 11 | name: 'runtime', 12 | message: 'What runtime does this package use?', 13 | choices: [ 14 | { name: 'Node.js', value: 'node' }, 15 | { name: 'Python', value: 'python' }, 16 | { name: 'Go', value: 'go' } 17 | ] 18 | } 19 | ]); 20 | return runtime; 21 | } 22 | 23 | function createUnknownPackage(packageName: string, runtime: 'node' | 'python' | 'go', version?: string): Package { 24 | return { 25 | name: packageName, 26 | description: 'Unverified package', 27 | runtime, 28 | vendor: '', 29 | sourceUrl: '', 30 | homepage: '', 31 | license: '', 32 | version 33 | }; 34 | } 35 | 36 | export async function installPackage(pkg: Package): Promise { 37 | return installPkg(pkg); 38 | } 39 | 40 | export async function install(packageName: string, version?: string): Promise { 41 | const packages = resolvePackages(); 42 | const pkg = packages.find(p => p.name === packageName); 43 | 44 | if (!pkg) { 45 | console.warn(chalk.yellow(`Package ${packageName} not found in the curated list.`)); 46 | 47 | const { proceedWithInstall } = await inquirer.prompt<{ proceedWithInstall: boolean }>([ 48 | { 49 | type: 'confirm', 50 | name: 'proceedWithInstall', 51 | message: `Would you like to try installing ${packageName}${version ? ` version ${version}` : ''} anyway? This package hasn't been verified.`, 52 | default: false 53 | } 54 | ]); 55 | 56 | if (proceedWithInstall) { 57 | console.log(chalk.cyan(`Proceeding with installation of ${packageName}${version ? ` version ${version}` : ''}...`)); 58 | 59 | // Prompt for runtime for unverified packages 60 | const runtime = await promptForRuntime(); 61 | 62 | // Create a basic package object for unverified packages 63 | const unknownPkg = createUnknownPackage(packageName, runtime, version); 64 | await installPkg(unknownPkg); 65 | } else { 66 | console.log('Installation cancelled.'); 67 | process.exit(1); 68 | } 69 | return; 70 | } 71 | 72 | if (version) { 73 | pkg.version = version; 74 | } 75 | 76 | await installPkg(pkg); 77 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@michaellatman/mcp-get", 3 | "version": "1.0.116", 4 | "description": "[DEPRECATED] We recommend Smithery (https://smithery.ai) - A NPX command to install and list packages", 5 | "deprecated": "This package is no longer maintained. We recommend Smithery at https://smithery.ai for MCP server management.", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "build": "tsc && chmod +x dist/index.js", 9 | "start": "node dist/index.js", 10 | "test": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.mjs", 11 | "test:file": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.mjs", 12 | "test:list": "node --loader ts-node/esm src/index.ts list", 13 | "test:install": "node --loader ts-node/esm src/index.ts install", 14 | "test:installed": "node --loader ts-node/esm src/index.ts installed", 15 | "test:update": "node --loader ts-node/esm src/index.ts update", 16 | "extract": "node --loader ts-node/esm src/extractors/modelcontextprotocol-extractor.ts", 17 | "test:uninstall": "node --loader ts-node/esm src/index.ts uninstall", 18 | "version:patch": "npm version patch", 19 | "version:minor": "npm version minor", 20 | "version:major": "npm version major", 21 | "publish:npm": "npm run build && npm publish --access public", 22 | "pr-check": "node src/scripts/pr-check.js", 23 | "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.mjs --watch", 24 | "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.mjs --coverage", 25 | "prepare": "npm run build", 26 | "registry:convert": "node src/scripts/convert-packages.js", 27 | "registry:add": "node src/scripts/add-package.js", 28 | "lint": "eslint . --ext .ts", 29 | "lint:fix": "eslint . --ext .ts --fix" 30 | }, 31 | "bin": { 32 | "mcp-get": "dist/index.js" 33 | }, 34 | "dependencies": { 35 | "@iarna/toml": "^2.2.5", 36 | "@octokit/rest": "^18.12.0", 37 | "@types/iarna__toml": "^2.0.5", 38 | "chalk": "^4.1.2", 39 | "cli-table3": "^0.6.5", 40 | "dotenv": "^16.4.5", 41 | "fuzzy": "^0.1.3", 42 | "inquirer": "^8.2.4", 43 | "inquirer-autocomplete-prompt": "^2.0.0", 44 | "open": "^10.1.0", 45 | "string-width": "^4.2.3", 46 | "typescript": "^4.0.0" 47 | }, 48 | "devDependencies": { 49 | "@types/inquirer": "^8.2.4", 50 | "@types/inquirer-autocomplete-prompt": "^3.0.3", 51 | "@types/jest": "^29.5.14", 52 | "@types/node": "^14.18.63", 53 | "@typescript-eslint/eslint-plugin": "^8.29.0", 54 | "@typescript-eslint/parser": "^8.29.0", 55 | "eslint": "^9.23.0", 56 | "jest": "^29.7.0", 57 | "ts-jest": "^29.2.5", 58 | "ts-node": "^10.9.1" 59 | }, 60 | "files": [ 61 | "dist", 62 | "README.md", 63 | "package.json", 64 | "packages", 65 | "LICENSE" 66 | ], 67 | "type": "module", 68 | "exports": { 69 | ".": { 70 | "import": "./dist/index.js" 71 | } 72 | }, 73 | "license": "MIT" 74 | } 75 | -------------------------------------------------------------------------------- /src/scripts/pr-check.test.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import { validateRequiredFields, getNewPackages } from './pr-check.js'; 3 | 4 | describe('validateRequiredFields', () => { 5 | test('accepts valid GitHub URLs', () => { 6 | const pkg = { 7 | name: 'test-package', 8 | description: 'Test package', 9 | vendor: 'Test Vendor', 10 | sourceUrl: 'https://github.com/owner/repo', 11 | homepage: 'https://github.com/owner/repo', 12 | license: 'MIT', 13 | runtime: 'node' 14 | }; 15 | expect(() => validateRequiredFields(pkg)).not.toThrow(); 16 | }); 17 | 18 | test('rejects invalid URLs without protocol', () => { 19 | const pkg = { 20 | name: 'test-package', 21 | description: 'Test package', 22 | vendor: 'Test Vendor', 23 | sourceUrl: 'github.com/owner/repo', 24 | homepage: 'https://github.com/owner/repo', 25 | license: 'MIT', 26 | runtime: 'node' 27 | }; 28 | expect(() => validateRequiredFields(pkg)).toThrow(/invalid sourceUrl URL/); 29 | }); 30 | 31 | test('accepts both http and https protocols', () => { 32 | const pkg = { 33 | name: 'test-package', 34 | description: 'Test package', 35 | vendor: 'Test Vendor', 36 | sourceUrl: 'http://github.com/owner/repo', 37 | homepage: 'https://github.com/owner/repo', 38 | license: 'MIT', 39 | runtime: 'node' 40 | }; 41 | expect(() => validateRequiredFields(pkg)).not.toThrow(); 42 | }); 43 | }); 44 | 45 | describe('getNewPackages', () => { 46 | beforeEach(() => { 47 | jest.clearAllMocks(); 48 | }); 49 | 50 | it('should correctly parse package with URL from git diff', () => { 51 | const mockDiff = ` 52 | + { 53 | + "name": "test-package", 54 | + "description": "Test Package", 55 | + "vendor": "test", 56 | + "sourceUrl": "https://github.com/test/repo", 57 | + "homepage": "https://github.com/test/repo", 58 | + "license": "MIT", 59 | + "runtime": "python" 60 | + }`; 61 | 62 | execSync.mockReturnValue(Buffer.from(mockDiff)); 63 | 64 | const packages = getNewPackages({}); 65 | expect(packages).toHaveLength(1); 66 | expect(packages[0].sourceUrl).toBe('https://github.com/test/repo'); 67 | }); 68 | 69 | it('should handle multiple packages in diff', () => { 70 | const mockDiff = ` 71 | + { 72 | + "name": "package1", 73 | + "sourceUrl": "https://github.com/test/repo1" 74 | + }, 75 | + { 76 | + "name": "package2", 77 | + "sourceUrl": "https://github.com/test/repo2" 78 | + }`; 79 | 80 | execSync.mockReturnValue(Buffer.from(mockDiff)); 81 | 82 | const packages = getNewPackages({}); 83 | expect(packages).toHaveLength(2); 84 | expect(packages[0].sourceUrl).toBe('https://github.com/test/repo1'); 85 | expect(packages[1].sourceUrl).toBe('https://github.com/test/repo2'); 86 | }); 87 | 88 | it('should handle invalid JSON gracefully', () => { 89 | const mockDiff = ` 90 | + { 91 | + "name": "broken-package", 92 | + "sourceUrl": "https://github.com/test/repo" 93 | + invalid-json-here 94 | + }`; 95 | 96 | execSync.mockReturnValue(Buffer.from(mockDiff)); 97 | 98 | const packages = getNewPackages({}); 99 | expect(packages).toHaveLength(0); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as os from 'os'; 4 | import { fileURLToPath } from 'url'; 5 | import { dirname } from 'path'; 6 | import { exec } from 'child_process'; 7 | import { promisify } from 'util'; 8 | import { loadPackage } from './package-registry.js'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | 13 | const execAsync = promisify(exec); 14 | 15 | export interface MCPServerConfig { 16 | command: string; 17 | args: string[]; 18 | env?: Record; 19 | runtime?: 'node' | 'python' | 'go'; 20 | } 21 | 22 | export interface ClaudeConfig { 23 | mcpServers?: Record; 24 | [key: string]: any; 25 | } 26 | 27 | function getPackageRuntime(packageName: string): 'node' | 'python' | 'go' { 28 | const pkg = loadPackage(packageName); 29 | return pkg?.runtime || 'node'; 30 | } 31 | 32 | export function getConfigPath(): string { 33 | if (process.platform === 'win32') { 34 | return path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json'); 35 | } 36 | 37 | const configDir = path.join(os.homedir(), 'Library', 'Application Support', 'Claude'); 38 | return path.join(configDir, 'claude_desktop_config.json'); 39 | } 40 | 41 | export function readConfig(): ClaudeConfig { 42 | const configPath = getConfigPath(); 43 | if (!fs.existsSync(configPath)) { 44 | return {}; 45 | } 46 | return JSON.parse(fs.readFileSync(configPath, 'utf8')); 47 | } 48 | 49 | export function writeConfig(config: ClaudeConfig): void { 50 | const configPath = getConfigPath(); 51 | const configDir = path.dirname(configPath); 52 | 53 | if (!fs.existsSync(configDir)) { 54 | fs.mkdirSync(configDir, { recursive: true }); 55 | } 56 | 57 | fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); 58 | } 59 | 60 | export async function installMCPServer(packageName: string, envVars?: Record, runtime?: 'node' | 'python' | 'go'): Promise { 61 | const config = readConfig(); 62 | const serverName = packageName.replace(/\//g, '-'); 63 | 64 | const effectiveRuntime = runtime || getPackageRuntime(packageName); 65 | 66 | if (!config.mcpServers) { 67 | config.mcpServers = {}; 68 | } 69 | 70 | let command = 'npx'; 71 | if (effectiveRuntime === 'python') { 72 | try { 73 | const { stdout } = await execAsync('which uvx'); 74 | command = stdout.trim(); 75 | } catch (error) { 76 | command = 'uvx'; // Fallback to just 'uvx' if which fails 77 | } 78 | } else if (effectiveRuntime === 'go') { 79 | command = 'go'; 80 | } 81 | 82 | const serverConfig: MCPServerConfig = { 83 | runtime: effectiveRuntime, 84 | env: envVars, 85 | command, 86 | args: effectiveRuntime === 'python' ? [packageName] : 87 | effectiveRuntime === 'go' ? ['run', packageName] : 88 | ['-y', packageName] 89 | }; 90 | 91 | config.mcpServers[serverName] = serverConfig; 92 | writeConfig(config); 93 | } 94 | 95 | export function envVarsToArgs(envVars: Record): string[] { 96 | return Object.entries(envVars).map(([key, value]) => { 97 | const argName = key.toLowerCase().replace(/_/g, '-'); 98 | return [`--${argName}`, value]; 99 | }).flat(); 100 | } -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md for mcp-get 2 | 3 | ## Build/Lint/Test Commands 4 | - Build: `npm run build` 5 | - Test all: `npm test` 6 | - Test single file: `npm run test:file src/__tests__/file.test.ts` 7 | - Test with watch: `npm run test:watch` 8 | - Test coverage: `npm run test:coverage` 9 | - PR check: `npm run pr-check` 10 | - Registry convert: `npm run registry:convert` 11 | - Add package to registry: `npm run registry:add ` 12 | 13 | ## Code Style Guidelines 14 | - **Module System**: ES Modules with `.js` extension in imports 15 | - **Formatting**: 2-space indentation, semicolons, single quotes 16 | - **Naming**: camelCase for functions/variables, PascalCase for interfaces/classes, kebab-case for files 17 | - **Types**: Explicit typing for functions, parameters, and returns; interfaces in dedicated files 18 | - **Error Handling**: Use try/catch blocks with appropriate console.error messages and error propagation 19 | - **Testing**: Jest with mocks for external dependencies; test files in `__tests__` directories with `.test.ts` extension 20 | - **Imports**: Group by external packages first, then internal modules 21 | - **Async**: Use async/await pattern for asynchronous code 22 | - **CLI Commands**: Follow command pattern with consistent structure in `commands/` directory 23 | - **Configuration**: Use `ConfigManager` for managing persistent configurations 24 | 25 | ## Testing Guidelines 26 | - Use proper TypeScript interfaces for mocks (with mockReturnValueOnce, mockResolvedValueOnce, etc.) 27 | - When testing CLI commands, mock all dependencies including inquirer, chalk, and utils 28 | - Use `as unknown as MockType` pattern for properly typing Jest mocks 29 | - For interactive CLI testing, mock the inquirer.prompt response values 30 | 31 | ## Package Registry Structure 32 | - Packages are stored as individual JSON files in the `packages/` directory 33 | - Each file follows the naming convention: `packageName.json` (scoped packages use `scope--name.json`) 34 | - Package files contain metadata and environment variables 35 | - To add a new package: 36 | 1. Create a JSON file with the package metadata 37 | 2. Use `npm run registry:add ` to validate and add it to the registry 38 | 3. Always run `npm run pr-check` before submitting a PR to validate package structure 39 | - Environment variables should be included in the package file under the `environmentVariables` key 40 | - The system will automatically find all packages by scanning the directory 41 | 42 | ## PR Guidelines for Package Additions 43 | - Always run `npm run pr-check` before opening a PR to validate packages 44 | - Package files must follow the standard format with all required fields 45 | - For scoped packages (e.g., `@scope/name`), use double hyphens in filename (e.g., `scope--name.json`) 46 | - Include an `environmentVariables` object even when empty 47 | - Verify package name matches sourceUrl owner/format 48 | - Ensure the license is correctly specified 49 | - Maintain proper runtime specification (node/python) 50 | 51 | ### Converting from Old Format 52 | - When a PR uses the old package-list.json format: 53 | 1. Convert to individual package files in packages/ directory 54 | 2. Follow proper naming convention (especially for scoped packages) 55 | 3. Preserve all metadata from original PR exactly 56 | 4. Ensure file format matches existing packages 57 | 5. Reference the original PR and explain the reformatting in your new PR 58 | 6. Close the original PR with a reference to your new PR -------------------------------------------------------------------------------- /src/__tests__/auto-update.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import { describe, it, expect, beforeEach } from '@jest/globals'; 3 | import type { ExecOptions, ChildProcess, ExecException } from 'child_process'; 4 | 5 | // Type definitions 6 | type ExecResult = { stdout: string; stderr: string }; 7 | 8 | // Setup mocks 9 | const mockExecPromise = jest.fn().mockName('execPromise') as jest.MockedFunction< 10 | (command: string) => Promise 11 | >; 12 | 13 | // Create a properly typed mock for exec 14 | const mockExec = jest.fn(( 15 | command: string, 16 | options: ExecOptions | undefined | null, 17 | callback?: (error: ExecException | null, stdout: string, stderr: string) => void 18 | ): ChildProcess => { 19 | return { 20 | on: jest.fn(), 21 | stdout: { on: jest.fn() }, 22 | stderr: { on: jest.fn() } 23 | } as unknown as ChildProcess; 24 | }); 25 | 26 | // Mock chalk module 27 | await jest.unstable_mockModule('chalk', () => ({ 28 | default: { 29 | yellow: jest.fn(str => str), 30 | cyan: jest.fn(str => str), 31 | green: jest.fn(str => str), 32 | red: jest.fn(str => str), 33 | } 34 | })); 35 | 36 | // Mock child_process module 37 | await jest.unstable_mockModule('child_process', () => ({ 38 | exec: mockExec 39 | })); 40 | 41 | // Mock util module 42 | await jest.unstable_mockModule('util', () => ({ 43 | promisify: jest.fn(() => mockExecPromise) 44 | })); 45 | 46 | // Mock fs module 47 | await jest.unstable_mockModule('fs', () => ({ 48 | readFileSync: jest.fn(() => JSON.stringify({ version: '1.0.48' })) 49 | })); 50 | 51 | // Import after mocking 52 | const { updatePackage } = await import('../auto-update.js'); 53 | 54 | // Helper to create exec result 55 | const createExecResult = (stdout: string, stderr: string = ''): ExecResult => ({ stdout, stderr }); 56 | 57 | describe('updatePackage', () => { 58 | beforeEach(() => { 59 | jest.clearAllMocks(); 60 | jest.spyOn(console, 'log').mockImplementation(() => {}); 61 | jest.spyOn(console, 'error').mockImplementation(() => {}); 62 | }); 63 | 64 | afterEach(() => { 65 | jest.restoreAllMocks(); 66 | }); 67 | 68 | it('should check for updates and install if available', async () => { 69 | mockExecPromise 70 | .mockResolvedValueOnce(createExecResult('1.0.50\n')) 71 | .mockResolvedValueOnce(createExecResult('success')); 72 | 73 | await updatePackage(); 74 | 75 | expect(mockExecPromise).toHaveBeenNthCalledWith(1, 'npm show @michaellatman/mcp-get version'); 76 | expect(mockExecPromise).toHaveBeenNthCalledWith(2, 'npm install -g @michaellatman/mcp-get@latest'); 77 | 78 | expect(console.log).toHaveBeenNthCalledWith(1, 79 | '\nA new version of mcp-get is available: 1.0.50 (current: 1.0.48)' 80 | ); 81 | expect(console.log).toHaveBeenNthCalledWith(2, 82 | 'Installing update...' 83 | ); 84 | expect(console.log).toHaveBeenNthCalledWith(3, 85 | 'success' 86 | ); 87 | expect(console.log).toHaveBeenNthCalledWith(4, 88 | '✓ Update complete\n' 89 | ); 90 | }); 91 | 92 | it('should handle version check errors gracefully', async () => { 93 | const error = new Error('Failed to check version'); 94 | mockExecPromise.mockRejectedValueOnce(error); 95 | 96 | await updatePackage(); 97 | 98 | expect(console.error).toHaveBeenCalledWith( 99 | 'Failed to check for updates:', 100 | 'Failed to check version' 101 | ); 102 | }); 103 | 104 | it('should handle installation errors gracefully', async () => { 105 | mockExecPromise 106 | .mockResolvedValueOnce(createExecResult('1.0.50\n')) 107 | .mockRejectedValueOnce(new Error('Installation failed')); 108 | 109 | await updatePackage(); 110 | 111 | expect(console.error).toHaveBeenCalledWith( 112 | 'Failed to install update:', 113 | 'Installation failed' 114 | ); 115 | }); 116 | 117 | describe('silent mode', () => { 118 | it('should not log messages in silent mode when update is available', async () => { 119 | mockExecPromise 120 | .mockResolvedValueOnce(createExecResult('1.0.50\n')) 121 | .mockResolvedValueOnce(createExecResult('success')); 122 | 123 | await updatePackage(true); 124 | expect(console.log).not.toHaveBeenCalled(); 125 | }); 126 | 127 | it('should not log errors in silent mode on failure', async () => { 128 | mockExecPromise.mockRejectedValueOnce(new Error('Failed to check version')); 129 | 130 | await updatePackage(true); 131 | expect(console.error).not.toHaveBeenCalled(); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /src/scripts/convert-packages.test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Test script to verify that the package conversion works correctly. 5 | * It focuses on checking environment variables are properly added. 6 | */ 7 | 8 | import fs from 'fs'; 9 | import path from 'path'; 10 | import { fileURLToPath } from 'url'; 11 | import assert from 'assert'; 12 | 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | const ROOT_DIR = path.join(__dirname, '../../'); 16 | const PACKAGES_DIR = path.join(ROOT_DIR, 'packages'); 17 | 18 | // Test cases - packages we expect to have environment variables 19 | const TEST_CASES = [ 20 | // Package name, expected env vars with their required flag 21 | { 22 | name: '@modelcontextprotocol/server-brave-search', 23 | envVars: { 24 | 'BRAVE_API_KEY': true 25 | } 26 | }, 27 | { 28 | name: '@kagi/mcp-server-kagi', 29 | envVars: { 30 | 'KAGI_API_KEY': true 31 | } 32 | }, 33 | { 34 | name: '@benborla29/mcp-server-mysql', 35 | envVars: { 36 | 'MYSQL_HOST': true, 37 | 'MYSQL_PORT': false, 38 | 'MYSQL_USER': true, 39 | 'MYSQL_PASS': true, 40 | 'MYSQL_DB': false 41 | } 42 | }, 43 | { 44 | name: '@enescinar/twitter-mcp', 45 | envVars: { 46 | 'API_KEY': true, 47 | 'API_SECRET_KEY': true, 48 | 'ACCESS_TOKEN': true, 49 | 'ACCESS_TOKEN_SECRET': true 50 | } 51 | } 52 | ]; 53 | 54 | /** 55 | * Gets the file path for a package 56 | */ 57 | function getPackageFilePath(packageName) { 58 | const safeFilename = packageName.replace(/^@/, '').replace(/\//g, '--') + '.json'; 59 | return path.join(PACKAGES_DIR, safeFilename); 60 | } 61 | 62 | /** 63 | * Reads a package file and returns its contents 64 | */ 65 | function readPackageFile(packageName) { 66 | const filePath = getPackageFilePath(packageName); 67 | if (!fs.existsSync(filePath)) { 68 | throw new Error(`Package file not found: ${filePath}`); 69 | } 70 | 71 | const fileContent = fs.readFileSync(filePath, 'utf8'); 72 | return JSON.parse(fileContent); 73 | } 74 | 75 | /** 76 | * Verifies all package files exist 77 | */ 78 | function verifyPackageFiles() { 79 | console.log('Verifying package files exist...'); 80 | 81 | // Verify all test case packages have files 82 | for (const testCase of TEST_CASES) { 83 | const filePath = getPackageFilePath(testCase.name); 84 | if (!fs.existsSync(filePath)) { 85 | throw new Error(`Package file not found: ${filePath}`); 86 | } 87 | } 88 | 89 | console.log('✅ All test case package files found'); 90 | } 91 | 92 | /** 93 | * Verifies a package has the expected environment variables 94 | */ 95 | function verifyPackage(testCase) { 96 | const packageName = testCase.name; 97 | const expectedEnvVars = testCase.envVars; 98 | 99 | console.log(`Verifying package: ${packageName}`); 100 | 101 | // Read the package file 102 | const packageData = readPackageFile(packageName); 103 | 104 | // Verify the package has environment variables 105 | if (!packageData.environmentVariables) { 106 | throw new Error(`Package ${packageName} does not have environment variables`); 107 | } 108 | 109 | // Verify all expected environment variables are present 110 | for (const [varName, isRequired] of Object.entries(expectedEnvVars)) { 111 | if (!packageData.environmentVariables[varName]) { 112 | throw new Error(`Package ${packageName} is missing environment variable: ${varName}`); 113 | } 114 | 115 | if (packageData.environmentVariables[varName].required !== isRequired) { 116 | throw new Error( 117 | `Package ${packageName} has incorrect required flag for ${varName}: ` + 118 | `expected ${isRequired}, got ${packageData.environmentVariables[varName].required}` 119 | ); 120 | } 121 | 122 | if (!packageData.environmentVariables[varName].description) { 123 | throw new Error(`Package ${packageName} is missing description for ${varName}`); 124 | } 125 | } 126 | 127 | console.log(`✅ Package ${packageName} passed verification`); 128 | } 129 | 130 | /** 131 | * Main test function 132 | */ 133 | function runTests() { 134 | try { 135 | console.log('Running verification tests...'); 136 | 137 | // Verify package files exist 138 | verifyPackageFiles(); 139 | 140 | // Verify each test case 141 | for (const testCase of TEST_CASES) { 142 | verifyPackage(testCase); 143 | } 144 | 145 | console.log('\n✅ All tests passed!'); 146 | 147 | } catch (error) { 148 | console.error('\n❌ Test failed:', error.message); 149 | process.exit(1); 150 | } 151 | } 152 | 153 | // Run the tests 154 | runTests(); -------------------------------------------------------------------------------- /src/utils/__tests__/package-registry.test.ts: -------------------------------------------------------------------------------- 1 | import { jest, describe, it, expect, beforeAll } from '@jest/globals'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | const ROOT_DIR = path.join(__dirname, '../../../'); 9 | const PACKAGES_DIR = path.join(ROOT_DIR, 'packages'); 10 | 11 | // Helper function to get a package file path 12 | function getPackageFilePath(packageName: string): string { 13 | const safeFilename = packageName.replace(/^@/, '').replace(/\//g, '--') + '.json'; 14 | return path.join(PACKAGES_DIR, safeFilename); 15 | } 16 | 17 | // Helper function to read a package file 18 | function readPackageFile(packageName: string): any { 19 | const filePath = getPackageFilePath(packageName); 20 | const fileContent = fs.readFileSync(filePath, 'utf8'); 21 | return JSON.parse(fileContent); 22 | } 23 | 24 | // Test cases - packages we expect to have environment variables 25 | const TEST_CASES = [ 26 | // Package name, expected env vars with their required flag 27 | { 28 | name: '@modelcontextprotocol/server-brave-search', 29 | envVars: { 30 | 'BRAVE_API_KEY': true 31 | } 32 | }, 33 | // Note: @kagi/mcp-server-kagi removed as it's not published on npm 34 | { 35 | name: '@benborla29/mcp-server-mysql', 36 | envVars: { 37 | 'MYSQL_HOST': true, 38 | 'MYSQL_PORT': false, 39 | 'MYSQL_USER': true, 40 | 'MYSQL_PASS': true, 41 | 'MYSQL_DB': false 42 | } 43 | }, 44 | { 45 | name: '@enescinar/twitter-mcp', 46 | envVars: { 47 | 'API_KEY': true, 48 | 'API_SECRET_KEY': true, 49 | 'ACCESS_TOKEN': true, 50 | 'ACCESS_TOKEN_SECRET': true 51 | } 52 | } 53 | ]; 54 | 55 | describe('Package Registry', () => { 56 | beforeAll(() => { 57 | // Check if packages directory exists 58 | if (!fs.existsSync(PACKAGES_DIR)) { 59 | throw new Error(` 60 | Packages directory not found at ${PACKAGES_DIR}. 61 | Please run 'npm run registry:convert' before running tests. 62 | `); 63 | } 64 | 65 | // Check if at least some package files exist 66 | const packageFiles = fs.readdirSync(PACKAGES_DIR) 67 | .filter(file => file.endsWith('.json') && file !== 'package-list.json'); 68 | 69 | if (packageFiles.length === 0) { 70 | throw new Error(` 71 | No package files found in ${PACKAGES_DIR}. 72 | Please run 'npm run registry:convert' before running tests. 73 | `); 74 | } 75 | }); 76 | 77 | describe('Package files', () => { 78 | it('should exist for test case packages', () => { 79 | // Verify all test case packages have files 80 | for (const testCase of TEST_CASES) { 81 | const filePath = getPackageFilePath(testCase.name); 82 | expect(fs.existsSync(filePath)).toBe(true); 83 | } 84 | }); 85 | }); 86 | 87 | describe('Package file format', () => { 88 | it('should have the correct format with basic properties', () => { 89 | // Use the first test case as a sample 90 | const samplePkg = TEST_CASES[0].name; 91 | const packageData = readPackageFile(samplePkg); 92 | 93 | expect(packageData).toHaveProperty('name'); 94 | expect(packageData).toHaveProperty('description'); 95 | expect(packageData).toHaveProperty('vendor'); 96 | expect(packageData).toHaveProperty('sourceUrl'); 97 | expect(packageData).toHaveProperty('homepage'); 98 | expect(packageData).toHaveProperty('license'); 99 | expect(packageData).toHaveProperty('runtime'); 100 | expect(packageData).toHaveProperty('environmentVariables'); 101 | }); 102 | }); 103 | 104 | describe('Environment variables', () => { 105 | // Test each package from our test cases 106 | TEST_CASES.forEach(testCase => { 107 | describe(`${testCase.name}`, () => { 108 | it('should have the expected environment variables', () => { 109 | const packageData = readPackageFile(testCase.name); 110 | 111 | expect(packageData).toHaveProperty('environmentVariables'); 112 | const envVars = packageData.environmentVariables; 113 | 114 | // Check each expected env var exists 115 | for (const [varName, isRequired] of Object.entries(testCase.envVars)) { 116 | expect(envVars).toHaveProperty(varName); 117 | expect(envVars[varName]).toHaveProperty('required', isRequired); 118 | expect(envVars[varName]).toHaveProperty('description'); 119 | expect(typeof envVars[varName].description).toBe('string'); 120 | expect(envVars[varName].description.length).toBeGreaterThan(0); 121 | } 122 | }); 123 | }); 124 | }); 125 | }); 126 | }); -------------------------------------------------------------------------------- /src/__tests__/package-publication.test.ts: -------------------------------------------------------------------------------- 1 | import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import fs from 'fs'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | const prCheckPath = path.join(__dirname, '../scripts/pr-check.js'); 9 | 10 | // Define the validatePackagePublication function directly based on the updated implementation 11 | // This avoids the complexity of trying to extract it from the module 12 | const validatePackagePublication = async (pkg: { name: string; runtime: string }) => { 13 | const { name, runtime } = pkg; 14 | 15 | if (runtime === 'node') { 16 | try { 17 | // Simulate npm check 18 | // In real function this would be: execSync(`npm view ${name} version`, { stdio: 'pipe' }); 19 | if (name === 'nonexistent-package') { 20 | throw new Error(`Package ${name} is not published on npm. Please publish it first.`); 21 | } 22 | return true; 23 | } catch (error) { 24 | throw new Error(`Package ${name} is not published on npm. Please publish it first.`); 25 | } 26 | } else if (runtime === 'python') { 27 | try { 28 | // Simulate pip check 29 | // In real function this would be: execSync(`pip install --dry-run ${name} 2>&1`, { encoding: 'utf-8' }); 30 | if (name === 'nonexistent-package') { 31 | throw new Error(`Package ${name} is not published on PyPI. Please publish it first.`); 32 | } 33 | return true; 34 | } catch (error: any) { 35 | // Check if the error is due to Python version requirements 36 | if (error.stdout && error.stdout.includes('Ignored the following versions that require a different python version')) { 37 | console.log(`Package ${name} exists on PyPI but requires a different Python version. This is acceptable.`); 38 | return true; 39 | } else { 40 | throw new Error(`Package ${name} is not published on PyPI. Please publish it first.`); 41 | } 42 | } 43 | } else if (runtime === 'go') { 44 | try { 45 | // Simulate go list check 46 | // In real function this would be: execSync(`go list ${name}`, { stdio: 'pipe' }); 47 | if (name === 'nonexistent-package') { 48 | throw new Error(`Package ${name} is not a valid Go package. Please ensure it's a valid Go module.`); 49 | } 50 | return true; 51 | } catch (error) { 52 | throw new Error(`Package ${name} is not a valid Go package. Please ensure it's a valid Go module.`); 53 | } 54 | } 55 | 56 | return false; 57 | }; 58 | 59 | // Read the actual pr-check.js file to verify our changes 60 | const prCheckContent = fs.readFileSync(path.join(__dirname, '../scripts/pr-check.js'), 'utf-8'); 61 | 62 | describe('Package Publication Validation', () => { 63 | // Test that the actual source code uses the package name directly 64 | it('should use exact package name for npm commands in pr-check.js', () => { 65 | // Verify npm view command uses name directly 66 | expect(prCheckContent).toContain('execSync(`npm view ${name} version`'); 67 | 68 | // Verify pip install command uses name directly 69 | expect(prCheckContent).toContain('execSync(`pip install --dry-run ${name} 2>&1`'); 70 | 71 | // Verify go list command uses name directly 72 | expect(prCheckContent).toContain('execSync(`go list ${name}`'); 73 | 74 | // Ensure normalization is not used anymore 75 | expect(prCheckContent).not.toContain('normalizedName'); 76 | }); 77 | 78 | it('should validate node packages using exact package name', async () => { 79 | const pkg = { name: 'test-package', runtime: 'node' }; 80 | await expect(validatePackagePublication(pkg)).resolves.toBe(true); 81 | 82 | const nonExistentPkg = { name: 'nonexistent-package', runtime: 'node' }; 83 | await expect(validatePackagePublication(nonExistentPkg)).rejects.toThrow( 84 | 'Package nonexistent-package is not published on npm' 85 | ); 86 | }); 87 | 88 | it('should validate python packages using exact package name', async () => { 89 | const pkg = { name: 'test-package', runtime: 'python' }; 90 | await expect(validatePackagePublication(pkg)).resolves.toBe(true); 91 | 92 | const nonExistentPkg = { name: 'nonexistent-package', runtime: 'python' }; 93 | await expect(validatePackagePublication(nonExistentPkg)).rejects.toThrow( 94 | 'Package nonexistent-package is not published on PyPI' 95 | ); 96 | }); 97 | 98 | it('should validate go packages using exact package name', async () => { 99 | const pkg = { name: 'github.com/test/package', runtime: 'go' }; 100 | await expect(validatePackagePublication(pkg)).resolves.toBe(true); 101 | 102 | const nonExistentPkg = { name: 'nonexistent-package', runtime: 'go' }; 103 | await expect(validatePackagePublication(nonExistentPkg)).rejects.toThrow( 104 | 'Package nonexistent-package is not a valid Go package' 105 | ); 106 | }); 107 | }); -------------------------------------------------------------------------------- /src/scripts/add-package.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script adds a new package to the registry by creating a new package file 5 | * and updating the index. It will also validate the package against the required fields. 6 | * 7 | * Usage: node src/scripts/add-package.js [--validate-only] 8 | */ 9 | 10 | import fs from 'fs'; 11 | import path from 'path'; 12 | import { fileURLToPath } from 'url'; 13 | 14 | const __filename = fileURLToPath(import.meta.url); 15 | const __dirname = path.dirname(__filename); 16 | 17 | // Define paths 18 | const ROOT_DIR = path.join(__dirname, '../../'); 19 | const PACKAGES_DIR = path.join(ROOT_DIR, 'packages'); 20 | 21 | // Required fields for a valid package 22 | const REQUIRED_FIELDS = ['name', 'description', 'vendor', 'sourceUrl', 'homepage', 'license', 'runtime']; 23 | const VALID_RUNTIMES = ['node', 'python']; 24 | 25 | /** 26 | * Safely creates a directory if it doesn't exist 27 | */ 28 | function ensureDirectoryExists(dirPath) { 29 | if (!fs.existsSync(dirPath)) { 30 | fs.mkdirSync(dirPath, { recursive: true }); 31 | console.log(`Created directory: ${dirPath}`); 32 | } 33 | } 34 | 35 | /** 36 | * Converts a package name to a safe filename 37 | */ 38 | function getPackageFilename(packageName) { 39 | // Replace special characters in package name to create a valid filename 40 | // Remove @ and replace / with -- to avoid directory nesting 41 | return packageName.replace(/^@/, '').replace(/\//g, '--'); 42 | } 43 | 44 | /** 45 | * Validates a package object against the required fields 46 | */ 47 | function validatePackage(pkg) { 48 | const errors = []; 49 | 50 | // Check required fields 51 | for (const field of REQUIRED_FIELDS) { 52 | if (!pkg[field]) { 53 | errors.push(`Missing required field: ${field}`); 54 | } 55 | } 56 | 57 | // Validate URLs 58 | const urlFields = ['sourceUrl', 'homepage']; 59 | for (const field of urlFields) { 60 | if (pkg[field] && !pkg[field].startsWith('http://') && !pkg[field].startsWith('https://')) { 61 | errors.push(`Invalid ${field} URL: ${pkg[field]} - must start with http:// or https://`); 62 | } 63 | } 64 | 65 | // Validate runtime 66 | if (pkg.runtime && !VALID_RUNTIMES.includes(pkg.runtime)) { 67 | errors.push(`Invalid runtime: ${pkg.runtime}. Must be one of: ${VALID_RUNTIMES.join(', ')}`); 68 | } 69 | 70 | return errors; 71 | } 72 | 73 | /** 74 | * Main function to add a new package 75 | */ 76 | async function addPackage(packageFilePath, validateOnly = false) { 77 | try { 78 | // Read the package file 79 | const packageContent = fs.readFileSync(packageFilePath, 'utf-8'); 80 | let packageObj; 81 | 82 | try { 83 | packageObj = JSON.parse(packageContent); 84 | } catch (error) { 85 | console.error('Error parsing package file:', error); 86 | process.exit(1); 87 | } 88 | 89 | // Validate the package 90 | const validationErrors = validatePackage(packageObj); 91 | if (validationErrors.length > 0) { 92 | console.error('Package validation failed:'); 93 | validationErrors.forEach(error => console.error(`- ${error}`)); 94 | process.exit(1); 95 | } 96 | 97 | console.log('Package validation successful!'); 98 | 99 | // Stop here if we're only validating 100 | if (validateOnly) { 101 | return; 102 | } 103 | 104 | // Make sure the packages directory exists 105 | ensureDirectoryExists(PACKAGES_DIR); 106 | 107 | // Get the package name and safe filename 108 | const packageName = packageObj.name; 109 | const safeFilename = getPackageFilename(packageName); 110 | 111 | // Check if the package already exists 112 | const newPackagePath = path.join(PACKAGES_DIR, `${safeFilename}.json`); 113 | if (fs.existsSync(newPackagePath)) { 114 | console.error(`Package ${packageName} already exists at ${newPackagePath}`); 115 | process.exit(1); 116 | } 117 | 118 | // We no longer maintain an index file - packages are detected by scanning the directory 119 | 120 | // Write the package file 121 | fs.writeFileSync(newPackagePath, JSON.stringify(packageObj, null, 2), 'utf-8'); 122 | console.log(`Created package file: ${newPackagePath}`); 123 | 124 | console.log(`\nPackage ${packageName} added successfully!`); 125 | 126 | } catch (error) { 127 | console.error('Error adding package:', error); 128 | process.exit(1); 129 | } 130 | } 131 | 132 | // Parse command line arguments 133 | const args = process.argv.slice(2); 134 | let validateOnly = false; 135 | let packageFilePath; 136 | 137 | if (args.includes('--validate-only')) { 138 | validateOnly = true; 139 | args.splice(args.indexOf('--validate-only'), 1); 140 | } 141 | 142 | packageFilePath = args[0]; 143 | 144 | if (!packageFilePath) { 145 | console.error('Please provide a package file path'); 146 | console.error('Usage: node src/scripts/add-package.js [--validate-only] '); 147 | process.exit(1); 148 | } 149 | 150 | if (!fs.existsSync(packageFilePath)) { 151 | console.error(`Package file not found: ${packageFilePath}`); 152 | process.exit(1); 153 | } 154 | 155 | // Run the script 156 | addPackage(packageFilePath, validateOnly); -------------------------------------------------------------------------------- /src/utils/package-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Package, ResolvedPackage } from '../types/package.js'; 2 | import { ConfigManager } from './config-manager.js'; 3 | import { loadAllPackages, loadPackage } from './package-registry.js'; 4 | 5 | export function isPackageInstalled(packageName: string): boolean { 6 | return ConfigManager.isPackageInstalled(packageName); 7 | } 8 | 9 | export function resolvePackages(): ResolvedPackage[] { 10 | try { 11 | // Load packages from registry 12 | const packages: Package[] = loadAllPackages(); 13 | 14 | if (packages.length === 0) { 15 | console.warn('No packages loaded from registry. This might be due to invalid package files.'); 16 | } 17 | 18 | // Get installed packages from config 19 | const config = ConfigManager.readConfig(); 20 | const installedServers = config.mcpServers || {}; 21 | const installedPackageNames = Object.keys(installedServers); 22 | 23 | // Create a map of existing packages with both original and sanitized names 24 | const packageMap = new Map(); 25 | for (const pkg of packages) { 26 | packageMap.set(pkg.name, pkg); 27 | // Also add sanitized version to map if different 28 | const sanitizedName = pkg.name.replace(/\//g, '-'); 29 | if (sanitizedName !== pkg.name) { 30 | packageMap.set(sanitizedName, pkg); 31 | } 32 | } 33 | 34 | // Process installed packages 35 | const resolvedPackages = new Map(); 36 | 37 | // First add all packages from package list 38 | for (const pkg of packages) { 39 | resolvedPackages.set(pkg.name, { 40 | ...pkg, 41 | runtime: pkg.runtime || 'node', 42 | isInstalled: false, 43 | isVerified: true 44 | }); 45 | } 46 | 47 | // Then process installed packages 48 | for (const serverName of installedPackageNames) { 49 | // Convert server name back to package name 50 | const packageName = serverName.replace(/-/g, '/'); 51 | const installedServer = installedServers[serverName]; 52 | 53 | // Check if this package exists in our package list (either by original or sanitized name) 54 | const existingPkg = packageMap.get(packageName) || packageMap.get(serverName); 55 | 56 | if (existingPkg) { 57 | // Update existing package's installation status 58 | resolvedPackages.set(existingPkg.name, { 59 | ...existingPkg, 60 | runtime: existingPkg.runtime || installedServer?.runtime || 'node', 61 | isInstalled: true, 62 | isVerified: true 63 | }); 64 | } else { 65 | // Add unverified package 66 | resolvedPackages.set(packageName, { 67 | name: packageName, 68 | description: 'Installed package (not in package list)', 69 | vendor: 'Unknown', 70 | sourceUrl: '', 71 | homepage: '', 72 | license: 'Unknown', 73 | runtime: installedServer?.runtime || 'node', 74 | isInstalled: true, 75 | isVerified: false 76 | }); 77 | } 78 | } 79 | 80 | return Array.from(resolvedPackages.values()); 81 | } catch (error) { 82 | console.error('Error resolving packages:', error); 83 | return []; 84 | } 85 | } 86 | 87 | export function resolvePackage(packageName: string): ResolvedPackage | null { 88 | try { 89 | // Try to load the package from registry 90 | const pkg = loadPackage(packageName); 91 | 92 | // Also try with sanitized name if needed 93 | const sanitizedName = packageName.replace(/\//g, '-'); 94 | 95 | if (!pkg) { 96 | // Check if it's an installed package (but not in registry) 97 | const config = ConfigManager.readConfig(); 98 | const serverName = packageName.replace(/\//g, '-'); 99 | const installedServer = config.mcpServers?.[serverName]; 100 | 101 | if (installedServer) { 102 | return { 103 | name: packageName, 104 | description: 'Installed package (not in package list)', 105 | vendor: 'Unknown', 106 | sourceUrl: '', 107 | homepage: '', 108 | license: 'Unknown', 109 | runtime: installedServer.runtime || 'node', 110 | isInstalled: true, 111 | isVerified: false 112 | }; 113 | } 114 | return null; 115 | } 116 | 117 | // Check installation status 118 | const isInstalled = isPackageInstalled(packageName); 119 | 120 | return { 121 | ...pkg, 122 | runtime: pkg.runtime || 'node', // Ensure runtime is set 123 | isInstalled, 124 | isVerified: true 125 | }; 126 | } catch (error) { 127 | console.error('Error resolving package:', error); 128 | return null; 129 | } 130 | } -------------------------------------------------------------------------------- /src/__tests__/uninstall.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; 3 | import type { ResolvedPackage } from '../types/package.js'; 4 | 5 | // Type for mockResolvePackage 6 | interface ResolvePackageMock { 7 | (packageName: string): ResolvedPackage | null; 8 | mockReturnValueOnce: (value: ResolvedPackage | null) => ResolvePackageMock; 9 | } 10 | 11 | // Type for mockUninstallPackage 12 | interface UninstallPackageMock { 13 | (packageName: string): Promise; 14 | mockResolvedValueOnce: () => UninstallPackageMock; 15 | mockRejectedValueOnce: (error: Error) => UninstallPackageMock; 16 | } 17 | 18 | // Type for mockPrompt 19 | interface PromptMock { 20 | (questions: any): Promise; 21 | mockResolvedValueOnce: (value: T) => PromptMock; 22 | } 23 | 24 | // Mock package-resolver module 25 | const mockResolvePackage = jest.fn() as unknown as ResolvePackageMock; 26 | jest.unstable_mockModule('../utils/package-resolver.js', () => ({ 27 | resolvePackage: mockResolvePackage 28 | })); 29 | 30 | // Mock package-management module 31 | const mockUninstallPackage = jest.fn() as unknown as UninstallPackageMock; 32 | jest.unstable_mockModule('../utils/package-management.js', () => ({ 33 | uninstallPackage: mockUninstallPackage 34 | })); 35 | 36 | // Mock inquirer 37 | const mockPrompt = jest.fn() as unknown as PromptMock; 38 | await jest.unstable_mockModule('inquirer', () => ({ 39 | default: { 40 | prompt: mockPrompt 41 | } 42 | })); 43 | 44 | // Mock chalk 45 | await jest.unstable_mockModule('chalk', () => ({ 46 | default: { 47 | red: jest.fn((text: string) => text), 48 | yellow: jest.fn((text: string) => text), 49 | green: jest.fn((text: string) => text) 50 | } 51 | })); 52 | 53 | // Import the function to test (after mocking dependencies) 54 | const { uninstall } = await import('../commands/uninstall.js'); 55 | 56 | describe('uninstall', () => { 57 | // Spy on console methods 58 | beforeEach(() => { 59 | jest.clearAllMocks(); 60 | jest.spyOn(console, 'log').mockImplementation(() => {}); 61 | jest.spyOn(console, 'error').mockImplementation(() => {}); 62 | jest.spyOn(process, 'exit').mockImplementation(((code: number) => { 63 | throw new Error(`Process.exit called with code ${code}`); 64 | }) as any); 65 | }); 66 | 67 | afterEach(() => { 68 | jest.restoreAllMocks(); 69 | }); 70 | 71 | it('should exit with error if no package name is provided', async () => { 72 | await expect(uninstall()).rejects.toThrow('Process.exit called with code 1'); 73 | expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Package name is required')); 74 | }); 75 | 76 | it('should show message if package is not found', async () => { 77 | mockResolvePackage.mockReturnValueOnce(null); 78 | 79 | await uninstall('non-existent-package'); 80 | 81 | expect(mockResolvePackage).toHaveBeenCalledWith('non-existent-package'); 82 | expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Package non-existent-package not found')); 83 | }); 84 | 85 | it('should show message if package is not installed', async () => { 86 | mockResolvePackage.mockReturnValueOnce({ 87 | name: 'test-package', 88 | isInstalled: false 89 | } as ResolvedPackage); 90 | 91 | await uninstall('test-package'); 92 | 93 | expect(mockResolvePackage).toHaveBeenCalledWith('test-package'); 94 | expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Package test-package is not installed')); 95 | }); 96 | 97 | it('should cancel uninstallation if user does not confirm', async () => { 98 | mockResolvePackage.mockReturnValueOnce({ 99 | name: 'test-package', 100 | isInstalled: true 101 | } as ResolvedPackage); 102 | 103 | mockPrompt.mockResolvedValueOnce({ confirmUninstall: false }); 104 | 105 | await uninstall('test-package'); 106 | 107 | expect(mockUninstallPackage).not.toHaveBeenCalled(); 108 | expect(console.log).toHaveBeenCalledWith('Uninstallation cancelled.'); 109 | }); 110 | 111 | it('should uninstall package if user confirms', async () => { 112 | mockResolvePackage.mockReturnValueOnce({ 113 | name: 'test-package', 114 | isInstalled: true 115 | } as ResolvedPackage); 116 | 117 | mockPrompt.mockResolvedValueOnce({ confirmUninstall: true }); 118 | 119 | await uninstall('test-package'); 120 | 121 | expect(mockUninstallPackage).toHaveBeenCalledWith('test-package'); 122 | expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Successfully uninstalled test-package')); 123 | }); 124 | 125 | it('should uninstall package with slashes in config if user confirms', async () => { 126 | mockResolvePackage.mockReturnValueOnce({ 127 | name: '@scope/package', 128 | isInstalled: true 129 | } as ResolvedPackage); 130 | 131 | mockPrompt.mockResolvedValueOnce({ confirmUninstall: true }); 132 | 133 | await uninstall('@scope/package'); 134 | 135 | expect(mockUninstallPackage).toHaveBeenCalledWith('@scope/package'); 136 | expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Successfully uninstalled @scope/package')); 137 | }); 138 | 139 | it('should handle errors during uninstallation', async () => { 140 | mockResolvePackage.mockReturnValueOnce({ 141 | name: 'test-package', 142 | isInstalled: true 143 | } as ResolvedPackage); 144 | 145 | mockPrompt.mockResolvedValueOnce({ confirmUninstall: true }); 146 | const error = new Error('Uninstall error'); 147 | mockUninstallPackage.mockRejectedValueOnce(error); 148 | 149 | await expect(uninstall('test-package')).rejects.toThrow('Process.exit called with code 1'); 150 | 151 | expect(mockUninstallPackage).toHaveBeenCalledWith('test-package'); 152 | expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Failed to uninstall package')); 153 | }); 154 | }); -------------------------------------------------------------------------------- /src/__tests__/installed.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; 3 | import type { ResolvedPackage } from '../types/package.js'; 4 | 5 | // Type definitions for mocks 6 | interface ResolvePackagesMock { 7 | (): ResolvedPackage[]; 8 | mockReturnValueOnce: (value: ResolvedPackage[]) => ResolvePackagesMock; 9 | } 10 | 11 | interface PromptMock { 12 | (questions: any): Promise; 13 | mockResolvedValueOnce: (value: T) => PromptMock; 14 | } 15 | 16 | interface DisplayMock { 17 | (pkg: ResolvedPackage): Promise; 18 | mockResolvedValueOnce: (value: string) => DisplayMock; 19 | } 20 | 21 | // Mock function for display and package actions 22 | const mockDisplayPackageDetailsWithActions = jest.fn() as unknown as DisplayMock; 23 | const mockHandlePackageAction = jest.fn(); 24 | const mockCreatePackagePrompt = jest.fn(); 25 | const mockPrintPackageListHeader = jest.fn(); 26 | 27 | // Mock package-resolver module 28 | const mockResolvePackages = jest.fn() as unknown as ResolvePackagesMock; 29 | jest.unstable_mockModule('../utils/package-resolver.js', () => ({ 30 | resolvePackages: mockResolvePackages 31 | })); 32 | 33 | // Mock display.js module 34 | jest.unstable_mockModule('../utils/display.js', () => ({ 35 | displayPackageDetailsWithActions: mockDisplayPackageDetailsWithActions 36 | })); 37 | 38 | // Mock package-actions.js module 39 | jest.unstable_mockModule('../utils/package-actions.js', () => ({ 40 | handlePackageAction: mockHandlePackageAction 41 | })); 42 | 43 | // Mock ui.js module 44 | jest.unstable_mockModule('../utils/ui.js', () => ({ 45 | createPackagePrompt: mockCreatePackagePrompt, 46 | printPackageListHeader: mockPrintPackageListHeader 47 | })); 48 | 49 | // Mock inquirer 50 | const mockRegisterPrompt = jest.fn(); 51 | const mockPrompt = jest.fn() as unknown as PromptMock; 52 | await jest.unstable_mockModule('inquirer', () => ({ 53 | default: { 54 | registerPrompt: mockRegisterPrompt, 55 | prompt: mockPrompt 56 | } 57 | })); 58 | 59 | // Mock chalk 60 | await jest.unstable_mockModule('chalk', () => ({ 61 | default: { 62 | yellow: jest.fn((text: string) => text) 63 | } 64 | })); 65 | 66 | // Mock inquirer-autocomplete-prompt 67 | await jest.unstable_mockModule('inquirer-autocomplete-prompt', () => ({ 68 | default: jest.fn() 69 | })); 70 | 71 | // Import the function to test (after mocking dependencies) 72 | const { listInstalledPackages } = await import('../commands/installed.js'); 73 | 74 | describe('listInstalledPackages', () => { 75 | beforeEach(() => { 76 | jest.clearAllMocks(); 77 | jest.spyOn(console, 'log').mockImplementation(() => {}); 78 | }); 79 | 80 | afterEach(() => { 81 | jest.restoreAllMocks(); 82 | }); 83 | 84 | it('should show message when no packages are installed', async () => { 85 | // Mock empty installed packages list 86 | mockResolvePackages.mockReturnValueOnce([ 87 | { name: 'pkg1', isInstalled: false } as ResolvedPackage, 88 | { name: 'pkg2', isInstalled: false } as ResolvedPackage 89 | ]); 90 | 91 | await listInstalledPackages(); 92 | 93 | expect(mockResolvePackages).toHaveBeenCalled(); 94 | expect(console.log).toHaveBeenCalledWith(expect.stringContaining('No MCP servers are currently installed')); 95 | expect(mockPrintPackageListHeader).not.toHaveBeenCalled(); 96 | expect(mockPrompt).not.toHaveBeenCalled(); 97 | }); 98 | 99 | it('should display installed packages and handle user selection', async () => { 100 | // Mock installed packages 101 | const installedPackages = [ 102 | { name: 'pkg1', isInstalled: true } as ResolvedPackage, 103 | { name: 'pkg2', isInstalled: true } as ResolvedPackage 104 | ]; 105 | mockResolvePackages.mockReturnValueOnce([ 106 | ...installedPackages, 107 | { name: 'pkg3', isInstalled: false } as ResolvedPackage 108 | ]); 109 | 110 | // Mock UI components 111 | const mockPromptObj = { type: 'autocomplete', name: 'selectedPackage' }; 112 | mockCreatePackagePrompt.mockReturnValueOnce(mockPromptObj); 113 | mockPrompt.mockResolvedValueOnce({ selectedPackage: installedPackages[0] }); 114 | 115 | // Mock display package details 116 | const mockAction = 'uninstall'; 117 | mockDisplayPackageDetailsWithActions.mockResolvedValueOnce(mockAction); 118 | 119 | await listInstalledPackages(); 120 | 121 | // Verify the correct functions were called 122 | expect(mockResolvePackages).toHaveBeenCalled(); 123 | expect(mockPrintPackageListHeader).toHaveBeenCalledWith(2, 'installed'); 124 | expect(mockCreatePackagePrompt).toHaveBeenCalledWith(installedPackages, expect.any(Object)); 125 | expect(mockPrompt).toHaveBeenCalledWith([mockPromptObj]); 126 | expect(mockDisplayPackageDetailsWithActions).toHaveBeenCalledWith(installedPackages[0]); 127 | expect(mockHandlePackageAction).toHaveBeenCalledWith( 128 | installedPackages[0], 129 | mockAction, 130 | expect.objectContaining({ 131 | onUninstall: expect.any(Function), 132 | onBack: expect.any(Function) 133 | }) 134 | ); 135 | }); 136 | 137 | it('should do nothing if no package is selected', async () => { 138 | // Mock installed packages 139 | const installedPackages = [ 140 | { name: 'pkg1', isInstalled: true } as ResolvedPackage, 141 | { name: 'pkg2', isInstalled: true } as ResolvedPackage 142 | ]; 143 | mockResolvePackages.mockReturnValueOnce(installedPackages); 144 | 145 | // Mock UI components 146 | mockCreatePackagePrompt.mockReturnValueOnce({}); 147 | mockPrompt.mockResolvedValueOnce({ selectedPackage: null }); 148 | 149 | await listInstalledPackages(); 150 | 151 | expect(mockResolvePackages).toHaveBeenCalled(); 152 | expect(mockPrompt).toHaveBeenCalled(); 153 | expect(mockDisplayPackageDetailsWithActions).not.toHaveBeenCalled(); 154 | expect(mockHandlePackageAction).not.toHaveBeenCalled(); 155 | }); 156 | }); -------------------------------------------------------------------------------- /src/utils/config-manager.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | import { Package } from '../types/package.js'; 5 | 6 | export interface MCPServer { 7 | runtime: 'node' | 'python' | 'go'; 8 | command?: string; 9 | args?: string[]; 10 | env?: Record; 11 | version?: string; // Add version field to track installed version 12 | } 13 | 14 | export interface MCPConfig { 15 | mcpServers: Record; 16 | [key: string]: any; // Allow other config options 17 | } 18 | 19 | export interface MCPPreferences { 20 | allowAnalytics?: boolean; 21 | } 22 | 23 | export class ConfigManager { 24 | private static configPath: string; 25 | private static preferencesPath: string; 26 | 27 | static { 28 | if (process.platform === 'win32') { 29 | const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); 30 | this.configPath = path.join(appData, 'Claude', 'claude_desktop_config.json'); 31 | this.preferencesPath = path.join(appData, 'mcp-get', 'preferences.json'); 32 | } else if (process.platform === 'darwin') { 33 | // macOS 34 | const homeDir = os.homedir(); 35 | this.configPath = path.join(homeDir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'); 36 | this.preferencesPath = path.join(homeDir, '.mcp-get', 'preferences.json'); 37 | } else { 38 | // Linux 39 | const homeDir = os.homedir(); 40 | const configDir = process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config'); 41 | this.configPath = path.join(configDir, 'Claude', 'claude_desktop_config.json'); 42 | this.preferencesPath = path.join(homeDir, '.mcp-get', 'preferences.json'); 43 | } 44 | } 45 | 46 | static getConfigPath(): string { 47 | return this.configPath; 48 | } 49 | 50 | static readConfig(): MCPConfig { 51 | try { 52 | if (!fs.existsSync(this.configPath)) { 53 | return { mcpServers: {} }; 54 | } 55 | const config = JSON.parse(fs.readFileSync(this.configPath, 'utf8')); 56 | return { 57 | ...config, 58 | mcpServers: config.mcpServers || {} 59 | }; 60 | } catch (error) { 61 | console.error('Error reading config:', error); 62 | return { mcpServers: {} }; 63 | } 64 | } 65 | 66 | static writeConfig(config: MCPConfig): void { 67 | try { 68 | const configDir = path.dirname(this.configPath); 69 | if (!fs.existsSync(configDir)) { 70 | fs.mkdirSync(configDir, { recursive: true }); 71 | } 72 | fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2)); 73 | } catch (error) { 74 | console.error('Error writing config:', error); 75 | throw error; 76 | } 77 | } 78 | 79 | static readPreferences(): MCPPreferences { 80 | try { 81 | if (!fs.existsSync(this.preferencesPath)) { 82 | return {}; 83 | } 84 | return JSON.parse(fs.readFileSync(this.preferencesPath, 'utf8')); 85 | } catch (error) { 86 | return {}; 87 | } 88 | } 89 | 90 | static writePreferences(prefs: MCPPreferences): void { 91 | try { 92 | const prefsDir = path.dirname(this.preferencesPath); 93 | if (!fs.existsSync(prefsDir)) { 94 | fs.mkdirSync(prefsDir, { recursive: true }); 95 | } 96 | fs.writeFileSync(this.preferencesPath, JSON.stringify(prefs, null, 2)); 97 | } catch (error) { 98 | console.error('Error writing preferences:', error); 99 | throw error; 100 | } 101 | } 102 | 103 | static isPackageInstalled(packageName: string): boolean { 104 | const config = this.readConfig(); 105 | const serverName = packageName.replace(/\//g, '-'); 106 | return serverName in (config.mcpServers || {}) || packageName in (config.mcpServers || {}); 107 | } 108 | 109 | static async installPackage(pkg: Package, envVars?: Record): Promise { 110 | const config = this.readConfig(); 111 | const serverName = pkg.name.replace(/\//g, '-'); 112 | 113 | const serverConfig: MCPServer = { 114 | runtime: pkg.runtime, 115 | env: envVars, 116 | version: pkg.version // Store version information 117 | }; 118 | 119 | // Add command and args based on runtime and version 120 | if (pkg.runtime === 'node') { 121 | serverConfig.command = 'npx'; 122 | serverConfig.args = ['-y', pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name]; 123 | } else if (pkg.runtime === 'python') { 124 | serverConfig.command = 'uvx'; 125 | serverConfig.args = [pkg.version ? `${pkg.name}==${pkg.version}` : pkg.name]; 126 | } else if (pkg.runtime === 'go') { 127 | serverConfig.command = 'go'; 128 | serverConfig.args = ['run', pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name]; 129 | } 130 | 131 | config.mcpServers[serverName] = serverConfig; 132 | this.writeConfig(config); 133 | } 134 | 135 | static async uninstallPackage(packageName: string): Promise { 136 | const config = this.readConfig(); 137 | const serverName = packageName.replace(/\//g, '-'); 138 | 139 | // Check for exact package name or server name using dash notation 140 | if (!config.mcpServers) { 141 | console.log(`Package ${packageName} is not installed.`); 142 | return; 143 | } 144 | 145 | // Check both formats - package may be stored with slashes or dashes 146 | if (config.mcpServers[serverName]) { 147 | delete config.mcpServers[serverName]; 148 | } else if (config.mcpServers[packageName]) { 149 | delete config.mcpServers[packageName]; 150 | } else { 151 | console.log(`Package ${packageName} is not installed.`); 152 | return; 153 | } 154 | 155 | this.writeConfig(config); 156 | } 157 | } -------------------------------------------------------------------------------- /src/__tests__/list.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; 3 | import type { ResolvedPackage } from '../types/package.js'; 4 | 5 | // Type definitions for mocks 6 | interface ResolvePackagesMock { 7 | (): ResolvedPackage[]; 8 | mockReturnValueOnce: (value: ResolvedPackage[]) => ResolvePackagesMock; 9 | mockImplementationOnce: (fn: () => ResolvedPackage[]) => ResolvePackagesMock; 10 | } 11 | 12 | interface PromptMock { 13 | (questions: any): Promise; 14 | mockResolvedValueOnce: (value: T) => PromptMock; 15 | } 16 | 17 | interface DisplayMock { 18 | (pkg: ResolvedPackage): Promise; 19 | mockResolvedValueOnce: (value: string) => DisplayMock; 20 | } 21 | 22 | // Mock all dependent modules 23 | const mockDisplayPackageDetailsWithActions = jest.fn() as unknown as DisplayMock; 24 | const mockHandlePackageAction = jest.fn(); 25 | const mockCreatePackagePrompt = jest.fn(); 26 | const mockPrintPackageListHeader = jest.fn(); 27 | const mockResolvePackages = jest.fn() as unknown as ResolvePackagesMock; 28 | 29 | // Mock package-resolver module 30 | jest.unstable_mockModule('../utils/package-resolver.js', () => ({ 31 | resolvePackages: mockResolvePackages 32 | })); 33 | 34 | // Mock display.js module 35 | jest.unstable_mockModule('../utils/display.js', () => ({ 36 | displayPackageDetailsWithActions: mockDisplayPackageDetailsWithActions 37 | })); 38 | 39 | // Mock package-actions.js module 40 | jest.unstable_mockModule('../utils/package-actions.js', () => ({ 41 | handlePackageAction: mockHandlePackageAction 42 | })); 43 | 44 | // Mock ui.js module 45 | jest.unstable_mockModule('../utils/ui.js', () => ({ 46 | createPackagePrompt: mockCreatePackagePrompt, 47 | printPackageListHeader: mockPrintPackageListHeader 48 | })); 49 | 50 | // Mock inquirer 51 | const mockRegisterPrompt = jest.fn(); 52 | const mockPrompt = jest.fn() as unknown as PromptMock; 53 | await jest.unstable_mockModule('inquirer', () => ({ 54 | default: { 55 | registerPrompt: mockRegisterPrompt, 56 | prompt: mockPrompt 57 | } 58 | })); 59 | 60 | // Mock chalk 61 | await jest.unstable_mockModule('chalk', () => ({ 62 | default: { 63 | red: jest.fn((text: string) => text) 64 | } 65 | })); 66 | 67 | // Mock inquirer-autocomplete-prompt 68 | await jest.unstable_mockModule('inquirer-autocomplete-prompt', () => ({ 69 | default: jest.fn() 70 | })); 71 | 72 | // Import the function to test 73 | const { list } = await import('../commands/list.js'); 74 | 75 | describe('list command', () => { 76 | beforeEach(() => { 77 | jest.clearAllMocks(); 78 | jest.spyOn(console, 'error').mockImplementation(() => {}); 79 | jest.spyOn(process, 'exit').mockImplementation(((code: number) => { 80 | throw new Error(`Process.exit called with code ${code}`); 81 | }) as any); 82 | }); 83 | 84 | afterEach(() => { 85 | jest.restoreAllMocks(); 86 | }); 87 | 88 | it('should display the package list and handle user selection', async () => { 89 | // Sample packages for test 90 | const packages = [ 91 | { name: 'package1', isInstalled: false } as ResolvedPackage, 92 | { name: 'package2', isInstalled: true } as ResolvedPackage 93 | ]; 94 | 95 | // Mock resolvePackages to return our sample packages 96 | mockResolvePackages.mockReturnValueOnce(packages); 97 | 98 | // Mock createPackagePrompt 99 | const mockPromptObj = { type: 'autocomplete', name: 'selectedPackage' }; 100 | mockCreatePackagePrompt.mockReturnValueOnce(mockPromptObj); 101 | 102 | // Mock user selecting a package 103 | mockPrompt.mockResolvedValueOnce({ selectedPackage: packages[0] }); 104 | 105 | // Mock display package details 106 | const mockAction = 'install'; 107 | mockDisplayPackageDetailsWithActions.mockResolvedValueOnce(mockAction); 108 | 109 | // Call the list function 110 | await list(); 111 | 112 | // Verify function calls 113 | expect(mockResolvePackages).toHaveBeenCalled(); 114 | expect(mockPrintPackageListHeader).toHaveBeenCalledWith(packages.length); 115 | expect(mockCreatePackagePrompt).toHaveBeenCalledWith(packages, { showInstallStatus: true }); 116 | expect(mockPrompt).toHaveBeenCalledWith([mockPromptObj]); 117 | expect(mockDisplayPackageDetailsWithActions).toHaveBeenCalledWith(packages[0]); 118 | expect(mockHandlePackageAction).toHaveBeenCalledWith( 119 | packages[0], 120 | mockAction, 121 | expect.objectContaining({ onBack: expect.any(Function) }) 122 | ); 123 | }); 124 | 125 | it('should do nothing if no package is selected', async () => { 126 | // Sample packages for test 127 | const packages = [ 128 | { name: 'package1', isInstalled: false } as ResolvedPackage, 129 | { name: 'package2', isInstalled: true } as ResolvedPackage 130 | ]; 131 | 132 | // Mock resolvePackages to return our sample packages 133 | mockResolvePackages.mockReturnValueOnce(packages); 134 | 135 | // Mock createPackagePrompt 136 | mockCreatePackagePrompt.mockReturnValueOnce({}); 137 | 138 | // Mock user not selecting a package 139 | mockPrompt.mockResolvedValueOnce({ selectedPackage: null }); 140 | 141 | // Call the list function 142 | await list(); 143 | 144 | // Verify function calls 145 | expect(mockResolvePackages).toHaveBeenCalled(); 146 | expect(mockPrintPackageListHeader).toHaveBeenCalled(); 147 | expect(mockPrompt).toHaveBeenCalled(); 148 | expect(mockDisplayPackageDetailsWithActions).not.toHaveBeenCalled(); 149 | expect(mockHandlePackageAction).not.toHaveBeenCalled(); 150 | }); 151 | 152 | it('should handle errors gracefully', async () => { 153 | // Mock an error when resolving packages 154 | const testError = new Error('Test error'); 155 | mockResolvePackages.mockImplementationOnce(() => { 156 | throw testError; 157 | }); 158 | 159 | // Call the list function and expect an error 160 | await expect(list()).rejects.toThrow('Process.exit called with code 1'); 161 | 162 | // Verify error logging 163 | expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Error loading package list')); 164 | expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Test error')); 165 | expect(process.exit).toHaveBeenCalledWith(1); 166 | }); 167 | }); -------------------------------------------------------------------------------- /src/scripts/migrate-env-vars.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script migrates environment variables from helpers/index.ts to the registry files 5 | */ 6 | 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | import { fileURLToPath } from 'url'; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | // Define paths 15 | const ROOT_DIR = path.join(__dirname, '../../'); 16 | const HELPERS_PATH = path.join(ROOT_DIR, 'src/helpers/index.ts'); 17 | const PACKAGES_DIR = path.join(ROOT_DIR, 'packages'); 18 | 19 | /** 20 | * Extracts packageHelpers object from the helpers/index.ts file 21 | * by reading and parsing it manually 22 | */ 23 | function extractPackageHelpers() { 24 | try { 25 | const helperContent = fs.readFileSync(HELPERS_PATH, 'utf-8'); 26 | 27 | // Find the beginning and end of the packageHelpers object 28 | const startIndex = helperContent.indexOf('export const packageHelpers: PackageHelpers = {'); 29 | const endIndex = helperContent.lastIndexOf('};'); 30 | 31 | if (startIndex === -1 || endIndex === -1) { 32 | console.error('Could not find packageHelpers object in file'); 33 | return {}; 34 | } 35 | 36 | // Extract the object content 37 | const packageHelpersContent = helperContent.slice( 38 | startIndex + 'export const packageHelpers: PackageHelpers = {'.length, 39 | endIndex 40 | ); 41 | 42 | // Parse each package and its environment variables 43 | const packages = {}; 44 | const packageMatches = [...packageHelpersContent.matchAll(/'([^']+)':\s*{([^}]+)}/g)]; 45 | 46 | for (const match of packageMatches) { 47 | const packageName = match[1]; 48 | const packageContent = match[2]; 49 | 50 | // Parse required environment variables 51 | if (packageContent.includes('requiredEnvVars')) { 52 | const envVarsMatch = packageContent.match(/requiredEnvVars:\s*{([^}]+)}/); 53 | if (envVarsMatch) { 54 | const envVarsContent = envVarsMatch[1]; 55 | const envVars = {}; 56 | 57 | // Find individual environment variables 58 | const envVarMatches = [...envVarsContent.matchAll(/(\w+(?:-\w+)?):\s*{([^}]+)}/g)]; 59 | 60 | for (const envVarMatch of envVarMatches) { 61 | const envVarName = envVarMatch[1]; 62 | const envVarContent = envVarMatch[2]; 63 | 64 | // Extract description 65 | const descriptionMatch = envVarContent.match(/description:\s*'([^']+)'/); 66 | const description = descriptionMatch ? descriptionMatch[1] : ''; 67 | 68 | // Extract required 69 | const requiredMatch = envVarContent.match(/required:\s*(true|false)/); 70 | const required = requiredMatch ? requiredMatch[1] === 'true' : true; 71 | 72 | // Extract argName if present 73 | const argNameMatch = envVarContent.match(/argName:\s*'([^']+)'/); 74 | const argName = argNameMatch ? argNameMatch[1] : undefined; 75 | 76 | // Create the environment variable object 77 | envVars[envVarName] = { 78 | description, 79 | required 80 | }; 81 | 82 | if (argName) { 83 | envVars[envVarName].argName = argName; 84 | } 85 | } 86 | 87 | packages[packageName] = { 88 | requiredEnvVars: envVars 89 | }; 90 | } 91 | } 92 | } 93 | 94 | return packages; 95 | } catch (error) { 96 | console.error('Error extracting package helpers:', error); 97 | return {}; 98 | } 99 | } 100 | 101 | /** 102 | * Updates registry files with environment variables 103 | */ 104 | async function migrateEnvVars() { 105 | // Extract package helpers 106 | const packageHelpers = extractPackageHelpers(); 107 | console.log(`Extracted environment variables for ${Object.keys(packageHelpers).length} packages`); 108 | 109 | // Read all package files 110 | const files = fs.readdirSync(PACKAGES_DIR); 111 | 112 | // Keep track of successful and failed migrations 113 | let successCount = 0; 114 | let skipCount = 0; 115 | const failures = []; 116 | 117 | // Process each package file 118 | for (const file of files) { 119 | if (!file.endsWith('.json') || file === 'package-list.json') { 120 | continue; 121 | } 122 | 123 | const filePath = path.join(PACKAGES_DIR, file); 124 | 125 | try { 126 | // Read the registry file 127 | const fileContent = fs.readFileSync(filePath, 'utf-8'); 128 | const packageData = JSON.parse(fileContent); 129 | const packageName = packageData.name; 130 | 131 | // If we have environment variables for this package, add them 132 | if (packageHelpers[packageName]?.requiredEnvVars) { 133 | // Skip if already has non-empty environment variables 134 | if (packageData.environmentVariables && Object.keys(packageData.environmentVariables).length > 0) { 135 | console.log(`Skipping ${packageName} - already has environment variables`); 136 | skipCount++; 137 | continue; 138 | } 139 | 140 | // Add environment variables 141 | packageData.environmentVariables = packageHelpers[packageName].requiredEnvVars; 142 | 143 | // Write the updated registry file 144 | fs.writeFileSync(filePath, JSON.stringify(packageData, null, 2), 'utf-8'); 145 | console.log(`Updated ${packageName} with ${Object.keys(packageData.environmentVariables).length} environment variables`); 146 | successCount++; 147 | } 148 | } catch (error) { 149 | console.error(`Error processing file ${file}:`, error); 150 | failures.push(file); 151 | } 152 | } 153 | 154 | console.log('\nMigration summary:'); 155 | console.log(`- Successfully updated: ${successCount} packages`); 156 | console.log(`- Skipped (already had environment variables): ${skipCount} packages`); 157 | console.log(`- Failed: ${failures.length} packages`); 158 | 159 | if (failures.length > 0) { 160 | console.log('\nFailed packages:'); 161 | failures.forEach(file => console.log(`- ${file}`)); 162 | } 163 | } 164 | 165 | // Run the migration 166 | migrateEnvVars(); -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { PackageHelpers } from '../types/index.js'; 2 | 3 | export const packageHelpers: PackageHelpers = { 4 | '@octomind/octomind-mcp': { 5 | requiredEnvVars: { 6 | APIKEY: { 7 | description: 'API key for octomind', 8 | required: true 9 | } 10 | } 11 | }, 12 | '@modelcontextprotocol/server-brave-search': { 13 | requiredEnvVars: { 14 | BRAVE_API_KEY: { 15 | description: 'API key for Brave Search', 16 | required: true 17 | } 18 | } 19 | }, 20 | '@modelcontextprotocol/server-github': { 21 | requiredEnvVars: { 22 | GITHUB_PERSONAL_ACCESS_TOKEN: { 23 | description: 'Personal access token for GitHub API access', 24 | required: true 25 | } 26 | } 27 | }, 28 | '@modelcontextprotocol/server-gitlab': { 29 | requiredEnvVars: { 30 | GITLAB_PERSONAL_ACCESS_TOKEN: { 31 | description: 'Personal access token for GitLab API access', 32 | required: true 33 | }, 34 | GITLAB_API_URL: { 35 | description: 'GitLab API URL (optional, for self-hosted instances)', 36 | required: false 37 | } 38 | } 39 | }, 40 | '@modelcontextprotocol/server-google-maps': { 41 | requiredEnvVars: { 42 | GOOGLE_MAPS_API_KEY: { 43 | description: 'API key for Google Maps services', 44 | required: true 45 | } 46 | } 47 | }, 48 | '@modelcontextprotocol/server-slack': { 49 | requiredEnvVars: { 50 | SLACK_BOT_TOKEN: { 51 | description: 'Slack Bot User OAuth Token (starts with xoxb-)', 52 | required: true 53 | }, 54 | SLACK_TEAM_ID: { 55 | description: 'Slack Team/Workspace ID', 56 | required: true 57 | } 58 | } 59 | }, 60 | '@raygun.io/mcp-server-raygun': { 61 | requiredEnvVars: { 62 | RAYGUN_PAT_TOKEN: { 63 | description: 'Personal access token for Raygun API access', 64 | required: true 65 | } 66 | } 67 | }, 68 | '@kagi/mcp-server-kagi': { 69 | requiredEnvVars: { 70 | KAGI_API_KEY: { 71 | description: 'API key for Kagi Search', 72 | required: true 73 | } 74 | } 75 | }, 76 | '@exa/mcp-server': { 77 | requiredEnvVars: { 78 | EXA_API_KEY: { 79 | description: 'API key for Exa AI Search', 80 | required: true 81 | } 82 | } 83 | }, 84 | '@search1api/mcp-server': { 85 | requiredEnvVars: { 86 | SEARCH1API_KEY: { 87 | description: 'API key for Search1API', 88 | required: true 89 | } 90 | } 91 | }, 92 | 'mcp-tinybird': { 93 | requiredEnvVars: { 94 | TB_API_URL: { 95 | description: 'API URL for Tinybird', 96 | required: true 97 | }, 98 | TB_ADMIN_TOKEN: { 99 | description: 'Admin token for Tinybird', 100 | required: true 101 | } 102 | } 103 | }, 104 | 'mcp-server-perplexity': { 105 | requiredEnvVars: { 106 | PERPLEXITY_API_KEY: { 107 | description: 'API key for Perplexity API access', 108 | required: true 109 | } 110 | } 111 | }, 112 | '@benborla29/mcp-server-mysql': { 113 | requiredEnvVars: { 114 | MYSQL_HOST: { 115 | description: 'MySQL Host address', 116 | required: true, 117 | }, 118 | MYSQL_PORT: { 119 | description: 'MySQL port defaults to 3306', 120 | required: false, 121 | }, 122 | MYSQL_USER: { 123 | description: 'MySQL username', 124 | required: true, 125 | }, 126 | MYSQL_PASS: { 127 | description: 'MySQL password', 128 | required: true, 129 | }, 130 | MYSQL_DB: { 131 | description: 'MySQL database to use', 132 | required: false, 133 | } 134 | } 135 | }, 136 | 'mcp-server-rememberizer': { 137 | requiredEnvVars: { 138 | REMEMBERIZER_API_TOKEN: { 139 | description: 'API token for Rememberizer', 140 | required: true 141 | } 142 | } 143 | }, 144 | 'airtable-mcp-server': { 145 | requiredEnvVars: { 146 | AIRTABLE_API_KEY: { 147 | description: 'API key for Airtable API', 148 | required: true 149 | } 150 | } 151 | }, 152 | '@enescinar/twitter-mcp': { 153 | requiredEnvVars: { 154 | API_KEY: { 155 | description: 'API key for X API', 156 | required: true 157 | }, 158 | API_SECRET_KEY: { 159 | description: 'API secret key for X API', 160 | required: true 161 | }, 162 | ACCESS_TOKEN: { 163 | description: 'API access token for X API', 164 | required: true 165 | }, 166 | ACCESS_TOKEN_SECRET: { 167 | description: 'API access token secret for X API', 168 | required: true 169 | } 170 | } 171 | }, 172 | '@llmindset/mcp-miro': { 173 | requiredEnvVars: { 174 | 'MIRO-OAUTH-KEY': { 175 | description: 'Authentication token for Miro API access (can also be provided via --token argument)', 176 | required: true, 177 | argName: 'token' 178 | } 179 | } 180 | }, 181 | 'mcp-rememberizer-vectordb': { 182 | requiredEnvVars: { 183 | REMEMBERIZER_VECTOR_STORE_API_KEY: { 184 | description: 'API token for Rememberizer Vector Store', 185 | required: true, 186 | }, 187 | }, 188 | }, 189 | '@chanmeng666/google-news-server': { 190 | requiredEnvVars: { 191 | SERP_API_KEY: { 192 | description: 'API key for Google News search', 193 | required: true 194 | } 195 | } 196 | }, 197 | 'mcp-server-stability-ai': { 198 | requiredEnvVars: { 199 | STABILITY_AI_API_KEY: { 200 | description: 'API key for Stability AI; get it from https://platform.stability.ai/account/keys.', 201 | required: true 202 | }, 203 | IMAGE_STORAGE_DIRECTORY: { 204 | description: 'Absolute path to a directory on filesystem to store output images.', 205 | required: true 206 | } 207 | } 208 | }, 209 | '@niledatabase/nile-mcp-server': { 210 | requiredEnvVars: { 211 | NILE_API_KEY: { 212 | description: 'API KEY for Nile', 213 | required: true 214 | }, 215 | NILE_WORKSPACE_SLUG: { 216 | description: 'Nile workspace name', 217 | required: true 218 | } 219 | } 220 | }, 221 | 'hyperbrowser-mcp': { 222 | requiredEnvVars: { 223 | HYPERBROWSER_API_KEY: { 224 | description: 'API KEY for Hyperbrowser', 225 | required: true 226 | }, 227 | } 228 | } 229 | }; 230 | -------------------------------------------------------------------------------- /src/utils/package-registry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Package Registry utility to load and manage MCP packages from the registry 3 | */ 4 | 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | import { Package } from '../types/package.js'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | 13 | // Define paths 14 | const PACKAGES_DIR = path.join(__dirname, '../../packages'); 15 | const OLD_PACKAGE_LIST_PATH = path.join(PACKAGES_DIR, 'package-list.json'); 16 | 17 | /** 18 | * Converts a package name to a safe filename 19 | */ 20 | function getPackageFilename(packageName: string): string { 21 | return packageName.replace(/^@/, '').replace(/\//g, '--'); 22 | } 23 | 24 | /** 25 | * Loads a single package from the registry 26 | * 27 | * @param packageName The name of the package to load 28 | * @returns The package object or undefined if not found 29 | */ 30 | export function loadPackage(packageName: string): Package | undefined { 31 | const safeFilename = getPackageFilename(packageName); 32 | const packagePath = path.join(PACKAGES_DIR, `${safeFilename}.json`); 33 | 34 | try { 35 | if (fs.existsSync(packagePath)) { 36 | const packageContent = fs.readFileSync(packagePath, 'utf-8'); 37 | return JSON.parse(packageContent); 38 | } 39 | 40 | // Fallback to old package list if registry file doesn't exist 41 | if (fs.existsSync(OLD_PACKAGE_LIST_PATH)) { 42 | const packageListContent = fs.readFileSync(OLD_PACKAGE_LIST_PATH, 'utf-8'); 43 | const packageList = JSON.parse(packageListContent); 44 | return packageList.find((pkg: Package) => pkg.name === packageName); 45 | } 46 | 47 | return undefined; 48 | } catch (error) { 49 | console.error(`Error loading package ${packageName}:`, error); 50 | return undefined; 51 | } 52 | } 53 | 54 | /** 55 | * Loads all packages from the registry 56 | * 57 | * @returns Array of package objects 58 | */ 59 | export function loadAllPackages(): Package[] { 60 | try { 61 | // Check if the packages directory exists 62 | if (fs.existsSync(PACKAGES_DIR)) { 63 | // Scan all JSON files in the directory 64 | const files = fs.readdirSync(PACKAGES_DIR) 65 | .filter(file => file.endsWith('.json') && file !== 'package-list.json'); 66 | 67 | // Load each package file 68 | const packages: Package[] = []; 69 | 70 | for (const file of files) { 71 | try { 72 | const filePath = path.join(PACKAGES_DIR, file); 73 | const fileContent = fs.readFileSync(filePath, 'utf-8'); 74 | const pkg = JSON.parse(fileContent); 75 | 76 | // Only add valid packages with a name 77 | if (pkg && pkg.name) { 78 | packages.push(pkg); 79 | } 80 | } catch (err) { 81 | // Skip invalid files 82 | console.warn(`Could not load package from ${file}:`, err); 83 | } 84 | } 85 | 86 | return packages; 87 | } 88 | 89 | // Fallback to old package list if registry doesn't exist 90 | if (fs.existsSync(OLD_PACKAGE_LIST_PATH)) { 91 | const packageListContent = fs.readFileSync(OLD_PACKAGE_LIST_PATH, 'utf-8'); 92 | return JSON.parse(packageListContent); 93 | } 94 | 95 | console.warn('No package registry found. Returning empty array.'); 96 | return []; 97 | } catch (error) { 98 | console.error('Error loading all packages:', error); 99 | return []; 100 | } 101 | } 102 | 103 | /** 104 | * Gets the required environment variables for a package from its registry entry 105 | * 106 | * @param packageName The name of the package 107 | * @returns Object containing the required environment variables or empty object if not found 108 | */ 109 | export function getPackageEnvironmentVariables(packageName: string): Record { 110 | const pkg = loadPackage(packageName); 111 | 112 | if (pkg && pkg.environmentVariables) { 113 | return pkg.environmentVariables; 114 | } 115 | 116 | // If package doesn't have environment variables in registry, return empty object 117 | return {}; 118 | } 119 | 120 | /** 121 | * Finds packages matching a search query 122 | * 123 | * @param query The search query 124 | * @returns Array of matching packages 125 | */ 126 | export function searchPackages(query: string): Package[] { 127 | const packages = loadAllPackages(); 128 | const lowerQuery = query.toLowerCase(); 129 | 130 | return packages.filter(pkg => { 131 | return ( 132 | pkg.name.toLowerCase().includes(lowerQuery) || 133 | pkg.description.toLowerCase().includes(lowerQuery) || 134 | pkg.vendor.toLowerCase().includes(lowerQuery) 135 | ); 136 | }); 137 | } 138 | 139 | /** 140 | * Updates a package in the registry 141 | * 142 | * @param packageObj The package object to update 143 | * @returns boolean indicating success 144 | */ 145 | export function updatePackage(packageObj: Package): boolean { 146 | if (!packageObj.name) { 147 | console.error('Package object must have a name'); 148 | return false; 149 | } 150 | 151 | try { 152 | const safeFilename = getPackageFilename(packageObj.name); 153 | const packagePath = path.join(PACKAGES_DIR, `${safeFilename}.json`); 154 | 155 | // Create packages directory if it doesn't exist 156 | if (!fs.existsSync(PACKAGES_DIR)) { 157 | fs.mkdirSync(PACKAGES_DIR, { recursive: true }); 158 | } 159 | 160 | // Write the package file 161 | fs.writeFileSync(packagePath, JSON.stringify(packageObj, null, 2), 'utf-8'); 162 | 163 | return true; 164 | } catch (error) { 165 | console.error(`Error updating package ${packageObj.name}:`, error); 166 | return false; 167 | } 168 | } 169 | 170 | /** 171 | * Adds a new package to the registry 172 | * 173 | * @param packageObj The package object to add 174 | * @returns boolean indicating success 175 | */ 176 | export function addPackage(packageObj: Package): boolean { 177 | return updatePackage(packageObj); 178 | } 179 | 180 | /** 181 | * Removes a package from the registry 182 | * 183 | * @param packageName The name of the package to remove 184 | * @returns boolean indicating success 185 | */ 186 | export function removePackage(packageName: string): boolean { 187 | try { 188 | const safeFilename = getPackageFilename(packageName); 189 | const packagePath = path.join(PACKAGES_DIR, `${safeFilename}.json`); 190 | 191 | // Remove the package file if it exists 192 | if (fs.existsSync(packagePath)) { 193 | fs.unlinkSync(packagePath); 194 | } 195 | 196 | return true; 197 | } catch (error) { 198 | console.error(`Error removing package ${packageName}:`, error); 199 | return false; 200 | } 201 | } -------------------------------------------------------------------------------- /scripts/gather-mcp-server-info.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Script to gather information about MCP servers from GitHub repositories 5 | * This script helps automate the process of adding MCP servers to mcp-get 6 | * 7 | * Usage: node gather-mcp-server-info.js 8 | * Example: node gather-mcp-server-info.js https://github.com/magarcia/mcp-server-giphy 9 | */ 10 | 11 | import { execSync } from 'child_process'; 12 | import fs from 'fs'; 13 | import path from 'path'; 14 | import https from 'https'; 15 | import { promisify } from 'util'; 16 | import { exec as execCallback } from 'child_process'; 17 | 18 | const exec = promisify(execCallback); 19 | 20 | function fetchUrl(url) { 21 | return new Promise((resolve, reject) => { 22 | https.get(url, (res) => { 23 | let data = ''; 24 | res.on('data', (chunk) => { 25 | data += chunk; 26 | }); 27 | res.on('end', () => { 28 | resolve(data); 29 | }); 30 | }).on('error', (err) => { 31 | reject(err); 32 | }); 33 | }); 34 | } 35 | 36 | async function checkNpmPackage(packageName) { 37 | try { 38 | const { stdout } = await exec(`npm view ${packageName} --json`); 39 | return { published: true, data: JSON.parse(stdout) }; 40 | } catch (error) { 41 | return { published: false, error: error.message }; 42 | } 43 | } 44 | 45 | async function checkPyPiPackage(packageName) { 46 | try { 47 | const data = await fetchUrl(`https://pypi.org/pypi/${packageName}/json`); 48 | return { published: true, data: JSON.parse(data) }; 49 | } catch (error) { 50 | return { published: false, error: error.message }; 51 | } 52 | } 53 | 54 | async function extractGitHubInfo(repoUrl) { 55 | const urlParts = repoUrl.split('/'); 56 | const owner = urlParts[3]; 57 | const repo = urlParts[4]; 58 | 59 | console.log(`\nGathering information for ${owner}/${repo}...`); 60 | 61 | try { 62 | const { stdout: repoDataStr } = await exec(`gh api repos/${owner}/${repo}`); 63 | const repoInfo = JSON.parse(repoDataStr); 64 | 65 | let packageInfo = null; 66 | let runtime = null; 67 | let packageName = null; 68 | 69 | try { 70 | const { stdout: packageJsonData } = await exec(`gh api repos/${owner}/${repo}/contents/package.json --raw`); 71 | packageInfo = JSON.parse(packageJsonData); 72 | runtime = 'node'; 73 | packageName = packageInfo.name; 74 | } catch (pkgError) { 75 | console.log('No package.json found, checking for pyproject.toml...'); 76 | try { 77 | const { stdout: pyprojectData } = await exec(`gh api repos/${owner}/${repo}/contents/pyproject.toml --raw`); 78 | const nameMatch = pyprojectData.match(/name\s*=\s*["']([^"']+)["']/); 79 | if (nameMatch) { 80 | packageName = nameMatch[1]; 81 | runtime = 'python'; 82 | packageInfo = { pyprojectToml: pyprojectData }; 83 | } 84 | } catch (pyError) { 85 | console.log('No pyproject.toml found either.'); 86 | } 87 | } 88 | 89 | let readmeContent = null; 90 | try { 91 | const { stdout: readmeData } = await exec(`gh api repos/${owner}/${repo}/contents/README.md --raw`); 92 | readmeContent = readmeData; 93 | } catch (error) { 94 | console.log('README.md not found.'); 95 | } 96 | 97 | let licenseContent = null; 98 | let license = repoInfo.license ? repoInfo.license.spdx_id : 'Unknown'; 99 | try { 100 | const { stdout: licenseData } = await exec(`gh api repos/${owner}/${repo}/contents/LICENSE --raw`); 101 | licenseContent = licenseData; 102 | } catch (error) { 103 | console.log('LICENSE file not found, using license from repo info:', license); 104 | } 105 | 106 | let publishStatus = { published: false }; 107 | if (packageName) { 108 | if (runtime === 'node') { 109 | publishStatus = await checkNpmPackage(packageName); 110 | } else if (runtime === 'python') { 111 | publishStatus = await checkPyPiPackage(packageName); 112 | } 113 | } 114 | 115 | const envVars = []; 116 | if (readmeContent) { 117 | const envVarMatches = readmeContent.match(/[A-Z_]{2,}(_KEY|_TOKEN|_SECRET|_API|_URL|_HOST|_PORT|_USER|_PASS|_DB)/g); 118 | if (envVarMatches) { 119 | const uniqueEnvVars = [...new Set(envVarMatches)]; 120 | uniqueEnvVars.forEach(varName => { 121 | envVars.push({ 122 | name: varName, 123 | description: `Environment variable for ${varName.toLowerCase().replace(/_/g, ' ')}`, 124 | required: true 125 | }); 126 | }); 127 | } 128 | } 129 | 130 | const mcpGetPackage = { 131 | name: packageName || `${repo}`, 132 | description: packageInfo?.description || repoInfo.description || `MCP server for ${repo}`, 133 | runtime: runtime || 'unknown', 134 | vendor: owner, 135 | sourceUrl: repoUrl, 136 | homepage: repoInfo.homepage || repoUrl, 137 | license: license 138 | }; 139 | 140 | const helperEntry = envVars.length > 0 ? { 141 | requiredEnvVars: envVars.reduce((acc, envVar) => { 142 | acc[envVar.name] = { 143 | description: envVar.description, 144 | required: envVar.required 145 | }; 146 | return acc; 147 | }, {}) 148 | } : {}; 149 | 150 | console.log('\n=== MCP Server Information ==='); 151 | console.log(`Repository: ${repoUrl}`); 152 | console.log(`Package Name: ${packageName || 'Unknown'}`); 153 | console.log(`Runtime: ${runtime || 'Unknown'}`); 154 | console.log(`License: ${license}`); 155 | console.log(`Published: ${publishStatus.published ? 'Yes' : 'No'}`); 156 | 157 | if (publishStatus.published) { 158 | console.log('\n=== Package JSON for mcp-get ==='); 159 | console.log(JSON.stringify(mcpGetPackage, null, 2)); 160 | 161 | console.log('\n=== Helper Entry for src/helpers/index.ts ==='); 162 | console.log(` '${mcpGetPackage.name}': ${JSON.stringify(helperEntry, null, 2)}`); 163 | 164 | const outputDir = path.join(process.cwd(), 'gathered-info'); 165 | if (!fs.existsSync(outputDir)) { 166 | fs.mkdirSync(outputDir, { recursive: true }); 167 | } 168 | 169 | fs.writeFileSync( 170 | path.join(outputDir, `${mcpGetPackage.name}.json`), 171 | JSON.stringify(mcpGetPackage, null, 2) 172 | ); 173 | 174 | console.log(`\nPackage JSON saved to: ${path.join(outputDir, `${mcpGetPackage.name}.json`)}`); 175 | } else { 176 | console.log('\n⚠️ Package is not published. Cannot add to mcp-get.'); 177 | } 178 | 179 | } catch (error) { 180 | console.error('Error gathering information:', error); 181 | } 182 | } 183 | 184 | const repoUrl = process.argv[2]; 185 | 186 | if (!repoUrl) { 187 | console.log('Usage: node gather-mcp-server-info.js '); 188 | console.log('Example: node gather-mcp-server-info.js https://github.com/magarcia/mcp-server-giphy'); 189 | process.exit(1); 190 | } 191 | 192 | (async () => { 193 | try { 194 | await extractGitHubInfo(repoUrl); 195 | } catch (error) { 196 | console.error(error); 197 | } 198 | })(); 199 | -------------------------------------------------------------------------------- /src/__tests__/install.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; 3 | import type { Package, ResolvedPackage } from '../types/package.js'; 4 | 5 | // Type definitions for mocks 6 | interface ResolvePackagesMock { 7 | (): ResolvedPackage[]; 8 | mockReturnValueOnce: (value: ResolvedPackage[]) => ResolvePackagesMock; 9 | } 10 | 11 | interface InstallPackageMock { 12 | (pkg: Package): Promise; 13 | mockResolvedValueOnce: () => InstallPackageMock; 14 | mockRejectedValueOnce: (error: Error) => InstallPackageMock; 15 | } 16 | 17 | interface PromptMock { 18 | (questions: any): Promise; 19 | mockResolvedValueOnce: (value: T) => PromptMock; 20 | } 21 | 22 | // Mock package-resolver module 23 | const mockResolvePackages = jest.fn() as unknown as ResolvePackagesMock; 24 | jest.unstable_mockModule('../utils/package-resolver.js', () => ({ 25 | resolvePackages: mockResolvePackages 26 | })); 27 | 28 | // Mock package-management module 29 | const mockInstallPkg = jest.fn() as unknown as InstallPackageMock; 30 | jest.unstable_mockModule('../utils/package-management.js', () => ({ 31 | installPackage: mockInstallPkg 32 | })); 33 | 34 | // Mock inquirer 35 | const mockPrompt = jest.fn() as unknown as PromptMock; 36 | await jest.unstable_mockModule('inquirer', () => ({ 37 | default: { 38 | prompt: mockPrompt 39 | } 40 | })); 41 | 42 | // Mock chalk 43 | await jest.unstable_mockModule('chalk', () => ({ 44 | default: { 45 | yellow: jest.fn((text: string) => text), 46 | cyan: jest.fn((text: string) => text) 47 | } 48 | })); 49 | 50 | // Import the function to test (after mocking dependencies) 51 | const { install } = await import('../commands/install.js'); 52 | 53 | describe('install', () => { 54 | beforeEach(() => { 55 | jest.clearAllMocks(); 56 | jest.spyOn(console, 'log').mockImplementation(() => {}); 57 | jest.spyOn(console, 'warn').mockImplementation(() => {}); 58 | jest.spyOn(console, 'error').mockImplementation(() => {}); 59 | jest.spyOn(process, 'exit').mockImplementation(((code: number) => { 60 | throw new Error(`Process.exit called with code ${code}`); 61 | }) as any); 62 | }); 63 | 64 | afterEach(() => { 65 | jest.restoreAllMocks(); 66 | }); 67 | 68 | it('should install a package when found in the curated list', async () => { 69 | const testPackage: ResolvedPackage = { 70 | name: 'test-package', 71 | description: 'A test package', 72 | runtime: 'node', 73 | vendor: 'Test Vendor', 74 | sourceUrl: 'https://example.com', 75 | homepage: 'https://example.com', 76 | license: 'MIT', 77 | isInstalled: false, 78 | isVerified: true 79 | }; 80 | 81 | mockResolvePackages.mockReturnValueOnce([testPackage]); 82 | 83 | await install('test-package'); 84 | 85 | expect(mockResolvePackages).toHaveBeenCalled(); 86 | expect(mockInstallPkg).toHaveBeenCalledWith(testPackage); 87 | }); 88 | 89 | describe('when package is not in the curated list', () => { 90 | beforeEach(() => { 91 | mockResolvePackages.mockReturnValueOnce([]); 92 | }); 93 | 94 | it('should warn user and exit if they choose not to proceed', async () => { 95 | mockPrompt.mockResolvedValueOnce({ proceedWithInstall: false }); 96 | 97 | await expect(install('unknown-package')).rejects.toThrow('Process.exit called with code 1'); 98 | 99 | expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Package unknown-package not found')); 100 | expect(console.log).toHaveBeenCalledWith('Installation cancelled.'); 101 | expect(mockInstallPkg).not.toHaveBeenCalled(); 102 | }); 103 | 104 | it('should prompt for runtime and install if user chooses to proceed', async () => { 105 | mockPrompt 106 | .mockResolvedValueOnce({ proceedWithInstall: true }) 107 | .mockResolvedValueOnce({ runtime: 'node' }); 108 | 109 | await install('unknown-package'); 110 | 111 | expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Package unknown-package not found')); 112 | expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Proceeding with installation of unknown-package')); 113 | 114 | // Verify created package object 115 | expect(mockInstallPkg).toHaveBeenCalledWith(expect.objectContaining({ 116 | name: 'unknown-package', 117 | runtime: 'node', 118 | description: 'Unverified package' 119 | })); 120 | }); 121 | 122 | it('should install a Python package when user selects Python runtime', async () => { 123 | mockPrompt 124 | .mockResolvedValueOnce({ proceedWithInstall: true }) 125 | .mockResolvedValueOnce({ runtime: 'python' }); 126 | 127 | await install('unknown-python-package'); 128 | 129 | expect(mockInstallPkg).toHaveBeenCalledWith(expect.objectContaining({ 130 | name: 'unknown-python-package', 131 | runtime: 'python' 132 | })); 133 | }); 134 | }); 135 | 136 | it('should handle installation errors', async () => { 137 | const testPackage: ResolvedPackage = { 138 | name: 'test-package', 139 | description: 'A test package', 140 | runtime: 'node', 141 | vendor: 'Test Vendor', 142 | sourceUrl: 'https://example.com', 143 | homepage: 'https://example.com', 144 | license: 'MIT', 145 | isInstalled: false, 146 | isVerified: true 147 | }; 148 | 149 | mockResolvePackages.mockReturnValueOnce([testPackage]); 150 | mockInstallPkg.mockRejectedValueOnce(new Error('Installation error')); 151 | 152 | await expect(install('test-package')).rejects.toThrow('Installation error'); 153 | 154 | expect(mockResolvePackages).toHaveBeenCalled(); 155 | expect(mockInstallPkg).toHaveBeenCalledWith(testPackage); 156 | }); 157 | 158 | it('should handle installing a specific version of a package', async () => { 159 | const testPackage: ResolvedPackage = { 160 | name: 'test-package', 161 | description: 'A test package', 162 | runtime: 'node', 163 | vendor: 'Test Vendor', 164 | sourceUrl: 'https://example.com', 165 | homepage: 'https://example.com', 166 | license: 'MIT', 167 | isInstalled: false, 168 | isVerified: true 169 | }; 170 | 171 | mockResolvePackages.mockReturnValueOnce([testPackage]); 172 | 173 | await install('test-package', '1.2.3'); 174 | 175 | // Verify the package has the version field set and is passed to installPkg 176 | expect(mockInstallPkg).toHaveBeenCalledWith(expect.objectContaining({ 177 | name: 'test-package', 178 | version: '1.2.3' 179 | })); 180 | }); 181 | 182 | it('should handle installing a specific version of an unverified package', async () => { 183 | mockResolvePackages.mockReturnValueOnce([]); 184 | mockPrompt 185 | .mockResolvedValueOnce({ proceedWithInstall: true }) 186 | .mockResolvedValueOnce({ runtime: 'node' }); 187 | 188 | await install('unknown-package', '2.0.0'); 189 | 190 | expect(mockInstallPkg).toHaveBeenCalledWith(expect.objectContaining({ 191 | name: 'unknown-package', 192 | version: '2.0.0', 193 | runtime: 'node' 194 | })); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /src/scripts/convert-packages.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Script to convert package-list.json to individual package files. 5 | * This script imports the helpers/index.ts directly to get environment variables. 6 | * 7 | * Usage: 8 | * node src/scripts/convert-packages.js # Convert all packages 9 | * node src/scripts/convert-packages.js packageName # Convert only the specified package 10 | * 11 | * After conversion, the package is removed from package-list.json 12 | */ 13 | 14 | import fs from 'fs'; 15 | import path from 'path'; 16 | import { fileURLToPath } from 'url'; 17 | import { exec } from 'child_process'; 18 | import { promisify } from 'util'; 19 | 20 | const execAsync = promisify(exec); 21 | 22 | const __filename = fileURLToPath(import.meta.url); 23 | const __dirname = path.dirname(__filename); 24 | const ROOT_DIR = path.join(__dirname, '../../'); 25 | const PACKAGE_LIST_PATH = path.join(ROOT_DIR, 'packages/package-list.json'); 26 | const PACKAGES_DIR = path.join(ROOT_DIR, 'packages'); 27 | const HELPERS_TS_PATH = path.join(ROOT_DIR, 'src/helpers/index.ts'); 28 | const HELPERS_JS_PATH = path.join(ROOT_DIR, 'temp', 'helpers.js'); 29 | 30 | /** 31 | * Converts a package name to a safe filename 32 | */ 33 | function getPackageFilename(packageName) { 34 | return packageName.replace(/^@/, '').replace(/\//g, '--') + '.json'; 35 | } 36 | 37 | /** 38 | * Creates a temporary JavaScript version of the helpers file 39 | * This makes it easier to import directly 40 | */ 41 | async function createTempHelpers() { 42 | // Ensure temp directory exists 43 | if (!fs.existsSync(path.join(ROOT_DIR, 'temp'))) { 44 | fs.mkdirSync(path.join(ROOT_DIR, 'temp'), { recursive: true }); 45 | } 46 | 47 | // Read the TypeScript file 48 | const helpersTsContent = fs.readFileSync(HELPERS_TS_PATH, 'utf8'); 49 | 50 | // Convert to JavaScript (simple conversion for our needs) 51 | const helpersJsContent = helpersTsContent 52 | .replace('import { PackageHelpers } from \'../types/index.js\';', '') 53 | .replace('export const packageHelpers: PackageHelpers =', 'export const packageHelpers ='); 54 | 55 | // Write to temp file 56 | fs.writeFileSync(HELPERS_JS_PATH, helpersJsContent, 'utf8'); 57 | 58 | // Run tsc on it to convert to valid JS 59 | try { 60 | await execAsync(`cd ${ROOT_DIR} && npx tsc --allowJs --outDir ./temp ./temp/helpers.js`); 61 | console.log('Helpers file converted to JavaScript'); 62 | } catch (error) { 63 | console.error('Error converting helpers file:', error); 64 | // If transpilation fails, we'll still have the basic conversion 65 | } 66 | } 67 | 68 | /** 69 | * Gets the helper data 70 | */ 71 | async function getHelperData() { 72 | await createTempHelpers(); 73 | 74 | try { 75 | // Dynamic import of the generated JS file 76 | const { packageHelpers } = await import(HELPERS_JS_PATH); 77 | 78 | if (!packageHelpers) { 79 | console.error('Could not load package helpers'); 80 | return {}; 81 | } 82 | 83 | return packageHelpers; 84 | } catch (error) { 85 | console.error('Error loading helper data:', error); 86 | 87 | // As a fallback, try to load it by parsing the file 88 | try { 89 | const helpersContent = fs.readFileSync(HELPERS_JS_PATH, 'utf8'); 90 | const helpersObject = helpersContent.substring( 91 | helpersContent.indexOf('packageHelpers = {') + 'packageHelpers = '.length, 92 | helpersContent.lastIndexOf('};') + 1 93 | ); 94 | 95 | // Replace single quotes with double quotes for JSON parsing 96 | const jsonObject = helpersObject 97 | .replace(/'/g, '"') 98 | .replace(/(\w+):/g, '"$1":'); 99 | 100 | return JSON.parse(jsonObject); 101 | } catch (fallbackError) { 102 | console.error('Fallback also failed:', fallbackError); 103 | return {}; 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * Extracts environment variables from package helper 110 | */ 111 | function getEnvironmentVariables(packageName, packageHelpers) { 112 | const helper = packageHelpers[packageName]; 113 | if (!helper) return {}; 114 | 115 | const envVars = helper.requiredEnvVars; 116 | if (!envVars) return {}; 117 | 118 | return envVars; 119 | } 120 | 121 | /** 122 | * Main function to convert package list to individual files 123 | * 124 | * @param {string} singlePackage - If provided, only convert this specific package 125 | */ 126 | async function convertPackages(singlePackage) { 127 | try { 128 | // Read the package list 129 | const packageListContent = fs.readFileSync(PACKAGE_LIST_PATH, 'utf8'); 130 | const packageList = JSON.parse(packageListContent); 131 | 132 | // Get the helper data with environment variables 133 | const packageHelpers = await getHelperData(); 134 | console.log(`Loaded helper data with ${Object.keys(packageHelpers).length} package entries`); 135 | 136 | // Create a backup of the original package list 137 | fs.copyFileSync(PACKAGE_LIST_PATH, `${PACKAGE_LIST_PATH}.bak`); 138 | console.log(`Created backup of package list: ${PACKAGE_LIST_PATH}.bak`); 139 | 140 | // Create the packages directory if it doesn't exist 141 | if (!fs.existsSync(PACKAGES_DIR)) { 142 | fs.mkdirSync(PACKAGES_DIR, { recursive: true }); 143 | } 144 | 145 | // Keep track of which packages will be removed from the original list 146 | const packagesToRemove = []; 147 | 148 | // Filter the package list if a single package was specified 149 | const packagesToConvert = singlePackage 150 | ? packageList.filter(pkg => pkg.name === singlePackage) 151 | : packageList; 152 | 153 | if (singlePackage && packagesToConvert.length === 0) { 154 | console.error(`Package "${singlePackage}" not found in package-list.json`); 155 | process.exit(1); 156 | } 157 | 158 | // Convert each package to an individual file 159 | for (const pkg of packagesToConvert) { 160 | const packageName = pkg.name; 161 | 162 | // Get environment variables for this package 163 | const environmentVariables = getEnvironmentVariables(packageName, packageHelpers); 164 | 165 | // Create the complete package object 166 | const packageObject = { 167 | ...pkg, 168 | environmentVariables 169 | }; 170 | 171 | // Write the package file 172 | const packageFilePath = path.join(PACKAGES_DIR, getPackageFilename(packageName)); 173 | fs.writeFileSync(packageFilePath, JSON.stringify(packageObject, null, 2), 'utf8'); 174 | 175 | console.log(`Created package file: ${packageFilePath}`); 176 | 177 | // Add to the list of packages to remove from the original package-list.json 178 | packagesToRemove.push(packageName); 179 | 180 | // Log environment variables 181 | const envVarCount = Object.keys(environmentVariables).length; 182 | if (envVarCount > 0) { 183 | console.log(` - Added ${envVarCount} environment variables`); 184 | Object.keys(environmentVariables).forEach(varName => { 185 | console.log(` - ${varName} (required: ${environmentVariables[varName].required})`); 186 | }); 187 | } 188 | } 189 | 190 | // We no longer create an index file 191 | console.log("Individual package files created. No index file is needed as we'll scan the directory directly."); 192 | 193 | // Update the original package-list.json by removing the converted packages 194 | if (packagesToRemove.length > 0) { 195 | // Filter out the converted packages from the original package list 196 | const remainingPackages = packageList.filter(pkg => !packagesToRemove.includes(pkg.name)); 197 | 198 | // Write the updated package list back to package-list.json 199 | fs.writeFileSync(PACKAGE_LIST_PATH, JSON.stringify(remainingPackages, null, 2), 'utf8'); 200 | console.log(`\nRemoved ${packagesToRemove.length} packages from the original package-list.json`); 201 | console.log(`Remaining packages in package-list.json: ${remainingPackages.length}`); 202 | } 203 | 204 | console.log('\nConversion completed successfully!'); 205 | console.log(`Total packages converted: ${packagesToRemove.length}`); 206 | 207 | // Clean up 208 | //fs.unlinkSync(HELPERS_JS_PATH); 209 | //console.log('Cleaned up temporary files'); 210 | 211 | } catch (error) { 212 | console.error('Error converting packages:', error); 213 | process.exit(1); 214 | } 215 | } 216 | 217 | // Parse command line arguments 218 | const args = process.argv.slice(2); 219 | const singlePackage = args[0]; // If provided, only convert this specific package 220 | 221 | // Run the conversion with optional single package filter 222 | convertPackages(singlePackage); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ DEPRECATED - This Project Has Moved 2 | 3 | **This repository is no longer actively maintained.** 4 | 5 | We want to extend our heartfelt thanks to everyone who contributed to mcp-get, submitted packages, reported issues, and helped build this community. Your contributions and support have been invaluable in advancing the Model Context Protocol ecosystem. 6 | 7 | ## 🎯 Recommendation 8 | 9 | **We recommend [Smithery](https://smithery.ai)** for discovering, installing, and managing MCP servers. 10 | 11 | Smithery provides: 12 | - A comprehensive, curated registry of MCP servers 13 | - Simple installation and management 14 | - Better discovery and documentation 15 | - Active maintenance and support 16 | 17 | Visit **[smithery.ai](https://smithery.ai)** to get started. 18 | 19 | **Note:** This tool will continue to work, but will no longer receive updates or new features. 20 | 21 | --- 22 | 23 | # mcp-get (Archived) 24 | 25 | A powerful command-line tool for discovering, installing, and managing Model Context Protocol (MCP) servers. This tool simplifies the process of connecting Large Language Models (LLMs) to external data sources, tools, and services. 26 | 27 | With mcp-get, you can: 28 | - Discover available MCP servers from our curated registry 29 | - Install servers with a single command 30 | - Manage environment variables and configurations 31 | - Update and uninstall servers as needed 32 | 33 | ## Quick Start 34 | 35 | Try mcp-get immediately: 36 | 37 | ```bash 38 | npx @michaellatman/mcp-get@latest list 39 | npx @michaellatman/mcp-get@latest install @modelcontextprotocol/server-brave-search 40 | ``` 41 | 42 | All packages added to the registry are automatically displayed on [mcp-get.com](https://mcp-get.com), making them discoverable to other users. 43 | 44 | ## About Model Context Protocol 45 | 46 | The Model Context Protocol (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you're building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need. 47 | 48 | Learn more about MCP at [modelcontextprotocol.io](https://modelcontextprotocol.io/introduction) 49 | 50 | ## What Packages Can You Install? 51 | 52 | This tool helps you install and manage MCP servers that connect Claude to various data sources and tools, including: 53 | 54 | - **Development Tools**: GitHub, GitLab 55 | - **Communication Tools**: Slack 56 | - **Search & Data**: Brave Search, Google Maps 57 | - **Database Systems**: PostgreSQL, SQLite 58 | - **Web Automation**: Puppeteer 59 | - **Cloud Storage**: Google Drive 60 | 61 | ## Prerequisites 62 | 63 | - Node.js (version 14 or higher) for Node.js-based MCP servers 64 | - Python (version 3.10 or higher) for Python-based MCP servers 65 | - Go (version 1.16 or higher) for Go-based MCP servers 66 | - Claude Desktop app (for local MCP server usage) 67 | 68 | ## Usage Examples 69 | 70 | ### Install a Package 71 | 72 | ``` 73 | npx @michaellatman/mcp-get@latest install @modelcontextprotocol/server-brave-search 74 | ``` 75 | 76 | Sample output: 77 | ``` 78 | Installing @modelcontextprotocol/server-brave-search... 79 | Installation complete. 80 | ``` 81 | 82 | #### Install a Specific Version 83 | 84 | You can also install a specific version of a package: 85 | 86 | ``` 87 | npx @michaellatman/mcp-get@latest install @modelcontextprotocol/server-brave-search 1.0.0 88 | ``` 89 | 90 | Sample output: 91 | ``` 92 | Installing @modelcontextprotocol/server-brave-search version 1.0.0... 93 | Installation complete. 94 | ``` 95 | 96 | The version syntax follows standard package manager conventions: 97 | - For Node.js packages: `package@version` (e.g., `@modelcontextprotocol/server-brave-search@1.0.0`) 98 | - For Python packages: `package==version` (e.g., `mcp-server-aidd==0.1.19`) 99 | - For Go packages: `package@version` (e.g., `example.com/go-server@v1.0.0`) 100 | 101 | ### List Packages 102 | 103 | ``` 104 | npx @michaellatman/mcp-get@latest list 105 | ``` 106 | 107 | Sample output: 108 | ``` 109 | 📦 Available Packages 110 | Found 11 packages 111 | 112 | @modelcontextprotocol/server-brave-search │ MCP server for Brave Search API integration │ Anthropic, PBC (https://anthropic.com) │ MIT 113 | @modelcontextprotocol/server-everything │ MCP server that exercises all the features of the MCP protocol │ Anthropic, PBC (https://anthropic.com) │ MIT 114 | ... 115 | ``` 116 | 117 | ### Uninstall a Package 118 | 119 | ``` 120 | npx @michaellatman/mcp-get@latest uninstall @modelcontextprotocol/server-brave-search 121 | ``` 122 | 123 | Sample output: 124 | ``` 125 | Uninstalling @modelcontextprotocol/server-brave-search... 126 | Uninstallation complete. 127 | ``` 128 | 129 | ### Update the Tool 130 | 131 | The tool automatically checks for updates when running commands. You can also manually update: 132 | 133 | ``` 134 | npx @michaellatman/mcp-get@latest update 135 | ``` 136 | 137 | Sample output: 138 | ``` 139 | Updating mcp-get... 140 | Update complete. 141 | ``` 142 | 143 | ## Contributing 144 | 145 | We welcome contributions to the project! If you would like to contribute, please follow these guidelines: 146 | 147 | 1. Fork the repository and create a new branch for your feature or bugfix. 148 | 2. Write tests for your changes and ensure all existing tests pass. 149 | 3. Submit a pull request with a clear description of your changes. 150 | 151 | ## License 152 | 153 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 154 | 155 | ## Contact Information 156 | 157 | If you have any questions or need help, feel free to reach out: 158 | 159 | - GitHub Issues: [michaellatman/mcp-get](https://github.com/michaellatman/mcp-get/issues) 160 | 161 | ## Adding Your Own MCP Server to the Registry 162 | 163 | There are two ways to add your MCP server to the registry: 164 | 165 | ### Option 1: Manual Package Addition 166 | 167 | If you want to maintain your own package: 168 | 169 | 1. **Create Your MCP Server**: 170 | - Develop your MCP server according to the [MCP protocol specifications](https://modelcontextprotocol.io) 171 | - Publish it as either an NPM package (installable via npm) or a Python package (installable via uvx) 172 | 173 | 2. **Add Your Package to the Package Folder**: Add your server as a JSON file in the `packages/` directory: 174 | 175 | - For regular packages, use `packages/your-package-name.json` 176 | - For scoped packages, use `packages/scope--package-name.json` (double hyphens between scope and name) 177 | 178 | ```json 179 | { 180 | "name": "your-package-name", 181 | "description": "A brief description of your MCP server", 182 | "vendor": "Your Name or Organization", 183 | "sourceUrl": "URL to the source code repository", 184 | "homepage": "URL to the homepage or documentation", 185 | "license": "License type (e.g., MIT)", 186 | "runtime": "node | python | go", 187 | "environmentVariables": { 188 | "SOME_API_KEY": { 189 | "description": "Description of what this key is for", 190 | "required": true 191 | } 192 | } 193 | } 194 | ``` 195 | 196 | Important notes: 197 | - The `name` field must be the exact resolvable package name in npm or pip 198 | - The `runtime` field specifies how your package should be installed: 199 | - Use `"runtime": "node"` for packages that should be installed via npm 200 | - Use `"runtime": "python"` for packages that should be installed via uvx 201 | - Always include an `environmentVariables` object (can be empty `{}` if none required) 202 | 203 | 3. **Validate Your Package**: Run the PR check to validate your package: 204 | ``` 205 | npm run pr-check 206 | ``` 207 | 208 | 4. **Submit a Pull Request**: Fork this repository, add your package file, and submit a PR. 209 | 210 | ### Option 2: Community Servers Repository 211 | 212 | If you don't want to manage package deployment and distribution: 213 | 214 | 1. **Fork Community Repository**: 215 | - Fork [mcp-get/community-servers](https://github.com/mcp-get/community-servers) 216 | - This repository follows the same structure as the official MCP servers 217 | 218 | 2. **Add Your Server**: 219 | - Add your implementation to the `src` directory 220 | - Follow the existing patterns and structure 221 | - Include necessary documentation and tests 222 | 223 | 3. **Submit a Pull Request**: 224 | - Submit your PR to the community servers repository 225 | - Once merged, your server will be automatically added to the registry 226 | 227 | Both options require following the [MCP protocol specifications](https://modelcontextprotocol.io). Choose the option that best fits your needs: 228 | - Option 1 if you want full control over your package distribution 229 | - Option 2 if you want to avoid managing package deployment and distribution 230 | -------------------------------------------------------------------------------- /src/utils/__tests__/package-validation.test.ts: -------------------------------------------------------------------------------- 1 | import { jest, describe, it, expect } from '@jest/globals'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | import { loadAllPackages } from '../package-registry.js'; 6 | import { Package } from '../../types/package.js'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | const PACKAGES_DIR = path.join(__dirname, '../../../packages'); 11 | 12 | // Define the JSON schema for package validation 13 | const packageSchema = { 14 | required: ['name', 'description', 'vendor', 'sourceUrl', 'homepage', 'license', 'runtime'], 15 | properties: { 16 | name: { type: 'string', minLength: 1 }, 17 | description: { type: 'string', minLength: 1 }, 18 | vendor: { type: 'string' }, 19 | sourceUrl: { 20 | type: 'string', 21 | pattern: '^https?://' 22 | }, 23 | homepage: { 24 | type: 'string', 25 | pattern: '^https?://' 26 | }, 27 | license: { type: 'string' }, 28 | runtime: { 29 | type: 'string', 30 | enum: ['node', 'python', 'go'] 31 | }, 32 | environmentVariables: { 33 | type: 'object', 34 | additionalProperties: { 35 | type: 'object', 36 | required: ['description', 'required'], 37 | properties: { 38 | description: { type: 'string', minLength: 1 }, 39 | required: { type: 'boolean' }, 40 | argName: { type: 'string' } 41 | } 42 | } 43 | } 44 | } 45 | }; 46 | 47 | // Helper function to validate a package against the schema 48 | function validatePackageAgainstSchema(pkg: Package): string[] { 49 | const errors: string[] = []; 50 | 51 | // Check required fields 52 | for (const field of packageSchema.required) { 53 | if (!(field in pkg) || !pkg[field as keyof Package]) { 54 | errors.push(`Missing required field: ${field}`); 55 | } 56 | } 57 | 58 | // Validate field types and patterns 59 | if (pkg.name && typeof pkg.name !== 'string') { 60 | errors.push('name must be a string'); 61 | } 62 | 63 | if (pkg.description && typeof pkg.description !== 'string') { 64 | errors.push('description must be a string'); 65 | } 66 | 67 | if (pkg.vendor && typeof pkg.vendor !== 'string') { 68 | errors.push('vendor must be a string'); 69 | } 70 | 71 | if (pkg.sourceUrl) { 72 | if (typeof pkg.sourceUrl !== 'string') { 73 | errors.push('sourceUrl must be a string'); 74 | } else if (!pkg.sourceUrl.match(/^https?:\/\//)) { 75 | errors.push('sourceUrl must start with http:// or https://'); 76 | } 77 | } 78 | 79 | if (pkg.homepage) { 80 | if (typeof pkg.homepage !== 'string') { 81 | errors.push('homepage must be a string'); 82 | } else if (!pkg.homepage.match(/^https?:\/\//)) { 83 | errors.push('homepage must start with http:// or https://'); 84 | } 85 | } 86 | 87 | if (pkg.license && typeof pkg.license !== 'string') { 88 | errors.push('license must be a string'); 89 | } 90 | 91 | if (pkg.runtime) { 92 | if (typeof pkg.runtime !== 'string') { 93 | errors.push('runtime must be a string'); 94 | } else if (!['node', 'python', 'go'].includes(pkg.runtime)) { 95 | errors.push('runtime must be either "node", "python", or "go"'); 96 | } 97 | } 98 | 99 | // Validate environment variables if present 100 | if (pkg.environmentVariables) { 101 | if (typeof pkg.environmentVariables !== 'object') { 102 | errors.push('environmentVariables must be an object'); 103 | } else { 104 | for (const [key, envVar] of Object.entries(pkg.environmentVariables)) { 105 | if (typeof envVar !== 'object') { 106 | errors.push(`environmentVariables.${key} must be an object`); 107 | continue; 108 | } 109 | 110 | if (!envVar.description) { 111 | errors.push(`environmentVariables.${key} missing required field: description`); 112 | } else if (typeof envVar.description !== 'string') { 113 | errors.push(`environmentVariables.${key}.description must be a string`); 114 | } 115 | 116 | if (envVar.required === undefined) { 117 | errors.push(`environmentVariables.${key} missing required field: required`); 118 | } else if (typeof envVar.required !== 'boolean') { 119 | errors.push(`environmentVariables.${key}.required must be a boolean`); 120 | } 121 | 122 | if (envVar.argName !== undefined && typeof envVar.argName !== 'string') { 123 | errors.push(`environmentVariables.${key}.argName must be a string`); 124 | } 125 | } 126 | } 127 | } 128 | 129 | return errors; 130 | } 131 | 132 | describe('Package Validation', () => { 133 | it('should validate all packages against schema', () => { 134 | const packages = loadAllPackages(); 135 | expect(packages.length).toBeGreaterThan(0); 136 | 137 | // Track validation errors for all packages 138 | const validationResults: Record = {}; 139 | let totalErrors = 0; 140 | 141 | // Validate each package 142 | for (const pkg of packages) { 143 | const errors = validatePackageAgainstSchema(pkg); 144 | if (errors.length > 0) { 145 | validationResults[pkg.name] = errors; 146 | totalErrors += errors.length; 147 | } 148 | } 149 | 150 | // If there are validation errors, format them nicely for the error message 151 | if (totalErrors > 0) { 152 | let errorMessage = `Found ${totalErrors} validation errors across ${Object.keys(validationResults).length} packages:\n\n`; 153 | 154 | for (const [pkgName, errors] of Object.entries(validationResults)) { 155 | errorMessage += `Package "${pkgName}":\n`; 156 | errors.forEach(err => errorMessage += ` - ${err}\n`); 157 | errorMessage += '\n'; 158 | } 159 | 160 | throw new Error(errorMessage); 161 | } 162 | 163 | // If we get here, all packages passed validation 164 | expect(totalErrors).toBe(0); 165 | }); 166 | 167 | it('should have valid filenames for all packages', () => { 168 | const packages = loadAllPackages(); 169 | const filesInDir = fs.readdirSync(PACKAGES_DIR) 170 | .filter(file => file.endsWith('.json') && file !== 'package-list.json' && file !== 'index.json'); 171 | 172 | expect(filesInDir.length).toBeGreaterThan(0); 173 | expect(filesInDir.length).toBeGreaterThanOrEqual(packages.length); 174 | 175 | // Helper to convert package name to expected filename 176 | const getExpectedFilename = (name: string): string => { 177 | return name.replace(/^@/, '').replace(/\//g, '--') + '.json'; 178 | }; 179 | 180 | // Check each package has a corresponding file with the expected name 181 | for (const pkg of packages) { 182 | const expectedFilename = getExpectedFilename(pkg.name); 183 | expect(filesInDir).toContain(expectedFilename); 184 | } 185 | }); 186 | 187 | it('should have consistent environment variables for packages that need them', () => { 188 | const packages = loadAllPackages(); 189 | 190 | // Check packages with environment variables 191 | const packagesWithEnvVars = packages.filter(pkg => 192 | pkg.environmentVariables && Object.keys(pkg.environmentVariables).length > 0 193 | ); 194 | 195 | expect(packagesWithEnvVars.length).toBeGreaterThan(0); 196 | 197 | // Validate that required environment variables have descriptions 198 | for (const pkg of packagesWithEnvVars) { 199 | const envVars = pkg.environmentVariables; 200 | 201 | for (const [key, envVar] of Object.entries(envVars || {})) { 202 | // Required env vars must have descriptions 203 | if (envVar.required) { 204 | expect(envVar.description).toBeTruthy(); 205 | expect(typeof envVar.description).toBe('string'); 206 | expect(envVar.description.length).toBeGreaterThan(0); 207 | } 208 | } 209 | } 210 | }); 211 | 212 | it('should verify all JSON files in packages directory are valid', () => { 213 | const filesInDir = fs.readdirSync(PACKAGES_DIR) 214 | .filter(file => file.endsWith('.json') && file !== 'package-list.json' && file !== 'index.json'); 215 | 216 | expect(filesInDir.length).toBeGreaterThan(0); 217 | 218 | const invalidFiles: string[] = []; 219 | 220 | // Check each JSON file is valid 221 | for (const file of filesInDir) { 222 | const filePath = path.join(PACKAGES_DIR, file); 223 | const fileContent = fs.readFileSync(filePath, 'utf-8'); 224 | 225 | 226 | try { 227 | const pkg = JSON.parse(fileContent); 228 | expect(pkg).toHaveProperty('name'); 229 | } catch (error) { 230 | invalidFiles.push(file); 231 | } 232 | } 233 | 234 | expect(invalidFiles).toEqual([]); 235 | }); 236 | }); 237 | --------------------------------------------------------------------------------