Skip to content

migrate the MCP server to rmcp 3.1 - #34

Merged
OrellBuehler merged 4 commits into
mainfrom
deps/rmcp-3
Aug 23, 2026
Merged

migrate the MCP server to rmcp 3.1#34
OrellBuehler merged 4 commits into
mainfrom
deps/rmcp-3

Conversation

@OrellBuehler

Copy link
Copy Markdown
Owner

Supersedes #26 (dependabot's rmcp 1.7.0 → 3.1.2), which is a lockfile-only bump the
code does not survive: rmcp 3 renamed Content and added inbound Host validation.
This does the migration and pins the parts of the contract that changed silently.

The rename

rmcp::model::ContentContentBlock. The constructors and CallToolResult::success
kept their signatures, so image_result was the only call site. The transport
constructor is unchanged. rmcp 3.1 needs Rust 1.88 against this repo's 1.95 MSRV, so
there is no floor to raise.

KERF_MCP_ADDR would have broken

rmcp 3 validates the inbound Host header against an allow-list that defaults to
loopback (a DNS-rebinding guard). KERF_MCP_ADDR exists precisely to move the endpoint
off loopback, so the stock default would have made every override 403 the clients it
just enabled. allowed_hosts (pure, unit-tested) derives the list from the bind
address: a concrete address joins the loopback defaults; a wildcard bind can't be
enumerated, so it yields the empty list — rmcp's "allow any".

Tool schemas switched generators without a compile error

schemars 0.8 → 1.x means draft 2020-12 with $defs. The output is equivalent, but
nothing failed to build when it changed, so the contract is now asserted: every tool
keeps a description and an object schema, Option<T> params stay out of required
(a great many tools document "omit to append"), and image results still serialize as
bare base64 under data rather than a data: URL. 8 tests, up from 1.

The server was introducing itself as "rmcp"

ServerInfo::default() fills server_info from rmcp's own build env, so clients listed
the endpoint as rmcp 3.1.4 — equally wrong on 1.7, and invisible from the Rust side.
It now names itself kerf + the crate version, with a test.

Verified against a running app

Not only CI: built it, launched under WSLg, and spoke real MCP over HTTP — initialize
handshake, all 78 tools listed, read / mutating / tool-error / staging round-trips.
Re-bound to 10.255.255.254:7788 and confirmed it accepts its own host (200) while a
foreign Host on that same bind still gets 403.

Then drove the GUI itself: imported a real 10 s clip through the file dialog, added it
to the timeline over MCP (the webview picked the edit up live), and pressed play —
playback stream ended playback_id=1 frames=237 first_frame_ms=72 elapsed_ms=9947,
with the preview showing moving footage and the playhead tracking it.

fmt, clippy --all-targets -D warnings, MSRV under a real 1.95 toolchain, and both
test jobs (216 core + 9 app) pass. Lockfile churn is contained to rmcp, sse-stream
and darling.

rmcp 3 renamed `Content` to `ContentBlock`; the constructors and
`CallToolResult::success` are otherwise unchanged, so the visual tools'
`image_result` helper is the only call site that moved.

Two things the rename does not cover:

`rmcp` now validates the inbound `Host` header against an allow-list that
defaults to loopback, to blunt DNS rebinding against local servers. But
`KERF_MCP_ADDR` exists to move the server *off* loopback, so the stock
default would have made every such override reject the clients it just
enabled. `allowed_hosts` derives the list from the bind address instead:
a concrete address joins the loopback defaults, while a wildcard bind
cannot be enumerated — the client's `Host` is whichever of this machine's
addresses it arrived on — so it yields the empty "allow any" list.

Tool schemas now come from schemars 1, which emits draft 2020-12 with
`$defs`. The generated surface is equivalent, but nothing failed to
compile when it changed, so the contract is now pinned by tests: every
tool keeps a description and an object schema, `Option<T>` parameters
stay out of `required` (a great many tools document a field as "omit
to ..."), and an image result still serializes as a caption plus bare
base64 under `data` rather than a `data:` URL.

Verified against a running app: initialize, all 78 tools listed, read /
mutating / error / staging round-trips, and both host-validation paths.
`ServerInfo::default()` fills `server_info` from rmcp's own build env, so
every client listed the endpoint as "rmcp" 3.1.4 — wrong on 1.7 too, and
invisible from the Rust side because nothing about it fails to compile.
Tauri's manifest rides in the .res, which cargo links into bins only. The
lib test binary thus ran with no activation context, bound comctl32 v5, and
died with STATUS_ENTRYPOINT_NOT_FOUND on rfd's TaskDialogIndirect import
before any test ran.
@OrellBuehler

Copy link
Copy Markdown
Owner Author

Force-pushed: dropped a temporary CI diagnostic and added a Windows fix the migration turned up.

build (windows-latest) failed here while passing on main — the kerf-app test exe died with STATUS_ENTRYPOINT_NOT_FOUND before running a test. A loader probe on the runner pinned it to comctl32.dll -> TaskDialogIndirect, which only the v6 side-by-side comctl32 exports. Tauri's app manifest, the thing that asks for v6, rides in the .res cargo links into bins only, so the test binary starts with no activation context and binds v5. rfd (via tauri-plugin-dialog) contributes that import; a dump on an unmodified main showed the exe there has no comctl32 import at all, so whether the linker pulls the object in shifts with unrelated dependency changes — the rmcp bump was simply the change that tipped it.

build.rs now takes the manifest away from Tauri and embeds windows-app-manifest.xml (byte-identical content) through the linker, which covers every binary the crate links, tests included. Windows build is green.

@OrellBuehler
OrellBuehler merged commit 00f7a54 into main Aug 23, 2026
23 of 24 checks passed
@OrellBuehler
OrellBuehler deleted the deps/rmcp-3 branch August 23, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant