# 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: -------------------------------------------------------------------------------- ``` # Node.js related node_modules/ npm-debug.log yarn-debug.log yarn-error.log # Build outputs dist/ build/ # Logs logs/ *.log # IDE related .idea/ .vscode/ *.swp *.swo # Environment variables .env .env.* !.env.example # OS related .DS_Store Thumbs.db .history .cursor ``` -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- ```json { "branches": [ "main" ], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", "@semantic-release/npm", "@semantic-release/github", [ "@semantic-release/git", { "assets": [ "package.json", "pnpm-lock.yaml", "CHANGELOG.md" ], "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } ] ] } ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown <p align="center"> <img src="https://imgur.com/DgWxkmv.png" width="200" height="200"> </p> [](https://cursor.com/install-mcp?name=mcp-hub&config=eyJjb21tYW5kIjoibnB4IC15IG1jcC1odWItbWNwIC0tY29uZmlnLXBhdGggfi8uY3Vyc29yL21jcC1odWIuanNvbiJ9) [](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) # MCP-Hub-MCP Server A hub server that connects to and manages other MCP (Model Context Protocol) servers. ## Overview This project builds an MCP hub server that connects to and manages multiple MCP (Model Context Protocol) servers through a single interface. 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. This reduces AI mistakes and improves performance by keeping the active tool set focused and manageable. ## Key Features - Automatic connection to other MCP servers via configuration file - List available tools on connected servers - Call tools on connected servers and return results ## Configuration Add this to your `mcp.json`: #### Using npx ```json { "mcpServers": { "other-tools": { "command": "npx", "args": [ "-y", "mcp-hub-mcp", "--config-path", "/Users/username/mcp.json" ] } } } ``` ## Installation and Running ### Requirements - Node.js 18.0.0 or higher - npm, yarn, or pnpm ### Installation ```bash # Clone repository git clone <repository-url> cd mcp-hub-mcp # Install dependencies npm install # or yarn install # or pnpm install ``` ### Build ```bash npm run build # or yarn build # or pnpm build ``` ### Run ```bash npm start # or yarn start # or pnpm start ``` ### Development Mode ```bash npm run dev # or yarn dev # or pnpm dev ``` ## Configuration File The MCP-Hub-MCP server uses a Claude Desktop format configuration file to automatically connect to other MCP servers. You can specify the configuration file in the following ways: 1. Environment variable: Set the `MCP_CONFIG_PATH` environment variable to the configuration file path 2. Command line argument: Use the `--config-path` option to specify the configuration file path 3. Default path: Use `mcp-config.json` file in the current directory Configuration file format: ```json { "mcpServers": { "serverName1": { "command": "command", "args": ["arg1", "arg2", ...], "env": { "ENV_VAR1": "value1", ... } }, "serverName2": { "command": "anotherCommand", "args": ["arg1", "arg2", ...] } } } ``` Example: ```json { "mcpServers": { "filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "/Users/username/Desktop", "/Users/username/Downloads" ] }, "other-server": { "command": "node", "args": ["path/to/other-mcp-server.js"] } } } ``` ## Usage The MCP-Hub-MCP server provides the following tools: ### 1. `list-all-tools` Returns a list of tools from all connected servers. ```json { "name": "list-all-tools", "arguments": {} } ``` ### 2. `call-tool` Calls a tool on a specific server. - `serverName`: Name of the MCP server to call the tool from - `toolName`: Name of the tool to call - `toolArgs`: Arguments to pass to the tool ```json { "name": "call-tool", "arguments": { "serverName": "filesystem", "toolName": "readFile", "toolArgs": { "path": "/Users/username/Desktop/example.txt" } } } ``` ### 3. `find-tools` Find tools matching a regex pattern across all connected servers (grep-like functionality). - `pattern`: Regex pattern to search for in tool names and descriptions - `searchIn`: Where to search: "name", "description", or "both" (default: "both") - `caseSensitive`: Whether the search should be case-sensitive (default: false) ```json { "name": "find-tools", "arguments": { "pattern": "file", "searchIn": "both", "caseSensitive": false } } ``` Example patterns: - `"file"` - Find all tools containing "file" - `"^read"` - Find all tools starting with "read" - `"(read|write).*file"` - Find tools for reading or writing files - `"config$"` - Find tools ending with "config" Example output: ```json { "filesystem": [ { "name": "readFile", "description": "Read the contents of a file", "inputSchema": { "type": "object", "properties": { "path": { "type": "string", "description": "Path to the file to read" } }, "required": ["path"] } }, { "name": "writeFile", "description": "Write content to a file", "inputSchema": { "type": "object", "properties": { "path": { "type": "string", "description": "Path to the file to write" }, "content": { "type": "string", "description": "Content to write to the file" } }, "required": ["path", "content"] } } ] } ``` ## Commit Message Convention This project follows [Conventional Commits](https://www.conventionalcommits.org/) for automatic versioning and CHANGELOG generation. Format: `<type>(<scope>): <description>` Examples: - `feat: add new hub connection feature` - `fix: resolve issue with server timeout` - `docs: update API documentation` - `chore: update dependencies` Types: - `feat`: New feature (MINOR version bump) - `fix`: Bug fix (PATCH version bump) - `docs`: Documentation only changes - `style`: Changes that do not affect the meaning of the code - `refactor`: Code change that neither fixes a bug nor adds a feature - `perf`: Code change that improves performance - `test`: Adding missing tests or correcting existing tests - `chore`: Changes to the build process or auxiliary tools Breaking Changes: Add `BREAKING CHANGE:` in the commit footer to trigger a MAJOR version bump. ## Other Links - [MCP Reviews](https://mcpreview.com/mcp-servers/warpdev/mcp-hub-mcp) ## License MIT ``` -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- ```markdown # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview 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. ## Development Commands ### Build and Run ```bash # Install dependencies pnpm install # Build TypeScript to JavaScript pnpm build # Run the server (after building) pnpm start # Build and run in one command pnpm dev ``` ### Development Workflow ```bash # For testing changes locally with npx npm link # Creates a global link to the local package npx mcp-hub-mcp # Test the linked version # To unlink after testing npm unlink -g mcp-hub-mcp ``` ### Release Process - Uses semantic-release with Conventional Commits - Commits should follow: `type(scope): description` - Types: feat, fix, docs, style, refactor, perf, test, chore - Breaking changes: add `!` after type or `BREAKING CHANGE:` in body - Releases are automated via GitHub Actions on the main branch ## Architecture ### Core Components 1. **src/index.ts** - Entry point that creates the MCP server with three tools: - `list-all-tools`: Lists all available tools from connected servers - `call-tool`: Executes a tool on a specific server - `find-tools`: Grep-like search for tools matching regex patterns 2. **src/server-manager.ts** - `McpServerManager` class that: - Loads server configurations from JSON file - Manages connections to multiple MCP servers - Proxies tool calls to appropriate servers - Handles server lifecycle and error recovery - Implements `findTools` method for regex-based tool search 3. **src/types.ts** - Type definitions and Zod schemas for: - Server configuration validation - MCP SDK type exports - Configuration file structure - Parameter schemas for all tools ### Configuration The server configuration is loaded from (in order of precedence): 1. Environment variable: `MCP_CONFIG_PATH` 2. Command-line argument: `--config-path` 3. Default location: `./mcp-config.json` or `{cwd}/mcp-config.json` Configuration format (Claude Desktop compatible): ```json { "mcpServers": { "server-name": { "command": "command-to-run", "args": ["arg1", "arg2"], "env": { "KEY": "value" } } } } ``` ### Tool Naming Convention Tools from connected servers are prefixed with the server name: - Original tool: `list-files` - Through hub: `server-name__list-files` ## Important Considerations 1. **Error Handling**: The hub gracefully handles server connection failures and continues operating with available servers. 2. **Environment Variables**: Server configurations can include environment variables that are passed to child processes. 3. **Logging**: Uses console.error for error messages since stdout is reserved for MCP protocol communication. 4. **No Tests**: Currently no test framework is set up. When implementing tests, consider: - Unit tests for server-manager.ts logic - Integration tests for MCP protocol handling - Mock MCP server responses for testing 5. **TypeScript Configuration**: - Target: ES2020 - Module: NodeNext with NodeNext resolution - Strict mode enabled - Outputs to `dist/` directory - Declaration files generated 6. **Dependencies**: - Keep dependencies minimal (currently only @modelcontextprotocol/sdk and zod) - All types are exported from types.ts for consistency 7. **Binary Execution**: Configured as npm binary `mcp-hub-mcp` with shebang in index.ts ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "strict": true, "outDir": "dist", "declaration": true, "skipLibCheck": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "mcp-hub-mcp", "version": "1.3.0", "description": "MCP Hub server that connects to and manages other MCP servers", "main": "dist/index.js", "type": "module", "files": [ "dist", "README.md", "LICENSE" ], "bin": { "mcp-hub-mcp": "dist/index.js" }, "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsc && node dist/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "mcp", "model-context-protocol" ], "author": "", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.13.0", "zod": "^3.25.67" }, "devDependencies": { "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@types/node": "^22.15.32", "semantic-release": "^24.2.5", "typescript": "^5.8.3" } } ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml name: Release on: push: branches: [main] permissions: contents: write issues: write pull-requests: write jobs: release: name: Release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: "lts/*" - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 9.12.2 run_install: false - name: Get pnpm store directory shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache uses: actions/cache@v3 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install - name: Build project run: pnpm build - name: Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: npx semantic-release ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown # [1.3.0](https://github.com/warpdev/mcp-hub-mcp/compare/v1.2.0...v1.3.0) (2025-07-05) ### Features * enhance MCP server functionality with new tools and transport options ([ae319dd](https://github.com/warpdev/mcp-hub-mcp/commit/ae319dd22311f3d2d6beaf07f53b7d5621bd7543)) # [1.2.0](https://github.com/warpdev/mcp-hub-mcp/compare/v1.1.1...v1.2.0) (2025-06-21) ### Features * add find-tools command for grep-like tool search ([8ffc2e7](https://github.com/warpdev/mcp-hub-mcp/commit/8ffc2e7e0012d8a8df7c0e399341c27ec6771c5b)) * enhance tool descriptions to promote find-tools usage ([353169d](https://github.com/warpdev/mcp-hub-mcp/commit/353169d92c55a67973d2d0704d3c447d03b4fab2)) ## [1.1.1](https://github.com/warpdev/mcp-hub-mcp/compare/v1.1.0...v1.1.1) (2025-06-02) ### Bug Fixes * allow Claude Desktop to use this MCP ([6406cda](https://github.com/warpdev/mcp-hub-mcp/commit/6406cdaaeaf554cc1fb5c2194a8024280d603a9c)) # [1.1.0](https://github.com/warpdev/mcp-hub-mcp/compare/v1.0.3...v1.1.0) (2025-04-16) ### Features * add description for better ai recognition ([0fd23a2](https://github.com/warpdev/mcp-hub-mcp/commit/0fd23a2d53337cf8fa36604c26bbccf7bcadcce1)) ## [1.0.3](https://github.com/warpdev/mcp-hub-mcp/compare/v1.0.2...v1.0.3) (2025-04-11) ### Bug Fixes * fix run with npx cli ([c44218c](https://github.com/warpdev/mcp-hub-mcp/commit/c44218c5e56f25c399a267075238404b806ee451)) ## [1.0.2](https://github.com/warpdev/mcp-hub-mcp/compare/v1.0.1...v1.0.2) (2025-04-11) ### Bug Fixes * fix missing bin ([7dd5a1f](https://github.com/warpdev/mcp-hub-mcp/commit/7dd5a1fc5e8e701c0135f4f31dddeec168a663bb)) ## [1.0.1](https://github.com/warpdev/mcp-hub-mcp/compare/v1.0.0...v1.0.1) (2025-04-11) ### Bug Fixes * fix package publish files ([627a30b](https://github.com/warpdev/mcp-hub-mcp/commit/627a30b74183e1dadc45aa5cec02ec3de374f165)) # 1.0.0 (2025-04-11) ### Bug Fixes * update GitHub Actions workflows to use pnpm instead of npm ([fd9f19e](https://github.com/warpdev/mcp-hub-mcp/commit/fd9f19e70f73a0cdba43dfd9132da850a4a3a760)) ### Features * add semantic-release for automated versioning ([ee3ebfd](https://github.com/warpdev/mcp-hub-mcp/commit/ee3ebfd84f34bef7b53200c74c8bb9fd75d69e21)) ``` -------------------------------------------------------------------------------- /.github/workflows/pr-feedback.yml: -------------------------------------------------------------------------------- ```yaml name: PR Feedback on: pull_request: types: - opened - synchronize - reopened - labeled - unlabeled permissions: contents: read pull-requests: write jobs: preview: name: Preview Release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: "lts/*" - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 9.12.2 run_install: false - name: Get pnpm store directory shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache uses: actions/cache@v3 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install - name: Preview Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npx semantic-release --dry-run - name: Find Comment uses: peter-evans/find-comment@v2 id: fc with: issue-number: ${{ github.event.pull_request.number }} comment-author: "github-actions[bot]" body-includes: "### Semantic Release Preview" - name: Generate Release Notes id: release_notes run: | echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV npx semantic-release --dry-run | grep -A 100 "Release note for" | sed 's/`//g' >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV - name: Create or Update Comment uses: peter-evans/create-or-update-comment@v2 with: comment-id: ${{ steps.fc.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} body: | ### Semantic Release Preview When this PR is merged to main, the following release will be created: ${{ env.RELEASE_NOTES }} The version is determined by [Conventional Commits](https://www.conventionalcommits.org/): - `fix:` = PATCH release (1.0.0 → 1.0.1) - `feat:` = MINOR release (1.0.0 → 1.1.0) - `BREAKING CHANGE:` = MAJOR release (1.0.0 → 2.0.0) edit-mode: replace ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; export const ConnectMcpParamsSchema = z.object({ // For stdio servers command: z .string() .describe("Command to run the MCP server") .optional(), args: z .array(z.string()) .optional() .describe("Command arguments"), // For HTTP servers type: z .enum(["stdio", "http"]) .optional() .describe("Server transport type"), url: z .string() .describe("URL for HTTP-based MCP server") .optional(), headers: z .record(z.string()) .optional() .describe("HTTP headers for authentication"), // Environment variables (applies to both) env: z .record(z.string()) .optional() .describe("Environment variables"), }); export type ConnectMcpParams = z.infer< typeof ConnectMcpParamsSchema >; export const ListToolsParamsSchema = z.object({ serverName: z .string() .describe("Name of the MCP server to list tools from"), }); export type ListToolsParams = z.infer< typeof ListToolsParamsSchema >; export const CallToolParamsSchema = z.object({ serverName: z .string() .describe("Name of the MCP server to call tool from"), toolName: z.string().describe("Name of the tool to call"), toolArgs: z .record(z.unknown()) .describe("Arguments to pass to the tool"), }); export type CallToolParams = z.infer< typeof CallToolParamsSchema >; export const FindToolsParamsSchema = z.object({ pattern: z .string() .describe("Regex pattern to search for in tool names and descriptions"), searchIn: z .enum(["name", "description", "both"]) .optional() .default("both") .describe("Where to search: in tool names, descriptions, or both"), caseSensitive: z .boolean() .optional() .default(false) .describe("Whether the search should be case-sensitive"), }); export type FindToolsParams = z.infer< typeof FindToolsParamsSchema >; export const GetToolParamsSchema = z.object({ serverName: z .string() .describe("Name of the MCP server containing the tool"), toolName: z .string() .describe("Exact name of the tool to retrieve"), }); export type GetToolParams = z.infer< typeof GetToolParamsSchema >; export const ListToolsInServerParamsSchema = z.object({ serverName: z .string() .describe("Name of the MCP server to list tools from"), }); export type ListToolsInServerParams = z.infer< typeof ListToolsInServerParamsSchema >; export const FindToolsInServerParamsSchema = z.object({ serverName: z .string() .describe("Name of the MCP server to search tools in"), pattern: z .string() .describe("Regex pattern to search for in tool names and descriptions"), searchIn: z .enum(["name", "description", "both"]) .default("both") .describe("Where to search: in tool names, descriptions, or both"), caseSensitive: z .boolean() .default(false) .describe("Whether the search should be case-sensitive"), }); export type FindToolsInServerParams = z.infer< typeof FindToolsInServerParamsSchema >; // MCP configuration file interface (claude_desktop_config.json format) export interface McpServerConfig { // For stdio servers command?: string; args?: string[]; env?: Record<string, string>; // For HTTP servers type?: "stdio" | "http"; url?: string; headers?: Record<string, string>; } export interface McpConfig { mcpServers: Record<string, McpServerConfig>; } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { McpServerManager } from "./server-manager.js"; import { CallToolParamsSchema, FindToolsParamsSchema, GetToolParamsSchema, ListToolsInServerParamsSchema, FindToolsInServerParamsSchema } from "./types.js"; // Create MCP server manager instance (auto load enabled) const serverManager = new McpServerManager({ autoLoad: true, }); // Create MCP server const server = new McpServer({ name: "MCP-Hub-Server", version: "1.0.0", description: "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.", }); // Tool to return tools list from all servers server.tool( "list-all-tools", "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", {}, // Use empty object when there are no parameters async (args, extra) => { try { const servers = serverManager.getConnectedServers(); if (servers.length === 0) { return { content: [ { type: "text", text: "No connected servers.", }, ], }; } const allTools: Record<string, any> = {}; // Get tools list from each server for (const serverName of servers) { try { const toolsResponse = await serverManager.listTools(serverName); // Filter to only include name and description if (toolsResponse.tools && Array.isArray(toolsResponse.tools)) { allTools[serverName] = { tools: toolsResponse.tools.map((tool: any) => ({ name: tool.name, description: tool.description, })) }; } else { allTools[serverName] = toolsResponse; } } catch (error) { allTools[serverName] = { error: `Failed to get tools list: ${(error as Error).message}`, }; } } return { content: [ { type: "text", text: JSON.stringify(allTools, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Failed to get tools list from all servers: ${ (error as Error).message }`, }, ], isError: true, }; } }, ); // Tool to call a specific tool from a specific server server.tool( "call-tool", "Call a specific tool from a specific server. TIP: Use find-tools first to discover the tool and get the correct serverName and toolName", { serverName: CallToolParamsSchema.shape.serverName, toolName: CallToolParamsSchema.shape.toolName, toolArgs: CallToolParamsSchema.shape.toolArgs, }, async (args, extra) => { try { const { serverName, toolName, toolArgs } = args; const result = await serverManager.callTool( serverName, toolName, toolArgs, ); return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Tool call failed: ${(error as Error).message}`, }, ], isError: true, }; } }, ); // Tool to find tools matching a pattern across all servers server.tool( "find-tools", `Use this tool to find best tools by searching with keywords or regex patterns. If you don't have a specific tool for a task, this is the best way to discover what tools are available. `, { pattern: FindToolsParamsSchema.shape.pattern, searchIn: FindToolsParamsSchema.shape.searchIn, caseSensitive: FindToolsParamsSchema.shape.caseSensitive, }, async (args, extra) => { try { const { pattern, searchIn, caseSensitive } = args; const results = await serverManager.findTools(pattern, { searchIn, caseSensitive, }); return { content: [ { type: "text", text: JSON.stringify(results, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Failed to find tools: ${(error as Error).message}`, }, ], isError: true, }; } }, ); // Tool to get detailed information about a specific tool server.tool( "get-tool", "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", { serverName: GetToolParamsSchema.shape.serverName, toolName: GetToolParamsSchema.shape.toolName, }, async (args, extra) => { try { const { serverName, toolName } = args; const tool = await serverManager.getTool(serverName, toolName); return { content: [ { type: "text", text: JSON.stringify(tool, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error getting tool: ${(error as Error).message}`, }, ], }; } } ); // Tool to list all tools from a specific server server.tool( "list-all-tools-in-server", "List ALL tools from a specific MCP server (returns name and description only)", { serverName: ListToolsInServerParamsSchema.shape.serverName, }, async (args, extra) => { try { const { serverName } = args; const result = await serverManager.listToolsInServer(serverName); return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error listing tools from server '${args.serverName}': ${(error as Error).message}`, }, ], }; } } ); // Tool to find tools in a specific server server.tool( "find-tools-in-server", "Find tools matching a pattern in a specific MCP server (returns name and description only)", { serverName: FindToolsInServerParamsSchema.shape.serverName, pattern: FindToolsInServerParamsSchema.shape.pattern, searchIn: FindToolsInServerParamsSchema.shape.searchIn, caseSensitive: FindToolsInServerParamsSchema.shape.caseSensitive, }, async (args, extra) => { try { const { serverName, pattern, searchIn, caseSensitive } = args; const results = await serverManager.findToolsInServer( serverName, pattern, searchIn, caseSensitive ); return { content: [ { type: "text", text: JSON.stringify({ tools: results }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error finding tools in server '${args.serverName}': ${(error as Error).message}`, }, ], }; } } ); // Tool to list all connected servers server.tool( "list-servers", "List all connected MCP servers", {}, // No parameters needed async (args, extra) => { try { const servers = serverManager.listServers(); return { content: [ { type: "text", text: JSON.stringify({ servers }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error listing servers: ${(error as Error).message}`, }, ], }; } } ); // Start server async function startServer() { try { // Communication through standard input/output const transport = new StdioServerTransport(); await server.connect(transport); // Disconnect all connections on process termination process.on("SIGINT", async () => { console.log("Shutting down server..."); await serverManager.disconnectAll(); process.exit(0); }); } catch (error) { console.error("Failed to start server:", error); process.exit(1); } } startServer(); ``` -------------------------------------------------------------------------------- /src/server-manager.ts: -------------------------------------------------------------------------------- ```typescript import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { ConnectMcpParams, McpConfig, McpServerConfig, } from "./types.js"; import fs from "fs"; import path from "path"; /** * Find configuration file path * Check in order: environment variable > command line argument > default path */ function findConfigPath(): string | undefined { // Check environment variable if (process.env.MCP_CONFIG_PATH) { return process.env.MCP_CONFIG_PATH; } // Check command line arguments const configArgIndex = process.argv.findIndex( (arg) => arg === "--config-path" ); if ( configArgIndex !== -1 && configArgIndex < process.argv.length - 1 ) { return process.argv[configArgIndex + 1]; } // Check default paths const defaultPaths = [ "./mcp-config.json", path.join(process.cwd(), "mcp-config.json"), ]; for (const defaultPath of defaultPaths) { if (fs.existsSync(defaultPath)) { return defaultPath; } } return undefined; } /** * Load configuration file */ function loadConfigFile(configPath: string): McpConfig { try { const configContent = fs.readFileSync( configPath, "utf-8" ); return JSON.parse(configContent) as McpConfig; } catch (error) { console.error( `Failed to load configuration file: ${ (error as Error).message }` ); throw new Error( `Failed to load configuration file '${configPath}': ${ (error as Error).message }` ); } } export class McpServerManager { private clients: Map<string, Client> = new Map(); private configPath?: string; /** * MCP Server Manager constructor */ constructor(options?: { configPath?: string; autoLoad?: boolean; }) { this.configPath = options?.configPath || findConfigPath(); if (options?.autoLoad && this.configPath) { try { this.loadFromConfig(this.configPath); } catch (error) { console.error( `Failed to load servers from configuration file: ${ (error as Error).message }` ); } } } /** * Load server configuration from configuration file */ async loadFromConfig(configPath?: string): Promise<void> { const path = configPath || this.configPath; if (!path) { throw new Error( "Configuration file path not specified." ); } const config = loadConfigFile(path); if ( !config.mcpServers || Object.keys(config.mcpServers).length === 0 ) { console.warn( "No server information in configuration file." ); return; } // Connect to all servers const serverEntries = Object.entries(config.mcpServers); for (const [ serverName, serverConfig, ] of serverEntries) { if (this.clients.has(serverName)) { continue; } try { await this.connectToServer( serverName, serverConfig ); } catch (error) { console.error( `Failed to connect to server '${serverName}' from configuration file: ${ (error as Error).message }` ); } } } /** * Connect to MCP server. */ async connectToServer( serverName: string, params: ConnectMcpParams | McpServerConfig ): Promise<void> { if (this.clients.has(serverName)) { throw new Error( `Already connected to server '${serverName}'.` ); } // Determine transport type const transportType = params.type || (params.command ? "stdio" : "http"); let transport: StdioClientTransport | StreamableHTTPClientTransport; if (transportType === "http") { // HTTP transport if (!params.url) { throw new Error( `HTTP server '${serverName}' requires a URL.` ); } const url = new URL(params.url); // Create transport with headers in requestInit const transportOptions: any = {}; if (params.headers) { transportOptions.requestInit = { headers: params.headers }; } transport = new StreamableHTTPClientTransport(url, transportOptions); } else { // Stdio transport if (!params.command) { throw new Error( `Stdio server '${serverName}' requires a command.` ); } // Set environment variables const env: Record<string, string | undefined> = { ...process.env, }; if ("env" in params && params.env) { Object.assign(env, params.env); } transport = new StdioClientTransport({ command: params.command, args: params.args || [], env: env as Record<string, string>, }); } const client = new Client({ name: `mcp-client-${serverName}`, version: "1.0.0", }); try { await client.connect(transport); this.clients.set(serverName, client); } catch (error) { console.error( `Failed to connect to server '${serverName}':`, error ); throw new Error( `Failed to connect to server '${serverName}': ${ (error as Error).message }` ); } } /** * Return the list of tools from connected server. */ async listTools(serverName: string): Promise<any> { const client = this.getClient(serverName); return await client.listTools(); } /** * Get a specific tool with complete schema from a connected server. */ async getTool(serverName: string, toolName: string): Promise<any> { const client = this.getClient(serverName); const toolsResponse = await client.listTools(); if (!toolsResponse.tools || !Array.isArray(toolsResponse.tools)) { throw new Error(`No tools found on server '${serverName}'`); } const tool = toolsResponse.tools.find((t: any) => t.name === toolName); if (!tool) { throw new Error(`Tool '${toolName}' not found on server '${serverName}'`); } return tool; } /** * List tools from a specific server (name and description only). */ async listToolsInServer(serverName: string): Promise<any> { const client = this.getClient(serverName); const toolsResponse = await client.listTools(); if (!toolsResponse.tools || !Array.isArray(toolsResponse.tools)) { return { tools: [] }; } // Filter to only include name and description return { tools: toolsResponse.tools.map((tool: any) => ({ name: tool.name, description: tool.description, })) }; } /** * Find tools matching a pattern in a specific server (name and description only). */ async findToolsInServer( serverName: string, pattern: string, searchIn: "name" | "description" | "both" = "both", caseSensitive: boolean = false ): Promise<any[]> { const client = this.getClient(serverName); const toolsResponse = await client.listTools(); if (!toolsResponse.tools || !Array.isArray(toolsResponse.tools)) { return []; } const flags = caseSensitive ? "g" : "gi"; const regex = new RegExp(pattern, flags); const matchedTools = toolsResponse.tools.filter((tool: any) => { const nameMatch = searchIn !== "description" && tool.name && regex.test(tool.name); const descriptionMatch = searchIn !== "name" && tool.description && regex.test(tool.description); return nameMatch || descriptionMatch; }); // Filter to only include name and description return matchedTools.map((tool: any) => ({ name: tool.name, description: tool.description, })); } /** * List all connected server names. */ listServers(): string[] { return this.getConnectedServers(); } /** * Call a tool on server. */ async callTool( serverName: string, toolName: string, args: Record<string, unknown> ): Promise<any> { const client = this.getClient(serverName); return await client.callTool({ name: toolName, arguments: args, }); } /** * Return all connected server names. */ getConnectedServers(): string[] { return Array.from(this.clients.keys()); } /** * Find tools matching a pattern across all connected servers. */ async findTools( pattern: string, options: { searchIn?: "name" | "description" | "both"; caseSensitive?: boolean; } = {} ): Promise<Record<string, any[]>> { const { searchIn = "both", caseSensitive = false } = options; const servers = this.getConnectedServers(); if (servers.length === 0) { return {}; } // Create regex pattern let regex: RegExp; try { regex = new RegExp(pattern, caseSensitive ? "" : "i"); } catch (error) { throw new Error(`Invalid regex pattern: ${(error as Error).message}`); } const results: Record<string, any[]> = {}; // Search tools in each server for (const serverName of servers) { try { const toolsResponse = await this.listTools(serverName); if (toolsResponse.tools && Array.isArray(toolsResponse.tools)) { const matchedTools = toolsResponse.tools.filter((tool: any) => { const nameMatch = searchIn !== "description" && tool.name && regex.test(tool.name); const descriptionMatch = searchIn !== "name" && tool.description && regex.test(tool.description); return nameMatch || descriptionMatch; }).map((tool: any) => ({ name: tool.name, description: tool.description, })); if (matchedTools.length > 0) { results[serverName] = matchedTools; } } } catch (error) { // Include error information in results results[serverName] = [{ error: `Failed to search tools: ${(error as Error).message}` }]; } } return results; } /** * Disconnect from server. */ async disconnectServer( serverName: string ): Promise<void> { const client = this.clients.get(serverName); if (!client) { throw new Error( `Not connected to server '${serverName}'.` ); } try { await client.close(); this.clients.delete(serverName); } catch (error) { console.error( `Failed to disconnect from server '${serverName}':`, error ); throw new Error( `Failed to disconnect from server '${serverName}': ${ (error as Error).message }` ); } } /** * Disconnect from all servers. */ async disconnectAll(): Promise<void> { const serverNames = this.getConnectedServers(); for (const serverName of serverNames) { await this.disconnectServer(serverName); } } private getClient(serverName: string): Client { const client = this.clients.get(serverName); if (!client) { throw new Error( `Not connected to server '${serverName}'.` ); } return client; } } ```