@openephemeris/mcp-server 4.17.0 → 4.19.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +10 -10
  3. package/dist/backend/client.d.ts +57 -1
  4. package/dist/backend/client.js +125 -15
  5. package/dist/index.js +8 -110
  6. package/dist/prompts.js +26 -24
  7. package/dist/server-sse.d.ts +18 -0
  8. package/dist/server-sse.js +48 -120
  9. package/dist/tools/apps/_render-token.d.ts +34 -0
  10. package/dist/tools/apps/_render-token.js +50 -0
  11. package/dist/tools/apps/bazi-app.d.ts +1 -1
  12. package/dist/tools/apps/bazi-app.js +39 -14
  13. package/dist/tools/apps/bi-wheel-app.d.ts +10 -3
  14. package/dist/tools/apps/bi-wheel-app.js +79 -119
  15. package/dist/tools/apps/bodygraph-app.d.ts +7 -4
  16. package/dist/tools/apps/bodygraph-app.js +101 -46
  17. package/dist/tools/apps/chart-wheel-app.d.ts +1 -1
  18. package/dist/tools/apps/chart-wheel-app.js +59 -18
  19. package/dist/tools/apps/location-tools.js +10 -3
  20. package/dist/tools/apps/moon-phase-app.d.ts +1 -1
  21. package/dist/tools/apps/moon-phase-app.js +13 -4
  22. package/dist/tools/apps/transit-timeline-app.d.ts +1 -1
  23. package/dist/tools/apps/transit-timeline-app.js +6 -6
  24. package/dist/tools/apps/ui-resource.d.ts +43 -0
  25. package/dist/tools/apps/ui-resource.js +66 -0
  26. package/dist/tools/apps/ui-resources.d.ts +22 -0
  27. package/dist/tools/apps/ui-resources.js +105 -0
  28. package/dist/tools/apps/vedic-chart-app.d.ts +1 -1
  29. package/dist/tools/apps/vedic-chart-app.js +13 -3
  30. package/dist/tools/datetime-historical.js +7 -2
  31. package/dist/tools/datetime.js +2 -1
  32. package/dist/tools/dev.js +7 -6
  33. package/dist/tools/specialized/electional.js +4 -3
  34. package/dist/tools/specialized/ephemeris_extended.js +19 -4
  35. package/dist/tools/specialized/hd_group.js +2 -2
  36. package/dist/tools/specialized/moon.d.ts +1 -1
  37. package/dist/tools/specialized/moon.js +51 -43
  38. package/dist/tools/specialized/progressed.js +2 -25
  39. package/dist/tools/specialized/transits.js +5 -5
  40. package/dist/ui/bazi.html +1199 -1161
  41. package/dist/ui/bi-wheel.html +573 -504
  42. package/dist/ui/bodygraph.html +437 -337
  43. package/dist/ui/chart-wheel.html +654 -584
  44. package/dist/ui/moon-phase.html +190 -155
  45. package/dist/ui/transit-timeline.html +865 -830
  46. package/dist/ui/vedic-chart.html +971 -934
  47. package/package.json +2 -1
@@ -12,10 +12,10 @@
12
12
  * Supported modes (BiWheelMode):
13
13
  * synastry — two natal charts (person1 inner, person2 outer)
14
14
  * transit — natal inner + transiting planets outer
15
- * progressed — natal inner + secondary progressions outer (POST /ephemeris/progressed)
15
+ * progressed — natal inner + secondary progressions outer (POST /ephemeris/progressed, method=secondary)
16
16
  * solar_return — natal inner + solar return chart outer (POST /predictive/returns/solar)
17
17
  * lunar_return — natal inner + lunar return chart outer (POST /predictive/returns/lunar)
18
- * solar_arc — natal inner + solar arc directions outer (client-side Naibod approximation)
18
+ * solar_arc — natal inner + solar arc directions outer (POST /ephemeris/progressed, method=solar_arc)
19
19
  *
20
20
  * Cross-aspects are computed client-side (UI) AND server-side (for summary/fallback).
