The noyalib-mcp Manual
A Model Context Protocol server giving AI agents lossless YAML editing. noyalib-mcp is one of the six crates of the noyalib family and
releases in lockstep with the core library at the identical =0.0.X.
This manual is the rendered form of the Markdown in the repository’s
docs/
directory; the repository copy is canonical. The family manual for the
core library, with the user guide, migrations, and policies, is at
https://sebastienrousseau.github.io/noyalib/manual/.
Pages
Install, quick start, and the full surface are in the README. The CHANGELOG is the complete release record.
Agent integration
Wire noyalib-mcp into the AI agent platforms that speak MCP.
Each section is self-contained — find your agent, paste the
config, restart, and the noyalib_get / noyalib_set tools
appear in the agent’s tool palette.
For the full tool surface and JSON Schema, see
tools-reference.md.
Install the binary
cargo install noyalib-mcp
The binary lands in ~/.cargo/bin/noyalib-mcp. Throughout this
document we assume that path is on PATH; if not, substitute the
absolute path in each agent’s config.
Claude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json
(macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"noyalib": {
"command": "noyalib-mcp",
"args": []
}
}
}
Restart Claude Desktop. The noyalib_get and noyalib_set tools
appear under the tool icon in any conversation.
To restrict the tools to a specific working directory:
{
"mcpServers": {
"noyalib": {
"command": "noyalib-mcp",
"args": [],
"env": {
"NOYALIB_MCP_ROOT": "/Users/me/Projects/configs"
}
}
}
}
(The NOYALIB_MCP_ROOT env var is honoured by the server when
present; tool calls that reference paths outside this root
return -32000 “outside permitted root”.)
Cursor
~/.cursor/mcp.json:
{
"mcpServers": {
"noyalib": {
"command": "noyalib-mcp"
}
}
}
Cursor picks up the change without restart on most workspaces. Open the AI sidebar; the noyalib tools are listed.
Continue.dev
~/.continue/config.json:
{
"mcpServers": [
{
"name": "noyalib",
"command": "noyalib-mcp"
}
]
}
Restart Continue. Tools appear in the agent’s tool palette.
Generic MCP client
Any client that speaks MCP over stdio can use (the server is dual-era: 2026-07-28 per-request _meta / server/discover, or the 2025-06-18 initialize handshake shown here):
noyalib-mcp
The server reads JSON-RPC requests from stdin and writes responses to stdout, line-delimited. No flags, no environment variables required for the basic case.
Verifying the server is wired up
In any MCP client, ask the agent: “List your available tools.” The
agent should mention noyalib_get and noyalib_set (or describe
them as “read/write YAML values at paths”).
For a CLI smoke test independent of any agent:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}' \
| noyalib-mcp
Should respond with the server capabilities. Then:
{
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}'
echo '{"jsonrpc":"2.0","method":"notifications/initialized"}'
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
} | noyalib-mcp
Should list both noyalib_get and noyalib_set with their full
input schemas. The full handshake script lives in
crates/noyalib-mcp/examples/handshake.sh.
Common agent prompts
Agents wielding the noyalib MCP tools handle prompts like:
“Update the version field in
Cargo.tomlto 0.0.2.”Agent: calls
noyalib_getwithfile=Cargo.toml, path=version, sees the current value, then callsnoyalib_setwith the new value. The diff ingit diffis one line.
“Read the database connection string from
config/prod.yaml.”Agent:
noyalib_getwithpath=database.connection_string.
“Increment the
replicascount in every*.yamlunderk8s/.”Agent:
Globfor the files, then per-filenoyalib_getto read the current count, thennoyalib_setto write the incremented value. Each file’s comments and indentation survive intact.
Why MCP for YAML editing?
Existing approaches:
sed/yq— text-or-AST replacement that often loses comments, normalises indentation, and breaks anchor referencesEdittool with full file rewrite — agent regenerates the whole file from memory, which scrambles comments, comments drift, and is hard to review- MCP
noyalib_set— surgical edit through the lossless CST. One value changes; everything else is byte-identical
This is the same property that makes noyafmt --fix safe: the
green tree preserves trivia by construction.
Security considerations
- Server has full filesystem access — it can read and write
any path the user running the binary can. For untrusted
environments, use
NOYALIB_MCP_ROOTto confine to a directory. - No authentication on the JSON-RPC channel — MCP runs over stdio; the security boundary is the OS process. Don’t expose the binary over a network without an authenticating proxy.
- Path traversal — the server resolves paths as-given. If client-side path sanitisation matters, sanitise before calling.
For the full security policy see the workspace SECURITY.md.
Troubleshooting
Tools don’t appear in the agent
- Verify the binary works:
noyalib-mcp --version - Verify the agent’s config syntax (most agents log MCP server startup; check the agent’s logs)
- Restart the agent — most clients only re-read the MCP config on launch
Edit fails with “path not found”
noyalib_set requires the parent path to exist. To set
server.host when server doesn’t exist, first set server to
{}, then set server.host. Or pre-populate the file with the
parent structure.
Agent says “no such tool”
Some agents serve a curated tool list and require explicit
allow-listing. Check the agent’s tool-permissions UI; toggle
noyalib_get and noyalib_set on.
Working directory differs from expectation
The server inherits the working directory of its parent process (the agent). Relative paths in tool arguments resolve from there. Use absolute paths when in doubt.
Related
- Tools reference — full input schemas and error codes
- Crate README — install + crate-level overview
crates/noyalib-mcp/examples/— handshake.sh and other diagnostic scripts
MCP tools reference
The complete list of tools noyalib-mcp advertises via
tools/list, with their JSON Schema input shape and example
invocations. This file is the user-facing reference; the
authoritative source is
crates/noyalib-mcp/src/tools.rs::descriptors().
The server is dual-era: it speaks MCP 2026-07-28 (stateless per-request _meta, server/discover) and answers the legacy 2025-06-18 initialize handshake. Tool calls go through
tools/call; see the MCP spec
for the JSON-RPC envelope.
Tool list
The server advertises 2 tools. Every tool delegates to
noyalib::cst::Document so edits round-trip with comments,
indentation, and sibling entries preserved byte-for-byte.
noyalib_get
Read the YAML value at a dotted/indexed path in a file. Returns
the source slice exactly — no re-quoting, no canonicalisation.
Preserves comments and formatting for any later noyalib_set
round-trip.
Input schema:
{
"type": "object",
"properties": {
"file": {
"type": "string",
"description": "Path to the YAML file on disk."
},
"path": {
"type": "string",
"description": "Dotted/indexed path into the YAML, e.g. `server.host` or `items[0].name`."
}
},
"required": ["file", "path"]
}
Example invocation:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "noyalib_get",
"arguments": {
"file": "/etc/myapp/config.yaml",
"path": "server.host"
}
}
}
Example response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{ "type": "text", "text": "api.example.com" }
]
}
}
Error codes:
| Code | Meaning |
|---|---|
-32000 | I/O error reading the file |
-32001 | Parse error in the YAML |
-32002 | Path not found in the document |
-32602 | Missing or non-string argument |
noyalib_set
Set the YAML value at a dotted/indexed path in a file. Only the touched span is rewritten — every comment, blank line, and sibling entry is preserved byte-for-byte. Useful for Renovate-style version bumps and config patches by AI agents.
Input schema:
{
"type": "object",
"properties": {
"file": {
"type": "string",
"description": "Path to the YAML file on disk."
},
"path": {
"type": "string",
"description": "Dotted/indexed path into the YAML."
},
"value": {
"type": "string",
"description": "Replacement value as a YAML fragment (e.g. `0.0.2`, `\"hello\"`, `[1, 2, 3]`). Must parse in the target position; the document is left unchanged on parse error."
}
},
"required": ["file", "path", "value"]
}
Example invocation:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "noyalib_set",
"arguments": {
"file": "/etc/myapp/config.yaml",
"path": "server.port",
"value": "9090"
}
}
}
Example response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{ "type": "text", "text": "set server.port = 9090 in /etc/myapp/config.yaml (lossless: comments and formatting preserved)" }
]
}
}
Error codes:
| Code | Meaning |
|---|---|
-32000 | I/O error reading or writing the file |
-32001 | Parse error in the source YAML |
-32003 | The new value cannot be applied at the path (parse error in the fragment, or path doesn’t exist) |
-32602 | Missing or non-string argument |
Lossless edit guarantee
Both tools route through noyalib::cst::Document, so the on-disk
file after a noyalib_set:
- Has the same comments at the same positions
- Has the same blank lines and indentation
- Has the same anchor / alias relationships
- Has the same key order
- Has the new value at the requested path
The only bytes that change are the bytes for the value itself.
Empirically: git diff after a single noyalib_set shows a 1-line
change. This is the contract.
Path syntax
The path argument follows the same syntax as
noyalib::Value::get_path and noyalib::cst::Document::get:
| Path | Selects |
|---|---|
name | Top-level key name |
server.host | host inside the mapping at server |
items[0] | First element of the sequence at items |
items[0].name | name inside the first element |
services.api.replicas | Deeply nested |
Path-not-found semantics
noyalib_get returns error -32002 if the path does not exist.
noyalib_set will create missing intermediate keys at the same
indent level when possible; if the parent path does not exist or
points to an incompatible shape (e.g. server.host[0] when
server.host is a string), it returns -32003.
Concurrency
The server processes one request at a time per stdio session. Concurrent edits to the same file from multiple sessions are not serialised by the server — clients should coordinate externally (e.g. via flock) if multiple agents may edit the same file.
Capabilities advertised at initialize
{
"protocolVersion": "2025-06-18",
"serverInfo": {
"name": "noyalib-mcp",
"version": "0.0.1"
},
"capabilities": {
"tools": {}
}
}
The server does not advertise prompts or resources capabilities
— it’s tool-only.
Related
- Agent integration — Claude Desktop, Cursor, and other client setup
- Crate README — install + crate-level overview
MCP delivery comparison
| Delivery surface | Lossless YAML edits | Remote transport | Typed tool results |
|---|---|---|---|
noyalib-mcp | Yes | Streamable HTTP and SSE | Yes |
noya-cli | Yes | No | Not applicable |
Native noyalib | Yes | No | Rust types |
noyalib-mcp is the protocol adapter when an MCP-aware agent must reach the
same parser and lossless editor used by the native tools. It is not a general
MCP gateway or an authentication service. Compare MCP servers by tool contract,
transport, trust boundary, protocol revision, and deployment model rather than
by tool count alone.
Benchmarks
The mcp_tools Criterion harness measures representative tool operations.
cargo bench --bench mcp_tools
Transport tests cover protocol correctness separately from the microbenchmark. Record the CPU, operating system, Rust version, commit, input size, transport, and command with any result. CI does not use shared-runner latency as a pass/fail threshold.
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).
Engineering policies
Version and dependency policy
noyalib-mcp releases in strict lockstep with noyalib. Version 0.0.58 pins
the core at exactly =0.0.58. Release work uses feat/v0.0.58; each subsequent
iteration increments exactly 0.0.1.
Minimum Rust version
The minimum supported Rust version is 1.89.0, the floor shared across the noyalib family; the MCP SDK stack alone requires 1.88.0. The manifest declares it and CI verifies it. A future increase is a breaking-axis change and requires a changelog entry.
Protocol and transport stability
Tool names, input and output schemas, prompts, resources, transport routes, header behaviour, and advertised protocol revisions are public compatibility surfaces. Bind network transports to loopback by default. Authentication and authorization belong at the deployment boundary.
During 0.0.x, the patch component is the breaking axis. Family-wide policies
live in the core
POLICIES.md.
Repository standard compliance grade
Assessment date: 2026-09-20. Source rubric:
/Users/seb/Code/REPO-STANDARD.md. This is a conservative static assessment of
checked-in evidence, not a substitute for successful CI runs.
Result
15/24 signals (62%). README template: pass. Strict candidate tier: L1.
| Category | Signals | Summary |
|---|---|---|
| Identity and README | 3/3 | Canonical structure and MCP-specific policy present |
| Documentation | 2/3 | Manual, architecture, and ADR evidence present |
| Build and install UX | 1/3 | Native build present; generated completions/manpage signals are absent |
| Releases and binaries | 2/3 | Automated releases present; complete L3 evidence remains open |
| Packaging | 2/3 | Container distribution present; Repology/reproducibility are incomplete |
| CI quality gates | 2/3 | Matrix and coverage signals present |
| Supply chain | 2/3 | Foundation controls present; advisory-audit detection remains open |
| Community | 1/3 | Foundation governance present; L2 template and docs-lint signals are open |
The cumulative tier remains L1. Priority work is generated CLI assets, distribution tracking and reproducibility, explicit advisory-audit CI, and audit-visible issue/PR templates and markdown lint enforcement.
Detailed README reference
noyalib-mcp
Model Context Protocol server exposing noyalib's lossless YAML editing to AI agents (Claude Desktop, Claude Code, Cursor, Zed, Continue.dev, …).
Contents
- Install — Cargo, npx, Docker
- Requirements — toolchain floor, platforms, the core pin
- Quick Start — JSON-RPC handshake
- Why this approach? — design rationale
- Connect — per-client configuration
- Tools — MCP tool reference
- Examples — runnable scripts
- Verification — cosign + npm provenance
- When not to use noyalib-mcp
- Documentation
- License
Install
cargo install noyalib-mcp
For environments without a Rust toolchain (the typical AI-agent deployment shape):
# npm wrapper: downloads the matching binary on first run, checks it
# against the SHA-256 in the package, caches it under
# ~/.cache/noyalib-mcp/<version>/. It refuses to run until releases
# publish per-platform archives and their digests.
npx @sebastienrousseau/noyalib-mcp
# Container — multi-arch (linux/amd64, linux/arm64).
docker run --rm -i ghcr.io/sebastienrousseau/noyalib-mcp:latest
Split from the monorepo since v0.0.13. Prior versions shipped from
sebastienrousseau/noyalib/crates/noyalib-mcp/under the workspace-lockstep release cadence. From v0.0.13 onwardnoyalib-mcplives here as its own crate, still released in strict lockstep with the parentnoyalibat the same version. See ADR-0005 for the rationale and rollback recipe.
Both consume the same signed binary attached to every GitHub Release. See Verification for the verify commands.
Requirements
- Rust 1.89.0 or newer to build from source:
rust-versionin the manifest, enforced by themsrv-coreCI job on every push. - Any tier-1 platform. CI runs the tests on Linux, macOS, and Windows with the stable, beta, and nightly toolchains; stable is the gate, beta and nightly are early warning.
- The matching core. This crate pins
noyalibat the identical=0.0.Xand releases in lockstep with it; Cargo resolves that pin for you. - An MCP client speaking JSON-RPC 2.0 over stdio (2025-06-18 or 2026-07-28 protocol eras); the README’s Connect section lists tested hosts.
Quick Start
The server speaks JSON-RPC 2.0 over stdio with newline-delimited
frames, per the
MCP specification. A typical
agent launches the binary as a child process, sends
initialize, then dispatches tool calls:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"agent","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"format","arguments":{"yaml":"a:1\nb:2\n"}}}
Why this approach?
AI agents that edit YAML configuration today regex-replace and corrupt comments, indentation, and document structure. The same agent fixing a port number in a Kubernetes manifest can shift every comment by a line, reorder sibling keys, or strip trailing whitespace that a downstream linter cared about.
noyalib’s CST does the edits losslessly — a set("server.port", "9090") rewrites only the byte span of the 8080 scalar; the
surrounding comments and indentation pass through untouched.
This server is the protocol shim that lets MCP-aware clients
drive that engine safely:
- Lossless mutation.
tools/call setreturns a document byte-identical to the input outside the touched span. - Surgical reads.
tools/call getwalks the dotted path and returns just the value, not the whole tree. - Schema validation.
tools/call validate --schemaruns the same JSON Schema 2020-12 enginenoyavalidateships. - Stdio transport. Standard MCP. Works with every spec-compliant client.
Connect
Claude Desktop / Claude Code
claude mcp add noyalib $(which noyalib-mcp)
Cursor
~/.cursor/mcp.json:
{
"mcpServers": {
"noyalib": {
"command": "noyalib-mcp"
}
}
}
Zed
~/.config/zed/settings.json:
{
"context_servers": {
"noyalib": {
"command": { "path": "noyalib-mcp" }
}
}
}
Continue.dev
~/.continue/config.json:
{
"experimental": {
"modelContextProtocolServers": [
{ "transport": { "type": "stdio", "command": "noyalib-mcp" } }
]
}
}
Any other MCP-aware client
Point at the binary; the transport is stdio with newline- delimited JSON-RPC 2.0.
Tools
The v0.0.1 server registers two file-oriented tools — both
operate on a YAML file at file: <path>, not on inline source
strings, so an agent’s edits land on disk losslessly:
noyalib_get— Takes{ file: string, path: string }; returns the raw source fragment at the dotted/indexed path (e.g.server.host,items[0].name). No re-quoting; no canonicalisation.noyalib_set— Takes{ file: string, path: string, value: string }; returns the file rewritten via the lossless CST so only the touched span changes; comments, blank lines, and sibling formatting survive byte-for-byte. Thevalueis a YAML fragment (0.0.2,"hello",[1, 2, 3]); a parse failure leaves the file unchanged.noyalib_parse— Takes{ yaml: string }; returns the JSON data model of the text (tags stripped, a stream as an array). Stateless: nothing on disk is touched.noyalib_edit— Takes{ yaml: string, path: string, value: string }; returns the whole text with that one value replaced losslessly. Stateless.noyalib_validate— Takes{ yaml: string, schema?: string }; returnsvalidwith either the parse error (line and column) or every JSON Schema violation with its path. Stateless.
Each tool’s full input schema lives in the response to
tools/list. The server also handles the standard
initialize / initialized / notifications/cancelled
lifecycle.
Command-line workflows are available through the
noya-cli binaries
(noyafmt, noyavalidate). Lower-level embedding uses the
noyalib library API.
Examples
Agent-driving demos under
examples/:
| Script | What it shows |
|---|---|
handshake.sh | initialize → tools/list smoke test. Confirms the binary speaks the protocol and announces the expected tools. |
format-call.sh | tools/call format on a poorly-spaced document. Demonstrates that comments + indentation pass through the CST formatter unchanged. |
set-then-get.sh | Round-trip the mutation surface: set rewrites server.port, get reads it back. Surgical edit; surrounding bytes untouched. |
chmod +x crates/noyalib-mcp/examples/*.sh
crates/noyalib-mcp/examples/handshake.sh | jq -c .
POSIX-shell only — no jq, no node dependencies. Pipe
through jq -c . if you want pretty-printed JSON responses.
Verification
GitHub Releases ship the crate archive and a CycloneDX SBOM, each with a sigstore bundle and checksums; the GHCR image is built from the tagged source. Pre-built binaries are not attached to releases yet. To verify a release artefact:
COSIGN_EXPERIMENTAL=1 cosign verify-blob \
--certificate-identity-regexp '^https://github\.com/sebastienrousseau/noyalib-mcp/\.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
--bundle <artefact>.bundle \
<artefact>
The npm wrapper additionally carries an npm provenance attestation:
npm view noyalib-mcp provenance
Full cookbook: pkg/VERIFY.md.
When not to use noyalib-mcp
- You don’t trust your AI agent with filesystem access at all. noyalib-mcp doesn’t read or write files itself — every operation takes the YAML document as a string argument and returns the result as a string. The agent decides what to do with the result. If the agent has filesystem access, it can persist the response wherever it wants.
- You need a sandboxed schema registry. noyalib-mcp accepts
schemas as inline strings in
tools/call validate; it does not fetch schemas from URLs. If your workflow needs network-resolved schemas, the agent is responsible for fetching the schema first and passing the bytes.
Compatibility
MSRV: Rust 1.89.0 stable, the floor shared across the noyalib family and the
maintainer’s other Rust crates (noyalib ADR-0014). The MCP SDK alone needs 1.88.0. The MCP wire surface
itself is text-only JSON-RPC and pulls no nightly-only dependencies.
CI verifies the floor on every PR via the msrv-core workflow job.
The bump policy lives in
docs/POLICIES.md.
Tier-1 platforms (CI-verified each PR): aarch64-apple-darwin,
x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc. The
binary writes via atomic file replacement on every platform —
on Windows via MoveFileExW(MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) semantics.
Documentation
The four entry points, identical across every repo in the family:
-
User Manual — this crate’s rendered book: its guides, architecture, and release notes; the family manual for the core library is at https://sebastienrousseau.github.io/noyalib/manual/
-
API reference — rustdoc on docs.rs
-
Developer docs — this repo’s dev entry point, pointing at the family guide
-
Ecosystem map — the six crates, the lockstep model, the scorecard
-
Engineering policies (MSRV, SemVer, security, performance, concurrency, platform support, feature flags):
docs/POLICIES.md -
Security policy:
SECURITY.md -
API reference: https://docs.rs/noyalib-mcp
-
Tools reference (input schemas + error codes):
docs/tools-reference.md -
Agent integration (Claude Desktop, Cursor, Continue.dev):
docs/agent-integration.md -
MCP specification: https://modelcontextprotocol.io
-
Workspace README: https://github.com/sebastienrousseau/noyalib#readme
Related MCP Servers
Sibling MCP servers by the same author — open-source, Apache-2.0 licensed, targeting banking and financial-services AI agents. noyalib-mcp complements them by giving agents lossless YAML editing for structured configuration files:
| Server | Purpose |
|---|---|
pain001-mcp | Generate & validate ISO 20022 pain.001 payment initiation files (Customer Credit Transfer) |
bankstatementparser-mcp | Parse bank statements (BAI2, MT940/MT942, CAMT.053, OFX, CSV) into structured transactions |
camt053-mcp | Parse & reconcile ISO 20022 camt.053 bank-to-customer statements — CBPR+/HVPS+ ready |
acmt001-mcp | Generate & validate ISO 20022 acmt.001 account management messages |
MCP Registry
mcp-name: io.github.sebastienrousseau/noyalib-mcp
Conformance
Every push runs the official yaml-test-suite
through this server’s tools, from the same vendored suite and the same core
commit as the noyalib core: 195 of 195 addressable cases (the other 211 have
no top-level key for noyalib_get to read; noyalib_parse sees all of them).
A two-document configuration that uses most of YAML at once
(tests/fixtures/ultra-complex/) parses to exactly its expected JSON through
noyalib_parse. Details and the family table:
noyalib.com/conformance.
License
Dual-licensed under Apache 2.0 or MIT, at your option.
Architecture decision records
One file per decision that shaped this server: context, options,
decision, consequences. Change a decision by adding a new record that
supersedes the old one. Decisions that shape the whole family — the
lockstep release model, the workspace split — live in the core
repository’s docs/adr/.
0001. Serve stdio, streamable HTTP and SSE from one command line
- Status: Accepted
- Date: 2026-09-19
- Deciders: maintainer
Context
Until v0.0.45 the server was a hand-written JSON-RPC loop over stdio,
speaking two revisions it implemented itself: the 2025-06-18
initialize handshake and a dual-era reading of 2026-07-28
(per-request _meta, server/discover). A client spawned the
process; nothing listened. That fits a developer’s laptop. It does not
fit a shared deployment, a gateway that fans one server out to many
agents, or an auditor that speaks HTTP.
Keeping up with the specification by hand is a protocol project, not a
YAML one — and the hand-written loop had already drifted: it answered
initialize with 2025-06-18 while the current handshake revision is
2025-11-25, it had no session layer, and it could not have served the
mirrored routing headers (SEP-2243) that 2026-07-28 requires over
HTTP. The official Rust SDK, rmcp, implements all of it, is
maintained by the people who write the specification, and is what the
sibling Rust servers of the suite are moving to.
Options considered
- Stay on stdio and the hand-written loop, and leave remote use to a wrapper process.
- Extend the hand-written loop with HTTP, sessions and both current revisions.
- Adopt
rmcpfor the protocol, keep the six tools as plain functions, and put the transport dispatch in one module copied verbatim into every Rust server of the suite.
Decision
Option 3. noyalib-mcp runs stdio; noyalib-mcp --transport streamable-http listens on --host/--port at /mcp and speaks
both current protocol revisions on that one endpoint, streaming
responses as server-sent events and offering the server-to-client
stream on GET; noyalib-mcp --transport sse serves the older
HTTP+SSE transport at /sse and /messages/. The SDK has no server
side for that last transport, so src/transport.rs carries a small
bridge: one event stream per session, a POST endpoint that feeds it,
and the SDK’s service running over an in-memory pair of channels.
The module binds loopback unless told otherwise and adds no
authentication of its own: a routable deployment sits behind a gateway
the operator trusts. It depends only on the SDK and on a
ServerHandler passed in, so the same file serves oxml-mcp and
rlg-mcp unchanged. It is the same code here, re-wrapped by this
repository’s rustfmt settings — oxml-mcp pins max_width = 80 and
this crate pins rustfmt’s defaults, so a byte-identical copy would
fail cargo fmt --check.
The tools keep their names, argument names and descriptions. Each now
also declares an outputSchema and returns its answer as structured
content beside the text, carries behaviour annotations, and gives
every argument an example so a client that has no document of its own
can still generate a well-formed call. A tool that ran and failed is
an isError result the model can read; so is an unknown tool, because
the stateless revision carries the SDK’s -32602 as an HTTP 400 that
no model ever sees. A request the protocol rejects is still a JSON-RPC
error.
Consequences
main() delegates to transport::run, which is the same file in
every server, so the suite is started, documented and tested the same
way. The server is verified over streamable HTTP with an independent
MCP auditor in both protocol eras and over SSE with the Python SDK’s
client before release.
The crate is no longer close to dependency-free: the SDK brings
tokio, axum, schemars and uuid, and the MSRV moves to 1.88.
The security posture changes shape rather than degrading: the tools
still touch only the files a caller names, and the listeners exist
only when asked for on the command line.
The failure taxonomy moves from JSON-RPC codes to result text. A
client that switched on -32000/-32001/-32002/-32003 now reads
isError and the message, which is what a model was always shown.
That is a wire-visible change inside a 0.0.x line, taken deliberately:
the codes were this server’s invention, the results are the
specification’s.
A stdio client must now perform the handshake — or, in the stateless
revision, name its protocol version in _meta — before its first
request. A bare tools/call as the first line, which the old loop
tolerated, is refused.
noyalib-mcp v0.0.17 Release Notes
Lockstep release with noyalib v0.0.17 (ADR-0005: publishes =0.0.17
pinned to the core).
What changed
- No crate-content change beyond the lockstep bump.
noyalibpin=0.0.16->=0.0.17; crate version bumped in lockstep.
Repository hardening (CI/docs only)
Coverage, MSRV, CodeQL, and OpenSSF Scorecard gates plus upstream cargo-vet audit imports were added across the satellites in this cycle; branch protection now makes commit signing unskippable. No user-facing change.
Upgrading
Requires Rust 1.86.0+.
noyalib-mcp v0.0.16 Release Notes
Lockstep release with noyalib v0.0.16 (ADR-0005 strict-lockstep: the
MCP server always publishes =X.Y.Z pinned to the core). No wire-format
or tool-inventory change to the MCP surface.
What changed
Security
crossbeam-epochbumped to 0.9.20 (RUSTSEC-2026-0204). Closes an invalid-pointer-dereference advisory in thefmt::Pointerimpl forAtomic/Shared, previously locked at a vulnerable version through a transitive dependency.
Dependencies
noyalibpin=0.0.15→=0.0.16and the crate version bumped in lockstep.- MSRV raised 1.85.0 → 1.86.0, matching the single lockstep floor the core adopted in v0.0.16. The crate builds cleanly on 1.86.
Docs / registry manifests
- README Tools section reformatted as a bullet list.
server.jsonandglama.json(MCP-registry manifests) aligned to0.0.16— version field and theghcr.io/...:0.0.16image tags. The releaseValidatejob enforces that these match the tag.
Engineering / CI (no user-facing change)
- Signed-history enforcement. Commits above the last release were re-signed and the branch-protection ruleset split so signatures are required with no bypass, while signed release pushes still work.
- Supply-chain hardening. Imported the upstream audit sets the core
uses (mozilla, google, et al.), so most dependency bumps no longer
churn
cargo-vet; adependabot-vetworkflow auto-refreshes the remaining exemptions. - New CI gates brought to parity with the core: a coverage gate (measured 95.7 % regions / 98.3 % functions / 94.4 % lines, enforced against the 93/96/94 floor), an MSRV gate verifying the 1.86 build, CodeQL, and OpenSSF Scorecard.
What did not change
- The MCP tool inventory, JSON-RPC wire format, and stdio transport.
#![forbid(unsafe_code)]— intact.- Public API of the
noyalib_mcplibrary.
Upgrading
Drop-in for most users:
noyalib-mcp = "0.0.16"
You must be on Rust 1.86.0+ (or stay on v0.0.15). The container image
is ghcr.io/sebastienrousseau/noyalib-mcp:0.0.16.