@openephemeris/mcp-server 4.4.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 +95 -0
- package/README.md +6 -5
- package/config/dev-allowlist.json +32 -2
- package/dist/tools/apps/bodygraph-app.js +48 -17
- package/dist/tools/apps/transit-timeline-app.js +7 -2
- package/dist/tools/index.js +1 -0
- package/dist/tools/specialized/eclipse.js +6 -4
- package/dist/tools/specialized/electional.js +90 -0
- package/dist/tools/specialized/ephemeris_extended.js +12 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,101 @@ 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
|
+
|
|
76
|
+
## [4.5.0] — 2026-07-30
|
|
77
|
+
|
|
78
|
+
Adds a tool for the question the astrocartography tools could not answer:
|
|
79
|
+
*when* a planet reaches an angle at a place, rather than what is angular
|
|
80
|
+
there at a fixed moment.
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
|
|
84
|
+
- **`electional_angle_crossings` — find when a body crosses the Ascendant,
|
|
85
|
+
Descendant, Midheaven, or Imum Coeli at a location.** The time-inverse of
|
|
86
|
+
astrocartography. `acg_hits` answers "which lines pass near Warsaw at this
|
|
87
|
+
moment"; this answers "at what moment does Mercury cross the Descendant
|
|
88
|
+
over Warsaw". Give it a latitude/longitude and a date range and it returns
|
|
89
|
+
the crossing moments chronologically, with each body's position and a
|
|
90
|
+
daylight hint at that instant.
|
|
91
|
+
|
|
92
|
+
Crossing times are solved on the same in-mundo (equatorial RA/Dec) geometry
|
|
93
|
+
the astrocartography line engine draws, so casting a map at a returned
|
|
94
|
+
moment puts that line through the query point — the search and the map
|
|
95
|
+
cannot disagree.
|
|
96
|
+
|
|
97
|
+
Defaults to the classical seven bodies and all four angles over the current
|
|
98
|
+
UTC day. Circumpolar cases — a body that never rises or never sets at that
|
|
99
|
+
latitude — simply return no event rather than an error. Ranges up to 366
|
|
100
|
+
days; an over-long range is rejected rather than quietly shortened.
|
|
101
|
+
|
|
102
|
+
Advertised in the default tool surface. 5 credits per call, Developer plan
|
|
103
|
+
and above.
|
|
104
|
+
|
|
10
105
|
## [4.4.0] — 2026-07-30
|
|
11
106
|
|
|
12
107
|
Puts the location resolver on the natural path for the four `explore_*`
|
package/README.md
CHANGED
|
@@ -440,10 +440,10 @@ When you update the MCP server logic (handlers, bug fixes, hardening), you shoul
|
|
|
440
440
|
|
|
441
441
|
Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live tool registry.
|
|
442
442
|
|
|
443
|
-
- Allowlisted operations: **
|
|
444
|
-
- Methods: `GET=
|
|
445
|
-
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **
|
|
446
|
-
- Typed tools: `account_usage`, `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bazi_annual_pillar`, `bazi_chart`, `bazi_compatibility`, `bazi_element_balance`, `bazi_luck_pillars`, `bazi_recalculate`, `bazi_ten_gods`, `bi_wheel_on_cross_aspect_click`, `bi_wheel_on_house_click`, `bi_wheel_on_planet_click`, `bi_wheel_recalculate`, `bi_wheel_synopsis`, `bodygraph_recalculate`, `chart_wheel_on_aspect_click`, `chart_wheel_on_house_click`, `chart_wheel_on_planet_click`, `chart_wheel_recalculate`, `chinese_bazi`, `dev_list_allowed`, `dev_read_api`, `dev_write_api`, `electional_aspect_search`, `electional_moment_analysis`, `electional_station_tracker`, `ephemeris_angles_points`, `ephemeris_aspect_check`, `ephemeris_bi_wheel`, `ephemeris_chart_wheel`, `ephemeris_composite`, `ephemeris_composite_midpoint`, `ephemeris_dignities`, `ephemeris_electional`, `ephemeris_fixed_stars`, `ephemeris_hermetic_lots`, `ephemeris_house_cusps`, `ephemeris_lunar_return`, `ephemeris_midpoints`, `ephemeris_moon_phase`, `ephemeris_natal_batch`, `ephemeris_natal_chart`, `ephemeris_natal_transits`, `ephemeris_next_eclipse`, `ephemeris_next_lunar_phase`, `ephemeris_overlay`, `ephemeris_planet_position`, `ephemeris_planetary_return`, `ephemeris_progressed_chart`, `ephemeris_relocation`, `ephemeris_retrograde_status`, `ephemeris_solar_return`, `ephemeris_synastry`, `ephemeris_transits`, `explore_bazi_chart`, `explore_bi_wheel`, `explore_human_design`, `explore_human_design_connection`, `explore_human_design_transit`, `explore_moon_phase`, `explore_natal_chart`, `explore_transit_timeline`, `explore_vedic_chart`, `hd_on_center_click`, `hd_on_channel_click`, `hd_on_connection_channel_click`, `hd_on_gate_click`, `hd_on_planet_click`, `hd_on_transit_channel_click`, `hd_on_variable_click`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `location_search`, `moon_phase_recalculate`, `timezone_resolve`, `vedic_chart`, `vedic_chart_recalculate`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
|
|
443
|
+
- Allowlisted operations: **31**
|
|
444
|
+
- Methods: `GET=5`, `POST=26`, `PUT=0`, `PATCH=0`, `DELETE=0`
|
|
445
|
+
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **92**
|
|
446
|
+
- Typed tools: `account_usage`, `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bazi_annual_pillar`, `bazi_chart`, `bazi_compatibility`, `bazi_element_balance`, `bazi_luck_pillars`, `bazi_recalculate`, `bazi_ten_gods`, `bi_wheel_on_cross_aspect_click`, `bi_wheel_on_house_click`, `bi_wheel_on_planet_click`, `bi_wheel_recalculate`, `bi_wheel_synopsis`, `bodygraph_recalculate`, `chart_wheel_on_aspect_click`, `chart_wheel_on_house_click`, `chart_wheel_on_planet_click`, `chart_wheel_recalculate`, `chinese_bazi`, `dev_list_allowed`, `dev_read_api`, `dev_write_api`, `electional_angle_crossings`, `electional_aspect_search`, `electional_moment_analysis`, `electional_station_tracker`, `ephemeris_angles_points`, `ephemeris_aspect_check`, `ephemeris_bi_wheel`, `ephemeris_chart_wheel`, `ephemeris_composite`, `ephemeris_composite_midpoint`, `ephemeris_dignities`, `ephemeris_electional`, `ephemeris_fixed_stars`, `ephemeris_hermetic_lots`, `ephemeris_house_cusps`, `ephemeris_lunar_return`, `ephemeris_midpoints`, `ephemeris_moon_phase`, `ephemeris_natal_batch`, `ephemeris_natal_chart`, `ephemeris_natal_transits`, `ephemeris_next_eclipse`, `ephemeris_next_lunar_phase`, `ephemeris_overlay`, `ephemeris_planet_position`, `ephemeris_planetary_return`, `ephemeris_progressed_chart`, `ephemeris_relocation`, `ephemeris_retrograde_status`, `ephemeris_solar_return`, `ephemeris_synastry`, `ephemeris_transits`, `explore_bazi_chart`, `explore_bi_wheel`, `explore_human_design`, `explore_human_design_connection`, `explore_human_design_transit`, `explore_moon_phase`, `explore_natal_chart`, `explore_transit_timeline`, `explore_vedic_chart`, `hd_on_center_click`, `hd_on_channel_click`, `hd_on_connection_channel_click`, `hd_on_gate_click`, `hd_on_planet_click`, `hd_on_transit_channel_click`, `hd_on_variable_click`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `location_search`, `moon_phase_recalculate`, `timezone_resolve`, `vedic_chart`, `vedic_chart_recalculate`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
|
|
447
447
|
- Generic tools:
|
|
448
448
|
|
|
449
449
|
### Allowlist Families
|
|
@@ -453,8 +453,9 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
453
453
|
| `acg` | 4 | `POST /acg/ccg`, `POST /acg/hits` |
|
|
454
454
|
| `chinese` | 5 | `POST /chinese/bazi/compatibility`, `POST /chinese/bazi/element-balance` |
|
|
455
455
|
| `comparative` | 5 | `POST /comparative/composite`, `POST /comparative/composite/midpoint` |
|
|
456
|
-
| `electional` |
|
|
456
|
+
| `electional` | 5 | `GET /electional/angle-crossings`, `GET /electional/aspect-search` |
|
|
457
457
|
| `ephemeris` | 1 | `POST /ephemeris/relocation` |
|
|
458
|
+
| `human-design` | 2 | `POST /human-design/composite`, `POST /human-design/transit-chart` |
|
|
458
459
|
| `predictive` | 6 | `POST /predictive/returns`, `POST /predictive/returns/lunar` |
|
|
459
460
|
| `visualization` | 3 | `POST /visualization/bi-wheel`, `POST /visualization/bodygraph` |
|
|
460
461
|
<!-- GENERATED:RUNTIME_SNAPSHOT:END -->
|
|
@@ -71,6 +71,10 @@
|
|
|
71
71
|
"method": "POST",
|
|
72
72
|
"path": "/comparative/synastry"
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
"method": "GET",
|
|
76
|
+
"path": "/electional/angle-crossings"
|
|
77
|
+
},
|
|
74
78
|
{
|
|
75
79
|
"method": "GET",
|
|
76
80
|
"path": "/electional/aspect-search"
|
|
@@ -91,6 +95,14 @@
|
|
|
91
95
|
"method": "POST",
|
|
92
96
|
"path": "/ephemeris/relocation"
|
|
93
97
|
},
|
|
98
|
+
{
|
|
99
|
+
"method": "POST",
|
|
100
|
+
"path": "/human-design/composite"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"method": "POST",
|
|
104
|
+
"path": "/human-design/transit-chart"
|
|
105
|
+
},
|
|
94
106
|
{
|
|
95
107
|
"method": "POST",
|
|
96
108
|
"path": "/predictive/returns/lunar"
|
|
@@ -128,8 +140,8 @@
|
|
|
128
140
|
"path": "/visualization/chart-wheel"
|
|
129
141
|
}
|
|
130
142
|
],
|
|
131
|
-
"last_generated_at": "2026-
|
|
132
|
-
"openapi_sha256": "
|
|
143
|
+
"last_generated_at": "2026-07-30T20:30:07.623Z",
|
|
144
|
+
"openapi_sha256": "c8d62588232551c514979433b2829ceb951ddce99334e65a3deed112a28bb11a",
|
|
133
145
|
"candidates_get": [
|
|
134
146
|
{
|
|
135
147
|
"method": "GET",
|
|
@@ -243,6 +255,14 @@
|
|
|
243
255
|
],
|
|
244
256
|
"operationId": "get_solar_eclipse_local_eclipse_solar_local_get"
|
|
245
257
|
},
|
|
258
|
+
{
|
|
259
|
+
"method": "GET",
|
|
260
|
+
"path": "/electional/angle-crossings",
|
|
261
|
+
"tags": [
|
|
262
|
+
"electional"
|
|
263
|
+
],
|
|
264
|
+
"operationId": "ElectionalAngleCrossingsElectionalAngleCrossingsGet"
|
|
265
|
+
},
|
|
246
266
|
{
|
|
247
267
|
"method": "GET",
|
|
248
268
|
"path": "/electional/aspect-search",
|
|
@@ -925,6 +945,14 @@
|
|
|
925
945
|
],
|
|
926
946
|
"operationId": "human_design_penta_human_design_penta_post"
|
|
927
947
|
},
|
|
948
|
+
{
|
|
949
|
+
"method": "POST",
|
|
950
|
+
"path": "/human-design/transit-chart",
|
|
951
|
+
"tags": [
|
|
952
|
+
"Human Design"
|
|
953
|
+
],
|
|
954
|
+
"operationId": "human_design_transit_chart_human_design_transit_chart_post"
|
|
955
|
+
},
|
|
928
956
|
{
|
|
929
957
|
"method": "POST",
|
|
930
958
|
"path": "/human-design/transit",
|
|
@@ -1105,6 +1133,8 @@
|
|
|
1105
1133
|
"Non-GET allowlisted: POST /comparative/overlay",
|
|
1106
1134
|
"Non-GET allowlisted: POST /comparative/synastry",
|
|
1107
1135
|
"Non-GET allowlisted: POST /ephemeris/relocation",
|
|
1136
|
+
"Non-GET allowlisted: POST /human-design/composite",
|
|
1137
|
+
"Non-GET allowlisted: POST /human-design/transit-chart",
|
|
1108
1138
|
"Non-GET allowlisted: POST /predictive/returns/lunar",
|
|
1109
1139
|
"Non-GET allowlisted: POST /predictive/returns/solar",
|
|
1110
1140
|
"Non-GET allowlisted: POST /predictive/returns",
|
|
@@ -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 },
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
});
|
|
@@ -239,3 +239,93 @@ registerTool({
|
|
|
239
239
|
});
|
|
240
240
|
},
|
|
241
241
|
});
|
|
242
|
+
// GET /electional/angle-crossings — OE-FEAT-2026-001
|
|
243
|
+
registerTool({
|
|
244
|
+
name: "electional_angle_crossings",
|
|
245
|
+
description: "Find WHEN a body crosses an angle (Ascendant, Descendant, Midheaven, Imum Coeli) at a specific " +
|
|
246
|
+
"place — the time-inverse of astrocartography. Answers 'when does Mercury cross the Descendant " +
|
|
247
|
+
"in Warsaw today?'\n\n" +
|
|
248
|
+
"✅ USE THIS FOR: 'when is Venus on my horizon in Lisbon?', 'what time does Jupiter culminate " +
|
|
249
|
+
"over Tokyo on Friday?', timing a moment by a planet's angularity at a place.\n" +
|
|
250
|
+
"❌ NOT FOR: which lines pass near a place at a fixed time → use acg_hits instead.\n\n" +
|
|
251
|
+
"Crossing times use the same in-mundo geometry as the astrocartography line engine, so casting a " +
|
|
252
|
+
"map at a returned moment puts that line through the query point. Circumpolar cases (a body that " +
|
|
253
|
+
"never rises or never sets at that latitude) simply return no event.\n\n" +
|
|
254
|
+
"CREDIT COST: 5 credits per call.\n\n" +
|
|
255
|
+
"EXAMPLE: When does Mercury cross the Descendant over Warsaw on 2026-07-30?\n" +
|
|
256
|
+
" latitude=52.2297, longitude=21.0122, bodies='Mercury', angles='DC',\n" +
|
|
257
|
+
" start='2026-07-30T00:00:00Z', end='2026-07-31T00:00:00Z'",
|
|
258
|
+
inputSchema: {
|
|
259
|
+
type: "object",
|
|
260
|
+
properties: {
|
|
261
|
+
latitude: {
|
|
262
|
+
type: "number",
|
|
263
|
+
description: "Latitude of the place in decimal degrees (positive = North). Resolve from a place name with location_search; never recall coordinates from memory.",
|
|
264
|
+
},
|
|
265
|
+
longitude: {
|
|
266
|
+
type: "number",
|
|
267
|
+
description: "Longitude of the place in decimal degrees (positive = East).",
|
|
268
|
+
},
|
|
269
|
+
start: {
|
|
270
|
+
type: "string",
|
|
271
|
+
description: "Start of the search range. " + WINDOW_DATE_DESC + " Defaults to the start of the current UTC day.",
|
|
272
|
+
},
|
|
273
|
+
end: {
|
|
274
|
+
type: "string",
|
|
275
|
+
description: "End of the search range. " + WINDOW_DATE_DESC + " Defaults to 24 hours after start. Maximum range 366 days.",
|
|
276
|
+
},
|
|
277
|
+
bodies: {
|
|
278
|
+
type: "string",
|
|
279
|
+
description: "Comma-separated body names, e.g. 'Mercury,Venus'. Defaults to the classical seven " +
|
|
280
|
+
"(Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn). Aliases: 'NorthNode'/'Rahu' → MeanNode, 'Ketu' → SouthNode.",
|
|
281
|
+
},
|
|
282
|
+
angles: {
|
|
283
|
+
type: "string",
|
|
284
|
+
description: "Comma-separated angles: AC, DC, MC, IC (ASC/DSC accepted). Defaults to all four.",
|
|
285
|
+
},
|
|
286
|
+
refraction_correction: {
|
|
287
|
+
type: "boolean",
|
|
288
|
+
description: "Apply atmospheric refraction to the AC/DC horizon. Defaults to false (geometric horizon), " +
|
|
289
|
+
"which is what the astrocartography endpoints use by default — leave it off to keep times and map lines aligned.",
|
|
290
|
+
},
|
|
291
|
+
max_results: {
|
|
292
|
+
type: "number",
|
|
293
|
+
description: "Maximum number of crossing events to return (default 5000).",
|
|
294
|
+
},
|
|
295
|
+
format: {
|
|
296
|
+
type: "string",
|
|
297
|
+
enum: ["json", "llm"],
|
|
298
|
+
description: "Output format. 'llm' = compact token-efficient output (available on all tiers).",
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
required: ["latitude", "longitude"],
|
|
302
|
+
additionalProperties: false,
|
|
303
|
+
},
|
|
304
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
305
|
+
annotations: { title: "Angle Crossing Search", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
306
|
+
handler: async (args) => {
|
|
307
|
+
validateRequired(args, ["latitude", "longitude"]);
|
|
308
|
+
const query = {
|
|
309
|
+
latitude: args.latitude,
|
|
310
|
+
longitude: args.longitude,
|
|
311
|
+
};
|
|
312
|
+
if (args.start)
|
|
313
|
+
query.start = args.start;
|
|
314
|
+
if (args.end)
|
|
315
|
+
query.end = args.end;
|
|
316
|
+
if (args.bodies)
|
|
317
|
+
query.bodies = args.bodies;
|
|
318
|
+
if (args.angles)
|
|
319
|
+
query.angles = args.angles;
|
|
320
|
+
if (args.refraction_correction !== undefined)
|
|
321
|
+
query.refraction_correction = args.refraction_correction;
|
|
322
|
+
if (args.max_results !== undefined)
|
|
323
|
+
query.max_results = args.max_results;
|
|
324
|
+
if (args.format)
|
|
325
|
+
query.format = args.format;
|
|
326
|
+
return await getActiveClient().request("GET", "/electional/angle-crossings", {
|
|
327
|
+
data: {},
|
|
328
|
+
params: query,
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
});
|
|
@@ -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
|
},
|