21
21
  * The payload cross_aspects array is capped at 30 tightest-orb aspects.
@@ -24,13 +24,14 @@ import fs from "node:fs";
24
24
  import path from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
26
  import { registerTool, SERVER_VERSION } from "../index.js";
27
+ import { UI_RESOURCE_MIME_TYPE, uiResourceUri } from "./ui-resource.js";
27
28
  import { getActiveClient } from "../../backend/client.js";
28
29
  import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
29
- import { DATETIME_DESC, timezoneProperty } from "../datetime.js";
30
+ import { DATETIME_DESC, timezoneProperty, toDateTimeInputBody } from "../datetime.js";
30
31
  import { coordsFromArgsOrLocation } from "./_location-resolver.js";
31
32
  // ── Constants ─────────────────────────────────────────────────────────────────
32
- export const BI_WHEEL_RESOURCE_URI = "ui://openephemeris/bi-wheel";
33
- export const BI_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
33
+ export const BI_WHEEL_RESOURCE_URI = uiResourceUri("bi-wheel");
34
+ export const BI_WHEEL_MIME_TYPE = UI_RESOURCE_MIME_TYPE;
34
35
  const here = path.dirname(fileURLToPath(import.meta.url));
35
36
  const BUNDLE_PATH = path.resolve(here, "..", "..", "..", "dist", "ui", "bi-wheel.html");
36
37
  let cachedBiWheelBundle = null;
@@ -130,120 +131,68 @@ function buildNatalBody(datetime, lat, lon, timezone, houseSystem = "placidus")
130
131
  }
131
132
  return body;
132
133
  }
