@openephemeris/mcp-server 3.13.9 → 3.13.10

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 (61) hide show
  1. package/README.md +2 -2
  2. package/config/dev-allowlist.json +28 -4
  3. package/dist/index.js +45 -0
  4. package/dist/oauth/dcr.d.ts +11 -0
  5. package/dist/oauth/dcr.js +49 -0
  6. package/dist/oauth/discovery.d.ts +11 -0
  7. package/dist/oauth/discovery.js +40 -0
  8. package/dist/oauth/pkce.d.ts +18 -0
  9. package/dist/oauth/pkce.js +34 -0
  10. package/dist/oauth/rate-limit.d.ts +20 -0
  11. package/dist/oauth/rate-limit.js +65 -0
  12. package/dist/oauth/store.d.ts +68 -0
  13. package/dist/oauth/store.js +247 -0
  14. package/dist/oauth/supabase-jwt.d.ts +39 -0
  15. package/dist/oauth/supabase-jwt.js +194 -0
  16. package/dist/oauth/token.d.ts +21 -0
  17. package/dist/oauth/token.js +213 -0
  18. package/dist/prompts.js +171 -159
  19. package/dist/server-sse.js +134 -36
  20. package/dist/tools/apps/bazi-app.d.ts +23 -0
  21. package/dist/tools/apps/bazi-app.js +224 -0
  22. package/dist/tools/apps/bi-wheel-app.d.ts +14 -7
  23. package/dist/tools/apps/bi-wheel-app.js +293 -108
  24. package/dist/tools/apps/bodygraph-app.js +38 -3
  25. package/dist/tools/apps/moon-phase-app.d.ts +19 -0
  26. package/dist/tools/apps/moon-phase-app.js +190 -0
  27. package/dist/tools/apps/transit-timeline-app.d.ts +20 -0
  28. package/dist/tools/apps/transit-timeline-app.js +243 -0
  29. package/dist/tools/apps/vedic-chart-app.d.ts +17 -0
  30. package/dist/tools/apps/vedic-chart-app.js +226 -0
  31. package/dist/tools/index.js +4 -0
  32. package/dist/tools/specialized/acg.js +2 -2
  33. package/dist/tools/specialized/bazi.js +367 -44
  34. package/dist/tools/specialized/bi_wheel.js +1 -1
  35. package/dist/tools/specialized/chart_wheel.js +1 -1
  36. package/dist/tools/specialized/comparative.js +4 -4
  37. package/dist/tools/specialized/eclipse.js +1 -1
  38. package/dist/tools/specialized/electional.js +4 -4
  39. package/dist/tools/specialized/ephemeris_core.js +2 -2
  40. package/dist/tools/specialized/ephemeris_extended.js +8 -8
  41. package/dist/tools/specialized/hd_bodygraph.js +1 -1
  42. package/dist/tools/specialized/hd_cycles.js +2 -2
  43. package/dist/tools/specialized/hd_group.js +2 -2
  44. package/dist/tools/specialized/human_design.js +1 -1
  45. package/dist/tools/specialized/moon.js +2 -2
  46. package/dist/tools/specialized/natal.js +1 -1
  47. package/dist/tools/specialized/progressed.js +1 -1
  48. package/dist/tools/specialized/relocation.js +1 -1
  49. package/dist/tools/specialized/returns.js +3 -3
  50. package/dist/tools/specialized/synastry.js +1 -1
  51. package/dist/tools/specialized/transits.js +1 -1
  52. package/dist/tools/specialized/vedic.js +1 -1
  53. package/dist/tools/specialized/venus_star_points.js +6 -6
  54. package/dist/ui/bazi.html +213 -0
  55. package/dist/ui/bi-wheel.html +762 -489
  56. package/dist/ui/bodygraph.html +1788 -1649
  57. package/dist/ui/chart-wheel.html +1772 -1593
  58. package/dist/ui/moon-phase.html +6758 -0
  59. package/dist/ui/transit-timeline.html +6835 -0
  60. package/dist/ui/vedic-chart.html +210 -0
  61. package/package.json +2 -2
