@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 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
 
@@ -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
- 52 typed tools
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`): **82**
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
@@ -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({
@@ -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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openephemeris/mcp-server",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "description": "Model Context Protocol server for the Open Ephemeris astronomical computation API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",