133
- function buildTransitBody(transitDate, natalDatetime, lat, lon, timezone) {
134
+ function locationBody(lat, lon, timezone) {
134
135
  return {
135
- subject: {
136
- name: "MCP Request",
137
- birth_datetime: { iso: natalDatetime },
138
- birth_location: {
139
- latitude: { decimal: lat ?? 0 },
140
- longitude: { decimal: lon ?? 0 },
141
- },
142
- },
143
- transit_datetime: { iso: transitDate },
136
+ latitude: { decimal: lat },
137
+ longitude: { decimal: lon },
144
138
  ...(timezone ? { timezone: { iana_name: timezone } } : {}),
145
- configuration: { house_system: HOUSE_SYSTEM_MAP["placidus"] },
146
139
  };
147
140
  }
148
- /** POST /ephemeris/progressed — secondary progressions */
149
- function buildProgressedBody(natalDatetime, lat, lon, timezone, targetDatetime) {
150
- const body = {
151
- subject: {
152
- name: "MCP Request",
153
- birth_datetime: { iso: natalDatetime },
154
- birth_location: {
155
- latitude: { decimal: lat ?? 0 },
156
- longitude: { decimal: lon ?? 0 },
157
- },
158
- },
159
- configuration: {
160
- target_datetime: { iso: targetDatetime },
161
- method: "secondary",
162
- house_system: HOUSE_SYSTEM_MAP["placidus"],
163
- },
164
- };
165
- if (timezone) {
166
- body.subject.birth_location.timezone = { iana_name: timezone };
167
- }
141
+ /**
142
+ * POST /ephemeris/progressed. `method` picks secondary progressions or true
143
+ * solar arc directions — the server directs planets, angles AND house cusps by
144
+ * the Sun's real arc (ProgressedChartRequest; target_datetime is top-level,
145
+ * the method rides in progression_options).
146
+ */
147
+ function buildProgressedBody(natalDatetime, lat, lon, timezone, targetDatetime, targetTimezone, method, houseSystem) {
148
+ const body = buildNatalBody(natalDatetime, lat, lon, timezone, houseSystem);
149
+ body.target_datetime = toDateTimeInputBody("person2_datetime", targetDatetime, targetTimezone);
150
+ body.progression_options = { method };
168
151
  return body;
169
152
  }
170
- /** POST /predictive/returns/solar or /predictive/returns/lunar */
171
- function buildReturnBody(natalDatetime, natalLat, natalLon, timezone, targetDatetime, returnLat, returnLon) {
172
- const body = {
173
- subject: {
174
- name: "MCP Request",
175
- birth_datetime: { iso: natalDatetime },
176
- birth_location: {
177
- latitude: { decimal: natalLat ?? 0 },
178
- longitude: { decimal: natalLon ?? 0 },
179
- },
180
- },
181
- return_location: {
182
- latitude: { decimal: returnLat ?? natalLat ?? 0 },
183
- longitude: { decimal: returnLon ?? natalLon ?? 0 },
184
- },
185
- target_datetime: { iso: targetDatetime },
186
- configuration: { house_system: HOUSE_SYSTEM_MAP["placidus"] },
153
+ /**
154
+ * POST /predictive/returns/solar or /predictive/returns/lunar. These take a
155
+ * flat body (birth_datetime at the top level, no `subject`) — see
156
+ * SolarReturnRequest / LunarReturnRequest in openapi.json.
157
+ */
158
+ function buildReturnBody(natalDatetime, natalLat, natalLon, timezone, targetDatetime, targetTimezone, returnLat, returnLon, houseSystem) {
159
+ return {
160
+ birth_datetime: toDateTimeInputBody("person1_datetime", natalDatetime, timezone, "person1_timezone"),
161
+ birth_location: locationBody(natalLat, natalLon, timezone),
162
+ return_location: locationBody(returnLat, returnLon),
163
+ target_datetime: toDateTimeInputBody("person2_datetime", targetDatetime, targetTimezone, "person2_timezone"),
164
+ house_system: HOUSE_SYSTEM_MAP[houseSystem] ?? HOUSE_SYSTEM_MAP["placidus"],
187
165
  };
188
- if (timezone) {
189
- body.subject.birth_location.timezone = { iana_name: timezone };
190
- }
191
- return body;
192
166
  }
193
167
  /**
194
- * Client-side solar arc: advance every natal planet longitude by elapsed years in degrees.
195
- * Uses the Naibod rate (1 tropical year ≈ 0.9856°). The natal data object is cloned and
196
- * the planets array/map is offset — houses are left at natal positions (standard practice).
168
+ * Route outer-wheel fetch to the correct API endpoint based on mode, and hand
169
+ * back a chart in the natal-chart shape (top-level planets/houses/angles):
170
+ * /ephemeris/progressed nests it under `data`, the return endpoints under `chart`.
197
171
  */
198
- function applySolarArcOffset(natalData, natalDatetime, targetDatetime) {
199
- const natalYear = new Date(natalDatetime).getFullYear() +
200
- new Date(natalDatetime).getMonth() / 12;
201
- const targetYear = new Date(targetDatetime).getFullYear() +
202
- new Date(targetDatetime).getMonth() / 12;
203
- const arcDegrees = (targetYear - natalYear) * 0.9856; // Naibod rate
204
- const offsetPlanets = (raw) => {
205
- if (Array.isArray(raw)) {
206
- return raw.map((p) => ({
207
- ...p,
208
- longitude: ((p.longitude ?? 0) + arcDegrees + 360) % 360,
209
- }));
172
+ export async function fetchOuterChart(client, mode, dt1, lat1, lon1, tz1, dt2, lat2, lon2, tz2, houseSystem = "placidus") {
173
+ const post = (path, body) => client.post(path, body);
174
+ const unwrap = (resp, key) => {
175
+ const inner = resp?.[key];
176
+ if (!inner || typeof inner !== "object") {
177
+ throw new Error(`Unexpected ${mode} response: no \`${key}\` chart in the API reply.`);
210
178
  }
211
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
212
- const out = {};
213
- for (const [k, v] of Object.entries(raw)) {
214
- if (v && typeof v === "object" && "longitude" in v) {
215
- out[k] = { ...v, longitude: ((v.longitude ?? 0) + arcDegrees + 360) % 360 };
216
- }
217
- else {
218
- out[k] = v;
219
- }
220
- }
221
- return out;
222
- }
223
- return raw;
179
+ return inner;
224
180
  };
225
- return {
226
- ...natalData,
227
- planets: offsetPlanets(natalData.planets),
228
- _solar_arc_degrees: Math.round(arcDegrees * 1000) / 1000,
229
- };
230
- }
231
- /** Route outer-wheel fetch to the correct API endpoint based on mode. */
232
- async function fetchOuterChart(client, mode, dt1, lat1, lon1, tz1, dt2, lat2, lon2, tz2) {
233
181
  switch (mode) {
234
182
  case "synastry":
235
- return client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2, lon2, tz2));
183
+ return post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2, lon2, tz2, houseSystem));
236
184
  case "transit":
237
- return client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2 ?? lat1, lon2 ?? lon1, tz2 ?? tz1));
185
+ return post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2 ?? lat1, lon2 ?? lon1, tz2 ?? tz1, houseSystem));
238
186
  case "progressed":
