# @getbourdon/mcp-server

Bourdon **L6 — the MCP server facade** (BUSL-1.1). The TypeScript mirror of the
Python `core/l6_server.py` on `@modelcontextprotocol/sdk`. It exposes
`@getbourdon/federation` (the L6Store + trust boundary) natively to any MCP-aware
agent (Claude Code, Codex, Cursor) and is **wire-compatible with the Python
server in BOTH directions**, so mixed-language federation works.

> Python (`pip install bourdon`) is the oracle. This package asserts its tool
> payloads against the `@getbourdon/conformance` `mcp_snapshots` fixtures,
> recovered through the exact same wire path a Python `RemoteL6Client` uses.

> **The npm packages and the Python engine share write locks.** From this release,
> `@getbourdon/federation` takes the same OS-level file locks and library gate as the
> Python engine (`pip install bourdon`), through `@getbourdon/fs-lock`, so writers from
> both can safely write one library at the same time. Older versions on either side use
> an incompatible protocol, so **upgrade both**: every npm package, and the Python engine
> to the next engine release. Stop every running Bourdon writer (MCP servers, CLIs, the
> tray) before upgrading, and start them again after.
>
> Writes need the lock binary, which ships for **Windows x64**, **Linux x64 and arm64**
> (glibc and musl) and **macOS on Apple silicon**. **Intel macOS and Windows arm64 are
> pending**: there the packages read normally but refuse writes with
> `OsLockUnavailableError` until those platforms pass and ship.

## The wire contract (the single highest-risk detail)

Every tool handler returns **JSON-in-TextContent**:

```ts
{ content: [{ type: "text", text: JSON.stringify(payload) }] }
```

— **not** the SDK's default structured content. The Python `RemoteL6Client` (and
`@getbourdon/client`) recover the payload with `json.loads(item.text)` /
`JSON.parse(text)`. Returning structured content instead makes a Python peer's
`json.loads(item.text)` get `None` and silently breaks federation. The
`mcp_snapshots` fixtures snapshot the **post-round-trip** payload, so the parity
test drives a real MCP client over an in-memory transport, recovers
`content[0].text`, `JSON.parse`s it, normalizes, and compares to the Python `.res`.

## Tools (15) + resources (3)

Thin delegates to `@getbourdon/federation` + `@getbourdon/recognition`. Tool
names, arg names, and defaults are byte-identical to the Python server —
including the **access_level default split**:

| default `public` (read/query) | default `team` (recognition / turn) |
|---|---|
| `query_agent_memory`, `list_recent_work`, `find_entity`, `get_cross_agent_summary` | `prepare_recognition_context`, `compile_codex_turn`, `get_deeper_context` |

`list_agents`, `export_agents`, `commit_to_federation`, `list_notes`, and
`list_workstreams` round out the read/write set. Version history ships as
`list_memory_versions` and `explain_memory_change` (reads) plus
`rollback_agent_memory` — a trusted-only, audit-gated restore of an archived
manifest, byte-for-byte; credential redaction applies at every read surface, so
restored bytes are never emitted unredacted.
`compile_codex_turn` returns the **deferred P7 stub** (`{_status:"deferred",
schema_version:"codex-turn-brief/v1", reason}`) — the turn compiler is
environment-bound and ships in Phase 7. Resources:
`agent-library://agents`, `agent-library://agents/{agent_id}/memory`,
`agent-library://entities/{name}`.

## Transports + auth

```ts
import { createL6Server, runStdio, runHttpServer } from "@getbourdon/mcp-server";
import { L6Store, FederationRegistry } from "@getbourdon/federation";

const store = new L6Store("/path/to/agent-library");
const registry = new FederationRegistry();

// stdio (the MCP-host default; resolves to OPERATOR — v0.8.0 behavior):
await runStdio(createL6Server(store, { registry }));

// streamable-HTTP (Bearer auth, stateless):
runHttpServer(() => createL6Server(store, { registry }), {
  host: "127.0.0.1", // default; a NON-loopback bind without auth REFUSES to start
  port: 7500,
  registry,
});
```

Security invariants are enforced **in code**: an empty Bearer can never
authenticate as OPERATOR (the legacy compare needs both a configured legacy token
and a non-empty presented token); a non-loopback bind without auth (or with
`--allow-unauthenticated`, which is loopback-only) throws `BindRefusedError`;
identity propagates to the tool handlers via `AsyncLocalStorage`
(`runWithCaller`) so a quarantined caller can never escalate.

The `bourdon-l6-server` bin mirrors `python -m bourdon.core.l6_server`:
`--library --transport {stdio,http} --port 7500 --host 127.0.0.1 --federate
--peer --peers-config --no-peers --allow-unauthenticated`.

### Explicit peer authority

The server starts with **zero remote peers** and does not read
`~/.bourdon/peers.yaml` by default. Networking is enabled only by one of these
explicit options:

- `--federate` loads `~/.bourdon/peers.yaml` and fails closed if it is missing or invalid.
- `--peers-config <path>` loads only the selected file and fails closed if it is missing or invalid.
- `--peer <url>` adds an endpoint directly and may be repeated.
- `--no-peers` asserts local-only operation and rejects every option above.

Configuration files use this shape:

```yaml
peers:
  - name: production-peer
    url: https://peer.example.com
    token_env: BOURDON_PEER_PRODUCTION
```

`token_env` defaults to `BOURDON_PEER_TOKEN` and must remain in the
`BOURDON_PEER_*` namespace. Peer URLs must use HTTPS unless they target a
loopback host; embedded credentials, queries, and fragments are rejected.

Migration note: installations that previously relied on automatic loading of
`~/.bourdon/peers.yaml` must now pass `--federate` explicitly.

## Calendar filters

`list_recent_work` and `list_notes` accept `since` as `YYYY-MM-DD`, or that
date followed by `T` or a space and `HH:MM`, optional `:SS` and fractional
seconds. Datetimes may end in `Z`, `+HH:MM`, `-HH:MM`, `+HHMM`, or `-HHMM`.
Filtering preserves the supplied calendar day; an offset does not shift it
to a different UTC day. Dates and time/offset components must be valid.

Impossible dates, invalid times and natural-language dates warn and use the
existing omitted-input behavior: the cursor's saved cutoff when available,
otherwise the default fourteen-day window. ISO week dates, basic dates,
and the broader grammar of Python's version-dependent parser are outside
this TypeScript input contract.

## License

BUSL-1.1 — see `LICENSE` + `LICENSE_FAQ.md`.
