@openephemeris/mcp-server 3.10.3 → 3.12.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,18 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [3.10.4] — 2026-04-24
11
+
12
+ ### Changed
13
+ - `ephemeris_transits` tool description updated with compute surcharge and search range limit documentation
14
+ - `dev_call` credit cost reference now includes compute surcharge information
15
+ - Tier-based compute timeouts: Explorer 30s, Developer 120s, Startup 5m, Scale 10m for compute-heavy endpoints
16
+ - Search range caps: `/predictive/transits/search` limited to 1y (Explorer), 5y (Developer), 10y (Startup)
17
+ - Compute surcharges: requests exceeding 30s incur 1 additional credit per 30s of wall-clock compute
18
+ - Daily credit caps now persistent across server restarts (PostgreSQL-backed)
19
+
20
+ ---
21
+
10
22
  ## [3.10.0] — 2026-04-23
11
23
 
12
24
  ### Added
package/README.md CHANGED
@@ -353,8 +353,8 @@ When you update the MCP server logic (handlers, bug fixes, hardening), you shoul
353
353
 
354
354
  Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live tool registry.
355
355
 
356
- - Allowlisted operations: **23**
357
- - Methods: `GET=4`, `POST=19`, `PUT=0`, `PATCH=0`, `DELETE=0`
356
+ - Allowlisted operations: **28**
357
+ - Methods: `GET=4`, `POST=24`, `PUT=0`, `PATCH=0`, `DELETE=0`
358
358
  - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **57**
359
359
  - Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `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_natal_chart`, `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`
360
360
  - Generic tools:
@@ -364,6 +364,7 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
364
364
  | Family | Operations | Example |
365
365
  |---|---:|---|
366
366
  | `acg` | 4 | `POST /acg/ccg`, `POST /acg/hits` |
367
+ | `chinese` | 5 | `POST /chinese/bazi/compatibility`, `POST /chinese/bazi/element-balance` |
367
368
  | `comparative` | 5 | `POST /comparative/composite`, `POST /comparative/composite/midpoint` |
368
369
  | `electional` | 4 | `GET /electional/aspect-search`, `GET /electional/find-window` |
369
370
  | `ephemeris` | 1 | `POST /ephemeris/relocation` |
@@ -31,6 +31,26 @@
31
31
  "method": "POST",
32
32
  "path": "/acg/power-lines"
33
33
  },
34
+ {
35
+ "method": "POST",
36
+ "path": "/chinese/bazi/compatibility"
37
+ },
38
+ {
39
+ "method": "POST",
40
+ "path": "/chinese/bazi/element-balance"
41
+ },
42
+ {
43
+ "method": "POST",
44
+ "path": "/chinese/bazi/luck-pillars"
45
+ },
46
+ {
47
+ "method": "POST",
48
+ "path": "/chinese/bazi/solar-terms"
49
+ },
50
+ {
51
+ "method": "POST",
52
+ "path": "/chinese/bazi/ten-gods"
53
+ },
34
54
  {
35
55
  "method": "POST",
36
56
  "path": "/comparative/composite/midpoint"
@@ -108,8 +128,8 @@
108
128
  "path": "/visualization/chart-wheel"
109
129
  }
110
130
  ],
111
- "last_generated_at": "2026-04-22T15:19:21.508Z",
112
- "openapi_sha256": "0c29ad2e37fd08d48800ec54059e262fbbdb681b844e00fed9274611bae678ac",
131
+ "last_generated_at": "2026-04-25T17:47:00.681Z",
132
+ "openapi_sha256": "9b2a79811fef658557233fc649b598178488985708b3ad10f252be598567080a",
113
133
  "candidates_get": [
114
134
  {
115
135
  "method": "GET",
@@ -391,6 +411,14 @@
391
411
  ],
392
412
  "operationId": "reverse_geocode_location_reverse_get"
393
413
  },
414
+ {
415
+ "method": "GET",
416
+ "path": "/tidal/forcing/deep-time",
417
+ "tags": [
418
+ "tidal"
419
+ ],
420
+ "operationId": "get_tidal_forcing_deep_time_endpoint_tidal_forcing_deep_time_get"
421
+ },
394
422
  {
395
423
  "method": "GET",
396
424
  "path": "/tidal/forcing",
@@ -521,6 +549,54 @@
521
549
  ],
