@openephemeris/mcp-server 3.6.1 → 3.8.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 +14 -0
- package/README.md +5 -5
- package/config/dev-allowlist.json +23 -3
- package/dist/backend/client.js +7 -3
- package/dist/tools/index.js +2 -0
- package/dist/tools/specialized/acg.js +21 -1
- package/dist/tools/specialized/comparative.js +8 -5
- package/dist/tools/specialized/ephemeris_extended.js +68 -10
- package/dist/tools/specialized/hd_bodygraph.d.ts +1 -0
- package/dist/tools/specialized/hd_bodygraph.js +51 -0
- package/dist/tools/specialized/moon.js +20 -6
- package/dist/tools/specialized/returns.js +21 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [3.8.0] — 2026-04-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `human_design_bodygraph` — new dedicated tool for generating Human Design Bodygraph images
|
|
14
|
+
(PNG or SVG). Renders all 9 centers, 36 channels, and 64 gates with Personality/Design
|
|
15
|
+
color-coding. Supports `light`, `dark`, and `mono` styles. Developer tier, 2 credits.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **HD Bodygraph: missing 10-57 channel** — The "Channel of Perfected Form" (Gate 10 ↔ Gate 57)
|
|
19
|
+
was silently dropped from the visualization geometry while the math engine still reported it as
|
|
20
|
+
active. Charts with both gates activated now correctly render the channel.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
10
24
|
## [3.7.0] — 2026-04-19
|
|
11
25
|
|
|
12
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -224,10 +224,10 @@ OpenEphemeris API
|
|
|
224
224
|
|
|
225
225
|
Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live tool registry.
|
|
226
226
|
|
|
227
|
-
- Allowlisted operations: **
|
|
228
|
-
- Methods: `GET=4`, `POST=
|
|
229
|
-
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **
|
|
230
|
-
- Typed tools: `auth_login`, `auth_logout`, `auth_status`, `chinese_bazi`, `dev_call`, `dev_list_allowed`, `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`, `hd_opposition`, `hd_planetary_return`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `vedic_chart`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
|
|
227
|
+
- Allowlisted operations: **23**
|
|
228
|
+
- Methods: `GET=4`, `POST=19`, `PUT=0`, `PATCH=0`, `DELETE=0`
|
|
229
|
+
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **52**
|
|
230
|
+
- Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `chinese_bazi`, `dev_call`, `dev_list_allowed`, `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`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `vedic_chart`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
|
|
231
231
|
- Generic tools:
|
|
232
232
|
|
|
233
233
|
### Allowlist Families
|
|
@@ -239,7 +239,7 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
239
239
|
| `electional` | 4 | `GET /electional/aspect-search`, `GET /electional/find-window` |
|
|
240
240
|
| `ephemeris` | 1 | `POST /ephemeris/relocation` |
|
|
241
241
|
| `predictive` | 6 | `POST /predictive/returns`, `POST /predictive/returns/lunar` |
|
|
242
|
-
| `visualization` |
|
|
242
|
+
| `visualization` | 3 | `POST /visualization/bi-wheel`, `POST /visualization/bodygraph` |
|
|
243
243
|
<!-- GENERATED:RUNTIME_SNAPSHOT:END -->
|
|
244
244
|
|
|
245
245
|
## Why OpenEphemeris for AI Agents?
|
|
@@ -99,13 +99,17 @@
|
|
|
99
99
|
"method": "POST",
|
|
100
100
|
"path": "/visualization/bi-wheel"
|
|
101
101
|
},
|
|
102
|
+
{
|
|
103
|
+
"method": "POST",
|
|
104
|
+
"path": "/visualization/bodygraph"
|
|
105
|
+
},
|
|
102
106
|
{
|
|
103
107
|
"method": "POST",
|
|
104
108
|
"path": "/visualization/chart-wheel"
|
|
105
109
|
}
|
|
106
110
|
],
|
|
107
|
-
"last_generated_at": "2026-04-
|
|
108
|
-
"openapi_sha256": "
|
|
111
|
+
"last_generated_at": "2026-04-22T01:16:37.771Z",
|
|
112
|
+
"openapi_sha256": "c649b6a9ba32e1576f9f12d2d78f1706d5bc279d7733a29f1e69a8ca042024d6",
|
|
109
113
|
"candidates_get": [
|
|
110
114
|
{
|
|
111
115
|
"method": "GET",
|
|
@@ -477,7 +481,14 @@
|
|
|
477
481
|
],
|
|
478
482
|
"operationId": "features_acg_features_post"
|
|
479
483
|
},
|
|
480
|
-
|
|
484
|
+
{
|
|
485
|
+
"method": "POST",
|
|
486
|
+
"path": "/acg/heatmap/composite",
|
|
487
|
+
"tags": [
|
|
488
|
+
"Astrocartography"
|
|
489
|
+
],
|
|
490
|
+
"operationId": "AcgHeatmapCompositeAcgHeatmapCompositePost"
|
|
491
|
+
},
|
|
481
492
|
{
|
|
482
493
|
"method": "POST",
|
|
483
494
|
"path": "/acg/hermetic-lines",
|
|
@@ -966,6 +977,14 @@
|
|
|
966
977
|
],
|
|
967
978
|
"operationId": "generate_bi_wheel_visualization_bi_wheel_post"
|
|
968
979
|
},
|
|
980
|
+
{
|
|
981
|
+
"method": "POST",
|
|
982
|
+
"path": "/visualization/bodygraph",
|
|
983
|
+
"tags": [
|
|
984
|
+
"visualization"
|
|
985
|
+
],
|
|
986
|
+
"operationId": "GenerateBodygraphVisualization_visualization_bodygraph_post"
|
|
987
|
+
},
|
|
969
988
|
{
|
|
970
989
|
"method": "POST",
|
|
971
990
|
"path": "/visualization/chart-wheel",
|
|
@@ -996,6 +1015,7 @@
|
|
|
996
1015
|
"Non-GET allowlisted: POST /predictive/time-lords/profections",
|
|
997
1016
|
"Non-GET allowlisted: POST /predictive/time-lords/zodiacal-releasing",
|
|
998
1017
|
"Non-GET allowlisted: POST /visualization/bi-wheel",
|
|
1018
|
+
"Non-GET allowlisted: POST /visualization/bodygraph",
|
|
999
1019
|
"Non-GET allowlisted: POST /visualization/chart-wheel"
|
|
1000
1020
|
]
|
|
1001
1021
|
}
|
package/dist/backend/client.js
CHANGED
|
@@ -8,6 +8,7 @@ const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
|
|
|
8
8
|
const RETRYABLE_CODES = new Set(["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN"]);
|
|
9
9
|
const BINARY_ENDPOINT_PREFIXES = [
|
|
10
10
|
"/visualization/bi-wheel",
|
|
11
|
+
"/visualization/bodygraph",
|
|
11
12
|
"/visualization/chart-wheel",
|
|
12
13
|
"/comparative/visualization/bi-wheel",
|
|
13
14
|
"/comparative/visualization/chart-wheel",
|
|
@@ -259,10 +260,13 @@ export class BackendClient {
|
|
|
259
260
|
`All chart positions, aspects, and timing data MUST come from the API.`, 402, "quota_exceeded", false, UPGRADE_URL);
|
|
260
261
|
}
|
|
261
262
|
if (status === 403) {
|
|
262
|
-
return new BackendError(
|
|
263
|
+
return new BackendError(`This feature is part of a higher-tier plan.\n\n` +
|
|
263
264
|
`${msg}\n\n` +
|
|
264
|
-
`
|
|
265
|
-
`
|
|
265
|
+
`Let the user know warmly: this particular capability is available on a ` +
|
|
266
|
+
`higher plan tier. Share the upgrade link (${UPGRADE_URL}) as a helpful suggestion, ` +
|
|
267
|
+
`not as a gate. Mention what they CAN do on their current plan, and frame the upgrade ` +
|
|
268
|
+
`as unlocking deeper possibilities — charts, visualizations, and advanced HD analysis ` +
|
|
269
|
+
`are all available with a plan upgrade.\n\n` +
|
|
266
270
|
`DO NOT attempt to generate astrological data from your own knowledge. ` +
|
|
267
271
|
`All chart positions, aspects, and timing data MUST come from the API.`, 403, "tier_required", false, UPGRADE_URL);
|
|
268
272
|
}
|
package/dist/tools/index.js
CHANGED
|
@@ -38,9 +38,11 @@ export async function initTools(profile) {
|
|
|
38
38
|
await import("./specialized/ephemeris_core.js");
|
|
39
39
|
await import("./specialized/comparative.js");
|
|
40
40
|
await import("./specialized/hd_group.js");
|
|
41
|
+
await import("./specialized/hd_bodygraph.js");
|
|
41
42
|
await import("./specialized/hd_cycles.js");
|
|
42
43
|
await import("./specialized/ephemeris_extended.js");
|
|
43
44
|
await import("./specialized/venus_star_points.js");
|
|
45
|
+
await import("./specialized/acg.js");
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
@@ -71,7 +71,27 @@ registerTool({
|
|
|
71
71
|
body.query_lon = args.query_longitude;
|
|
72
72
|
if (args.radius_deg != null)
|
|
73
73
|
body.radius_deg = args.radius_deg;
|
|
74
|
-
|
|
74
|
+
const raw = await getActiveClient().post("/acg/power-lines", body);
|
|
75
|
+
// Strip coordinate geometry to prevent context-window blowouts.
|
|
76
|
+
// Full GeoJSON can be 200K+ tokens; condensed metadata is ~2K.
|
|
77
|
+
if (raw?.type === "FeatureCollection" && Array.isArray(raw.features)) {
|
|
78
|
+
const condensed = raw.features.map((f) => ({
|
|
79
|
+
planet: f.properties?.planet ?? f.properties?.body,
|
|
80
|
+
angle: f.properties?.angle ?? f.properties?.line_type,
|
|
81
|
+
...(f.properties?.label ? { label: f.properties.label } : {}),
|
|
82
|
+
...(f.properties?.zodiac_sign ? { zodiac_sign: f.properties.zodiac_sign } : {}),
|
|
83
|
+
...(f.properties?.longitude_at_equator != null
|
|
84
|
+
? { longitude_at_equator: f.properties.longitude_at_equator }
|
|
85
|
+
: {}),
|
|
86
|
+
}));
|
|
87
|
+
return {
|
|
88
|
+
type: "acg_power_lines_summary",
|
|
89
|
+
total_lines: condensed.length,
|
|
90
|
+
note: "Coordinate geometry stripped for LLM context efficiency. Use the full API or Ela Map for geographic rendering.",
|
|
91
|
+
lines: condensed,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return raw;
|
|
75
95
|
},
|
|
76
96
|
});
|
|
77
97
|
// POST /acg/hits — OE-018
|
|
@@ -173,18 +173,21 @@ registerTool({
|
|
|
173
173
|
},
|
|
174
174
|
handler: async (args) => {
|
|
175
175
|
validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude"]);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
// Backend expects: { subject: {...}, transit_datetime?: {...} }
|
|
177
|
+
// NOT a subjects[] array — natal-transits is a single-subject endpoint
|
|
178
|
+
const body = {
|
|
179
|
+
subject: buildSubject("Natal", args.natal_datetime, args.natal_latitude, args.natal_longitude, args.natal_timezone),
|
|
180
|
+
};
|
|
179
181
|
if (args.transit_datetime) {
|
|
180
|
-
|
|
182
|
+
// transit_datetime is a DateTimeInput on the backend
|
|
183
|
+
body.transit_datetime = { iso: args.transit_datetime };
|
|
181
184
|
}
|
|
182
185
|
const query = {};
|
|
183
186
|
if (args.format)
|
|
184
187
|
query.format = args.format;
|
|
185
188
|
return await getActiveClient().request("POST", "/comparative/natal-transits", {
|
|
186
189
|
params: query,
|
|
187
|
-
data:
|
|
190
|
+
data: body,
|
|
188
191
|
});
|
|
189
192
|
},
|
|
190
193
|
});
|
|
@@ -72,11 +72,15 @@ registerTool({
|
|
|
72
72
|
},
|
|
73
73
|
});
|
|
74
74
|
// POST /ephemeris/retrograde-status
|
|
75
|
+
// NOTE: the backend handles one planet per call (planet_id is a non-nullable int
|
|
76
|
+
// that defaults to 0/Sun when omitted). We fan out to all 10 planets here so
|
|
77
|
+
// the documented "all planets" behaviour works correctly.
|
|
75
78
|
registerTool({
|
|
76
79
|
name: "ephemeris_retrograde_status",
|
|
77
80
|
description: "Get retrograde/direct status and speed for all planets at a given date/time. " +
|
|
78
|
-
"Returns is_retrograde flag, longitude speed, and station proximity.\n\n" +
|
|
79
|
-
"CREDIT COST: 1 credit per call
|
|
81
|
+
"Returns is_retrograde flag, longitude speed, and station proximity for every planet.\n\n" +
|
|
82
|
+
"CREDIT COST: 1 credit per call.\n\n" +
|
|
83
|
+
"Optionally pass planet_id (0-9) to query a single planet.",
|
|
80
84
|
inputSchema: {
|
|
81
85
|
type: "object",
|
|
82
86
|
properties: {
|
|
@@ -88,12 +92,34 @@ registerTool({
|
|
|
88
92
|
},
|
|
89
93
|
handler: async (args) => {
|
|
90
94
|
validateRequired(args, ["datetime"]);
|
|
91
|
-
const
|
|
95
|
+
const client = getActiveClient();
|
|
96
|
+
// If a specific planet is requested, delegate directly.
|
|
97
|
+
if (args.planet_id != null) {
|
|
98
|
+
return await client.post("/ephemeris/retrograde-status", {
|
|
99
|
+
date_time: { iso: args.datetime },
|
|
100
|
+
planet_id: args.planet_id,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Fan-out: the backend only handles one planet per call; query all 10 in parallel.
|
|
104
|
+
const PLANET_IDS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
105
|
+
const results = await Promise.all(PLANET_IDS.map((pid) => client.post("/ephemeris/retrograde-status", {
|
|
92
106
|
date_time: { iso: args.datetime },
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
planet_id: pid,
|
|
108
|
+
}).catch(() => null)));
|
|
109
|
+
// Merge into a keyed object: { planet_name: {...status} }
|
|
110
|
+
const merged = {};
|
|
111
|
+
for (const res of results) {
|
|
112
|
+
if (res && typeof res === "object" && res.planet_name) {
|
|
113
|
+
const r = res;
|
|
114
|
+
merged[r.planet_name] = {
|
|
115
|
+
planet_id: r.planet_id,
|
|
116
|
+
is_retrograde: r.is_retrograde,
|
|
117
|
+
longitude_speed: r.longitude_speed,
|
|
118
|
+
motion_type: r.motion_type,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { planets: merged, datetime: args.datetime, success: true };
|
|
97
123
|
},
|
|
98
124
|
});
|
|
99
125
|
// POST /ephemeris/midpoints
|
|
@@ -150,7 +176,38 @@ registerTool({
|
|
|
150
176
|
body.star_names = args.star_names;
|
|
151
177
|
if (args.orb != null)
|
|
152
178
|
body.orb = args.orb;
|
|
153
|
-
|
|
179
|
+
const raw = await getActiveClient().post("/ephemeris/fixed-stars", body);
|
|
180
|
+
// Bug 6 workaround: backend returns longitude as degrees-within-sign (0–30),
|
|
181
|
+
// not full ecliptic longitude (0–360). Compute full_longitude from sign name.
|
|
182
|
+
const SIGN_ORDER = {
|
|
183
|
+
Aries: 0, Taurus: 1, Gemini: 2, Cancer: 3, Leo: 4, Virgo: 5,
|
|
184
|
+
Libra: 6, Scorpio: 7, Sagittarius: 8, Capricorn: 9, Aquarius: 10, Pisces: 11,
|
|
185
|
+
};
|
|
186
|
+
function enrichStar(star) {
|
|
187
|
+
if (!star || typeof star !== "object")
|
|
188
|
+
return star;
|
|
189
|
+
const signLon = typeof star.longitude === "number" ? star.longitude : 0;
|
|
190
|
+
const signName = star.sign ?? star.sign_name ?? "";
|
|
191
|
+
const signIndex = SIGN_ORDER[signName] ?? -1;
|
|
192
|
+
const fullLon = signIndex >= 0 ? signIndex * 30 + signLon : signLon;
|
|
193
|
+
return {
|
|
194
|
+
...star,
|
|
195
|
+
longitude: fullLon, // ecliptic longitude 0–360
|
|
196
|
+
sign_longitude: signLon, // original within-sign degrees preserved
|
|
197
|
+
sign: signName || undefined,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// Normalise the response — backend may return array or { stars: [...] }
|
|
201
|
+
if (Array.isArray(raw)) {
|
|
202
|
+
return raw.map(enrichStar);
|
|
203
|
+
}
|
|
204
|
+
if (raw && typeof raw === "object") {
|
|
205
|
+
const starsKey = ["stars", "data", "fixed_stars", "results"].find((k) => Array.isArray(raw[k]));
|
|
206
|
+
if (starsKey) {
|
|
207
|
+
return { ...raw, [starsKey]: raw[starsKey].map(enrichStar) };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return raw;
|
|
154
211
|
},
|
|
155
212
|
});
|
|
156
213
|
// POST /ephemeris/hermetic-lots
|
|
@@ -223,9 +280,10 @@ registerTool({
|
|
|
223
280
|
},
|
|
224
281
|
handler: async (args) => {
|
|
225
282
|
validateRequired(args, ["longitude_1", "longitude_2"]);
|
|
283
|
+
// Backend field names are longitude1 / longitude2 (no underscore)
|
|
226
284
|
const body = {
|
|
227
|
-
|
|
228
|
-
|
|
285
|
+
longitude1: args.longitude_1,
|
|
286
|
+
longitude2: args.longitude_2,
|
|
229
287
|
};
|
|
230
288
|
if (args.max_orb != null)
|
|
231
289
|
body.max_orb = args.max_orb;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { registerTool, validateRequired } from "../index.js";
|
|
2
|
+
import { getActiveClient } from "../../backend/client.js";
|
|
3
|
+
/** Append 'Z' if no timezone offset is present in the datetime string. */
|
|
4
|
+
function ensureTimezone(dt) {
|
|
5
|
+
if (/[Zz]$/.test(dt) || /[+-]\d{2}:\d{2}$/.test(dt))
|
|
6
|
+
return dt;
|
|
7
|
+
return dt + "Z";
|
|
8
|
+
}
|
|
9
|
+
registerTool({
|
|
10
|
+
name: "human_design_bodygraph",
|
|
11
|
+
description: "Generate a Human Design Bodygraph image (PNG or SVG) from a birth datetime (UTC). " +
|
|
12
|
+
"Renders the full bodygraph circuit board showing all 9 centers, 36 channels, and 64 gates " +
|
|
13
|
+
"with Personality (conscious) and Design (unconscious) activations color-coded. " +
|
|
14
|
+
"Defined centers are filled with their HD doctrine color; open/undefined centers remain muted. " +
|
|
15
|
+
"The tool returns a raw image (base64) which most Vision-enabled AI agents can display and visually read.\n\n" +
|
|
16
|
+
"CREDIT COST: 2 credits per call.\n\n" +
|
|
17
|
+
"EXAMPLE: Generate a bodygraph for someone born April 15, 1990 at 19:30 UTC:\n" +
|
|
18
|
+
" datetime='1990-04-15T19:30:00Z'",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
datetime: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "ISO 8601 birth datetime in UTC, e.g. '1990-04-15T19:30:00Z'. " +
|
|
25
|
+
"Human Design chart calculation is time-sensitive — accuracy to the minute matters.",
|
|
26
|
+
},
|
|
27
|
+
style: {
|
|
28
|
+
type: "string",
|
|
29
|
+
enum: ["light", "dark", "mono"],
|
|
30
|
+
description: "Aesthetic style of the bodygraph: 'dark' (default, deep background), " +
|
|
31
|
+
"'light' (cream background), or 'mono' (black-and-white print-ready).",
|
|
32
|
+
},
|
|
33
|
+
format: {
|
|
34
|
+
type: "string",
|
|
35
|
+
enum: ["png", "svg"],
|
|
36
|
+
description: "Output format. Defaults to 'png'.",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ["datetime"],
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
},
|
|
42
|
+
handler: async (args) => {
|
|
43
|
+
validateRequired(args, ["datetime"]);
|
|
44
|
+
const body = {
|
|
45
|
+
birth_datetime_utc: ensureTimezone(args.datetime),
|
|
46
|
+
};
|
|
47
|
+
const format = args.format || "png";
|
|
48
|
+
const styleParam = args.style ? `&style=${args.style}` : "";
|
|
49
|
+
return await getActiveClient().post(`/visualization/bodygraph?format=${format}&size=800${styleParam}`, body);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -36,8 +36,9 @@ registerTool({
|
|
|
36
36
|
handler: async (args) => {
|
|
37
37
|
validateCoordinates(args, "latitude", "longitude");
|
|
38
38
|
const params = {};
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// Backend requires datetime even though the schema marks it optional.
|
|
40
|
+
// Default to current UTC time when caller omits it.
|
|
41
|
+
params.datetime = args.datetime ?? new Date().toISOString();
|
|
41
42
|
if (args.latitude != null)
|
|
42
43
|
params.latitude = args.latitude;
|
|
43
44
|
if (args.longitude != null)
|
|
@@ -109,10 +110,23 @@ registerTool({
|
|
|
109
110
|
const result = await getActiveClient().request("GET", "/calendar/astrology/moon-phases", {
|
|
110
111
|
params: { start_date: startDate, end_date: endDate },
|
|
111
112
|
});
|
|
112
|
-
// Normalise response shape —
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
// Normalise response shape — defensively handle every known API envelope shape.
|
|
114
|
+
// Crash "allPhases.filter is not a function" occurs when a property like
|
|
115
|
+
// result.data is truthy but is an object, not an array.
|
|
116
|
+
function extractArray(val) {
|
|
117
|
+
if (Array.isArray(val))
|
|
118
|
+
return val;
|
|
119
|
+
if (val && typeof val === "object") {
|
|
120
|
+
// one level deeper: { phases: [...] }, { data: [...] }, etc.
|
|
121
|
+
for (const key of ["phases", "data", "events", "moon_phases", "results", "items"]) {
|
|
122
|
+
const nested = val[key];
|
|
123
|
+
if (Array.isArray(nested))
|
|
124
|
+
return nested;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const allPhases = extractArray(result) ?? [];
|
|
116
130
|
const matching = allPhases
|
|
117
131
|
.filter((p) => {
|
|
118
132
|
const name = (p.phase_name ?? p.name ?? p.phase ?? "").toLowerCase();
|
|
@@ -127,9 +127,9 @@ registerTool({
|
|
|
127
127
|
"Uranus returns (~84 years), etc.\n\n" +
|
|
128
128
|
"CREDIT COST: 5 credits per call.\n\n" +
|
|
129
129
|
"EXAMPLE: Saturn return for birth 1990-04-15 near year 2019:\n" +
|
|
130
|
-
" body='saturn', birth_datetime='1990-04-15T14:30:
|
|
130
|
+
" body='saturn', birth_datetime='1990-04-15T14:30:00Z', target_datetime='2019-01-01T00:00:00Z'\n" +
|
|
131
131
|
"EXAMPLE: Chiron return for birth 1975-03-10 near age 50:\n" +
|
|
132
|
-
" body='chiron', birth_datetime='1975-03-10T08:00:
|
|
132
|
+
" body='chiron', birth_datetime='1975-03-10T08:00:00Z', target_datetime='2025-01-01T00:00:00Z'",
|
|
133
133
|
inputSchema: {
|
|
134
134
|
type: "object",
|
|
135
135
|
properties: {
|
|
@@ -145,16 +145,33 @@ registerTool({
|
|
|
145
145
|
type: "string",
|
|
146
146
|
description: "Date/time near which to find the return (ISO 8601). MUST include offset or Z. Required.",
|
|
147
147
|
},
|
|
148
|
+
birth_latitude: {
|
|
149
|
+
type: "number",
|
|
150
|
+
description: "Birth latitude in decimal degrees. Required to get a full return chart (house cusps, angles).",
|
|
151
|
+
},
|
|
152
|
+
birth_longitude: {
|
|
153
|
+
type: "number",
|
|
154
|
+
description: "Birth longitude in decimal degrees. Required to get a full return chart (house cusps, angles).",
|
|
155
|
+
},
|
|
148
156
|
},
|
|
149
157
|
required: ["body", "birth_datetime", "target_datetime"],
|
|
150
158
|
additionalProperties: false,
|
|
151
159
|
},
|
|
152
160
|
handler: async (args) => {
|
|
153
161
|
validateRequired(args, ["body", "birth_datetime", "target_datetime"]);
|
|
154
|
-
|
|
162
|
+
const requestBody = {
|
|
155
163
|
body: args.body,
|
|
156
164
|
birth_datetime: { iso: args.birth_datetime },
|
|
157
165
|
target_datetime: { iso: args.target_datetime },
|
|
158
|
-
}
|
|
166
|
+
};
|
|
167
|
+
// Forward birth coordinates so the backend can compute the full return chart.
|
|
168
|
+
// Without these, computeReturnChartData sees {lat:0, lon:0} and returns nil.
|
|
169
|
+
if (args.birth_latitude != null && args.birth_longitude != null) {
|
|
170
|
+
requestBody.location = {
|
|
171
|
+
latitude: { decimal: args.birth_latitude },
|
|
172
|
+
longitude: { decimal: args.birth_longitude },
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return await getActiveClient().post("/predictive/returns", requestBody);
|
|
159
176
|
},
|
|
160
177
|
});
|