@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 (decimal degrees, +N). Optional." },
1323
- longitude: { type: "number", description: "Natal birth longitude (decimal degrees, +E). Optional." },
1324
- location: { type: "string", description: "Natal location name, display only. This is a caption, NOT a geocoder — it does not set the chart location. Supply latitude/longitude too (resolve them with location_search), or the chart is computed at 0N 0E." },
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
- const timezone = args.timezone;
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 = args.latitude ?? 0;
1361
- const lon = args.longitude ?? 0;
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: args.location ?? "Subject",
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 (decimal degrees, +N). Optional." },
1445
- longitude: { type: "number", description: "Birth longitude (decimal degrees, +E). Optional." },
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 (decimal degrees, +N). Optional." },
1456
- longitude: { type: "number", description: "Birth longitude (decimal degrees, +E). Optional." },
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
- const toSubject = (p, label) => {
1490
- const tz = p?.timezone;
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: p?.latitude ?? 0,
1494
- longitude: p?.longitude ?? 0,
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: { iso: args.natal_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 — route to global endpoint
64
- const params = {};
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
- const endpoint = args.eclipse_type === "lunar" ? "/eclipse/lunar/global" : "/eclipse/solar/global";
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
- function buildSubject(name, datetime, lat, lon) {
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: { iso: 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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openephemeris/mcp-server",
3
- "version": "4.5.0",
3
+ "version": "4.6.0",
4
4
  "description": "Model Context Protocol server for the Open Ephemeris astronomical computation API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",