mcp-facade
README.md
# mcp-facade
A generic MCP facade: one stdio process sits in front of an upstream MCP server
and exposes only a configured subset of its tools — with compacted schemas —
plus three meta-tools (`discover`, `describe`, `call`) that keep the rest of the
catalog reachable on demand.
## Why
Every tool an MCP server exposes is injected into the model's context as a JSON
schema on **every request**. A fat server with 40 tools can cost tens of
thousands of tokens per session before any work happens — most of it for tools
you never call.
The facade flips the economics: you pay full schema tokens only for the tools
you actually use (listed in `used`), compacted to their essentials. Everything
else stays discoverable through the meta-tools, which cost three small schemas
total.
## What it does
- Runs as a stdio MCP server: `bun run facade.ts --server <name>`. One process
per upstream server.
- Reads `facade.servers.json` (next to `facade.ts`) and picks the `<name>` entry.
- On the first `tools/list`, fetches the upstream catalog and caches it to disk
(`~/.omp/agent/mcp-facade/catalogs/<name>.json`, 7-day TTL). The upstream
connection is lazy — nothing connects until first use.
- Serves each `used` tool with a pruned schema:
- every `description` string (tool-level and inside the JSON schema) is cut to
its first sentence (tool-level max 140 chars, field-level max 100);
- `$comment`, `examples`, `default`, and `additionalProperties` keys are dropped recursively;
- recursion follows `required` fields; optional subtrees collapse to
`{ "type": …, "description": "≤60 chars" }`, so a fat optional branch costs
one line instead of dozens;
- the full original shape stays one `describe` hop away;
- tool names are lowercased; lookup is case-insensitive.
- Always appends the three meta-tools (see below).
- If the catalog can't be fetched at `tools/list` time, it degrades to serving
the meta-tools only and logs the reason to stderr.
- Forwards calls to the upstream. For HTTP upstreams with a `credentialId`, a
401/unauthorized/expired-token error triggers one token force-refresh and a
single retry.
## The meta-tools
| Tool | Purpose |
| --- | --- |
| `discover` | Keyword-search the full upstream catalog (name + description, substring, max 10 hits). Returns `name — one-line description` lines. |
| `describe` | Return the **full original** schema and documentation for one tool, by lowercase name. Use before calling an unfamiliar tool. |
| `call` | Call any upstream tool by name with an `args` object, including tools not in `used`. |
Typical agent flow: `discover "worklog"` → `describe addworklog` →
`call { tool: "addworklog", args: { ... } }`.
## Requirements
- [Bun](https://bun.sh) (the facade runs TypeScript directly).
- For OAuth-protected HTTP upstreams: the OMP `omp` CLI installed at `~/.bun/bin/omp`, with the credential already authorized. The facade fetches tokens via `omp token <credentialId>` (and `omp token --force-refresh <credentialId>` on retry). Secrets are never stored in the config.
- For stdio upstreams that need env vars (API keys, tokens): an existing Claude
host config at `~/.claude.json` holding that server's `env` block (see
`envFrom` below).
## Install
```sh
bun install
cp facade.servers.example.json facade.servers.json # then edit
```
`facade.servers.json` is gitignored — it may contain local paths.
## Configuration
`facade.servers.json` maps a server name to its upstream and used-tools list:
```jsonc
{
"<name>": {
"upstream": {
// HTTP upstream (Streamable HTTP transport):
"url": "https://mcp.example.com/v1/mcp",
"credentialId": "mcp_oauth:profile:default:https://mcp.example.com/v1/mcp" // optional
// optional: direct OAuth refresh on 401 (see refresh.ts). Never put the
// secret itself in config — reference it:
"oauth": {
// "tokenUrl": "https://example.com/v1/token",
// "clientId": "<client-id>",
// "clientSecretFrom": "keychain:<service>" | "env:<VAR>", // omit for public clients
// "authStyle": "body" | "basic" // body = default; basic required by Figma
}
// …or stdio upstream:
// "command": "/usr/local/bin/npx",
// "args": ["-y", "@example/mcp-server"],
// "envFrom": "claude:<server-name>", // optional: pull env from ~/.claude.json mcpServers.<server-name>.env
// "env": { "EXTRA": "value" } // optional: merged on top
},
"used": ["tool_one", "tool_two"] // exposed directly; everything else via meta-tools
}
}
```
Notes:
- `used` entries are matched case-insensitively and served lowercased.
- `envFrom` currently only supports the `claude:<name>` prefix.
- An empty `used` list is valid: the facade then exposes only the meta-tools.
## Registering with a host
Point your host's MCP config at the facade, one entry per upstream:
```jsonc
{
"mcpServers": {
"acme": {
"command": "/path/to/bun",
"args": ["run", "/path/to/mcp-facade/facade.ts", "--server", "acme-http"]
}
}
}
```
## ⚠️ stdout is protocol
The stdio transport owns stdout. **Never** write logs, diagnostics, or debug
output to stdout — anything on stdout corrupts the JSON-RPC stream and wedges
the host. The facade logs only to stderr (`console.error`); keep it that way in
any fork.
## Limitations
- Hardcoded paths: catalog cache at `~/.omp/agent/mcp-facade/catalogs/`, OMP
binary at `~/.bun/bin/omp`, `envFrom` reads `~/.claude.json` only.
- Catalog is fetched with a single `listTools` call — no pagination, no
`tools/list_changed` handling. Restart the facade (or wait out the 7-day TTL)
to pick up upstream tool changes.
- `discover` is a simple substring match, capped at 10 results.
- One retry on auth failure; other upstream errors propagate as-is.
- No support for upstream prompts, resources, or sampling — tools only.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues