AGENTS.md@packages/mcp · git:20261001.cb2effa · 2026-10-01 · sha256 ffc557ab40f608ee

AGENTS.md@packages/mcp git:20261001.cb2effaA

Immutable. This exact content never changes and is served at /api/v1/blob/ffc557ab40f608ee.

# mcp

## Purpose

Loom's adapter for the standalone `gleam_mcp` library. This package turns
untrusted tool listings into `cap/mcp/<server>` Gleam source and translates
values between Loom's capability MessagePack wire and the library's JSON
vocabulary. Protocol codecs, stdio framing, the client state machine and
native process ownership belong to `gleam_mcp`.

MCP reaches a model through code mode only: one generated module per server.
`codegen.generate` returns source text and a rendered surface at boot. It
never compiles code, starts a process, or writes a file. The jailed build
compiles only the generated modules the submitted program imports.
`client/mcp` owns configuration, client startup, cleanup custody, and routing.

## Key Types

- `mcp/codegen.Generated(module_name, source, surface)` is the complete
  generated artifact. `GenerateError` refuses excessive tool counts,
  name collisions, schema budgets, excessive source or surface sizes, and
  sanitizer failures.
- `mcp/schema.Shape` is the recursive rendering plan: primitives, string
  enums, lists, records, mappings, nullable values, disjoint alternatives,
  raw object/array shapes, null and explicit `ValueFallback(reason)`.
  `Field` retains each original name, required/optional presence, note and
  default annotation; `Openness` records whether extra properties are allowed.
  `input_fields` plans a usable top-level object, while `shape` handles a
  recursive input or output value. The older `Plan` and `ParamType` remain
  for scalar accounting; the structural renderer consumes `Shape`.
  Every required name survives; an unusable input object becomes a whole
  arguments value.
- `mcp/name` maps wire names to Gleam identifiers, retaining an injected
  digest suffix whenever a name changes. Wire names themselves never change.
- `mcp/interchange.InterchangeFault` names an unrepresentable value's path.
  `to_json` converts `core/msgpack.MsgPackValue` directly to
  `gleam_mcp/json.JsonValue`; `to_msgpack` performs the reverse conversion.
- `gleam_mcp/protocol.ToolDescriptor` supplies the generator's tool name,
  descriptions and raw schemas. The library's JSON type is used directly
  throughout this adapter; Loom's frozen `core/json` remains independent.

## Relationships

- **Depends on**: `gleam_stdlib`; `core` for `core/msgpack`; `gleam_mcp`
  for protocol descriptors and JSON values. The local modules perform
  no I/O. The dependency includes an Erlang runtime, so this package does
  not claim the portable subset held by `core`, `machine` and `prompt`.
- **Depended on by**: `client`, through `client/catalog`'s name checks and
  `client/mcp`'s generation and capability conversion. Generated source
  imports `cap/internal/mcp`, the fixed `cap/internal/mcp_codec`, `cap/mcp`
  and `cap/report`, but this package has no dependency on `cap`: those
  imports are emitted as source text.
- **FFI**: none. Native stdio process operations belong to the external
  library's `gleam_mcp/internal/ffi_port` module.

## Traffic

- **Actor messages, commits and registers**: none in this package.
- **Wire values**: `interchange` translates the arguments and structured
  results crossing `client/mcp`'s `mcp.<server>` capability arm. It does
  not own either wire's transport or framing.
- **Generated artifacts**: `codegen.generate` returns source for
  `cap/mcp/<segment>` plus the surface shown to the model. `client/mcp`
  retains both. `codemode/build` writes selected modules inside the
  vendored capability package after seed cloning and before compilation.

## Invariants

- **Code-mode authority stays per server.** Generated functions close over
  the original server and tool names and call `cap/internal/mcp.invoke`.
  Typed output calls use `invoke_typed`; neither form exposes a generic
  model-callable dispatcher. The records and decoders grant no authority.
- **Wire names travel verbatim.** Mangling affects Gleam names only.
  Escaped literals preserve original tool and parameter names; a residual
  tool-name collision refuses the server. A parameter-label collision
  degrades that tool to whole-value arguments.
- **Server prose remains inert.** Descriptions lose control and direction
  changing codepoints and are capped. Every generated doc line starts
  `/// `. `scan_for_at` refuses an attribute outside a string or comment.
- **Generation is bounded.** Across input and output schemas, the precheck
  admits at most 16,384 JSON nodes, 262,144 text bytes and JSON depth 32.
  Recursive typed planning falls back beyond depth 12 or 32 union branches.
  `max_tools` is 256;
  generated source is capped at 524,288 bytes and the full surface at
  65,536 bytes. Aggregate overruns refuse the server; per-field depth
  fallback keeps the surrounding structure.
- **Source and surface describe the same types.** `internal/typed_codegen`
  derives declarations, options constants, encoders and total output decoders
  from one plan. Optional defaults omit keys so the server applies its own
  defaults. Unknown shapes carry explicit raw-value reasons, and generated
  booleans carry named variants rather than naked `Bool` fields.
- **Structural typing has a stated limit.** Types and the fixed satellite
  codec check represented shapes, enums, string literals and closed objects.
  Numeric bounds, patterns and general schema refinements remain server
  admission checks. Open-record output fields not in the declared projection
  are accepted and omitted from the typed record; raw-value fallback fields
  remain available for caller inspection.
- **Value translation is total.** JSON integers outside `[-2^63, 2^64 - 1]`
  fail the whole MessagePack conversion. Binary values and non-string map
  keys cannot become JSON arguments. No wrapping, clamping or guessed
  encoding is permitted. JSON and MessagePack parser nesting limits remain
  aligned, and the client separately checks the final capability envelope.
- **Loom owns result reduction.** The library retains non-text blocks as
  `Other(kind, raw)`. `client/mcp.content_block` sends only their kind to
  `cap/mcp.Other`; raw image, audio and resource payloads do not enter the
  capability result. Text and structured results retain existing handling.
- **Loom owns lifecycle policy.** `client/mcp` uses the library's parked
  `prepare_owned` clients, publishes every cleanup handle before connecting,
  and requires explicit native exit and normal actor retirement. Moving
  these primitives out of the tree does not weaken that custody boundary.
- **Client identity is caller-owned.** The external library defaults to
  `gleam-mcp`; `client/mcp.start_one` explicitly identifies Loom as `loom`.
  The declared client capabilities remain empty.

## Deep Docs

- [README.md](README.md) explains the extraction boundary and local tests.
- [MCP architecture](../../docs/architecture/mcp.md) follows configuration,
  startup, generation, compilation, calls and retirement end to end.
- [Code-mode architecture](../../docs/architecture/code-mode.md) describes
  import vetting and the jailed build that compiles generated modules.
- [Capability package](../cap/CLAUDE.md) owns the invocation envelope,
  result vocabulary and fixed structural codec.
- [Client package](../client/CLAUDE.md) owns lifecycle wiring and routing.
- [Root CLAUDE.md](../../CLAUDE.md) holds repository rules and the doc graph.