Repository navigation
feat: protojson-compatible google.protobuf.FieldMask in tool args, results and schemas (#20) - #29
Merged
erikist merged 3 commits intoOct 10, 2026
Conversation
added 3 commits
October 10, 2026 03:22
FieldMask's schema now matches Go's protojson-compatible encoding
("type": "string") instead of pbjson's raw {"paths": [...]}
shape. server.rs's generated per-message JSON rewrites (next commit)
make the actual argument/result encoding match this schema.
For a request or response message that transitively contains a
google.protobuf.FieldMask field (through singular, repeated or
map-valued message fields, including recursive messages), the
generator now emits a memoized <service>_fm_in_<message>/
<service>_fm_out_<message> function and wires it into the method's
handler: fm_in runs on the raw JSON arguments after schema validation
and before the pbjson decode, fm_out runs on the pbjson-encoded
response before it is stringified.
Two shared per-service helpers (<service>_fm_str_to_obj/
<service>_fm_obj_to_str) convert between protojson's comma-joined
lowerCamel path string and pbjson's own {"paths": [...]} shape,
built on two small lowerCamel<->snake_case path-segment converters
matching protojson's strs.JSONSnakeCase/JSONCamelCase rules.
Methods whose request and response have no FieldMask field anywhere
(every method in example.proto) generate exactly the same handler
body as before: the golden file and example.proto's parity fixture
are unchanged.
…tes (#20) Add an UpdateVibe RPC to test-fixtures' mcpgen fixture with a top-level FieldMask, one nested inside a message field, and a repeated FieldMask, plus a response that echoes all three back, to exercise the generator's new fm_in/fm_out wiring end to end over a real in-process tonic backend and MCP transport: - the tool's input schema maps every FieldMask field to {"type": "string"}, matching Go - {"updateMask": "vibe,someField"} decodes into paths ["vibe", "some_field"]; the nested and repeated masks convert the same way - a response FieldMask comes back as a comma-joined lowerCamel string - an empty mask string round-trips to "", not an empty object - a non-string updateMask is rejected by schema validation before the backend is ever called Also add protoc-gen-rust-mcp/src/field_mask.rs unit tests for a map<string, FieldMask> field (needs_rewrite and the generated iterate-values rewrite body), rounding out singular, repeated and map field coverage. make generate and the golden/parity tests are confirmed unchanged for example.proto, which has no FieldMask field.
This was referenced Oct 10, 2026
Contributor
Author
|
🔍 Review: LGTM. It follows the design, leaves |
erikist
deleted the
factory/20-feat-protojson-compatible-google-protobuf-fieldmask
branch
October 10, 2026 04:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #20
Summary
Makes
google.protobuf.FieldMaskprotojson-compatible in tool arguments, results and schemas, matching Go'sprotoc-gen-go-mcpinstead of pbjson's raw wire shape ({"paths": [...]}).schema.rs):google.protobuf.FieldMasknow maps to{"type": "string"}, exactly like Go, instead of the plain-message{"type":"object","properties":{"paths":...}}shape pbjson's own serde would otherwise imply.server.rs+ newfield_mask.rs): for each RPC's request/response message that transitively contains a FieldMask field (through singular, repeated or map-valued message fields, including recursive messages), the generator emits a memoized<service>_fm_in_<message>/<service>_fm_out_<message>function, keyed by each field'sjson_name. Two shared per-service helpers (<service>_fm_str_to_obj/<service>_fm_obj_to_str) do the actual conversion between protojson's comma-joined lowerCamel string and pbjson's{"paths": [...]}shape, built on two small path-segment case converters matching protojson'sstrs.JSONSnakeCase/JSONCamelCaserules.string) →fm_inon the raw arguments → pbjson decode → tonic call →serde_json::to_value→fm_out→to_string. A method whose request and response have no FieldMask anywhere generates exactly the same handler body as before (no extra branch, nolet mut):example.protohas no FieldMask field, so the golden file and its parity fixture are byte-for-byte unchanged.UpdateVibeRPC totest-fixtures/testdata/mcpgen/fixture.protowith a top-level FieldMask, one nested inside a message field, and a repeated FieldMask in the request, and a response that echoes all three back — exercised end-to-end over a real in-process tonic backend and MCP transport inprotoc-gen-rust-mcp/tests/server_integration.rs. Also unit tests infield_mask.rsfor the transitive-containment detection (direct, nested, map-valued, and purely-cyclic-without-FieldMask cases) and the generated rewrite bodies.Design decisions
Followed the approach a maintainer outlined directly in a comment on #20 (option (a) from the issue: the generator knows statically where FieldMask fields are, emits small JSON rewrites, no
extern_path/build-setup burden on users). Two decisions beyond that comment, both confirmed by the end-to-end tests:updateMask: "") round-trips through pbjson as{}(proto3 message-field presence means it isn't omitted the way an absent field would be);fm_outconverts that{}to""rather than leaving it as an empty object, matching what Go's protojson would also produce for a zero-path FieldMask...._INPUT_SCHEMA/..._VALIDATORconstants already avoid cross-service name collisions in oneinclude!d file.README
Issue #20 asked for a "Differences from protoc-gen-go-mcp" section (or to tell #13, the dedicated README-parity issue, what goes in it). #13 is still open and owns the whole README rewrite, so I left a note there (see the comment I'm about to leave on #13) instead of writing a partial section here that #13 would have to reconcile.
Test evidence
Ran exactly the checks
AGENTS.mdspecifies, from the repo root:cargo fmt --all --check cargo clippy --workspace --all-targets -- -D warnings cargo build --workspace cargo test --workspaceAll pass:
cargo fmt --all --checkis silent (no diff),cargo clippyreports no warnings,cargo build --workspacesucceeds, andcargo test --workspacepasses all suites, including:protoc-gen-rust-mcp'sgolden.rs(committed golden file unchanged) andexample_proto_descriptions.rs,protoc-gen-rust-mcp's newfield_maskunit tests (10 tests) and updatedschematests,protoc-gen-rust-mcp/tests/server_integration.rs's newUpdateVibetests (schema shape, top-level/nested/repeated decode, result encoding, empty-mask round-trip, wrong-type rejection),examples'e2e.rs,handler.rs,mcp_tool_parity.rs,mcp_vibe_binary.rs(all pre-existing, unaffected).Also ran:
Rust toolchain/compiler versions used: as pinned by
rust-toolchain.tomland the factory image (protoc29.3,protoc-gen-prost/protoc-gen-tonic/protoc-gen-prost-serdeperAGENTS.md); no dependency versions changed (noCargo.toml/Cargo.lockdiff).Notes for reviewers
Self-reviewed via the
reviewersubagent against this issue's acceptance criteria; no blocking findings. Two optional gaps it flagged, left as follow-ups rather than blocking this PR:This branch does not touch
examples/,.github/workflows/**,AGENTS.md,docs/GOAL.md,CHANGELOG.md, or the release-please config/manifest.test-fixtures/build.rsnow also routesgoogle.protobufwell-known types topbjson_typesfor themcpgenfixture (needed forUpdateVibeRequest/Responseto have theSerialize/Deserializeimpls the new handler code calls), mirroring whatschemapb's build config already did.Factory-authored