522
550
  "operationId": "power_lines_acg_power_lines_post"
523
551
  },
552
+ {
553
+ "method": "POST",
554
+ "path": "/chinese/bazi/annual-pillar",
555
+ "tags": [
556
+ "chinese"
557
+ ],
558
+ "operationId": "bazi_annual_pillar_chinese_bazi_annual_pillar_post"
559
+ },
560
+ {
561
+ "method": "POST",
562
+ "path": "/chinese/bazi/compatibility",
563
+ "tags": [
564
+ "chinese"
565
+ ],
566
+ "operationId": "bazi_compatibility_chinese_bazi_compatibility_post"
567
+ },
568
+ {
569
+ "method": "POST",
570
+ "path": "/chinese/bazi/element-balance",
571
+ "tags": [
572
+ "chinese"
573
+ ],
574
+ "operationId": "bazi_element_balance_chinese_bazi_element_balance_post"
575
+ },
576
+ {
577
+ "method": "POST",
578
+ "path": "/chinese/bazi/luck-pillars",
579
+ "tags": [
580
+ "chinese"
581
+ ],
582
+ "operationId": "bazi_luck_pillars_chinese_bazi_luck_pillars_post"
583
+ },
584
+ {
585
+ "method": "POST",
586
+ "path": "/chinese/bazi/solar-terms",
587
+ "tags": [
588
+ "chinese"
589
+ ],
590
+ "operationId": "bazi_solar_terms_chinese_bazi_solar_terms_post"
591
+ },
592
+ {
593
+ "method": "POST",
594
+ "path": "/chinese/bazi/ten-gods",
595
+ "tags": [
596
+ "chinese"
597
+ ],
598
+ "operationId": "bazi_ten_gods_chinese_bazi_ten_gods_post"
599
+ },
524
600
  {
525
601
  "method": "POST",
526
602
  "path": "/chinese/bazi",
@@ -994,6 +1070,11 @@
994
1070
  "Non-GET allowlisted: POST /acg/hits",
995
1071
  "Non-GET allowlisted: POST /acg/local-space",
996
1072
  "Non-GET allowlisted: POST /acg/power-lines",
1073
+ "Non-GET allowlisted: POST /chinese/bazi/compatibility",
1074
+ "Non-GET allowlisted: POST /chinese/bazi/element-balance",
1075
+ "Non-GET allowlisted: POST /chinese/bazi/luck-pillars",
1076
+ "Non-GET allowlisted: POST /chinese/bazi/solar-terms",
1077
+ "Non-GET allowlisted: POST /chinese/bazi/ten-gods",
997
1078
  "Non-GET allowlisted: POST /comparative/composite/midpoint",
998
1079
  "Non-GET allowlisted: POST /comparative/composite",
999
1080
  "Non-GET allowlisted: POST /comparative/natal-transits",
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
8
- import { initTools, toolRegistry, formatToolResponse } from "./tools/index.js";
8
+ import { initTools, toolRegistry, formatToolResponse, modelVisibleTools } from "./tools/index.js";
9
9
  import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
10
10
  function resolveServerVersion() {
11
11
  try {
@@ -41,7 +41,7 @@ const server = new Server({
41
41
  // List available tools
42
42
  server.setRequestHandler(ListToolsRequestSchema, async () => {
43
43
  return {
44
- tools: Object.values(toolRegistry).map((tool) => ({
44
+ tools: modelVisibleTools().map((tool) => ({
45
45
  name: tool.name,
46
46
  description: tool.description,
47
47
  inputSchema: tool.inputSchema,
@@ -22,7 +22,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
22
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
23
23
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
24
24
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, isInitializeRequest, } from "@modelcontextprotocol/sdk/types.js";
25
- import { initTools, toolRegistry, formatToolResponse } from "./tools/index.js";
25
+ import { initTools, toolRegistry, formatToolResponse, modelVisibleTools } from "./tools/index.js";
26
26
  import { BackendClient, runWithClient } from "./backend/client.js";
27
27
  import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
28
28
  // ---------------------------------------------------------------------------
@@ -193,7 +193,7 @@ function createMcpServer() {
193
193
  });
194
194
  // --- Tool handlers ---
195
195
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
196
- tools: Object.values(toolRegistry).map((tool) => ({
196
+ tools: modelVisibleTools().map((tool) => ({
197
197
  name: tool.name,
198
198
  description: tool.description,
199
199
  inputSchema: tool.inputSchema,
@@ -315,7 +315,7 @@ async function main() {
315
315
  // Spec: https://smithery.ai/docs/build/publish#server-scanning
316
316
  // ---------------------------------------------------------------------------
317
317
  app.get("/.well-known/mcp/server-card.json", (_req, res) => {
318
- const tools = Object.values(toolRegistry).map((tool) => ({
318
+ const tools = modelVisibleTools().map((tool) => ({
319
319
  name: tool.name,
320
320
  description: tool.description,
321
321
  inputSchema: tool.inputSchema,
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * chart-wheel-app.ts — MCP App tool registration for the Chart Wheel Explorer.
3
3
  *
4
- * Registers 5 tools:
5
- * • explore_natal_chart — primary entry point, returns data + UI resource
6
- * • chart_wheel_on_planet_click event handler for planet clicks
7
- * • chart_wheel_on_house_clickevent handler for house clicks
8
- * • chart_wheel_on_aspect_click event handler for aspect clicks
9
- * • chart_wheel_recalculate recalculates with a different house system
4
+ * Registers 7 tools:
5
+ * • explore_natal_chart — primary entry point, returns data + UI resource [model]
6
+ * • chart_wheel_fetch_svg lazy SVG fetcher called by UI on first render [app-only]
7
+ * • chart_wheel_on_planet_clickplanet click handler [app-only]
8
+ * • chart_wheel_on_house_click house click handler [app-only]
9
+ * • chart_wheel_on_aspect_click aspect click handler [app-only]
10
+ * • chart_wheel_recalculate — house-system switcher [app-only]
11
+ *
12
+ * SVG payloads are NEVER embedded in model-facing text blocks. The UI calls
13
+ * chart_wheel_fetch_svg lazily to keep Claude's context lean.
10
14
  *
11
15
  * Also exports resource helpers (getChartWheelBundle, etc.) for use in
12
16
  * index.ts and server-sse.ts.
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * chart-wheel-app.ts — MCP App tool registration for the Chart Wheel Explorer.
3
3
  *
4
- * Registers 5 tools:
5
- * • explore_natal_chart — primary entry point, returns data + UI resource
6
- * • chart_wheel_on_planet_click event handler for planet clicks
7
- * • chart_wheel_on_house_clickevent handler for house clicks
8
- * • chart_wheel_on_aspect_click event handler for aspect clicks
9
- * • chart_wheel_recalculate recalculates with a different house system
4
+ * Registers 7 tools:
5
+ * • explore_natal_chart — primary entry point, returns data + UI resource [model]
6
+ * • chart_wheel_fetch_svg lazy SVG fetcher called by UI on first render [app-only]
7
+ * • chart_wheel_on_planet_clickplanet click handler [app-only]
8
+ * • chart_wheel_on_house_click house click handler [app-only]
9
+ * • chart_wheel_on_aspect_click aspect click handler [app-only]
10
+ * • chart_wheel_recalculate — house-system switcher [app-only]
11
+ *
12
+ * SVG payloads are NEVER embedded in model-facing text blocks. The UI calls
13
+ * chart_wheel_fetch_svg lazily to keep Claude's context lean.
10
14
  *
11
15
  * Also exports resource helpers (getChartWheelBundle, etc.) for use in
12
16
  * index.ts and server-sse.ts.
@@ -55,6 +59,22 @@ export function getChartWheelBundle() {
55
59
  export function clearBundleCache() {
56
60
  cachedBundle = null;
57
61
  }
62
+ // ── Per-request SVG cache (serves chart_wheel_fetch_svg lazily) ────────────
63
+ // Keyed by "datetime|lat|lon|houseSystem". Bounded to last 50 renders.
64
+ const SVG_CACHE_MAX = 50;
65
+ const cachedSvgMap = new Map();
66
+ function buildSvgCacheKey(datetime, lat, lon, houseSystem) {
67
+ return `${datetime}|${lat ?? 0}|${lon ?? 0}|${houseSystem}`;
68
+ }
69
+ function cacheSetSvg(key, svg) {
70
+ if (cachedSvgMap.size >= SVG_CACHE_MAX) {
71
+ // Evict oldest entry
72
+ const first = cachedSvgMap.keys().next().value;
73
+ if (first !== undefined)
74
+ cachedSvgMap.delete(first);
75
+ }
76
+ cachedSvgMap.set(key, svg);
77
+ }
58
78
  // ── House system helpers ───────────────────────────────────────────────────
59
79
  const HOUSE_SYSTEM_MAP = {
60
80
  placidus: "P", whole_sign: "W", equal: "E", koch: "K",
@@ -136,39 +156,26 @@ registerTool({
136
156
  const lat = args.latitude;
137
157
  const lon = args.longitude;
138
158
  const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
139
- // Parallel: fetch JSON data + SVG visual
140
- const [chartData, svgBinary] = await Promise.all([
141
- client.post("/ephemeris/natal-chart", natalBody),
142
- client.post(`/visualization/chart-wheel?format=svg&size=800`, { ...natalBody, style: "light" }).catch(() => null),
143
- ]);
144
- // Decode SVG from binary response (base64 → UTF-8 string)
145
- let svgString = null;
146
- if (svgBinary && typeof svgBinary === "object" && svgBinary.encoding === "base64" && svgBinary.data_base64) {
147
- svgString = Buffer.from(svgBinary.data_base64, "base64").toString("utf-8");
148
- }
149
- // Build the enriched chart payload for the UI
150
- const uiPayload = {
151
- ...chartData,
152
- _svg_base: svgString,
153
- _birth_params: {
154
- datetime,
155
- timezone: args.timezone ?? null,
156
- latitude: lat ?? null,
157
- longitude: lon ?? null,
158
- location: args.location ?? null,
159
- },
160
- house_system: houseSystem,
161
- };
162
- // Build human-readable summary for the LLM context / fallback text
159
+ // Fetch natal chart JSON. The chart is rendered client-side in the UI iframe,
160
+ // so we do NOT call the /visualization/chart-wheel endpoint — that was the
161
+ // source of the ~90 second blocking delay.
162
+ const chartData = await client.post("/ephemeris/natal-chart", natalBody);
163
+ // Build human-readable summary for the LLM context
163
164
  const summary = buildChartSummary(chartData, String(args.location ?? `${lat}, ${lon}`));
164
- // Check if the UI bundle is available
165
+ // Build the UI payload (planets normalised to array, aspects computed)
166
+ const modelPayload = buildModelPayload(chartData, {
167
+ datetime,
168
+ timezone: args.timezone ?? null,
169
+ latitude: lat ?? null,
170
+ longitude: lon ?? null,
171
+ location: args.location ?? null,
172
+ }, houseSystem);
165
173
  const bundleAvailable = Boolean(getChartWheelBundle());
166
174
  if (bundleAvailable) {
167
- // Return: text fallback + JSON payload + resource reference
168
175
  return {
169
176
  content: [
170
177
  { type: "text", text: summary },
171
- { type: "text", text: JSON.stringify(uiPayload) },
178
+ { type: "text", text: JSON.stringify(modelPayload) },
172
179
  {
173
180
  type: "resource",
174
181
  resource: {
@@ -183,17 +190,14 @@ registerTool({
183
190
  },
184
191
  };
185
192
  }
186
- // Fallback: text summary + static SVG image block
187
- const content = [
188
- { type: "text", text: summary },
189
- ];
190
- if (svgString) {
191
- const b64 = Buffer.from(svgString).toString("base64");
192
- content.push({ type: "image", data: b64, mimeType: "image/svg+xml" });
193
- }
194
- return { content };
193
+ // Fallback: text summary only (non-App hosts; no SVG available here)
194
+ return { content: [{ type: "text", text: summary }] };
195
195
  },
196
196
  });
197
+ // ── chart_wheel_fetch_svg removed ────────────────────────────────────────────
198
+ // SVG is now embedded inline in the explore_natal_chart payload (_svg_base).
199
+ // The UI fast-path in main.ts checks _svg_base first and skips the tool call.
200
+ // The SVG cache is retained for chart_wheel_recalculate house system switches.
197
201
  // ── Tool: chart_wheel_on_planet_click ──────────────────────────────────────
198
202
  registerTool({
199
203
  name: "chart_wheel_on_planet_click",
@@ -214,6 +218,7 @@ registerTool({
214
218
  required: ["planet", "longitude"],
215
219
  },
216
220
  annotations: { title: "Planet Interpretation", readOnlyHint: true, openWorldHint: false },
221
+ _meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
217
222
  handler: async (args) => {
218
223
  const planet = String(args.planet);
219
224
  const sign = args.sign ? String(args.sign) : zodSignFromLon(Number(args.longitude));
@@ -247,6 +252,7 @@ registerTool({
247
252
  required: ["house_number"],
248
253
  },
249
254
  annotations: { title: "House Interpretation", readOnlyHint: true, openWorldHint: false },
255
+ _meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
250
256
  handler: async (args) => {
251
257
  const num = Number(args.house_number);
252
258
  const sign = args.sign ? String(args.sign) : "";
@@ -281,6 +287,7 @@ registerTool({
281
287
  required: ["planet1", "planet2"],
282
288
  },
283
289
  annotations: { title: "Aspect Interpretation", readOnlyHint: true, openWorldHint: false },
290
+ _meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
284
291
  handler: async (args) => {
285
292
  const p1 = capitalize(String(args.planet1));
286
293
  const p2 = capitalize(String(args.planet2));
@@ -319,6 +326,7 @@ registerTool({
319
326
  required: ["datetime", "latitude", "longitude", "house_system"],
320
327
  },
321
328
  annotations: { title: "Recalculate Chart", readOnlyHint: true, openWorldHint: false },
329
+ _meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
322
330
  handler: async (args) => {
323
331
  const client = getActiveClient();
324
332
  const houseSystem = String(args.house_system);
@@ -326,17 +334,11 @@ registerTool({
326
334
  const lat = args.latitude;
327
335
  const lon = args.longitude;
328
336
  const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
329
- const [chartData, svgBinary] = await Promise.all([
330
- client.post("/ephemeris/natal-chart", natalBody),
331
- client.post(`/visualization/chart-wheel?format=svg&size=800`, { ...natalBody, style: "light" }).catch(() => null),
332
- ]);
333
- let svgString = null;
334
- if (svgBinary && typeof svgBinary === "object" && svgBinary.encoding === "base64" && svgBinary.data_base64) {
335
- svgString = Buffer.from(svgBinary.data_base64, "base64").toString("utf-8");
336
- }
337
- const uiPayload = {
337
+ // Fetch updated chart JSON only — chart renders client-side in the iframe
338
+ const chartData = await client.post("/ephemeris/natal-chart", natalBody);
339
+ // Return only chart data (no SVG) the UI calls chart_wheel_fetch_svg
340
+ const payload = {
338
341
  ...chartData,
339
- _svg_base: svgString,
340
342
  _birth_params: {
341
343
  datetime,
342
344
  timezone: args.timezone ?? null,
@@ -347,7 +349,7 @@ registerTool({
347
349
  house_system: houseSystem,
348
350
  };
349
351
  return {
350
- content: [{ type: "text", text: JSON.stringify(uiPayload) }],
352
+ content: [{ type: "text", text: JSON.stringify(payload) }],
351
353
  };
352
354
  },
353
355
  });
@@ -365,6 +367,97 @@ function zodSignFromLon(lon) {
365
367
  "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"];
366
368
  return signs[Math.floor(((lon % 360) + 360) % 360 / 30)];
367
369
  }
370
+ const CLASSICAL_PLANETS = new Set([
371
+ "sun", "moon", "mercury", "venus", "mars",
372
+ "jupiter", "saturn", "uranus", "neptune", "pluto",
373
+ "chiron", "north_node", "south_node", "true_node", "asc", "mc",
374
+ ]);
375
+ /**
376
+ * Build a lean, SVG-free payload for Claude's text context.
377
+ * Strips SVG, filters to classical planets only, and packages birth params.
378
+ * Tokens saved vs. full uiPayload: ~85–95% reduction (no 100KB SVG, no asteroid flood).
379
+ */
380
+ /**
381
+ * Compute major aspects between a set of planets.
382
+ * Returns a structured AspectData[] array — the natal chart API does not
383
+ * include aspects in its JSON response; we derive them from longitude data.
384
+ */
385
+ function computeAspects(planets) {
386
+ const ASPECTS = [
387
+ { type: "conjunction", angle: 0, orb: 8 },
388
+ { type: "opposition", angle: 180, orb: 8 },
389
+ { type: "trine", angle: 120, orb: 8 },
390
+ { type: "square", angle: 90, orb: 7 },
391
+ { type: "sextile", angle: 60, orb: 6 },
392
+ { type: "quincunx", angle: 150, orb: 3 },
393
+ { type: "semisquare", angle: 45, orb: 2 },
394
+ ];
395
+ const results = [];
396
+ for (let i = 0; i < planets.length; i++) {
397
+ for (let j = i + 1; j < planets.length; j++) {
398
+ const p1 = planets[i];
399
+ const p2 = planets[j];
400
+ // Angular separation: shortest arc (0–180)
401
+ let diff = Math.abs(p1.longitude - p2.longitude) % 360;
402
+ if (diff > 180)
403
+ diff = 360 - diff;
404
+ for (const asp of ASPECTS) {
405
+ const orb = Math.abs(diff - asp.angle);
406
+ if (orb <= asp.orb) {
407
+ // Applying: combined longitudinal speed closing the orb
408
+ const applying = ((p1.speed ?? 1) - (p2.speed ?? 0)) < 0 ? diff < asp.angle : diff > asp.angle;
409
+ results.push({
410
+ planet1: p1.name,
411
+ planet2: p2.name,
412
+ type: asp.type,
413
+ angle: asp.angle,
414
+ orb: Math.round(orb * 100) / 100,
415
+ applying,
416
+ });
417
+ break; // Each pair only matches one aspect type
418
+ }
419
+ }
420
+ }
421
+ }
422
+ return results;
423
+ }
424
+ function buildModelPayload(chartData, birthParams, houseSystem, svgBase) {
425
+ // ── 1. Normalize planets: keyed object → typed PlanetData array ──────────
426
+ // The natal API returns { sun: { longitude, sign_name, is_retrograde, ... }, moon: {...} }.
427
+ // The chart wheel UI expects PlanetData[] with canonical field names.
428
+ const rawPlanets = chartData.planets;
429
+ let planetsArray = [];
430
+ if (rawPlanets && typeof rawPlanets === "object" && !Array.isArray(rawPlanets)) {
431
+ planetsArray = Object.entries(rawPlanets)
432
+ .filter(([k]) => CLASSICAL_PLANETS.has(k.toLowerCase()))
433
+ .map(([name, p]) => ({
434
+ name: name.toLowerCase(),
435
+ longitude: (p.longitude ?? p.lon ?? 0),
436
+ latitude: (p.latitude ?? p.lat),
437
+ speed: (p.longitude_speed ?? p.speed),
438
+ retrograde: Boolean(p.is_retrograde ?? p.retrograde),
439
+ sign: (p.sign_name ?? p.sign),
440
+ house: (p.house),
441
+ }));
442
+ }
443
+ else if (Array.isArray(chartData.planets)) {
444
+ // Already normalised (e.g. house system recalculate path)
445
+ planetsArray = chartData.planets;
446
+ }
447
+ // ── 2. Compute aspects from longitude data (API doesn't return them) ──────
448
+ const aspects = computeAspects(planetsArray);
449
+ return {
450
+ ...chartData,
451
+ // Typed array replaces the keyed object — the renderer can now iterate it
452
+ planets: planetsArray,
453
+ // Structured aspect array — powers highlighting and info panel
454
+ aspects,
455
+ // Embed SVG for fast first-render in the iframe.
456
+ _svg_base: svgBase ?? undefined,
457
+ _birth_params: birthParams,
458
+ house_system: houseSystem,
459
+ };
460
+ }
368
461
  function buildChartSummary(data, location) {
369
462
  // The natal API returns planets as a keyed object { sun: {...}, moon: {...} }.
370
463
  // Normalise to an array before processing.
@@ -373,12 +466,65 @@ function buildChartSummary(data, location) {
373
466
  ? raw
374
467
  : Object.entries(raw).map(([name, p]) => ({
375
468
  name,
376
- sign: p.sign,
469
+ sign: (p.sign_name ?? p.sign),
377
470
  house: p.house,
378
- retrograde: Boolean(p.retrograde),
471
+ retrograde: Boolean(p.is_retrograde ?? p.retrograde),
472
+ longitude: p.longitude,
473
+ longitude_speed: p.longitude_speed,
379
474
  }));
380
- const lines = planetsArray.slice(0, 10).map((p) => `• **${capitalize(p.name)}** in ${p.sign ?? "?"} (H${p.house ?? "?"})${p.retrograde ? " ℞" : ""}`);
475
+ const CLASSICAL_ORDER = [
476
+ "sun", "moon", "mercury", "venus", "mars",
477
+ "jupiter", "saturn", "uranus", "neptune", "pluto",
478
+ "chiron", "north_node", "south_node", "true_node", "asc", "mc",
479
+ ];
480
+ planetsArray.sort((a, b) => {
481
+ const ai = CLASSICAL_ORDER.indexOf(a.name.toLowerCase());
482
+ const bi = CLASSICAL_ORDER.indexOf(b.name.toLowerCase());
483
+ if (ai !== -1 && bi !== -1)
484
+ return ai - bi;
485
+ if (ai !== -1)
486
+ return -1;
487
+ if (bi !== -1)
488
+ return 1;
489
+ return a.name.localeCompare(b.name);
490
+ });
491
+ const lines = planetsArray.slice(0, 12).map((p) => `• **${capitalize(p.name)}** in ${p.sign ?? "?"} (H${p.house ?? "?"})${p.retrograde ? " ℞" : ""}`);
492
+ // ── Enrichment: sect, element tally, strongest aspect ──────────────────
493
+ const sun = planetsArray.find((p) => p.name.toLowerCase() === "sun");
494
+ const ascLon = data.ascendant;
495
+ const sunLon = sun?.longitude;
496
+ let sectNote = "";
497
+ if (sunLon != null && ascLon != null) {
498
+ // Day chart: Sun above horizon (houses 7–12, i.e. longitude within 180° of ASC going upper half)
499
+ const sunRelative = ((sunLon - ascLon) + 360) % 360;
500
+ sectNote = sunRelative < 180 ? "Day chart ☀️" : "Night chart 🌙";
501
+ }
502
+ const ELEMENT_MAP = {
503
+ aries: "fire", leo: "fire", sagittarius: "fire",
504
+ taurus: "earth", virgo: "earth", capricorn: "earth",
505
+ gemini: "air", libra: "air", aquarius: "air",
506
+ cancer: "water", scorpio: "water", pisces: "water",
507
+ };
508
+ const elementCount = { fire: 0, earth: 0, air: 0, water: 0 };
509
+ for (const p of planetsArray.slice(0, 10)) {
510
+ const sign = (p.sign ?? "").toLowerCase();
511
+ const el = ELEMENT_MAP[sign];
512
+ if (el)
513
+ elementCount[el]++;
514
+ }
515
+ const domElement = Object.entries(elementCount).sort((a, b) => b[1] - a[1])[0];
516
+ // Strongest aspect by tightest orb (use computed aspects from planetsArray)
517
+ const aspects = computeAspects(planetsArray
518
+ .filter((p) => p.longitude != null)
519
+ .map((p) => ({ name: p.name, longitude: p.longitude, speed: p.longitude_speed })));
520
+ const strongest = aspects.sort((a, b) => a.orb - b.orb)[0];
521
+ const strongestNote = strongest
522
+ ? `Closest aspect: ${capitalize(strongest.planet1)} ${strongest.type} ${capitalize(strongest.planet2)} (${strongest.orb.toFixed(1)}° orb)`
523
+ : "";
524
+ const enrichment = [sectNote, domElement ? `Dominant element: ${domElement[0]} (${domElement[1]})` : "", strongestNote]
525
+ .filter(Boolean).join(" · ");
381
526
  return (`**Natal Chart — ${location}**\n\n` +
527
+ (enrichment ? `${enrichment}\n\n` : "") +
382
528
  `Key Placements:\n${lines.join("\n")}\n\n` +
383
529
  "Click any planet, house, or aspect line for interpretation.");
384
530
  }
package/dist/tools/dev.js CHANGED
@@ -47,6 +47,7 @@ registerTool({
47
47
  " • ACG / astrocartography: 10 credits (acg/hits: 15 credits)\n" +
48
48
  " • Calendar endpoints: 10 credits\n" +
49
49
  " • Catalog / metadata / health endpoints: 0 credits\n" +
50
+ " • Compute surcharge: requests > 30s add 1 credit per 30s (predictive, acg, calendar, electional)\n" +
50
51
  " • format=llm (token-optimized output): available on all tiers\n\n" +
51
52
  "COMMON CALLS:\n" +
52
53
  " POST /ephemeris/natal-chart — Full natal chart (body: {subject: {name: 'Name', birth_datetime: {iso: '1990-04-15T14:30:00-05:00'}, birth_location: {latitude: {decimal: 40.0}, longitude: {decimal: -70.0}, timezone: {}}}})\n" +
@@ -10,8 +10,25 @@ export interface ToolDefinition {
10
10
  idempotentHint?: boolean;
11
11
  openWorldHint?: boolean;
12
12
  };
13
+ /**
14
+ * UI metadata — mirrors the ext-apps SDK _meta.ui shape.
15
+ * When visibility includes "app" only (not "model"), the tool is hidden from
16
+ * Claude's tool list (ListTools) but remains callable by the UI via callServerTool().
17
+ */
18
+ _meta?: {
19
+ ui?: {
20
+ resourceUri?: string;
21
+ /** SDK visibility array — use ["app"] to hide from model, ["model"] for model-only. */
22
+ visibility?: Array<"app" | "model">;
23
+ };
24
+ };
13
25
  handler: (args: any) => Promise<any>;
14
26
  }
27
+ /**
28
+ * Returns tools that should be exposed to Claude in the ListTools response.
29
+ * Filters out tools that have visibility restricted to the UI (app-only).
30
+ */
31
+ export declare function modelVisibleTools(): ToolDefinition[];
15
32
  export declare const toolRegistry: Record<string, ToolDefinition>;
16
33
  export declare function registerTool(tool: ToolDefinition): void;
17
34
  export type ToolProfile = "dev" | "legacy";
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Returns tools that should be exposed to Claude in the ListTools response.
3
+ * Filters out tools that have visibility restricted to the UI (app-only).
4
+ */
5
+ export function modelVisibleTools() {
6
+ return Object.values(toolRegistry).filter((t) => {
7
+ const vis = t._meta?.ui?.visibility;
8
+ // If visibility is explicitly ["app"] (no "model"), hide from model
9
+ if (vis && !vis.includes("model") && vis.includes("app"))
10
+ return false;
11
+ return true;
12
+ });
13
+ }
1
14
  export const toolRegistry = {};
2
15
  export function registerTool(tool) {
3
16
  toolRegistry[tool.name] = tool;
@@ -15,7 +15,10 @@ registerTool({
15
15
  " Example: Chiron Return → transiting_planets=['chiron'], natal_points=['chiron'], aspect_angle=0\n\n" +
16
16
  "SUPPORTED BODIES: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune, " +
17
17
  "Pluto, Chiron, Pholus, Ceres, Pallas, Juno, Vesta, MeanNode.\n\n" +
18
- "CREDIT COST: 6 credits per call (1 for natal + 5 for transit search).\n\n" +
18
+ "CREDIT COST: 6 credits per call (1 for natal + 5 for transit search). " +
19
+ "Requests exceeding 30 seconds incur 1 additional credit per 30s of compute time.\n\n" +
20
+ "SEARCH RANGE LIMITS: Explorer tier is limited to 1-year windows; Developer to 5 years. " +
21
+ "Ranges exceeding the limit are silently clamped.\n\n" +
19
22
  "EXAMPLE: Find all Saturn transits to the natal Sun/Moon for the next 6 months:\n" +
20
23
  " natal_datetime='1990-04-15T14:30:00', natal_latitude=41.8781, natal_longitude=-87.6298,\n" +
21
24
  " start_date='2026-01-01', end_date='2026-06-30', transiting_planets=['saturn']",