239
- return client.post("/ephemeris/progressed", buildProgressedBody(dt1, lat1, lon1, tz1, dt2));
187
+ case "solar_arc":
188
+ return unwrap(await post("/ephemeris/progressed", buildProgressedBody(dt1, lat1, lon1, tz1, dt2, tz2 ?? tz1, mode === "progressed" ? "secondary" : "solar_arc", houseSystem)), "data");
240
189
  case "solar_return":
241
- return client.post("/predictive/returns/solar", buildReturnBody(dt1, lat1, lon1, tz1, dt2, lat2 ?? lat1, lon2 ?? lon1));
242
- case "lunar_return":
243
- return client.post("/predictive/returns/lunar", buildReturnBody(dt1, lat1, lon1, tz1, dt2, lat2 ?? lat1, lon2 ?? lon1));
244
- case "solar_arc": {
245
- const natal = await client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1));
246
- return applySolarArcOffset(natal, dt1, dt2);
190
+ case "lunar_return": {
191
+ if (lat1 == null || lon1 == null) {
192
+ throw new Error(`Internal: ${mode} requires Person 1's latitude and longitude.`);
193
+ }
194
+ const path = mode === "solar_return" ? "/predictive/returns/solar" : "/predictive/returns/lunar";
195
+ return unwrap(await post(path, buildReturnBody(dt1, lat1, lon1, tz1, dt2, tz2 ?? tz1, lat2 ?? lat1, lon2 ?? lon1, houseSystem)), "chart");
247
196
  }
248
197
  }
249
198
  }
