# 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:
--------------------------------------------------------------------------------
```
# Auto detect text files and perform LF normalization
* text=auto
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
dist/**/*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MetaMCP MCP Server
> **🚨 DEPRECATED PACKAGE WARNING 🚨**
>
> This local proxy package is deprecated in MetaMCP's 2.0 all-in-one architecture.
>
> **Please checkout https://github.com/metatool-ai/metamcp for more details.**
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.
[](https://smithery.ai/server/@metatool-ai/mcp-server-metamcp)
<a href="https://glama.ai/mcp/servers/0po36lc7i6">
<img width="380" height="200" src="https://glama.ai/mcp/servers/0po36lc7i6/badge" alt="MetaServer MCP server" />
</a>
MetaMCP App repo: https://github.com/metatool-ai/metatool-app
## Installation
### Installing via Smithery
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.
To install MetaMCP MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@metatool-ai/mcp-server-metamcp):
```bash
npx -y @smithery/cli install @metatool-ai/mcp-server-metamcp --client claude
```
### Manual Installation
```bash
export METAMCP_API_KEY=<env>
npx -y @metamcp/mcp-server-metamcp@latest
```
```json
{
"mcpServers": {
"MetaMCP": {
"command": "npx",
"args": ["-y", "@metamcp/mcp-server-metamcp@latest"],
"env": {
"METAMCP_API_KEY": "<your api key>"
}
}
}
}
```
## Usage
### Using as a stdio server (default)
```bash
mcp-server-metamcp --metamcp-api-key <your-api-key>
```
### Using as an SSE server
```bash
mcp-server-metamcp --metamcp-api-key <your-api-key> --transport sse --port 12006
```
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.
### Using as a Streamable HTTP server
```bash
mcp-server-metamcp --metamcp-api-key <your-api-key> --transport streamable-http --port 12006
```
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.
### Using with Docker
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:
```bash
mcp-server-metamcp --metamcp-api-key <your-api-key> --transport sse --port 12006 --use-docker-host
```
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.
### Configuring stderr handling
For STDIO transport, you can control how stderr is handled from child MCP processes:
```bash
# Use inherit to see stderr output from child processes
mcp-server-metamcp --metamcp-api-key <your-api-key> --stderr inherit
# Use pipe to capture stderr (default is ignore)
mcp-server-metamcp --metamcp-api-key <your-api-key> --stderr pipe
# Or set via environment variable
METAMCP_STDERR=inherit mcp-server-metamcp --metamcp-api-key <your-api-key>
```
Available stderr options:
- `ignore` (default): Ignore stderr output from child processes
- `inherit`: Pass through stderr from child processes to the parent
- `pipe`: Capture stderr in a pipe for processing
- `overlapped`: Use overlapped I/O (Windows-specific)
### Command Line Options
```
Options:
--metamcp-api-key <key> API key for MetaMCP (can also be set via METAMCP_API_KEY env var)
--metamcp-api-base-url <url> Base URL for MetaMCP API (can also be set via METAMCP_API_BASE_URL env var)
--report Fetch all MCPs, initialize clients, and report tools to MetaMCP API
--transport <type> Transport type to use (stdio, sse, or streamable-http) (default: "stdio")
--port <port> Port to use for SSE or Streamable HTTP transport, defaults to 12006 (default: "12006")
--require-api-auth Require API key in SSE or Streamable HTTP URL path
--stateless Use stateless mode for Streamable HTTP transport
--use-docker-host Transform localhost URLs to use host.docker.internal (can also be set via USE_DOCKER_HOST env var)
--stderr <type> Stderr handling for STDIO transport (overlapped, pipe, ignore, inherit) (default: "ignore")
-h, --help display help for command
```
## Environment Variables
- `METAMCP_API_KEY`: API key for MetaMCP
- `METAMCP_API_BASE_URL`: Base URL for MetaMCP API
- `USE_DOCKER_HOST`: When set to "true", transforms localhost URLs to host.docker.internal for Docker compatibility
- `METAMCP_STDERR`: Stderr handling for STDIO transport (overlapped, pipe, ignore, inherit). Defaults to "ignore"
## Development
```bash
# Install dependencies
npm install
# Build the application
npm run build
# Watch for changes
npm run watch
```
## Highlights
- Compatible with ANY MCP Client
- Multi-Workspaces layer enables you to switch to another set of MCP configs within one-click.
- GUI dynamic updates of MCP configs.
- Namespace isolation for joined MCPs.
## Architecture Overview
```mermaid
sequenceDiagram
participant MCPClient as MCP Client (e.g. Claude Desktop)
participant MetaMCP-mcp-server as MetaMCP MCP Server
participant MetaMCPApp as MetaMCP App
participant MCPServers as Installed MCP Servers in Metatool App
MCPClient ->> MetaMCP-mcp-server: Request list tools
MetaMCP-mcp-server ->> MetaMCPApp: Get tools configuration & status
MetaMCPApp ->> MetaMCP-mcp-server: Return tools configuration & status
loop For each listed MCP Server
MetaMCP-mcp-server ->> MCPServers: Request list_tools
MCPServers ->> MetaMCP-mcp-server: Return list of tools
end
MetaMCP-mcp-server ->> MetaMCP-mcp-server: Aggregate tool lists
MetaMCP-mcp-server ->> MCPClient: Return aggregated list of tools
MCPClient ->> MetaMCP-mcp-server: Call tool
MetaMCP-mcp-server ->> MCPServers: call_tool to target MCP Server
MCPServers ->> MetaMCP-mcp-server: Return tool response
MetaMCP-mcp-server ->> MCPClient: Return tool response
```
## Credits
- Inspirations and some code (refactored in this project) from https://github.com/adamwattis/mcp-proxy-server/
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
```yaml
services:
mcp-server:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
env_file:
- .env.production.local
entrypoint: ["/bin/bash"]
command: ["-c", "uvx --version && echo 'uvx is working!' && tail -f /dev/null"]
healthcheck:
test: ["CMD", "ps", "aux", "|", "grep", "tail"]
interval: 30s
timeout: 10s
retries: 3
environment:
- NODE_ENV=production
restart: unless-stopped
# Add any additional environment variables or command arguments here
# command: --metamcp-api-key your-api-key --metamcp-api-base-url your-base-url
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Use the official uv Debian image as base
FROM ghcr.io/astral-sh/uv:debian
# Install Node.js and npm
RUN apt-get update && apt-get install -y \
curl \
gnupg \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Verify Node.js and npm installation
RUN node --version && npm --version
# Verify uv is installed correctly
RUN uv --version
# Verify npx is available
RUN npx --version || npm install -g npx
# Set the working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy the rest of the application
COPY . .
# Build the application
RUN npm run build
# Set environment variables
ENV NODE_ENV=production
# Expose the application port
EXPOSE 3000
# Run the application
ENTRYPOINT ["node", "dist/index.js"]
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
required:
- metamcpApiKey
properties:
metamcpApiKey:
type: string
description: The API key from metamcp.com/api-keys. Required.
metamcpApiBaseUrl:
type: string
description: Optional override for the MetaMCP App URL (default is https://api.metamcp.com).
commandFunction:
# A function that produces the CLI command to start the MCP on stdio.
# Note: Command line arguments can also be used directly:
# --metamcp-api-key <your-api-key> --metamcp-api-base-url <base-url>
|-
(config) => ({ command: 'node', args: ['dist/index.js'], env: { METAMCP_API_KEY: config.metamcpApiKey, ...(config.metamcpApiBaseUrl && { METAMCP_API_BASE_URL: config.metamcpApiBaseUrl }) } })
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@metamcp/mcp-server-metamcp",
"version": "0.6.5",
"description": "MCP Server MetaMCP manages all your other MCPs in one MCP.",
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"watch": "tsc --watch",
"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}",
"inspector:prod": "dotenv -e .env.production.local npx @modelcontextprotocol/inspector dist/index.js -e METAMCP_API_KEY=${METAMCP_API_KEY}",
"report": "dotenv -e .env.local -- node dist/index.js --report"
},
"repository": {
"type": "git",
"url": "git+https://github.com/metatool-ai/mcp-server-metamcp.git"
},
"author": "James Zhang",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/metatool-ai/mcp-server-metamcp/issues"
},
"homepage": "https://github.com/metatool-ai/mcp-server-metamcp#readme",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.4",
"axios": "^1.7.9",
"commander": "^13.1.0",
"express": "^4.21.2",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/express": "^5.0.1",
"@types/node": "^22.13.4",
"dotenv-cli": "^8.0.0",
"shx": "^0.3.4",
"typescript": "^5.8.2"
},
"type": "module",
"bin": {
"mcp-server-metamcp": "dist/index.js"
},
"files": [
"dist"
]
}
```
--------------------------------------------------------------------------------
/src/sessions.ts:
--------------------------------------------------------------------------------
```typescript
import { getMcpServers, ServerParameters } from "./fetch-metamcp.js";
import {
ConnectedClient,
createMetaMcpClient,
connectMetaMcpClient,
} from "./client.js";
import { getSessionKey } from "./utils.js";
const _sessions: Record<string, ConnectedClient> = {};
export const getSession = async (
sessionKey: string,
uuid: string,
params: ServerParameters
): Promise<ConnectedClient | undefined> => {
if (sessionKey in _sessions) {
return _sessions[sessionKey];
} else {
// Close existing session for this UUID if it exists with a different hash
const old_session_keys = Object.keys(_sessions).filter((k) =>
k.startsWith(`${uuid}_`)
);
await Promise.allSettled(
old_session_keys.map(async (old_session_key) => {
await _sessions[old_session_key].cleanup();
delete _sessions[old_session_key];
})
);
const { client, transport } = createMetaMcpClient(params);
if (!client || !transport) {
return;
}
const newClient = await connectMetaMcpClient(client, transport);
if (!newClient) {
return;
}
_sessions[sessionKey] = newClient;
return newClient;
}
};
export const initSessions = async (): Promise<void> => {
const serverParams = await getMcpServers(true);
await Promise.allSettled(
Object.entries(serverParams).map(async ([uuid, params]) => {
const sessionKey = getSessionKey(uuid, params);
try {
await getSession(sessionKey, uuid, params);
} catch (error) {}
})
);
};
export const cleanupAllSessions = async (): Promise<void> => {
await Promise.allSettled(
Object.entries(_sessions).map(async ([sessionKey, session]) => {
await session.cleanup();
delete _sessions[sessionKey];
})
);
};
```
--------------------------------------------------------------------------------
/src/fetch-tools.ts:
--------------------------------------------------------------------------------
```typescript
import axios from "axios";
import { getMetaMcpApiBaseUrl, getMetaMcpApiKey } from "./utils.js";
enum ToolStatus {
ACTIVE = "ACTIVE",
INACTIVE = "INACTIVE",
}
// Define interface for tool parameters with only required fields
export interface ToolParameters {
mcp_server_uuid: string;
name: string;
status: ToolStatus;
}
let _toolsCache: Record<string, ToolParameters> | null = null;
let _toolsCacheTimestamp: number = 0;
const CACHE_TTL_MS = 1000; // 1 second cache TTL
export async function getInactiveTools(
forceRefresh: boolean = false
): Promise<Record<string, ToolParameters>> {
const currentTime = Date.now();
const cacheAge = currentTime - _toolsCacheTimestamp;
// Use cache if it exists, is not null, and either:
// 1. forceRefresh is false, or
// 2. forceRefresh is true but cache is less than 1 second old
if (_toolsCache !== null && (!forceRefresh || cacheAge < CACHE_TTL_MS)) {
return _toolsCache;
}
try {
const apiKey = getMetaMcpApiKey();
const apiBaseUrl = getMetaMcpApiBaseUrl();
if (!apiKey) {
console.error(
"METAMCP_API_KEY is not set. Please set it via environment variable or command line argument."
);
return _toolsCache || {};
}
const headers = { Authorization: `Bearer ${apiKey}` };
const response = await axios.get(
`${apiBaseUrl}/api/tools?status=${ToolStatus.INACTIVE}`,
{
headers,
}
);
const data = response.data;
const toolDict: Record<string, ToolParameters> = {};
// Access the 'results' array in the response
if (data && data.results) {
for (const tool of data.results) {
const params: ToolParameters = {
mcp_server_uuid: tool.mcp_server_uuid,
name: tool.name,
status: tool.status,
};
const uniqueId = `${tool.mcp_server_uuid}:${tool.name}`;
toolDict[uniqueId] = params;
}
}
_toolsCache = toolDict;
_toolsCacheTimestamp = currentTime;
return toolDict;
} catch (error) {
// Return empty object if API doesn't exist or has errors
if (_toolsCache !== null) {
return _toolsCache;
}
return {};
}
}
```
--------------------------------------------------------------------------------
/src/fetch-capabilities.ts:
--------------------------------------------------------------------------------
```typescript
import axios from "axios";
import { getMetaMcpApiBaseUrl, getMetaMcpApiKey } from "./utils.js";
export enum ProfileCapability {
TOOLS_MANAGEMENT = "TOOLS_MANAGEMENT",
TOOL_LOGS = "TOOL_LOGS",
}
let _capabilitiesCache: ProfileCapability[] | null = null;
let _capabilitiesCacheTimestamp: number = 0;
const CACHE_TTL_MS = 1000; // 1 second cache TTL
export async function getProfileCapabilities(
forceRefresh: boolean = false
): Promise<ProfileCapability[]> {
const currentTime = Date.now();
const cacheAge = currentTime - _capabilitiesCacheTimestamp;
// Use cache if it exists, is not null, and either:
// 1. forceRefresh is false, or
// 2. forceRefresh is true but cache is less than 1 second old
if (
_capabilitiesCache !== null &&
(!forceRefresh || cacheAge < CACHE_TTL_MS)
) {
return _capabilitiesCache;
}
try {
const apiKey = getMetaMcpApiKey();
const apiBaseUrl = getMetaMcpApiBaseUrl();
if (!apiKey) {
console.error(
"METAMCP_API_KEY is not set. Please set it via environment variable or command line argument."
);
return _capabilitiesCache || [];
}
const headers = { Authorization: `Bearer ${apiKey}` };
const response = await axios.get(`${apiBaseUrl}/api/profile-capabilities`, {
headers,
});
const data = response.data;
// Access the 'profileCapabilities' array in the response
if (data && data.profileCapabilities) {
const capabilities = data.profileCapabilities
.map((capability: string) => {
// Map string to enum value if it exists, otherwise return undefined
return ProfileCapability[
capability as keyof typeof ProfileCapability
];
})
.filter(
(
capability: ProfileCapability | undefined
): capability is ProfileCapability => capability !== undefined
);
_capabilitiesCache = capabilities;
_capabilitiesCacheTimestamp = currentTime;
return capabilities;
}
return _capabilitiesCache || [];
} catch (error) {
// Return empty array if API doesn't exist or has errors
if (_capabilitiesCache !== null) {
return _capabilitiesCache;
}
return [];
}
}
```
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
```typescript
import { ServerParameters } from "./fetch-metamcp.js";
import crypto from "crypto";
/**
* Environment variables to inherit by default, if an environment is not explicitly given.
*/
export const DEFAULT_INHERITED_ENV_VARS =
process.platform === "win32"
? [
"APPDATA",
"HOMEDRIVE",
"HOMEPATH",
"LOCALAPPDATA",
"PATH",
"PROCESSOR_ARCHITECTURE",
"SYSTEMDRIVE",
"SYSTEMROOT",
"TEMP",
"USERNAME",
"USERPROFILE",
]
: /* list inspired by the default env inheritance of sudo */
["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"];
/**
* Returns a default environment object including only environment variables deemed safe to inherit.
*/
export function getDefaultEnvironment(): Record<string, string> {
const env: Record<string, string> = {};
for (const key of DEFAULT_INHERITED_ENV_VARS) {
const value = process.env[key];
if (value === undefined) {
continue;
}
if (value.startsWith("()")) {
// Skip functions, which are a security risk.
continue;
}
env[key] = value;
}
return env;
}
/**
* Get the MetaMCP API base URL from environment variables
*/
export function getMetaMcpApiBaseUrl(): string {
return process.env.METAMCP_API_BASE_URL || "https://api.metamcp.com";
}
/**
* Get the MetaMCP API key from environment variables
*/
export function getMetaMcpApiKey(): string | undefined {
return process.env.METAMCP_API_KEY;
}
export function sanitizeName(name: string): string {
return name.replace(/[^a-zA-Z0-9_-]/g, "");
}
export function computeParamsHash(
params: ServerParameters,
uuid: string
): string {
let paramsDict: any;
// Default to "STDIO" if type is undefined
if (!params.type || params.type === "STDIO") {
paramsDict = {
uuid,
type: "STDIO", // Explicitly set type to "STDIO" for consistent hashing
command: params.command,
args: params.args,
env: params.env
? Object.fromEntries(
Object.entries(params.env).sort((a, b) => a[0].localeCompare(b[0]))
)
: null,
};
} else if (params.type === "SSE" || params.type === "STREAMABLE_HTTP") {
paramsDict = {
uuid,
type: params.type,
url: params.url,
};
} else {
throw new Error(`Unsupported server type: ${params.type}`);
}
const paramsJson = JSON.stringify(paramsDict);
return crypto.createHash("sha256").update(paramsJson).digest("hex");
}
export function getSessionKey(uuid: string, params: ServerParameters): string {
return `${uuid}_${computeParamsHash(params, uuid)}`;
}
```
--------------------------------------------------------------------------------
/src/sse.ts:
--------------------------------------------------------------------------------
```typescript
import express from "express";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
export interface SSEServerOptions {
port: number;
requireApiAuth?: boolean;
}
// Starts an SSE server and returns a cleanup function
export async function startSSEServer(
server: Server,
options: SSEServerOptions
): Promise<() => Promise<void>> {
const app = express();
const port = options.port || 12006;
const requireApiAuth = options.requireApiAuth || false;
const apiKey = process.env.METAMCP_API_KEY;
// to support multiple simultaneous connections we have a lookup object from
// sessionId to transport
const transports: { [sessionId: string]: SSEServerTransport } = {};
// Define the SSE endpoint based on authentication requirement
const sseEndpoint = requireApiAuth ? `/:apiKey/sse` : `/sse`;
app.get(sseEndpoint, async (req: express.Request, res: express.Response) => {
// If API auth is required, validate the API key
if (requireApiAuth) {
const requestApiKey = req.params.apiKey;
if (!apiKey || requestApiKey !== apiKey) {
res.status(401).send("Unauthorized: Invalid API key");
return;
}
}
// Set the messages path based on authentication requirement
const messagesPath = requireApiAuth ? `/${apiKey}/messages` : `/messages`;
const transport = new SSEServerTransport(messagesPath, res);
transports[transport.sessionId] = transport;
res.on("close", () => {
delete transports[transport.sessionId];
});
await server.connect(transport);
});
// Define the messages endpoint
const messagesEndpoint = requireApiAuth ? `/:apiKey/messages` : `/messages`;
app.post(
messagesEndpoint,
async (req: express.Request, res: express.Response) => {
// If API auth is required, validate the API key
if (requireApiAuth) {
const requestApiKey = req.params.apiKey;
if (!apiKey || requestApiKey !== apiKey) {
res.status(401).send("Unauthorized: Invalid API key");
return;
}
}
const sessionId = req.query.sessionId as string;
const transport = transports[sessionId];
if (transport) {
await transport.handlePostMessage(req, res);
} else {
res.status(400).send("No transport found for sessionId");
}
}
);
const serverInstance = app.listen(port, () => {
const baseUrl = `http://localhost:${port}`;
const sseUrl = requireApiAuth
? `${baseUrl}/${apiKey}/sse`
: `${baseUrl}/sse`;
console.log(`SSE server listening on port ${port}`);
console.log(`SSE endpoint: ${sseUrl}`);
});
// Return cleanup function
return async () => {
// Close all active transports
await Promise.all(
Object.values(transports).map((transport) => transport.close())
);
serverInstance.close();
};
}
```
--------------------------------------------------------------------------------
/src/fetch-metamcp.ts:
--------------------------------------------------------------------------------
```typescript
import axios from "axios";
import {
getDefaultEnvironment,
getMetaMcpApiBaseUrl,
getMetaMcpApiKey,
} from "./utils.js";
// Define IOType for stderr handling
export type IOType = "overlapped" | "pipe" | "ignore" | "inherit";
// Define a new interface for server parameters that can be STDIO, SSE or STREAMABLE_HTTP
export interface ServerParameters {
uuid: string;
name: string;
description: string;
type?: "STDIO" | "SSE" | "STREAMABLE_HTTP"; // Optional field, defaults to "STDIO" when undefined
command?: string | null;
args?: string[] | null;
env?: Record<string, string> | null;
stderr?: IOType; // Optional field for stderr handling, defaults to "ignore"
url?: string | null;
created_at: string;
profile_uuid: string;
status: string;
oauth_tokens?: {
access_token: string;
token_type: string;
expires_in?: number | undefined;
scope?: string | undefined;
refresh_token?: string | undefined;
} | null;
}
let _mcpServersCache: Record<string, ServerParameters> | null = null;
let _mcpServersCacheTimestamp: number = 0;
const CACHE_TTL_MS = 1000; // 1 second cache TTL
export async function getMcpServers(
forceRefresh: boolean = false
): Promise<Record<string, ServerParameters>> {
const currentTime = Date.now();
const cacheAge = currentTime - _mcpServersCacheTimestamp;
// Use cache if it exists, is not null, and either:
// 1. forceRefresh is false, or
// 2. forceRefresh is true but cache is less than 1 second old
if (_mcpServersCache !== null && (!forceRefresh || cacheAge < CACHE_TTL_MS)) {
return _mcpServersCache;
}
try {
const apiKey = getMetaMcpApiKey();
const apiBaseUrl = getMetaMcpApiBaseUrl();
if (!apiKey) {
console.error(
"METAMCP_API_KEY is not set. Please set it via environment variable or command line argument."
);
return _mcpServersCache || {};
}
const headers = { Authorization: `Bearer ${apiKey}` };
const response = await axios.get(`${apiBaseUrl}/api/mcp-servers`, {
headers,
});
const data = response.data;
const serverDict: Record<string, ServerParameters> = {};
for (const serverParams of data) {
const params: ServerParameters = {
...serverParams,
type: serverParams.type || "STDIO",
};
// Process based on server type
if (params.type === "STDIO") {
if ("args" in params && !params.args) {
params.args = undefined;
}
params.env = {
...getDefaultEnvironment(),
...(params.env || {}),
};
} else if (params.type === "SSE" || params.type === "STREAMABLE_HTTP") {
// For SSE or STREAMABLE_HTTP servers, ensure url is present
if (!params.url) {
console.warn(
`${params.type} server ${params.uuid} is missing url field, skipping`
);
continue;
}
}
const uuid = params.uuid;
if (uuid) {
serverDict[uuid] = params;
}
}
_mcpServersCache = serverDict;
_mcpServersCacheTimestamp = currentTime;
return serverDict;
} catch (error) {
if (_mcpServersCache !== null) {
return _mcpServersCache;
}
return {};
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "./mcp-proxy.js";
import { Command } from "commander";
import { reportAllTools } from "./report-tools.js";
import { cleanupAllSessions } from "./sessions.js";
import { startSSEServer } from "./sse.js";
import { startStreamableHTTPServer } from "./streamable-http.js";
import { IOType } from "./fetch-metamcp.js";
const program = new Command();
program
.name("mcp-server-metamcp")
.description("MetaMCP MCP Server - The One MCP to manage all your MCPs")
.option(
"--metamcp-api-key <key>",
"API key for MetaMCP (can also be set via METAMCP_API_KEY env var)"
)
.option(
"--metamcp-api-base-url <url>",
"Base URL for MetaMCP API (can also be set via METAMCP_API_BASE_URL env var)"
)
.option(
"--report",
"Fetch all MCPs, initialize clients, and report tools to MetaMCP API"
)
.option("--transport <type>", "Transport type to use (stdio, sse, or streamable-http)", "stdio")
.option("--port <port>", "Port to use for SSE or Streamable HTTP transport, defaults to 12006", "12006")
.option("--require-api-auth", "Require API key in SSE or Streamable HTTP URL path")
.option("--stateless", "Use stateless mode for Streamable HTTP transport")
.option(
"--use-docker-host",
"Transform localhost URLs to use host.docker.internal (can also be set via USE_DOCKER_HOST env var)"
)
.option(
"--stderr <type>",
"Stderr handling for STDIO transport (overlapped, pipe, ignore, inherit)",
"ignore"
)
.parse(process.argv);
const options = program.opts();
// Validate stderr option
const validStderrTypes: IOType[] = ["overlapped", "pipe", "ignore", "inherit"];
if (!validStderrTypes.includes(options.stderr as IOType)) {
console.error(`Invalid stderr type: ${options.stderr}. Must be one of: ${validStderrTypes.join(", ")}`);
process.exit(1);
}
// Set environment variables from command line arguments
if (options.metamcpApiKey) {
process.env.METAMCP_API_KEY = options.metamcpApiKey;
}
if (options.metamcpApiBaseUrl) {
process.env.METAMCP_API_BASE_URL = options.metamcpApiBaseUrl;
}
if (options.useDockerHost) {
process.env.USE_DOCKER_HOST = "true";
}
if (options.stderr) {
process.env.METAMCP_STDERR = options.stderr;
}
async function main() {
// If --report flag is set, run the reporting function instead of starting the server
if (options.report) {
await reportAllTools();
await cleanupAllSessions();
return;
}
const { server, cleanup } = await createServer();
if (options.transport.toLowerCase() === "sse") {
// Start SSE server
const port = parseInt(options.port) || 12006;
const sseCleanup = await startSSEServer(server, {
port,
requireApiAuth: options.requireApiAuth,
});
// Cleanup on exit
const handleExit = async () => {
await cleanup();
await sseCleanup();
await server.close();
process.exit(0);
};
process.on("SIGINT", handleExit);
process.on("SIGTERM", handleExit);
} else if (options.transport.toLowerCase() === "streamable-http") {
// Start Streamable HTTP server
const port = parseInt(options.port) || 12006;
const streamableHttpCleanup = await startStreamableHTTPServer(server, {
port,
requireApiAuth: options.requireApiAuth,
stateless: options.stateless,
});
// Cleanup on exit
const handleExit = async () => {
await cleanup();
await streamableHttpCleanup();
await server.close();
process.exit(0);
};
process.on("SIGINT", handleExit);
process.on("SIGTERM", handleExit);
} else {
// Default: Start stdio server
const transport = new StdioServerTransport();
await server.connect(transport);
const handleExit = async () => {
await cleanup();
await transport.close();
await server.close();
process.exit(0);
};
// Cleanup on exit
process.on("SIGINT", handleExit);
process.on("SIGTERM", handleExit);
process.stdin.resume();
process.stdin.on("end", handleExit);
process.stdin.on("close", handleExit);
}
}
main().catch((error) => {
console.error("Server error:", error);
});
```
--------------------------------------------------------------------------------
/src/client.ts:
--------------------------------------------------------------------------------
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
StdioClientTransport,
StdioServerParameters,
} from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { ServerParameters, IOType } from "./fetch-metamcp.js";
const sleep = (time: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), time));
export interface ConnectedClient {
client: Client;
cleanup: () => Promise<void>;
}
/**
* Transforms localhost URLs to use host.docker.internal when running inside Docker
*/
const transformDockerUrl = (url: string): string => {
if (process.env.USE_DOCKER_HOST === "true") {
return url.replace(/localhost|127\.0\.0\.1/g, "host.docker.internal");
}
return url;
};
export const createMetaMcpClient = (
serverParams: ServerParameters
): { client: Client | undefined; transport: Transport | undefined } => {
let transport: Transport | undefined;
// Create the appropriate transport based on server type
// Default to "STDIO" if type is undefined
if (!serverParams.type || serverParams.type === "STDIO") {
// Get stderr value from serverParams, environment variable, or default to "ignore"
const stderrValue: IOType =
serverParams.stderr ||
(process.env.METAMCP_STDERR as IOType) ||
"ignore";
const stdioParams: StdioServerParameters = {
command: serverParams.command || "",
args: serverParams.args || undefined,
env: serverParams.env || undefined,
stderr: stderrValue,
};
transport = new StdioClientTransport(stdioParams);
// Handle stderr stream when set to "pipe"
if (stderrValue === "pipe" && (transport as any).stderr) {
const stderrStream = (transport as any).stderr;
stderrStream.on('data', (chunk: Buffer) => {
console.error(`[${serverParams.name}] ${chunk.toString().trim()}`);
});
stderrStream.on('error', (error: Error) => {
console.error(`[${serverParams.name}] stderr error:`, error);
});
}
} else if (serverParams.type === "SSE" && serverParams.url) {
// Transform the URL if USE_DOCKER_HOST is set to "true"
const transformedUrl = transformDockerUrl(serverParams.url);
if (!serverParams.oauth_tokens) {
transport = new SSEClientTransport(new URL(transformedUrl));
} else {
const headers: HeadersInit = {};
headers[
"Authorization"
] = `Bearer ${serverParams.oauth_tokens.access_token}`;
transport = new SSEClientTransport(new URL(transformedUrl), {
requestInit: {
headers,
},
eventSourceInit: {
fetch: (url, init) => fetch(url, { ...init, headers }),
},
});
}
} else if (serverParams.type === "STREAMABLE_HTTP" && serverParams.url) {
// Transform the URL if USE_DOCKER_HOST is set to "true"
const transformedUrl = transformDockerUrl(serverParams.url);
if (!serverParams.oauth_tokens) {
transport = new StreamableHTTPClientTransport(new URL(transformedUrl));
} else {
const headers: HeadersInit = {};
headers[
"Authorization"
] = `Bearer ${serverParams.oauth_tokens.access_token}`;
transport = new StreamableHTTPClientTransport(new URL(transformedUrl), {
requestInit: {
headers,
},
});
}
} else {
console.error(`Unsupported server type: ${serverParams.type}`);
return { client: undefined, transport: undefined };
}
const client = new Client(
{
name: "MetaMCP",
version: "0.6.5",
},
{
capabilities: {
prompts: {},
resources: { subscribe: true },
tools: {},
},
}
);
return { client, transport };
};
export const connectMetaMcpClient = async (
client: Client,
transport: Transport
): Promise<ConnectedClient | undefined> => {
const waitFor = 2500;
const retries = 3;
let count = 0;
let retry = true;
while (retry) {
try {
await client.connect(transport);
return {
client,
cleanup: async () => {
await transport.close();
await client.close();
},
};
} catch (error) {
count++;
retry = count < retries;
if (retry) {
try {
await client.close();
} catch {}
await sleep(waitFor);
}
}
}
};
```
--------------------------------------------------------------------------------
/src/streamable-http.ts:
--------------------------------------------------------------------------------
```typescript
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { randomUUID } from "crypto";
export interface StreamableHTTPServerOptions {
port: number;
requireApiAuth?: boolean;
stateless?: boolean;
}
// Starts a Streamable HTTP server and returns a cleanup function
export async function startStreamableHTTPServer(
server: Server,
options: StreamableHTTPServerOptions
): Promise<() => Promise<void>> {
const app = express();
app.use(express.json());
const port = options.port || 12006;
const requireApiAuth = options.requireApiAuth || false;
const stateless = options.stateless || false;
const apiKey = process.env.METAMCP_API_KEY;
// Map to store transports by session ID (when using stateful mode)
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
// Define the MCP endpoint path based on authentication requirement
const mcpEndpoint = requireApiAuth ? `/:apiKey/mcp` : `/mcp`;
// Handle all HTTP methods for the MCP endpoint
app.all(mcpEndpoint, async (req: express.Request, res: express.Response) => {
// If API auth is required, validate the API key
if (requireApiAuth) {
const requestApiKey = req.params.apiKey;
if (!apiKey || requestApiKey !== apiKey) {
res.status(401).send("Unauthorized: Invalid API key");
return;
}
}
if (stateless) {
// Stateless mode: Create a new transport for each request
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // No session management
});
res.on("close", () => {
transport.close();
});
try {
// Connect to the server
await server.connect(transport);
// Handle the request
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("Error handling streamable HTTP request:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error",
},
id: null,
});
}
}
} else {
// Stateful mode: Use session management
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && req.method === "POST") {
// New initialization request
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
// Store the transport by session ID
transports[sessionId] = transport;
}
});
// Clean up transport when closed
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};
// Connect to the server
await server.connect(transport);
} else {
// Invalid request
res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Bad Request: No valid session ID provided",
},
id: null,
});
return;
}
try {
// Handle the request
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("Error handling streamable HTTP request:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error",
},
id: null,
});
}
}
}
});
const serverInstance = app.listen(port, () => {
const baseUrl = `http://localhost:${port}`;
const mcpUrl = requireApiAuth ? `${baseUrl}/${apiKey}/mcp` : `${baseUrl}/mcp`;
console.log(`Streamable HTTP server listening on port ${port}`);
console.log(`MCP endpoint: ${mcpUrl}`);
console.log(`Mode: ${stateless ? "Stateless" : "Stateful"}`);
});
// Return cleanup function
return async () => {
// Close all active transports
await Promise.all(
Object.values(transports).map((transport) => transport.close())
);
serverInstance.close();
};
}
```
--------------------------------------------------------------------------------
/src/report-tools.ts:
--------------------------------------------------------------------------------
```typescript
import axios from "axios";
import { getMetaMcpApiBaseUrl, getMetaMcpApiKey } from "./utils.js";
import { getMcpServers } from "./fetch-metamcp.js";
import { initSessions, getSession } from "./sessions.js";
import { getSessionKey } from "./utils.js";
import { ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
// Define interface for tool data structure
export interface MetaMcpTool {
name: string;
description?: string;
toolSchema: any;
mcp_server_uuid: string;
}
// API route handler for submitting tools to MetaMCP
export async function reportToolsToMetaMcp(tools: MetaMcpTool[]) {
try {
const apiKey = getMetaMcpApiKey();
const apiBaseUrl = getMetaMcpApiBaseUrl();
if (!apiKey) {
return { error: "API key not set" };
}
// Validate that tools is an array
if (!Array.isArray(tools) || tools.length === 0) {
return {
error: "Request must include a non-empty array of tools",
status: 400,
};
}
// Validate required fields for all tools and prepare for submission
const validTools = [];
const errors = [];
for (const tool of tools) {
const { name, description, toolSchema, mcp_server_uuid } = tool;
// Validate required fields for each tool
if (!name || !toolSchema || !mcp_server_uuid) {
errors.push({
tool,
error:
"Missing required fields: name, toolSchema, or mcp_server_uuid",
});
continue;
}
validTools.push({
name,
description,
toolSchema,
mcp_server_uuid,
});
}
// Submit valid tools to MetaMCP API
let results: any[] = [];
if (validTools.length > 0) {
try {
const response = await axios.post(
`${apiBaseUrl}/api/tools`,
{ tools: validTools },
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
}
);
results = response.data.results || [];
} catch (error: any) {
if (error.response) {
// The request was made and the server responded with a status code outside of 2xx
return {
error: error.response.data.error || "Failed to submit tools",
status: error.response.status,
details: error.response.data,
};
} else if (error.request) {
// The request was made but no response was received
return {
error: "No response received from server",
details: error.request,
};
} else {
// Something happened in setting up the request
return {
error: "Error setting up request",
details: error.message,
};
}
}
}
return {
results,
errors,
success: results.length > 0,
failureCount: errors.length,
successCount: results.length,
};
} catch (error: any) {
return {
error: "Failed to process tools request",
status: 500,
};
}
}
// Function to fetch all MCP servers, initialize clients, and report tools to MetaMCP API
export async function reportAllTools() {
console.log("Fetching all MCPs and initializing clients...");
// Get all MCP servers
const serverParams = await getMcpServers();
// Initialize all sessions
await initSessions();
console.log(`Found ${Object.keys(serverParams).length} MCP servers`);
// For each server, get its tools and report them
await Promise.allSettled(
Object.entries(serverParams).map(async ([uuid, params]) => {
const sessionKey = getSessionKey(uuid, params);
const session = await getSession(sessionKey, uuid, params);
if (!session) {
console.log(`Could not establish session for ${params.name} (${uuid})`);
return;
}
const capabilities = session.client.getServerCapabilities();
if (!capabilities?.tools) {
console.log(`Server ${params.name} (${uuid}) does not support tools`);
return;
}
try {
console.log(`Fetching tools from ${params.name} (${uuid})...`);
const result = await session.client.request(
{ method: "tools/list", params: {} },
ListToolsResultSchema
);
if (result.tools && result.tools.length > 0) {
console.log(
`Reporting ${result.tools.length} tools from ${params.name} to MetaMCP API...`
);
const reportResult = await reportToolsToMetaMcp(
result.tools.map((tool) => ({
name: tool.name,
description: tool.description,
toolSchema: tool.inputSchema,
mcp_server_uuid: uuid,
}))
);
console.log(
`Reported tools from ${params.name}: ${reportResult.successCount} succeeded, ${reportResult.failureCount} failed`
);
} else {
console.log(`No tools found for ${params.name}`);
}
} catch (error) {
console.error(`Error reporting tools for ${params.name}:`, error);
}
})
);
console.log("Finished reporting all tools to MetaMCP API");
process.exit(0);
}
```
--------------------------------------------------------------------------------
/src/tool-logs.ts:
--------------------------------------------------------------------------------
```typescript
import axios from "axios";
import { getMetaMcpApiBaseUrl, getMetaMcpApiKey } from "./utils.js";
import {
ProfileCapability,
getProfileCapabilities,
} from "./fetch-capabilities.js";
// Define status enum for tool execution
export enum ToolExecutionStatus {
SUCCESS = "SUCCESS",
ERROR = "ERROR",
PENDING = "PENDING",
}
// Define interface for tool execution log data
export interface ToolExecutionLog {
id?: string;
tool_name: string;
payload: any;
status: ToolExecutionStatus;
result?: any;
mcp_server_uuid: string;
error_message?: string | null;
execution_time_ms: number;
created_at?: string;
updated_at?: string;
}
// Response interfaces
export interface ToolLogResponse {
id?: string;
success: boolean;
data?: any;
error?: string;
status?: number;
details?: any;
}
// Class to manage tool execution logs
export class ToolLogManager {
private static instance: ToolLogManager;
private logStore: Map<string, ToolExecutionLog> = new Map();
private constructor() {}
public static getInstance(): ToolLogManager {
if (!ToolLogManager.instance) {
ToolLogManager.instance = new ToolLogManager();
}
return ToolLogManager.instance;
}
/**
* Creates a new tool execution log
* @param toolName Name of the tool
* @param serverUuid UUID of the MCP server
* @param payload The input parameters for the tool
* @returns Log object with tracking ID
*/
public async createLog(
toolName: string,
serverUuid: string,
payload: any
): Promise<ToolExecutionLog> {
// Check for TOOL_LOGS capability first
const profileCapabilities = await getProfileCapabilities();
const hasToolsLogCapability = profileCapabilities.includes(
ProfileCapability.TOOL_LOGS
);
// Generate a temporary ID for tracking
const tempId = `${Date.now()}-${Math.random()
.toString(36)
.substring(2, 9)}`;
const log: ToolExecutionLog = {
id: tempId, // Will be replaced with the real ID from the API
tool_name: toolName,
mcp_server_uuid: serverUuid,
payload,
status: ToolExecutionStatus.PENDING,
execution_time_ms: 0,
created_at: new Date().toISOString(),
};
// Store in memory
this.logStore.set(tempId, log);
// Submit to API only if TOOL_LOGS capability is present
if (hasToolsLogCapability) {
const response = await reportToolExecutionLog(log);
// Update with real ID if available
if (response.success && response.data?.id) {
const newId = response.data.id;
log.id = newId;
this.logStore.delete(tempId);
this.logStore.set(newId, log);
}
}
return log;
}
/**
* Updates the status of a tool execution log
* @param logId ID of the log to update
* @param status New status
* @param result Optional result data
* @param errorMessage Optional error message
* @param executionTimeMs Optional execution time in milliseconds
* @returns Updated log
*/
public async updateLogStatus(
logId: string,
status: ToolExecutionStatus,
result?: any,
errorMessage?: string | null,
executionTimeMs?: number
): Promise<ToolExecutionLog | null> {
const log = this.logStore.get(logId);
if (!log) {
console.error(`Cannot update log: Log with ID ${logId} not found`);
return null;
}
// Update log properties
log.status = status;
if (result !== undefined) log.result = result;
if (errorMessage !== undefined) log.error_message = errorMessage;
if (executionTimeMs !== undefined) log.execution_time_ms = executionTimeMs;
log.updated_at = new Date().toISOString();
// Update in memory
this.logStore.set(logId, log);
// Check for TOOL_LOGS capability before sending update to API
const profileCapabilities = await getProfileCapabilities();
const hasToolsLogCapability = profileCapabilities.includes(
ProfileCapability.TOOL_LOGS
);
// Send update to API only if TOOL_LOGS capability is present
if (hasToolsLogCapability) {
await updateToolExecutionLog(logId, {
status,
result,
error_message: errorMessage,
execution_time_ms: executionTimeMs,
});
}
return log;
}
/**
* Get a log by ID
* @param logId ID of the log
* @returns Log or null if not found
*/
public getLog(logId: string): ToolExecutionLog | null {
return this.logStore.get(logId) || null;
}
/**
* Complete the tool execution log with success status
* @param logId ID of the log to complete
* @param result Result data
* @param executionTimeMs Execution time in milliseconds
* @returns Updated log
*/
public async completeLog(
logId: string,
result: any,
executionTimeMs: number
): Promise<ToolExecutionLog | null> {
return this.updateLogStatus(
logId,
ToolExecutionStatus.SUCCESS,
result,
null,
executionTimeMs
);
}
/**
* Mark the tool execution log as failed
* @param logId ID of the log to fail
* @param errorMessage Error message
* @param executionTimeMs Execution time in milliseconds
* @returns Updated log
*/
public async failLog(
logId: string,
errorMessage: string,
executionTimeMs: number
): Promise<ToolExecutionLog | null> {
return this.updateLogStatus(
logId,
ToolExecutionStatus.ERROR,
null,
errorMessage,
executionTimeMs
);
}
}
/**
* Reports a tool execution log to the MetaMCP API
* @param logData The tool execution log data
* @returns Result of the API call
*/
export async function reportToolExecutionLog(
logData: ToolExecutionLog
): Promise<ToolLogResponse> {
try {
// Check for TOOL_LOGS capability first
const profileCapabilities = await getProfileCapabilities();
const hasToolsLogCapability = profileCapabilities.includes(
ProfileCapability.TOOL_LOGS
);
if (!hasToolsLogCapability) {
return { success: false, error: "TOOL_LOGS capability not enabled" };
}
const apiKey = getMetaMcpApiKey();
const apiBaseUrl = getMetaMcpApiBaseUrl();
if (!apiKey) {
return { success: false, error: "API key not set" };
}
// Validate required fields
if (!logData.tool_name || !logData.mcp_server_uuid) {
return {
success: false,
error: "Missing required fields: tool_name or mcp_server_uuid",
status: 400,
};
}
// Submit log to MetaMCP API
try {
const response = await axios.post(
`${apiBaseUrl}/api/tool-execution-logs`,
logData,
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
}
);
return {
success: true,
data: response.data,
};
} catch (error: any) {
if (error.response) {
// The request was made and the server responded with a status code outside of 2xx
return {
success: false,
error:
error.response.data.error || "Failed to submit tool execution log",
status: error.response.status,
details: error.response.data,
};
} else if (error.request) {
// The request was made but no response was received
return {
success: false,
error: "No response received from server",
details: error.request,
};
} else {
// Something happened in setting up the request
return {
success: false,
error: "Error setting up request",
details: error.message,
};
}
}
} catch (error: any) {
return {
success: false,
error: "Failed to process tool execution log request",
status: 500,
details: error.message,
};
}
}
/**
* Updates an existing tool execution log
* @param logId The ID of the log to update
* @param updateData The updated log data
* @returns Result of the API call
*/
export async function updateToolExecutionLog(
logId: string,
updateData: Partial<ToolExecutionLog>
): Promise<ToolLogResponse> {
try {
// Check for TOOL_LOGS capability first
const profileCapabilities = await getProfileCapabilities();
const hasToolsLogCapability = profileCapabilities.includes(
ProfileCapability.TOOL_LOGS
);
if (!hasToolsLogCapability) {
return { success: false, error: "TOOL_LOGS capability not enabled" };
}
const apiKey = getMetaMcpApiKey();
const apiBaseUrl = getMetaMcpApiBaseUrl();
if (!apiKey) {
return { success: false, error: "API key not set" };
}
if (!logId) {
return {
success: false,
error: "Log ID is required for updates",
};
}
// Submit update to MetaMCP API
try {
const response = await axios.put(
`${apiBaseUrl}/api/tool-execution-logs/${logId}`,
updateData,
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
}
);
return {
success: true,
data: response.data,
};
} catch (error: any) {
if (error.response) {
return {
success: false,
error:
error.response.data.error || "Failed to update tool execution log",
status: error.response.status,
details: error.response.data,
};
} else if (error.request) {
return {
success: false,
error: "No response received from server",
details: error.request,
};
} else {
return {
success: false,
error: "Error setting up request",
details: error.message,
};
}
}
} catch (error: any) {
return {
success: false,
error: "Failed to process update request",
status: 500,
details: error.message,
};
}
}
/**
* Simple function to log a tool execution
* @param toolName Name of the tool
* @param serverUuid UUID of the MCP server
* @param payload The input parameters for the tool
* @param result The result of the tool execution
* @param status Status of the execution
* @param errorMessage Optional error message if execution failed
* @param executionTimeMs Time taken to execute the tool in milliseconds
* @returns Result of the API call
*/
export async function logToolExecution(
toolName: string,
serverUuid: string,
payload: any,
result: any = null,
status: ToolExecutionStatus = ToolExecutionStatus.SUCCESS,
errorMessage: string | null = null,
executionTimeMs: number = 0
): Promise<ToolLogResponse> {
// Check for TOOL_LOGS capability first
const profileCapabilities = await getProfileCapabilities();
const hasToolsLogCapability = profileCapabilities.includes(
ProfileCapability.TOOL_LOGS
);
if (!hasToolsLogCapability) {
return { success: false, error: "TOOL_LOGS capability not enabled" };
}
const logData: ToolExecutionLog = {
tool_name: toolName,
mcp_server_uuid: serverUuid,
payload,
status,
result,
error_message: errorMessage,
execution_time_ms: executionTimeMs,
};
return await reportToolExecutionLog(logData);
}
```
--------------------------------------------------------------------------------
/src/mcp-proxy.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
CallToolRequestSchema,
GetPromptRequestSchema,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
Tool,
ListToolsResultSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
ReadResourceResultSchema,
ListResourceTemplatesRequestSchema,
ListResourceTemplatesResultSchema,
ResourceTemplate,
CompatibilityCallToolResultSchema,
GetPromptResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { getMcpServers } from "./fetch-metamcp.js";
import { getSessionKey, sanitizeName } from "./utils.js";
import { cleanupAllSessions, getSession, initSessions } from "./sessions.js";
import { ConnectedClient } from "./client.js";
import { reportToolsToMetaMcp } from "./report-tools.js";
import { getInactiveTools, ToolParameters } from "./fetch-tools.js";
import {
getProfileCapabilities,
ProfileCapability,
} from "./fetch-capabilities.js";
import { ToolLogManager } from "./tool-logs.js";
const toolToClient: Record<string, ConnectedClient> = {};
const toolToServerUuid: Record<string, string> = {};
const promptToClient: Record<string, ConnectedClient> = {};
const resourceToClient: Record<string, ConnectedClient> = {};
const inactiveToolsMap: Record<string, boolean> = {};
export const createServer = async () => {
const server = new Server(
{
name: "MetaMCP",
version: "0.6.5",
},
{
capabilities: {
prompts: {},
resources: {},
tools: {},
},
}
);
// Initialize sessions in the background when server starts
initSessions().catch();
// List Tools Handler
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
const profileCapabilities = await getProfileCapabilities(true);
const serverParams = await getMcpServers(true);
// Fetch inactive tools only if tools management capability is present
let inactiveTools: Record<string, ToolParameters> = {};
if (profileCapabilities.includes(ProfileCapability.TOOLS_MANAGEMENT)) {
inactiveTools = await getInactiveTools(true);
// Clear existing inactive tools map before rebuilding
Object.keys(inactiveToolsMap).forEach(
(key) => delete inactiveToolsMap[key]
);
}
const allTools: Tool[] = [];
await Promise.allSettled(
Object.entries(serverParams).map(async ([uuid, params]) => {
const sessionKey = getSessionKey(uuid, params);
const session = await getSession(sessionKey, uuid, params);
if (!session) return;
const capabilities = session.client.getServerCapabilities();
if (!capabilities?.tools) return;
const serverName = session.client.getServerVersion()?.name || "";
try {
const result = await session.client.request(
{
method: "tools/list",
params: { _meta: request.params?._meta },
},
ListToolsResultSchema
);
const toolsWithSource =
result.tools
?.filter((tool) => {
// Only filter inactive tools if tools management is enabled
if (
profileCapabilities.includes(
ProfileCapability.TOOLS_MANAGEMENT
)
) {
return !inactiveTools[`${uuid}:${tool.name}`];
}
return true;
})
.map((tool) => {
const toolName = `${sanitizeName(serverName)}__${tool.name}`;
toolToClient[toolName] = session;
toolToServerUuid[toolName] = uuid;
return {
...tool,
name: toolName,
description: tool.description,
};
}) || [];
// Update our inactive tools map only if tools management is enabled
if (
profileCapabilities.includes(ProfileCapability.TOOLS_MANAGEMENT)
) {
result.tools?.forEach((tool) => {
const isInactive = inactiveTools[`${uuid}:${tool.name}`];
if (isInactive) {
const formattedName = `${sanitizeName(serverName)}__${
tool.name
}`;
inactiveToolsMap[formattedName] = true;
}
});
// Report full tools for this server
reportToolsToMetaMcp(
result.tools.map((tool) => ({
name: tool.name,
description: tool.description,
toolSchema: tool.inputSchema,
mcp_server_uuid: uuid,
}))
).catch();
}
allTools.push(...toolsWithSource);
} catch (error) {
console.error(`Error fetching tools from: ${serverName}`, error);
}
})
);
return { tools: allTools };
});
// Call Tool Handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const originalToolName = name.split("__")[1];
const clientForTool = toolToClient[name];
const toolLogManager = ToolLogManager.getInstance();
let logId: string | undefined;
let startTime = Date.now();
if (!clientForTool) {
throw new Error(`Unknown tool: ${name}`);
}
// Get MCP server UUID for the tool
const mcpServerUuid = toolToServerUuid[name] || "";
if (!mcpServerUuid) {
console.error(`Could not determine MCP server UUID for tool: ${name}`);
}
// Get profile capabilities
const profileCapabilities = await getProfileCapabilities();
// Only check inactive tools if tools management capability is present
if (
profileCapabilities.includes(ProfileCapability.TOOLS_MANAGEMENT) &&
inactiveToolsMap[name]
) {
throw new Error(`Tool is inactive: ${name}`);
}
// Check if TOOL_LOGS capability is enabled
const hasToolsLogCapability = profileCapabilities.includes(
ProfileCapability.TOOL_LOGS
);
try {
// Create initial pending log only if TOOL_LOGS capability is present
if (hasToolsLogCapability) {
const log = await toolLogManager.createLog(
originalToolName,
mcpServerUuid,
args || {}
);
logId = log.id;
}
// Reset the timer right before making the actual tool call
startTime = Date.now();
// Use the correct schema for tool calls
const result = await clientForTool.client.request(
{
method: "tools/call",
params: {
name: originalToolName,
arguments: args || {},
_meta: {
progressToken: request.params._meta?.progressToken,
},
},
},
CompatibilityCallToolResultSchema
);
const executionTime = Date.now() - startTime;
// Update log with success result only if TOOL_LOGS capability is present
if (hasToolsLogCapability && logId) {
try {
await toolLogManager.completeLog(logId, result, executionTime);
} catch (logError) {}
}
return result;
} catch (error: any) {
const executionTime = Date.now() - startTime;
// Update log with error only if TOOL_LOGS capability is present
if (hasToolsLogCapability && logId) {
try {
await toolLogManager.failLog(
logId,
error.message || "Unknown error",
executionTime
);
} catch (logError) {}
}
console.error(
`Error calling tool "${name}" through ${
clientForTool.client.getServerVersion()?.name || "unknown"
}:`,
error
);
throw error;
}
});
// Get Prompt Handler
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name } = request.params;
const clientForPrompt = promptToClient[name];
if (!clientForPrompt) {
throw new Error(`Unknown prompt: ${name}`);
}
try {
const promptName = name.split("__")[1];
const response = await clientForPrompt.client.request(
{
method: "prompts/get",
params: {
name: promptName,
arguments: request.params.arguments || {},
_meta: request.params._meta,
},
},
GetPromptResultSchema
);
return response;
} catch (error) {
console.error(
`Error getting prompt through ${
clientForPrompt.client.getServerVersion()?.name
}:`,
error
);
throw error;
}
});
// List Prompts Handler
server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
const serverParams = await getMcpServers(true);
const allPrompts: z.infer<typeof ListPromptsResultSchema>["prompts"] = [];
await Promise.allSettled(
Object.entries(serverParams).map(async ([uuid, params]) => {
const sessionKey = getSessionKey(uuid, params);
const session = await getSession(sessionKey, uuid, params);
if (!session) return;
const capabilities = session.client.getServerCapabilities();
if (!capabilities?.prompts) return;
const serverName = session.client.getServerVersion()?.name || "";
try {
const result = await session.client.request(
{
method: "prompts/list",
params: {
cursor: request.params?.cursor,
_meta: request.params?._meta,
},
},
ListPromptsResultSchema
);
if (result.prompts) {
const promptsWithSource = result.prompts.map((prompt) => {
const promptName = `${sanitizeName(serverName)}__${prompt.name}`;
promptToClient[promptName] = session;
return {
...prompt,
name: promptName,
description: prompt.description || "",
};
});
allPrompts.push(...promptsWithSource);
}
} catch (error) {
console.error(`Error fetching prompts from: ${serverName}`, error);
}
})
);
return {
prompts: allPrompts,
nextCursor: request.params?.cursor,
};
});
// List Resources Handler
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
const serverParams = await getMcpServers(true);
const allResources: z.infer<typeof ListResourcesResultSchema>["resources"] =
[];
await Promise.allSettled(
Object.entries(serverParams).map(async ([uuid, params]) => {
const sessionKey = getSessionKey(uuid, params);
const session = await getSession(sessionKey, uuid, params);
if (!session) return;
const capabilities = session.client.getServerCapabilities();
if (!capabilities?.resources) return;
const serverName = session.client.getServerVersion()?.name || "";
try {
const result = await session.client.request(
{
method: "resources/list",
params: {
cursor: request.params?.cursor,
_meta: request.params?._meta,
},
},
ListResourcesResultSchema
);
if (result.resources) {
const resourcesWithSource = result.resources.map((resource) => {
resourceToClient[resource.uri] = session;
return {
...resource,
name: resource.name || "",
};
});
allResources.push(...resourcesWithSource);
}
} catch (error) {
console.error(`Error fetching resources from: ${serverName}`, error);
}
})
);
return {
resources: allResources,
nextCursor: request.params?.cursor,
};
});
// Read Resource Handler
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
const clientForResource = resourceToClient[uri];
if (!clientForResource) {
throw new Error(`Unknown resource: ${uri}`);
}
try {
return await clientForResource.client.request(
{
method: "resources/read",
params: {
uri,
_meta: request.params._meta,
},
},
ReadResourceResultSchema
);
} catch (error) {
console.error(
`Error reading resource through ${
clientForResource.client.getServerVersion()?.name
}:`,
error
);
throw error;
}
});
// List Resource Templates Handler
server.setRequestHandler(
ListResourceTemplatesRequestSchema,
async (request) => {
const serverParams = await getMcpServers(true);
const allTemplates: ResourceTemplate[] = [];
await Promise.allSettled(
Object.entries(serverParams).map(async ([uuid, params]) => {
const sessionKey = getSessionKey(uuid, params);
const session = await getSession(sessionKey, uuid, params);
if (!session) return;
const capabilities = session.client.getServerCapabilities();
if (!capabilities?.resources) return;
try {
const result = await session.client.request(
{
method: "resources/templates/list",
params: {
cursor: request.params?.cursor,
_meta: request.params?._meta,
},
},
ListResourceTemplatesResultSchema
);
if (result.resourceTemplates) {
const templatesWithSource = result.resourceTemplates.map(
(template) => ({
...template,
name: template.name || "",
})
);
allTemplates.push(...templatesWithSource);
}
} catch (error) {
return;
}
})
);
return {
resourceTemplates: allTemplates,
nextCursor: request.params?.cursor,
};
}
);
const cleanup = async () => {
await cleanupAllSessions();
};
return { server, cleanup };
};
```