@openephemeris/mcp-server 3.13.7 → 3.13.8

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/README.md CHANGED
@@ -355,8 +355,8 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
355
355
 
356
356
  - Allowlisted operations: **28**
357
357
  - Methods: `GET=4`, `POST=24`, `PUT=0`, `PATCH=0`, `DELETE=0`
358
- - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **64**
359
- - Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bi_wheel_on_cross_aspect_click`, `bi_wheel_on_planet_click`, `chart_wheel_on_aspect_click`, `chart_wheel_on_house_click`, `chart_wheel_on_planet_click`, `chart_wheel_recalculate`, `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`, `explore_bi_wheel`, `explore_human_design`, `explore_natal_chart`, `hd_on_center_click`, `hd_on_channel_click`, `hd_on_gate_click`, `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`
358
+ - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **72**
359
+ - Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bi_wheel_on_cross_aspect_click`, `bi_wheel_on_house_click`, `bi_wheel_on_planet_click`, `bi_wheel_recalculate`, `bi_wheel_synopsis`, `bodygraph_recalculate`, `chart_wheel_on_aspect_click`, `chart_wheel_on_house_click`, `chart_wheel_on_planet_click`, `chart_wheel_recalculate`, `chinese_bazi`, `dev_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`, `explore_bi_wheel`, `explore_human_design`, `explore_natal_chart`, `hd_on_center_click`, `hd_on_channel_click`, `hd_on_gate_click`, `hd_on_planet_click`, `hd_on_variable_click`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `location_search`, `timezone_resolve`, `vedic_chart`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
360
360
  - Generic tools:
361
361
 
362
362
  ### Allowlist Families
@@ -15,7 +15,7 @@ const BINARY_ENDPOINT_PREFIXES = [
15
15
  ];
16
16
  const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboard?tab=account";
17
17
  const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
18
- const UPGRADE_URL = "https://openephemeris.com/pay";
18
+ const UPGRADE_URL = "https://openephemeris.com/pricing";
19
19
  function sleep(ms) {
20
20
  return new Promise((resolve) => setTimeout(resolve, ms));
21
21
  }
@@ -251,18 +251,36 @@ export class BackendClient {
251
251
  `or set OPENEPHEMERIS_API_KEY in your MCP server config.`, 401, "auth_required", false, LOGIN_SIGNUP_URL);
252
252
  }
253
253
  if (status === 402) {
254
+ const dataObj = data && typeof data === "object" ? data : {};
255
+ const walletUrl = dataObj.upgrade?.wallets_url || "https://openephemeris.com/wallet";
256
+ const currentTier = dataObj.upgrade?.current_tier || dataObj.tier || "explorer";
257
+ const isPro = currentTier === "developer" || currentTier === "pro" || currentTier === "startup" || currentTier === "scale";
258
+ let upsellMsg = "";
259
+ if (currentTier === 'explorer') {
260
+ upsellMsg =
261
+ `Let the user know warmly: Their 150 free discovery credits are used up. ` +
262
+ `They can top up their wallet starting at $5 for 150 credits at ` +
263
+ `${walletUrl} — or upgrade to Pro ($29/mo) ` +
264
+ `for 75,000 credits/month at ${UPGRADE_URL}`;
265
+ }
266
+ else if (currentTier === 'payg') {
267
+ upsellMsg =
268
+ `Let the user know warmly: Their wallet balance is empty. They can top up at ` +
269
+ `${walletUrl} — $5 for 150 credits, ` +
270
+ `$10 for 350, or $20 for 900.`;
271
+ }
272
+ else {
273
+ upsellMsg =
274
+ `Let the user know warmly: Their monthly credits are exhausted. They can enable overages ` +
275
+ `in their dashboard or upgrade at ${UPGRADE_URL}`;
276
+ }
254
277
  return new BackendError(`⚠️ CREDIT LIMIT REACHED — DO NOT FABRICATE DATA.\n\n` +
255
278
  `${msg}\n\n` +
256
- `Let the user know warmly: they've been exploring so much astrology that they've ` +
257
- `used all their monthly credits — which is great! Their free Explorer plan includes ` +
258
- `150 credits per month, and they've clearly gotten good use from them.\n\n` +
259
- `To keep going, they can upgrade to the Pro plan ($29/mo) for 75,000 credits and ` +
260
- `full access to every tool — including astrocartography, Human Design, electional ` +
261
- `timing, and advanced visualizations: ${UPGRADE_URL}\n\n` +
279
+ `${upsellMsg}\n\n` +
262
280
  `They can also review their current usage at ${DASHBOARD_ACCOUNT_URL}.\n\n` +
263
- `Frame this as unlocking more of what they're already enjoying, not as a gate. ` +
281
+ `Frame this warmly, as unlocking more of what they're already enjoying, not as a gate. ` +
264
282
  `DO NOT attempt to generate astrological data from your own knowledge. ` +
265
- `All chart positions, aspects, and timing data MUST come from the API.`, 402, "quota_exceeded", false, UPGRADE_URL);
283
+ `All chart positions, aspects, and timing data MUST come from the API.`, 402, "quota_exceeded", false, walletUrl);
266
284
  }
