@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 +26 -0
- package/dist/tools/apps/_location-resolver.js +35 -6
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
(
|
|
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
|
|
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
|
|
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
|
|
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
|