Skip to content

fix(mcp-server): honour the API key's nonce mode in REST and SDK requests (PREDICT-11349) - #76

Merged
SohumDesai27 merged 4 commits into
mainfrom
predict-11349-support-per-key-gemini-api-nonce-modes-in-mcp
Nov 12, 2026
Merged

SohumDesai27 merged 4 commits into
mainfrom
predict-11349-support-per-key-gemini-api-nonce-modes-in-mcp

Conversation

@SohumDesai27

@SohumDesai27 SohumDesai27 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Whats included

  • Adds a per-key GEMINI_API_NONCE_MODE setting: incremental (default) or time-based. It has to match the API key's "Uses a time based nonce" setting, so it is never inferred from the base URL. Any other value stops the server from starting.
  • Why. Gemini checks nonces per key. Incremental keys need a strictly increasing value (fix(mcp-server): sign requests with a millisecond nonce #75 moved the MCP to increasing milliseconds for the QA100 keys). Time-based keys need epoch seconds within 30 seconds of server time. The production key created by Agentic onboarding is time-based, so since fix(mcp-server): sign requests with a millisecond nonce #75 its legacy REST calls (balances, account, etc.) fail with InvalidNonce: Nonce '1794333647192' is not within 30 seconds of server time '1794333647'.
  • Legacy REST and SDK follow the same mode on every host. fix(mcp-server): sign requests with a millisecond nonce #75 picked the unit by host: legacy REST sent milliseconds everywhere, and the SDK sent seconds on api.gemini.com/sandbox and milliseconds on custom hosts. Both now use the configured mode. Incremental keeps fix(mcp-server): sign requests with a millisecond nonce #75's shared millisecond counter for both clients. Time-based sends stateless epoch seconds, using the SDK's native time-based mode.
  • WebSocket auth is unchanged. It still uses epoch seconds in both modes.
  • Nonce errors say what to fix. The tool layer appends a hint to InvalidNonce errors: set time-based when a key rejects milliseconds, use incremental when it rejects seconds, check the host clock for an out-of-window error under time-based, and a general hint for SDK errors (the SDK keeps only reason=InvalidNonce, not the server's message).
  • Alerts daemon and docs. GEMINI_API_NONCE_MODE is added to the daemon's env whitelist so an installed daemon keeps it. README.md, ALERTS.md and claude_desktop_config.example.json document it, and the Claude Code, Claude Desktop and ChatGPT examples set it.
  • Behavior change for time-based keys. For someone on production or sandbox with a time-based key and no variable set: before this PR, prediction (SDK) tools worked and legacy tools failed. After it, both fail until GEMINI_API_NONCE_MODE=time-based is set, and the error says exactly that. Incremental keys (the default for keys created without the time-based box) need no change.
  • Not in this PR:
    • The onboarding-generated MCP setup doesn't set the variable yet. That's the web change in PREDICT-11376.
    • Mixed concurrent legacy and SDK calls on an incremental key can reach the server out of nonce order. Tracked in PREDICT-11354.
  • No changes to tool names, schemas, descriptions, or the confirm: true gate.

Linear: https://linear.app/gemini/issue/PREDICT-11349/support-per-key-gemini-api-nonce-modes-in-mcp

Test plan

  • npm run build — clean
  • npm run typecheck — clean
  • npm test — 374/374 passing, including:
    • mode parsing: default, exact values, invalid values, and the env → config startup path in a child process
    • legacy REST: milliseconds by default, seconds with a valid signature under time-based
    • SDK: incremental shares one counter with legacy REST on standard and custom hosts; time-based sends seconds
    • hints for both mismatch directions through wrapHandler, and an SDK InvalidNonce error end to end through the real SDK with a fake fetch
    • all of fix(mcp-server): sign requests with a millisecond nonce #75's nonce tests still pass, and WebSocket auth still sends seconds
  • Mutation checks: reverting the unit selection in http.ts/sdk.ts, matching the hint on message only, always passing incremental from the wrapper, and ignoring the env in config.ts each make the new tests fail
  • Tested against QA100 and production through a local MCP built from this branch, read-only calls only. Each case calls gemini_get_balances (legacy REST) and gemini_get_prediction_positions (SDK).

Test 1 — production, Agentic (time-based) key, GEMINI_API_NONCE_MODE=time-based

Prompt: Using only the gemini-prod-test MCP server, call gemini_get_balances, then call gemini_get_prediction_positions.

Both succeeded. Balances returned the account's USD balance, which is the call that failed before this PR. Positions returned {"positions": []}.

Test 2 — production, same key, burst

Prompt: call gemini_get_balances 5 times one after another, then 5 times in parallel

10/10 succeeded, with no InvalidNonce.

Test 3 — production, same key, variable unset (reproduces the original bug)

Prompt: Using only the gemini-prod-test MCP server, call gemini_get_balances, then call gemini_get_prediction_positions.

gemini_get_balances:

Error: Gemini API error 400: {"result":"error","reason":"InvalidNonce","message":"Nonce '1794495111052' is not within 30 seconds of server time '1794495111''"}
This key rejects millisecond nonces, so it is a time-based key. Set GEMINI_API_NONCE_MODE=time-based in the MCP server environment and restart the server.

gemini_get_prediction_positions:

Error: HTTP 400 (reason=InvalidNonce, code=authentication_failed, category=authentication)
The server rejected this key's nonce. Check GEMINI_API_NONCE_MODE against the key's "Uses a time based nonce" setting: a time-based key needs GEMINI_API_NONCE_MODE=time-based, and an incremental key should leave it unset. Time-based nonces also need this host's clock within 30 seconds of server time.

Test 4 — QA100, time-based key

With the variable unset, gemini_get_balances failed with:

Nonce '1794341053970' is not within 30 seconds of server time '1794341054'
This key rejects millisecond nonces, so it is a time-based key. Set GEMINI_API_NONCE_MODE=time-based in the MCP server environment and restart the server.

With GEMINI_API_NONCE_MODE=time-based, balances and positions both succeeded, and 5 sequential + 5 parallel balance calls all succeeded.

Test 5 — QA100, incremental key, default mode

A new QA100 key created with "Uses a time based nonce" unchecked, and no variable set. Balances, positions, and 5 sequential + 5 parallel balance calls all succeeded, so #75's behavior is unchanged.

Test 6 — QA100, same incremental key, GEMINI_API_NONCE_MODE=time-based

gemini_get_balances:

Nonce '1794494235' has not increased since your last call to the Gemini API.
This key rejects time-based nonces, so it is an incremental key.
Remove GEMINI_API_NONCE_MODE or set it to incremental, then restart the server.

Back on the default mode, balances succeeded again, so the wrong setting didn't affect the key.

Note for anyone repeating this: a brand-new incremental key accepts a seconds nonce for the first few minutes, because the server saves a key's last nonce after a delay. The rejection above showed up once that had happened.

Test 7 — invalid setting

Command: GEMINI_API_NONCE_MODE=bogus node dist/index.js

Error: Invalid GEMINI_API_NONCE_MODE "bogus": must be "incremental" or "time-based" (or unset, which defaults to incremental).

The server exits without starting.

Not tested live: an incremental key on production or sandbox, a sandbox key, and the alerts daemon or WebSocket streams with a time-based key. Unit tests cover the daemon whitelist and WebSocket auth.

…ests (PREDICT-11349)

Keys created with "Uses a time based nonce" (including Agentic onboarding
keys) reject millisecond nonces, while the QA100 keys from PR #75 need them.
The legacy REST signer always sent milliseconds, and the SDK hardcoded
seconds on standard hosts and milliseconds on custom hosts, so a single
key could not work on every path.

- Add GEMINI_API_NONCE_MODE = incremental | time-based. Unset defaults to
  incremental, preserving PR #75 behaviour. Any other value fails startup.
- Legacy REST signs with the configured unit. Incremental keeps the shared
  millisecond sequence; time-based uses epoch seconds with no state.
- SDK auth follows the same mode on every base URL. Incremental routes
  through the shared sequence so SDK and legacy requests never interleave
  out of order. Time-based uses the SDK's native time-based mode.
- WebSocket auth is unchanged and always uses epoch seconds.
- Nonce rejections get an operator hint naming the mode to set.
- Add the variable to the alerts daemon's env whitelist and the README.
@SohumDesai27

Copy link
Copy Markdown
Collaborator Author

@svc-grace review

@sdlc-pr-bot

sdlc-pr-bot Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Change Control Evidence Check

Authorization — ✅ Pass

Evidence:

  • Linear ticket: PREDICT-11349
  • Change owner: Sohum Desai
  • Type of change: Normal
Testing — ✅ Pass

Evidence:

  • Tests from CI checks: ➖ No tests ran
  • Security scans: ✅ Passed
  • Testing summary: ✅ Found in Linear ticket
    • Summary: Unit testing confirmed that all three code snippets contain the GEMINI_API_NONCE_MODE=time-based constant and that removing it from the payload causes detectable changes. End-to-end testing verified that generated snippets function correctly with MCP builds including PR fix(mcp-server): honour the API key's nonce mode in REST and SDK requests (PREDICT-11349) #76, with gemini_get_balances and gemini_get_prediction_positions succeeding using the Agentic key, and confirmed expected failure behavior in
  • Evidence link: View run
Approval — ✅ Pass

Evidence:

  • Approver: Maneesh Kolli
  • Approval source: GitHub PR Review
  • Approval timestamp: 2026-11-12 15:19 UTC
Segregation of Duties — ✅ Pass

Validated:

  • PR author: Sohum Desai
  • Commit author(s): SohumDesai27
  • Linear assignee: Sohum Desai
  • Approver: Maneesh Kolli
  • Result: Implementer and approver are different people ✅

Last checked: 2026-11-12 16:51 UTC

↩ Re-run Clarissa if tickets, CI, or approvals have changed.

@linear-code

linear-code Bot commented Nov 10, 2026

Copy link
Copy Markdown

PREDICT-11349

@SohumDesai27
SohumDesai27 marked this pull request as ready for review November 10, 2026 19:08
@nostradamus-bot

Copy link
Copy Markdown

Nostradamus Risk Rating — High

The PR modifies HMAC-SHA384 nonce generation in packages/mcp-server/src/auth/signer.ts, src/client/http.ts, and src/client/sdk.ts — all Critical-tier components that sign every authenticated financial API request (orders, withdrawals, funds transfers). The change introduces a configurable nonce mode (GEMINI_API_NONCE_MODE) with strict startup validation and no new unauthenticated attack surface, so Change Characteristics are Medium; combined with Critical component sensitivity, the NIST SP 800-30r1 matrix yields High.

High/Critical — a full threat model follows.

@svc-grace svc-grace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agentic Review

Nice, this covers the main nonce-mode paths, but SDK-authenticated nonce errors still miss the mode hint, so users can hit the mismatch without the guidance they get on the REST path. There are also a few test and setup-example gaps around the time-based mode; those are worth a follow-up but aren’t what’s driving the requested changes.

Review process

Grace version: v0.0.254
Files reviewed (13): README.md, config.test.ts, config.ts, http.request.test.ts, http.ts, index.test.ts, index.ts, sdk.test.ts, sdk.ts, signer.test.ts, signer.ts, types.ts
Guidelines: none discovered
Verification: 4 of 6 findings verified
Findings: 0 critical, 1 important, 3 suggestions
Linked tickets: PREDICT-11349
LLM usage: 100 calls — gpt-6-luna: 86 calls, 2434176 tokens, us.anthropic.claude-opus-5-5: 14 calls, 363225 tokens

Comment thread packages/mcp-server/src/tools/index.ts Resolved
Comment thread packages/mcp-server/src/config.test.ts Resolved
Comment thread packages/mcp-server/README.md Resolved
@nostradamus-bot

Copy link
Copy Markdown

Nostradamus Threat Model - Passed - APPSEC-6108

…ts (PREDICT-11349)

- SDK nonce rejections now get the mode hint. The SDK keeps only the
  rejection code (reason=InvalidNonce) and a generic "HTTP <status>"
  message, so the hint now matches the full error text and falls back to a
  generic hint covering both mismatch directions when the server sentence
  is absent.
- An out-of-window rejection under time-based mode now reports clock skew.
- Cover the time-based "has not increased" branch through wrapHandler, and
  an SDK-shaped InvalidNonce error end to end.
- Load config in a child process with GEMINI_API_NONCE_MODE set, and with an
  invalid value, to cover the env-to-config startup path.
- Add GEMINI_API_NONCE_MODE to the Claude Desktop and ChatGPT README examples.
@SohumDesai27

Copy link
Copy Markdown
Collaborator Author

@svc-grace review

@svc-grace svc-grace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agentic Review

All four carried-over concerns are resolved — nice progress. I also missed the shared nonce counter’s SDK/REST dispatch issue on an earlier pass; that code hasn’t changed, so it’s a follow-up worth a look, not a blocker. This pass also flags a Windows-specific config test import issue; the inline note has the details.

Review process

Grace version: v0.0.254
Files reviewed (7): README.md, config.test.ts, index.test.ts, index.ts, sdk.test.ts, signer.test.ts, signer.ts
Files skipped: 12
Guidelines: none discovered
Verification: 2 of 5 findings verified
Findings: 0 critical, 0 important, 2 suggestions
Linked tickets: PREDICT-11349, APPSEC-6108
LLM usage: 78 calls — gpt-6-luna: 65 calls, 1685668 tokens, us.anthropic.claude-opus-5-5: 13 calls, 420875 tokens

Comment thread packages/mcp-server/src/client/sdk.ts Resolved
Comment thread packages/mcp-server/src/config.test.ts Resolved
… file: URL (PREDICT-11349)

import() reads a native Windows path such as C:\repo\config.ts as a URL
scheme, so the valid-mode config test failed on Windows. Pass a file: URL
derived from import.meta.url instead.
@SohumDesai27

Copy link
Copy Markdown
Collaborator Author

@svc-grace review

@svc-grace svc-grace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agentic Review

The earlier setup-example, environment-config, Windows import, wrapper-coverage, and SDK-hint issues are all resolved. The shared nonce counter still doesn’t serialize SDK and REST dispatch; that’s a non-blocking follow-up if concurrent calls can interleave. This change carries the API key’s configured nonce mode through REST and SDK requests, and the key is keeping both paths aligned on the same mode and counter where incremental signing is used.

Persisted from prior passes (1 open)
Review process

Grace version: v0.0.254
Files reviewed (1): config.test.ts
Files skipped: 23
Guidelines: none discovered
Findings: 0 critical, 0 important, 0 suggestions
Linked tickets: PREDICT-11349, APPSEC-6108
LLM usage: 16 calls — gpt-6-luna: 16 calls, 201994 tokens

… desktop config example (PREDICT-11349)

- ALERTS.md: list the env vars the daemon bakes into its unit as they are in
  code, adding GEMINI_API_NONCE_MODE and the previously missing GEMINI_SDK_ENV,
  and note that a time-based key needs the mode baked into the unit.
- claude_desktop_config.example.json: set GEMINI_API_NONCE_MODE to time-based,
  matching the README examples, so copying it with an Agentic key works.
@SohumDesai27
SohumDesai27 merged commit 7a3f561 into main Nov 12, 2026
9 checks passed
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.

3 participants