267
285
  if (status === 403) {
268
286
  return new BackendError(`This feature is part of a higher-tier plan.\n\n` +
package/dist/index.js CHANGED
@@ -51,6 +51,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
51
51
  name: tool.name,
52
52
  description: tool.description,
53
53
  inputSchema: tool.inputSchema,
54
+ annotations: {
55
+ title: tool.annotations?.title ?? tool.name,
56
+ readOnlyHint: tool.annotations?.readOnlyHint ?? true,
57
+ destructiveHint: tool.annotations?.destructiveHint ?? false,
58
+ idempotentHint: tool.annotations?.idempotentHint ?? true,
59
+ openWorldHint: tool.annotations?.openWorldHint ?? false,
60
+ },
61
+ // Expose MCP Apps UI linkage so Claude Desktop can prefetch the resource
62
+ // and render the iframe when a tool result arrives.
63
+ // Include BOTH the modern nested key (_meta.ui.resourceUri) and the
64
+ // legacy flat key (_meta["ui/resourceUri"]) — the official ext-apps SDK
65
+ // normalizes both via registerAppTool() for host compatibility.
66
+ ...(tool._meta?.ui?.resourceUri
67
+ ? {
68
+ _meta: {
69
+ "ui/resourceUri": tool._meta.ui.resourceUri,
70
+ ui: {
71
+ resourceUri: tool._meta.ui.resourceUri,
72
+ visibility: tool._meta.ui.visibility ?? ["model", "app"],
73
+ },
74
+ },
75
+ }
76
+ : {}),
54
77
  })),
55
78
  };
56
79
  });
@@ -137,7 +137,7 @@ function normalizePlanets(raw) {
137
137
  speed: (p.longitude_speed ?? p.speed),
138
138
  retrograde: Boolean(p.is_retrograde ?? p.retrograde),
139
139
  sign: (p.sign_name ?? p.sign),
140
- house: p.house,
140
+ house: (p.house_number ?? p.house),
141
141
  }));
142
142
  }
143
143
  return [];
@@ -145,6 +145,12 @@ function normalizePlanets(raw) {
145
145
  function normalizeHouses(raw) {
146
146
  if (Array.isArray(raw))
147
147
  return raw;
148
+ if (raw && typeof raw === "object" && "cusps" in raw && Array.isArray(raw.cusps)) {
149
+ return raw.cusps.map((cusp, i) => ({
150
+ number: i + 1,
151
+ cusp_lon: cusp,
152
+ }));
153
+ }
148
154
  return [];
149
155
  }
150
156
  // ── Payload builder ────────────────────────────────────────────────────────────
@@ -306,6 +312,7 @@ registerTool({
306
312
  { type: "text", text: JSON.stringify({ ...payload, server_version: SERVER_VERSION }) },
307
313
  ],
308
314
  _meta: {
315
+ "ui/resourceUri": BI_WHEEL_RESOURCE_URI,
309
316
  ui: { resourceUri: BI_WHEEL_RESOURCE_URI },
310
317
  },
311
318
  };
@@ -314,6 +321,74 @@ registerTool({
314
321
  return { content: [{ type: "text", text: summary }] };
315
322
  },
316
323
  });
