@openephemeris/mcp-server 3.16.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/README.md +6 -4
- package/dist/backend/client.js +3 -1
- package/dist/oauth/token.js +54 -7
- package/dist/tools/apps/bodygraph-app.d.ts +2 -1
- package/dist/tools/apps/bodygraph-app.js +144 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@openephemeris/mcp-server)
|
|
5
5
|
[](https://status.openephemeris.com/)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+

|
|
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
|
|
|
@@ -337,7 +339,7 @@ When you update the MCP server logic (handlers, bug fixes, hardening), you shoul
|
|
|
337
339
|
┌────▼────────────────────┐
|
|
338
340
|
│ openephemeris-mcp │
|
|
339
341
|
│ Node.js MCP Server │
|
|
340
|
-
│
|
|
342
|
+
│ typed tools │
|
|
341
343
|
│ auth: Key > JWT │
|
|
342
344
|
└────────────┬────────────┘
|
|
343
345
|
│ HTTPS
|
|
@@ -357,8 +359,8 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
357
359
|
|
|
358
360
|
- Allowlisted operations: **28**
|
|
359
361
|
- Methods: `GET=4`, `POST=24`, `PUT=0`, `PATCH=0`, `DELETE=0`
|
|
360
|
-
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **
|
|
361
|
-
- 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_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`
|
|
362
364
|
- Generic tools:
|
|
363
365
|
|
|
364
366
|
### Allowlist Families
|
package/dist/backend/client.js
CHANGED
|
@@ -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
|
|
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 : {};
|
package/dist/oauth/token.js
CHANGED
|
@@ -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
|
-
//
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
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({
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* • explore_human_design_transit — natal + transit overlay (premium)
|
|
7
7
|
* • explore_human_design_connection — two-person connection/synastry overlay (premium)
|
|
8
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
|
|
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).
|
|
10
11
|
* The transit + connection tools reuse this bodygraph iframe resource; their
|
|
11
12
|
* overlay SVG is returned inline by /human-design/transit-chart and
|
|
12
13
|
* /human-design/composite (include_visual) rather than a separate render call.
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* • explore_human_design_transit — natal + transit overlay (premium)
|
|
7
7
|
* • explore_human_design_connection — two-person connection/synastry overlay (premium)
|
|
8
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
|
|
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).
|
|
10
11
|
* The transit + connection tools reuse this bodygraph iframe resource; their
|
|
11
12
|
* overlay SVG is returned inline by /human-design/transit-chart and
|
|
12
13
|
* /human-design/composite (include_visual) rather than a separate render call.
|
|
@@ -704,6 +705,148 @@ registerTool({
|
|
|
704
705
|
};
|
|
705
706
|
},
|
|
706
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
|
+
});
|
|
707
850
|
const HD_CENTER_DATA = {
|
|
708
851
|
Head: {
|
|
709
852
|
keynote: "Inspiration, mental pressure, questioning",
|