Skip to content

fix(mcp-server): sign requests with a millisecond nonce - #75

Open
deeputom wants to merge 4 commits into
mainfrom
fix/mcp-server-millisecond-nonce
Open

deeputom wants to merge 4 commits into
mainfrom
fix/mcp-server-millisecond-nonce

Conversation

@deeputom

@deeputom deeputom commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Linear: https://linear.app/gemini/issue/INVEST-681/fix-gemini-mcp-server-nonce-units-seconds-scale-nonce-permanently

Problem

Every authenticated tool on the MCP server fails with:

400 {"result":"error","reason":"InvalidNonce",
     "message":"Nonce '1793808885' has not increased since your last call to the Gemini API."}

src/auth/signer.ts built the nonce as Math.floor(Date.now() / 1000) — epoch
seconds. The Gemini API tracks the highest nonce accepted per API key, and
that value is an epoch millisecond timestamp. Once a key has accepted one
millisecond nonce (~1.79e12), every seconds nonce (~1.79e9) is three orders
of magnitude too small and is rejected permanently. The server never recovers on
its own, and the error names no caller, so it reads like a concurrent-client
collision.

README.md:76 already documents a "millisecond timestamp nonce"; the code
disagreed with its own documentation.

Second commit: gemini_get_accounts targets POST /v1/account/list, a
master-key-only endpoint. Every scoped key gets a bare 400 InvalidApiKey while
the tool description promised "list all accounts". The tool now maps that
rejection to an actionable error and documents the scope requirement.

Measured on https://api.qa100.aurora7.net, read-only POST /v1/account, five
distinct staging keys:

nonce unit result
seconds 400 InvalidNonce on all 5 keys
milliseconds 200 on all 5 keys

Fix

  • Nonce is now epoch milliseconds.
  • nextNonce() keeps nonces strictly increasing within the process, so tool
    calls issued in the same millisecond cannot share a nonce.
  • gemini_get_accounts maps the scoped-key InvalidApiKey to an error naming
    the master-key requirement and gemini_get_account as the alternative.

Test Plan

  • npx tsx --test 'src/**/*.test.ts': 346/346 pass (after merging main, whose SDK + WebSocket work landed in the same package) — nonce unit, same-millisecond strict increase, frozen clock, wall-clock catch-up, master-key error mapping, unrelated-error passthrough.
  • Live on qa100 through the rebuilt dist: seconds nonce → 400 InvalidNonce; ms nonce → 200 on all 5 staging keys; 5 concurrent signed /v1/account calls per key → 5/5 ok on all keys.
  • End-to-end trading through the MCPs: placed and filled prediction orders on both sides of a live contract (GEMI-BTC2611041700-HI79200: 5 YES @ 0.95, 5 NO @ 0.144 avg); the contract settled and payouts landed (lg1 +$0.24, lg2 −$0.75).
  • Live gemini_get_accounts for a scoped key now returns: "requires a master API key with account-list permission … use gemini_get_account".

Ready for review; testing evidence also mirrored in Linear INVEST-681.

Merge

Merged main (6dd34be) in d4a30ec: kept this branch's REST millisecond nonce, preserved main's WebSocket auth (which documents an epoch-seconds nonce), and unioned both signer.test.ts suites. npm run typecheck and npm run build clean; dependency install refreshed for @gemini-markets/sdk.

SDK custom-host regression repair (d38deda)

Main migrated predictions to the SDK, whose environment selector routed QA100 requests to api.gemini.com despite GEMINI_API_BASE_URL. The shared SDK factory now routes custom REST hosts through the supported fetch hook and shares the legacy millisecond nonce allocator for custom targets. Standard endpoint SDK behavior and WebSockets are unchanged.

Verification

  • npm run typecheck, npm test (348/348 pass), npm run build succeeded.
  • Fake-fetch regression exercises custom-host reads and order creation without submitting real orders.
  • After rebuilding/reloading MCPs, live active-orders reads succeed on gemini-staging-2 and predict-cash-lg1/lg2/lg3.
  • gemini-staging reaches the correct host but still returns InvalidNonce; an earlier accepted diagnostic nonce was future-dated to 2026-11-05T16:24:17.397Z. No new live orders submitted in this repair.

The signer used Math.floor(Date.now() / 1000), a seconds-scale value, while
the Gemini API's per-key nonce floor is an epoch-millisecond timestamp. Any
key that has ever accepted a millisecond nonce rejects every subsequent
seconds nonce, so every authenticated tool failed with:

  InvalidNonce: Nonce '1793808885' has not increased since your last call

Nonces are now epoch milliseconds and monotonic within the process, so
concurrent tool calls issued in the same millisecond no longer collide.
@sdlc-pr-bot

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

Copy link
Copy Markdown

Change Control Evidence Check

Authorization — ✅ Pass

Evidence:

  • Linear ticket: PREDICT-10991
  • Change owner: Deepu Thomas
  • Type of change: Normal
Testing — ✅ Pass

Evidence:

  • Tests from CI checks: ➖ No tests ran
  • Security scans: ✅ Passed
  • Testing summary: ✅ Found in Linear ticket
    • Summary: All unit tests passed (346/346), type checking and build completed successfully, and live QA testing confirmed correct nonce validation and concurrent request handling across all keys. The millisecond nonce implementation functions as intended with proper error responses for invalid nonces.
  • Evidence link: View run
Approval — ⏳ Awaiting approval

Evidence:
(no approvers yet)

Segregation of Duties — ⏳ Awaiting approval

Validated:

  • PR author: Deepu Thomas
  • Linear assignee: Deepu Thomas

Last checked: 2026-11-05 05:24 UTC

…ounts

/v1/account/list is a master-key endpoint, so every scoped key gets a bare
400 InvalidApiKey. gemini_get_accounts promised 'list all accounts' while it
can only ever fail for scoped keys. Map the rejection to an actionable error
pointing at gemini_get_account, and say the scope requirement in the tool
description.
@deeputom
deeputom marked this pull request as ready for review November 4, 2026 20:02
@nostradamus-bot

Copy link
Copy Markdown

Nostradamus Risk Rating — High

The PR modifies packages/mcp-server/src/auth/signer.ts, the core HMAC-SHA384 signing primitive that authenticates every request including orders, withdrawals, and fund transfers (Critical component sensitivity). The change itself corrects a nonce unit bug (seconds → milliseconds) with process-level monotonicity enforcement and adds error mapping in account.ts — no new attack surface, no cryptographic algorithm changes, and no auth bypass introduced — placing change characteristics at Medium. NIST SP 800-30r1 Critical × Medium yields High.

High/Critical — a full threat model follows.

@nostradamus-bot

Copy link
Copy Markdown

Nostradamus Threat Model - Passed - APPSEC-4773

…nto fix/mcp-server-millisecond-nonce

# Conflicts:
#	packages/mcp-server/src/auth/signer.test.ts

This branch has not been deployed

No deployments
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.

2 participants