Repository navigation
feat: generate tool handlers (validate, pbjson decode, tonic call, pbjson encode) (#7) - #25
Conversation
Replace the "not implemented" stub handler body with the real one:
validate tool call arguments against the tool's input schema (compiled
once per tool into a package-level `jsonschema::Validator`, next to the
existing input-schema constant), decode them with the pbjson-generated
`Deserialize`, call the tonic client, and encode the response with the
pbjson-generated `Serialize`. A `None` arguments value is treated as `{}`.
Schema validation failures and decode failures become tool errors without
calling the backend; a `tonic::Status` becomes a tool error formatted like
grpc-go's `Status.Error()` (`rpc error: code = NotFound desc = ...`).
The generated server's `T` type parameter now carries the
`tonic::client::GrpcService<tonic::body::Body>` bounds the tonic-generated
client's own `unary` method needs, instead of just `Clone + Send + Sync +
'static`.
Regenerate the committed example (`make generate`, protoc 29.3) and update
the golden file to match.
register_default_tools's doc comment still said "with its stub handler" after the handler bodies became real; update it, regenerate the committed example, and refresh server_integration.rs's module doc comment to also describe the #7 handler-body coverage added in this PR.
|
🔍 Review: this matches the verified design. Mutation check: disabling the schema-validation branch in the generated example makes
|
Addresses erikist's review on PR #25: - examples/Cargo.toml: add rmcp with "client"/"transport-io" dev-deps so `cargo test -p examples` compiles standalone (it previously relied on feature unification with protoc-gen-rust-mcp's dev-deps under `cargo test --workspace`). - examples/tests/handler.rs: add non_finite_floats_in_result_are_encoded_as_json_null, pinning the pbjson-vs-protojson NaN/Infinity result divergence documented in server.rs (and implement FakeVibeService::set_vibe_array to actually echo its request, so this test has a backend to call). - examples/tests/handler.rs: add enum_as_number_is_rejected_by_schema_without_calling_backend, the one case that isolates schema validation from pbjson's own decode (which would accept an enum given as its numeric value). - protoc-gen-rust-mcp/src/server.rs: note in the module doc comment that the NaN/Infinity divergence is now pinned by a test, not just documented.
|
🤖 Factory (dispatched 2026-10-10T01:24:36Z): Addressed all three points from the review comment (#25 (comment)):
Also added one sentence to Commit: Checks run (all clean): Self-review: ran the No open questions beyond that. All three requested changes are done and verified; no other feedback was open (failing checks: none). |
Closes #7
Summary
Replaces the "not implemented" stub tool handler body (left by #6) with
the real one, the counterpart of Go's
generateHandler(
cmd/protoc-gen-go-mcp/mcp.goat9e072f9) plus the argument validationGo's
mcp.AddTooldoes and the official Rust MCP SDK (rmcp) does not.For each unary RPC's
call_<method>associated function:Nonetreated as{})against a
jsonschema::Validatorcompiled once per tool into a newpackage-level
static <SERVICE>_<METHOD>_VALIDATOR: LazyLock, next tothe existing
..._INPUT_SCHEMAconstant it's built from. On failure,returns a tool error listing every failing instance path and reason
(
"invalid arguments: /vibe: 5 is not of type \"string\"; ..."), andthe backend is not called.
serde_json::from_value::<Request>(thepbjson-generated
Deserialize). A decode failure also becomes a toolerror without calling the backend.
client.<method>(tonic::Request::new(request)),on a cloned client as before). A
tonic::Statusbecomes a tool errorformatted like grpc-go's
Status.Error():"rpc error: code = NotFound desc = vibe not found"—tonic::Code'sDebugimpl (tonic 0.14.6) happens to render exactlygrpc-go's CamelCase code names, so no separate name table was needed.
serde_json::to_string(thepbjson-generated
Serialize) and returnCallToolResult::success(vec![Content::text(json)]).Generated API change:
T's trait boundsThe generated
<Service>McpServer<T>'simpl<T>block and itsrmcp::ServerHandlerimpl now boundTwith exactly what thetonic-generated client's own
unarymethod needs to be callable(
tonic::client::GrpcService<tonic::body::Body>plus the associated-typebounds on
Error/ResponseBody/Future), instead of just `Clone + Send. This is a public API change to every generatedMcpServer(it now requires a real gRPC-capableT, matching what the sibling tonic client already required); verified by compiling and running the generated handler against a real in-process tonic server (tonic::transport::Channelcontinues to satisfy these bounds, as doesInterceptedService`).pbjson vs. protojson difference (result encoding only)
Flagged on #18's review: pbjson serializes a non-finite float/double as
JSON
null, where protojson emits the string"NaN"/"Infinity"/"-Infinity". This only affects tool results;both pbjson's and protojson's generated deserializers accept all three
strings on input (pinned by
schema.rs's existingschema_validates_non_finite_floatstest and this PR'snon_finite_floats_are_accepted_as_input). Documented inserver.rs's module doc comment. Making results match Go is anice-to-have per that review comment, not required by this issue.
Regenerated example
make generate(protoc 29.3, protoc-gen-prost 0.5.0, protoc-gen-tonic0.5.0, protoc-gen-prost-serde 0.4.0, all from the factory image) against
examples/protos/example.proto; the golden test(
protoc-gen-rust-mcp/tests/golden.rs) passes against the regenerated,committed
examples.v1.mcp.rs.Dependency added
jsonschema = { version = "0.58", default-features = false }(sameversion
protoc-gen-rust-mcpalready uses as a dev-dependency for its ownschema tests) is now a runtime dependency of
examples(thegenerated code calls it), checked via
cargo info jsonschemaagainst thelatest 0.58.x release.
Decisions made
LazyLock<jsonschema::Validator>per tool, built from the schema constant already emitted for that tool
(
VALIDATORnext toINPUT_SCHEMA), rather than a shared validatorkeyed by tool name at runtime — keeps the per-tool cost at first use
and needs no runtime lookup.
(
"invalid arguments: ...","decoding arguments: ...","encoding response: ...") isn't specified by Go beyond the gRPC-errorformat, which is pinned exactly; these three are this generator's own
wording, pinned by tests.
register_tool's requirement thatTsatisfy the new clientbounds: no change needed there —
register_tool/register_default_toolsalready live inside the
impl<T> ... where <bounds>block, so thisflows through automatically.
Test evidence
Commands run in this worktree (protoc 29.3, tools from the factory
image):
All four
AGENTS.mdchecks pass clean (no warnings, no diff aftercargo fmt --all --check).cargo test --workspace: 49 + 51 + ... allgreen, including:
protoc-gen-rust-mcp'stests/golden.rs(
generated_output_matches_committed_golden_file).protoc-gen-rust-mcp'stests/server_integration.rs: 12 tests,including 5 new ones exercising the real handler body against a real
in-process tonic server built from
tests/testdata/mcpgen/fixture.proto(success, gRPC
NotFound→ tool error text, wrong-type/unknown-fieldrejected without calling the backend, missing-arguments-as-
{}), plusupdated versions of the pre-existing override/composition tests that
used to assert "not implemented" stub text.
examples' newtests/handler.rs: 6 tests against the real, committedexample
VibeServiceserver and a real in-process tonic backend,covering every bullet in the issue's acceptance criteria list
(protojson-matching success JSON, gRPC
NotFoundin grpc-go format,wrong argument type, unknown field including the snake_case
previous_vibeform, int64 as both number and string, enum by name,and
NaN/Infinity/-Infinityfloats as input).protoc-gen-rust-mcp'ssrc/server.rsunit tests for the newrust_message_pathhelper (top-level and nested-message cases; noexisting fixture proto has a nested message as an RPC's request/response
type, so this needed a hand-built
FileDescriptorProto).Self-review: ran the
reviewersubagent once; it found two stale"stub handler" doc-comment references (one of which had leaked into the
committed generated file) and a module-doc comment that hadn't been
updated to mention this issue's own test coverage. Both fixed in a
follow-up commit, and the golden file/example were regenerated again
afterward. No blocking or in-scope findings remained.
Notes for reviewers
The widened
Tbounds (see above) are the one generated-API-visiblechange beyond the handler bodies themselves; flagging per AGENTS.md's
"document any change to [generated code's runtime dependencies]" rule.
protoc-gen-rust-mcp/build.rs's fixture compilation now also runspbjson-buildfortests/testdata/mcpgen/fixture.proto(it previouslyonly ran
tonic-prost-build), since the generated handler body needsSerialize/Deserializeimpls for the fixture's own types too, notjust
examples/'s committed ones.protoc-gen-rust-mcp's dev-dependency ontonicgained theserverand
routerfeatures (forServer::builder()/add_service/TcpIncoming), soserver_integration.rscould run a real in-processgRPC server instead of asserting against a never-connecting client.
examples' dev-dependency ontonicgainedrouterfor the samereason in
handler.rs.No out-of-scope follow-ups identified.
Factory-authored