@@ -1,15 +1,21 @@
1
1
  /**
2
2
  * bi-wheel-app.ts — MCP App tool registration for the Bi-Wheel Explorer.
3
3
  *
4
- * Registers 3 tools:
5
- * • explore_bi_wheel — primary entry point: fetches two charts, computes
6
- * cross-aspects, returns data + UI resource [model]
4
+ * Registers 6 tools:
5
+ * • explore_bi_wheel — primary entry point [model + app]
6
+ * bi_wheel_recalculate — re-fetch with new params [app-only]
7
7
  * • bi_wheel_on_cross_aspect_click — cross-aspect click handler [app-only]
8
- * • bi_wheel_on_planet_click — planet click handler (inner or outer wheel) [app-only]
8
+ * • bi_wheel_on_planet_click — planet click handler [app-only]
9
+ * • bi_wheel_on_house_click — house cusp click handler [app-only]
10
+ * • bi_wheel_synopsis — full reading overview [model + app]
9
11
  *
10
- * Modes:
11
- * synastry fetch two natal charts in parallel (person1 inner, person2 outer)
12
- * transit fetch one natal chart (inner) + one transit chart (outer)
12
+ * Supported modes (BiWheelMode):
13
+ * synastry — two natal charts (person1 inner, person2 outer)
14
+ * transit — natal inner + transiting planets outer
15
+ * progressed — natal inner + secondary progressions outer (POST /ephemeris/progressed)
16
+ * solar_return — natal inner + solar return chart outer (POST /predictive/returns/solar)
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)
13
19
  *
14
20
  * Cross-aspects are computed client-side (UI) AND server-side (for summary/fallback).
15
21
  * The payload cross_aspects array is capped at 30 tightest-orb aspects.
@@ -42,6 +48,53 @@ export function getBiWheelBundle() {
42
48
  export function clearBiWheelBundleCache() {
43
49
  cachedBiWheelBundle = null;
44
50
  }
51
+ const ALL_MODES = [
52
+ "synastry", "transit", "progressed", "solar_return", "lunar_return", "solar_arc",
53
+ ];
54
+ const MODE_META = {
55
+ synastry: {
56
+ outerLabel: (_, name) => name ?? "Person 2",
57
+ innerRole: "Person 1 (inner)",
58
+ outerRole: "Person 2 (outer)",
59
+ credits: 1,
60
+ modeLabel: "Synastry",
61
+ },
62
+ transit: {
63
+ outerLabel: (dt) => `Transit · ${dt.slice(0, 10)}`,
64
+ innerRole: "Natal (inner)",
65
+ outerRole: "Transits (outer)",
66
+ credits: 1,
67
+ modeLabel: "Transit",
68
+ },
69
+ progressed: {
70
+ outerLabel: (dt) => `Progressions · ${dt.slice(0, 10)}`,
71
+ innerRole: "Natal (inner)",
72
+ outerRole: "Progressed (outer)",
73
+ credits: 1,
74
+ modeLabel: "Secondary Progressions",
75
+ },
76
+ solar_return: {
77
+ outerLabel: (dt) => `Solar Return · ${dt.slice(0, 4)}`,
78
+ innerRole: "Natal (inner)",
79
+ outerRole: "Solar Return (outer)",
80
+ credits: 5,
81
+ modeLabel: "Solar Return",
82
+ },
83
+ lunar_return: {
84
+ outerLabel: (dt) => `Lunar Return · ${dt.slice(0, 10)}`,
85
+ innerRole: "Natal (inner)",
86
+ outerRole: "Lunar Return (outer)",
87
+ credits: 5,
88
+ modeLabel: "Lunar Return",
89
+ },
90
+ solar_arc: {
91
+ outerLabel: (dt) => `Solar Arc · ${dt.slice(0, 10)}`,
92
+ innerRole: "Natal (inner)",
93
+ outerRole: "Solar Arc Directions (outer)",
94
+ credits: 1,
95
+ modeLabel: "Solar Arc Directions",
96
+ },
97
+ };
45
98
  // ── House system helpers (shared with chart-wheel) ────────────────────────────
46
99
  const HOUSE_SYSTEM_MAP = {
47
100
  placidus: "P", whole_sign: "W", equal: "E", koch: "K",
@@ -81,6 +134,108 @@ function buildTransitBody(transitDate, natalDatetime, lat, lon, timezone) {
81
134
  configuration: { house_system: HOUSE_SYSTEM_MAP["placidus"] },
82
135
  };
83
136
  }
137
+ /** POST /ephemeris/progressed — secondary progressions */
138
+ function buildProgressedBody(natalDatetime, lat, lon, timezone, targetDatetime) {
139
+ const body = {
140
+ subject: {
141
+ name: "MCP Request",
142
+ birth_datetime: { iso: natalDatetime },
143
+ birth_location: {
144
+ latitude: { decimal: lat ?? 0 },
145
+ longitude: { decimal: lon ?? 0 },
146
+ },
147
+ },
148
+ configuration: {
149
+ target_datetime: { iso: targetDatetime },
150
+ method: "secondary",
151
+ house_system: HOUSE_SYSTEM_MAP["placidus"],
152
+ },
153
+ };
154
+ if (timezone) {
155
+ body.subject.birth_location.timezone = { iana_name: timezone };
156
+ }
157
+ return body;
158
+ }
159
+ /** POST /predictive/returns/solar or /predictive/returns/lunar */
160
+ function buildReturnBody(natalDatetime, natalLat, natalLon, timezone, targetDatetime, returnLat, returnLon) {
161
+ const body = {
162
+ subject: {
163
+ name: "MCP Request",
164
+ birth_datetime: { iso: natalDatetime },
165
+ birth_location: {
166
+ latitude: { decimal: natalLat ?? 0 },
167
+ longitude: { decimal: natalLon ?? 0 },
168
+ },
169
+ },
170
+ return_location: {
171
+ latitude: { decimal: returnLat ?? natalLat ?? 0 },
172
+ longitude: { decimal: returnLon ?? natalLon ?? 0 },
173
+ },
174
+ target_datetime: { iso: targetDatetime },
175
+ configuration: { house_system: HOUSE_SYSTEM_MAP["placidus"] },
176
+ };
177
+ if (timezone) {
178
+ body.subject.birth_location.timezone = { iana_name: timezone };
179
+ }
180
+ return body;
181
+ }
182
+ /**
183
+ * Client-side solar arc: advance every natal planet longitude by elapsed years in degrees.
184
+ * Uses the Naibod rate (1 tropical year ≈ 0.9856°). The natal data object is cloned and
185
+ * the planets array/map is offset — houses are left at natal positions (standard practice).
186
+ */
187
+ function applySolarArcOffset(natalData, natalDatetime, targetDatetime) {
188
+ const natalYear = new Date(natalDatetime).getFullYear() +
189
+ new Date(natalDatetime).getMonth() / 12;
190
+ const targetYear = new Date(targetDatetime).getFullYear() +
191
+ new Date(targetDatetime).getMonth() / 12;
192
+ const arcDegrees = (targetYear - natalYear) * 0.9856; // Naibod rate
193
+ const offsetPlanets = (raw) => {
194
+ if (Array.isArray(raw)) {
195
+ return raw.map((p) => ({
196
+ ...p,
197
+ longitude: ((p.longitude ?? 0) + arcDegrees + 360) % 360,
198
+ }));
199
+ }
200
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
201
+ const out = {};
202
+ for (const [k, v] of Object.entries(raw)) {
203
+ if (v && typeof v === "object" && "longitude" in v) {
204
+ out[k] = { ...v, longitude: ((v.longitude ?? 0) + arcDegrees + 360) % 360 };
205
+ }
206
+ else {
207
+ out[k] = v;
208
+ }
209
+ }
210
+ return out;
211
+ }
212
+ return raw;
213
+ };
214
+ return {
215
+ ...natalData,
216
+ planets: offsetPlanets(natalData.planets),
217
+ _solar_arc_degrees: Math.round(arcDegrees * 1000) / 1000,
218
+ };
219
+ }
220
+ /** Route outer-wheel fetch to the correct API endpoint based on mode. */
221
+ async function fetchOuterChart(client, mode, dt1, lat1, lon1, tz1, dt2, lat2, lon2, tz2) {
222
+ switch (mode) {
223
+ case "synastry":
224
+ return client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2, lon2, tz2));
225
+ case "transit":
226
+ return client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2 ?? lat1, lon2 ?? lon1, tz2 ?? tz1));
227
+ case "progressed":
228
+ return client.post("/ephemeris/progressed", buildProgressedBody(dt1, lat1, lon1, tz1, dt2));
229
+ case "solar_return":
230
+ return client.post("/predictive/returns/solar", buildReturnBody(dt1, lat1, lon1, tz1, dt2, lat2 ?? lat1, lon2 ?? lon1));
231
+ case "lunar_return":
232
+ return client.post("/predictive/returns/lunar", buildReturnBody(dt1, lat1, lon1, tz1, dt2, lat2 ?? lat1, lon2 ?? lon1));
233
+ case "solar_arc": {
234
+ const natal = await client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1));
235
+ return applySolarArcOffset(natal, dt1, dt2);
236
+ }
237
+ }
238
+ }
84
239
  const CROSS_ASPECT_DEFS = [
85
240
  { type: "conjunction", angle: 0, orb: 8 },
86
241
  { type: "opposition", angle: 180, orb: 8 },
@@ -196,10 +351,10 @@ function buildBiWheelSummary(innerPlanets, outerPlanets, crossAspects, mode, loc
196
351
  square: "□", sextile: "⚹",
197
352
  };
198
353
  const top5 = crossAspects.slice(0, 5).map((ca) => `• ${capitalize(ca.planet1)} ${ASPECT_SYMBOL[ca.type] ?? ca.type} ${capitalize(ca.planet2)} (${ca.orb.toFixed(1)}° orb${ca.applying ? ", applying" : ""})`);
199
- const modeLabel = mode === "synastry" ? "Synastry" : "Transit";
200
- const label1 = mode === "synastry" ? "Person 1 (inner)" : "Natal (inner)";
201
- const label2 = mode === "synastry" ? "Person 2 (outer)" : "Transit (outer)";
202
- return (`**${modeLabel} Bi-Wheel**\n\n` +
354
+ const meta = MODE_META[mode];
355
+ const label1 = meta.innerRole;
356
+ const label2 = meta.outerRole;
357
+ return (`**${meta.modeLabel} Bi-Wheel**\n\n` +
203
358
  `${label1}: ${location1} — ${innerBasics || "chart loaded"}\n` +
204
359
  `${label2}: ${location2}\n\n` +
205
360
  `**Top Cross-Aspects:**\n${top5.join("\n") || "No major cross-aspects within orb."}\n\n` +
@@ -208,12 +363,16 @@ function buildBiWheelSummary(innerPlanets, outerPlanets, crossAspects, mode, loc
208
363
  // ── Tool: explore_bi_wheel ─────────────────────────────────────────────────────
209
364
  registerTool({
210
365
  name: "explore_bi_wheel",
211
- description: "Generate an interactive bi-wheel chart for synastry (two natal charts) or transit analysis " +
212
- "(natal chart + transiting planets). In synastry mode, the inner wheel shows Person 1's natal " +
213
- "chart and the outer ring shows Person 2's natal chart. In transit mode, the inner wheel shows " +
214
- "the natal chart and the outer ring shows transiting planets for the given date. " +
366
+ description: "Generate an interactive bi-wheel chart comparing two astrological chart positions. " +
367
+ "The inner wheel is always Person 1's natal chart. The outer ring depends on mode:\n" +
368
+ " synastry — Person 2's natal chart (two-person compatibility). 1 credit.\n" +
369
+ " transit — Transiting planets for a given date. 1 credit.\n" +
370
+ "• progressed — Secondary progressed positions (person2_datetime = target date). 1 credit.\n" +
371
+ "• solar_return — Nearest solar return chart (person2_datetime = year to return for). 5 credits.\n" +
372
+ "• lunar_return — Nearest lunar return chart (person2_datetime = target month). 5 credits.\n" +
373
+ "• solar_arc — Solar arc directed positions (person2_datetime = target date). 1 credit.\n" +
215
374
  "Cross-aspects between both wheels are computed and displayed as coloured dashed lines. " +
216
- "Click any planet or cross-aspect line for interpretation.",
375
+ "Click any planet, aspect line, or house cusp for interpretation.",
217
376
  inputSchema: {
218
377
  type: "object",
219
378
  properties: {
@@ -230,14 +389,16 @@ registerTool({
230
389
  },
231
390
  person2_datetime: {
232
391
  type: "string",
233
- description: "ISO 8601 datetime for Person 2 (synastry) or the transit date (transit mode). Required.",
392
+ description: "ISO 8601 datetime for the outer wheel. Meaning depends on mode: " +
393
+ "synastry = Person 2 birth datetime; transit/progressed/solar_arc = target date; " +
394
+ "solar_return/lunar_return = any date within the target year/month.",
234
395
  },
235
- person2_latitude: { type: "number", description: "Birth latitude for Person 2 / transit location (decimal degrees)." },
236
- person2_longitude: { type: "number", description: "Birth longitude for Person 2 / transit location (decimal degrees)." },
237
- person2_timezone: { type: "string", description: "IANA timezone for Person 2 / transit (e.g. 'America/Chicago')." },
396
+ person2_latitude: { type: "number", description: "Birth latitude for Person 2 (synastry) or return/transit location. Defaults to Person 1 coords for single-person modes." },
397
+ person2_longitude: { type: "number", description: "Birth longitude for Person 2 (synastry) or return/transit location. Defaults to Person 1 coords for single-person modes." },
398
+ person2_timezone: { type: "string", description: "IANA timezone for Person 2 / outer chart (e.g. 'America/Chicago')." },
238
399
  person2_name: {
239
400
  type: "string",
240
- description: "Display name for Person 2 / Transit chart (e.g. 'Bob'). Used in the badge and summary.",
401
+ description: "Display name for Person 2 (synastry mode). Used in the badge and summary.",
241
402
  },
242
403
  location: {
243
404
  type: "string",
@@ -245,13 +406,12 @@ registerTool({
245
406
  },
246
407
  mode: {
247
408
  type: "string",
248
- enum: ["synastry", "transit"],
249
- description: "Chart mode: 'synastry' (two natal charts) or 'transit' (natal + transits). Defaults to 'synastry'.",
409
+ enum: ["synastry", "transit", "progressed", "solar_return", "lunar_return", "solar_arc"],
410
+ description: "Chart comparison mode. Defaults to 'synastry'.",
250
411
  },
251
412
  },
252
413
  required: ["person1_datetime", "person2_datetime",
253
- "person1_latitude", "person1_longitude",
254
- "person2_latitude", "person2_longitude"],
414
+ "person1_latitude", "person1_longitude"],
255
415
  },
256
416
  annotations: {
257
417
  title: "Interactive Bi-Wheel Explorer",
@@ -278,25 +438,12 @@ registerTool({
278
438
  const lon2 = args.person2_longitude;
279
439
  const tz2 = args.person2_timezone;
280
440
  const loc1 = String(args.person1_name ?? args.location ?? `${lat1 ?? "?"},${lon1 ?? "?"}`);
281
- const loc2 = mode === "transit"
282
- ? `Transit ${dt2.slice(0, 10)}`
283
- : String(args.person2_name ?? `${lat2 ?? ""},${lon2 ?? ""}`);
284
- // Fetch both charts in parallel
285
- let innerData;
286
- let outerData;
287
- if (mode === "synastry") {
288
- [innerData, outerData] = await Promise.all([
289
- client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
290
- client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2, lon2, tz2)),
291
- ]);
292
- }
293
- else {
294
- // Transit mode: inner = natal (dt1), outer = transits for dt2 at natal location
295
- [innerData, outerData] = await Promise.all([
296
- client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
297
- client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2 ?? lat1, lon2 ?? lon1, tz2 ?? tz1)),
298
- ]);
299
- }
441
+ const loc2 = MODE_META[mode].outerLabel(dt2, args.person2_name);
442
+ // Fetch inner (natal) and outer (mode-dependent) charts in parallel where possible
443
+ const [innerData, outerData] = await Promise.all([
444
+ client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
445
+ fetchOuterChart(client, mode, dt1, lat1, lon1, tz1, dt2, lat2, lon2, tz2),
446
+ ]);
300
447
  const innerPlanets = normalizePlanets(innerData.planets);
301
448
  const outerPlanets = normalizePlanets(outerData.planets);
302
449
  const crossAspects = computeCrossAspects(innerPlanets, outerPlanets);
@@ -324,11 +471,11 @@ registerTool({
324
471
  // ── Tool: bi_wheel_recalculate ──────────────────────────────────────────────
325
472
  registerTool({
326
473
  name: "bi_wheel_recalculate",
327
- description: "Recalculates a bi-wheel chart (synastry, transit, or progressed mode) with new parameters.",
474
+ description: "Recalculates a bi-wheel chart with new parameters. Supports all modes: synastry, transit, progressed, solar_return, lunar_return, solar_arc.",
328
475
  inputSchema: {
329
476
  type: "object",
330
477
  properties: {
331
- mode: { type: "string", enum: ["synastry", "transit", "progressed"] },
478
+ mode: { type: "string", enum: ALL_MODES },
332
479
  person1_datetime: { type: "string" },
333
480
  person1_latitude: { type: "number" },
334
481
  person1_longitude: { type: "number" },
@@ -339,7 +486,7 @@ registerTool({
339
486
  person2_longitude: { type: "number" },
340
487
  person2_timezone: { type: "string" },
341
488
  person2_name: { type: "string" },
342
- location: { type: "string" }, // fallback
489
+ location: { type: "string" },
343
490
  },
344
491
  required: ["mode", "person1_datetime", "person1_latitude", "person1_longitude", "person2_datetime"],
345
492
  },
@@ -357,30 +504,11 @@ registerTool({
357
504
  const lon2 = args.person2_longitude;
358
505
  const tz2 = args.person2_timezone;
359
506
  const loc1 = String(args.person1_name ?? args.location ?? `${lat1 ?? "?"},${lon1 ?? "?"}`);
360
- const loc2 = mode === "transit" ? `Transit ${dt2.slice(0, 10)}` : String(args.person2_name ?? `${lat2 ?? ""},${lon2 ?? ""}`);
361
- let innerData;
362
- let outerData;
363
- if (mode === "synastry") {
364
- [innerData, outerData] = await Promise.all([
365
- client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
366
- client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2, lon2, tz2)),
367
- ]);
368
- }
369
- else if (mode === "progressed") {
370
- [innerData, outerData] = await Promise.all([
371
- client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
372
- client.post("/predictive/progressed", {
373
- subject: { birth_datetime: { iso: dt1 }, birth_location: { latitude: { decimal: lat1 ?? 0 }, longitude: { decimal: lon1 ?? 0 }, timezone: tz1 ? { iana_name: tz1 } : undefined } },
374
- configuration: { target_datetime: { iso: dt2 }, method: "secondary" }
375
- }),
376
- ]);
377
- }
378
- else {
379
- [innerData, outerData] = await Promise.all([
380
- client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
381
- client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2 ?? lat1, lon2 ?? lon1, tz2 ?? tz1)),
382
- ]);
383
- }
507
+ const loc2 = MODE_META[mode].outerLabel(dt2, args.person2_name);
508
+ const [innerData, outerData] = await Promise.all([
509
+ client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
510
+ fetchOuterChart(client, mode, dt1, lat1, lon1, tz1, dt2, lat2, lon2, tz2),
511
+ ]);
384
512
  const params1 = { datetime: dt1, timezone: tz1 ?? null, latitude: lat1 ?? null, longitude: lon1 ?? null, location: loc1 };
385
513
  const params2 = { datetime: dt2, timezone: tz2 ?? null, latitude: lat2 ?? null, longitude: lon2 ?? null, location: loc2 };
386
514
  const payload = buildBiWheelPayload(innerData, outerData, mode, params1, params2);
@@ -393,19 +521,19 @@ registerTool({
393
521
  registerTool({
394
522
  name: "bi_wheel_on_cross_aspect_click",
395
523
  description: "Event handler called when the user clicks a cross-aspect line in the bi-wheel. " +
396
- "Returns a synastry or transit interpretation of that aspect between the two wheels. " +
524
+ "Returns an interpretation of that aspect contextualised by chart mode (synastry, transit, progressed, etc.). " +
397
525
  "This tool is called automatically by the bi-wheel UI; you do not need to call it directly.",
398
526
  inputSchema: {
399
527
  type: "object",
400
528
  properties: {
401
529
  planet1: { type: "string", description: "Planet name from the inner (natal) wheel" },
402
530
  wheel1: { type: "string", enum: ["inner", "outer"], description: "Which wheel planet1 belongs to" },
403
- planet2: { type: "string", description: "Planet name from the outer (synastry/transit) wheel" },
531
+ planet2: { type: "string", description: "Planet name from the outer wheel" },
404
532
  wheel2: { type: "string", enum: ["inner", "outer"], description: "Which wheel planet2 belongs to" },
405
533
  aspect_type: { type: "string", description: "Aspect type: conjunction, opposition, trine, square, sextile" },
406
534
  orb: { type: "number", description: "Orb in degrees" },
407
535
  applying: { type: "boolean", description: "True if the aspect is applying (tightening)" },
408
- mode: { type: "string", enum: ["synastry", "transit"], description: "Chart mode (synastry or transit). Determines interpretation style." },
536
+ mode: { type: "string", enum: ALL_MODES, description: "Chart mode. Determines interpretation style." },
409
537
  },
410
538
  required: ["planet1", "planet2", "aspect_type"],
411
539
  },
@@ -438,12 +566,12 @@ registerTool({
438
566
  type: "object",
439
567
  properties: {
440
568
  planet: { type: "string", description: "Planet name (e.g. 'saturn')" },
441
- wheel: { type: "string", enum: ["inner", "outer"], description: "Which wheel (inner=natal/person1, outer=synastry/transit)" },
569
+ wheel: { type: "string", enum: ["inner", "outer"], description: "Which wheel (inner=natal/person1, outer=mode-dependent)" },
442
570
  longitude: { type: "number", description: "Ecliptic longitude in degrees" },
443
571
  sign: { type: "string", description: "Zodiac sign" },
444
572
  house: { type: "number", description: "House number (1–12) if available" },
445
573
  retrograde: { type: "boolean", description: "Whether the planet is retrograde" },
446
- mode: { type: "string", enum: ["synastry", "transit"], description: "Chart mode" },
574
+ mode: { type: "string", enum: ALL_MODES, description: "Chart mode" },
447
575
  },
448
576
  required: ["planet", "wheel", "longitude"],
449
577
  },
@@ -452,16 +580,17 @@ registerTool({
452
580
  handler: async (args) => {
453
581
  const planet = String(args.planet);
454
582
  const wheel = args.wheel ?? "inner";
583
+ const mode = args.mode ?? "synastry";
455
584
  const sign = args.sign ? String(args.sign) : zodSignFromLon(Number(args.longitude));
456
585
  const houseNum = args.house != null ? Number(args.house) : null;
457
586
  const house = houseNum != null && houseNum > 0 ? ` in the ${ordinal(houseNum)} house` : "";
458
- const wheelLabel = wheel === "inner" ? "Natal / Person 1" : "Synastry / Transit";
587
+ const wheelLabel = wheel === "inner" ? MODE_META[mode].innerRole : MODE_META[mode].outerRole;
459
588
  const deg = `${Math.floor(Number(args.longitude) % 30)}° ${sign}`;
460
589
  return {
461
590
  content: [{
462
591
  type: "text",
463
592
  text: `**${capitalize(planet)} at ${deg}${house}** *(${wheelLabel})*\n\n` +
464
- interpretBiWheelPlanet(planet, sign, houseNum ?? 0, wheel, Boolean(args.retrograde)),
593
+ interpretBiWheelPlanet(planet, sign, houseNum ?? 0, wheel, Boolean(args.retrograde), mode),
465
594
  }],
466
595
  };
467
596
  },
@@ -555,7 +684,24 @@ function interpretBiWheelPlanet(planet, sign, house, wheel, retrograde = false,
555
684
  return (`**Transiting Planet:** Transiting ${capitalize(planet)} represents ${theme}. Moving through ${capitalize(sign)}, it carries the quality of ${signQuality}.${houseCtx}${retroCtx}\n\n` +
556
685
  `As a **transit**, this planet is temporarily activating the natal chart. Its influence is current and time-limited — watch for events, shifts in awareness, or opportunities related to its themes in the houses and planets it touches.`);
557
686
  }
687
+ else if (mode === "progressed") {
688
+ return (`**Progressed Planet:** Progressed ${capitalize(planet)} in ${capitalize(sign)} shows how the natal ${capitalize(planet)} archetype has evolved through lived experience.${houseCtx}${retroCtx}\n\n` +
689
+ `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.`);
690
+ }
691
+ else if (mode === "solar_arc") {
692
+ return (`**Solar Arc ${capitalize(planet)}:** This directed position represents natal ${capitalize(planet)} advanced by approximately 1° per year of life.${houseCtx}${retroCtx}\n\n` +
693
+ `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.`);
694
+ }
695
+ else if (mode === "solar_return") {
696
+ return (`**Solar Return ${capitalize(planet)}:** In this solar return chart, ${capitalize(planet)} falls at ${Math.floor((Number(sign) || 0) % 30)}° ${capitalize(sign)}.${houseCtx}${retroCtx}\n\n` +
697
+ `Solar return planets describe themes that will be especially prominent during this **solar year** — from this birthday to the next. ${capitalize(planet)} in the return chart highlights where the energy of ${theme} will be most active, especially in the house it occupies and through contacts to natal planets.`);
698
+ }
699
+ else if (mode === "lunar_return") {
700
+ return (`**Lunar Return ${capitalize(planet)}:** In this lunar return chart, ${capitalize(planet)} sets the tone for the coming **lunar month**.${houseCtx}${retroCtx}\n\n` +
701
+ `Lunar returns reset monthly as the Moon returns to its natal degree. ${capitalize(planet)}'s position here — through ${signQuality} — shapes the emotional and circumstantial flavour of the next four weeks. Contacts to natal planets or angles sharpen its influence considerably.`);
702
+ }
558
703
  else {
704
+ // synastry fallback
559
705
  return (`**Synastry Planet:** ${capitalize(planet)} represents ${theme}. In ${capitalize(sign)}, this manifests through ${signQuality}.${houseCtx}${retroCtx}\n\n` +
560
706
  `This is the **outer person's natal ${capitalize(planet)}** — when this planet makes contact with the inner chart, it activates the natal themes of both parties simultaneously, creating a dynamic of mutual recognition and activation.`);
561
707
  }
@@ -584,8 +730,11 @@ const CROSS_ASPECT_NATURE = {
584
730
  };
585
731
  function interpretCrossAspect(p1, p2, type, applying, mode) {
586
732
  const natures = CROSS_ASPECT_NATURE[type.toLowerCase()];
733
+ // Single-person time-based modes share the transit nature text; synastry/solar arc use synastry
734
+ const isTimeBased = mode === "transit" || mode === "progressed" || mode === "solar_arc" ||
735
+ mode === "solar_return" || mode === "lunar_return";
587
736
  const nature = natures
588
- ? (mode === "transit" ? natures.transit : natures.synastry)
737
+ ? (isTimeBased ? natures.transit : natures.synastry)
589
738
  : "creates a notable connection across the two charts.";
590
739
  let timing;
591
740
  if (mode === "transit") {
@@ -593,6 +742,17 @@ function interpretCrossAspect(p1, p2, type, applying, mode) {
593
742
  ? "This aspect is **applying** — its influence is building and moving toward maximum expression. The themes involved are intensifying now."
594
743
  : "This aspect is **separating** — its peak has passed; the energy is waning and the lessons of this transit are moving into integration.";
595
744
  }
745
+ else if (mode === "progressed" || mode === "solar_arc") {
746
+ timing = applying
747
+ ? "This aspect is **applying** — progressed and directed aspects move slowly, often unfolding over months or years before reaching exactitude. The themes it represents are gradually intensifying."
748
+ : "This aspect is **separating** — it has passed its peak of exactitude and is now in a phase of integration. The defining moment of this developmental aspect lies in the recent past.";
749
+ }
750
+ else if (mode === "solar_return") {
751
+ timing = "As a **solar return aspect**, this connection is active for the entire solar year from this birthday to the next. It describes a consistent theme or dynamic that will colour the whole year rather than arriving as a dated event.";
752
+ }
753
+ else if (mode === "lunar_return") {
754
+ timing = "As a **lunar return aspect**, this connection is active for the coming lunar month. It shapes the emotional atmosphere and events of the next four weeks before the next lunar return resets the cycle.";
755
+ }
596
756
  else {
597
757
  timing = "As a **synastry aspect**, this connection exists as a permanent feature of this relationship dynamic — it neither builds nor fades, but remains as a constant undercurrent in how these two people interact.";
598
758
  }
@@ -611,7 +771,7 @@ registerTool({
611
771
  house_number: { type: "number", description: "House number (1–12)" },
612
772
  sign: { type: "string", description: "Sign on the house cusp (inner natal wheel)" },
613
773
  cusp_longitude: { type: "number", description: "Cusp longitude in degrees" },
614
- mode: { type: "string", enum: ["synastry", "transit"], description: "Chart mode" },
774
+ mode: { type: "string", enum: ALL_MODES, description: "Chart mode" },
615
775
  },
616
776
  required: ["house_number"],
617
777
  },
@@ -625,14 +785,19 @@ registerTool({
625
785
  const signFlavor = sign
626
786
  ? `\n\n${capitalize(sign)} on this cusp emphasizes ${SIGN_HOUSE_FLAVOR[sign.toLowerCase()] ?? "its natural qualities"} in the matters of this house.`
627
787
  : "";
628
- const modeNote = mode === "transit"
629
- ? "\n\nWatch for transiting planets moving through this house they will activate and bring events related to these life areas during their passage."
630
- : "\n\nIn synastry, planets from the outer chart falling in this house create experiences related to these themes in the relationship — often powerfully felt by the person whose house is being activated.";
788
+ const HOUSE_MODE_NOTES = {
789
+ synastry: "\n\nIn synastry, planets from the outer natal chart falling in this house create experiences related to these themes in the relationship — often powerfully felt by the person whose house is being activated.",
790
+ transit: "\n\nWatch for transiting planets moving through this house they will activate and bring events related to these life areas during their passage.",
791
+ progressed: "\n\nAs progressed planets move through this house, they gradually shift the inner developmental focus toward these life themes. Natal house boundaries mark important life-chapter transitions in secondary progression work.",
792
+ solar_arc: "\n\nSolar arc directed planets entering this house signal a new chapter of life experience in these domains. The ingress year often corresponds to a major shift in the themes of this house.",
793
+ solar_return: "\n\nPlanets in this house in the solar return chart will be emphasised life areas for the coming year. A concentration of solar return planets here intensifies activity and attention in this domain.",
794
+ lunar_return: "\n\nPlanets in this house in the lunar return chart highlight the month's dominant emotional themes and practical focus areas. The lunar return ascendant and its house emphasis set the tone for the entire lunar cycle.",
795
+ };
631
796
  return {
632
797
  content: [{
633
798
  type: "text",
634
799
  text: `**House ${num}${sign ? ` — ${capitalize(sign)} on the cusp` : ""}** *(Inner Natal Wheel)*\n\n` +
635
- meaning + signFlavor + modeNote,
800
+ meaning + signFlavor + HOUSE_MODE_NOTES[mode],
636
801
  }],
637
802
  };
638
803
  },
@@ -648,9 +813,9 @@ registerTool({
648
813
  inputSchema: {
649
814
  type: "object",
650
815
  properties: {
651
- mode: { type: "string", enum: ["synastry", "transit"], description: "Chart mode" },
652
- person1_name: { type: "string", description: "Name or label for the inner wheel (natal/person 1)" },
653
- person2_name: { type: "string", description: "Name or label for the outer wheel (synastry person 2 or transit date)" },
816
+ mode: { type: "string", enum: ALL_MODES, description: "Chart mode" },
817
+ person1_name: { type: "string", description: "Name or label for the inner wheel (natal / person 1)" },
818
+ person2_name: { type: "string", description: "Name or label for the outer wheel (person 2, transit date, progression target, etc.)" },
654
819
  cross_aspects: {
655
820
  type: "array",
656
821
  description: "Array of cross-aspects from the bi-wheel payload (up to 30, sorted by orb)",
@@ -673,8 +838,9 @@ registerTool({
673
838
  _meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app", "model"] } },
674
839
  handler: async (args) => {
675
840
  const mode = args.mode ?? "synastry";
676
- const label1 = args.person1_name ? String(args.person1_name) : (mode === "transit" ? "Natal Chart" : "Person 1");
677
- const label2 = args.person2_name ? String(args.person2_name) : (mode === "transit" ? "Transits" : "Person 2");
841
+ const meta = MODE_META[mode];
842
+ const label1 = args.person1_name ? String(args.person1_name) : meta.innerRole;
843
+ const label2 = args.person2_name ? String(args.person2_name) : meta.outerRole;
678
844
  const topN = args.top_n ? Number(args.top_n) : 8;
679
845
  const rawAspects = args.cross_aspects ?? [];
680
846
  const aspects = rawAspects.slice(0, topN);
@@ -684,36 +850,55 @@ registerTool({
684
850
  const harmonious = aspects.filter((a) => ["trine", "sextile"].includes((a.aspect_type ?? a.type ?? "").toLowerCase()));
685
851
  const dynamic = aspects.filter((a) => ["square", "opposition"].includes((a.aspect_type ?? a.type ?? "").toLowerCase()));
686
852
  const fusing = aspects.filter((a) => (a.aspect_type ?? a.type ?? "").toLowerCase() === "conjunction");
853
+ // For time-based modes, show applying/separating; for relational modes show tightness
854
+ const isTimeBased = mode !== "synastry";
687
855
  const lines = aspects.map((ca) => {
688
856
  const type = ca.aspect_type ?? ca.type ?? "aspect";
689
857
  const sym = ASPECT_SYM[type.toLowerCase()] ?? type;
690
- const dir = mode === "transit"
858
+ const dir = isTimeBased
691
859
  ? (ca.applying ? "↗ applying" : "↘ separating")
692
860
  : (ca.orb <= 1 ? "exact" : ca.orb <= 3 ? "tight" : "wide");
693
861
  return `• ${capitalize(ca.planet1)} ${sym} ${capitalize(ca.planet2)} (${ca.orb.toFixed(1)}° — ${dir})`;
694
862
  });
695
- const themeSummary = mode === "synastry"
696
- ? [
697
- fusing.length > 0 ? `**${fusing.length} conjunction${fusing.length > 1 ? "s" : ""}** — areas of deep merging and fusion between the two charts.` : "",
698
- harmonious.length > 0 ? `**${harmonious.length} flowing aspect${harmonious.length > 1 ? "s" : ""}** (trines/sextiles) — natural ease, support, and compatibility.` : "",
699
- dynamic.length > 0 ? `**${dynamic.length} dynamic aspect${dynamic.length > 1 ? "s" : ""}** (squares/oppositions) — areas of growth, friction, and mutual challenge.` : "",
700
- ].filter(Boolean).join("\n")
701
- : [
702
- fusing.length > 0 ? `**${fusing.length} conjunction${fusing.length > 1 ? "s" : ""}** — direct activation of natal points; peak transit influence.` : "",
703
- harmonious.length > 0 ? `**${harmonious.length} flowing transit${harmonious.length > 1 ? "s" : ""}** — periods of ease, opportunity, and support.` : "",
704
- dynamic.length > 0 ? `**${dynamic.length} challenging transit${dynamic.length > 1 ? "s" : ""}** — active pressure points requiring conscious engagement.` : "",
705
- ].filter(Boolean).join("\n");
706
- const intro = mode === "synastry"
707
- ? `**Synastry Overview: ${label1} × ${label2}**\n\nThis synastry bi-wheel compares two natal charts to reveal the dynamics of interaction and connection between these two individuals. The cross-aspects show how their planetary energies align, challenge, and transform each other.`
708
- : `**Transit Overview: ${label1} × ${label2}**\n\nThis transit bi-wheel overlays current planetary positions onto the natal chart, revealing which natal points are being activated right now and the nature of those influences.`;
863
+ // Theme summary copy varies by mode
864
+ const synastryTheme = [
865
+ fusing.length > 0 ? `**${fusing.length} conjunction${fusing.length > 1 ? "s" : ""}** — areas of deep merging and fusion between the two charts.` : "",
866
+ harmonious.length > 0 ? `**${harmonious.length} flowing aspect${harmonious.length > 1 ? "s" : ""}** (trines/sextiles) — natural ease, support, and compatibility.` : "",
867
+ dynamic.length > 0 ? `**${dynamic.length} dynamic aspect${dynamic.length > 1 ? "s" : ""}** (squares/oppositions) — areas of growth, friction, and mutual challenge.` : "",
868
+ ].filter(Boolean).join("\n");
869
+ const timeBasedTheme = [
870
+ fusing.length > 0 ? `**${fusing.length} conjunction${fusing.length > 1 ? "s" : ""}** — direct activation of natal points; peak influence for this ${meta.modeLabel.toLowerCase()}.` : "",
871
+ harmonious.length > 0 ? `**${harmonious.length} flowing aspect${harmonious.length > 1 ? "s" : ""}** (trines/sextiles) — periods of ease, opportunity, and natural support.` : "",
872
+ dynamic.length > 0 ? `**${dynamic.length} dynamic aspect${dynamic.length > 1 ? "s" : ""}** (squares/oppositions) — active pressure points requiring conscious engagement.` : "",
873
+ ].filter(Boolean).join("\n");
874
+ const themeSummary = mode === "synastry" ? synastryTheme : timeBasedTheme;
875
+ const SYNOPSIS_INTRO = {
876
+ synastry: `**Synastry Overview: ${label1} × ${label2}**\n\n` +
877
+ `This synastry bi-wheel compares two natal charts to reveal the dynamics of interaction and connection between these two individuals. The cross-aspects show how their planetary energies align, challenge, and transform each other.`,
878
+ transit: `**Transit Overview: ${label1} × ${label2}**\n\n` +
879
+ `This transit bi-wheel overlays current planetary positions onto the natal chart, revealing which natal points are being activated right now and the nature of those influences.`,
880
+ progressed: `**Secondary Progressions Overview: ${label1} × ${label2}**\n\n` +
881
+ `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.`,
882
+ solar_arc: `**Solar Arc Directions Overview: ${label1} × ${label2}**\n\n` +
883
+ `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.`,
884
+ solar_return: `**Solar Return Overview: ${label1} × ${label2}**\n\n` +
885
+ `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.`,
886
+ lunar_return: `**Lunar Return Overview: ${label1} × ${label2}**\n\n` +
887
+ `This lunar return bi-wheel compares the natal chart against the lunar return. The outer ring shows planetary positions when the Moon returned to its natal degree, setting the emotional tone and circumstantial focus of the coming lunar month.`,
888
+ };
889
+ const SYNOPSIS_CLOSING = {
890
+ synastry: "\n\nClick any planet or aspect line in the bi-wheel for a deeper interpretation of individual connections.",
891
+ transit: "\n\nClick any planet or cross-aspect line for a detailed transit interpretation. Navigate the date forward or backward using the step controls to track how transits evolve.",
892
+ progressed: "\n\nClick any progressed planet or cross-aspect line for a detailed interpretation. The most significant turning points occur when a progressed planet reaches exactitude with a natal planet or angle.",
893
+ solar_arc: "\n\nClick any directed planet or cross-aspect line for interpretation. Solar arc contacts to natal angles (ASC, MC, IC, DSC) are among the most powerful triggers in predictive astrology.",
894
+ solar_return: "\n\nClick any solar return planet or cross-aspect line for interpretation. The solar return ascendant and its ruler are the master theme-setters for the year.",
895
+ lunar_return: "\n\nClick any lunar return planet or cross-aspect line for interpretation. The lunar return Moon's house placement describes this month's emotional centre of gravity.",
896
+ };
709
897
  const body = lines.length > 0
710
898
  ? `\n\n**Top Cross-Aspects by Orb:**\n${lines.join("\n")}\n\n**Thematic Summary:**\n${themeSummary || "No major aspects within standard orbs."}`
711
899
  : "\n\nNo major cross-aspects are within orb at this time.";
712
- const closing = mode === "synastry"
713
- ? "\n\nClick any planet or aspect line in the bi-wheel for a deeper interpretation of individual connections."
714
- : "\n\nClick any planet or cross-aspect line for a detailed transit interpretation. Navigate the date forward or backward using the step controls to track how transits evolve.";
715
900
  return {
716
- content: [{ type: "text", text: intro + body + closing }],
901
+ content: [{ type: "text", text: SYNOPSIS_INTRO[mode] + body + SYNOPSIS_CLOSING[mode] }],
717
902
  };
718
903
  },
719
904
  });