Architecture
noyalib-mcp is a Model Context Protocol server that gives an AI
agent lossless YAML editing. It speaks MCP over stdio, streamable
HTTP or the older HTTP+SSE transport, and delegates every YAML
operation to noyalib::cst::Document, so an edit preserves comments,
indentation, and sibling entries byte for byte.
The protocol itself is rmcp, the
official Rust SDK: the framing, the handshake, the sessions and the
protocol revisions are its, and this crate supplies the tools and the
text a model reads. See
ADR 0001 for why.
Layers
src/main.rsis three lines: it hands the library’s handler totransport::run.src/transport.rsis the transport dispatch: the command line (--transport,--host,--port,--version,--help), the stdio session, the streamable HTTP listener at/mcp, and a bridge for the 2024-11-05 HTTP+SSE transport at/sseand/messages/(insrc/transport/sse.rs) that the SDK no longer ships a server for. It depends only on the SDK and on aServerHandlerpassed in — nothing in it knows what the tools are — so it is the same module in every Rust server of the suite.src/lib.rsis the handler:YamlServerholds the tool and prompt routers, describes the server forinitializeandserver/discover, serves the resources, and turns a call for a tool that does not exist into a result the model can read instead of the SDK’s-32602.src/tools.rsis the YAML work.get,set,set_multidoc,parse,editandvalidateare plain functions that know nothing about MCP; the#[tool_router]block below them is whattools/listadvertises andtools/callreaches. Each tool returns its answer twice — as text for the model, and asstructuredContentagainst theoutputSchemait declares.src/resources.rsbacksresources/list,resources/templates/list, andresources/readwith read-only reference material an agent can pin without a tool call: the failure taxonomy, the tool reference, and per-tool descriptors. None of it touches the filesystem or accepts caller paths.src/prompts.rsbacksprompts/listandprompts/getwith a pure text template that teaches the host model the recommendednoyalib_get/noyalib_setworkflow.
Two kinds of failure
A tool that ran and could not do the job — a file that cannot be read,
YAML that does not parse, a path that does not exist, a fragment that
cannot be applied — is a successful JSON-RPC response carrying
isError: true and text the model can act on. A tool the server does
not have is reported the same way, naming the ones that exist: the
stateless HTTP revision carries the SDK’s -32602 as an HTTP 400,
which the client reports as a transport fault and the model never
reads.
A request the protocol rejects — an unknown method, a resource that does not exist, a routing header that disagrees with the body — is a JSON-RPC error, which the client handles.
Trust boundary
Everything the agent sends arrives through the SDK and reaches the six
functions as deserialised arguments. Those functions are the surface
this crate owns, and the stateless three are fuzzed directly
(fuzz_targets/tools.rs); CI replays the seed corpus on every push.
The HTTP listeners exist only when --transport asks for them, bind
the loopback interface unless --host says otherwise, and do not
authenticate: a routable deployment belongs behind a gateway the
operator trusts.
Distribution
server.json and glama.json are the registry manifests; both carry
the version and the ghcr.io image tag, and the release workflow’s
Validate job refuses a tag they disagree with. pkg/npm-wrapper ships
the same binary through npm. The container entrypoint is stdio, which
is what docker run -i gives an agent; the HTTP transports are
reached from it by passing --transport.
Lockstep
The crate pins noyalib at the identical =0.0.X and releases with
it (core ADR-0005).