# @leumas/mcp-bridge

The stdio ⇄ streamable-HTTP bridge — one line in any coding agent's config connects it to a Leumas install, ours or the customer's. Carries no tools of its own.


<!-- npm:strip -->
> Published to npm as **`leumas-mcp`** — unscoped, zero dependencies, its two `@leumas/*`
> imports inlined by `pnpm bridge:build`. This package stays `private` in the workspace for ever;
> the published manifest is derived, so the two cannot drift. `pnpm bridge:release` is the gate.
<!-- /npm:strip -->

**One line in any coding agent's config, pointed at any Leumas install.**

leumas-api speaks MCP over streamable HTTP. Coding agents are not uniform about it — some speak it,
some only spawn a subprocess and talk newline-delimited JSON-RPC over its stdio, and the ones that do
both configure HTTP differently from each other. This is the subprocess.

```sh
leumas-mcp --url https://api.leumas.tech        # Leumas-hosted
leumas-mcp --url http://localhost:3000          # the customer's own Studio
leumas-mcp --url http://localhost:3000 --site my-app   # one hosted MCP server
```

Because the endpoint is an argument, **"our infrastructure or yours" is the same line with a
different URL.** There is no second server, and no second thing to keep in sync.

## What it does, and what it refuses to do

It forwards. Every message goes out verbatim and every response comes back verbatim, so a tool added
to an install appears here with no release of this package. That is not laziness — the moment a
bridge knows what a tool is, it can be wrong about one.

### The one exception, and its physical reason

`leumas__deploy` is implemented **here**, not on the server, because **the files are on this
machine**. A remote tool cannot see your `dist/`, and handing it the bytes as an argument is capped
by the `/mcp` body limit and inflated 4/3 by base64 on top — a mistake this repo has already paid
for once, at 78 MB against a 2 MB limit.

It is one tool rather than five because a deploy is an ordered sequence — mint a session, create the
site, push N files, retarget, publish — and an agent handed five ordered tools gets the order wrong.
The failure is a live site pointed at a half-uploaded build, with nothing reporting an error. So the
sequence is code.

```
leumas__deploy { folder: "/path/to/my-app", name: "my-app" }
  → walks the folder, strips the wrapper, skips node_modules and .env
  → mints a deploy credential scoped to one site and one build directory
  → uploads into builds/<new>, so the live build is untouched throughout
  → points the site at it in ONE write, and publishes
  → returns the live URL
```

This is the exception that has a physical reason, not the start of a habit. Everything else forwards.

## The two rules

**1 · stdout is the protocol.** One JSON message per line and nothing else, ever. A stray banner or
progress line is a parse error inside the agent, and what the person sees is *"the MCP server failed
to start"* with nothing naming the cause. Every human-readable notice goes to stderr — including the
usage text, which is why `--help` writes nothing to stdout.

**2 · an HTTP failure must become a JSON-RPC error.** The agent is waiting on an `id`. A 401, a 403
from the gateway scope check, a 502 from a proxy, a refused connection — none of those produce a
JSON-RPC envelope of their own. A bridge that logs one and moves on leaves that request pending
**for ever**: the agent does not time out, it simply never answers the person. So every non-2xx,
every 200 with a non-JSON body, and every thrown fetch is translated, carrying the id it answers and
quoting what the server actually said.

A **notification** is the one thing that is never answered — not even when it is refused. Answering
one makes several clients error out.

## Which endpoint a URL means

| You pass | It posts to | Why |
|---|---|---|
| `https://api.leumas.tech` | `…/mcp/rpc` | a bare origin means the whole gateway |
| `…` + `--site abc` | `…/mcp/s/abc` | one hosted server — **the only form that works on a local appliance**, where there is no DNS for a subdomain to resolve through |
| `https://api.leumas.tech/mcp/s/abc` | itself | an explicit path is honoured as given |

A URL that does not parse **throws**. Posting nowhere silently is indistinguishable from a server
that has no tools.

## Credentials

`--key`, or `$LEUMAS_API_KEY`. Mint one at `<origin>/connect`; it is scoped `mcp` and reaches the MCP
planes and nothing else.

A missing key is a **warning, not a refusal** — against a local appliance the caller may already be
the owner over a session, and a hosted server may be deliberately public. The server's own 401 is a
better error than one this bridge could invent, because it is the one that is actually true.

## Verify

```sh
pnpm smoke:mcp-bridge
```

Framing and failure are checked on injected streams and a fake fetch; the last section boots the API
in-process, mints a key through `POST /keys/agent`, and drives the real `bin/leumas-mcp.mjs` as a
child process — because neither "does the credential exist" nor "does the spawned binary speak the
protocol" can be seen by reading the code.


---
Source: shared/packages/mcp-bridge/README.md
Canonical: https://docs.leumas.tech/p/packages/mcp-bridge
