You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Fix MCP output schema validation across backup, replication, snapshot, and pool tools - #62
Nanos preservation:formatProtobufTimestamp() now includes protobuf nanos when converting to ISO 8601 (e.g. { seconds: 1234567890, nanos: 500000000 } → 2009-02-13T23:31:30.500Z).
MCP output schema test: added listBackupsHandler structuredContent passes MCP output schema validation to mirror the server-side validation path used by Gemini/Claude clients.
CI: pinned fast-uri@3.1.8 to clear high-severity npm audit failures.
Gemini CLI proof: added scripts/verify-gemini-mcp-backup-list.mjs — connects over stdio (same transport as gemini-extension.json) and confirms gcnv_backup_list advertises enforcedRetentionEndTime as string.
garyamannetapp
changed the title
Fix gcnv_backup_list schema validation for enforcedRetentionEndTime
Fix MCP output schema validation for backup, replication, and pool tools
Sep 25, 2026
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟡 Changes recommended
Unresolved issues include fabricated resource identifiers, lossy int64 conversion, possible pool schema mismatches, and insufficient live-test assertions.
Get a fresh assessment by requesting another Copilot review.
garyamannetapp
changed the title
Fix MCP output schema validation for backup, replication, and pool tools
Fix MCP output schema validation across backup, replication, snapshot, and pool tools
Sep 25, 2026
Missing snapshot fields are replaced with fabricated values
src/tools/handlers/snapshot-handler.ts:39
These fallbacks turn missing API fields into valid-looking values: unknown is not the source volume and an empty string is not a creation timestamp. Clients can therefore mistake an incomplete snapshot for a real resource with a valid time. Preserve the fields as absent (with optional schema fields) or return an error rather than fabricating them.
Handle sparse list items with required identity fields
src/tools/handlers/backup-handler.ts:61
The list formatter still cannot produce schema-required identity fields for an undefined/sparse item: list context has no backupId, so backupId remains absent and the !result.name branch never runs. The existing listBackupsHandler test exercises exactly [undefined] but does not validate its output against listBackupsTool; use a per-item fallback (or omit such items) so name and backupId are present before MCP validation.
Validate snapshot names before preserving them
src/tools/handlers/snapshot-handler.ts:64
formatSnapshotData preserves any truthy snapshot.name, so the sparse response described in this PR (projects/.../locations/us-central1/snapshots/s1) remains a non-canonical snapshot resource name even though volumeId is recovered from the request. Only retain name when it matches the /volumes/{volume}/snapshots/{id} shape; otherwise keep the ID and let ensureSnapshotRequiredFields synthesize the canonical name from the request context.
…, and pool tools.
Normalize protobuf timestamps, enums, and int64 fields in shared helpers and handlers so MCP SDK 1.29+ output validation passes. Add required-field fallbacks for sparse API responses, restore backup byte-size schema descriptions, reject incomplete timestamps, and pin brace-expansion for CI security audit.
Co-authored-by: Cursor <cursoragent@cursor.com>
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
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.
Summary
Fixes MCP output validation when handlers return raw GCNV/protobuf values that do not match Zod output schemas (MCP SDK 1.29.0+).
Shared helpers:
src/utils/proto-format-utils.tsGEMINI CLI
Use local PR build (not
npx gcnv-mcp-server@latest):Then run (replace
<tool>/ prompt as shown per section):gemini --skip-trust --approval-mode yolo -p "Call <tool> with <args> and show the JSON result"backup (
gcnv_backup_list)NOT WORKING (main)
Command
gemini --skip-trust --approval-mode yolo -p "Call gcnv_backup_list with projectId my-gcp-project location us-central1 backupVaultId vault-1 pageSize 3"IN
{ "projectId": "my-gcp-project", "location": "us-central1", "backupVaultId": "vault-1", "pageSize": 3 }GCNV API (handler receives)
{ "backupId": "backup-1", "enforcedRetentionEndTime": { "seconds": "1791522585", "nanos": 0 } }OUT
{ "isError": true, "error": "MCP error -32602: Output validation error: Expected number, received object at backups[0].enforcedRetentionEndTime" }WORKING (this PR)
Command — same as above (PR branch server in
.gemini/settings.json)OUT
{ "isError": false, "backups": [ { "backupId": "backup-1", "enforcedRetentionEndTime": "2026-10-09T05:09:45.000Z", "createTime": "2026-09-25T05:09:45.000Z", "state": "READY", "volumeUsagebytes": "0" } ] }snapshot (
gcnv_snapshot_list)NOT WORKING (main)
Command
gemini --skip-trust --approval-mode yolo -p "Call gcnv_snapshot_list with projectId my-gcp-project location us-central1 volumeId vol1 pageSize 2"IN
{ "projectId": "my-gcp-project", "location": "us-central1", "volumeId": "vol1", "pageSize": 2 }GCNV API returns snapshot missing required schema fields (example: name only, no
/volumes/{id}/in path){ "name": "projects/my-gcp-project/locations/us-central1/snapshots/s1" }Pre-fix handler output (sent to validator)
{ "snapshots": [ { "name": "projects/my-gcp-project/locations/us-central1/snapshots/s1", "snapshotId": "s1" } ] }OUT
{ "isError": true, "error": "Output validation error: Required at snapshots[0].volumeId; Required at snapshots[0].state; Required at snapshots[0].createTime" }WORKING (this PR)
Command — same as above
GCNV API (example snapshot response)
{ "name": "projects/my-gcp-project/locations/us-central1/volumes/vol1/snapshots/snapshot-1", "state": "READY", "createTime": { "seconds": "1790236454", "nanos": 0 } }Post-fix handler output
{ "snapshots": [ { "snapshotId": "snapshot-1", "volumeId": "vol1", "state": "READY", "createTime": "2026-09-24T07:54:14.000Z" } ] }OUT
{ "isError": false, "snapshots": [ { "volumeId": "vol1", "state": "READY", "createTime": "2026-09-24T07:54:14.000Z" } ] }backup vault (
gcnv_backup_vault_get)NOT WORKING (main)
Command
gemini --skip-trust --approval-mode yolo -p "Call gcnv_backup_vault_get with projectId my-gcp-project location us-central1 backupVaultId vault-1"IN
{ "projectId": "my-gcp-project", "location": "us-central1", "backupVaultId": "vault-1" }GCNV API returns minimal vault (only name + protobuf createTime)
{ "name": "projects/my-gcp-project/locations/us-central1/backupVaults/bv1", "createTime": { "seconds": "1", "nanos": 0 } }Pre-fix handler output
{ "name": "projects/my-gcp-project/locations/us-central1/backupVaults/bv1", "backupVaultId": "bv1", "createTime": "1970-01-01T00:00:01.000Z" }OUT
{ "isError": true, "error": "Output validation error: Required at state; Required at backupVaultType" }WORKING (this PR)
Command — same as above
OUT (example output)
{ "isError": false, "name": "projects/my-gcp-project/locations/us-central1/backupVaults/vault-1", "backupVaultId": "vault-1", "state": "READY", "createTime": "2026-08-30T19:26:47.000Z", "backupVaultType": "IN_REGION", "backupRetentionPolicy": { "backupMinimumEnforcedRetentionDays": 14, "manualBackupImmutable": true } }quota (
gcnv_quota_rule_list)NOT WORKING (main)
Command
gemini --skip-trust --approval-mode yolo -p "Call gcnv_quota_rule_list with projectId my-gcp-project location us-central1 volumeId vol1 pageSize 3"IN
{ "projectId": "my-gcp-project", "location": "us-central1", "volumeId": "vol1", "pageSize": 3 }GCNV API returns string enum for quota type
{ "quotaRules": [ { "name": "projects/p1/locations/us-central1/volumes/vol1/quotaRules/q1", "quotaRuleId": "q1", "type": "INDIVIDUAL_USER_QUOTA", "diskLimitMib": 1024 } ] }Pre-fix handler output (copied as-is)
{ "quotaRules": [ { "quotaRuleId": "q1", "type": "INDIVIDUAL_USER_QUOTA", "quotaType": "INDIVIDUAL_USER_QUOTA" } ] }OUT
{ "isError": true, "error": "Output validation error: Expected number, received string at quotaRules[0].type" }WORKING (this PR)
Post-fix handler output
{ "quotaRules": [ { "quotaRuleId": "q1", "type": 1, "quotaType": 1, "diskLimitMib": 1024 } ] }OUT
{ "isError": false, "quotaRules": [ { "quotaRuleId": "q1", "type": 1, "quotaType": 1 } ] }kms / replication / storage pool
Same format as above — see backup section pattern. Storage pool
gcnv_storage_pool_listverified:{ "qosType": "AUTO", "serviceLevel": "FLEX" }.CURSOR
Point
~/.cursor/mcp.jsonat server binary:path/to/gcnv-mcp-server/build/index.js(main)mcp/gcnv-mcp-server/build/index.js(this PR, afternpm run build)Restart MCP, then ask agent to call the tool (same IN JSON as Gemini sections).
snapshot (
gcnv_snapshot_list)NOT WORKING (main) — same IN/OUT as Gemini NOT WORKING above
WORKING (this PR)
IN
{ "projectId": "my-gcp-project", "location": "us-central1", "volumeId": "vol1", "pageSize": 2 }OUT
{ "snapshots": [ { "snapshotId": "snapshot-1", "volumeId": "vol1", "state": "READY", "createTime": "2026-09-24T07:54:14.000Z" } ] }backup vault (
gcnv_backup_vault_get)NOT WORKING (main) — same IN/OUT as Gemini NOT WORKING above
WORKING (this PR) — example output
IN
{ "projectId": "my-gcp-project", "location": "us-central1", "backupVaultId": "vault-1" }OUT
{ "backupVaultId": "vault-1", "state": "READY", "createTime": "2026-08-30T19:26:47.000Z", "backupVaultType": "IN_REGION", "backupRetentionPolicy": { "backupMinimumEnforcedRetentionDays": 14 } }quota (
gcnv_quota_rule_list)NOT WORKING (main) — same IN/OUT as Gemini NOT WORKING above
WORKING (this PR) — same normalized numeric
type/quotaTypeOUT as Geminibackup / kms — example output
See Gemini sections; backup NOT WORKING and WORKING shown in examples above.
Tests
npm test— 727 passedCI
Node 20 / Node 24 green