@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 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: **22**
228
- - Methods: `GET=4`, `POST=18`, `PUT=0`, `PATCH=0`, `DELETE=0`
229
- - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **49**
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` | 2 | `POST /visualization/bi-wheel`, `POST /visualization/chart-wheel` |
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-16T23:21:31.665Z",
108
- "openapi_sha256": "2349bd8e3e36dad609b75cc61acb1722206990581085d986b0cdad79317296aa",
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
  }
@@ -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(`⚠️ ENDPOINT ACCESS RESTRICTED DO NOT FABRICATE DATA.\n\n` +
263
+ return new BackendError(`This feature is part of a higher-tier plan.\n\n` +
263
264
  `${msg}\n\n` +
264
- `Tell the user: this feature requires a higher-tier plan. ` +
265
- `They can upgrade at ${UPGRADE_URL} or manage their account at ${DASHBOARD_ACCOUNT_URL}.\n\n` +
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
  }
@@ -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
- return await getActiveClient().post("/acg/power-lines", body);
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
- const subjects = [
177
- buildSubject("Natal", args.natal_datetime, args.natal_latitude, args.natal_longitude, args.natal_timezone),
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
- subjects.push(buildSubject("Transit", args.transit_datetime, args.natal_latitude, args.natal_longitude, args.transit_timezone));
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: { subjects }
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 body = {
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
- if (args.planet_id != null)
95
- body.planet_id = args.planet_id;
96
- return await getActiveClient().post("/ephemeris/retrograde-status", body);
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
- return await getActiveClient().post("/ephemeris/fixed-stars", body);
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
- longitude_1: args.longitude_1,
228
- longitude_2: args.longitude_2,
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
- if (args.datetime)
40
- params.datetime = args.datetime;
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 — API may return array, or { phases: [...] }, or { data: [...] }
113
- const allPhases = Array.isArray(result)
114
- ? result
115
- : result?.phases ?? result?.data ?? result?.events ?? [];
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:00', target_datetime='2019-01-01'\n" +
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:00', target_datetime='2025-01-01'",
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
- return await getActiveClient().post("/predictive/returns", {
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openephemeris/mcp-server",
3
- "version": "3.6.1",
3
+ "version": "3.8.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",