# Directory Structure
```
├── .gitattributes
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│ ├── client.ts
│ ├── fetch-capabilities.ts
│ ├── fetch-metamcp.ts
│ ├── fetch-tools.ts
│ ├── index.ts
│ ├── mcp-proxy.ts
│ ├── report-tools.ts
│ ├── sessions.ts
│ ├── sse.ts
│ ├── streamable-http.ts
│ ├── tool-logs.ts
│ └── utils.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
```
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
132 | dist/**/*
133 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MetaMCP MCP Server
2 |
3 | > **🚨 DEPRECATED PACKAGE WARNING 🚨**
4 | >
5 | > This local proxy package is deprecated in MetaMCP's 2.0 all-in-one architecture.
6 | >
7 | > **Please checkout https://github.com/metatool-ai/metamcp for more details.**
8 |
9 | MetaMCP MCP Server is a proxy server that joins multiple MCP servers into one. It fetches tool/prompt/resource configurations from MetaMCP App and routes tool/prompt/resource requests to the correct underlying server.
10 |
11 | [](https://smithery.ai/server/@metatool-ai/mcp-server-metamcp)
12 |
13 | <a href="https://glama.ai/mcp/servers/0po36lc7i6">
14 | <img width="380" height="200" src="https://glama.ai/mcp/servers/0po36lc7i6/badge" alt="MetaServer MCP server" />
15 | </a>
16 |
17 | MetaMCP App repo: https://github.com/metatool-ai/metatool-app
18 |
19 | ## Installation
20 |
21 | ### Installing via Smithery
22 |
23 | Sometimes Smithery works (confirmed in Windsurf locally) but sometimes it is unstable because MetaMCP is special that it runs other MCPs on top of it. Please consider using manual installation if it doesn't work instead.
24 |
25 | To install MetaMCP MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@metatool-ai/mcp-server-metamcp):
26 |
27 | ```bash
28 | npx -y @smithery/cli install @metatool-ai/mcp-server-metamcp --client claude
29 | ```
30 |
31 | ### Manual Installation
32 |
33 | ```bash
34 | export METAMCP_API_KEY=<env>
35 | npx -y @metamcp/mcp-server-metamcp@latest
36 | ```
37 |
38 | ```json
39 | {
40 | "mcpServers": {
41 | "MetaMCP": {
42 | "command": "npx",
43 | "args": ["-y", "@metamcp/mcp-server-metamcp@latest"],
44 | "env": {
45 | "METAMCP_API_KEY": "<your api key>"
46 | }
47 | }
48 | }
49 | }
50 | ```
51 |
52 | ## Usage
53 |
54 | ### Using as a stdio server (default)
55 |
56 | ```bash
57 | mcp-server-metamcp --metamcp-api-key <your-api-key>
58 | ```
59 |
60 | ### Using as an SSE server
61 |
62 | ```bash
63 | mcp-server-metamcp --metamcp-api-key <your-api-key> --transport sse --port 12006
64 | ```
65 |
66 | With the SSE transport option, the server will start an Express.js web server that listens for SSE connections on the `/sse` endpoint and accepts messages on the `/messages` endpoint.
67 |
68 | ### Using as a Streamable HTTP server
69 |
70 | ```bash
71 | mcp-server-metamcp --metamcp-api-key <your-api-key> --transport streamable-http --port 12006
72 | ```
73 |
74 | With the Streamable HTTP transport option, the server will start an Express.js web server that handles HTTP requests. You can optionally use `--stateless` mode for stateless operation.
75 |
76 | ### Using with Docker
77 |
78 | When running the server inside a Docker container and connecting to services on the host machine, use the `--use-docker-host` option to automatically transform localhost URLs:
79 |
80 | ```bash
81 | mcp-server-metamcp --metamcp-api-key <your-api-key> --transport sse --port 12006 --use-docker-host
82 | ```
83 |
84 | This will transform any localhost or 127.0.0.1 URLs to `host.docker.internal`, allowing the container to properly connect to services running on the host.
85 |
86 | ### Configuring stderr handling
87 |
88 | For STDIO transport, you can control how stderr is handled from child MCP processes:
89 |
90 | ```bash
91 | # Use inherit to see stderr output from child processes
92 | mcp-server-metamcp --metamcp-api-key <your-api-key> --stderr inherit
93 |
94 | # Use pipe to capture stderr (default is ignore)
95 | mcp-server-metamcp --metamcp-api-key <your-api-key> --stderr pipe
96 |
97 | # Or set via environment variable
98 | METAMCP_STDERR=inherit mcp-server-metamcp --metamcp-api-key <your-api-key>
99 | ```
100 |
101 | Available stderr options:
102 | - `ignore` (default): Ignore stderr output from child processes
103 | - `inherit`: Pass through stderr from child processes to the parent
104 | - `pipe`: Capture stderr in a pipe for processing
105 | - `overlapped`: Use overlapped I/O (Windows-specific)
106 |
107 | ### Command Line Options
108 |
109 | ```
110 | Options:
111 | --metamcp-api-key <key> API key for MetaMCP (can also be set via METAMCP_API_KEY env var)
112 | --metamcp-api-base-url <url> Base URL for MetaMCP API (can also be set via METAMCP_API_BASE_URL env var)
113 | --report Fetch all MCPs, initialize clients, and report tools to MetaMCP API
114 | --transport <type> Transport type to use (stdio, sse, or streamable-http) (default: "stdio")
115 | --port <port> Port to use for SSE or Streamable HTTP transport, defaults to 12006 (default: "12006")
116 | --require-api-auth Require API key in SSE or Streamable HTTP URL path
117 | --stateless Use stateless mode for Streamable HTTP transport
118 | --use-docker-host Transform localhost URLs to use host.docker.internal (can also be set via USE_DOCKER_HOST env var)
119 | --stderr <type> Stderr handling for STDIO transport (overlapped, pipe, ignore, inherit) (default: "ignore")
120 | -h, --help display help for command
121 | ```
122 |
123 | ## Environment Variables
124 |
125 | - `METAMCP_API_KEY`: API key for MetaMCP
126 | - `METAMCP_API_BASE_URL`: Base URL for MetaMCP API
127 | - `USE_DOCKER_HOST`: When set to "true", transforms localhost URLs to host.docker.internal for Docker compatibility
128 | - `METAMCP_STDERR`: Stderr handling for STDIO transport (overlapped, pipe, ignore, inherit). Defaults to "ignore"
129 |
130 | ## Development
131 |
132 | ```bash
133 | # Install dependencies
134 | npm install
135 |
136 | # Build the application
137 | npm run build
138 |
139 | # Watch for changes
140 | npm run watch
141 | ```
142 |
143 | ## Highlights
144 |
145 | - Compatible with ANY MCP Client
146 | - Multi-Workspaces layer enables you to switch to another set of MCP configs within one-click.
147 | - GUI dynamic updates of MCP configs.
148 | - Namespace isolation for joined MCPs.
149 |
150 | ## Architecture Overview
151 |
152 | ```mermaid
153 | sequenceDiagram
154 | participant MCPClient as MCP Client (e.g. Claude Desktop)
155 | participant MetaMCP-mcp-server as MetaMCP MCP Server
156 | participant MetaMCPApp as MetaMCP App
157 | participant MCPServers as Installed MCP Servers in Metatool App
158 |
159 | MCPClient ->> MetaMCP-mcp-server: Request list tools
160 | MetaMCP-mcp-server ->> MetaMCPApp: Get tools configuration & status
161 | MetaMCPApp ->> MetaMCP-mcp-server: Return tools configuration & status
162 |
163 | loop For each listed MCP Server
164 | MetaMCP-mcp-server ->> MCPServers: Request list_tools
165 | MCPServers ->> MetaMCP-mcp-server: Return list of tools
166 | end
167 |
168 | MetaMCP-mcp-server ->> MetaMCP-mcp-server: Aggregate tool lists
169 | MetaMCP-mcp-server ->> MCPClient: Return aggregated list of tools
170 |
171 | MCPClient ->> MetaMCP-mcp-server: Call tool
172 | MetaMCP-mcp-server ->> MCPServers: call_tool to target MCP Server
173 | MCPServers ->> MetaMCP-mcp-server: Return tool response
174 | MetaMCP-mcp-server ->> MCPClient: Return tool response
175 | ```
176 |
177 | ## Credits
178 |
179 | - Inspirations and some code (refactored in this project) from https://github.com/adamwattis/mcp-proxy-server/
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./dist",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
```yaml
1 | services:
2 | mcp-server:
3 | build:
4 | context: .
5 | dockerfile: Dockerfile
6 | ports:
7 | - "3000:3000"
8 | env_file:
9 | - .env.production.local
10 | entrypoint: ["/bin/bash"]
11 | command: ["-c", "uvx --version && echo 'uvx is working!' && tail -f /dev/null"]
12 | healthcheck:
13 | test: ["CMD", "ps", "aux", "|", "grep", "tail"]
14 | interval: 30s
15 | timeout: 10s
16 | retries: 3
17 | environment:
18 | - NODE_ENV=production
19 | restart: unless-stopped
20 | # Add any additional environment variables or command arguments here
21 | # command: --metamcp-api-key your-api-key --metamcp-api-base-url your-base-url
22 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Use the official uv Debian image as base
2 | FROM ghcr.io/astral-sh/uv:debian
3 |
4 | # Install Node.js and npm
5 | RUN apt-get update && apt-get install -y \
6 | curl \
7 | gnupg \
8 | && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
9 | && apt-get install -y nodejs \
10 | && apt-get clean \
11 | && rm -rf /var/lib/apt/lists/*
12 |
13 | # Verify Node.js and npm installation
14 | RUN node --version && npm --version
15 |
16 | # Verify uv is installed correctly
17 | RUN uv --version
18 |
19 | # Verify npx is available
20 | RUN npx --version || npm install -g npx
21 |
22 | # Set the working directory
23 | WORKDIR /app
24 |
25 | # Copy package files
26 | COPY package*.json ./
27 |
28 | # Install dependencies
29 | RUN npm ci
30 |
31 | # Copy the rest of the application
32 | COPY . .
33 |
34 | # Build the application
35 | RUN npm run build
36 |
37 | # Set environment variables
38 | ENV NODE_ENV=production
39 |
40 | # Expose the application port
41 | EXPOSE 3000
42 |
43 | # Run the application
44 | ENTRYPOINT ["node", "dist/index.js"]
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required:
9 | - metamcpApiKey
10 | properties:
11 | metamcpApiKey:
12 | type: string
13 | description: The API key from metamcp.com/api-keys. Required.
14 | metamcpApiBaseUrl:
15 | type: string
16 | description: Optional override for the MetaMCP App URL (default is https://api.metamcp.com).
17 | commandFunction:
18 | # A function that produces the CLI command to start the MCP on stdio.
19 | # Note: Command line arguments can also be used directly:
20 | # --metamcp-api-key <your-api-key> --metamcp-api-base-url <base-url>
21 | |-
22 | (config) => ({ command: 'node', args: ['dist/index.js'], env: { METAMCP_API_KEY: config.metamcpApiKey, ...(config.metamcpApiBaseUrl && { METAMCP_API_BASE_URL: config.metamcpApiBaseUrl }) } })
23 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@metamcp/mcp-server-metamcp",
3 | "version": "0.6.5",
4 | "description": "MCP Server MetaMCP manages all your other MCPs in one MCP.",
5 | "scripts": {
6 | "build": "tsc && shx chmod +x dist/*.js",
7 | "watch": "tsc --watch",
8 | "inspector": "dotenv -e .env.local npx @modelcontextprotocol/inspector dist/index.js -e METAMCP_API_KEY=${METAMCP_API_KEY} -e METAMCP_API_BASE_URL=${METAMCP_API_BASE_URL}",
9 | "inspector:prod": "dotenv -e .env.production.local npx @modelcontextprotocol/inspector dist/index.js -e METAMCP_API_KEY=${METAMCP_API_KEY}",
10 | "report": "dotenv -e .env.local -- node dist/index.js --report"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/metatool-ai/mcp-server-metamcp.git"
15 | },
16 | "author": "James Zhang",
17 | "license": "Apache-2.0",
18 | "bugs": {
19 | "url": "https://github.com/metatool-ai/mcp-server-metamcp/issues"
20 | },
21 | "homepage": "https://github.com/metatool-ai/mcp-server-metamcp#readme",
22 | "dependencies": {
23 | "@modelcontextprotocol/sdk": "^1.11.4",
24 | "axios": "^1.7.9",
25 | "commander": "^13.1.0",
26 | "express": "^4.21.2",
27 | "zod": "^3.24.2"
28 | },
29 | "devDependencies": {
30 | "@types/express": "^5.0.1",
31 | "@types/node": "^22.13.4",
32 | "dotenv-cli": "^8.0.0",
33 | "shx": "^0.3.4",
34 | "typescript": "^5.8.2"
35 | },
36 | "type": "module",
37 | "bin": {
38 | "mcp-server-metamcp": "dist/index.js"
39 | },
40 | "files": [
41 | "dist"
42 | ]
43 | }
44 |
```
--------------------------------------------------------------------------------
/src/sessions.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { getMcpServers, ServerParameters } from "./fetch-metamcp.js";
2 | import {
3 | ConnectedClient,
4 | createMetaMcpClient,
5 | connectMetaMcpClient,
6 | } from "./client.js";
7 | import { getSessionKey } from "./utils.js";
8 |
9 | const _sessions: Record<string, ConnectedClient> = {};
10 |
11 | export const getSession = async (
12 | sessionKey: string,
13 | uuid: string,
14 | params: ServerParameters
15 | ): Promise<ConnectedClient | undefined> => {
16 | if (sessionKey in _sessions) {
17 | return _sessions[sessionKey];
18 | } else {
19 | // Close existing session for this UUID if it exists with a different hash
20 | const old_session_keys = Object.keys(_sessions).filter((k) =>
21 | k.startsWith(`${uuid}_`)
22 | );
23 |
24 | await Promise.allSettled(
25 | old_session_keys.map(async (old_session_key) => {
26 | await _sessions[old_session_key].cleanup();
27 | delete _sessions[old_session_key];
28 | })
29 | );
30 |
31 | const { client, transport } = createMetaMcpClient(params);
32 | if (!client || !transport) {
33 | return;
34 | }
35 |
36 | const newClient = await connectMetaMcpClient(client, transport);
37 | if (!newClient) {
38 | return;
39 | }
40 |
41 | _sessions[sessionKey] = newClient;
42 |
43 | return newClient;
44 | }
45 | };
46 |
47 | export const initSessions = async (): Promise<void> => {
48 | const serverParams = await getMcpServers(true);
49 |
50 | await Promise.allSettled(
51 | Object.entries(serverParams).map(async ([uuid, params]) => {
52 | const sessionKey = getSessionKey(uuid, params);
53 | try {
54 | await getSession(sessionKey, uuid, params);
55 | } catch (error) {}
56 | })
57 | );
58 | };
59 |
60 | export const cleanupAllSessions = async (): Promise<void> => {
61 | await Promise.allSettled(
62 | Object.entries(_sessions).map(async ([sessionKey, session]) => {
63 | await session.cleanup();
64 | delete _sessions[sessionKey];
65 | })
66 | );
67 | };
68 |
```
--------------------------------------------------------------------------------
/src/fetch-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import axios from "axios";
2 | import { getMetaMcpApiBaseUrl, getMetaMcpApiKey } from "./utils.js";
3 |
4 | enum ToolStatus {
5 | ACTIVE = "ACTIVE",
6 | INACTIVE = "INACTIVE",
7 | }
8 |
9 | // Define interface for tool parameters with only required fields
10 | export interface ToolParameters {
11 | mcp_server_uuid: string;
12 | name: string;
13 | status: ToolStatus;
14 | }
15 |
16 | let _toolsCache: Record<string, ToolParameters> | null = null;
17 | let _toolsCacheTimestamp: number = 0;
18 | const CACHE_TTL_MS = 1000; // 1 second cache TTL
19 |
20 | export async function getInactiveTools(
21 | forceRefresh: boolean = false
22 | ): Promise<Record<string, ToolParameters>> {
23 | const currentTime = Date.now();
24 | const cacheAge = currentTime - _toolsCacheTimestamp;
25 |
26 | // Use cache if it exists, is not null, and either:
27 | // 1. forceRefresh is false, or
28 | // 2. forceRefresh is true but cache is less than 1 second old
29 | if (_toolsCache !== null && (!forceRefresh || cacheAge < CACHE_TTL_MS)) {
30 | return _toolsCache;
31 | }
32 |
33 | try {
34 | const apiKey = getMetaMcpApiKey();
35 | const apiBaseUrl = getMetaMcpApiBaseUrl();
36 |
37 | if (!apiKey) {
38 | console.error(
39 | "METAMCP_API_KEY is not set. Please set it via environment variable or command line argument."
40 | );
41 | return _toolsCache || {};
42 | }
43 |
44 | const headers = { Authorization: `Bearer ${apiKey}` };
45 | const response = await axios.get(
46 | `${apiBaseUrl}/api/tools?status=${ToolStatus.INACTIVE}`,
47 | {
48 | headers,
49 | }
50 | );
51 | const data = response.data;
52 |
53 | const toolDict: Record<string, ToolParameters> = {};
54 | // Access the 'results' array in the response
55 | if (data && data.results) {
56 | for (const tool of data.results) {
57 | const params: ToolParameters = {
58 | mcp_server_uuid: tool.mcp_server_uuid,
59 | name: tool.name,
60 | status: tool.status,
61 | };
62 |
63 | const uniqueId = `${tool.mcp_server_uuid}:${tool.name}`;
64 | toolDict[uniqueId] = params;
65 | }
66 | }
67 |
68 | _toolsCache = toolDict;
69 | _toolsCacheTimestamp = currentTime;
70 | return toolDict;
71 | } catch (error) {
72 | // Return empty object if API doesn't exist or has errors
73 | if (_toolsCache !== null) {
74 | return _toolsCache;
75 | }
76 | return {};
77 | }
78 | }
79 |
```
--------------------------------------------------------------------------------
/src/fetch-capabilities.ts:
--------------------------------------------------------------------------------
```typescript
1 | import axios from "axios";
2 | import { getMetaMcpApiBaseUrl, getMetaMcpApiKey } from "./utils.js";
3 |
4 | export enum ProfileCapability {
5 | TOOLS_MANAGEMENT = "TOOLS_MANAGEMENT",
6 | TOOL_LOGS = "TOOL_LOGS",
7 | }
8 |
9 | let _capabilitiesCache: ProfileCapability[] | null = null;
10 | let _capabilitiesCacheTimestamp: number = 0;
11 | const CACHE_TTL_MS = 1000; // 1 second cache TTL
12 |
13 | export async function getProfileCapabilities(
14 | forceRefresh: boolean = false
15 | ): Promise<ProfileCapability[]> {
16 | const currentTime = Date.now();
17 | const cacheAge = currentTime - _capabilitiesCacheTimestamp;
18 |
19 | // Use cache if it exists, is not null, and either:
20 | // 1. forceRefresh is false, or
21 | // 2. forceRefresh is true but cache is less than 1 second old
22 | if (
23 | _capabilitiesCache !== null &&
24 | (!forceRefresh || cacheAge < CACHE_TTL_MS)
25 | ) {
26 | return _capabilitiesCache;
27 | }
28 |
29 | try {
30 | const apiKey = getMetaMcpApiKey();
31 | const apiBaseUrl = getMetaMcpApiBaseUrl();
32 |
33 | if (!apiKey) {
34 | console.error(
35 | "METAMCP_API_KEY is not set. Please set it via environment variable or command line argument."
36 | );
37 | return _capabilitiesCache || [];
38 | }
39 |
40 | const headers = { Authorization: `Bearer ${apiKey}` };
41 | const response = await axios.get(`${apiBaseUrl}/api/profile-capabilities`, {
42 | headers,
43 | });
44 | const data = response.data;
45 |
46 | // Access the 'profileCapabilities' array in the response
47 | if (data && data.profileCapabilities) {
48 | const capabilities = data.profileCapabilities
49 | .map((capability: string) => {
50 | // Map string to enum value if it exists, otherwise return undefined
51 | return ProfileCapability[
52 | capability as keyof typeof ProfileCapability
53 | ];
54 | })
55 | .filter(
56 | (
57 | capability: ProfileCapability | undefined
58 | ): capability is ProfileCapability => capability !== undefined
59 | );
60 |
61 | _capabilitiesCache = capabilities;
62 | _capabilitiesCacheTimestamp = currentTime;
63 | return capabilities;
64 | }
65 |
66 | return _capabilitiesCache || [];
67 | } catch (error) {
68 | // Return empty array if API doesn't exist or has errors
69 | if (_capabilitiesCache !== null) {
70 | return _capabilitiesCache;
71 | }
72 | return [];
73 | }
74 | }
75 |
```
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ServerParameters } from "./fetch-metamcp.js";
2 | import crypto from "crypto";
3 |
4 | /**
5 | * Environment variables to inherit by default, if an environment is not explicitly given.
6 | */
7 | export const DEFAULT_INHERITED_ENV_VARS =
8 | process.platform === "win32"
9 | ? [
10 | "APPDATA",
11 | "HOMEDRIVE",
12 | "HOMEPATH",
13 | "LOCALAPPDATA",
14 | "PATH",
15 | "PROCESSOR_ARCHITECTURE",
16 | "SYSTEMDRIVE",
17 | "SYSTEMROOT",
18 | "TEMP",
19 | "USERNAME",
20 | "USERPROFILE",
21 | ]
22 | : /* list inspired by the default env inheritance of sudo */
23 | ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"];
24 |
25 | /**
26 | * Returns a default environment object including only environment variables deemed safe to inherit.
27 | */
28 | export function getDefaultEnvironment(): Record<string, string> {
29 | const env: Record<string, string> = {};
30 |
31 | for (const key of DEFAULT_INHERITED_ENV_VARS) {
32 | const value = process.env[key];
33 | if (value === undefined) {
34 | continue;
35 | }
36 |
37 | if (value.startsWith("()")) {
38 | // Skip functions, which are a security risk.
39 | continue;
40 | }
41 |
42 | env[key] = value;
43 | }
44 |
45 | return env;
46 | }
47 |
48 | /**
49 | * Get the MetaMCP API base URL from environment variables
50 | */
51 | export function getMetaMcpApiBaseUrl(): string {
52 | return process.env.METAMCP_API_BASE_URL || "https://api.metamcp.com";
53 | }
54 |
55 | /**
56 | * Get the MetaMCP API key from environment variables
57 | */
58 | export function getMetaMcpApiKey(): string | undefined {
59 | return process.env.METAMCP_API_KEY;
60 | }
61 |
62 | export function sanitizeName(name: string): string {
63 | return name.replace(/[^a-zA-Z0-9_-]/g, "");
64 | }
65 |
66 | export function computeParamsHash(
67 | params: ServerParameters,
68 | uuid: string
69 | ): string {
70 | let paramsDict: any;
71 |
72 | // Default to "STDIO" if type is undefined
73 | if (!params.type || params.type === "STDIO") {
74 | paramsDict = {
75 | uuid,
76 | type: "STDIO", // Explicitly set type to "STDIO" for consistent hashing
77 | command: params.command,
78 | args: params.args,
79 | env: params.env
80 | ? Object.fromEntries(
81 | Object.entries(params.env).sort((a, b) => a[0].localeCompare(b[0]))
82 | )
83 | : null,
84 | };
85 | } else if (params.type === "SSE" || params.type === "STREAMABLE_HTTP") {
86 | paramsDict = {
87 | uuid,
88 | type: params.type,
89 | url: params.url,
90 | };
91 | } else {
92 | throw new Error(`Unsupported server type: ${params.type}`);
93 | }
94 |
95 | const paramsJson = JSON.stringify(paramsDict);
96 | return crypto.createHash("sha256").update(paramsJson).digest("hex");
97 | }
98 |
99 | export function getSessionKey(uuid: string, params: ServerParameters): string {
100 | return `${uuid}_${computeParamsHash(params, uuid)}`;
101 | }
102 |
```
--------------------------------------------------------------------------------
/src/sse.ts:
--------------------------------------------------------------------------------
```typescript
1 | import express from "express";
2 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4 |
5 | export interface SSEServerOptions {
6 | port: number;
7 | requireApiAuth?: boolean;
8 | }
9 |
10 | // Starts an SSE server and returns a cleanup function
11 | export async function startSSEServer(
12 | server: Server,
13 | options: SSEServerOptions
14 | ): Promise<() => Promise<void>> {
15 | const app = express();
16 | const port = options.port || 12006;
17 | const requireApiAuth = options.requireApiAuth || false;
18 | const apiKey = process.env.METAMCP_API_KEY;
19 |
20 | // to support multiple simultaneous connections we have a lookup object from
21 | // sessionId to transport
22 | const transports: { [sessionId: string]: SSEServerTransport } = {};
23 |
24 | // Define the SSE endpoint based on authentication requirement
25 | const sseEndpoint = requireApiAuth ? `/:apiKey/sse` : `/sse`;
26 |
27 | app.get(sseEndpoint, async (req: express.Request, res: express.Response) => {
28 | // If API auth is required, validate the API key
29 | if (requireApiAuth) {
30 | const requestApiKey = req.params.apiKey;
31 | if (!apiKey || requestApiKey !== apiKey) {
32 | res.status(401).send("Unauthorized: Invalid API key");
33 | return;
34 | }
35 | }
36 |
37 | // Set the messages path based on authentication requirement
38 | const messagesPath = requireApiAuth ? `/${apiKey}/messages` : `/messages`;
39 | const transport = new SSEServerTransport(messagesPath, res);
40 | transports[transport.sessionId] = transport;
41 | res.on("close", () => {
42 | delete transports[transport.sessionId];
43 | });
44 | await server.connect(transport);
45 | });
46 |
47 | // Define the messages endpoint
48 | const messagesEndpoint = requireApiAuth ? `/:apiKey/messages` : `/messages`;
49 |
50 | app.post(
51 | messagesEndpoint,
52 | async (req: express.Request, res: express.Response) => {
53 | // If API auth is required, validate the API key
54 | if (requireApiAuth) {
55 | const requestApiKey = req.params.apiKey;
56 | if (!apiKey || requestApiKey !== apiKey) {
57 | res.status(401).send("Unauthorized: Invalid API key");
58 | return;
59 | }
60 | }
61 |
62 | const sessionId = req.query.sessionId as string;
63 | const transport = transports[sessionId];
64 | if (transport) {
65 | await transport.handlePostMessage(req, res);
66 | } else {
67 | res.status(400).send("No transport found for sessionId");
68 | }
69 | }
70 | );
71 |
72 | const serverInstance = app.listen(port, () => {
73 | const baseUrl = `http://localhost:${port}`;
74 | const sseUrl = requireApiAuth
75 | ? `${baseUrl}/${apiKey}/sse`
76 | : `${baseUrl}/sse`;
77 | console.log(`SSE server listening on port ${port}`);
78 | console.log(`SSE endpoint: ${sseUrl}`);
79 | });
80 |
81 | // Return cleanup function
82 | return async () => {
83 | // Close all active transports
84 | await Promise.all(
85 | Object.values(transports).map((transport) => transport.close())
86 | );
87 | serverInstance.close();
88 | };
89 | }
90 |
```
--------------------------------------------------------------------------------
/src/fetch-metamcp.ts:
--------------------------------------------------------------------------------
```typescript
1 | import axios from "axios";
2 | import {
3 | getDefaultEnvironment,
4 | getMetaMcpApiBaseUrl,
5 | getMetaMcpApiKey,
6 | } from "./utils.js";
7 |
8 | // Define IOType for stderr handling
9 | export type IOType = "overlapped" | "pipe" | "ignore" | "inherit";
10 |
11 | // Define a new interface for server parameters that can be STDIO, SSE or STREAMABLE_HTTP
12 | export interface ServerParameters {
13 | uuid: string;
14 | name: string;
15 | description: string;
16 | type?: "STDIO" | "SSE" | "STREAMABLE_HTTP"; // Optional field, defaults to "STDIO" when undefined
17 | command?: string | null;
18 | args?: string[] | null;
19 | env?: Record<string, string> | null;
20 | stderr?: IOType; // Optional field for stderr handling, defaults to "ignore"
21 | url?: string | null;
22 | created_at: string;
23 | profile_uuid: string;
24 | status: string;
25 | oauth_tokens?: {
26 | access_token: string;
27 | token_type: string;
28 | expires_in?: number | undefined;
29 | scope?: string | undefined;
30 | refresh_token?: string | undefined;
31 | } | null;
32 | }
33 |
34 | let _mcpServersCache: Record<string, ServerParameters> | null = null;
35 | let _mcpServersCacheTimestamp: number = 0;
36 | const CACHE_TTL_MS = 1000; // 1 second cache TTL
37 |
38 | export async function getMcpServers(
39 | forceRefresh: boolean = false
40 | ): Promise<Record<string, ServerParameters>> {
41 | const currentTime = Date.now();
42 | const cacheAge = currentTime - _mcpServersCacheTimestamp;
43 |
44 | // Use cache if it exists, is not null, and either:
45 | // 1. forceRefresh is false, or
46 | // 2. forceRefresh is true but cache is less than 1 second old
47 | if (_mcpServersCache !== null && (!forceRefresh || cacheAge < CACHE_TTL_MS)) {
48 | return _mcpServersCache;
49 | }
50 |
51 | try {
52 | const apiKey = getMetaMcpApiKey();
53 | const apiBaseUrl = getMetaMcpApiBaseUrl();
54 |
55 | if (!apiKey) {
56 | console.error(
57 | "METAMCP_API_KEY is not set. Please set it via environment variable or command line argument."
58 | );
59 | return _mcpServersCache || {};
60 | }
61 |
62 | const headers = { Authorization: `Bearer ${apiKey}` };
63 | const response = await axios.get(`${apiBaseUrl}/api/mcp-servers`, {
64 | headers,
65 | });
66 | const data = response.data;
67 |
68 | const serverDict: Record<string, ServerParameters> = {};
69 | for (const serverParams of data) {
70 | const params: ServerParameters = {
71 | ...serverParams,
72 | type: serverParams.type || "STDIO",
73 | };
74 |
75 | // Process based on server type
76 | if (params.type === "STDIO") {
77 | if ("args" in params && !params.args) {
78 | params.args = undefined;
79 | }
80 |
81 | params.env = {
82 | ...getDefaultEnvironment(),
83 | ...(params.env || {}),
84 | };
85 | } else if (params.type === "SSE" || params.type === "STREAMABLE_HTTP") {
86 | // For SSE or STREAMABLE_HTTP servers, ensure url is present
87 | if (!params.url) {
88 | console.warn(
89 | `${params.type} server ${params.uuid} is missing url field, skipping`
90 | );
91 | continue;
92 | }
93 | }
94 |
95 | const uuid = params.uuid;
96 | if (uuid) {
97 | serverDict[uuid] = params;
98 | }
99 | }
100 |
101 | _mcpServersCache = serverDict;
102 | _mcpServersCacheTimestamp = currentTime;
103 | return serverDict;
104 | } catch (error) {
105 | if (_mcpServersCache !== null) {
106 | return _mcpServersCache;
107 | }
108 | return {};
109 | }
110 | }
111 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4 | import { createServer } from "./mcp-proxy.js";
5 | import { Command } from "commander";
6 | import { reportAllTools } from "./report-tools.js";
7 | import { cleanupAllSessions } from "./sessions.js";
8 | import { startSSEServer } from "./sse.js";
9 | import { startStreamableHTTPServer } from "./streamable-http.js";
10 | import { IOType } from "./fetch-metamcp.js";
11 |
12 | const program = new Command();
13 |
14 | program
15 | .name("mcp-server-metamcp")
16 | .description("MetaMCP MCP Server - The One MCP to manage all your MCPs")
17 | .option(
18 | "--metamcp-api-key <key>",
19 | "API key for MetaMCP (can also be set via METAMCP_API_KEY env var)"
20 | )
21 | .option(
22 | "--metamcp-api-base-url <url>",
23 | "Base URL for MetaMCP API (can also be set via METAMCP_API_BASE_URL env var)"
24 | )
25 | .option(
26 | "--report",
27 | "Fetch all MCPs, initialize clients, and report tools to MetaMCP API"
28 | )
29 | .option("--transport <type>", "Transport type to use (stdio, sse, or streamable-http)", "stdio")
30 | .option("--port <port>", "Port to use for SSE or Streamable HTTP transport, defaults to 12006", "12006")
31 | .option("--require-api-auth", "Require API key in SSE or Streamable HTTP URL path")
32 | .option("--stateless", "Use stateless mode for Streamable HTTP transport")
33 | .option(
34 | "--use-docker-host",
35 | "Transform localhost URLs to use host.docker.internal (can also be set via USE_DOCKER_HOST env var)"
36 | )
37 | .option(
38 | "--stderr <type>",
39 | "Stderr handling for STDIO transport (overlapped, pipe, ignore, inherit)",
40 | "ignore"
41 | )
42 | .parse(process.argv);
43 |
44 | const options = program.opts();
45 |
46 | // Validate stderr option
47 | const validStderrTypes: IOType[] = ["overlapped", "pipe", "ignore", "inherit"];
48 | if (!validStderrTypes.includes(options.stderr as IOType)) {
49 | console.error(`Invalid stderr type: ${options.stderr}. Must be one of: ${validStderrTypes.join(", ")}`);
50 | process.exit(1);
51 | }
52 |
53 | // Set environment variables from command line arguments
54 | if (options.metamcpApiKey) {
55 | process.env.METAMCP_API_KEY = options.metamcpApiKey;
56 | }
57 | if (options.metamcpApiBaseUrl) {
58 | process.env.METAMCP_API_BASE_URL = options.metamcpApiBaseUrl;
59 | }
60 | if (options.useDockerHost) {
61 | process.env.USE_DOCKER_HOST = "true";
62 | }
63 | if (options.stderr) {
64 | process.env.METAMCP_STDERR = options.stderr;
65 | }
66 |
67 | async function main() {
68 | // If --report flag is set, run the reporting function instead of starting the server
69 | if (options.report) {
70 | await reportAllTools();
71 | await cleanupAllSessions();
72 | return;
73 | }
74 |
75 | const { server, cleanup } = await createServer();
76 |
77 | if (options.transport.toLowerCase() === "sse") {
78 | // Start SSE server
79 | const port = parseInt(options.port) || 12006;
80 | const sseCleanup = await startSSEServer(server, {
81 | port,
82 | requireApiAuth: options.requireApiAuth,
83 | });
84 |
85 | // Cleanup on exit
86 | const handleExit = async () => {
87 | await cleanup();
88 | await sseCleanup();
89 | await server.close();
90 | process.exit(0);
91 | };
92 |
93 | process.on("SIGINT", handleExit);
94 | process.on("SIGTERM", handleExit);
95 | } else if (options.transport.toLowerCase() === "streamable-http") {
96 | // Start Streamable HTTP server
97 | const port = parseInt(options.port) || 12006;
98 | const streamableHttpCleanup = await startStreamableHTTPServer(server, {
99 | port,
100 | requireApiAuth: options.requireApiAuth,
101 | stateless: options.stateless,
102 | });
103 |
104 | // Cleanup on exit
105 | const handleExit = async () => {
106 | await cleanup();
107 | await streamableHttpCleanup();
108 | await server.close();
109 | process.exit(0);
110 | };
111 |
112 | process.on("SIGINT", handleExit);
113 | process.on("SIGTERM", handleExit);
114 | } else {
115 | // Default: Start stdio server
116 | const transport = new StdioServerTransport();
117 | await server.connect(transport);
118 |
119 | const handleExit = async () => {
120 | await cleanup();
121 | await transport.close();
122 | await server.close();
123 | process.exit(0);
124 | };
125 |
126 | // Cleanup on exit
127 | process.on("SIGINT", handleExit);
128 | process.on("SIGTERM", handleExit);
129 |
130 | process.stdin.resume();
131 | process.stdin.on("end", handleExit);
132 | process.stdin.on("close", handleExit);
133 | }
134 | }
135 |
136 | main().catch((error) => {
137 | console.error("Server error:", error);
138 | });
139 |
```
--------------------------------------------------------------------------------
/src/client.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2 | import {
3 | StdioClientTransport,
4 | StdioServerParameters,
5 | } from "@modelcontextprotocol/sdk/client/stdio.js";
6 | import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
7 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8 | import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
9 | import { ServerParameters, IOType } from "./fetch-metamcp.js";
10 |
11 | const sleep = (time: number) =>
12 | new Promise<void>((resolve) => setTimeout(() => resolve(), time));
13 | export interface ConnectedClient {
14 | client: Client;
15 | cleanup: () => Promise<void>;
16 | }
17 |
18 | /**
19 | * Transforms localhost URLs to use host.docker.internal when running inside Docker
20 | */
21 | const transformDockerUrl = (url: string): string => {
22 | if (process.env.USE_DOCKER_HOST === "true") {
23 | return url.replace(/localhost|127\.0\.0\.1/g, "host.docker.internal");
24 | }
25 | return url;
26 | };
27 |
28 | export const createMetaMcpClient = (
29 | serverParams: ServerParameters
30 | ): { client: Client | undefined; transport: Transport | undefined } => {
31 | let transport: Transport | undefined;
32 |
33 | // Create the appropriate transport based on server type
34 | // Default to "STDIO" if type is undefined
35 | if (!serverParams.type || serverParams.type === "STDIO") {
36 | // Get stderr value from serverParams, environment variable, or default to "ignore"
37 | const stderrValue: IOType =
38 | serverParams.stderr ||
39 | (process.env.METAMCP_STDERR as IOType) ||
40 | "ignore";
41 |
42 | const stdioParams: StdioServerParameters = {
43 | command: serverParams.command || "",
44 | args: serverParams.args || undefined,
45 | env: serverParams.env || undefined,
46 | stderr: stderrValue,
47 | };
48 | transport = new StdioClientTransport(stdioParams);
49 |
50 | // Handle stderr stream when set to "pipe"
51 | if (stderrValue === "pipe" && (transport as any).stderr) {
52 | const stderrStream = (transport as any).stderr;
53 |
54 | stderrStream.on('data', (chunk: Buffer) => {
55 | console.error(`[${serverParams.name}] ${chunk.toString().trim()}`);
56 | });
57 |
58 | stderrStream.on('error', (error: Error) => {
59 | console.error(`[${serverParams.name}] stderr error:`, error);
60 | });
61 | }
62 | } else if (serverParams.type === "SSE" && serverParams.url) {
63 | // Transform the URL if USE_DOCKER_HOST is set to "true"
64 | const transformedUrl = transformDockerUrl(serverParams.url);
65 |
66 | if (!serverParams.oauth_tokens) {
67 | transport = new SSEClientTransport(new URL(transformedUrl));
68 | } else {
69 | const headers: HeadersInit = {};
70 | headers[
71 | "Authorization"
72 | ] = `Bearer ${serverParams.oauth_tokens.access_token}`;
73 | transport = new SSEClientTransport(new URL(transformedUrl), {
74 | requestInit: {
75 | headers,
76 | },
77 | eventSourceInit: {
78 | fetch: (url, init) => fetch(url, { ...init, headers }),
79 | },
80 | });
81 | }
82 | } else if (serverParams.type === "STREAMABLE_HTTP" && serverParams.url) {
83 | // Transform the URL if USE_DOCKER_HOST is set to "true"
84 | const transformedUrl = transformDockerUrl(serverParams.url);
85 |
86 | if (!serverParams.oauth_tokens) {
87 | transport = new StreamableHTTPClientTransport(new URL(transformedUrl));
88 | } else {
89 | const headers: HeadersInit = {};
90 | headers[
91 | "Authorization"
92 | ] = `Bearer ${serverParams.oauth_tokens.access_token}`;
93 | transport = new StreamableHTTPClientTransport(new URL(transformedUrl), {
94 | requestInit: {
95 | headers,
96 | },
97 | });
98 | }
99 | } else {
100 | console.error(`Unsupported server type: ${serverParams.type}`);
101 | return { client: undefined, transport: undefined };
102 | }
103 |
104 | const client = new Client(
105 | {
106 | name: "MetaMCP",
107 | version: "0.6.5",
108 | },
109 | {
110 | capabilities: {
111 | prompts: {},
112 | resources: { subscribe: true },
113 | tools: {},
114 | },
115 | }
116 | );
117 | return { client, transport };
118 | };
119 |
120 | export const connectMetaMcpClient = async (
121 | client: Client,
122 | transport: Transport
123 | ): Promise<ConnectedClient | undefined> => {
124 | const waitFor = 2500;
125 | const retries = 3;
126 | let count = 0;
127 | let retry = true;
128 |
129 | while (retry) {
130 | try {
131 | await client.connect(transport);
132 |
133 | return {
134 | client,
135 | cleanup: async () => {
136 | await transport.close();
137 | await client.close();
138 | },
139 | };
140 | } catch (error) {
141 | count++;
142 | retry = count < retries;
143 | if (retry) {
144 | try {
145 | await client.close();
146 | } catch {}
147 | await sleep(waitFor);
148 | }
149 | }
150 | }
151 | };
152 |
```
--------------------------------------------------------------------------------
/src/streamable-http.ts:
--------------------------------------------------------------------------------
```typescript
1 | import express from "express";
2 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4 | import { randomUUID } from "crypto";
5 |
6 | export interface StreamableHTTPServerOptions {
7 | port: number;
8 | requireApiAuth?: boolean;
9 | stateless?: boolean;
10 | }
11 |
12 | // Starts a Streamable HTTP server and returns a cleanup function
13 | export async function startStreamableHTTPServer(
14 | server: Server,
15 | options: StreamableHTTPServerOptions
16 | ): Promise<() => Promise<void>> {
17 | const app = express();
18 | app.use(express.json());
19 |
20 | const port = options.port || 12006;
21 | const requireApiAuth = options.requireApiAuth || false;
22 | const stateless = options.stateless || false;
23 | const apiKey = process.env.METAMCP_API_KEY;
24 |
25 | // Map to store transports by session ID (when using stateful mode)
26 | const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
27 |
28 | // Define the MCP endpoint path based on authentication requirement
29 | const mcpEndpoint = requireApiAuth ? `/:apiKey/mcp` : `/mcp`;
30 |
31 | // Handle all HTTP methods for the MCP endpoint
32 | app.all(mcpEndpoint, async (req: express.Request, res: express.Response) => {
33 | // If API auth is required, validate the API key
34 | if (requireApiAuth) {
35 | const requestApiKey = req.params.apiKey;
36 | if (!apiKey || requestApiKey !== apiKey) {
37 | res.status(401).send("Unauthorized: Invalid API key");
38 | return;
39 | }
40 | }
41 |
42 | if (stateless) {
43 | // Stateless mode: Create a new transport for each request
44 | const transport = new StreamableHTTPServerTransport({
45 | sessionIdGenerator: undefined, // No session management
46 | });
47 |
48 | res.on("close", () => {
49 | transport.close();
50 | });
51 |
52 | try {
53 | // Connect to the server
54 | await server.connect(transport);
55 | // Handle the request
56 | await transport.handleRequest(req, res, req.body);
57 | } catch (error) {
58 | console.error("Error handling streamable HTTP request:", error);
59 | if (!res.headersSent) {
60 | res.status(500).json({
61 | jsonrpc: "2.0",
62 | error: {
63 | code: -32603,
64 | message: "Internal server error",
65 | },
66 | id: null,
67 | });
68 | }
69 | }
70 | } else {
71 | // Stateful mode: Use session management
72 | const sessionId = req.headers["mcp-session-id"] as string | undefined;
73 | let transport: StreamableHTTPServerTransport;
74 |
75 | if (sessionId && transports[sessionId]) {
76 | // Reuse existing transport
77 | transport = transports[sessionId];
78 | } else if (!sessionId && req.method === "POST") {
79 | // New initialization request
80 | transport = new StreamableHTTPServerTransport({
81 | sessionIdGenerator: () => randomUUID(),
82 | onsessioninitialized: (sessionId) => {
83 | // Store the transport by session ID
84 | transports[sessionId] = transport;
85 | }
86 | });
87 |
88 | // Clean up transport when closed
89 | transport.onclose = () => {
90 | if (transport.sessionId) {
91 | delete transports[transport.sessionId];
92 | }
93 | };
94 |
95 | // Connect to the server
96 | await server.connect(transport);
97 | } else {
98 | // Invalid request
99 | res.status(400).json({
100 | jsonrpc: "2.0",
101 | error: {
102 | code: -32000,
103 | message: "Bad Request: No valid session ID provided",
104 | },
105 | id: null,
106 | });
107 | return;
108 | }
109 |
110 | try {
111 | // Handle the request
112 | await transport.handleRequest(req, res, req.body);
113 | } catch (error) {
114 | console.error("Error handling streamable HTTP request:", error);
115 | if (!res.headersSent) {
116 | res.status(500).json({
117 | jsonrpc: "2.0",
118 | error: {
119 | code: -32603,
120 | message: "Internal server error",
121 | },
122 | id: null,
123 | });
124 | }
125 | }
126 | }
127 | });
128 |
129 | const serverInstance = app.listen(port, () => {
130 | const baseUrl = `http://localhost:${port}`;
131 | const mcpUrl = requireApiAuth ? `${baseUrl}/${apiKey}/mcp` : `${baseUrl}/mcp`;
132 | console.log(`Streamable HTTP server listening on port ${port}`);
133 | console.log(`MCP endpoint: ${mcpUrl}`);
134 | console.log(`Mode: ${stateless ? "Stateless" : "Stateful"}`);
135 | });
136 |
137 | // Return cleanup function
138 | return async () => {
139 | // Close all active transports
140 | await Promise.all(
141 | Object.values(transports).map((transport) => transport.close())
142 | );
143 | serverInstance.close();
144 | };
145 | }
```
--------------------------------------------------------------------------------
/src/report-tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import axios from "axios";
2 | import { getMetaMcpApiBaseUrl, getMetaMcpApiKey } from "./utils.js";
3 | import { getMcpServers } from "./fetch-metamcp.js";
4 | import { initSessions, getSession } from "./sessions.js";
5 | import { getSessionKey } from "./utils.js";
6 | import { ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
7 |
8 | // Define interface for tool data structure
9 | export interface MetaMcpTool {
10 | name: string;
11 | description?: string;
12 | toolSchema: any;
13 | mcp_server_uuid: string;
14 | }
15 |
16 | // API route handler for submitting tools to MetaMCP
17 | export async function reportToolsToMetaMcp(tools: MetaMcpTool[]) {
18 | try {
19 | const apiKey = getMetaMcpApiKey();
20 | const apiBaseUrl = getMetaMcpApiBaseUrl();
21 |
22 | if (!apiKey) {
23 | return { error: "API key not set" };
24 | }
25 |
26 | // Validate that tools is an array
27 | if (!Array.isArray(tools) || tools.length === 0) {
28 | return {
29 | error: "Request must include a non-empty array of tools",
30 | status: 400,
31 | };
32 | }
33 |
34 | // Validate required fields for all tools and prepare for submission
35 | const validTools = [];
36 | const errors = [];
37 |
38 | for (const tool of tools) {
39 | const { name, description, toolSchema, mcp_server_uuid } = tool;
40 |
41 | // Validate required fields for each tool
42 | if (!name || !toolSchema || !mcp_server_uuid) {
43 | errors.push({
44 | tool,
45 | error:
46 | "Missing required fields: name, toolSchema, or mcp_server_uuid",
47 | });
48 | continue;
49 | }
50 |
51 | validTools.push({
52 | name,
53 | description,
54 | toolSchema,
55 | mcp_server_uuid,
56 | });
57 | }
58 |
59 | // Submit valid tools to MetaMCP API
60 | let results: any[] = [];
61 | if (validTools.length > 0) {
62 | try {
63 | const response = await axios.post(
64 | `${apiBaseUrl}/api/tools`,
65 | { tools: validTools },
66 | {
67 | headers: {
68 | "Content-Type": "application/json",
69 | Authorization: `Bearer ${apiKey}`,
70 | },
71 | }
72 | );
73 |
74 | results = response.data.results || [];
75 | } catch (error: any) {
76 | if (error.response) {
77 | // The request was made and the server responded with a status code outside of 2xx
78 | return {
79 | error: error.response.data.error || "Failed to submit tools",
80 | status: error.response.status,
81 | details: error.response.data,
82 | };
83 | } else if (error.request) {
84 | // The request was made but no response was received
85 | return {
86 | error: "No response received from server",
87 | details: error.request,
88 | };
89 | } else {
90 | // Something happened in setting up the request
91 | return {
92 | error: "Error setting up request",
93 | details: error.message,
94 | };
95 | }
96 | }
97 | }
98 |
99 | return {
100 | results,
101 | errors,
102 | success: results.length > 0,
103 | failureCount: errors.length,
104 | successCount: results.length,
105 | };
106 | } catch (error: any) {
107 | return {
108 | error: "Failed to process tools request",
109 | status: 500,
110 | };
111 | }
112 | }
113 |
114 | // Function to fetch all MCP servers, initialize clients, and report tools to MetaMCP API
115 | export async function reportAllTools() {
116 | console.log("Fetching all MCPs and initializing clients...");
117 |
118 | // Get all MCP servers
119 | const serverParams = await getMcpServers();
120 |
121 | // Initialize all sessions
122 | await initSessions();
123 |
124 | console.log(`Found ${Object.keys(serverParams).length} MCP servers`);
125 |
126 | // For each server, get its tools and report them
127 | await Promise.allSettled(
128 | Object.entries(serverParams).map(async ([uuid, params]) => {
129 | const sessionKey = getSessionKey(uuid, params);
130 | const session = await getSession(sessionKey, uuid, params);
131 |
132 | if (!session) {
133 | console.log(`Could not establish session for ${params.name} (${uuid})`);
134 | return;
135 | }
136 |
137 | const capabilities = session.client.getServerCapabilities();
138 | if (!capabilities?.tools) {
139 | console.log(`Server ${params.name} (${uuid}) does not support tools`);
140 | return;
141 | }
142 |
143 | try {
144 | console.log(`Fetching tools from ${params.name} (${uuid})...`);
145 |
146 | const result = await session.client.request(
147 | { method: "tools/list", params: {} },
148 | ListToolsResultSchema
149 | );
150 |
151 | if (result.tools && result.tools.length > 0) {
152 | console.log(
153 | `Reporting ${result.tools.length} tools from ${params.name} to MetaMCP API...`
154 | );
155 |
156 | const reportResult = await reportToolsToMetaMcp(
157 | result.tools.map((tool) => ({
158 | name: tool.name,
159 | description: tool.description,
160 | toolSchema: tool.inputSchema,
161 | mcp_server_uuid: uuid,
162 | }))
163 | );
164 |
165 | console.log(
166 | `Reported tools from ${params.name}: ${reportResult.successCount} succeeded, ${reportResult.failureCount} failed`
167 | );
168 | } else {
169 | console.log(`No tools found for ${params.name}`);
170 | }
171 | } catch (error) {
172 | console.error(`Error reporting tools for ${params.name}:`, error);
173 | }
174 | })
175 | );
176 |
177 | console.log("Finished reporting all tools to MetaMCP API");
178 | process.exit(0);
179 | }
180 |
```
--------------------------------------------------------------------------------
/src/tool-logs.ts:
--------------------------------------------------------------------------------
```typescript
1 | import axios from "axios";
2 | import { getMetaMcpApiBaseUrl, getMetaMcpApiKey } from "./utils.js";
3 | import {
4 | ProfileCapability,
5 | getProfileCapabilities,
6 | } from "./fetch-capabilities.js";
7 |
8 | // Define status enum for tool execution
9 | export enum ToolExecutionStatus {
10 | SUCCESS = "SUCCESS",
11 | ERROR = "ERROR",
12 | PENDING = "PENDING",
13 | }
14 |
15 | // Define interface for tool execution log data
16 | export interface ToolExecutionLog {
17 | id?: string;
18 | tool_name: string;
19 | payload: any;
20 | status: ToolExecutionStatus;
21 | result?: any;
22 | mcp_server_uuid: string;
23 | error_message?: string | null;
24 | execution_time_ms: number;
25 | created_at?: string;
26 | updated_at?: string;
27 | }
28 |
29 | // Response interfaces
30 | export interface ToolLogResponse {
31 | id?: string;
32 | success: boolean;
33 | data?: any;
34 | error?: string;
35 | status?: number;
36 | details?: any;
37 | }
38 |
39 | // Class to manage tool execution logs
40 | export class ToolLogManager {
41 | private static instance: ToolLogManager;
42 | private logStore: Map<string, ToolExecutionLog> = new Map();
43 |
44 | private constructor() {}
45 |
46 | public static getInstance(): ToolLogManager {
47 | if (!ToolLogManager.instance) {
48 | ToolLogManager.instance = new ToolLogManager();
49 | }
50 | return ToolLogManager.instance;
51 | }
52 |
53 | /**
54 | * Creates a new tool execution log
55 | * @param toolName Name of the tool
56 | * @param serverUuid UUID of the MCP server
57 | * @param payload The input parameters for the tool
58 | * @returns Log object with tracking ID
59 | */
60 | public async createLog(
61 | toolName: string,
62 | serverUuid: string,
63 | payload: any
64 | ): Promise<ToolExecutionLog> {
65 | // Check for TOOL_LOGS capability first
66 | const profileCapabilities = await getProfileCapabilities();
67 | const hasToolsLogCapability = profileCapabilities.includes(
68 | ProfileCapability.TOOL_LOGS
69 | );
70 |
71 | // Generate a temporary ID for tracking
72 | const tempId = `${Date.now()}-${Math.random()
73 | .toString(36)
74 | .substring(2, 9)}`;
75 |
76 | const log: ToolExecutionLog = {
77 | id: tempId, // Will be replaced with the real ID from the API
78 | tool_name: toolName,
79 | mcp_server_uuid: serverUuid,
80 | payload,
81 | status: ToolExecutionStatus.PENDING,
82 | execution_time_ms: 0,
83 | created_at: new Date().toISOString(),
84 | };
85 |
86 | // Store in memory
87 | this.logStore.set(tempId, log);
88 |
89 | // Submit to API only if TOOL_LOGS capability is present
90 | if (hasToolsLogCapability) {
91 | const response = await reportToolExecutionLog(log);
92 |
93 | // Update with real ID if available
94 | if (response.success && response.data?.id) {
95 | const newId = response.data.id;
96 | log.id = newId;
97 | this.logStore.delete(tempId);
98 | this.logStore.set(newId, log);
99 | }
100 | }
101 |
102 | return log;
103 | }
104 |
105 | /**
106 | * Updates the status of a tool execution log
107 | * @param logId ID of the log to update
108 | * @param status New status
109 | * @param result Optional result data
110 | * @param errorMessage Optional error message
111 | * @param executionTimeMs Optional execution time in milliseconds
112 | * @returns Updated log
113 | */
114 | public async updateLogStatus(
115 | logId: string,
116 | status: ToolExecutionStatus,
117 | result?: any,
118 | errorMessage?: string | null,
119 | executionTimeMs?: number
120 | ): Promise<ToolExecutionLog | null> {
121 | const log = this.logStore.get(logId);
122 |
123 | if (!log) {
124 | console.error(`Cannot update log: Log with ID ${logId} not found`);
125 | return null;
126 | }
127 |
128 | // Update log properties
129 | log.status = status;
130 | if (result !== undefined) log.result = result;
131 | if (errorMessage !== undefined) log.error_message = errorMessage;
132 | if (executionTimeMs !== undefined) log.execution_time_ms = executionTimeMs;
133 | log.updated_at = new Date().toISOString();
134 |
135 | // Update in memory
136 | this.logStore.set(logId, log);
137 |
138 | // Check for TOOL_LOGS capability before sending update to API
139 | const profileCapabilities = await getProfileCapabilities();
140 | const hasToolsLogCapability = profileCapabilities.includes(
141 | ProfileCapability.TOOL_LOGS
142 | );
143 |
144 | // Send update to API only if TOOL_LOGS capability is present
145 | if (hasToolsLogCapability) {
146 | await updateToolExecutionLog(logId, {
147 | status,
148 | result,
149 | error_message: errorMessage,
150 | execution_time_ms: executionTimeMs,
151 | });
152 | }
153 |
154 | return log;
155 | }
156 |
157 | /**
158 | * Get a log by ID
159 | * @param logId ID of the log
160 | * @returns Log or null if not found
161 | */
162 | public getLog(logId: string): ToolExecutionLog | null {
163 | return this.logStore.get(logId) || null;
164 | }
165 |
166 | /**
167 | * Complete the tool execution log with success status
168 | * @param logId ID of the log to complete
169 | * @param result Result data
170 | * @param executionTimeMs Execution time in milliseconds
171 | * @returns Updated log
172 | */
173 | public async completeLog(
174 | logId: string,
175 | result: any,
176 | executionTimeMs: number
177 | ): Promise<ToolExecutionLog | null> {
178 | return this.updateLogStatus(
179 | logId,
180 | ToolExecutionStatus.SUCCESS,
181 | result,
182 | null,
183 | executionTimeMs
184 | );
185 | }
186 |
187 | /**
188 | * Mark the tool execution log as failed
189 | * @param logId ID of the log to fail
190 | * @param errorMessage Error message
191 | * @param executionTimeMs Execution time in milliseconds
192 | * @returns Updated log
193 | */
194 | public async failLog(
195 | logId: string,
196 | errorMessage: string,
197 | executionTimeMs: number
198 | ): Promise<ToolExecutionLog | null> {
199 | return this.updateLogStatus(
200 | logId,
201 | ToolExecutionStatus.ERROR,
202 | null,
203 | errorMessage,
204 | executionTimeMs
205 | );
206 | }
207 | }
208 |
209 | /**
210 | * Reports a tool execution log to the MetaMCP API
211 | * @param logData The tool execution log data
212 | * @returns Result of the API call
213 | */
214 | export async function reportToolExecutionLog(
215 | logData: ToolExecutionLog
216 | ): Promise<ToolLogResponse> {
217 | try {
218 | // Check for TOOL_LOGS capability first
219 | const profileCapabilities = await getProfileCapabilities();
220 | const hasToolsLogCapability = profileCapabilities.includes(
221 | ProfileCapability.TOOL_LOGS
222 | );
223 |
224 | if (!hasToolsLogCapability) {
225 | return { success: false, error: "TOOL_LOGS capability not enabled" };
226 | }
227 |
228 | const apiKey = getMetaMcpApiKey();
229 | const apiBaseUrl = getMetaMcpApiBaseUrl();
230 |
231 | if (!apiKey) {
232 | return { success: false, error: "API key not set" };
233 | }
234 |
235 | // Validate required fields
236 | if (!logData.tool_name || !logData.mcp_server_uuid) {
237 | return {
238 | success: false,
239 | error: "Missing required fields: tool_name or mcp_server_uuid",
240 | status: 400,
241 | };
242 | }
243 |
244 | // Submit log to MetaMCP API
245 | try {
246 | const response = await axios.post(
247 | `${apiBaseUrl}/api/tool-execution-logs`,
248 | logData,
249 | {
250 | headers: {
251 | "Content-Type": "application/json",
252 | Authorization: `Bearer ${apiKey}`,
253 | },
254 | }
255 | );
256 |
257 | return {
258 | success: true,
259 | data: response.data,
260 | };
261 | } catch (error: any) {
262 | if (error.response) {
263 | // The request was made and the server responded with a status code outside of 2xx
264 | return {
265 | success: false,
266 | error:
267 | error.response.data.error || "Failed to submit tool execution log",
268 | status: error.response.status,
269 | details: error.response.data,
270 | };
271 | } else if (error.request) {
272 | // The request was made but no response was received
273 | return {
274 | success: false,
275 | error: "No response received from server",
276 | details: error.request,
277 | };
278 | } else {
279 | // Something happened in setting up the request
280 | return {
281 | success: false,
282 | error: "Error setting up request",
283 | details: error.message,
284 | };
285 | }
286 | }
287 | } catch (error: any) {
288 | return {
289 | success: false,
290 | error: "Failed to process tool execution log request",
291 | status: 500,
292 | details: error.message,
293 | };
294 | }
295 | }
296 |
297 | /**
298 | * Updates an existing tool execution log
299 | * @param logId The ID of the log to update
300 | * @param updateData The updated log data
301 | * @returns Result of the API call
302 | */
303 | export async function updateToolExecutionLog(
304 | logId: string,
305 | updateData: Partial<ToolExecutionLog>
306 | ): Promise<ToolLogResponse> {
307 | try {
308 | // Check for TOOL_LOGS capability first
309 | const profileCapabilities = await getProfileCapabilities();
310 | const hasToolsLogCapability = profileCapabilities.includes(
311 | ProfileCapability.TOOL_LOGS
312 | );
313 |
314 | if (!hasToolsLogCapability) {
315 | return { success: false, error: "TOOL_LOGS capability not enabled" };
316 | }
317 |
318 | const apiKey = getMetaMcpApiKey();
319 | const apiBaseUrl = getMetaMcpApiBaseUrl();
320 |
321 | if (!apiKey) {
322 | return { success: false, error: "API key not set" };
323 | }
324 |
325 | if (!logId) {
326 | return {
327 | success: false,
328 | error: "Log ID is required for updates",
329 | };
330 | }
331 |
332 | // Submit update to MetaMCP API
333 | try {
334 | const response = await axios.put(
335 | `${apiBaseUrl}/api/tool-execution-logs/${logId}`,
336 | updateData,
337 | {
338 | headers: {
339 | "Content-Type": "application/json",
340 | Authorization: `Bearer ${apiKey}`,
341 | },
342 | }
343 | );
344 |
345 | return {
346 | success: true,
347 | data: response.data,
348 | };
349 | } catch (error: any) {
350 | if (error.response) {
351 | return {
352 | success: false,
353 | error:
354 | error.response.data.error || "Failed to update tool execution log",
355 | status: error.response.status,
356 | details: error.response.data,
357 | };
358 | } else if (error.request) {
359 | return {
360 | success: false,
361 | error: "No response received from server",
362 | details: error.request,
363 | };
364 | } else {
365 | return {
366 | success: false,
367 | error: "Error setting up request",
368 | details: error.message,
369 | };
370 | }
371 | }
372 | } catch (error: any) {
373 | return {
374 | success: false,
375 | error: "Failed to process update request",
376 | status: 500,
377 | details: error.message,
378 | };
379 | }
380 | }
381 |
382 | /**
383 | * Simple function to log a tool execution
384 | * @param toolName Name of the tool
385 | * @param serverUuid UUID of the MCP server
386 | * @param payload The input parameters for the tool
387 | * @param result The result of the tool execution
388 | * @param status Status of the execution
389 | * @param errorMessage Optional error message if execution failed
390 | * @param executionTimeMs Time taken to execute the tool in milliseconds
391 | * @returns Result of the API call
392 | */
393 | export async function logToolExecution(
394 | toolName: string,
395 | serverUuid: string,
396 | payload: any,
397 | result: any = null,
398 | status: ToolExecutionStatus = ToolExecutionStatus.SUCCESS,
399 | errorMessage: string | null = null,
400 | executionTimeMs: number = 0
401 | ): Promise<ToolLogResponse> {
402 | // Check for TOOL_LOGS capability first
403 | const profileCapabilities = await getProfileCapabilities();
404 | const hasToolsLogCapability = profileCapabilities.includes(
405 | ProfileCapability.TOOL_LOGS
406 | );
407 |
408 | if (!hasToolsLogCapability) {
409 | return { success: false, error: "TOOL_LOGS capability not enabled" };
410 | }
411 |
412 | const logData: ToolExecutionLog = {
413 | tool_name: toolName,
414 | mcp_server_uuid: serverUuid,
415 | payload,
416 | status,
417 | result,
418 | error_message: errorMessage,
419 | execution_time_ms: executionTimeMs,
420 | };
421 |
422 | return await reportToolExecutionLog(logData);
423 | }
424 |
```
--------------------------------------------------------------------------------
/src/mcp-proxy.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import {
3 | CallToolRequestSchema,
4 | GetPromptRequestSchema,
5 | ListPromptsRequestSchema,
6 | ListResourcesRequestSchema,
7 | ListToolsRequestSchema,
8 | ReadResourceRequestSchema,
9 | Tool,
10 | ListToolsResultSchema,
11 | ListPromptsResultSchema,
12 | ListResourcesResultSchema,
13 | ReadResourceResultSchema,
14 | ListResourceTemplatesRequestSchema,
15 | ListResourceTemplatesResultSchema,
16 | ResourceTemplate,
17 | CompatibilityCallToolResultSchema,
18 | GetPromptResultSchema,
19 | } from "@modelcontextprotocol/sdk/types.js";
20 | import { z } from "zod";
21 | import { getMcpServers } from "./fetch-metamcp.js";
22 | import { getSessionKey, sanitizeName } from "./utils.js";
23 | import { cleanupAllSessions, getSession, initSessions } from "./sessions.js";
24 | import { ConnectedClient } from "./client.js";
25 | import { reportToolsToMetaMcp } from "./report-tools.js";
26 | import { getInactiveTools, ToolParameters } from "./fetch-tools.js";
27 | import {
28 | getProfileCapabilities,
29 | ProfileCapability,
30 | } from "./fetch-capabilities.js";
31 | import { ToolLogManager } from "./tool-logs.js";
32 |
33 | const toolToClient: Record<string, ConnectedClient> = {};
34 | const toolToServerUuid: Record<string, string> = {};
35 | const promptToClient: Record<string, ConnectedClient> = {};
36 | const resourceToClient: Record<string, ConnectedClient> = {};
37 | const inactiveToolsMap: Record<string, boolean> = {};
38 |
39 | export const createServer = async () => {
40 | const server = new Server(
41 | {
42 | name: "MetaMCP",
43 | version: "0.6.5",
44 | },
45 | {
46 | capabilities: {
47 | prompts: {},
48 | resources: {},
49 | tools: {},
50 | },
51 | }
52 | );
53 |
54 | // Initialize sessions in the background when server starts
55 | initSessions().catch();
56 |
57 | // List Tools Handler
58 | server.setRequestHandler(ListToolsRequestSchema, async (request) => {
59 | const profileCapabilities = await getProfileCapabilities(true);
60 | const serverParams = await getMcpServers(true);
61 |
62 | // Fetch inactive tools only if tools management capability is present
63 | let inactiveTools: Record<string, ToolParameters> = {};
64 | if (profileCapabilities.includes(ProfileCapability.TOOLS_MANAGEMENT)) {
65 | inactiveTools = await getInactiveTools(true);
66 | // Clear existing inactive tools map before rebuilding
67 | Object.keys(inactiveToolsMap).forEach(
68 | (key) => delete inactiveToolsMap[key]
69 | );
70 | }
71 |
72 | const allTools: Tool[] = [];
73 |
74 | await Promise.allSettled(
75 | Object.entries(serverParams).map(async ([uuid, params]) => {
76 | const sessionKey = getSessionKey(uuid, params);
77 | const session = await getSession(sessionKey, uuid, params);
78 | if (!session) return;
79 |
80 | const capabilities = session.client.getServerCapabilities();
81 | if (!capabilities?.tools) return;
82 |
83 | const serverName = session.client.getServerVersion()?.name || "";
84 | try {
85 | const result = await session.client.request(
86 | {
87 | method: "tools/list",
88 | params: { _meta: request.params?._meta },
89 | },
90 | ListToolsResultSchema
91 | );
92 |
93 | const toolsWithSource =
94 | result.tools
95 | ?.filter((tool) => {
96 | // Only filter inactive tools if tools management is enabled
97 | if (
98 | profileCapabilities.includes(
99 | ProfileCapability.TOOLS_MANAGEMENT
100 | )
101 | ) {
102 | return !inactiveTools[`${uuid}:${tool.name}`];
103 | }
104 | return true;
105 | })
106 | .map((tool) => {
107 | const toolName = `${sanitizeName(serverName)}__${tool.name}`;
108 | toolToClient[toolName] = session;
109 | toolToServerUuid[toolName] = uuid;
110 | return {
111 | ...tool,
112 | name: toolName,
113 | description: tool.description,
114 | };
115 | }) || [];
116 |
117 | // Update our inactive tools map only if tools management is enabled
118 | if (
119 | profileCapabilities.includes(ProfileCapability.TOOLS_MANAGEMENT)
120 | ) {
121 | result.tools?.forEach((tool) => {
122 | const isInactive = inactiveTools[`${uuid}:${tool.name}`];
123 | if (isInactive) {
124 | const formattedName = `${sanitizeName(serverName)}__${
125 | tool.name
126 | }`;
127 | inactiveToolsMap[formattedName] = true;
128 | }
129 | });
130 |
131 | // Report full tools for this server
132 | reportToolsToMetaMcp(
133 | result.tools.map((tool) => ({
134 | name: tool.name,
135 | description: tool.description,
136 | toolSchema: tool.inputSchema,
137 | mcp_server_uuid: uuid,
138 | }))
139 | ).catch();
140 | }
141 |
142 | allTools.push(...toolsWithSource);
143 | } catch (error) {
144 | console.error(`Error fetching tools from: ${serverName}`, error);
145 | }
146 | })
147 | );
148 |
149 | return { tools: allTools };
150 | });
151 |
152 | // Call Tool Handler
153 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
154 | const { name, arguments: args } = request.params;
155 | const originalToolName = name.split("__")[1];
156 | const clientForTool = toolToClient[name];
157 | const toolLogManager = ToolLogManager.getInstance();
158 | let logId: string | undefined;
159 | let startTime = Date.now();
160 |
161 | if (!clientForTool) {
162 | throw new Error(`Unknown tool: ${name}`);
163 | }
164 |
165 | // Get MCP server UUID for the tool
166 | const mcpServerUuid = toolToServerUuid[name] || "";
167 |
168 | if (!mcpServerUuid) {
169 | console.error(`Could not determine MCP server UUID for tool: ${name}`);
170 | }
171 |
172 | // Get profile capabilities
173 | const profileCapabilities = await getProfileCapabilities();
174 |
175 | // Only check inactive tools if tools management capability is present
176 | if (
177 | profileCapabilities.includes(ProfileCapability.TOOLS_MANAGEMENT) &&
178 | inactiveToolsMap[name]
179 | ) {
180 | throw new Error(`Tool is inactive: ${name}`);
181 | }
182 |
183 | // Check if TOOL_LOGS capability is enabled
184 | const hasToolsLogCapability = profileCapabilities.includes(
185 | ProfileCapability.TOOL_LOGS
186 | );
187 |
188 | try {
189 | // Create initial pending log only if TOOL_LOGS capability is present
190 | if (hasToolsLogCapability) {
191 | const log = await toolLogManager.createLog(
192 | originalToolName,
193 | mcpServerUuid,
194 | args || {}
195 | );
196 | logId = log.id;
197 | }
198 |
199 | // Reset the timer right before making the actual tool call
200 | startTime = Date.now();
201 |
202 | // Use the correct schema for tool calls
203 | const result = await clientForTool.client.request(
204 | {
205 | method: "tools/call",
206 | params: {
207 | name: originalToolName,
208 | arguments: args || {},
209 | _meta: {
210 | progressToken: request.params._meta?.progressToken,
211 | },
212 | },
213 | },
214 | CompatibilityCallToolResultSchema
215 | );
216 |
217 | const executionTime = Date.now() - startTime;
218 |
219 | // Update log with success result only if TOOL_LOGS capability is present
220 | if (hasToolsLogCapability && logId) {
221 | try {
222 | await toolLogManager.completeLog(logId, result, executionTime);
223 | } catch (logError) {}
224 | }
225 |
226 | return result;
227 | } catch (error: any) {
228 | const executionTime = Date.now() - startTime;
229 |
230 | // Update log with error only if TOOL_LOGS capability is present
231 | if (hasToolsLogCapability && logId) {
232 | try {
233 | await toolLogManager.failLog(
234 | logId,
235 | error.message || "Unknown error",
236 | executionTime
237 | );
238 | } catch (logError) {}
239 | }
240 |
241 | console.error(
242 | `Error calling tool "${name}" through ${
243 | clientForTool.client.getServerVersion()?.name || "unknown"
244 | }:`,
245 | error
246 | );
247 | throw error;
248 | }
249 | });
250 |
251 | // Get Prompt Handler
252 | server.setRequestHandler(GetPromptRequestSchema, async (request) => {
253 | const { name } = request.params;
254 | const clientForPrompt = promptToClient[name];
255 |
256 | if (!clientForPrompt) {
257 | throw new Error(`Unknown prompt: ${name}`);
258 | }
259 |
260 | try {
261 | const promptName = name.split("__")[1];
262 | const response = await clientForPrompt.client.request(
263 | {
264 | method: "prompts/get",
265 | params: {
266 | name: promptName,
267 | arguments: request.params.arguments || {},
268 | _meta: request.params._meta,
269 | },
270 | },
271 | GetPromptResultSchema
272 | );
273 |
274 | return response;
275 | } catch (error) {
276 | console.error(
277 | `Error getting prompt through ${
278 | clientForPrompt.client.getServerVersion()?.name
279 | }:`,
280 | error
281 | );
282 | throw error;
283 | }
284 | });
285 |
286 | // List Prompts Handler
287 | server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
288 | const serverParams = await getMcpServers(true);
289 | const allPrompts: z.infer<typeof ListPromptsResultSchema>["prompts"] = [];
290 |
291 | await Promise.allSettled(
292 | Object.entries(serverParams).map(async ([uuid, params]) => {
293 | const sessionKey = getSessionKey(uuid, params);
294 | const session = await getSession(sessionKey, uuid, params);
295 | if (!session) return;
296 |
297 | const capabilities = session.client.getServerCapabilities();
298 | if (!capabilities?.prompts) return;
299 |
300 | const serverName = session.client.getServerVersion()?.name || "";
301 | try {
302 | const result = await session.client.request(
303 | {
304 | method: "prompts/list",
305 | params: {
306 | cursor: request.params?.cursor,
307 | _meta: request.params?._meta,
308 | },
309 | },
310 | ListPromptsResultSchema
311 | );
312 |
313 | if (result.prompts) {
314 | const promptsWithSource = result.prompts.map((prompt) => {
315 | const promptName = `${sanitizeName(serverName)}__${prompt.name}`;
316 | promptToClient[promptName] = session;
317 | return {
318 | ...prompt,
319 | name: promptName,
320 | description: prompt.description || "",
321 | };
322 | });
323 | allPrompts.push(...promptsWithSource);
324 | }
325 | } catch (error) {
326 | console.error(`Error fetching prompts from: ${serverName}`, error);
327 | }
328 | })
329 | );
330 |
331 | return {
332 | prompts: allPrompts,
333 | nextCursor: request.params?.cursor,
334 | };
335 | });
336 |
337 | // List Resources Handler
338 | server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
339 | const serverParams = await getMcpServers(true);
340 | const allResources: z.infer<typeof ListResourcesResultSchema>["resources"] =
341 | [];
342 |
343 | await Promise.allSettled(
344 | Object.entries(serverParams).map(async ([uuid, params]) => {
345 | const sessionKey = getSessionKey(uuid, params);
346 | const session = await getSession(sessionKey, uuid, params);
347 | if (!session) return;
348 |
349 | const capabilities = session.client.getServerCapabilities();
350 | if (!capabilities?.resources) return;
351 |
352 | const serverName = session.client.getServerVersion()?.name || "";
353 | try {
354 | const result = await session.client.request(
355 | {
356 | method: "resources/list",
357 | params: {
358 | cursor: request.params?.cursor,
359 | _meta: request.params?._meta,
360 | },
361 | },
362 | ListResourcesResultSchema
363 | );
364 |
365 | if (result.resources) {
366 | const resourcesWithSource = result.resources.map((resource) => {
367 | resourceToClient[resource.uri] = session;
368 | return {
369 | ...resource,
370 | name: resource.name || "",
371 | };
372 | });
373 | allResources.push(...resourcesWithSource);
374 | }
375 | } catch (error) {
376 | console.error(`Error fetching resources from: ${serverName}`, error);
377 | }
378 | })
379 | );
380 |
381 | return {
382 | resources: allResources,
383 | nextCursor: request.params?.cursor,
384 | };
385 | });
386 |
387 | // Read Resource Handler
388 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
389 | const { uri } = request.params;
390 | const clientForResource = resourceToClient[uri];
391 |
392 | if (!clientForResource) {
393 | throw new Error(`Unknown resource: ${uri}`);
394 | }
395 |
396 | try {
397 | return await clientForResource.client.request(
398 | {
399 | method: "resources/read",
400 | params: {
401 | uri,
402 | _meta: request.params._meta,
403 | },
404 | },
405 | ReadResourceResultSchema
406 | );
407 | } catch (error) {
408 | console.error(
409 | `Error reading resource through ${
410 | clientForResource.client.getServerVersion()?.name
411 | }:`,
412 | error
413 | );
414 | throw error;
415 | }
416 | });
417 |
418 | // List Resource Templates Handler
419 | server.setRequestHandler(
420 | ListResourceTemplatesRequestSchema,
421 | async (request) => {
422 | const serverParams = await getMcpServers(true);
423 | const allTemplates: ResourceTemplate[] = [];
424 |
425 | await Promise.allSettled(
426 | Object.entries(serverParams).map(async ([uuid, params]) => {
427 | const sessionKey = getSessionKey(uuid, params);
428 | const session = await getSession(sessionKey, uuid, params);
429 | if (!session) return;
430 |
431 | const capabilities = session.client.getServerCapabilities();
432 | if (!capabilities?.resources) return;
433 |
434 | try {
435 | const result = await session.client.request(
436 | {
437 | method: "resources/templates/list",
438 | params: {
439 | cursor: request.params?.cursor,
440 | _meta: request.params?._meta,
441 | },
442 | },
443 | ListResourceTemplatesResultSchema
444 | );
445 |
446 | if (result.resourceTemplates) {
447 | const templatesWithSource = result.resourceTemplates.map(
448 | (template) => ({
449 | ...template,
450 | name: template.name || "",
451 | })
452 | );
453 | allTemplates.push(...templatesWithSource);
454 | }
455 | } catch (error) {
456 | return;
457 | }
458 | })
459 | );
460 |
461 | return {
462 | resourceTemplates: allTemplates,
463 | nextCursor: request.params?.cursor,
464 | };
465 | }
466 | );
467 |
468 | const cleanup = async () => {
469 | await cleanupAllSessions();
470 | };
471 |
472 | return { server, cleanup };
473 | };
474 |
```