Meta Manager
FreeNot checkedFederates multiple MCP servers into a single endpoint with search, call, and admin tools, plus inbound/outbound OAuth.
About
Federates multiple MCP servers into a single endpoint with search, call, and admin tools, plus inbound/outbound OAuth.
README
One MCP endpoint for every tool you own.
Self-hosted control plane that federates stdio and remote MCP servers, keeps them warm, indexes tools for retrieval, and exposes a single Streamable HTTP surface to Cursor, Claude, ChatGPT, and other agents.
Cursor / Claude / ChatGPT
│ one connection
▼
Meta MCP Manager ──► GitHub · Logfire · Context7 · your stdio servers · …
│
search → schema → call
manage_servers · manage_auth (admin)
- Scale past client tool caps — agents see a small fixed meta-tool set, not 500 schemas.
- Cursor-compatible config — drop in an
mcp.jsonyou already use. - Inbound OAuth — Claude/ChatGPT paste your URL; PKCE + DCR + consent page.
- Outbound OAuth — connect protected upstreams (e.g. Logfire) via
manage_auth. - Open source, single binary-ish Docker image — MIT, no phone-home.
Table of contents
- Why
- Features
- Quick start
- Docker
- Connect clients
- Meta tools
- Configuration
- OAuth
- Brand & connector packaging
- Architecture
- Development
- Security notes
- Status & roadmap
- Contributing
- Security
- License
Why
AI clients hard-cap tools and burn tokens on every schema. Loading dozens of MCPs directly:
- Hits limits (e.g. ~40 tools in some IDEs).
- Degrades tool selection accuracy.
- Makes OAuth, restarts, and inventory unmanageable.
Meta sits in the middle: one public MCP, many upstreams, search-first discovery.
Features
| Area | Capability |
|---|---|
| Transports | Warm stdio pool; remote Streamable HTTP (+ SSE where configured) |
| Discovery | BM25 search_tools, paginated list_servers / list_tools |
| Admin | manage_servers CRUD; enable/disable/reload |
| Outbound auth | OAuth 2.1 + PKCE to upstreams; encrypted token vault; refresh |
| Inbound auth | OAuth AS + PRM for Claude/ChatGPT; API key for local clients |
| Scopes | meta:read (catalog), meta:invoke (call), meta:admin (manage_*) |
| Ops | Health JSON, audit log (arg keys only), Docker Compose |
| Packaging | Logo SVG, docs/legal pages, CSP + security headers |
Quick start
Requirements
- Python 3.11+
- Optional: Node (
npx) if your upstream MCPs need it - Optional: uv
Install & run
git clone https://github.com/arthurkatcher/meta-mcp-manager.git
cd meta-mcp-manager
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
# or: uv pip install -e ".[dev]"
cp config/mcp.example.json mcp.json
# Edit paths / servers as needed
export META_API_KEY="$(openssl rand -hex 16)"
meta serve --config mcp.json --host 127.0.0.1 --port 8080 --data-dir ./data
| URL | Description |
|---|---|
http://127.0.0.1:8080/mcp |
MCP endpoint |
http://127.0.0.1:8080/health |
Health + stats |
http://127.0.0.1:8080/docs |
Human docs |
http://127.0.0.1:8080/static/logo.svg |
Logo |
curl -s http://127.0.0.1:8080/health | jq .
Docker
export META_API_KEY="$(openssl rand -hex 16)"
docker compose up --build
Compose mounts config/mcp.docker.json and persists /data. For public clients (Claude/ChatGPT) set:
export META_PUBLIC_URL=https://your.domain.example
export META_OAUTH_PASSWORD="$META_API_KEY" # or a separate consent password
docker compose up --build
Connect clients
Cursor / VS Code (API key)
{
"mcpServers": {
"meta": {
"url": "http://127.0.0.1:8080/mcp",
"headers": {
"Authorization": "Bearer YOUR_META_API_KEY"
}
}
}
}
Grok (~/.grok/config.toml)
[mcp_servers.meta]
url = "http://127.0.0.1:8080/mcp"
enabled = true
[mcp_servers.meta.headers]
Authorization = "Bearer YOUR_META_API_KEY"
Or: grok mcp add --transport http meta http://127.0.0.1:8080/mcp --header "Authorization: Bearer YOUR_META_API_KEY"
Claude Desktop (stdio bridge + API key)
{
"mcpServers": {
"meta": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://127.0.0.1:8080/mcp",
"--header",
"Authorization: Bearer YOUR_META_API_KEY"
]
}
}
}
Claude / ChatGPT (remote + inbound OAuth)
Expose Meta with HTTPS (tunnel or reverse proxy):
ngrok http 8080 export META_PUBLIC_URL=https://YOUR_SUBDOMAIN.ngrok-free.dev meta serve --config mcp.json --host 0.0.0.0 --port 8080 \ --public-url "$META_PUBLIC_URL" --api-key "$META_API_KEY"In the client, add remote MCP URL:
https://YOUR_SUBDOMAIN.ngrok-free.dev/mcpComplete browser consent with
META_OAUTH_PASSWORDorMETA_API_KEY.
No static Bearer header required for that path.
Meta tools
| Tool | Purpose |
|---|---|
search_tools |
BM25 search over the catalog |
get_tool_schema |
Full JSON schema for server/tool |
call_tool |
Invoke upstream tool |
list_servers |
Paginated server inventory |
list_tools |
Paginated tool catalog (server filter optional) |
stats |
Counts / health |
manage_servers |
create · update · delete · get · list · enable · disable · reload |
manage_auth |
login · status · logout · refresh (no raw tokens to the model) |
Agent path: search_tools → get_tool_schema → call_tool.
Add an upstream MCP (+ OAuth)
manage_servers(action="create", server_id="logfire",
url="https://logfire-us.pydantic.dev/mcp")
# if runtime_status == "auth_required":
manage_auth(action="login", server_id="logfire")
# open authorization_url → Meta /oauth/callback completes
manage_auth(action="status", server_id="logfire")
search_tools(query="list projects")
call_tool(tool_id="logfire/project_list", arguments={})
Admin tools require scope meta:admin. API keys grant full admin.
Configuration
mcp.json (Cursor-compatible)
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
},
"context7": {
"url": "https://mcp.context7.com/mcp"
},
"protected": {
"url": "https://example.com/mcp",
"headers": {
"Authorization": "Bearer ${env:SERVICE_TOKEN}"
},
"auth": {
"CLIENT_ID": "${env:OAUTH_CLIENT_ID}",
"CLIENT_SECRET": "${env:OAUTH_CLIENT_SECRET}",
"scopes": ["read", "write"]
}
}
}
}
${env:NAME} is interpolated at load time.
Environment variables
| Variable | Description |
|---|---|
META_API_KEY |
Bearer key for local clients; default consent password if unset |
META_OAUTH_PASSWORD |
Consent password for inbound OAuth (optional) |
META_PUBLIC_URL |
Public base URL (required for OAuth redirects / PRM) |
META_CONFIG |
Path to mcp.json |
META_DATA_DIR |
SQLite, vaults, logs |
META_HOST / META_PORT |
Bind address |
META_OAUTH_ENABLED |
Inbound OAuth AS (default true) |
META_ALLOW_API_KEY |
Accept API key on /mcp (default true) |
META_REQUIRE_API_KEY |
Require auth on /mcp (default true) |
META_STRICT_STDIO |
Require absolute stdio commands (default false) |
META_STDIO_ALLOW_PREFIXES |
:-separated path prefixes allowed when strict |
CLI:
meta serve --config mcp.json --host 127.0.0.1 --port 8080 \
--api-key "$META_API_KEY" --public-url "$META_PUBLIC_URL"
OAuth
Two directions
| Direction | Meaning |
|---|---|
| Outbound | Meta → upstream MCP (Logfire, Linear, …) via manage_auth + /oauth/callback |
| Inbound | Claude/ChatGPT → Meta via PRM/AS + /oauth/authorize + /oauth/token |
Inbound discovery endpoints
| Endpoint | Spec role |
|---|---|
GET /.well-known/oauth-protected-resource |
RFC 9728 PRM |
GET /.well-known/oauth-authorization-server |
RFC 8414 AS metadata |
POST /oauth/register |
Dynamic client registration |
GET/POST /oauth/authorize |
Authorization code + consent |
POST /oauth/token |
Code exchange / refresh |
Scopes
| Scope | Access |
|---|---|
meta:read |
search, list, schema, stats (catalog) |
meta:invoke |
call_tool when META_REQUIRE_INVOKE_SCOPE=true |
meta:admin |
manage_servers, manage_auth |
Default OAuth consent grants meta:read + meta:invoke (catalog + call tools).meta:admin is never default — must be requested explicitly. API keys get all scopes.
Set META_REQUIRE_INVOKE_SCOPE=true to enforce that call_tool needs meta:invoke (recommended when you also issue read-only tokens).
Rate limits (defaults, per principal / min)
| Surface | Default | Env |
|---|---|---|
call_tool |
60 | META_RATE_CALL_TOOL |
manage_* |
30 | META_RATE_MANAGE |
catalog (search / list_* / stats) |
180 | META_RATE_CATALOG |
| OAuth register / token / authorize | separate | (built-in) |
Window: META_RATE_WINDOW_SEC (default 60). Set a limit to 0 to disable that bucket. Exceeded tools return error: rate_limited.
Product database (one SQLite file)
Everything durable lives in {META_DATA_DIR}/meta.db (WAL). Multiple tables, one file for backup/UI:
| Table(s) | Purpose |
|---|---|
servers, tools |
Catalog of upstream MCPs + tool schemas |
activity |
Meta-tool call log (Activity UI) |
oauth_tokens, oauth_pending |
Outbound OAuth (token blobs Fernet-encrypted) |
oauth_clients, auth_codes, access_tokens, … |
Inbound OAuth AS |
meta_schema |
Schema version |
Still separate on disk: mcp.json (config templates), token.key (Fernet key, mode 0600), optional JSONL mirror logs/audit.jsonl.
Legacy split files (activity.db, oauth.db, inbound_oauth.db) are merged into meta.db once on startup and renamed *.migrated.
Activity columns: event_id, ts, duration_ms, meta_tool, actor_label (oauth · Claude, …), ok, error_code, args_keys (names only), target_tool_id, …
OAuth actors use registered client_name from DCR when available.
Inbound bearer secrets (access/refresh/auth codes/client_secret) are stored as SHA-256 digests in SQLite (plaintext only at issuance).
Production checklist
| Control | Env / action |
|---|---|
| Strong API key | META_API_KEY ≥16 chars, unique |
| Consent password | META_OAUTH_PASSWORD (separate from API key preferred) |
| Public bind | Auto strict_stdio; refuse weak keys |
| DCR lock | META_DCR_TOKEN=… and/or META_DCR_REDIRECT_ALLOWLIST=claude.ai,.openai.com |
| Disable DCR | META_DCR_ENABLED=false |
| Catalog-only OAuth | META_REQUIRE_INVOKE_SCOPE=true + grant meta:invoke when needed |
| SSRF | META_ALLOW_PRIVATE_URLS=false when not needing localhost MCPs |
| Activity retention | META_ACTIVITY_RETENTION_DAYS=30 (default) |
| Single worker | Do not set WEB_CONCURRENCY>1 (refused at start) |
| Backup | meta backup --out /safe/path (copies meta.db, token.key, mcp.json) |
| TLS | Terminate at reverse proxy / tunnel |
Brand & connector packaging
| Asset | Path |
|---|---|
| Logo | /static/logo.svg |
| Favicon | /static/favicon.svg |
| Docs | /docs |
| Terms | /legal/terms |
| Privacy | /legal/privacy |
| Brand pack | /brand |
PRM includes logo_uri, resource_documentation, resource_tos_uri, resource_policy_uri.
HTML/OAuth responses send CSP and standard security headers.
See docs/CONNECTOR.md for the full surface and production checklist.
Architecture
src/meta_manager/
app.py # HTTP routes, lifespan
main.py # CLI: meta serve
config/ # mcp.json load/save, settings
runtime/ # stdio + HTTP session pool
catalog/ # SQLite catalog + BM25
edge/ # meta tools, auth middleware, CSP
oauth/ # outbound vault + inbound AS
admin/ # manage_servers / manage_auth
brand/ # docs/legal HTML
static/ # logo, favicon
Design notes: ARCHITECTURE.md.
Development
source .venv/bin/activate
pip install -e ".[dev]"
pytest -q --timeout=90
# optional real-MCP stress (Meta must be running):
# META_URL=http://127.0.0.1:8080/mcp META_API_KEY=... python scripts/hardcore_eval_real.py
Fixtures under fixtures/ provide stdio math/echo/many-tools and an HTTP time server for CI without flaky public remotes.
Security notes
- Self-hosted: you operate the instance; protect
META_API_KEY/META_OAUTH_PASSWORDand network exposure. - Prefer HTTPS and a stable domain for inbound OAuth (not long-term free tunnels).
- Upstream tokens are stored under
META_DATA_DIR(Fernet-encrypted vault); back up and restrict that directory. - Admin tools can add arbitrary commands (stdio) and URLs — treat
meta:adminlike root. - Audit log records tool ids and argument keys, not secret values.
- Legal pages under
/legal/*are OSS self-host templates, not formal counsel.
Status & roadmap
v0.1 — production-capable for single-operator self-host: federation, BM25 search, warm stdio, remote HTTP, outbound OAuth, inbound OAuth (PRM/AS/DCR/PKCE), admin tools with scope gates, Docker, connector branding, unified SQLite, activity log, rate limits, resource-bound tokens.
Still out of scope for multi-tenant SaaS: external IdP, distributed rate limits (Redis), multi-replica HA, marketplace listings.
See CHANGELOG.md for release notes.
Contributing
See CONTRIBUTING.md and CODE_OF_CONDUCT.md. PRs and issues welcome.
Security
Please report vulnerabilities privately — SECURITY.md.
License
MIT — Copyright (c) 2026 Meta MCP Manager contributors
Install Meta Manager in Claude Desktop, Claude Code & Cursor
unyly install meta-mcp-managerInstalls into Claude Desktop, Claude Code, Cursor & VS Code — handles npx, uvx and build-from-source repos for you.
First time? Get the CLI: curl -fsSL https://unyly.org/install | sh
Or configure manually
Run in your terminal:
claude mcp add meta-mcp-manager -- uvx --from git+https://github.com/arthurkatcher/meta-mcp-manager meta-managerFAQ
Is Meta Manager MCP free?
Yes, Meta Manager MCP is free — one-click install via Unyly at no cost.
Does Meta Manager need an API key?
No, Meta Manager runs without API keys or environment variables.
Is Meta Manager hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Meta Manager in Claude Desktop, Claude Code or Cursor?
Open Meta Manager on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
GitHub
PRs, issues, code search, CI status
by GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
by mcpdotdirectCompare Meta Manager with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All development MCPs
