@openephemeris/mcp-server 4.11.1 → 4.11.2

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/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [4.11.2] — 2026-08-13
11
+
12
+ ### Fixed
13
+ - **Location resolver rejected `"City, Country"` as ambiguous.** The
14
+ ambiguity qualifier matched a region name, a US state abbreviation, or a
15
+ two-letter ISO country *code* — but never a country *name*. So
16
+ `location: "Paris, France"` lost the qualification check against its US
17
+ namesakes and threw, while the far rarer `"Paris, fr"` resolved fine.
18
+ Live, `"Paris, France"` matches 8 same-named places and `"London, UK"`
19
+ matches 6, so the most natural phrasing for a foreign birthplace failed
20
+ outright. Country names now qualify via `Intl.DisplayNames` (the runtime's
21
+ own code→name table, so there is no list to maintain), plus colloquial
22
+ aliases Intl doesn't emit (`uk` → GB). Completes the class of fix started
23
+ for US state abbreviations in 4.11.1.
24
+
25
+ ### Changed
26
+ - **Location resolver errors now carry a stable `code`.** `mcp_tool_error`
27
+ telemetry records no message and no stack, so every resolver rejection
28
+ arrived as `code: "none"` / `error_kind: "local"` — indistinguishable from
29
+ a genuine server-side crash. Throws now carry `location_ambiguous`,
30
+ `location_not_found`, `location_missing_coordinates`, `location_empty`, or
31
+ `coords_partial` on the property the event already ships. The codes are
32
+ constants; no user input is added to telemetry.
33
+
34
+ ---
35
+
10
36
  ## [4.11.1] — 2026-08-12
11
37
 
12
38
  ### Fixed
@@ -34,6 +34,31 @@ const US_STATE_ABBREVIATIONS = {
34
34
  dc: "district of columbia", pr: "puerto rico", vi: "virgin islands", gu: "guam",
35
35
  as: "american samoa", mp: "northern mariana islands",
36
36
  };
37
+ // The API reports the country as an ISO code ("FR"), but callers type the
38
+ // country *name* — "Paris, France" is the phrasing a model reaches for when
39
+ // it has a foreign birthplace. Matching only the code meant that query lost
40
+ // the qualification check against its US namesake (Paris, TX) and threw
41
+ // ambiguous, while the far rarer "Paris, fr" resolved fine. Intl.DisplayNames
42
+ // is the runtime's own code→name table, so there is no list to keep in sync.
43
+ const REGION_NAMES = new Intl.DisplayNames(["en"], { type: "region" });
44
+ function countryNameFor(code) {
45
+ try {
46
+ return String(REGION_NAMES.of(code.toUpperCase()) ?? "").toLowerCase();
47
+ }
48
+ catch {
49
+ return ""; // not a valid region subtag — fall through to the other checks
50
+ }
51
+ }
52
+ // Colloquial forms Intl does not produce: it renders GB as "United Kingdom",
53
+ // so a bare "uk" token would otherwise miss.
54
+ const COUNTRY_ALIASES = { uk: "gb", usa: "us", uae: "ae" };
55
+ // `code` rides along to PostHog via the existing `code` property on
56
+ // mcp_tool_error. The event deliberately records no message or stack, so
57
+ // without a code every one of these lands as code:"none" / error_kind:"local"
58
+ // and is indistinguishable from a genuine server-side crash.
59
+ function codedError(code, message) {
60
+ return Object.assign(new Error(message), { code });
61
+ }
37
62
  /**
38
63
  * Resolve a place name to lat/lon/tz. Throws if ambiguous or unresolvable.
39
64
  *
@@ -44,14 +69,14 @@ const US_STATE_ABBREVIATIONS = {
44
69
  export async function resolveLocationOrThrow(location) {
45
70
  const query = location.trim();
46
71
  if (query === "") {
47
- throw new Error("location is empty");
72
+ throw codedError("location_empty", "location is empty");
48
73
  }
49
74
  const raw = (await getActiveClient().request("GET", "/location/autocomplete", {
50
75
  params: { query },
51
76
  }));
52
77
  const list = Array.isArray(raw?.suggestions) ? raw.suggestions : [];
53
78
  if (list.length === 0) {
54
- throw new Error(`location "${query}" did not match any known place. Call location_search directly to inspect suggestions, ` +
79
+ throw codedError("location_not_found", `location "${query}" did not match any known place. Call location_search directly to inspect suggestions, ` +
55
80
  `or supply latitude/longitude explicitly.`);
56
81
  }
57
82
  const norm = (v) => String(v ?? "").trim().toLowerCase();
@@ -60,19 +85,23 @@ export async function resolveLocationOrThrow(location) {
60
85
  const q = norm(query);
61
86
  const qTokens = new Set(q.split(/[^a-z0-9]+/).filter(Boolean));
62
87
  const topRegion = norm(top.region);
88
+ const topCountry = norm(top.country_code);
89
+ const topCountryName = topCountry === "" ? "" : countryNameFor(topCountry);
63
90
  const qualified = (topRegion !== "" && q.includes(topRegion)) ||
64
91
  (topRegion !== "" && [...qTokens].some((t) => US_STATE_ABBREVIATIONS[t] === topRegion)) ||
65
- (norm(top.country_code) !== "" && qTokens.has(norm(top.country_code)));
92
+ (topCountry !== "" && qTokens.has(topCountry)) ||
93
+ (topCountryName !== "" && q.includes(topCountryName)) ||
94
+ (topCountry !== "" && [...qTokens].some((t) => COUNTRY_ALIASES[t] === topCountry));
66
95
  const ambiguous = rivals.length > 1 && !qualified;
67
96
  if (ambiguous) {
68
97
  const options = rivals
69
98
  .slice(0, 4)
70
99
  .map((s) => `${s.display_name ?? s.short_name} (${s.country_code ?? "??"})`)
71
100
  .join("; ");
72
- throw new Error(`location "${query}" is ambiguous — matches ${rivals.length} places (${options}${rivals.length > 4 ? ", …" : ""}). Call location_search directly and pass the disambiguated place, or supply latitude/longitude explicitly.`);
101
+ throw codedError("location_ambiguous", `location "${query}" is ambiguous — matches ${rivals.length} places (${options}${rivals.length > 4 ? ", …" : ""}). Call location_search directly and pass the disambiguated place, or supply latitude/longitude explicitly.`);
73
102
  }
74
103
  if (top.latitude == null || top.longitude == null) {
75
- throw new Error(`location "${query}" matched "${top.display_name ?? top.short_name}" but the record is missing coordinates. ` +
104
+ throw codedError("location_missing_coordinates", `location "${query}" matched "${top.display_name ?? top.short_name}" but the record is missing coordinates. ` +
76
105
  `Supply latitude/longitude explicitly.`);
77
106
  }
78
107
  return {
@@ -101,7 +130,7 @@ export async function coordsFromArgsOrLocation(args) {
101
130
  return { latitude: lat, longitude: lon, timezone: tz, location: loc, resolvedFromLocation: false };
102
131
  }
103
132
  if (latSet !== lonSet) {
104
- throw new Error("latitude and longitude must both be supplied (or both omitted with a `location` name).");
133
+ throw codedError("coords_partial", "latitude and longitude must both be supplied (or both omitted with a `location` name).");
105
134
  }
106
135
  if (!loc || String(loc).trim() === "") {
107
136
  // Neither coords nor location — leave undefined; the tool's own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openephemeris/mcp-server",
3
- "version": "4.11.1",
3
+ "version": "4.11.2",
4
4
  "description": "Model Context Protocol server for the Open Ephemeris astronomical computation API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",