FGRibreau / FGRibreau/mcp-google-ads
Writes broken since 0.12.0: optional non-string params rejected (invalid type: string "false", expected a boolean)
- Lenguaje dominante
- Rust
- Estrellas
- 48
- Forks
- 21
- Merge medio
- 4 h 25 min
- PR fusionados (30 d)
- 6
Descripción
Summary
Since 0.12.0, confirm_and_apply(dry_run=false) fails before any HTTP traffic reaches Google, so no mutation can be applied at all. Draft tools still work and return a plan_id, but the plan can never be executed.
0.11.0 works against the same account, from the same client, with the identical calls. The only variable is the server binary.
The likely cause is the rmcp 0.16 → 3.1.4 bump in 0.12.0, which brought schemars 0.8 → 1.0. Optional fields are now published as union types (["boolean","null"]) instead of a single type. Clients that do not handle the union fall back to sending the value as a JSON string, and serde then rejects it.
Environment
|
-- | --
Broken | 0.13.0 (prebuilt binary, x86_64-pc-windows-gnu)
Working | 0.11.0, same machine, same config, same account
Untested | 0.12.0 — but it is the release that bumps rmcp
Client | Claude Desktop, MCP over stdio
API | Google Ads v25, live account
Steps to reproduce
- Draft any mutation — all-string parameters, this part succeeds:
pause_entity(customer_id="1234567890", entity_type="ad_group", entity_id="111111111")
→ {"plan_id":"7e3daca6", "status":"PENDING_CONFIRMATION", ...}- Apply it:
confirm_and_apply(plan_id="7e3daca6", dry_run=false)Actual (0.13.0):
failed to deserialize parameters: invalid type: string "false", expected a booleanExpected (and what 0.11.0 returns):
{"status":"APPLIED","responses":[{"adGroupResult":{"resourceName":"customers/1234567890/adGroups/111111111"}}]}The same failure class hits read tools with numeric options:
get_search_terms(customer_id="1234567890", limit=3)
→ failed to deserialize parameters: invalid type: string "3", expected u32Evidence: published inputSchema
Raw tools/list over stdio against the 0.13.0 binary (serverInfo.version confirms 0.13.0):
"limit": {"type":["integer","null"], "format":"uint32", "minimum":0}
"customer_id": {"type":["string","null"]}On 0.11.0 the client receives single types — "number", "string", "boolean".
Note the asymmetry this produces: Option<String> keeps working, because a string sent as a string is still valid. Only optional non-string fields break — and that set includes dry_run, the one flag required to apply anything.
Impact
- Every write path is unreachable.
draft_*,pause_entity,enable_entityand friends still return a plan, butconfirm_and_applycannot be invoked withdry_run=false. - Reads are unaffected except where a numeric or boolean option is passed (
get_search_terms(limit=…)). - The 0.12.0 changelog notes that
tools/call"round-trips exactly as before" — that holds for clients which send natively typed JSON, which is presumably why this went unnoticed.
Related
- modelcontextprotocol/rust-sdk#135 — optional fields emitted as
type: ["T","null"]cause client incompatibility (Cursor and Windsurf named there) - schemars 1.0 changelog —
SchemaSettings::option_nullable/option_add_null_typeremoved; generated schemas now always include the"null"type
Possible fix
Publish a single type for optional parameters rather than the union — either #[serde(default)] combined with #[schemars(schema_with = …)] on the affected fields, or a schema transform applied to every tool input schema before it is published.
Worth noting: schemars' own AddNullable transform does not solve this. It adds "nullable": true while keeping the ["T","null"] union, which is the part clients choke on.
I'm happy to test a patched build against a live account and report back.
Summary
Since 0.12.0, confirm_and_apply(dry_run=false) fails before any HTTP traffic reaches Google, so no mutation can be applied at all. Draft tools still work and return a plan_id, but the plan can never be executed.
0.11.0 works against the same account, from the same client, with the identical calls. The only variable is the server binary.
The likely cause is the rmcp 0.16 → 3.1.4 bump in 0.12.0, which brought schemars 0.8 → 1.0. Optional fields are now published as union types (["boolean","null"]) instead of a single type. Clients that do not handle the union fall back to sending the value as a JSON string, and serde then rejects it.
Environment
Broken 0.13.0 (prebuilt binary, x86_64-pc-windows-gnu)
Working 0.11.0, same machine, same config, same account
Untested 0.12.0 — but it is the release that bumps rmcp
Client Claude Desktop, MCP over stdio
API Google Ads v25, live account
Steps to reproduce
Draft any mutation — all-string parameters, this part succeeds:
pause_entity(customer_id="1234567890", entity_type="ad_group", entity_id="111111111")
→ {"plan_id":"7e3daca6", "status":"PENDING_CONFIRMATION", ...}
Apply it:
confirm_and_apply(plan_id="7e3daca6", dry_run=false)
Actual (0.13.0):
failed to deserialize parameters: invalid type: string "false", expected a boolean
Expected (and what 0.11.0 returns):
json
{"status":"APPLIED","responses":[{"adGroupResult":{"resourceName":"customers/1234567890/adGroups/111111111"}}]}
The same failure class hits read tools with numeric options:
get_search_terms(customer_id="1234567890", limit=3)
→ failed to deserialize parameters: invalid type: string "3", expected u32
Evidence: published inputSchema
Raw tools/list over stdio against the 0.13.0 binary (serverInfo.version confirms 0.13.0):
json
"limit": {"type":["integer","null"], "format":"uint32", "minimum":0}
"customer_id": {"type":["string","null"]}
On 0.11.0 the client receives single types — "number", "string", "boolean".
Note the asymmetry this produces: Option keeps working, because a string sent as a string is still valid. Only optional non-string fields break — and that set includes dry_run, the one flag required to apply anything.
Impact
Every write path is unreachable. draft_*, pause_entity, enable_entity and friends still return a plan, but confirm_and_apply cannot be invoked with dry_run=false.
Reads are unaffected except where a numeric or boolean option is passed (get_search_terms(limit=…)).
The 0.12.0 changelog notes that tools/call "round-trips exactly as before" — that holds for clients which send natively typed JSON, which is presumably why this went unnoticed.
Related
[modelcontextprotocol/rust-sdk#135](https://github.com/modelcontextprotocol/rust-sdk/issues/135) — optional fields emitted as type: ["T","null"] cause client incompatibility (Cursor and Windsurf named there)
[schemars 1.0 changelog](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md) — SchemaSettings::option_nullable / option_add_null_type removed; generated schemas now always include the "null" type
Possible fix
Publish a single type for optional parameters rather than the union — either #[serde(default)] combined with #[schemars(schema_with = …)] on the affected fields, or a schema transform applied to every tool input schema before it is published.
Worth noting: schemars' own AddNullable transform does not solve this. It adds "nullable": true while keeping the ["T","null"] union, which is the part clients choke on.
I'm happy to test a patched build against a live account and report back.
Guía de contribución
No hay ninguna guía de contribución indexada para este repositorio
Evaluación
Este issue todavía no se ha evaluado.