@openephemeris/mcp-server 3.15.0 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [3.16.0] — 2026-07-05
11
+
12
+ ### Added
13
+ - **`explore_human_design_transit`** — personalized Human Design transit overlay. Overlays a transiting moment on a person's natal bodygraph and highlights the channels the transit temporarily completes plus any newly-defined centers. Premium (Developer tier); renders an interactive overlay bodygraph in MCP Apps hosts.
14
+ - **`explore_human_design_connection`** — two-person Human Design connection (synastry) overlay. Classifies every connected channel as electromagnetic / companionship / dominance / compromise and renders a two-person overlay bodygraph. Premium (Developer tier).
15
+
16
+ ### Changed
17
+ - **`human_design_composite` is now Developer-tier** (was Explorer) and returns real connection-channel scoring — the previously-stubbed relationship endpoint (`/human-design/composite`) is now live end-to-end.
18
+
19
+ ---
20
+
10
21
  ## [3.15.0] — 2026-06-15
11
22
 
12
23
  ### Changed
package/README.md CHANGED
@@ -4,7 +4,9 @@
4
4
  [![npm version](https://img.shields.io/npm/v/@openephemeris/mcp-server)](https://www.npmjs.com/package/@openephemeris/mcp-server)
5
5
  [![System Status](https://img.shields.io/badge/Status-Operational-brightgreen)](https://status.openephemeris.com/)
6
6
 
7
- Model Context Protocol server for OpenEphemeris 52 typed astrology tools powered by the NASA JPL DE440 ephemeris. Zero hallucination on planetary positions, dates, and degrees. Covers 1,100 years of astronomical data.
7
+ ![OpenEphemeris in Claudeask in plain language, get a real computed chart](https://raw.githubusercontent.com/openephemeris/openephemeris/main/docs/internal/PH-ASSETS/hero-demo.gif)
8
+
9
+ Model Context Protocol server for OpenEphemeris — typed astrology tools powered by the NASA JPL DE440 ephemeris. Zero hallucination on planetary positions, dates, and degrees. Covers 1,100 years of astronomical data.
8
10
 
9
11
  **Hosted endpoint:** `https://mcp.openephemeris.com/mcp` (Streamable HTTP, MCP 2025-11-25 spec)
10
12
 
@@ -229,7 +231,9 @@ The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable
229
231
  | Station tracker | `electional_station_tracker` | Developer |
230
232
  | Aspect search | `electional_aspect_search` | Developer |
231
233
  | Human Design chart | `human_design_chart` | Explorer |
232
- | HD composite | `human_design_composite` | Explorer |
234
+ | HD composite | `human_design_composite` | Developer |
235
+ | HD transit overlay | `explore_human_design_transit` | Developer |
236
+ | HD connection (synastry) | `explore_human_design_connection` | Developer |
233
237
  | HD penta | `human_design_penta` | Explorer |
234
238
  | HD return / opposition | `hd_planetary_return`, `hd_opposition` | Explorer |
235
239
  | Vedic chart | `vedic_chart` | Explorer |
@@ -335,7 +339,7 @@ When you update the MCP server logic (handlers, bug fixes, hardening), you shoul
335
339
  ┌────▼────────────────────┐
336
340
  │ openephemeris-mcp │
337
341
  │ Node.js MCP Server │
338
- 52 typed tools
342
+ │ typed tools
339
343
  │ auth: Key > JWT │
340
344
  └────────────┬────────────┘
341
345
  │ HTTPS
@@ -355,8 +359,8 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
355
359
 
356
360
  - Allowlisted operations: **28**
357
361
  - Methods: `GET=4`, `POST=24`, `PUT=0`, `PATCH=0`, `DELETE=0`
358
- - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **80**
359
- - Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bazi_annual_pillar`, `bazi_chart`, `bazi_compatibility`, `bazi_element_balance`, `bazi_luck_pillars`, `bazi_ten_gods`, `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_list_allowed`, `dev_read_api`, `dev_write_api`, `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_moon_phase`, `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`
362
+ - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **84**
363
+ - Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bazi_annual_pillar`, `bazi_chart`, `bazi_compatibility`, `bazi_element_balance`, `bazi_luck_pillars`, `bazi_ten_gods`, `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_list_allowed`, `dev_read_api`, `dev_write_api`, `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_human_design_connection`, `explore_human_design_transit`, `explore_moon_phase`, `explore_natal_chart`, `hd_on_center_click`, `hd_on_channel_click`, `hd_on_connection_channel_click`, `hd_on_gate_click`, `hd_on_planet_click`, `hd_on_transit_channel_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
364
  - Generic tools:
361
365
 
362
366
  ### Allowlist Families
@@ -248,7 +248,9 @@ export class BackendClient {
248
248
  `If you don't have an account, you can create one at the link above (free tier, no credit card needed).`, 401, "auth_required", true, authInfo.verification_uri_complete);
249
249
  }
250
250
  return new BackendError(`Authentication required: ${msg}. Run auth.login to connect your account, ` +
251
- `or set OPENEPHEMERIS_API_KEY in your MCP server config.`, 401, "auth_required", false, LOGIN_SIGNUP_URL);
251
+ `or set OPENEPHEMERIS_API_KEY in your MCP server config. If you connected ` +
252
+ `through Claude's connector settings, disconnect and reconnect the ` +
253
+ `OpenEphemeris connector to re-authorize.`, 401, "auth_required", false, LOGIN_SIGNUP_URL);
252
254
  }
253
255
  if (status === 402) {
254
256
  const dataObj = data && typeof data === "object" ? data : {};
@@ -21,6 +21,20 @@ import { timingSafeEqual } from "node:crypto";
21
21
  import { verifyCodeChallenge } from "./pkce.js";
22
22
  import { issueSupabaseJWT, refreshSupabaseSession } from "./supabase-jwt.js";
23
23
  // ---------------------------------------------------------------------------
24
+ // Supabase config check (mirrors store.ts / supabase-jwt.ts)
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * True when Supabase env vars are configured, meaning callers expect a real
28
+ * JWT. In that case a JWT-issuance failure must be reported as an OAuth
29
+ * error rather than silently degrading to a placeholder token — a
30
+ * placeholder access_token is classified as an API key downstream
31
+ * (server-sse.ts extractAuth) and will 401 on every subsequent request,
32
+ * leaving the connector stuck in a "connected but broken" state.
33
+ */
34
+ function isSupabaseConfigured() {
35
+ return Boolean(process.env["SUPABASE_URL"] && process.env["SUPABASE_SERVICE_ROLE_KEY"]);
36
+ }
37
+ // ---------------------------------------------------------------------------
24
38
  // Helpers
25
39
  // ---------------------------------------------------------------------------
26
40
  /**
@@ -113,8 +127,10 @@ async function handleAuthorizationCode(req, res, store) {
113
127
  store.storeRefreshToken(oauthRefreshToken, {
114
128
  client_id: stored.client_id,
115
129
  user_id: stored.user_id,
116
- // Keep the Supabase refresh token for downstream refresh
117
- supabase_refresh_token: supabaseTokens.refresh_token,
130
+ // Real Supabase refresh tokens aren't available for self-signed JWTs
131
+ // (see supabase-jwt.ts) — storing null avoids a doomed Supabase
132
+ // refresh round-trip on the next /oauth/token refresh_token grant.
133
+ supabase_refresh_token: null,
118
134
  });
119
135
  console.info(`[token] Supabase JWT issued for user ${stored.user_id}`);
120
136
  res.json({
@@ -125,10 +141,23 @@ async function handleAuthorizationCode(req, res, store) {
125
141
  });
126
142
  return;
127
143
  }
128
- // Supabase JWT issuance failed log a warning but continue to placeholder
144
+ // Supabase JWT issuance failed. If Supabase is configured, the caller
145
+ // expects a real JWT — returning a placeholder here would leave the
146
+ // connector "connected" while every subsequent API call 401s (a
147
+ // placeholder access_token is classified as an API key, not a JWT, by
148
+ // server-sse.ts's extractAuth). Report a proper OAuth error instead so
149
+ // the client can retry or surface the failure.
150
+ if (isSupabaseConfigured()) {
151
+ console.error(`[token] Supabase JWT issuance failed for user ${stored.user_id}; returning server_error`);
152
+ res.status(500).json({
153
+ error: "server_error",
154
+ error_description: "Failed to issue an access token. Please try again.",
155
+ });
156
+ return;
157
+ }
129
158
  console.warn(`[token] Supabase JWT issuance failed for user ${stored.user_id}, falling back to placeholder`);
130
159
  }
131
- // Fallback: local placeholder tokens (test/dev path)
160
+ // Fallback: local placeholder tokens (test/dev path — Supabase not configured)
132
161
  const refreshToken = store.generateRefreshToken();
133
162
  store.storeRefreshToken(refreshToken, { client_id: stored.client_id });
134
163
  res.json({
@@ -166,7 +195,10 @@ async function handleRefreshToken(req, res, store) {
166
195
  // ---------------------------------------------------------------------------
167
196
  const userId = storedMeta.user_id;
168
197
  const supabaseRefresh = storedMeta.supabase_refresh_token;
169
- // Try Supabase refresh first
198
+ // Try Supabase refresh first — only meaningful if we ever obtained a real
199
+ // Supabase refresh token. Self-signed JWTs (see supabase-jwt.ts) never do,
200
+ // so supabase_refresh_token is stored as null and this is skipped in favor
201
+ // of going straight to the working issueSupabaseJWT(userId) fallback below.
170
202
  if (supabaseRefresh) {
171
203
  const refreshed = await refreshSupabaseSession(supabaseRefresh);
172
204
  if (refreshed) {
@@ -192,7 +224,9 @@ async function handleRefreshToken(req, res, store) {
192
224
  const newRefreshToken = store.generateRefreshToken();
193
225
  store.storeRefreshToken(newRefreshToken, {
194
226
  ...storedMeta,
195
- supabase_refresh_token: freshTokens.refresh_token,
227
+ // See note above — self-signed JWTs have no real Supabase refresh
228
+ // token, so store null rather than the JWT's own (also fake) one.
229
+ supabase_refresh_token: null,
196
230
  });
197
231
  res.json({
198
232
  access_token: freshTokens.access_token,
@@ -202,8 +236,21 @@ async function handleRefreshToken(req, res, store) {
202
236
  });
203
237
  return;
204
238
  }
239
+ // We have a user_id (Supabase-issued grant) but JWT issuance failed.
240
+ // Same reasoning as the authorization_code path: a placeholder token
241
+ // here would falsely appear "refreshed" while every subsequent request
242
+ // 401s. Report a proper OAuth error so the client can retry/reauth.
243
+ if (isSupabaseConfigured()) {
244
+ console.error(`[token] Refresh: Supabase JWT re-issuance failed for user ${userId}; returning server_error`);
245
+ res.status(500).json({
246
+ error: "server_error",
247
+ error_description: "Failed to refresh the access token. Please try again.",
248
+ });
249
+ return;
250
+ }
205
251
  }
206
- // Fallback: placeholder tokens
252
+ // Fallback: placeholder tokens (test/dev path — Supabase not configured,
253
+ // or this refresh token predates any user_id/Supabase association)
207
254
  const newRefreshToken = store.generateRefreshToken();
208
255
  store.storeRefreshToken(newRefreshToken, storedMeta);
209
256
  res.json({
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * bodygraph-app.ts — MCP App tool registration for the Human Design Bodygraph Explorer.
3
3
  *
4
- * Registers 4 tools:
5
- * • explore_human_design — primary entry point, returns data + UI resource [model]
6
- * • hd_on_center_click — center click handler [app-only]
7
- * • hd_on_gate_click — gate click handler [app-only]
8
- * • hd_on_channel_click — channel click handler [app-only]
4
+ * Entry tools [model + app]:
5
+ * • explore_human_design — natal bodygraph, data + UI resource
6
+ * • explore_human_design_transit — natal + transit overlay (premium)
7
+ * • explore_human_design_connection — two-person connection/synastry overlay (premium)
8
+ * Click handlers [app-only]: hd_on_center_click, hd_on_gate_click,
9
+ * hd_on_channel_click, hd_on_planet_click, hd_on_variable_click,
10
+ * hd_on_connection_channel_click, hd_on_transit_channel_click (overlay, Phase 5).
11
+ * The transit + connection tools reuse this bodygraph iframe resource; their
12
+ * overlay SVG is returned inline by /human-design/transit-chart and
13
+ * /human-design/composite (include_visual) rather than a separate render call.
9
14
  *
10
15
  * The bodygraph is rendered client-side from structured JSON — no SVG endpoint
11
16
  * is called, keeping latency at zero and the architecture consistent with
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * bodygraph-app.ts — MCP App tool registration for the Human Design Bodygraph Explorer.
3
3
  *
4
- * Registers 4 tools:
5
- * • explore_human_design — primary entry point, returns data + UI resource [model]
6
- * • hd_on_center_click — center click handler [app-only]
7
- * • hd_on_gate_click — gate click handler [app-only]
8
- * • hd_on_channel_click — channel click handler [app-only]
4
+ * Entry tools [model + app]:
5
+ * • explore_human_design — natal bodygraph, data + UI resource
6
+ * • explore_human_design_transit — natal + transit overlay (premium)
7
+ * • explore_human_design_connection — two-person connection/synastry overlay (premium)
8
+ * Click handlers [app-only]: hd_on_center_click, hd_on_gate_click,
9
+ * hd_on_channel_click, hd_on_planet_click, hd_on_variable_click,
10
+ * hd_on_connection_channel_click, hd_on_transit_channel_click (overlay, Phase 5).
11
+ * The transit + connection tools reuse this bodygraph iframe resource; their
12
+ * overlay SVG is returned inline by /human-design/transit-chart and
13
+ * /human-design/composite (include_visual) rather than a separate render call.
9
14
  *
10
15
  * The bodygraph is rendered client-side from structured JSON — no SVG endpoint
11
16
  * is called, keeping latency at zero and the architecture consistent with
@@ -700,6 +705,148 @@ registerTool({
700
705
  };
701
706
  },
702
707
  });
708
+ const CONNECTION_TYPE_DATA = {
709
+ electromagnetic: {
710
+ label: "Electromagnetic",
711
+ tagline: "attraction — opposites complete the channel",
712
+ body: "Each of you holds one different gate of this channel; together you complete a current neither runs alone. This is the classic draw — and, on the same wiring, the friction. What the channel does comes alive only in the meeting; apart, each of you is back to half. The gift is chemistry and mutual completion. The edge is that the completeness depends on the other, and the same difference that pulls you together can push you apart. Each of you stays with your own strategy and inner authority — the connection is information about the interplay, not an instruction.",
713
+ },
714
+ companionship: {
715
+ label: "Companionship",
716
+ tagline: "sameness — you both already run the whole channel",
717
+ body: "You each already define this whole channel on your own, so meeting here is recognition rather than completion. Nothing has to be negotiated; you simply operate the same way. Friendship and ease live here. The edge is that there is no charge to cross — sameness can go flat, or become the place you take each other for granted. Ease is not the same as aliveness.",
718
+ },
719
+ dominance: {
720
+ label: "Dominance",
721
+ tagline: "one defines the whole channel, the other is fully open here",
722
+ body: "One of you defines this entire channel; the other holds neither gate and is completely open here. The defined partner conditions the open one steadily and consistently, and the open partner takes the current in fully, unfiltered by any definition of their own — seeing the defined one clearly, for exactly who they are. It is meant to be accepted and surrendered to, not fixed or matched. The gift is consistency and clear sight. The edge is the not-self reading of \"I always have to do it your way\": the open partner can lose themselves to the steady conditioning if they drift from their own authority. Surrender here is a choice made from authority, not a disappearance.",
723
+ },
724
+ compromise: {
725
+ label: "Compromise",
726
+ tagline: "one defines the whole channel, the other holds a single gate",
727
+ body: "One of you defines this whole channel; the other holds exactly one of its two gates. Push and pull land on that shared gate — the half-definition keeps getting overridden by the full one, so the single-gate partner is consistently compromised here. This is the spot where \"you do it your way, I do it mine\" surfaces most. Met consciously, the recurring friction on that one gate names exactly where the work is and can sharpen both of you. Unwatched, it calcifies into a standing grievance instead of a workable edge.",
728
+ },
729
+ };
730
+ const TRANSIT_CLASS_DATA = {
731
+ transit_completed: {
732
+ label: "Transit completes this channel",
733
+ body: "For a window, a passing transit completes this channel: one of its gates is defined in your own design, the other is switched on by the sky. While that holds, the current runs in you the way it does for someone built this way full-time — except you are not, and that is the whole point. It is a gift on loan: a real taste of standing definition here, without your having become someone else for it. The pull is to grab it and decide this is now who you are; when the transit separates and the channel opens back up, that reading only leaves you grieving. Nothing about your type, strategy, or authority has changed. Use the borrowed current while it lasts, keep deciding from your own authority, and let it go cleanly when the sky moves on.",
734
+ },
735
+ transit_only: {
736
+ label: "Transit defines this channel on its own",
737
+ body: "For a window, a passing transit lights both gates of this channel — neither is yours natally. The channel runs entirely on borrowed weather; it is not part of your design at all, and it switches off completely when the transit moves on. Notice the temporary current for what it is, and keep deciding from your own authority rather than mistaking it for a new capacity.",
738
+ },
739
+ reinforced: {
740
+ label: "Transit reinforces your definition here",
741
+ body: "You already define this channel, and a passing transit also lands on one of its gates today — your standing definition, amplified for a window. Nothing new is added; the current you already run simply gets louder. When the transit moves on, it settles back to its usual level.",
742
+ },
743
+ natal: {
744
+ label: "Your standing definition",
745
+ body: "Both gates of this channel are defined in your own design. This is part of your fixed wiring, not transit weather — the passing sky does not change it. It reads here for context alongside the temporary activations.",
746
+ },
747
+ };
748
+ // ── Tool: hd_on_connection_channel_click [app-only] ────────────────────────
749
+ // Fired when the user clicks a connection (synastry) channel in the overlay
750
+ // bodygraph. Serves the connection-type dynamic for that specific channel.
751
+ registerTool({
752
+ name: "hd_on_connection_channel_click",
753
+ description: "Event handler called when the user clicks a connected channel in a Human Design connection (synastry) overlay bodygraph. " +
754
+ "Returns the channel identity and the relationship dynamic for its connection type " +
755
+ "(electromagnetic / companionship / dominance / compromise). " +
756
+ "This tool is called automatically by the bodygraph UI; you do not need to call it directly.",
757
+ inputSchema: {
758
+ type: "object",
759
+ properties: {
760
+ channel: { type: "string", description: "Channel identifier string (e.g. '1-8', '20-34')." },
761
+ connection_type: {
762
+ type: "string",
763
+ description: "Connection type: electromagnetic | companionship | dominance | compromise.",
764
+ },
765
+ gate1: { type: "number", description: "First gate number." },
766
+ gate2: { type: "number", description: "Second gate number." },
767
+ },
768
+ required: ["channel"],
769
+ },
770
+ outputSchema: OUTPUT_SCHEMA_JSON,
771
+ annotations: { title: "Connection Channel Interpretation", readOnlyHint: true, openWorldHint: false },
772
+ _meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
773
+ handler: async (args) => {
774
+ const channelStr = String(args.channel ?? "");
775
+ const g1 = args.gate1 != null ? Number(args.gate1) : null;
776
+ const g2 = args.gate2 != null ? Number(args.gate2) : null;
777
+ const typeKey = String(args.connection_type ?? "").toLowerCase();
778
+ const key = channelStr.includes("-") ? channelStr : g1 != null && g2 != null ? `${g1}-${g2}` : channelStr;
779
+ const chan = HD_CHANNEL_DATA[key] ?? HD_CHANNEL_DATA[key.split("-").reverse().join("-")];
780
+ const conn = CONNECTION_TYPE_DATA[typeKey];
781
+ const chanName = chan?.name ?? "this channel";
782
+ const gatesLine = `**Gates:** ${g1 ?? channelStr.split("-")[0] ?? "?"} ↔ ${g2 ?? channelStr.split("-")[1] ?? "?"}`;
783
+ if (!conn) {
784
+ return {
785
+ content: [{
786
+ type: "text",
787
+ text: `**Channel ${channelStr} — ${chanName}**\n\n${gatesLine}\n\n` +
788
+ `This channel is defined across both people. ${chan?.description ?? ""}`.trim(),
789
+ }],
790
+ };
791
+ }
792
+ return {
793
+ content: [{
794
+ type: "text",
795
+ text: `**${conn.label} Connection — Channel of ${chanName} (${channelStr})**\n\n` +
796
+ `${gatesLine}\n\n` +
797
+ `*${conn.tagline}*\n\n` +
798
+ `${conn.body}` +
799
+ (chan?.description ? `\n\n**This channel:** ${chan.description}` : ""),
800
+ }],
801
+ };
802
+ },
803
+ });
804
+ // ── Tool: hd_on_transit_channel_click [app-only] ──────────────────────────
805
+ // Fired when the user clicks a channel in a transit overlay bodygraph. Serves
806
+ // the meaning of the channel's transit class (esp. transit_completed).
807
+ registerTool({
808
+ name: "hd_on_transit_channel_click",
809
+ description: "Event handler called when the user clicks a channel in a Human Design transit overlay bodygraph. " +
810
+ "Returns the channel identity and what its transit class means — especially transit_completed, where a transit " +
811
+ "temporarily completes a channel (one natal gate + one transit gate). " +
812
+ "This tool is called automatically by the bodygraph UI; you do not need to call it directly.",
813
+ inputSchema: {
814
+ type: "object",
815
+ properties: {
816
+ channel: { type: "string", description: "Channel identifier string (e.g. '64-47')." },
817
+ transit_class: {
818
+ type: "string",
819
+ description: "Transit class: natal | reinforced | transit_completed | transit_only.",
820
+ },
821
+ gate1: { type: "number", description: "First gate number." },
822
+ gate2: { type: "number", description: "Second gate number." },
823
+ },
824
+ required: ["channel"],
825
+ },
826
+ outputSchema: OUTPUT_SCHEMA_JSON,
827
+ annotations: { title: "Transit Channel Interpretation", readOnlyHint: true, openWorldHint: false },
828
+ _meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
829
+ handler: async (args) => {
830
+ const channelStr = String(args.channel ?? "");
831
+ const g1 = args.gate1 != null ? Number(args.gate1) : null;
832
+ const g2 = args.gate2 != null ? Number(args.gate2) : null;
833
+ const cls = String(args.transit_class ?? "transit_completed").toLowerCase();
834
+ const key = channelStr.includes("-") ? channelStr : g1 != null && g2 != null ? `${g1}-${g2}` : channelStr;
835
+ const chan = HD_CHANNEL_DATA[key] ?? HD_CHANNEL_DATA[key.split("-").reverse().join("-")];
836
+ const info = TRANSIT_CLASS_DATA[cls] ?? TRANSIT_CLASS_DATA.transit_completed;
837
+ const chanName = chan?.name ?? "this channel";
838
+ const gatesLine = `**Gates:** ${g1 ?? channelStr.split("-")[0] ?? "?"} ↔ ${g2 ?? channelStr.split("-")[1] ?? "?"}`;
839
+ return {
840
+ content: [{
841
+ type: "text",
842
+ text: `**Channel of ${chanName} (${channelStr}) — ${info.label}**\n\n` +
843
+ `${gatesLine}\n\n` +
844
+ `${info.body}` +
845
+ (chan?.description ? `\n\n**This channel:** ${chan.description}` : ""),
846
+ }],
847
+ };
848
+ },
849
+ });
703
850
  const HD_CENTER_DATA = {
704
851
  Head: {
705
852
  keynote: "Inspiration, mental pressure, questioning",
@@ -1173,3 +1320,181 @@ registerTool({
1173
1320
  };
1174
1321
  },
1175
1322
  });
1323
+ // ── Tool: explore_human_design_transit ───────────────────────────────────────
1324
+ // Personalized Human Design transit overlay (natal chart + a transiting moment).
1325
+ // Premium (Developer tier). Calls POST /human-design/transit-chart.
1326
+ registerTool({
1327
+ name: "explore_human_design_transit",
1328
+ description: "Overlay the current (or a chosen) planetary transit on a person's natal Human Design bodygraph. " +
1329
+ "Highlights the channels a transit temporarily COMPLETES with the natal chart and any centers it " +
1330
+ "newly defines — the core of a Human Design transit reading. " +
1331
+ "Returns an interactive overlay bodygraph in MCP Apps-capable hosts (Claude Desktop), with a text " +
1332
+ "summary fallback elsewhere. Premium (Developer tier). Calculated with NASA JPL DE440 ephemerides.",
1333
+ inputSchema: {
1334
+ type: "object",
1335
+ properties: {
1336
+ datetime: {
1337
+ type: "string",
1338
+ description: "Natal birth datetime, ISO 8601 (e.g. '1990-04-15T19:30:00Z'). Include 'Z' or an offset, " +
1339
+ "or supply timezone for local time. HD is time-sensitive to the minute.",
1340
+ },
1341
+ latitude: { type: "number", description: "Natal birth latitude (decimal degrees, +N). Optional." },
1342
+ longitude: { type: "number", description: "Natal birth longitude (decimal degrees, +E). Optional." },
1343
+ location: { type: "string", description: "Natal location name, display only." },
1344
+ timezone: { type: "string", description: "IANA timezone for the birth location (e.g. 'America/New_York')." },
1345
+ transit_datetime: {
1346
+ type: "string",
1347
+ description: "Transit moment, ISO 8601. Defaults to now (UTC) when omitted.",
1348
+ },
1349
+ },
1350
+ required: ["datetime"],
1351
+ },
1352
+ outputSchema: OUTPUT_SCHEMA_JSON,
1353
+ annotations: {
1354
+ title: "Human Design Transit Overlay",
1355
+ readOnlyHint: true,
1356
+ destructiveHint: false,
1357
+ idempotentHint: false,
1358
+ openWorldHint: false,
1359
+ },
1360
+ _meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["model", "app"] } },
1361
+ handler: async (args) => {
1362
+ const client = getActiveClient();
1363
+ const timezone = args.timezone;
1364
+ const natalIso = localToUtcIso(String(args.datetime), timezone);
1365
+ const lat = args.latitude ?? 0;
1366
+ const lon = args.longitude ?? 0;
1367
+ // SubjectRequest wire shape: name (required), birth_datetime.iso, and
1368
+ // birth_location with struct-typed coordinate/timezone inputs (matches the
1369
+ // live bi-wheel/transit-timeline tools — bare scalars are rejected).
1370
+ const body = {
1371
+ subject: {
1372
+ name: args.location ?? "Subject",
1373
+ birth_datetime: { iso: natalIso },
1374
+ birth_location: {
1375
+ latitude: { decimal: lat },
1376
+ longitude: { decimal: lon },
1377
+ ...(timezone ? { timezone: { iana_name: timezone } } : {}),
1378
+ },
1379
+ },
1380
+ };
1381
+ if (args.transit_datetime) {
1382
+ body.transit_datetime = { iso: ensureTimezone(String(args.transit_datetime)) };
1383
+ }
1384
+ const bundleAvailable = Boolean(getBodygraphBundle());
1385
+ if (bundleAvailable)
1386
+ body.include_visual = true;
1387
+ const resp = await client.request("POST", "/human-design/transit-chart", { data: body });
1388
+ const transit = (resp?.transit ?? {});
1389
+ const completed = Array.isArray(transit.completed_channels) ? transit.completed_channels : [];
1390
+ const newCenters = Array.isArray(transit.newly_defined_centers) ? transit.newly_defined_centers : [];
1391
+ const natal = (resp?.natal ?? {});
1392
+ const svg = resp?.visual?.data;
1393
+ const summary = `**Human Design Transit Overlay**\n\n` +
1394
+ `Natal: ${natal.type ?? "—"} ${natal.profile ?? ""} · ${natal.authority ?? ""} authority.\n\n` +
1395
+ (completed.length
1396
+ ? `The transit **completes ${completed.length} channel${completed.length === 1 ? "" : "s"}**: ${completed.join(", ")}.`
1397
+ : `The transit completes no channels right now.`) +
1398
+ (newCenters.length
1399
+ ? `\n\nNewly-defined center${newCenters.length === 1 ? "" : "s"}: **${newCenters.join(", ")}** — a temporary shift while the transit holds.`
1400
+ : "");
1401
+ const payload = { ...resp, _svg: svg, server_version: SERVER_VERSION };
1402
+ if (bundleAvailable && svg) {
1403
+ return {
1404
+ content: [{ type: "text", text: summary }],
1405
+ structuredContent: payload,
1406
+ _meta: { "ui/resourceUri": BODYGRAPH_RESOURCE_URI, ui: { resourceUri: BODYGRAPH_RESOURCE_URI } },
1407
+ };
1408
+ }
1409
+ return { content: [{ type: "text", text: summary }], structuredContent: payload };
1410
+ },
1411
+ });
1412
+ // ── Tool: explore_human_design_connection ────────────────────────────────────
1413
+ // Two-person Human Design connection (synastry) overlay. Premium (Developer tier).
1414
+ // Calls POST /human-design/composite.
1415
+ registerTool({
1416
+ name: "explore_human_design_connection",
1417
+ description: "Compare two people's Human Design charts and classify every connected channel by HD connection " +
1418
+ "theory — electromagnetic (attraction), companionship (sameness), dominance (one defines), and " +
1419
+ "compromise (friction). Returns an interactive two-person overlay bodygraph in MCP Apps-capable " +
1420
+ "hosts, with a text summary fallback elsewhere. Premium (Developer tier). NASA JPL DE440 ephemerides.",
1421
+ inputSchema: {
1422
+ type: "object",
1423
+ properties: {
1424
+ person_a: {
1425
+ type: "object",
1426
+ description: "First person's birth data.",
1427
+ properties: {
1428
+ datetime: { type: "string", description: "Birth datetime, ISO 8601 (Z/offset, or local with timezone)." },
1429
+ latitude: { type: "number", description: "Birth latitude (decimal degrees, +N). Optional." },
1430
+ longitude: { type: "number", description: "Birth longitude (decimal degrees, +E). Optional." },
1431
+ timezone: { type: "string", description: "IANA timezone (e.g. 'America/New_York')." },
1432
+ },
1433
+ required: ["datetime"],
1434
+ },
1435
+ person_b: {
1436
+ type: "object",
1437
+ description: "Second person's birth data.",
1438
+ properties: {
1439
+ datetime: { type: "string", description: "Birth datetime, ISO 8601 (Z/offset, or local with timezone)." },
1440
+ latitude: { type: "number", description: "Birth latitude (decimal degrees, +N). Optional." },
1441
+ longitude: { type: "number", description: "Birth longitude (decimal degrees, +E). Optional." },
1442
+ timezone: { type: "string", description: "IANA timezone (e.g. 'America/New_York')." },
1443
+ },
1444
+ required: ["datetime"],
1445
+ },
1446
+ },
1447
+ required: ["person_a", "person_b"],
1448
+ },
1449
+ outputSchema: OUTPUT_SCHEMA_JSON,
1450
+ annotations: {
1451
+ title: "Human Design Connection (Synastry)",
1452
+ readOnlyHint: true,
1453
+ destructiveHint: false,
1454
+ idempotentHint: true,
1455
+ openWorldHint: false,
1456
+ },
1457
+ _meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["model", "app"] } },
1458
+ handler: async (args) => {
1459
+ const client = getActiveClient();
1460
+ const toSubject = (p) => {
1461
+ const tz = p?.timezone;
1462
+ return {
1463
+ birth_datetime_utc: localToUtcIso(String(p?.datetime), tz),
1464
+ latitude: p?.latitude ?? 0,
1465
+ longitude: p?.longitude ?? 0,
1466
+ };
1467
+ };
1468
+ const body = {
1469
+ subject_1: toSubject(args.person_a),
1470
+ subject_2: toSubject(args.person_b),
1471
+ };
1472
+ const bundleAvailable = Boolean(getBodygraphBundle());
1473
+ if (bundleAvailable)
1474
+ body.include_visual = true;
1475
+ const resp = await client.request("POST", "/human-design/composite", { data: body });
1476
+ const connections = Array.isArray(resp?.connections) ? resp.connections : [];
1477
+ const svg = resp?.visual?.data;
1478
+ const byType = (t) => connections.filter((c) => c?.type === t).map((c) => c.id);
1479
+ const line = (label, t) => {
1480
+ const ids = byType(t);
1481
+ return ids.length ? `\n- **${label}** (${ids.length}): ${ids.join(", ")}` : "";
1482
+ };
1483
+ const summary = `**Human Design Connection**\n\n` +
1484
+ `${connections.length} connected channel${connections.length === 1 ? "" : "s"}:` +
1485
+ line("Electromagnetic — attraction", "electromagnetic") +
1486
+ line("Companionship — sameness", "companionship") +
1487
+ line("Dominance — one defines", "dominance") +
1488
+ line("Compromise — friction", "compromise") +
1489
+ (connections.length ? "" : " none.");
1490
+ const payload = { ...resp, _svg: svg, server_version: SERVER_VERSION };
1491
+ if (bundleAvailable && svg) {
1492
+ return {
1493
+ content: [{ type: "text", text: summary }],
1494
+ structuredContent: payload,
1495
+ _meta: { "ui/resourceUri": BODYGRAPH_RESOURCE_URI, ui: { resourceUri: BODYGRAPH_RESOURCE_URI } },
1496
+ };
1497
+ }
1498
+ return { content: [{ type: "text", text: summary }], structuredContent: payload };
1499
+ },
1500
+ });