@@ -292,25 +241,35 @@ export function computeCrossAspects(planets1, planets2) {
292
241
  const CLASSICAL_PLANETS = new Set([
293
242
  "sun", "moon", "mercury", "venus", "mars",
294
243
  "jupiter", "saturn", "uranus", "neptune", "pluto",
295
- "chiron", "north_node", "south_node", "true_node", "asc", "mc",
244
+ "chiron", "asc", "mc",
245
+ // The four lunar nodes keep the API's own names (lowercased) — Mean and True
246
+ // are different points and must never share a name.
247
+ "north node (mean)", "north node (true)", "south node (mean)", "south node (true)",
296
248
  ]);
297
249
  /**
298
- * Normalise the Go backend's verbose planet key (e.g. "North Node (Mean)")
299
- * to the canonical lowercase underscore form the renderer expects.
250
+ * Normalise the Go backend's verbose planet key to the canonical lowercase form
251
+ * the renderer expects. The lunar nodes keep their full names ("north node
252
+ * (mean)" etc.), matching packages/widget-ui/shared/lunar-nodes.ts.
300
253
  */
301
254
  function canonicalizePlanetName(raw) {
302
255
  const lower = raw.toLowerCase();
303
256
  // Various node/chiron aliases the Go PlanetName() function emits.
304
- // Keep mean vs true DISTINCT so the UI can offer a node toggle; collapsing
305
- // both to "north_node" (the old behaviour) produced two overlapping ☊ glyphs.
257
+ // Keep mean vs true DISTINCT (North and South alike) so the UI can offer a
258
+ // node toggle and every label says which node it is.
306
259
  if (lower === "north node (true)" || lower === "true node")
307
- return "true_node";
260
+ return "north node (true)";
308
261
  if (lower === "north node (mean)" || lower === "mean node" || lower === "north node")
309
- return "north_node";
310
- if (lower === "south node (mean)" || lower === "south node (true)")
311
- return "south_node";
312
- if (lower === "mean apogee" || lower === "black moon lilith" || lower === "lilith (mean)" || lower === "lilith (true)")
313
- return "lilith";
262
+ return "north node (mean)";
263
+ if (lower === "south node (true)" || lower === "true south node")
264
+ return "south node (true)";
265
+ if (lower === "south node (mean)" || lower === "south node")
266
+ return "south node (mean)";
267
+ if (lower === "lilith (true)" || lower === "lilith_true" || lower === "true_lilith" || lower === "osculating apogee")
268
+ return "lilith (true)";
269
+ if (lower === "lilith (interpolated)" || lower === "lilith_interpolated" || lower === "lilith_natural" || lower === "interpolated apogee")
270
+ return "lilith (interpolated)";
271
+ if (lower === "lilith (mean)" || lower === "lilith" || lower === "mean_lilith" || lower === "mean apogee" || lower === "black moon lilith")
272
+ return "lilith (mean)";
314
273
  // Strip parenthetical qualifiers for any other bodies just in case
315
274
  return lower.replace(/\s*\(.*?\)/g, "").replace(/\s+/g, "_");
316
275
  }
@@ -613,7 +572,7 @@ registerTool({
613
572
  const loc2 = MODE_META[mode].outerLabel(dt2, args.person2_name);
614
573
  const [innerData, outerData] = await Promise.all([
615
574
  client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1, houseSystem)),
616
- fetchOuterChart(client, mode, dt1, lat1, lon1, tz1, dt2, lat2, lon2, tz2),
575
+ fetchOuterChart(client, mode, dt1, lat1, lon1, tz1, dt2, lat2, lon2, tz2, houseSystem),
617
576
  ]);
618
577
  const params1 = { datetime: dt1, timezone: tz1 ?? null, latitude: lat1 ?? null, longitude: lon1 ?? null, location: loc1 };
619
578
  const params2 = { datetime: dt2, timezone: tz2 ?? null, latitude: lat2 ?? null, longitude: lon2 ?? null, location: loc2 };
@@ -721,9 +680,10 @@ const PLANET_THEMES = {
721
680
  neptune: "dreams, idealism, compassion, and dissolution",
722
681
  pluto: "transformation, power dynamics, and profound change",
723
682
  chiron: "the wounded healer — deep vulnerability, mastery through pain, and the gift of empathy forged in suffering",
724
- north_node: "soul direction and karmic growth edge; the evolutionary path forward in this lifetime",
725
- south_node: "accumulated karma and innate past-life gifts; the comfort zone that must be transcended",
726
- true_node: "soul direction and karmic growth edge; the evolutionary path forward in this lifetime",
683
+ "north node (mean)": "soul direction and karmic growth edge; the evolutionary path forward in this lifetime",
684
+ "north node (true)": "soul direction and karmic growth edge; the evolutionary path forward in this lifetime",
685
+ "south node (mean)": "accumulated karma and innate past-life gifts; the comfort zone that must be transcended",
686
+ "south node (true)": "accumulated karma and innate past-life gifts; the comfort zone that must be transcended",
727
687
  asc: "the rising sign and outer personality mask; the social self, physical appearance, and the lens through which life is encountered",
728
688
  mc: "public vocation, highest ambition, and the world's face; the pinnacle of achievement this person strives toward",
729
689
  };
@@ -797,7 +757,7 @@ function interpretBiWheelPlanet(planet, sign, house, wheel, retrograde = false,
797
757
  `Unlike transits, progressed planets move slowly — a sign ingress can take years to unfold. This position describes a chapter of inner development: the themes of ${capitalize(planet)} are now being expressed with the colouring of ${signQuality}. Aspects from this progressed position to natal points mark turning points in that developmental arc.`);
798
758
  }
