Value: add numberValue (Double?) that coerces .int and .double (closes #225)#226
Open
gsdali wants to merge 1 commit intomodelcontextprotocol:mainfrom
Open
Value: add numberValue (Double?) that coerces .int and .double (closes #225)#226gsdali wants to merge 1 commit intomodelcontextprotocol:mainfrom
gsdali wants to merge 1 commit intomodelcontextprotocol:mainfrom
Conversation
JSON has a single `number` type, but Swift's JSONEncoder writes whole-number Doubles without a decimal point (`Value.double(0)` → JSON `0`) and Value's decoder tries Int first (JSON `0` → `Value.int(0)`). Result: `Value.double(0).doubleValue` returns the value, but the same value after a JSON round-trip returns nil — a footgun for any tool dispatch reading numeric tool arguments from LLMs (`[0, 0, 1]` is the natural unit-vector shape). `numberValue: Double?` returns the underlying scalar promoted to Double for both .int and .double, nil for everything else. Additive to existing `intValue` / `doubleValue` accessors; no API change, no breaking change. Closes modelcontextprotocol#225.
3 tasks
gsdali
added a commit
to gsdali/OCCTMCP
that referenced
this pull request
May 10, 2026
Eats our own dog food on modelcontextprotocol/swift-sdk#226 (closes upstream issue #225). The fork branch (gsdali/swift-sdk#add-value-numbervalue) ships Value.numberValue — exactly the helper our Server.swift dispatch wanted. Package.swift: dep flips from modelcontextprotocol/swift-sdk from: "0.11.0" to gsdali/swift-sdk branch: "add-value-numbervalue" with a comment noting the pin is temporary and reverts to the upstream tag floor once swift-sdk#226 merges. Server.swift: drops the local `extension Value { var asDouble }` back-port (was added in OCCTMCP v1.3) and switches all 19 dispatch sites — translate / rotate / mirror / pattern / circular / select_topology / find_correspondences scalar + array reads — from .asDouble to .numberValue. Identical semantics; we just stop maintaining a private copy of the helper. No release tag — main is intentionally on a fork pin until upstream merges. Cohort downstream consumers building OCCTMCP from main pull the fork transparently; the moment upstream lands we revert Package.swift to `from: "0.11.0"` and tag a release. 25/25 tests still pass. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
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 #225.
Summary
Adds
Value.numberValue: Double?that coerces both.intand.doubletoDouble. Motivation in #225 — short version:JSON has a single
numbertype with no integer/floating-point distinction. Swift'sJSONEncoderwrites whole-numberDoubles without a decimal point (Value.double(0)→ JSON0) andValue'sinit(from:)triesIntfirst (JSON 0→Value.int(0)). So any consumer reading numeric tool arguments viaarr[N].doubleValuesilently fails for[0, 0, 1](the natural unit-vector shape an LLM would emit) — works fine for[0.5, 0.5, 0.5]test fixtures, breaks in production.numberValuereturns the underlying scalar promoted toDoublefor.int(_)and.double(_); nil otherwise. Additive — no breaking changes.Why on the SDK side
I hit this in OCCTMCP on a coordinate-array tool argument; the workaround is a one-line extension that every Swift MCP server is going to copy:
Better to ship it once on the SDK so consumers don't reinvent it (or, more likely, hit the silent-fail path before realising they need to).
Implementation
Doc comment explicitly calls out the JSON whole-number-decodes-as-int gotcha so future readers learn why this exists alongside
intValue/doubleValue.Test plan
swift buildcleanswift test— 557/557 tests pass (was 551; +6 inValueTests)ValueTestscover: int → Double, double → Double pass-through, nil for non-numeric cases, and the round-trip case that motivated this — encodeValue.double(0)and assertdecoded.doubleValue == nil(the gotcha),decoded.numberValue == 0.0(the fix), plus a[0, 0, 1]-shaped coordinate-array round-trip end-to-end.Out of scope
Bool-coercing accessor (no0/1↔ true/false precedent in MCP schemas).Stringnumerics like"42"as numbers — schemas should reject those.🤖 Generated with Claude Code