#
tokens: 14887/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=mcp-hub&config=eyJjb21tYW5kIjoibnB4IC15IG1jcC1odWItbWNwIC0tY29uZmlnLXBhdGggfi8uY3Vyc29yL21jcC1odWIuanNvbiJ9)
  6 | 
  7 | [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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 | 
```