324
+ // ── Tool: bi_wheel_recalculate ──────────────────────────────────────────────
325
+ registerTool({
326
+ name: "bi_wheel_recalculate",
327
+ description: "Recalculates a bi-wheel chart (synastry, transit, or progressed mode) with new parameters.",
328
+ inputSchema: {
329
+ type: "object",
330
+ properties: {
331
+ mode: { type: "string", enum: ["synastry", "transit", "progressed"] },
332
+ person1_datetime: { type: "string" },
333
+ person1_latitude: { type: "number" },
334
+ person1_longitude: { type: "number" },
335
+ person1_timezone: { type: "string" },
336
+ person1_name: { type: "string" },
337
+ person2_datetime: { type: "string" },
338
+ person2_latitude: { type: "number" },
339
+ person2_longitude: { type: "number" },
340
+ person2_timezone: { type: "string" },
341
+ person2_name: { type: "string" },
342
+ location: { type: "string" }, // fallback
343
+ },
344
+ required: ["mode", "person1_datetime", "person1_latitude", "person1_longitude", "person2_datetime"],
345
+ },
346
+ annotations: { title: "Recalculate Bi-Wheel", readOnlyHint: true, openWorldHint: false },
347
+ _meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
348
+ handler: async (args) => {
349
+ const client = getActiveClient();
350
+ const mode = args.mode ?? "synastry";
351
+ const dt1 = String(args.person1_datetime);
352
+ const lat1 = args.person1_latitude;
353
+ const lon1 = args.person1_longitude;
354
+ const tz1 = args.person1_timezone;
355
+ const dt2 = String(args.person2_datetime);
356
+ const lat2 = args.person2_latitude;
357
+ const lon2 = args.person2_longitude;
358
+ const tz2 = args.person2_timezone;
359
+ 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
+ }
384
+ const params1 = { datetime: dt1, timezone: tz1 ?? null, latitude: lat1 ?? null, longitude: lon1 ?? null, location: loc1 };
385
+ const params2 = { datetime: dt2, timezone: tz2 ?? null, latitude: lat2 ?? null, longitude: lon2 ?? null, location: loc2 };
386
+ const payload = buildBiWheelPayload(innerData, outerData, mode, params1, params2);
387
+ return {
388
+ content: [{ type: "text", text: JSON.stringify({ ...payload, server_version: SERVER_VERSION }) }],
389
+ };
390
+ },
391
+ });
317
392
  // ── Tool: bi_wheel_on_cross_aspect_click ──────────────────────────────────────