799
759
  else if (mode === "solar_arc") {
800
- return (`**Solar Arc ${capitalize(planet)}:** This directed position represents natal ${capitalize(planet)} advanced by approximately 1° per year of life.${houseCtx}${retroCtx}\n\n` +
760
+ return (`**Solar Arc ${capitalize(planet)}:** This directed position represents natal ${capitalize(planet)} advanced by the solar arc — the distance the progressed Sun has travelled since birth (roughly 1° a year).${houseCtx}${retroCtx}\n\n` +
801
761
  `Solar arc directions symbolise the symbolic maturation of each natal archetype. Where the directed ${capitalize(planet)} now falls — and especially which natal planets or angles it contacts — marks a significant chapter in the long development of ${theme}. Conjunctions, squares, and oppositions to natal points are the most potent activation signals.`);
802
762
  }
803
763
  else if (mode === "solar_return") {
@@ -990,7 +950,7 @@ registerTool({
990
950
  progressed: `**Secondary Progressions Overview: ${label1} × ${label2}**\n\n` +
991
951
  `This progression bi-wheel places the natal chart inside and secondary progressed positions in the outer ring. Progressed planets reflect the gradual inner maturation of natal archetypal themes using the day-for-a-year method. Cross-aspects from progressed to natal points mark the defining chapters of inner development.`,
992
952
  solar_arc: `**Solar Arc Directions Overview: ${label1} × ${label2}**\n\n` +
993
- `This solar arc bi-wheel shows natal positions inside and solar arc directed positions in the outer ring. Solar arc direction advances every natal planet by approximately 1° per year of life (Naibod rate). Contacts from directed positions to natal points — especially conjunctions — mark pivotal life moments.`,
953
+ `This solar arc bi-wheel shows natal positions inside and solar arc directed positions in the outer ring. Solar arc direction advances every natal planet, angle and house cusp by the same arc: the distance the progressed Sun has travelled since birth (roughly 1° a year). Contacts from directed positions to natal points — especially conjunctions — mark pivotal life moments.`,
994
954
  solar_return: `**Solar Return Overview: ${label1} × ${label2}**\n\n` +
995
955
  `This solar return bi-wheel compares the natal chart against the solar return for the target year. The outer ring shows where planets fall at the exact moment the Sun returns to its natal degree, describing the dominant themes and developmental focus of the coming solar year.`,
996
956
  lunar_return: `**Lunar Return Overview: ${label1} × ${label2}**\n\n` +
@@ -12,14 +12,17 @@
12
12
  * overlay SVG is returned inline by /human-design/transit-chart and
13
13
  * /human-design/composite (include_visual) rather than a separate render call.
14
14
  *
15
- * The bodygraph is rendered client-side from structured JSON — no SVG endpoint
16
- * is called, keeping latency at zero and the architecture consistent with
17
- * the chart-wheel-app approach.
15
+ * The bodygraph picture is the Go engine's SVG, rendered server-side
16
+ * (fetchGoBodygraphSVG, in parallel with the chart call; overlays inline as
17
+ * above) and returned as `_svg`.
18
+ * The iframe inlines that SVG and binds interactivity to its data-* attributes
19
+ * (packages/widget-ui/bodygraph/src/wire-go-svg.ts); it does not draw the
20
+ * graph itself.
18
21
  *
19
22
  * Also exports resource helpers (getBodygraphBundle, etc.) for use in
20
23
  * index.ts and server-sse.ts.
21
24
  */
22
- export declare const BODYGRAPH_RESOURCE_URI = "ui://openephemeris/bodygraph";
25
+ export declare const BODYGRAPH_RESOURCE_URI: string;
23
26
  export declare const BODYGRAPH_MIME_TYPE = "text/html;profile=mcp-app";
24
27
  /** Read the pre-built HTML bundle. Returns null if not yet built. */
25
28
  export declare function getBodygraphBundle(): string | null;