@openephemeris/mcp-server 4.5.0 → 4.6.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
CHANGED
|
@@ -7,6 +7,72 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [4.6.0] — 2026-07-30
|
|
11
|
+
|
|
12
|
+
Fixes the tool that could not answer the question it is named for, closes the
|
|
13
|
+
last two places a chart was silently computed at 0°N 0°E, and stops two tools
|
|
14
|
+
discarding a timezone the caller supplied. The first three come from a 75-run
|
|
15
|
+
behavioural eval (25 prompts × 3, fresh context each) run against 4.4.0; the
|
|
16
|
+
last from the directory-submission readiness review.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **`ephemeris_next_eclipse` can now find the next eclipse.** Without a
|
|
21
|
+
location it routed to `/eclipse/solar/global` or `/eclipse/lunar/global`,
|
|
22
|
+
which classify whatever eclipse is occurring *at* a given instant rather
|
|
23
|
+
than searching forward from one. The consequences compounded: `after_date`
|
|
24
|
+
was documented as optional and defaulting to today, but the backend
|
|
25
|
+
rejected the call without it; an ordinary date returned *"no solar eclipse
|
|
26
|
+
occurs at the requested date"*; and `eclipse_type: 'any'` was accepted and
|
|
27
|
+
ignored. Asking *"when is the next solar eclipse"* was unanswerable.
|
|
28
|
+
|
|
29
|
+
It now calls a new `/eclipse/next` endpoint that genuinely searches forward
|
|
30
|
+
from `after_date` (defaulting to now), honours `any` by returning whichever
|
|
31
|
+
of solar/lunar comes first, and needs no arguments beyond the type. Server
|
|
32
|
+
side in [`openephemeris#473`](https://github.com/openephemeris/openephemeris/pull/473).
|
|
33
|
+
|
|
34
|
+
Worth stating plainly, because it is the reason this is the lede: a broken
|
|
35
|
+
tool did not surface an error to the end user. Across three runs of the
|
|
36
|
+
same prompt the model answered from its own memory twice — once correctly,
|
|
37
|
+
once naming an eclipse in the wrong year — and disagreed with itself. Only
|
|
38
|
+
one run in three reported that the tool had failed.
|
|
39
|
+
|
|
40
|
+
- **`explore_human_design_transit` and `explore_human_design_connection` no
|
|
41
|
+
longer silently compute at 0°N 0°E.** 4.4.0 fixed this for
|
|
42
|
+
`explore_human_design` and wired `location` into four chart tools; these
|
|
43
|
+
two were missed. `explore_human_design_transit` accepted a `location` that
|
|
44
|
+
was display-only — its own description conceded the chart would be cast on
|
|
45
|
+
the Gulf of Guinea — and `explore_human_design_connection` had no location
|
|
46
|
+
field at all for either person. Both now resolve a place name through the
|
|
47
|
+
same lookup `location_search` uses, and reject the call outright when
|
|
48
|
+
neither coordinates nor a resolvable location is supplied.
|
|
49
|
+
|
|
50
|
+
- **`ephemeris_natal_batch` and `explore_transit_timeline` no longer discard a
|
|
51
|
+
supplied timezone.** Both declared a timezone parameter and then built the
|
|
52
|
+
request body without it, so a naive local birth time plus an IANA zone — the
|
|
53
|
+
remedy the server's own error message recommends — was rejected by the
|
|
54
|
+
datetime contract. `ephemeris_natal_batch` failed this way for *every*
|
|
55
|
+
subject in a batch, and its documented example was the failing shape;
|
|
56
|
+
`explore_transit_timeline` never read `natal_timezone` at all. Both now route
|
|
57
|
+
the value through the same canonical helper the rest of the surface uses, so
|
|
58
|
+
the zone reaches the wire.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **Two server-side fixes change what these tools return** (no MCP change
|
|
63
|
+
required, listed because the output differs):
|
|
64
|
+
`ephemeris_natal_chart` with `format: 'llm'` now distinguishes the mean and
|
|
65
|
+
true lunar nodes — both previously carried the id `north_node`, so a
|
|
66
|
+
consumer keying by id silently dropped one and the `aspects` indices
|
|
67
|
+
inherited the ambiguity ([`#475`](https://github.com/openephemeris/openephemeris/pull/475)).
|
|
68
|
+
And `server_version` in calculation metadata is now derived from the
|
|
69
|
+
deployed image reference rather than Fly's per-machine version counter, so
|
|
70
|
+
every instance serving a given release reports the same value; a separate
|
|
71
|
+
`instance_id` carries per-machine attribution
|
|
72
|
+
([`#474`](https://github.com/openephemeris/openephemeris/pull/474)).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
10
76
|
## [4.5.0] — 2026-07-30
|
|
11
77
|
|
|
12
78
|
Adds a tool for the question the astrocartography tools could not answer:
|
|
@@ -1319,9 +1319,9 @@ registerTool({
|
|
|
1319
1319
|
description: "Natal birth datetime, ISO 8601 (e.g. '1990-04-15T19:30:00Z'). Include 'Z' or an offset, " +
|
|
1320
1320
|
"or supply timezone for local time. HD is time-sensitive to the minute.",
|
|
1321
1321
|
},
|
|
1322
|
-
latitude: { type: "number", description: "Natal birth latitude
|
|
1323
|
-
longitude: { type: "number", description: "Natal birth longitude
|
|
1324
|
-
location: { type: "string", description: "Natal location
|
|
1322
|
+
latitude: { type: "number", description: "Natal birth latitude in decimal degrees (positive = North). Optional if `location` is a place name — the resolver fills it in." },
|
|
1323
|
+
longitude: { type: "number", description: "Natal birth longitude in decimal degrees (positive = East). Optional if `location` is a place name." },
|
|
1324
|
+
location: { type: "string", description: "Natal 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 silent 0°N 0°E charts." },
|
|
1325
1325
|
timezone: { type: "string", description: "IANA timezone for the birth location (e.g. 'America/New_York')." },
|
|
1326
1326
|
transit_datetime: {
|
|
1327
1327
|
type: "string",
|
|
@@ -1355,16 +1355,30 @@ registerTool({
|
|
|
1355
1355
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["model", "app"] } },
|
|
1356
1356
|
handler: async (args) => {
|
|
1357
1357
|
const client = getActiveClient();
|
|
1358
|
-
|
|
1358
|
+
// Resolve `location` → lat/lon/tz when the caller left coords unset.
|
|
1359
|
+
// If neither is provided we throw rather than silently computing at
|
|
1360
|
+
// 0°N 0°E (a plausible-looking chart nothing downstream can detect).
|
|
1361
|
+
const resolved = await coordsFromArgsOrLocation({
|
|
1362
|
+
latitude: args.latitude,
|
|
1363
|
+
longitude: args.longitude,
|
|
1364
|
+
timezone: args.timezone,
|
|
1365
|
+
location: args.location,
|
|
1366
|
+
});
|
|
1367
|
+
const timezone = args.timezone ?? resolved.timezone;
|
|
1359
1368
|
const natalIso = localToUtcIso("datetime", String(args.datetime), timezone);
|
|
1360
|
-
const lat =
|
|
1361
|
-
const lon =
|
|
1369
|
+
const lat = resolved.latitude;
|
|
1370
|
+
const lon = resolved.longitude;
|
|
1371
|
+
if (lat == null || lon == null) {
|
|
1372
|
+
throw new Error("explore_human_design_transit requires either `latitude` + `longitude` or a resolvable `location` name. " +
|
|
1373
|
+
"Human Design is sensitive to birth location; a chart at 0°N 0°E is silently wrong.");
|
|
1374
|
+
}
|
|
1375
|
+
const location = String(resolved.location ?? args.location ?? "Subject").slice(0, 120);
|
|
1362
1376
|
// SubjectRequest wire shape: name (required), birth_datetime.iso, and
|
|
1363
1377
|
// birth_location with struct-typed coordinate/timezone inputs (matches the
|
|
1364
1378
|
// live bi-wheel/transit-timeline tools — bare scalars are rejected).
|
|
1365
1379
|
const body = {
|
|
1366
1380
|
subject: {
|
|
1367
|
-
name:
|
|
1381
|
+
name: location,
|
|
1368
1382
|
birth_datetime: { iso: natalIso },
|
|
1369
1383
|
birth_location: {
|
|
1370
1384
|
latitude: { decimal: lat },
|
|
@@ -1441,8 +1455,9 @@ registerTool({
|
|
|
1441
1455
|
description: "First person's birth data.",
|
|
1442
1456
|
properties: {
|
|
1443
1457
|
datetime: { type: "string", description: "Birth datetime, ISO 8601 (Z/offset, or local with timezone)." },
|
|
1444
|
-
latitude: { type: "number", description: "Birth latitude
|
|
1445
|
-
longitude: { type: "number", description: "Birth longitude
|
|
1458
|
+
latitude: { type: "number", description: "Birth latitude, decimal degrees (+N). Optional if `location` is set." },
|
|
1459
|
+
longitude: { type: "number", description: "Birth longitude, decimal degrees (+E). Optional if `location` is set." },
|
|
1460
|
+
location: { type: "string", description: "Birth location, e.g. 'New York, NY' — resolved like location_search (ambiguous names throw). Omitting both this and lat/lon rejects the call rather than defaulting to 0°N 0°E." },
|
|
1446
1461
|
timezone: { type: "string", description: "IANA timezone (e.g. 'America/New_York')." },
|
|
1447
1462
|
},
|
|
1448
1463
|
required: ["datetime"],
|
|
@@ -1452,8 +1467,9 @@ registerTool({
|
|
|
1452
1467
|
description: "Second person's birth data.",
|
|
1453
1468
|
properties: {
|
|
1454
1469
|
datetime: { type: "string", description: "Birth datetime, ISO 8601 (Z/offset, or local with timezone)." },
|
|
1455
|
-
latitude: { type: "number", description: "Birth latitude
|
|
1456
|
-
longitude: { type: "number", description: "Birth longitude
|
|
1470
|
+
latitude: { type: "number", description: "Birth latitude, decimal degrees (+N). Optional if `location` is set." },
|
|
1471
|
+
longitude: { type: "number", description: "Birth longitude, decimal degrees (+E). Optional if `location` is set." },
|
|
1472
|
+
location: { type: "string", description: "Birth location, e.g. 'New York, NY' — resolved like location_search (ambiguous names throw). Omitting both this and lat/lon rejects the call rather than defaulting to 0°N 0°E." },
|
|
1457
1473
|
timezone: { type: "string", description: "IANA timezone (e.g. 'America/New_York')." },
|
|
1458
1474
|
},
|
|
1459
1475
|
required: ["datetime"],
|
|
@@ -1486,17 +1502,32 @@ registerTool({
|
|
|
1486
1502
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["model", "app"] } },
|
|
1487
1503
|
handler: async (args) => {
|
|
1488
1504
|
const client = getActiveClient();
|
|
1489
|
-
|
|
1490
|
-
|
|
1505
|
+
// Resolve each person's `location` → lat/lon/tz when coords are unset.
|
|
1506
|
+
// If neither is provided we throw rather than silently computing at
|
|
1507
|
+
// 0°N 0°E (a plausible-looking chart nothing downstream can detect).
|
|
1508
|
+
const toSubject = async (p, label) => {
|
|
1509
|
+
const resolved = await coordsFromArgsOrLocation({
|
|
1510
|
+
latitude: p?.latitude,
|
|
1511
|
+
longitude: p?.longitude,
|
|
1512
|
+
timezone: p?.timezone,
|
|
1513
|
+
location: p?.location,
|
|
1514
|
+
});
|
|
1515
|
+
const tz = p?.timezone ?? resolved.timezone;
|
|
1516
|
+
const lat = resolved.latitude;
|
|
1517
|
+
const lon = resolved.longitude;
|
|
1518
|
+
if (lat == null || lon == null) {
|
|
1519
|
+
throw new Error(`${label} requires either \`latitude\` + \`longitude\` or a resolvable \`location\` name. ` +
|
|
1520
|
+
"Human Design is sensitive to birth location; a chart at 0°N 0°E is silently wrong.");
|
|
1521
|
+
}
|
|
1491
1522
|
return {
|
|
1492
1523
|
birth_datetime_utc: localToUtcIso(`${label}.datetime`, String(p?.datetime), tz, `${label}.timezone`),
|
|
1493
|
-
latitude:
|
|
1494
|
-
longitude:
|
|
1524
|
+
latitude: lat,
|
|
1525
|
+
longitude: lon,
|
|
1495
1526
|
};
|
|
1496
1527
|
};
|
|
1497
1528
|
const body = {
|
|
1498
|
-
subject_1: toSubject(args.person_a, "person_a"),
|
|
1499
|
-
subject_2: toSubject(args.person_b, "person_b"),
|
|
1529
|
+
subject_1: await toSubject(args.person_a, "person_a"),
|
|
1530
|
+
subject_2: await toSubject(args.person_b, "person_b"),
|
|
1500
1531
|
};
|
|
1501
1532
|
const bundleAvailable = Boolean(getBodygraphBundle());
|
|
1502
1533
|
// Mirror the transit tool: explicit dark default, host-theme refetch.
|
|
@@ -18,7 +18,7 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
import { registerTool, validateRequired, SERVER_VERSION } from "../index.js";
|
|
19
19
|
import { getActiveClient } from "../../backend/client.js";
|
|
20
20
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
21
|
-
import { DATETIME_DESC, WINDOW_DATE_DESC, timezoneProperty } from "../datetime.js";
|
|
21
|
+
import { DATETIME_DESC, WINDOW_DATE_DESC, timezoneProperty, toDateTimeInputBody } from "../datetime.js";
|
|
22
22
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
23
23
|
export const TRANSIT_TIMELINE_RESOURCE_URI = "ui://openephemeris/transit-timeline";
|
|
24
24
|
export const TRANSIT_TIMELINE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -150,10 +150,15 @@ registerTool({
|
|
|
150
150
|
validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"]);
|
|
151
151
|
const client = getActiveClient();
|
|
152
152
|
// ── Step 1: natal chart → real planetary longitudes ──────────────────────
|
|
153
|
+
// natal_timezone is a declared parameter — it must reach the wire body.
|
|
154
|
+
// It previously did not, so a caller following the server's own remedy for
|
|
155
|
+
// a naive datetime ("name the zone in the sibling `timezone` argument")
|
|
156
|
+
// had the zone silently dropped and got a 400 from the Go contract with no
|
|
157
|
+
// working alternative.
|
|
153
158
|
const natalBody = {
|
|
154
159
|
subject: {
|
|
155
160
|
name: "Transit Natal Subject",
|
|
156
|
-
birth_datetime:
|
|
161
|
+
birth_datetime: toDateTimeInputBody("natal_datetime", String(args.natal_datetime), args.natal_timezone, "natal_timezone"),
|
|
157
162
|
birth_location: {
|
|
158
163
|
latitude: { decimal: args.natal_latitude },
|
|
159
164
|
longitude: { decimal: args.natal_longitude },
|
|
@@ -60,12 +60,14 @@ registerTool({
|
|
|
60
60
|
return await getActiveClient().request("GET", "/eclipse/next-visible", { params, timeoutMs: 60_000 });
|
|
61
61
|
}
|
|
62
62
|
else {
|
|
63
|
-
// Global query —
|
|
64
|
-
|
|
63
|
+
// Global query — forward-search for the next eclipse of this type,
|
|
64
|
+
// anywhere on Earth. /eclipse/solar|lunar/global classify whatever
|
|
65
|
+
// eclipse (if any) is occurring exactly at a given date, so they
|
|
66
|
+
// can't answer "when is the next one" — /eclipse/next can.
|
|
67
|
+
const params = { eclipse_type: args.eclipse_type ?? "any" };
|
|
65
68
|
if (args.after_date)
|
|
66
69
|
params.date = args.after_date;
|
|
67
|
-
|
|
68
|
-
return await getActiveClient().request("GET", endpoint, { params, timeoutMs: 60_000 });
|
|
70
|
+
return await getActiveClient().request("GET", "/eclipse/next", { params, timeoutMs: 60_000 });
|
|
69
71
|
}
|
|
70
72
|
},
|
|
71
73
|
});
|
|
@@ -2,10 +2,18 @@ import { registerTool, validateRequired, validateCoordinates } from "../index.js
|
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
4
|
import { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime, toDateTimeInputBody } from "../datetime.js";
|
|
5
|
-
|
|
5
|
+
// `field`/`timezoneField` name the caller's own argument path (e.g.
|
|
6
|
+
// "subjects[2].datetime") so a zone-less value is rejected with a message
|
|
7
|
+
// that points at the offending subject rather than the batch as a whole.
|
|
8
|
+
//
|
|
9
|
+
// birth_datetime MUST go through toDateTimeInputBody: a naive clock time plus
|
|
10
|
+
// a sibling `timezone` is a valid input shape, and dropping the zone here
|
|
11
|
+
// turns it into an ambiguous datetime the Go contract rejects per-subject —
|
|
12
|
+
// which is what this tool used to do to every subject in the batch.
|
|
13
|
+
function buildSubject(name, datetime, lat, lon, timezone, field = "datetime", timezoneField = "timezone") {
|
|
6
14
|
return {
|
|
7
15
|
name,
|
|
8
|
-
birth_datetime:
|
|
16
|
+
birth_datetime: toDateTimeInputBody(field, datetime, timezone, timezoneField),
|
|
9
17
|
birth_location: {
|
|
10
18
|
latitude: { decimal: lat },
|
|
11
19
|
longitude: { decimal: lon },
|
|
@@ -52,8 +60,8 @@ registerTool({
|
|
|
52
60
|
handler: async (args) => {
|
|
53
61
|
validateRequired(args, ["subjects"]);
|
|
54
62
|
args.subjects.forEach((s, i) => assertZonedDatetime(`subjects[${i}].datetime`, s?.datetime, s?.timezone, `subjects[${i}].timezone`));
|
|
55
|
-
const items = args.subjects.map((s) => ({
|
|
56
|
-
subject: buildSubject(s.name, s.datetime, s.latitude, s.longitude),
|
|
63
|
+
const items = args.subjects.map((s, i) => ({
|
|
64
|
+
subject: buildSubject(s.name, s.datetime, s.latitude, s.longitude, s?.timezone, `subjects[${i}].datetime`, `subjects[${i}].timezone`),
|
|
57
65
|
}));
|
|
58
66
|
return await getActiveClient().post("/ephemeris/natal/batch", { items });
|
|
59
67
|
},
|