#
tokens: 23895/50000 20/20 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![smithery badge](https://smithery.ai/badge/@metatool-ai/mcp-server-metamcp)](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 | 
```