@openephemeris/mcp-server 4.3.0 → 4.4.0
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 +72 -0
- package/dist/tools/apps/_location-resolver.d.ts +34 -0
- package/dist/tools/apps/_location-resolver.js +100 -0
- package/dist/tools/apps/bi-wheel-app.js +18 -9
- package/dist/tools/apps/bodygraph-app.js +24 -12
- package/dist/tools/apps/chart-wheel-app.js +25 -10
- package/dist/tools/apps/vedic-chart-app.js +22 -11
- package/dist/tools/specialized/account.js +12 -2
- package/dist/tools/specialized/natal.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,78 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [4.4.0] — 2026-07-30
|
|
11
|
+
|
|
12
|
+
Puts the location resolver on the natural path for the four `explore_*`
|
|
13
|
+
chart tools, and corrects two parameter descriptions that had drifted from
|
|
14
|
+
the code.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`location` on `explore_natal_chart`, `explore_human_design`,
|
|
19
|
+
`explore_vedic_chart`, and `explore_bi_wheel` now resolves internally.**
|
|
20
|
+
Supply a plain place name — `location: "Portland, ME"` — and the tool
|
|
21
|
+
fills in the coordinates and (unless overridden) the timezone via the
|
|
22
|
+
same `/location/autocomplete` lookup `location_search` uses. Supplied
|
|
23
|
+
`latitude`/`longitude` always win. Ambiguous names (e.g. bare
|
|
24
|
+
`"portland"`) throw with a disambiguation hint rather than silently
|
|
25
|
+
picking the top hit — the same behaviour `location_search` reports as
|
|
26
|
+
`ambiguous: true`.
|
|
27
|
+
A prior smoke run measured `location_search` adoption at **1 in 8** when
|
|
28
|
+
it was documented but off the natural path; every request supplied
|
|
29
|
+
coordinates from model memory instead. Wiring `location` into the chart
|
|
30
|
+
tools themselves makes the resolver the minimum-effort call.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **`explore_human_design` no longer silently computes at 0°N 0°E.**
|
|
35
|
+
When neither `latitude`/`longitude` nor a resolvable `location` is
|
|
36
|
+
supplied, the call is now rejected with a clear message. Previously a
|
|
37
|
+
bare `datetime` produced a plausible-looking bodygraph anchored on the
|
|
38
|
+
Gulf of Guinea — a location-sensitive system silently defaulting to a
|
|
39
|
+
wrong location is the failure class the Phase-0 audits keep catching.
|
|
40
|
+
- **`include_fixed_stars` and `include_arabic_parts` descriptions on
|
|
41
|
+
`ephemeris_natal_chart` no longer say "Reserved for future use".** Both
|
|
42
|
+
parameters have always been wired to their respective configuration
|
|
43
|
+
keys (`configuration.fixed_star_options.include: true` and
|
|
44
|
+
`options.include_hermetic_lots: true`); only the descriptions were
|
|
45
|
+
stale.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## [4.3.1] — 2026-07-29
|
|
50
|
+
|
|
51
|
+
Two rendering fixes surfaced by the Phase-0 v4.3 audit, plus a companion
|
|
52
|
+
server-side fix that unblocks the tool this release advertised.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- **`account_usage` no longer tells unlimited-plan customers they have "0 remaining".**
|
|
57
|
+
A service-tier row reports `included_units = -1` (the sentinel for unlimited);
|
|
58
|
+
the tool rendered it literally as *"Credits: 993 used of -1 included — 0 remaining
|
|
59
|
+
(0% used)"* — three mutually contradictory statements about the same quota, and
|
|
60
|
+
the "0 remaining" reading a paid customer as cut off. Unlimited plans now render
|
|
61
|
+
as *"Credits: 993 used · unlimited plan"* with no synthesized percentage.
|
|
62
|
+
NEW-11 from the audit.
|
|
63
|
+
- **Companion:** `ephemeris_house_cusps` accepts the `timezone` companion the
|
|
64
|
+
4.3.0 schema advertised. The Go server rejected the field at the JSON decode
|
|
65
|
+
layer with *"unknown field provided in DateTimeInput"*, so an agent following
|
|
66
|
+
OE's own naive-datetime error message hit a second error. Fixed server-side
|
|
67
|
+
in [`openephemeris/openephemeris#466`](https://github.com/openephemeris/openephemeris/pull/466)
|
|
68
|
+
(no MCP change) — the schema, generated struct, and handler were all correct;
|
|
69
|
+
only the bespoke JSON decoder's allowlist was out of sync. NEW-10 from the
|
|
70
|
+
audit.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **`account_usage` now prints a `Server version` line.** External audits and
|
|
75
|
+
eval runs could not previously attribute a score to a specific published
|
|
76
|
+
build — the version is set in `serverInfo.version` on `initialize` but was
|
|
77
|
+
not surfaced anywhere a human-readable tool response could echo. NEW-9 from
|
|
78
|
+
the audit.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
10
82
|
## [4.3.0] — 2026-07-29
|
|
11
83
|
|
|
12
84
|
The bodygraph iframes get the same view toggle across every mode (natal, transit,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface ResolvedLocation {
|
|
2
|
+
latitude: number;
|
|
3
|
+
longitude: number;
|
|
4
|
+
timezone: string | null;
|
|
5
|
+
displayName: string;
|
|
6
|
+
placeId: string | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a place name to lat/lon/tz. Throws if ambiguous or unresolvable.
|
|
10
|
+
*
|
|
11
|
+
* Ambiguity is detected the same way `location_search` reports it — a
|
|
12
|
+
* count of *rivals* for the top short_name, unless the query itself
|
|
13
|
+
* qualifies the place (e.g. "portland uk", "dallas texas").
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveLocationOrThrow(location: string): Promise<ResolvedLocation>;
|
|
16
|
+
/**
|
|
17
|
+
* Convenience wrapper: fills in missing lat/lon (and optionally timezone)
|
|
18
|
+
* from `location` when the caller left them unset. Returns the effective
|
|
19
|
+
* coordinates + a display-safe location string. Coords passed in win —
|
|
20
|
+
* this only fires when both are unset. If only one of lat/lon is set,
|
|
21
|
+
* throws (partial input is a user error).
|
|
22
|
+
*/
|
|
23
|
+
export declare function coordsFromArgsOrLocation(args: {
|
|
24
|
+
latitude?: unknown;
|
|
25
|
+
longitude?: unknown;
|
|
26
|
+
timezone?: unknown;
|
|
27
|
+
location?: unknown;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
latitude: number | undefined;
|
|
30
|
+
longitude: number | undefined;
|
|
31
|
+
timezone: string | undefined;
|
|
32
|
+
location: string | undefined;
|
|
33
|
+
resolvedFromLocation: boolean;
|
|
34
|
+
}>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Internal location resolver used by `explore_*` tools when the caller
|
|
2
|
+
// supplies a place name (`location`) but omits `latitude`/`longitude`.
|
|
3
|
+
//
|
|
4
|
+
// The point is to put the resolver ON the model's natural path rather than
|
|
5
|
+
// hoping tool-description prose ("call location_search first") convinces
|
|
6
|
+
// the model to detour when it already has plausible coordinates in memory.
|
|
7
|
+
// A live smoke test measured that path at 1-in-8 adoption vs 8-in-8 for
|
|
8
|
+
// the same guidance when the request-shape *forced* the model's hand
|
|
9
|
+
// (naive datetime → 400).
|
|
10
|
+
//
|
|
11
|
+
// Ambiguity handling MUST mirror `location_search` — if the top hit has
|
|
12
|
+
// rival cities of the same short name and the query didn't already qualify
|
|
13
|
+
// it (e.g. "portland uk"), throw rather than silently pick suggestions[0].
|
|
14
|
+
// A quiet default to Portland, OR when the user meant Portland, ME
|
|
15
|
+
// produces a chart nothing downstream can detect as wrong. Same failure
|
|
16
|
+
// class as the naive-UTC ASC and the unlabelled cusp longitudes.
|
|
17
|
+
import { getActiveClient } from "../../backend/client.js";
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a place name to lat/lon/tz. Throws if ambiguous or unresolvable.
|
|
20
|
+
*
|
|
21
|
+
* Ambiguity is detected the same way `location_search` reports it — a
|
|
22
|
+
* count of *rivals* for the top short_name, unless the query itself
|
|
23
|
+
* qualifies the place (e.g. "portland uk", "dallas texas").
|
|
24
|
+
*/
|
|
25
|
+
export async function resolveLocationOrThrow(location) {
|
|
26
|
+
const query = location.trim();
|
|
27
|
+
if (query === "") {
|
|
28
|
+
throw new Error("location is empty");
|
|
29
|
+
}
|
|
30
|
+
const raw = (await getActiveClient().request("GET", "/location/autocomplete", {
|
|
31
|
+
params: { query },
|
|
32
|
+
}));
|
|
33
|
+
const list = Array.isArray(raw?.suggestions) ? raw.suggestions : [];
|
|
34
|
+
if (list.length === 0) {
|
|
35
|
+
throw new Error(`location "${query}" did not match any known place. Call location_search directly to inspect suggestions, ` +
|
|
36
|
+
`or supply latitude/longitude explicitly.`);
|
|
37
|
+
}
|
|
38
|
+
const norm = (v) => String(v ?? "").trim().toLowerCase();
|
|
39
|
+
const top = list[0];
|
|
40
|
+
const rivals = list.filter((s) => norm(s.short_name) === norm(top.short_name));
|
|
41
|
+
const q = norm(query);
|
|
42
|
+
const qTokens = new Set(q.split(/[^a-z0-9]+/).filter(Boolean));
|
|
43
|
+
const qualified = (norm(top.region) !== "" && q.includes(norm(top.region))) ||
|
|
44
|
+
(norm(top.country_code) !== "" && qTokens.has(norm(top.country_code)));
|
|
45
|
+
const ambiguous = rivals.length > 1 && !qualified;
|
|
46
|
+
if (ambiguous) {
|
|
47
|
+
const options = rivals
|
|
48
|
+
.slice(0, 4)
|
|
49
|
+
.map((s) => `${s.display_name ?? s.short_name} (${s.country_code ?? "??"})`)
|
|
50
|
+
.join("; ");
|
|
51
|
+
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.`);
|
|
52
|
+
}
|
|
53
|
+
if (top.latitude == null || top.longitude == null) {
|
|
54
|
+
throw new Error(`location "${query}" matched "${top.display_name ?? top.short_name}" but the record is missing coordinates. ` +
|
|
55
|
+
`Supply latitude/longitude explicitly.`);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
latitude: Number(top.latitude),
|
|
59
|
+
longitude: Number(top.longitude),
|
|
60
|
+
timezone: top.timezone ?? null,
|
|
61
|
+
displayName: String(top.display_name ?? top.short_name ?? query),
|
|
62
|
+
placeId: top.place_id ?? null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Convenience wrapper: fills in missing lat/lon (and optionally timezone)
|
|
67
|
+
* from `location` when the caller left them unset. Returns the effective
|
|
68
|
+
* coordinates + a display-safe location string. Coords passed in win —
|
|
69
|
+
* this only fires when both are unset. If only one of lat/lon is set,
|
|
70
|
+
* throws (partial input is a user error).
|
|
71
|
+
*/
|
|
72
|
+
export async function coordsFromArgsOrLocation(args) {
|
|
73
|
+
const lat = args.latitude;
|
|
74
|
+
const lon = args.longitude;
|
|
75
|
+
const tz = args.timezone;
|
|
76
|
+
const loc = args.location;
|
|
77
|
+
const latSet = lat != null;
|
|
78
|
+
const lonSet = lon != null;
|
|
79
|
+
if (latSet && lonSet) {
|
|
80
|
+
return { latitude: lat, longitude: lon, timezone: tz, location: loc, resolvedFromLocation: false };
|
|
81
|
+
}
|
|
82
|
+
if (latSet !== lonSet) {
|
|
83
|
+
throw new Error("latitude and longitude must both be supplied (or both omitted with a `location` name).");
|
|
84
|
+
}
|
|
85
|
+
if (!loc || String(loc).trim() === "") {
|
|
86
|
+
// Neither coords nor location — leave undefined; the tool's own
|
|
87
|
+
// validation decides whether that's acceptable (some tools, like
|
|
88
|
+
// explore_human_design, historically defaulted to 0°N 0°E, which
|
|
89
|
+
// is a silent-wrong we want to end).
|
|
90
|
+
return { latitude: undefined, longitude: undefined, timezone: tz, location: loc, resolvedFromLocation: false };
|
|
91
|
+
}
|
|
92
|
+
const resolved = await resolveLocationOrThrow(String(loc));
|
|
93
|
+
return {
|
|
94
|
+
latitude: resolved.latitude,
|
|
95
|
+
longitude: resolved.longitude,
|
|
96
|
+
timezone: tz ?? resolved.timezone ?? undefined,
|
|
97
|
+
location: resolved.displayName,
|
|
98
|
+
resolvedFromLocation: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -27,6 +27,7 @@ import { registerTool, SERVER_VERSION } from "../index.js";
|
|
|
27
27
|
import { getActiveClient } from "../../backend/client.js";
|
|
28
28
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
29
29
|
import { DATETIME_DESC, timezoneProperty } from "../datetime.js";
|
|
30
|
+
import { coordsFromArgsOrLocation } from "./_location-resolver.js";
|
|
30
31
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
31
32
|
export const BI_WHEEL_RESOURCE_URI = "ui://openephemeris/bi-wheel";
|
|
32
33
|
export const BI_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -430,8 +431,8 @@ registerTool({
|
|
|
430
431
|
type: "string",
|
|
431
432
|
description: "Birth datetime for Person 1 / Natal chart. " + DATETIME_DESC,
|
|
432
433
|
},
|
|
433
|
-
person1_latitude: { type: "number", description: "Birth latitude for Person 1 (decimal degrees, positive = North).
|
|
434
|
-
person1_longitude: { type: "number", description: "Birth longitude for Person 1 (decimal degrees, positive = East)." },
|
|
434
|
+
person1_latitude: { type: "number", description: "Birth latitude for Person 1 (decimal degrees, positive = North). Optional if `location` is a place name — the resolver fills it in. Never recall coordinates from memory." },
|
|
435
|
+
person1_longitude: { type: "number", description: "Birth longitude for Person 1 (decimal degrees, positive = East). Optional if `location` is a place name." },
|
|
435
436
|
person1_timezone: timezoneProperty("Person 1's birth location", "America/New_York"),
|
|
436
437
|
person1_name: {
|
|
437
438
|
type: "string",
|
|
@@ -452,7 +453,7 @@ registerTool({
|
|
|
452
453
|
},
|
|
453
454
|
location: {
|
|
454
455
|
type: "string",
|
|
455
|
-
description: "
|
|
456
|
+
description: "Person 1 / Natal location. Prefer a plain place name like 'New York, NY' — the server resolves it via the same lookup `location_search` uses (unambiguous names → coords + timezone; ambiguous names throw with a disambiguation hint). Supplied person1_latitude/person1_longitude always win.",
|
|
456
457
|
},
|
|
457
458
|
mode: {
|
|
458
459
|
type: "string",
|
|
@@ -460,8 +461,7 @@ registerTool({
|
|
|
460
461
|
description: "Chart comparison mode. Defaults to 'synastry'.",
|
|
461
462
|
},
|
|
462
463
|
},
|
|
463
|
-
required: ["person1_datetime", "person2_datetime",
|
|
464
|
-
"person1_latitude", "person1_longitude"],
|
|
464
|
+
required: ["person1_datetime", "person2_datetime"],
|
|
465
465
|
},
|
|
466
466
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
467
467
|
annotations: {
|
|
@@ -480,15 +480,24 @@ registerTool({
|
|
|
480
480
|
handler: async (args) => {
|
|
481
481
|
const client = getActiveClient();
|
|
482
482
|
const mode = args.mode ?? "synastry";
|
|
483
|
+
const resolved1 = await coordsFromArgsOrLocation({
|
|
484
|
+
latitude: args.person1_latitude,
|
|
485
|
+
longitude: args.person1_longitude,
|
|
486
|
+
timezone: args.person1_timezone,
|
|
487
|
+
location: args.location,
|
|
488
|
+
});
|
|
489
|
+
if (resolved1.latitude == null || resolved1.longitude == null) {
|
|
490
|
+
throw new Error("explore_bi_wheel requires either `person1_latitude` + `person1_longitude` or a resolvable `location` name.");
|
|
491
|
+
}
|
|
483
492
|
const dt1 = String(args.person1_datetime);
|
|
484
|
-
const lat1 =
|
|
485
|
-
const lon1 =
|
|
486
|
-
const tz1 = args.person1_timezone;
|
|
493
|
+
const lat1 = resolved1.latitude;
|
|
494
|
+
const lon1 = resolved1.longitude;
|
|
495
|
+
const tz1 = args.person1_timezone ?? resolved1.timezone;
|
|
487
496
|
const dt2 = String(args.person2_datetime);
|
|
488
497
|
const lat2 = args.person2_latitude;
|
|
489
498
|
const lon2 = args.person2_longitude;
|
|
490
499
|
const tz2 = args.person2_timezone;
|
|
491
|
-
const loc1 = String(args.person1_name ?? args.location ?? `${lat1
|
|
500
|
+
const loc1 = String(args.person1_name ?? resolved1.location ?? args.location ?? `${lat1},${lon1}`);
|
|
492
501
|
const loc2 = MODE_META[mode].outerLabel(dt2, args.person2_name);
|
|
493
502
|
// Fetch inner (natal) and outer (mode-dependent) charts in parallel where possible
|
|
494
503
|
const [innerData, outerData] = await Promise.all([
|
|
@@ -26,6 +26,7 @@ import { registerTool, SERVER_VERSION } from "../index.js";
|
|
|
26
26
|
import { getActiveClient } from "../../backend/client.js";
|
|
27
27
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
28
28
|
import { localToUtcIso } from "../datetime.js";
|
|
29
|
+
import { coordsFromArgsOrLocation } from "./_location-resolver.js";
|
|
29
30
|
// ── Constants ─────────────────────────────────────────────────────────────
|
|
30
31
|
export const BODYGRAPH_RESOURCE_URI = "ui://openephemeris/bodygraph";
|
|
31
32
|
// Appended to every model-visible HD tool description — trademark hygiene.
|
|
@@ -326,15 +327,15 @@ registerTool({
|
|
|
326
327
|
},
|
|
327
328
|
latitude: {
|
|
328
329
|
type: "number",
|
|
329
|
-
description: "Birth latitude in decimal degrees (positive = North). Optional
|
|
330
|
+
description: "Birth latitude in decimal degrees (positive = North). Optional if `location` is a place name — the resolver fills it in. Never recall coordinates from memory; either supply a place name or resolve with location_search first.",
|
|
330
331
|
},
|
|
331
332
|
longitude: {
|
|
332
333
|
type: "number",
|
|
333
|
-
description: "Birth longitude in decimal degrees (positive = East). Optional
|
|
334
|
+
description: "Birth longitude in decimal degrees (positive = East). Optional if `location` is a place name.",
|
|
334
335
|
},
|
|
335
336
|
location: {
|
|
336
337
|
type: "string",
|
|
337
|
-
description: "
|
|
338
|
+
description: "Birth location. Prefer a plain place name like 'New York, NY' — the server resolves it via the same lookup `location_search` uses (unambiguous names → coords + timezone; ambiguous names throw with a disambiguation hint). If neither location nor lat/lon is supplied the call is rejected — no more silent 0°N 0°E charts.",
|
|
338
339
|
},
|
|
339
340
|
timezone: {
|
|
340
341
|
type: "string",
|
|
@@ -361,18 +362,29 @@ registerTool({
|
|
|
361
362
|
},
|
|
362
363
|
handler: async (args) => {
|
|
363
364
|
const client = getActiveClient();
|
|
364
|
-
|
|
365
|
+
// Resolve `location` → lat/lon/tz when the caller left coords unset.
|
|
366
|
+
// If neither is provided we throw rather than silently computing at
|
|
367
|
+
// 0°N 0°E (a plausible-looking chart nothing downstream can detect).
|
|
368
|
+
const resolved = await coordsFromArgsOrLocation({
|
|
369
|
+
latitude: args.latitude,
|
|
370
|
+
longitude: args.longitude,
|
|
371
|
+
timezone: args.timezone,
|
|
372
|
+
location: args.location,
|
|
373
|
+
});
|
|
374
|
+
const timezone = args.timezone ?? resolved.timezone;
|
|
365
375
|
const datetime = localToUtcIso("datetime", String(args.datetime), timezone);
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
|
|
376
|
+
const lat = resolved.latitude;
|
|
377
|
+
const lon = resolved.longitude;
|
|
378
|
+
if (lat == null || lon == null) {
|
|
379
|
+
throw new Error("explore_human_design requires either `latitude` + `longitude` or a resolvable `location` name. " +
|
|
380
|
+
"Human Design is sensitive to birth location; a chart at 0°N 0°E is silently wrong.");
|
|
381
|
+
}
|
|
382
|
+
const location = String(resolved.location ?? args.location ?? `${lat}, ${lon}`).slice(0, 120);
|
|
369
383
|
const body = {
|
|
370
384
|
birth_datetime_utc: datetime,
|
|
371
385
|
};
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (lon != null)
|
|
375
|
-
body.longitude = lon;
|
|
386
|
+
body.latitude = lat;
|
|
387
|
+
body.longitude = lon;
|
|
376
388
|
const bundleAvailable = Boolean(getBodygraphBundle());
|
|
377
389
|
// Fetch chart data and Go-rendered SVG in parallel. SVG only fetched when
|
|
378
390
|
// the iframe bundle is available (text-fallback hosts have no use for it,
|
|
@@ -390,7 +402,7 @@ registerTool({
|
|
|
390
402
|
// Build payload once — summary and model data both derive from the same object
|
|
391
403
|
const modelPayload = buildHdModelPayload(chart, {
|
|
392
404
|
datetime,
|
|
393
|
-
location: args.location ?? null,
|
|
405
|
+
location: resolved.location ?? args.location ?? null,
|
|
394
406
|
timezone: timezone ?? null,
|
|
395
407
|
latitude: lat ?? null,
|
|
396
408
|
longitude: lon ?? null,
|
|
@@ -22,6 +22,7 @@ import { registerTool, SERVER_VERSION } from "../index.js";
|
|
|
22
22
|
import { getActiveClient } from "../../backend/client.js";
|
|
23
23
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
24
24
|
import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
|
|
25
|
+
import { coordsFromArgsOrLocation } from "./_location-resolver.js";
|
|
25
26
|
// ── Constants ─────────────────────────────────────────────────────────────
|
|
26
27
|
export const CHART_WHEEL_RESOURCE_URI = "ui://openephemeris/chart-wheel";
|
|
27
28
|
export const CHART_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -125,15 +126,15 @@ registerTool({
|
|
|
125
126
|
timezone: TIMEZONE_PROPERTY,
|
|
126
127
|
latitude: {
|
|
127
128
|
type: "number",
|
|
128
|
-
description: "Birth latitude in decimal degrees (positive = North).
|
|
129
|
+
description: "Birth latitude in decimal degrees (positive = North). Optional if `location` is a place name — the resolver fills it in. If you supply lat/lon directly, resolve them with location_search first; never recall coordinates from memory.",
|
|
129
130
|
},
|
|
130
131
|
longitude: {
|
|
131
132
|
type: "number",
|
|
132
|
-
description: "Birth longitude in decimal degrees (positive = East).",
|
|
133
|
+
description: "Birth longitude in decimal degrees (positive = East). Optional if `location` is a place name.",
|
|
133
134
|
},
|
|
134
135
|
location: {
|
|
135
136
|
type: "string",
|
|
136
|
-
description: "
|
|
137
|
+
description: "Birth location. Prefer a plain place name like 'New York, NY' — the server resolves it via the same lookup `location_search` uses (unambiguous names → coords + timezone; ambiguous names throw with a disambiguation hint). If you already have coords, either omit this or use it as a display caption; supplied coords always win.",
|
|
137
138
|
},
|
|
138
139
|
house_system: {
|
|
139
140
|
type: "string",
|
|
@@ -149,7 +150,7 @@ registerTool({
|
|
|
149
150
|
"Example: ['sun','moon','lilith','vertex'].",
|
|
150
151
|
},
|
|
151
152
|
},
|
|
152
|
-
required: ["datetime"
|
|
153
|
+
required: ["datetime"],
|
|
153
154
|
},
|
|
154
155
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
155
156
|
annotations: {
|
|
@@ -169,9 +170,23 @@ registerTool({
|
|
|
169
170
|
const client = getActiveClient();
|
|
170
171
|
const houseSystem = args.house_system ?? "placidus";
|
|
171
172
|
const datetime = String(args.datetime);
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
const resolved = await coordsFromArgsOrLocation({
|
|
174
|
+
latitude: args.latitude,
|
|
175
|
+
longitude: args.longitude,
|
|
176
|
+
timezone: args.timezone,
|
|
177
|
+
location: args.location,
|
|
178
|
+
});
|
|
179
|
+
const lat = resolved.latitude;
|
|
180
|
+
const lon = resolved.longitude;
|
|
181
|
+
if (lat == null || lon == null) {
|
|
182
|
+
throw new Error("explore_natal_chart requires either `latitude` + `longitude` or a resolvable `location` name. " +
|
|
183
|
+
"The server tried to compute a chart with no coordinates and refused — a chart at 0°N 0°E is silently wrong.");
|
|
184
|
+
}
|
|
185
|
+
// A location resolved from the place name carries its own tz. The
|
|
186
|
+
// caller's explicit timezone still wins when supplied.
|
|
187
|
+
const effectiveTimezone = args.timezone ?? resolved.timezone;
|
|
188
|
+
const effectiveLocation = resolved.location ?? args.location;
|
|
189
|
+
const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, effectiveTimezone);
|
|
175
190
|
// Determine which bodies are requested
|
|
176
191
|
const requestedBodies = args.bodies;
|
|
177
192
|
const wantsAll = requestedBodies?.some(b => b.toLowerCase() === "all");
|
|
@@ -188,14 +203,14 @@ registerTool({
|
|
|
188
203
|
// source of the ~90 second blocking delay.
|
|
189
204
|
const chartData = await client.post("/ephemeris/natal-chart", natalBody);
|
|
190
205
|
// Build human-readable summary for the LLM context
|
|
191
|
-
const summary = buildChartSummary(chartData, String(
|
|
206
|
+
const summary = buildChartSummary(chartData, String(effectiveLocation ?? `${lat}, ${lon}`), houseSystem);
|
|
192
207
|
// Build the UI payload (planets normalised to array, server aspects mapped)
|
|
193
208
|
const modelPayload = buildModelPayload(chartData, {
|
|
194
209
|
datetime,
|
|
195
|
-
timezone:
|
|
210
|
+
timezone: effectiveTimezone ?? null,
|
|
196
211
|
latitude: lat ?? null,
|
|
197
212
|
longitude: lon ?? null,
|
|
198
|
-
location:
|
|
213
|
+
location: effectiveLocation ?? null,
|
|
199
214
|
}, houseSystem, undefined, requestedBodies);
|
|
200
215
|
const bundleAvailable = Boolean(getChartWheelBundle());
|
|
201
216
|
if (bundleAvailable) {
|
|
@@ -24,6 +24,7 @@ import { registerTool, validateRequired, validateCoordinates, SERVER_VERSION } f
|
|
|
24
24
|
import { getActiveClient } from "../../backend/client.js";
|
|
25
25
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
26
26
|
import { localToUtcIso } from "../datetime.js";
|
|
27
|
+
import { coordsFromArgsOrLocation } from "./_location-resolver.js";
|
|
27
28
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
28
29
|
export const VEDIC_CHART_RESOURCE_URI = "ui://openephemeris/vedic-chart";
|
|
29
30
|
export const VEDIC_CHART_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -112,15 +113,15 @@ registerTool({
|
|
|
112
113
|
},
|
|
113
114
|
latitude: {
|
|
114
115
|
type: "number",
|
|
115
|
-
description: "Birth latitude in decimal degrees (positive = North).
|
|
116
|
+
description: "Birth latitude in decimal degrees (positive = North). Optional if `location` is a place name — the resolver fills it in. Never recall coordinates from memory; either supply a place name or resolve with location_search first.",
|
|
116
117
|
},
|
|
117
118
|
longitude: {
|
|
118
119
|
type: "number",
|
|
119
|
-
description: "Birth longitude in decimal degrees (positive = East).",
|
|
120
|
+
description: "Birth longitude in decimal degrees (positive = East). Optional if `location` is a place name.",
|
|
120
121
|
},
|
|
121
122
|
location: {
|
|
122
123
|
type: "string",
|
|
123
|
-
description: "
|
|
124
|
+
description: "Birth location. Prefer a plain place name like 'Mumbai, India' — the server resolves it via the same lookup `location_search` uses (unambiguous names → coords + timezone; ambiguous names throw with a disambiguation hint). Supplied coords always win.",
|
|
124
125
|
},
|
|
125
126
|
timezone: {
|
|
126
127
|
type: "string",
|
|
@@ -133,7 +134,7 @@ registerTool({
|
|
|
133
134
|
description: "Ayanamsa system for sidereal conversion. Defaults to 'lahiri'.",
|
|
134
135
|
},
|
|
135
136
|
},
|
|
136
|
-
required: ["datetime"
|
|
137
|
+
required: ["datetime"],
|
|
137
138
|
},
|
|
138
139
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
139
140
|
annotations: {
|
|
@@ -150,15 +151,25 @@ registerTool({
|
|
|
150
151
|
},
|
|
151
152
|
},
|
|
152
153
|
handler: async (args) => {
|
|
153
|
-
validateRequired(args, ["datetime"
|
|
154
|
-
|
|
154
|
+
validateRequired(args, ["datetime"]);
|
|
155
|
+
const resolved = await coordsFromArgsOrLocation({
|
|
156
|
+
latitude: args.latitude,
|
|
157
|
+
longitude: args.longitude,
|
|
158
|
+
timezone: args.timezone,
|
|
159
|
+
location: args.location,
|
|
160
|
+
});
|
|
161
|
+
if (resolved.latitude == null || resolved.longitude == null) {
|
|
162
|
+
throw new Error("explore_vedic_chart requires either `latitude` + `longitude` or a resolvable `location` name.");
|
|
163
|
+
}
|
|
164
|
+
const argsWithResolved = { ...args, latitude: resolved.latitude, longitude: resolved.longitude };
|
|
165
|
+
validateCoordinates(argsWithResolved, "latitude", "longitude");
|
|
155
166
|
const client = getActiveClient();
|
|
156
|
-
const timezone = args.timezone;
|
|
167
|
+
const timezone = args.timezone ?? resolved.timezone;
|
|
157
168
|
const datetime = localToUtcIso("datetime", String(args.datetime), timezone);
|
|
158
|
-
const lat = Number(
|
|
159
|
-
const lon = Number(
|
|
169
|
+
const lat = Number(resolved.latitude);
|
|
170
|
+
const lon = Number(resolved.longitude);
|
|
160
171
|
const ayanamsa = args.ayanamsa;
|
|
161
|
-
const location = String(args.location ?? `${lat}, ${lon}`).slice(0, 120);
|
|
172
|
+
const location = String(resolved.location ?? args.location ?? `${lat}, ${lon}`).slice(0, 120);
|
|
162
173
|
const bundleAvailable = Boolean(getVedicChartBundle());
|
|
163
174
|
const theme = "dark"; // app shell default; iframe reconciles to host theme on load
|
|
164
175
|
const body = {
|
|
@@ -175,7 +186,7 @@ registerTool({
|
|
|
175
186
|
const chartData = await client.request("POST", "/vedic/chart", { data: body });
|
|
176
187
|
const modelPayload = buildVedicModelPayload(chartData, {
|
|
177
188
|
datetime,
|
|
178
|
-
location: args.location ?? null,
|
|
189
|
+
location: resolved.location ?? args.location ?? null,
|
|
179
190
|
timezone: timezone ?? null,
|
|
180
191
|
latitude: lat,
|
|
181
192
|
longitude: lon,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { registerTool } from "../index.js";
|
|
1
|
+
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
2
2
|
import { getActiveClient, BackendError, DASHBOARD_ACCOUNT_URL, LOGIN_SIGNUP_URL, UPGRADE_URL, WALLET_TOPUP_URL, } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
4
|
// Tiers where the primary next step is a wallet top-up rather than a plan change.
|
|
@@ -63,6 +63,10 @@ registerTool({
|
|
|
63
63
|
const period = row.month || args?.month || new Date().toISOString().substring(0, 7);
|
|
64
64
|
const used = row.total_units ?? 0;
|
|
65
65
|
const included = row.included_units ?? 0;
|
|
66
|
+
// `included_units < 0` is a sentinel for unlimited (service/enterprise plans).
|
|
67
|
+
// Rendering it literally as "0 remaining (0% used)" tells a paid customer they
|
|
68
|
+
// are cut off — the exact opposite of the truth. NEW-11 from the audit.
|
|
69
|
+
const unlimited = included < 0;
|
|
66
70
|
const remaining = Math.max(included - used, 0);
|
|
67
71
|
const percent = row.percent_units_used ?? (included > 0 ? Math.round((used / included) * 1000) / 10 : 0);
|
|
68
72
|
const totalCalls = row.total_calls ?? 0;
|
|
@@ -71,7 +75,9 @@ registerTool({
|
|
|
71
75
|
``,
|
|
72
76
|
`- **Plan:** ${tier}`,
|
|
73
77
|
`- **Period:** ${period}`,
|
|
74
|
-
|
|
78
|
+
unlimited
|
|
79
|
+
? `- **Credits:** ${used.toLocaleString()} used · unlimited plan`
|
|
80
|
+
: `- **Credits:** ${used.toLocaleString()} used of ${included.toLocaleString()} included — **${remaining.toLocaleString()} remaining** (${percent}% used)`,
|
|
75
81
|
`- **Total API calls:** ${totalCalls.toLocaleString()}`,
|
|
76
82
|
];
|
|
77
83
|
if ((row.overage_units ?? 0) > 0) {
|
|
@@ -86,6 +92,10 @@ registerTool({
|
|
|
86
92
|
lines.push(`- **Renews:** ${renewal.toISOString().substring(0, 10)}`);
|
|
87
93
|
}
|
|
88
94
|
}
|
|
95
|
+
// Verifiable build identifier so external evals can attribute every score to
|
|
96
|
+
// a specific published version rather than trusting the tester's word.
|
|
97
|
+
// NEW-9 from the audit.
|
|
98
|
+
lines.push(`- **Server version:** ${SERVER_VERSION}`);
|
|
89
99
|
lines.push(``);
|
|
90
100
|
if (WALLET_TIERS.has(tier)) {
|
|
91
101
|
lines.push(`**Need more credits?** Top up your wallet at ${WALLET_TOPUP_URL} (from $5), ` +
|
|
@@ -47,11 +47,11 @@ registerTool({
|
|
|
47
47
|
},
|
|
48
48
|
include_arabic_parts: {
|
|
49
49
|
type: "boolean",
|
|
50
|
-
description: "
|
|
50
|
+
description: "Include Hermetic Lots / Arabic Parts in the natal response (routed as `options.include_hermetic_lots: true`). For a standalone lots-only payload without a full natal chart, use `/ephemeris/hermetic-lots` instead.",
|
|
51
51
|
},
|
|
52
52
|
include_fixed_stars: {
|
|
53
53
|
type: "boolean",
|
|
54
|
-
description: "
|
|
54
|
+
description: "Include fixed-star positions in the natal response (routed as `configuration.fixed_star_options.include: true`). For a standalone fixed-stars payload without a full natal chart, use `/ephemeris/fixed-stars` instead.",
|
|
55
55
|
},
|
|
56
56
|
include_visual: {
|
|
57
57
|
type: "boolean",
|