318
393
  registerTool({
319
394
  name: "bi_wheel_on_cross_aspect_click",
@@ -416,42 +491,229 @@ const PLANET_THEMES = {
416
491
  mc: "public vocation, highest ambition, and the world's face; the pinnacle of achievement this person strives toward",
417
492
  };
418
493
  const HOUSE_AREAS = {
419
- 1: "self and identity", 2: "values and resources",
420
- 3: "communication", 4: "home and roots",
421
- 5: "creativity and joy", 6: "work and health",
422
- 7: "partnerships", 8: "shared depth and transformation",
423
- 9: "beliefs and expansion", 10: "career and public life",
424
- 11: "community and hopes", 12: "solitude and the unconscious",
494
+ 1: "self, appearance, and first impressions",
495
+ 2: "money, material values, and personal resources",
496
+ 3: "communication, siblings, and local environment",
497
+ 4: "home, family, roots, and emotional foundations",
498
+ 5: "creativity, romance, children, and pleasure",
499
+ 6: "work, health, routines, and service",
500
+ 7: "partnerships, marriage, and close one-on-one relationships",
501
+ 8: "transformation, shared resources, sexuality, and the depths",
502
+ 9: "philosophy, higher education, long travel, and belief",
503
+ 10: "career, public reputation, and life calling",
504
+ 11: "friendships, groups, collective causes, and long-term hopes",
505
+ 12: "solitude, the unconscious, hidden matters, and spiritual retreat",
506
+ };
507
+ const HOUSE_MEANINGS = {
508
+ 1: "The 1st house governs identity, appearance, and how we project ourselves into the world. In synastry, planets here from the outer wheel can reshape how this person is perceived or how they see themselves.",
509
+ 2: "The 2nd house governs personal values, self-worth, and material security. Synastry or transit planets here trigger questions around money, possessions, and what one truly values.",
510
+ 3: "The 3rd house governs communication, learning, short journeys, and the immediate environment. Planets here stimulate the mind, increase dialogue, and sharpen or soften communication.",
511
+ 4: "The 4th house (IC) governs the home, family, ancestry, and emotional foundations. This is the most private sector of the chart — planets here reach deep into the inner life.",
512
+ 5: "The 5th house governs creativity, romance, play, and children. Synastry planets here light up pleasure-seeking, creative collaboration, and romantic sparks.",
513
+ 6: "The 6th house governs work, health, and daily rhythm. Planets here affect how one shows up day-to-day — discipline, habits, service, and the body.",
514
+ 7: "The 7th house (Descendant) governs committed partnerships. Planets here activate relationship patterns — the qualities we project onto partners or seek in them.",
515
+ 8: "The 8th house governs deep transformation, shared finances, intimacy, and taboo. Planets here create intense, soul-level connections or power dynamics that cannot be ignored.",
516
+ 9: "The 9th house governs worldview, higher learning, travel, and spiritual quest. Planets here expand perspective, challenge beliefs, and open new intellectual horizons.",
517
+ 10: "The 10th house (Midheaven) governs career, public life, and legacy. Planets here can profoundly affect one's sense of purpose and how the outer world recognizes them.",
518
+ 11: "The 11th house governs friends, networks, social causes, and future visions. Planets here energize collective activity, shared ideals, and the aspiration toward belonging.",
519
+ 12: "The 12th house governs the unconscious, hidden matters, solitude, and spiritual development. Planets here work in subtle and sometimes invisible ways — potent but beneath the surface.",
425
520
  };
426
- function interpretBiWheelPlanet(planet, sign, house, wheel, retrograde = false) {
521
+ const SIGN_QUALITIES = {
522
+ aries: "directness, initiative, and pioneering courage",
523
+ taurus: "steady determination, sensory pleasure, and practical patience",
524
+ gemini: "curiosity, adaptability, and intellectual versatility",
525
+ cancer: "emotional depth, nurturing instincts, and protective sensitivity",
526
+ leo: "bold self-expression, generosity, and regal confidence",
527
+ virgo: "precision, discernment, and devoted service to improvement",
528
+ libra: "harmony-seeking, relational grace, and aesthetic refinement",
529
+ scorpio: "intense focus, emotional depth, and transformative power",
530
+ sagittarius: "philosophical expansiveness, optimism, and adventurous seeking",
531
+ capricorn: "disciplined ambition, structural mastery, and long-term commitment",
532
+ aquarius: "innovative originality, humanitarian vision, and detached idealism",
533
+ pisces: "compassionate empathy, spiritual receptivity, and imaginative flow",
534
+ };
535
+ const SIGN_HOUSE_FLAVOR = {
536
+ aries: "initiative and directness", taurus: "patience and material stability",
537
+ gemini: "intellectual curiosity and communication", cancer: "emotional nurturing and memory",
538
+ leo: "creative expression and leadership", virgo: "analytical precision and service",
539
+ libra: "balance and relationship dynamics", scorpio: "depth, intensity, and transformation",
540
+ sagittarius: "expansion and philosophical seeking", capricorn: "discipline, structure, and achievement",
541
+ aquarius: "innovation, groups, and humanitarian ideals", pisces: "compassion, dreams, and dissolution of boundaries",
542
+ };
543
+ function interpretBiWheelPlanet(planet, sign, house, wheel, retrograde = false, mode = "synastry") {
427
544
  const theme = PLANET_THEMES[planet.toLowerCase()] ?? "a deep archetypal force";
428
- const houseCtx = house ? ` This energy operates in the sphere of ${HOUSE_AREAS[house] ?? "life experience"}.` : "";
545
+ const signQuality = SIGN_QUALITIES[sign.toLowerCase()] ?? "the qualities of this sign";
546
+ const houseCtx = house ? ` This energy is active in the **${ordinal(house)} house** — the sphere of ${HOUSE_AREAS[house] ?? "life experience"}.` : "";
429
547
  const retroCtx = retrograde
430
- ? ` Note: this planet is **retrograde** — its energy tends to be more internalized, reflective, or nonconforming in its expression.`
548
+ ? ` Note: this planet is **retrograde** — its energy turns inward; themes are more private, reflective, and may require more deliberate integration before they can be expressed outward.`
431
549
  : "";
432
- const wheelCtx = wheel === "inner"
433
- ? "This is a **natal planet** a core, stable part of this person's identity."
434
- : "This is an **outer planet** — a transiting or synastry energy currently activating the inner wheel.";
435
- return (`${capitalize(planet)} represents ${theme}. In ${capitalize(sign)}, this energy is expressed through the qualities of that sign.${houseCtx}${retroCtx}\n\n${wheelCtx}`);
550
+ if (wheel === "inner") {
551
+ return (`**Natal Planet:** ${capitalize(planet)} represents ${theme}. In ${capitalize(sign)}, this energy flows through ${signQuality}.${houseCtx}${retroCtx}\n\n` +
552
+ `This is a **core natal placement** — a stable, defining feature of this person's chart. It shows how ${capitalize(planet)}'s themes are wired into their fundamental nature.`);
553
+ }
554
+ else if (mode === "transit") {
555
+ return (`**Transiting Planet:** Transiting ${capitalize(planet)} represents ${theme}. Moving through ${capitalize(sign)}, it carries the quality of ${signQuality}.${houseCtx}${retroCtx}\n\n` +
556
+ `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
+ }
558
+ else {
559
+ return (`**Synastry Planet:** ${capitalize(planet)} represents ${theme}. In ${capitalize(sign)}, this manifests through ${signQuality}.${houseCtx}${retroCtx}\n\n` +
560
+ `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
+ }
436
562
  }
437
563
  const CROSS_ASPECT_NATURE = {
438
- conjunction: "creates a powerful merging of these two planetary energies across the chart boundary. What one person (or the natal chart) embodies, the other amplifies — this can feel fated, intense, and highly activating.",
439
- opposition: "creates a magnetic pull-and-push dynamic. The two energies are drawn to each other yet challenge each other to grow. In synastry, this often manifests as fascination and tension; in transit, as external pressure to integrate.",
440
- trine: "creates an easy, harmonious flow of energy between the two charts. This aspect supports mutual understanding, natural rapport, and a sense of ease gifts arrive without much effort.",
441
- square: "creates productive friction between the two charts. Growth and evolution are required. In synastry, this generates passion and drive; in transit, it signals action and adjustment.",
442
- sextile: "creates a gentle, supportive connection that rewards intentional effort. Opportunities arise where these two energies meet, but initiative is needed to activate them fully.",
564
+ conjunction: {
565
+ synastry: "creates a powerful merging of these two planetary energies across both charts. What one person embodies, the other amplifies this can feel fated, magnetic, and highly activating. The themes of both planets fuse and are difficult to separate.",
566
+ transit: "marks a moment of direct activation. The transiting planet sits exactly upon a natal planet, amplifying its themes with full force. This is often experienced as an event, a catalytic encounter, or a decisive shift in awareness.",
567
+ },
568
+ opposition: {
569
+ synastry: "creates a magnetic polarity across the chart boundary. The two energies are drawn to each other yet challenge integration — each person may embody qualities the other both desires and finds difficult. Fascination and tension coexist.",
570
+ transit: "brings an external pressure that demands integration. The transiting planet opposes a natal point, often projecting themes outward through encounters, relationships, or confrontations that mirror what needs to be balanced internally.",
571
+ },
572
+ trine: {
573
+ synastry: "creates an easy, harmonious flow between the two charts. Natural rapport, ease of connection, and effortless support arise where these themes meet. This feels comfortable — even familiar — often from the very first interaction.",
574
+ transit: "brings a period of natural ease and opportunity. The themes of the transiting planet flow harmoniously into the natal point — doors open, situations resolve with less effort, and the native feels supported by outer circumstances.",
575
+ },
576
+ square: {
577
+ synastry: "creates productive friction between the two charts. Life together requires growth and adjustment. This dynamic generates passion and drive, but also tension that must be consciously worked with rather than avoided. The reward is mutual evolution.",
578
+ transit: "signals a call to action. Current circumstances are pressing against a natal point, requiring adjustment, decision, or effort. Discomfort is the teacher here — the square challenges, but in doing so, builds capacity and strength.",
579
+ },
580
+ sextile: {
581
+ synastry: "creates a gentle, supportive connection that rewards intentional effort. The ease is more subtle than a trine, but the potential for growth is real. Collaboration, mutual curiosity, and opening to new shared possibilities arise here.",
582
+ transit: "brings a period of opportunity that rewards initiative. The transiting planet offers a supportive opening — but unlike a trine, it requires the native to consciously engage and take action rather than simply receiving.",
583
+ },
443
584
  };
444
585
  function interpretCrossAspect(p1, p2, type, applying, mode) {
445
- const nature = CROSS_ASPECT_NATURE[type.toLowerCase()] ?? "creates a notable connection across the two charts.";
586
+ const natures = CROSS_ASPECT_NATURE[type.toLowerCase()];
587
+ const nature = natures
588
+ ? (mode === "transit" ? natures.transit : natures.synastry)
589
+ : "creates a notable connection across the two charts.";
446
590
  let timing;
447
591
  if (mode === "transit") {
448
592
  timing = applying
449
- ? "This aspect is **applying** — its influence is building and moving toward maximum expression."
450
- : "This aspect is **separating** — its peak has passed; integration of its themes is underway.";
593
+ ? "This aspect is **applying** — its influence is building and moving toward maximum expression. The themes involved are intensifying now."
594
+ : "This aspect is **separating** — its peak has passed; the energy is waning and the lessons of this transit are moving into integration.";
451
595
  }
452
596
  else {
453
- // In synastry, both charts are frozenapplying/separating has no temporal meaning.
454
- timing = "As a synastry aspect, this connection exists as a permanent feature of this relationship dynamic — neither building nor fading, but always present as a foundational thread between these two charts.";
597
+ 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.";
455
598
  }
456
599
  return (`The ${type} between ${capitalize(p1)} and ${capitalize(p2)} ${nature}\n\n${timing}`);
457
600
  }
601
+ // ── Tool: bi_wheel_on_house_click ─────────────────────────────────────────────
602
+ registerTool({
603
+ name: "bi_wheel_on_house_click",
604
+ description: "Event handler called when the user clicks a house cusp in the inner wheel of the bi-wheel chart. " +
605
+ "Returns the meaning of that house in the context of synastry or transit — what life area it governs " +
606
+ "and how outer planets activating it tend to manifest. " +
607
+ "This tool is called automatically by the bi-wheel UI; you do not need to call it directly.",
608
+ inputSchema: {
609
+ type: "object",
610
+ properties: {
611
+ house_number: { type: "number", description: "House number (1–12)" },
612
+ sign: { type: "string", description: "Sign on the house cusp (inner natal wheel)" },
613
+ cusp_longitude: { type: "number", description: "Cusp longitude in degrees" },
614
+ mode: { type: "string", enum: ["synastry", "transit"], description: "Chart mode" },
615
+ },
616
+ required: ["house_number"],
617
+ },
618
+ annotations: { title: "Bi-Wheel House Interpretation", readOnlyHint: true, openWorldHint: false },
619
+ _meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
620
+ handler: async (args) => {
621
+ const num = Number(args.house_number);
622
+ const sign = args.sign ? String(args.sign) : "";
623
+ const mode = args.mode ?? "synastry";
624
+ const meaning = HOUSE_MEANINGS[num] ?? `The ${ordinal(num)} house governs a distinct area of lived experience.`;
625
+ const signFlavor = sign
626
+ ? `\n\n${capitalize(sign)} on this cusp emphasizes ${SIGN_HOUSE_FLAVOR[sign.toLowerCase()] ?? "its natural qualities"} in the matters of this house.`
627
+ : "";
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.";
631
+ return {
632
+ content: [{
633
+ type: "text",
634
+ text: `**House ${num}${sign ? ` — ${capitalize(sign)} on the cusp` : ""}** *(Inner Natal Wheel)*\n\n` +
635
+ meaning + signFlavor + modeNote,
636
+ }],
637
+ };
638
+ },
639
+ });
640
+ // ── Tool: bi_wheel_synopsis ────────────────────────────────────────────────────
641
+ registerTool({
642
+ name: "bi_wheel_synopsis",
643
+ description: "Generate a master-astrologer-grade overview of the entire bi-wheel configuration — " +
644
+ "summarising the mode (synastry or transit), key cross-aspects by quality, dominant themes, " +
645
+ "stationing or slow-moving outer planet influences, and an interpretive narrative. " +
646
+ "Use this when the user asks for a full reading, a relationship overview, or a transit overview " +
647
+ "rather than clicking a specific element. Accepts the cross-aspect data from the chart payload.",
648
+ inputSchema: {
649
+ type: "object",
650
+ 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)" },
654
+ cross_aspects: {
655
+ type: "array",
656
+ description: "Array of cross-aspects from the bi-wheel payload (up to 30, sorted by orb)",
657
+ items: {
658
+ type: "object",
659
+ properties: {
660
+ planet1: { type: "string" },
661
+ planet2: { type: "string" },
662
+ aspect_type: { type: "string" },
663
+ orb: { type: "number" },
664
+ applying: { type: "boolean" },
665
+ },
666
+ },
667
+ },
668
+ top_n: { type: "number", description: "Number of top aspects to highlight (default 8)" },
669
+ },
670
+ required: ["mode"],
671
+ },
672
+ annotations: { title: "Bi-Wheel Synopsis", readOnlyHint: true, openWorldHint: false },
673
+ _meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app", "model"] } },
674
+ handler: async (args) => {
675
+ 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");
678
+ const topN = args.top_n ? Number(args.top_n) : 8;
679
+ const rawAspects = args.cross_aspects ?? [];
680
+ const aspects = rawAspects.slice(0, topN);
681
+ const ASPECT_SYM = {
682
+ conjunction: "☌", opposition: "☍", trine: "△", square: "□", sextile: "⚹",
683
+ };
684
+ const harmonious = aspects.filter((a) => ["trine", "sextile"].includes((a.aspect_type ?? a.type ?? "").toLowerCase()));
685
+ const dynamic = aspects.filter((a) => ["square", "opposition"].includes((a.aspect_type ?? a.type ?? "").toLowerCase()));
686
+ const fusing = aspects.filter((a) => (a.aspect_type ?? a.type ?? "").toLowerCase() === "conjunction");
687
+ const lines = aspects.map((ca) => {
688
+ const type = ca.aspect_type ?? ca.type ?? "aspect";
689
+ const sym = ASPECT_SYM[type.toLowerCase()] ?? type;
690
+ const dir = mode === "transit"
691
+ ? (ca.applying ? "↗ applying" : "↘ separating")
692
+ : (ca.orb <= 1 ? "exact" : ca.orb <= 3 ? "tight" : "wide");
693
+ return `• ${capitalize(ca.planet1)} ${sym} ${capitalize(ca.planet2)} (${ca.orb.toFixed(1)}° — ${dir})`;
694
+ });
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.`;
709
+ const body = lines.length > 0
710
+ ? `\n\n**Top Cross-Aspects by Orb:**\n${lines.join("\n")}\n\n**Thematic Summary:**\n${themeSummary || "No major aspects within standard orbs."}`
711
+ : "\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
+ return {
716
+ content: [{ type: "text", text: intro + body + closing }],
717
+ };
718
+ },
719
+ });