apple-numbers-mcp 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/utils/numbers_reader.py +52 -0
package/package.json
CHANGED
|
@@ -320,10 +320,62 @@ def _coerce_value(raw, type_hint=None, where=None):
|
|
|
320
320
|
f"Dates must be ISO 8601 (e.g. \"2025-06-01\" or a full ISO datetime)."
|
|
321
321
|
)
|
|
322
322
|
return raw
|
|
323
|
+
# No explicit type: auto-detect a clean numeric string as a number so a bare
|
|
324
|
+
# numeric value stores as a real number cell, matching create-spreadsheet /
|
|
325
|
+
# add-rows / update-rows / import-csv (all of which store the same value
|
|
326
|
+
# numerically). Some MCP clients deliver a JSON number as its string form
|
|
327
|
+
# (e.g. "30"), which would otherwise land here and be written as text.
|
|
328
|
+
if type_hint is None and isinstance(raw, str):
|
|
329
|
+
num = _numeric_str_to_number(raw)
|
|
330
|
+
if num is not None:
|
|
331
|
+
return num
|
|
323
332
|
# Default: string
|
|
324
333
|
return str(raw)
|
|
325
334
|
|
|
326
335
|
|
|
336
|
+
def _numeric_str_to_number(s):
|
|
337
|
+
"""Return int/float if `s` is a clean numeric literal, else None.
|
|
338
|
+
|
|
339
|
+
Conservative on purpose: only a plain decimal integer or float is converted.
|
|
340
|
+
Strings that a user almost certainly means as text are left alone — a leading
|
|
341
|
+
zero (zip codes, IDs like "007"), surrounding whitespace, thousands
|
|
342
|
+
separators, currency symbols, hex/binary, inf/nan, and complex/exponent-only
|
|
343
|
+
forms all stay text. An explicit type="number" override still forces numeric
|
|
344
|
+
coercion via the branch above.
|
|
345
|
+
"""
|
|
346
|
+
if not isinstance(s, str) or s == "":
|
|
347
|
+
return None
|
|
348
|
+
body = s[1:] if s[0] in "+-" else s
|
|
349
|
+
if body == "":
|
|
350
|
+
return None
|
|
351
|
+
# Reject leading-zero integers ("007", "0123") so identifier-like strings
|
|
352
|
+
# keep their exact text; a lone "0" and decimals like "0.5" are fine.
|
|
353
|
+
if body.isdigit():
|
|
354
|
+
if len(body) > 1 and body[0] == "0":
|
|
355
|
+
return None
|
|
356
|
+
try:
|
|
357
|
+
return int(s)
|
|
358
|
+
except ValueError:
|
|
359
|
+
return None
|
|
360
|
+
# Float form: require a decimal point and digits on at least one side; this
|
|
361
|
+
# naturally excludes "1e5", "inf", "nan", "1,000", "$5", "0x1f", etc.
|
|
362
|
+
if "." in body:
|
|
363
|
+
int_part, _, frac_part = body.partition(".")
|
|
364
|
+
if "." in frac_part:
|
|
365
|
+
return None
|
|
366
|
+
if not (int_part + frac_part).isdigit():
|
|
367
|
+
return None
|
|
368
|
+
if int_part == "" and frac_part == "":
|
|
369
|
+
return None
|
|
370
|
+
if len(int_part) > 1 and int_part[0] == "0":
|
|
371
|
+
return None
|
|
372
|
+
try:
|
|
373
|
+
return float(s)
|
|
374
|
+
except ValueError:
|
|
375
|
+
return None
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
|
|
327
379
|
def cmd_create(args):
|
|
328
380
|
"""Create a new .numbers file with headers and optional data rows."""
|
|
329
381
|
headers = json.loads(args.headers)
|