Serve every MCP tool server you already run to your agents from one endpoint, with one key.
What an upstream is
An upstream is an MCP server the middleware connects to on your behalf. MCP is the open protocol agents use to call tool servers. The middleware gathers the tools from every upstream you configure and serves them from one endpoint, so an agent configures one connection and sees every tool. The middleware explains what else that endpoint does.
Why you would want one
- Your agent needs tools from three servers. Without the middleware you configure three connections in each agent. With it you configure one, and add servers in one place.
- A hosted tool server wants its own credential. The middleware holds that credential and adds it to each request, so the agent never sees it.
- You want every tool call checked and counted. Each call passes through the one endpoint, so one place can refuse it, and one place can count it.
How it works
Follow the top row of the diagram. Your agent asks for a tool by name. The middleware's MCP endpoint checks who is calling, looks the tool up in the catalog it built from every upstream, and forwards the call to the upstream that owns the tool. The upstream runs the tool. The result comes back through the same endpoint, where the call is counted. The bottom row is the other connection, hooks, which Hooks and the plugin covers.
Try it
Start with an MCP server you already run. This example uses an HTTP upstream to keep the connection visible. Stdio upstreams work too, as Reference shows. Run this from the repository root:
export CONDENSATE_HOST=127.0.0.1
export CONDENSATE_MCP_UPSTREAMS='[{"name":"tools","url":"http://127.0.0.1:9000/mcp","auth":"none"}]'
export CONDENSATE_LOCAL_PRINCIPAL_ID=local-dev
export CONDENSATE_LOCAL_API_KEY="$(openssl rand -hex 32)"
bun apps/middleware/backend/index.ts
You should see the backend start and listen on port 4787. In another terminal, set CONDENSATE_LOCAL_API_KEY to the same key used by the backend. Then request the catalog:
curl --fail-with-body http://127.0.0.1:4787/mcp \
-H "x-condensate-key: $CONDENSATE_LOCAL_API_KEY" \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
You should see a JSON-RPC response with result.tools. Aggregated tool names take the form <upstream>__<tool>, so a read tool on the tools upstream becomes tools__read.
What happens when
You point an agent at the endpoint
Configure your agent's HTTP MCP client with:
| Setting | Value |
|---|---|
| URL | http://127.0.0.1:4787/mcp |
| Header | x-condensate-key: YOUR_LOCAL_KEY |
The client configuration format depends on the agent. Use its supported HTTP MCP setup rather than copying one client's configuration into another. For Claude Code and Codex, the Condensate plugin sets this up for you.
The endpoint takes POST requests. A GET returns HTTP 405. It also accepts initialize and ping. Each tools/call is metered; Code mode and credits explains the count.
The middleware has no way to check callers
The middleware needs a way to check callers before it serves /mcp, /hooks, /watcher/* or /credits/topup. Without one, those routes refuse requests with HTTP 503. For a single-operator machine, set CONDENSATE_LOCAL_API_KEY. The key is checked in process, with no network call. A hosted deployment uses EXECUTOR_URL or WorkOS sign-in (WORKOS_CLIENT_ID) instead. If both EXECUTOR_URL and CONDENSATE_LOCAL_API_KEY are set, the backend refuses to boot.
CONDENSATE_LOCAL_PRINCIPAL_ID names the caller that the key belongs to. Credits, consent tiers and code-mode allow-lists are all keyed on that id, so use the same id everywhere you refer to this caller.
An agent switches its own rules
The middleware also serves harness__set_mode, which switches the session's guidance-note profile. A switch that narrows authority applies at once. A switch that widens it waits for a person to confirm it. Two upstream names are reserved for the middleware's own tools: the prefix of harness__set_mode and the prefix of the codemode__ tools. Do not use either for an upstream.
You want a smaller tool list
Set CONDENSATE_MCP_TOOL_SURFACE to control what tools/list advertises. full (the default) lists every tool with its schema. lean advertises only the built-in search tools, plus the code-mode tools for callers allowed to use them, and tells clients to find everything else through codemode__search. Use lean only together with code mode. Calls keep the same authority under either setting.
Reference
The backend listens on port 4787 unless you set CONDENSATE_PORT. Setting CONDENSATE_HOST=127.0.0.1 keeps the listener on loopback. Without it, the backend accepts connections on every interface.
Upstream entries
CONDENSATE_MCP_UPSTREAMS takes a JSON array. Each entry needs a name that is unique and contains no double underscore, because __ separates the upstream name from the tool name. An entry uses either url (HTTP) or command (stdio), never both.
| Field | Used with | Meaning |
|---|---|---|
url | HTTP | The upstream's MCP endpoint. Must be http or https. |
auth | HTTP | How the middleware authenticates to the upstream. See the table below. Defaults to none. |
headers | HTTP | An object that maps header names to env var references such as "$UPSTREAM_KEY". Cannot be combined with auth. |
command | stdio | The program to start. The middleware reaps these child processes on shutdown. |
args, env, cwd | stdio | Arguments, environment and working directory for the child. Stdio entries take no auth, so put credentials in env. |
auth value | What the middleware sends upstream |
|---|---|
none | No credential. |
bearer:$ENV_NAME | Authorization: Bearer with the value of that environment variable. The variable must be set at boot. |
byok | The caller's x-upstream-key-<name> header, forwarded as Authorization to that upstream only. It is never stored or logged. |
x402-key | The caller's own executor API key, for gateways that meter by key. |
trust:$ENV_NAME | A shared trust token from that environment variable, sent as x-condensate-trust so a metering gateway skips its per-call payment check. The variable must be set at boot. |
{ "headers": { "Name": "$ENV" } } | Arbitrary headers, each read from an environment variable. |
Any other value stops the backend from booting.
The auth in the example describes the connection from the middleware to the upstream server. Choose the mode your own upstream expects. Notion, Linear and GitHub are hosted providers with pinned auth modes. Switch them on with their CONDENSATE_MCP_<PROVIDER>_URL and credential variables, or list them in CONDENSATE_MCP_PROVIDERS, rather than writing their auth by hand.
Settings on this page
| Setting | What it does |
|---|---|
CONDENSATE_PORT | The port the backend listens on. Default 4787. |
CONDENSATE_HOST | The address the backend listens on. 127.0.0.1 keeps it on loopback. |
CONDENSATE_MCP_UPSTREAMS | The JSON array of upstream entries above. |
CONDENSATE_MCP_PROVIDERS, CONDENSATE_MCP_<PROVIDER>_URL | Switch on a hosted provider with its pinned auth mode. |
CONDENSATE_MCP_TOOL_SURFACE | full (default) or lean. |
CONDENSATE_LOCAL_API_KEY | The key a single-operator machine checks callers with, in process. |
CONDENSATE_LOCAL_PRINCIPAL_ID | The caller id that key belongs to. |
EXECUTOR_URL, WORKOS_CLIENT_ID | The caller check a hosted deployment uses instead of the local key. |
Read next
- Hooks and the plugin, for the second connection: the events your agent sends and what the middleware may do with them.
- Code mode and credits, for how each call through this endpoint is counted, and how one program can make several calls.
- Hook up Claude Code and Codex, for the install steps when your agent is one of those two.