# Directory Structure ``` ├── .github │ └── workflows │ ├── pr-feedback.yml │ └── release.yml ├── .gitignore ├── .releaserc.json ├── CHANGELOG.md ├── CLAUDE.md ├── LICENSE ├── package.json ├── pnpm-lock.yaml ├── README.md ├── src │ ├── index.ts │ ├── server-manager.ts │ └── types.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Node.js related 2 | node_modules/ 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | 7 | # Build outputs 8 | dist/ 9 | build/ 10 | 11 | # Logs 12 | logs/ 13 | *.log 14 | 15 | # IDE related 16 | .idea/ 17 | .vscode/ 18 | *.swp 19 | *.swo 20 | 21 | # Environment variables 22 | .env 23 | .env.* 24 | !.env.example 25 | 26 | # OS related 27 | .DS_Store 28 | Thumbs.db 29 | 30 | .history 31 | .cursor 32 | ``` -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "branches": [ 3 | "main" 4 | ], 5 | "plugins": [ 6 | "@semantic-release/commit-analyzer", 7 | "@semantic-release/release-notes-generator", 8 | "@semantic-release/changelog", 9 | "@semantic-release/npm", 10 | "@semantic-release/github", 11 | [ 12 | "@semantic-release/git", 13 | { 14 | "assets": [ 15 | "package.json", 16 | "pnpm-lock.yaml", 17 | "CHANGELOG.md" 18 | ], 19 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 20 | } 21 | ] 22 | ] 23 | } ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | <p align="center"> 2 | <img src="https://imgur.com/DgWxkmv.png" width="200" height="200"> 3 | </p> 4 | 5 | [](https://cursor.com/install-mcp?name=mcp-hub&config=eyJjb21tYW5kIjoibnB4IC15IG1jcC1odWItbWNwIC0tY29uZmlnLXBhdGggfi8uY3Vyc29yL21jcC1odWIuanNvbiJ9) 6 | 7 | [](https://insiders.vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22mcp-hub%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-hub-mcp%40latest%22%2C%22--config-path%22%2C%22~%2Fmcp-hub.json%22%5D%7D) 8 | 9 | # MCP-Hub-MCP Server 10 | 11 | A hub server that connects to and manages other MCP (Model Context Protocol) servers. 12 | 13 | ## Overview 14 | 15 | This project builds an MCP hub server that connects to and manages multiple MCP (Model Context Protocol) servers through a single interface. 16 | It helps prevent excessive context usage and pollution from infrequently used MCPs (e.g., Atlassian MCP, Playwright MCP) by allowing you to connect them only when needed. 17 | This reduces AI mistakes and improves performance by keeping the active tool set focused and manageable. 18 | 19 | ## Key Features 20 | 21 | - Automatic connection to other MCP servers via configuration file 22 | - List available tools on connected servers 23 | - Call tools on connected servers and return results 24 | 25 | ## Configuration 26 | 27 | Add this to your `mcp.json`: 28 | 29 | #### Using npx 30 | 31 | ```json 32 | { 33 | "mcpServers": { 34 | "other-tools": { 35 | "command": "npx", 36 | "args": [ 37 | "-y", 38 | "mcp-hub-mcp", 39 | "--config-path", 40 | "/Users/username/mcp.json" 41 | ] 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | 48 | ## Installation and Running 49 | 50 | ### Requirements 51 | 52 | - Node.js 18.0.0 or higher 53 | - npm, yarn, or pnpm 54 | 55 | ### Installation 56 | 57 | ```bash 58 | # Clone repository 59 | git clone <repository-url> 60 | cd mcp-hub-mcp 61 | 62 | # Install dependencies 63 | npm install 64 | # or 65 | yarn install 66 | # or 67 | pnpm install 68 | ``` 69 | 70 | ### Build 71 | 72 | ```bash 73 | npm run build 74 | # or 75 | yarn build 76 | # or 77 | pnpm build 78 | ``` 79 | 80 | ### Run 81 | 82 | ```bash 83 | npm start 84 | # or 85 | yarn start 86 | # or 87 | pnpm start 88 | ``` 89 | 90 | ### Development Mode 91 | 92 | ```bash 93 | npm run dev 94 | # or 95 | yarn dev 96 | # or 97 | pnpm dev 98 | ``` 99 | 100 | ## Configuration File 101 | 102 | The MCP-Hub-MCP server uses a Claude Desktop format configuration file to automatically connect to other MCP servers. 103 | You can specify the configuration file in the following ways: 104 | 105 | 1. Environment variable: Set the `MCP_CONFIG_PATH` environment variable to the configuration file path 106 | 2. Command line argument: Use the `--config-path` option to specify the configuration file path 107 | 3. Default path: Use `mcp-config.json` file in the current directory 108 | 109 | Configuration file format: 110 | 111 | ```json 112 | { 113 | "mcpServers": { 114 | "serverName1": { 115 | "command": "command", 116 | "args": ["arg1", "arg2", ...], 117 | "env": { "ENV_VAR1": "value1", ... } 118 | }, 119 | "serverName2": { 120 | "command": "anotherCommand", 121 | "args": ["arg1", "arg2", ...] 122 | } 123 | } 124 | } 125 | ``` 126 | 127 | Example: 128 | 129 | ```json 130 | { 131 | "mcpServers": { 132 | "filesystem": { 133 | "command": "npx", 134 | "args": [ 135 | "-y", 136 | "@modelcontextprotocol/server-filesystem", 137 | "/Users/username/Desktop", 138 | "/Users/username/Downloads" 139 | ] 140 | }, 141 | "other-server": { 142 | "command": "node", 143 | "args": ["path/to/other-mcp-server.js"] 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | ## Usage 150 | 151 | The MCP-Hub-MCP server provides the following tools: 152 | 153 | ### 1. `list-all-tools` 154 | 155 | Returns a list of tools from all connected servers. 156 | 157 | ```json 158 | { 159 | "name": "list-all-tools", 160 | "arguments": {} 161 | } 162 | ``` 163 | 164 | ### 2. `call-tool` 165 | 166 | Calls a tool on a specific server. 167 | 168 | - `serverName`: Name of the MCP server to call the tool from 169 | - `toolName`: Name of the tool to call 170 | - `toolArgs`: Arguments to pass to the tool 171 | 172 | ```json 173 | { 174 | "name": "call-tool", 175 | "arguments": { 176 | "serverName": "filesystem", 177 | "toolName": "readFile", 178 | "toolArgs": { 179 | "path": "/Users/username/Desktop/example.txt" 180 | } 181 | } 182 | } 183 | ``` 184 | 185 | ### 3. `find-tools` 186 | 187 | Find tools matching a regex pattern across all connected servers (grep-like functionality). 188 | 189 | - `pattern`: Regex pattern to search for in tool names and descriptions 190 | - `searchIn`: Where to search: "name", "description", or "both" (default: "both") 191 | - `caseSensitive`: Whether the search should be case-sensitive (default: false) 192 | 193 | ```json 194 | { 195 | "name": "find-tools", 196 | "arguments": { 197 | "pattern": "file", 198 | "searchIn": "both", 199 | "caseSensitive": false 200 | } 201 | } 202 | ``` 203 | 204 | Example patterns: 205 | - `"file"` - Find all tools containing "file" 206 | - `"^read"` - Find all tools starting with "read" 207 | - `"(read|write).*file"` - Find tools for reading or writing files 208 | - `"config$"` - Find tools ending with "config" 209 | 210 | Example output: 211 | ```json 212 | { 213 | "filesystem": [ 214 | { 215 | "name": "readFile", 216 | "description": "Read the contents of a file", 217 | "inputSchema": { 218 | "type": "object", 219 | "properties": { 220 | "path": { 221 | "type": "string", 222 | "description": "Path to the file to read" 223 | } 224 | }, 225 | "required": ["path"] 226 | } 227 | }, 228 | { 229 | "name": "writeFile", 230 | "description": "Write content to a file", 231 | "inputSchema": { 232 | "type": "object", 233 | "properties": { 234 | "path": { 235 | "type": "string", 236 | "description": "Path to the file to write" 237 | }, 238 | "content": { 239 | "type": "string", 240 | "description": "Content to write to the file" 241 | } 242 | }, 243 | "required": ["path", "content"] 244 | } 245 | } 246 | ] 247 | } 248 | ``` 249 | 250 | ## Commit Message Convention 251 | 252 | This project follows [Conventional Commits](https://www.conventionalcommits.org/) for automatic versioning and CHANGELOG generation. 253 | 254 | Format: `<type>(<scope>): <description>` 255 | 256 | Examples: 257 | 258 | - `feat: add new hub connection feature` 259 | - `fix: resolve issue with server timeout` 260 | - `docs: update API documentation` 261 | - `chore: update dependencies` 262 | 263 | Types: 264 | 265 | - `feat`: New feature (MINOR version bump) 266 | - `fix`: Bug fix (PATCH version bump) 267 | - `docs`: Documentation only changes 268 | - `style`: Changes that do not affect the meaning of the code 269 | - `refactor`: Code change that neither fixes a bug nor adds a feature 270 | - `perf`: Code change that improves performance 271 | - `test`: Adding missing tests or correcting existing tests 272 | - `chore`: Changes to the build process or auxiliary tools 273 | 274 | Breaking Changes: 275 | Add `BREAKING CHANGE:` in the commit footer to trigger a MAJOR version bump. 276 | 277 | ## Other Links 278 | 279 | - [MCP Reviews](https://mcpreview.com/mcp-servers/warpdev/mcp-hub-mcp) 280 | 281 | ## License 282 | 283 | MIT 284 | ``` -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- ```markdown 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | This is an MCP (Model Context Protocol) Hub server that acts as a proxy/aggregator for multiple MCP servers. It bypasses tool limitations in AI assistants (especially Cursor's 40-tool limit) and provides a unified interface to multiple MCP servers. 8 | 9 | ## Development Commands 10 | 11 | ### Build and Run 12 | ```bash 13 | # Install dependencies 14 | pnpm install 15 | 16 | # Build TypeScript to JavaScript 17 | pnpm build 18 | 19 | # Run the server (after building) 20 | pnpm start 21 | 22 | # Build and run in one command 23 | pnpm dev 24 | ``` 25 | 26 | ### Development Workflow 27 | ```bash 28 | # For testing changes locally with npx 29 | npm link # Creates a global link to the local package 30 | npx mcp-hub-mcp # Test the linked version 31 | 32 | # To unlink after testing 33 | npm unlink -g mcp-hub-mcp 34 | ``` 35 | 36 | ### Release Process 37 | - Uses semantic-release with Conventional Commits 38 | - Commits should follow: `type(scope): description` 39 | - Types: feat, fix, docs, style, refactor, perf, test, chore 40 | - Breaking changes: add `!` after type or `BREAKING CHANGE:` in body 41 | - Releases are automated via GitHub Actions on the main branch 42 | 43 | ## Architecture 44 | 45 | ### Core Components 46 | 47 | 1. **src/index.ts** - Entry point that creates the MCP server with three tools: 48 | - `list-all-tools`: Lists all available tools from connected servers 49 | - `call-tool`: Executes a tool on a specific server 50 | - `find-tools`: Grep-like search for tools matching regex patterns 51 | 52 | 2. **src/server-manager.ts** - `McpServerManager` class that: 53 | - Loads server configurations from JSON file 54 | - Manages connections to multiple MCP servers 55 | - Proxies tool calls to appropriate servers 56 | - Handles server lifecycle and error recovery 57 | - Implements `findTools` method for regex-based tool search 58 | 59 | 3. **src/types.ts** - Type definitions and Zod schemas for: 60 | - Server configuration validation 61 | - MCP SDK type exports 62 | - Configuration file structure 63 | - Parameter schemas for all tools 64 | 65 | ### Configuration 66 | 67 | The server configuration is loaded from (in order of precedence): 68 | 1. Environment variable: `MCP_CONFIG_PATH` 69 | 2. Command-line argument: `--config-path` 70 | 3. Default location: `./mcp-config.json` or `{cwd}/mcp-config.json` 71 | 72 | Configuration format (Claude Desktop compatible): 73 | ```json 74 | { 75 | "mcpServers": { 76 | "server-name": { 77 | "command": "command-to-run", 78 | "args": ["arg1", "arg2"], 79 | "env": { "KEY": "value" } 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | ### Tool Naming Convention 86 | 87 | Tools from connected servers are prefixed with the server name: 88 | - Original tool: `list-files` 89 | - Through hub: `server-name__list-files` 90 | 91 | ## Important Considerations 92 | 93 | 1. **Error Handling**: The hub gracefully handles server connection failures and continues operating with available servers. 94 | 95 | 2. **Environment Variables**: Server configurations can include environment variables that are passed to child processes. 96 | 97 | 3. **Logging**: Uses console.error for error messages since stdout is reserved for MCP protocol communication. 98 | 99 | 4. **No Tests**: Currently no test framework is set up. When implementing tests, consider: 100 | - Unit tests for server-manager.ts logic 101 | - Integration tests for MCP protocol handling 102 | - Mock MCP server responses for testing 103 | 104 | 5. **TypeScript Configuration**: 105 | - Target: ES2020 106 | - Module: NodeNext with NodeNext resolution 107 | - Strict mode enabled 108 | - Outputs to `dist/` directory 109 | - Declaration files generated 110 | 111 | 6. **Dependencies**: 112 | - Keep dependencies minimal (currently only @modelcontextprotocol/sdk and zod) 113 | - All types are exported from types.ts for consistency 114 | 115 | 7. **Binary Execution**: Configured as npm binary `mcp-hub-mcp` with shebang in index.ts ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "dist", 9 | "declaration": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["node_modules", "dist"] 14 | } 15 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-hub-mcp", 3 | "version": "1.3.0", 4 | "description": "MCP Hub server that connects to and manages other MCP servers", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "files": [ 8 | "dist", 9 | "README.md", 10 | "LICENSE" 11 | ], 12 | "bin": { 13 | "mcp-hub-mcp": "dist/index.js" 14 | }, 15 | "scripts": { 16 | "build": "tsc", 17 | "start": "node dist/index.js", 18 | "dev": "tsc && node dist/index.js", 19 | "test": "echo \"Error: no test specified\" && exit 1" 20 | }, 21 | "keywords": [ 22 | "mcp", 23 | "model-context-protocol" 24 | ], 25 | "author": "", 26 | "license": "MIT", 27 | "dependencies": { 28 | "@modelcontextprotocol/sdk": "^1.13.0", 29 | "zod": "^3.25.67" 30 | }, 31 | "devDependencies": { 32 | "@semantic-release/changelog": "^6.0.3", 33 | "@semantic-release/git": "^10.0.1", 34 | "@types/node": "^22.15.32", 35 | "semantic-release": "^24.2.5", 36 | "typescript": "^5.8.3" 37 | } 38 | } 39 | ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Release 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | permissions: 7 | contents: write 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | release: 13 | name: Release 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: "lts/*" 24 | - name: Setup pnpm 25 | uses: pnpm/action-setup@v4 26 | with: 27 | version: 9.12.2 28 | run_install: false 29 | - name: Get pnpm store directory 30 | shell: bash 31 | run: | 32 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 33 | - name: Setup pnpm cache 34 | uses: actions/cache@v3 35 | with: 36 | path: ${{ env.STORE_PATH }} 37 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 38 | restore-keys: | 39 | ${{ runner.os }}-pnpm-store- 40 | - name: Install dependencies 41 | run: pnpm install 42 | - name: Build project 43 | run: pnpm build 44 | - name: Release 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | run: npx semantic-release 49 | ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # [1.3.0](https://github.com/warpdev/mcp-hub-mcp/compare/v1.2.0...v1.3.0) (2025-07-05) 2 | 3 | 4 | ### Features 5 | 6 | * enhance MCP server functionality with new tools and transport options ([ae319dd](https://github.com/warpdev/mcp-hub-mcp/commit/ae319dd22311f3d2d6beaf07f53b7d5621bd7543)) 7 | 8 | # [1.2.0](https://github.com/warpdev/mcp-hub-mcp/compare/v1.1.1...v1.2.0) (2025-06-21) 9 | 10 | 11 | ### Features 12 | 13 | * add find-tools command for grep-like tool search ([8ffc2e7](https://github.com/warpdev/mcp-hub-mcp/commit/8ffc2e7e0012d8a8df7c0e399341c27ec6771c5b)) 14 | * enhance tool descriptions to promote find-tools usage ([353169d](https://github.com/warpdev/mcp-hub-mcp/commit/353169d92c55a67973d2d0704d3c447d03b4fab2)) 15 | 16 | ## [1.1.1](https://github.com/warpdev/mcp-hub-mcp/compare/v1.1.0...v1.1.1) (2025-06-02) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * allow Claude Desktop to use this MCP ([6406cda](https://github.com/warpdev/mcp-hub-mcp/commit/6406cdaaeaf554cc1fb5c2194a8024280d603a9c)) 22 | 23 | # [1.1.0](https://github.com/warpdev/mcp-hub-mcp/compare/v1.0.3...v1.1.0) (2025-04-16) 24 | 25 | 26 | ### Features 27 | 28 | * add description for better ai recognition ([0fd23a2](https://github.com/warpdev/mcp-hub-mcp/commit/0fd23a2d53337cf8fa36604c26bbccf7bcadcce1)) 29 | 30 | ## [1.0.3](https://github.com/warpdev/mcp-hub-mcp/compare/v1.0.2...v1.0.3) (2025-04-11) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * fix run with npx cli ([c44218c](https://github.com/warpdev/mcp-hub-mcp/commit/c44218c5e56f25c399a267075238404b806ee451)) 36 | 37 | ## [1.0.2](https://github.com/warpdev/mcp-hub-mcp/compare/v1.0.1...v1.0.2) (2025-04-11) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * fix missing bin ([7dd5a1f](https://github.com/warpdev/mcp-hub-mcp/commit/7dd5a1fc5e8e701c0135f4f31dddeec168a663bb)) 43 | 44 | ## [1.0.1](https://github.com/warpdev/mcp-hub-mcp/compare/v1.0.0...v1.0.1) (2025-04-11) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * fix package publish files ([627a30b](https://github.com/warpdev/mcp-hub-mcp/commit/627a30b74183e1dadc45aa5cec02ec3de374f165)) 50 | 51 | # 1.0.0 (2025-04-11) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * update GitHub Actions workflows to use pnpm instead of npm ([fd9f19e](https://github.com/warpdev/mcp-hub-mcp/commit/fd9f19e70f73a0cdba43dfd9132da850a4a3a760)) 57 | 58 | 59 | ### Features 60 | 61 | * add semantic-release for automated versioning ([ee3ebfd](https://github.com/warpdev/mcp-hub-mcp/commit/ee3ebfd84f34bef7b53200c74c8bb9fd75d69e21)) 62 | ``` -------------------------------------------------------------------------------- /.github/workflows/pr-feedback.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: PR Feedback 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - synchronize 7 | - reopened 8 | - labeled 9 | - unlabeled 10 | 11 | permissions: 12 | contents: read 13 | pull-requests: write 14 | 15 | jobs: 16 | preview: 17 | name: Preview Release 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: "lts/*" 28 | - name: Setup pnpm 29 | uses: pnpm/action-setup@v4 30 | with: 31 | version: 9.12.2 32 | run_install: false 33 | - name: Get pnpm store directory 34 | shell: bash 35 | run: | 36 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 37 | - name: Setup pnpm cache 38 | uses: actions/cache@v3 39 | with: 40 | path: ${{ env.STORE_PATH }} 41 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 42 | restore-keys: | 43 | ${{ runner.os }}-pnpm-store- 44 | - name: Install dependencies 45 | run: pnpm install 46 | - name: Preview Release 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | run: npx semantic-release --dry-run 50 | - name: Find Comment 51 | uses: peter-evans/find-comment@v2 52 | id: fc 53 | with: 54 | issue-number: ${{ github.event.pull_request.number }} 55 | comment-author: "github-actions[bot]" 56 | body-includes: "### Semantic Release Preview" 57 | - name: Generate Release Notes 58 | id: release_notes 59 | run: | 60 | echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV 61 | npx semantic-release --dry-run | grep -A 100 "Release note for" | sed 's/`//g' >> $GITHUB_ENV 62 | echo "EOF" >> $GITHUB_ENV 63 | - name: Create or Update Comment 64 | uses: peter-evans/create-or-update-comment@v2 65 | with: 66 | comment-id: ${{ steps.fc.outputs.comment-id }} 67 | issue-number: ${{ github.event.pull_request.number }} 68 | body: | 69 | ### Semantic Release Preview 70 | 71 | When this PR is merged to main, the following release will be created: 72 | 73 | ${{ env.RELEASE_NOTES }} 74 | 75 | The version is determined by [Conventional Commits](https://www.conventionalcommits.org/): 76 | - `fix:` = PATCH release (1.0.0 → 1.0.1) 77 | - `feat:` = MINOR release (1.0.0 → 1.1.0) 78 | - `BREAKING CHANGE:` = MAJOR release (1.0.0 → 2.0.0) 79 | edit-mode: replace 80 | ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from "zod"; 2 | 3 | export const ConnectMcpParamsSchema = z.object({ 4 | // For stdio servers 5 | command: z 6 | .string() 7 | .describe("Command to run the MCP server") 8 | .optional(), 9 | args: z 10 | .array(z.string()) 11 | .optional() 12 | .describe("Command arguments"), 13 | 14 | // For HTTP servers 15 | type: z 16 | .enum(["stdio", "http"]) 17 | .optional() 18 | .describe("Server transport type"), 19 | url: z 20 | .string() 21 | .describe("URL for HTTP-based MCP server") 22 | .optional(), 23 | headers: z 24 | .record(z.string()) 25 | .optional() 26 | .describe("HTTP headers for authentication"), 27 | 28 | // Environment variables (applies to both) 29 | env: z 30 | .record(z.string()) 31 | .optional() 32 | .describe("Environment variables"), 33 | }); 34 | 35 | export type ConnectMcpParams = z.infer< 36 | typeof ConnectMcpParamsSchema 37 | >; 38 | 39 | export const ListToolsParamsSchema = z.object({ 40 | serverName: z 41 | .string() 42 | .describe("Name of the MCP server to list tools from"), 43 | }); 44 | 45 | export type ListToolsParams = z.infer< 46 | typeof ListToolsParamsSchema 47 | >; 48 | 49 | export const CallToolParamsSchema = z.object({ 50 | serverName: z 51 | .string() 52 | .describe("Name of the MCP server to call tool from"), 53 | toolName: z.string().describe("Name of the tool to call"), 54 | toolArgs: z 55 | .record(z.unknown()) 56 | .describe("Arguments to pass to the tool"), 57 | }); 58 | 59 | export type CallToolParams = z.infer< 60 | typeof CallToolParamsSchema 61 | >; 62 | 63 | export const FindToolsParamsSchema = z.object({ 64 | pattern: z 65 | .string() 66 | .describe("Regex pattern to search for in tool names and descriptions"), 67 | searchIn: z 68 | .enum(["name", "description", "both"]) 69 | .optional() 70 | .default("both") 71 | .describe("Where to search: in tool names, descriptions, or both"), 72 | caseSensitive: z 73 | .boolean() 74 | .optional() 75 | .default(false) 76 | .describe("Whether the search should be case-sensitive"), 77 | }); 78 | 79 | export type FindToolsParams = z.infer< 80 | typeof FindToolsParamsSchema 81 | >; 82 | 83 | export const GetToolParamsSchema = z.object({ 84 | serverName: z 85 | .string() 86 | .describe("Name of the MCP server containing the tool"), 87 | toolName: z 88 | .string() 89 | .describe("Exact name of the tool to retrieve"), 90 | }); 91 | 92 | export type GetToolParams = z.infer< 93 | typeof GetToolParamsSchema 94 | >; 95 | 96 | export const ListToolsInServerParamsSchema = z.object({ 97 | serverName: z 98 | .string() 99 | .describe("Name of the MCP server to list tools from"), 100 | }); 101 | 102 | export type ListToolsInServerParams = z.infer< 103 | typeof ListToolsInServerParamsSchema 104 | >; 105 | 106 | export const FindToolsInServerParamsSchema = z.object({ 107 | serverName: z 108 | .string() 109 | .describe("Name of the MCP server to search tools in"), 110 | pattern: z 111 | .string() 112 | .describe("Regex pattern to search for in tool names and descriptions"), 113 | searchIn: z 114 | .enum(["name", "description", "both"]) 115 | .default("both") 116 | .describe("Where to search: in tool names, descriptions, or both"), 117 | caseSensitive: z 118 | .boolean() 119 | .default(false) 120 | .describe("Whether the search should be case-sensitive"), 121 | }); 122 | 123 | export type FindToolsInServerParams = z.infer< 124 | typeof FindToolsInServerParamsSchema 125 | >; 126 | 127 | // MCP configuration file interface (claude_desktop_config.json format) 128 | export interface McpServerConfig { 129 | // For stdio servers 130 | command?: string; 131 | args?: string[]; 132 | env?: Record<string, string>; 133 | 134 | // For HTTP servers 135 | type?: "stdio" | "http"; 136 | url?: string; 137 | headers?: Record<string, string>; 138 | } 139 | 140 | export interface McpConfig { 141 | mcpServers: Record<string, McpServerConfig>; 142 | } 143 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { McpServerManager } from "./server-manager.js"; 6 | import { 7 | CallToolParamsSchema, 8 | FindToolsParamsSchema, 9 | GetToolParamsSchema, 10 | ListToolsInServerParamsSchema, 11 | FindToolsInServerParamsSchema 12 | } from "./types.js"; 13 | 14 | // Create MCP server manager instance (auto load enabled) 15 | const serverManager = new McpServerManager({ 16 | autoLoad: true, 17 | }); 18 | 19 | // Create MCP server 20 | const server = new McpServer({ 21 | name: "MCP-Hub-Server", 22 | version: "1.0.0", 23 | description: 24 | "Your central hub for ALL available tools. Use this server to discover and execute any tool you need. All system tools are accessible through here - search, find, and call them via this server.", 25 | }); 26 | 27 | // Tool to return tools list from all servers 28 | server.tool( 29 | "list-all-tools", 30 | "List ALL available tools from all connected servers. NOTE: For better performance, use find-tools with keywords first. Only use this when you need to see everything or if find-tools didn't find what you need", 31 | {}, // Use empty object when there are no parameters 32 | async (args, extra) => { 33 | try { 34 | const servers = serverManager.getConnectedServers(); 35 | 36 | if (servers.length === 0) { 37 | return { 38 | content: [ 39 | { 40 | type: "text", 41 | text: "No connected servers.", 42 | }, 43 | ], 44 | }; 45 | } 46 | 47 | const allTools: Record<string, any> = {}; 48 | 49 | // Get tools list from each server 50 | for (const serverName of servers) { 51 | try { 52 | const toolsResponse = await serverManager.listTools(serverName); 53 | 54 | // Filter to only include name and description 55 | if (toolsResponse.tools && Array.isArray(toolsResponse.tools)) { 56 | allTools[serverName] = { 57 | tools: toolsResponse.tools.map((tool: any) => ({ 58 | name: tool.name, 59 | description: tool.description, 60 | })) 61 | }; 62 | } else { 63 | allTools[serverName] = toolsResponse; 64 | } 65 | } catch (error) { 66 | allTools[serverName] = { 67 | error: `Failed to get tools list: ${(error as Error).message}`, 68 | }; 69 | } 70 | } 71 | 72 | return { 73 | content: [ 74 | { 75 | type: "text", 76 | text: JSON.stringify(allTools, null, 2), 77 | }, 78 | ], 79 | }; 80 | } catch (error) { 81 | return { 82 | content: [ 83 | { 84 | type: "text", 85 | text: `Failed to get tools list from all servers: ${ 86 | (error as Error).message 87 | }`, 88 | }, 89 | ], 90 | isError: true, 91 | }; 92 | } 93 | }, 94 | ); 95 | 96 | // Tool to call a specific tool from a specific server 97 | server.tool( 98 | "call-tool", 99 | "Call a specific tool from a specific server. TIP: Use find-tools first to discover the tool and get the correct serverName and toolName", 100 | { 101 | serverName: CallToolParamsSchema.shape.serverName, 102 | toolName: CallToolParamsSchema.shape.toolName, 103 | toolArgs: CallToolParamsSchema.shape.toolArgs, 104 | }, 105 | async (args, extra) => { 106 | try { 107 | const { serverName, toolName, toolArgs } = args; 108 | const result = await serverManager.callTool( 109 | serverName, 110 | toolName, 111 | toolArgs, 112 | ); 113 | 114 | return { 115 | content: [ 116 | { 117 | type: "text", 118 | text: JSON.stringify(result, null, 2), 119 | }, 120 | ], 121 | }; 122 | } catch (error) { 123 | return { 124 | content: [ 125 | { 126 | type: "text", 127 | text: `Tool call failed: ${(error as Error).message}`, 128 | }, 129 | ], 130 | isError: true, 131 | }; 132 | } 133 | }, 134 | ); 135 | 136 | // Tool to find tools matching a pattern across all servers 137 | server.tool( 138 | "find-tools", 139 | `Use this tool to find best tools by searching with keywords or regex patterns. 140 | If you don't have a specific tool for a task, this is the best way to discover what tools are available. 141 | `, 142 | { 143 | pattern: FindToolsParamsSchema.shape.pattern, 144 | searchIn: FindToolsParamsSchema.shape.searchIn, 145 | caseSensitive: FindToolsParamsSchema.shape.caseSensitive, 146 | }, 147 | async (args, extra) => { 148 | try { 149 | const { pattern, searchIn, caseSensitive } = args; 150 | const results = await serverManager.findTools(pattern, { 151 | searchIn, 152 | caseSensitive, 153 | }); 154 | 155 | return { 156 | content: [ 157 | { 158 | type: "text", 159 | text: JSON.stringify(results, null, 2), 160 | }, 161 | ], 162 | }; 163 | } catch (error) { 164 | return { 165 | content: [ 166 | { 167 | type: "text", 168 | text: `Failed to find tools: ${(error as Error).message}`, 169 | }, 170 | ], 171 | isError: true, 172 | }; 173 | } 174 | }, 175 | ); 176 | 177 | // Tool to get detailed information about a specific tool 178 | server.tool( 179 | "get-tool", 180 | "Get complete schema for a specific tool from a specific server, including inputSchema. TIP: Use find-tools first to discover the tool and get the correct serverName and toolName", 181 | { 182 | serverName: GetToolParamsSchema.shape.serverName, 183 | toolName: GetToolParamsSchema.shape.toolName, 184 | }, 185 | async (args, extra) => { 186 | try { 187 | const { serverName, toolName } = args; 188 | const tool = await serverManager.getTool(serverName, toolName); 189 | 190 | return { 191 | content: [ 192 | { 193 | type: "text", 194 | text: JSON.stringify(tool, null, 2), 195 | }, 196 | ], 197 | }; 198 | } catch (error) { 199 | return { 200 | content: [ 201 | { 202 | type: "text", 203 | text: `Error getting tool: ${(error as Error).message}`, 204 | }, 205 | ], 206 | }; 207 | } 208 | } 209 | ); 210 | 211 | // Tool to list all tools from a specific server 212 | server.tool( 213 | "list-all-tools-in-server", 214 | "List ALL tools from a specific MCP server (returns name and description only)", 215 | { 216 | serverName: ListToolsInServerParamsSchema.shape.serverName, 217 | }, 218 | async (args, extra) => { 219 | try { 220 | const { serverName } = args; 221 | const result = await serverManager.listToolsInServer(serverName); 222 | 223 | return { 224 | content: [ 225 | { 226 | type: "text", 227 | text: JSON.stringify(result, null, 2), 228 | }, 229 | ], 230 | }; 231 | } catch (error) { 232 | return { 233 | content: [ 234 | { 235 | type: "text", 236 | text: `Error listing tools from server '${args.serverName}': ${(error as Error).message}`, 237 | }, 238 | ], 239 | }; 240 | } 241 | } 242 | ); 243 | 244 | // Tool to find tools in a specific server 245 | server.tool( 246 | "find-tools-in-server", 247 | "Find tools matching a pattern in a specific MCP server (returns name and description only)", 248 | { 249 | serverName: FindToolsInServerParamsSchema.shape.serverName, 250 | pattern: FindToolsInServerParamsSchema.shape.pattern, 251 | searchIn: FindToolsInServerParamsSchema.shape.searchIn, 252 | caseSensitive: FindToolsInServerParamsSchema.shape.caseSensitive, 253 | }, 254 | async (args, extra) => { 255 | try { 256 | const { serverName, pattern, searchIn, caseSensitive } = args; 257 | const results = await serverManager.findToolsInServer( 258 | serverName, 259 | pattern, 260 | searchIn, 261 | caseSensitive 262 | ); 263 | 264 | return { 265 | content: [ 266 | { 267 | type: "text", 268 | text: JSON.stringify({ tools: results }, null, 2), 269 | }, 270 | ], 271 | }; 272 | } catch (error) { 273 | return { 274 | content: [ 275 | { 276 | type: "text", 277 | text: `Error finding tools in server '${args.serverName}': ${(error as Error).message}`, 278 | }, 279 | ], 280 | }; 281 | } 282 | } 283 | ); 284 | 285 | // Tool to list all connected servers 286 | server.tool( 287 | "list-servers", 288 | "List all connected MCP servers", 289 | {}, // No parameters needed 290 | async (args, extra) => { 291 | try { 292 | const servers = serverManager.listServers(); 293 | 294 | return { 295 | content: [ 296 | { 297 | type: "text", 298 | text: JSON.stringify({ servers }, null, 2), 299 | }, 300 | ], 301 | }; 302 | } catch (error) { 303 | return { 304 | content: [ 305 | { 306 | type: "text", 307 | text: `Error listing servers: ${(error as Error).message}`, 308 | }, 309 | ], 310 | }; 311 | } 312 | } 313 | ); 314 | 315 | // Start server 316 | async function startServer() { 317 | try { 318 | // Communication through standard input/output 319 | const transport = new StdioServerTransport(); 320 | await server.connect(transport); 321 | 322 | // Disconnect all connections on process termination 323 | process.on("SIGINT", async () => { 324 | console.log("Shutting down server..."); 325 | await serverManager.disconnectAll(); 326 | process.exit(0); 327 | }); 328 | } catch (error) { 329 | console.error("Failed to start server:", error); 330 | process.exit(1); 331 | } 332 | } 333 | 334 | startServer(); 335 | ``` -------------------------------------------------------------------------------- /src/server-manager.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 3 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; 4 | import { 5 | ConnectMcpParams, 6 | McpConfig, 7 | McpServerConfig, 8 | } from "./types.js"; 9 | import fs from "fs"; 10 | import path from "path"; 11 | 12 | /** 13 | * Find configuration file path 14 | * Check in order: environment variable > command line argument > default path 15 | */ 16 | function findConfigPath(): string | undefined { 17 | // Check environment variable 18 | if (process.env.MCP_CONFIG_PATH) { 19 | return process.env.MCP_CONFIG_PATH; 20 | } 21 | 22 | // Check command line arguments 23 | const configArgIndex = process.argv.findIndex( 24 | (arg) => arg === "--config-path" 25 | ); 26 | if ( 27 | configArgIndex !== -1 && 28 | configArgIndex < process.argv.length - 1 29 | ) { 30 | return process.argv[configArgIndex + 1]; 31 | } 32 | 33 | // Check default paths 34 | const defaultPaths = [ 35 | "./mcp-config.json", 36 | path.join(process.cwd(), "mcp-config.json"), 37 | ]; 38 | 39 | for (const defaultPath of defaultPaths) { 40 | if (fs.existsSync(defaultPath)) { 41 | return defaultPath; 42 | } 43 | } 44 | 45 | return undefined; 46 | } 47 | 48 | /** 49 | * Load configuration file 50 | */ 51 | function loadConfigFile(configPath: string): McpConfig { 52 | try { 53 | const configContent = fs.readFileSync( 54 | configPath, 55 | "utf-8" 56 | ); 57 | return JSON.parse(configContent) as McpConfig; 58 | } catch (error) { 59 | console.error( 60 | `Failed to load configuration file: ${ 61 | (error as Error).message 62 | }` 63 | ); 64 | throw new Error( 65 | `Failed to load configuration file '${configPath}': ${ 66 | (error as Error).message 67 | }` 68 | ); 69 | } 70 | } 71 | 72 | export class McpServerManager { 73 | private clients: Map<string, Client> = new Map(); 74 | private configPath?: string; 75 | 76 | /** 77 | * MCP Server Manager constructor 78 | */ 79 | constructor(options?: { 80 | configPath?: string; 81 | autoLoad?: boolean; 82 | }) { 83 | this.configPath = 84 | options?.configPath || findConfigPath(); 85 | 86 | if (options?.autoLoad && this.configPath) { 87 | try { 88 | this.loadFromConfig(this.configPath); 89 | } catch (error) { 90 | console.error( 91 | `Failed to load servers from configuration file: ${ 92 | (error as Error).message 93 | }` 94 | ); 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * Load server configuration from configuration file 101 | */ 102 | async loadFromConfig(configPath?: string): Promise<void> { 103 | const path = configPath || this.configPath; 104 | if (!path) { 105 | throw new Error( 106 | "Configuration file path not specified." 107 | ); 108 | } 109 | 110 | const config = loadConfigFile(path); 111 | 112 | if ( 113 | !config.mcpServers || 114 | Object.keys(config.mcpServers).length === 0 115 | ) { 116 | console.warn( 117 | "No server information in configuration file." 118 | ); 119 | return; 120 | } 121 | 122 | // Connect to all servers 123 | const serverEntries = Object.entries(config.mcpServers); 124 | for (const [ 125 | serverName, 126 | serverConfig, 127 | ] of serverEntries) { 128 | if (this.clients.has(serverName)) { 129 | continue; 130 | } 131 | 132 | try { 133 | await this.connectToServer( 134 | serverName, 135 | serverConfig 136 | ); 137 | } catch (error) { 138 | console.error( 139 | `Failed to connect to server '${serverName}' from configuration file: ${ 140 | (error as Error).message 141 | }` 142 | ); 143 | } 144 | } 145 | } 146 | 147 | /** 148 | * Connect to MCP server. 149 | */ 150 | async connectToServer( 151 | serverName: string, 152 | params: ConnectMcpParams | McpServerConfig 153 | ): Promise<void> { 154 | if (this.clients.has(serverName)) { 155 | throw new Error( 156 | `Already connected to server '${serverName}'.` 157 | ); 158 | } 159 | 160 | // Determine transport type 161 | const transportType = params.type || (params.command ? "stdio" : "http"); 162 | 163 | let transport: StdioClientTransport | StreamableHTTPClientTransport; 164 | 165 | if (transportType === "http") { 166 | // HTTP transport 167 | if (!params.url) { 168 | throw new Error( 169 | `HTTP server '${serverName}' requires a URL.` 170 | ); 171 | } 172 | 173 | const url = new URL(params.url); 174 | 175 | // Create transport with headers in requestInit 176 | const transportOptions: any = {}; 177 | if (params.headers) { 178 | transportOptions.requestInit = { 179 | headers: params.headers 180 | }; 181 | } 182 | 183 | transport = new StreamableHTTPClientTransport(url, transportOptions); 184 | } else { 185 | // Stdio transport 186 | if (!params.command) { 187 | throw new Error( 188 | `Stdio server '${serverName}' requires a command.` 189 | ); 190 | } 191 | 192 | // Set environment variables 193 | const env: Record<string, string | undefined> = { 194 | ...process.env, 195 | }; 196 | if ("env" in params && params.env) { 197 | Object.assign(env, params.env); 198 | } 199 | 200 | transport = new StdioClientTransport({ 201 | command: params.command, 202 | args: params.args || [], 203 | env: env as Record<string, string>, 204 | }); 205 | } 206 | 207 | const client = new Client({ 208 | name: `mcp-client-${serverName}`, 209 | version: "1.0.0", 210 | }); 211 | 212 | try { 213 | await client.connect(transport); 214 | this.clients.set(serverName, client); 215 | } catch (error) { 216 | console.error( 217 | `Failed to connect to server '${serverName}':`, 218 | error 219 | ); 220 | throw new Error( 221 | `Failed to connect to server '${serverName}': ${ 222 | (error as Error).message 223 | }` 224 | ); 225 | } 226 | } 227 | 228 | /** 229 | * Return the list of tools from connected server. 230 | */ 231 | async listTools(serverName: string): Promise<any> { 232 | const client = this.getClient(serverName); 233 | return await client.listTools(); 234 | } 235 | 236 | /** 237 | * Get a specific tool with complete schema from a connected server. 238 | */ 239 | async getTool(serverName: string, toolName: string): Promise<any> { 240 | const client = this.getClient(serverName); 241 | const toolsResponse = await client.listTools(); 242 | 243 | if (!toolsResponse.tools || !Array.isArray(toolsResponse.tools)) { 244 | throw new Error(`No tools found on server '${serverName}'`); 245 | } 246 | 247 | const tool = toolsResponse.tools.find((t: any) => t.name === toolName); 248 | 249 | if (!tool) { 250 | throw new Error(`Tool '${toolName}' not found on server '${serverName}'`); 251 | } 252 | 253 | return tool; 254 | } 255 | 256 | /** 257 | * List tools from a specific server (name and description only). 258 | */ 259 | async listToolsInServer(serverName: string): Promise<any> { 260 | const client = this.getClient(serverName); 261 | const toolsResponse = await client.listTools(); 262 | 263 | if (!toolsResponse.tools || !Array.isArray(toolsResponse.tools)) { 264 | return { tools: [] }; 265 | } 266 | 267 | // Filter to only include name and description 268 | return { 269 | tools: toolsResponse.tools.map((tool: any) => ({ 270 | name: tool.name, 271 | description: tool.description, 272 | })) 273 | }; 274 | } 275 | 276 | /** 277 | * Find tools matching a pattern in a specific server (name and description only). 278 | */ 279 | async findToolsInServer( 280 | serverName: string, 281 | pattern: string, 282 | searchIn: "name" | "description" | "both" = "both", 283 | caseSensitive: boolean = false 284 | ): Promise<any[]> { 285 | const client = this.getClient(serverName); 286 | const toolsResponse = await client.listTools(); 287 | 288 | if (!toolsResponse.tools || !Array.isArray(toolsResponse.tools)) { 289 | return []; 290 | } 291 | 292 | const flags = caseSensitive ? "g" : "gi"; 293 | const regex = new RegExp(pattern, flags); 294 | 295 | const matchedTools = toolsResponse.tools.filter((tool: any) => { 296 | const nameMatch = searchIn !== "description" && tool.name && regex.test(tool.name); 297 | const descriptionMatch = searchIn !== "name" && tool.description && regex.test(tool.description); 298 | return nameMatch || descriptionMatch; 299 | }); 300 | 301 | // Filter to only include name and description 302 | return matchedTools.map((tool: any) => ({ 303 | name: tool.name, 304 | description: tool.description, 305 | })); 306 | } 307 | 308 | /** 309 | * List all connected server names. 310 | */ 311 | listServers(): string[] { 312 | return this.getConnectedServers(); 313 | } 314 | 315 | /** 316 | * Call a tool on server. 317 | */ 318 | async callTool( 319 | serverName: string, 320 | toolName: string, 321 | args: Record<string, unknown> 322 | ): Promise<any> { 323 | const client = this.getClient(serverName); 324 | return await client.callTool({ 325 | name: toolName, 326 | arguments: args, 327 | }); 328 | } 329 | 330 | /** 331 | * Return all connected server names. 332 | */ 333 | getConnectedServers(): string[] { 334 | return Array.from(this.clients.keys()); 335 | } 336 | 337 | /** 338 | * Find tools matching a pattern across all connected servers. 339 | */ 340 | async findTools( 341 | pattern: string, 342 | options: { 343 | searchIn?: "name" | "description" | "both"; 344 | caseSensitive?: boolean; 345 | } = {} 346 | ): Promise<Record<string, any[]>> { 347 | const { searchIn = "both", caseSensitive = false } = options; 348 | const servers = this.getConnectedServers(); 349 | 350 | if (servers.length === 0) { 351 | return {}; 352 | } 353 | 354 | // Create regex pattern 355 | let regex: RegExp; 356 | try { 357 | regex = new RegExp(pattern, caseSensitive ? "" : "i"); 358 | } catch (error) { 359 | throw new Error(`Invalid regex pattern: ${(error as Error).message}`); 360 | } 361 | 362 | const results: Record<string, any[]> = {}; 363 | 364 | // Search tools in each server 365 | for (const serverName of servers) { 366 | try { 367 | const toolsResponse = await this.listTools(serverName); 368 | 369 | if (toolsResponse.tools && Array.isArray(toolsResponse.tools)) { 370 | const matchedTools = toolsResponse.tools.filter((tool: any) => { 371 | const nameMatch = searchIn !== "description" && tool.name && regex.test(tool.name); 372 | const descriptionMatch = searchIn !== "name" && tool.description && regex.test(tool.description); 373 | return nameMatch || descriptionMatch; 374 | }).map((tool: any) => ({ 375 | name: tool.name, 376 | description: tool.description, 377 | })); 378 | 379 | if (matchedTools.length > 0) { 380 | results[serverName] = matchedTools; 381 | } 382 | } 383 | } catch (error) { 384 | // Include error information in results 385 | results[serverName] = [{ 386 | error: `Failed to search tools: ${(error as Error).message}` 387 | }]; 388 | } 389 | } 390 | 391 | return results; 392 | } 393 | 394 | /** 395 | * Disconnect from server. 396 | */ 397 | async disconnectServer( 398 | serverName: string 399 | ): Promise<void> { 400 | const client = this.clients.get(serverName); 401 | if (!client) { 402 | throw new Error( 403 | `Not connected to server '${serverName}'.` 404 | ); 405 | } 406 | try { 407 | await client.close(); 408 | this.clients.delete(serverName); 409 | } catch (error) { 410 | console.error( 411 | `Failed to disconnect from server '${serverName}':`, 412 | error 413 | ); 414 | throw new Error( 415 | `Failed to disconnect from server '${serverName}': ${ 416 | (error as Error).message 417 | }` 418 | ); 419 | } 420 | } 421 | 422 | /** 423 | * Disconnect from all servers. 424 | */ 425 | async disconnectAll(): Promise<void> { 426 | const serverNames = this.getConnectedServers(); 427 | for (const serverName of serverNames) { 428 | await this.disconnectServer(serverName); 429 | } 430 | } 431 | 432 | private getClient(serverName: string): Client { 433 | const client = this.clients.get(serverName); 434 | if (!client) { 435 | throw new Error( 436 | `Not connected to server '${serverName}'.` 437 | ); 438 | } 439 | return client; 440 | } 441 | } 442 | ```