@openephemeris/mcp-server 3.10.0 → 3.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -355,8 +355,8 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
355
355
 
356
356
  - Allowlisted operations: **23**
357
357
  - Methods: `GET=4`, `POST=19`, `PUT=0`, `PATCH=0`, `DELETE=0`
358
- - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **52**
359
- - Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `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`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `vedic_chart`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
358
+ - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **57**
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:
361
361
 
362
362
  ### Allowlist Families
@@ -391,14 +391,6 @@
391
391
  ],
392
392
  "operationId": "reverse_geocode_location_reverse_get"
393
393
  },
394
- {
395
- "method": "GET",
396
- "path": "/tidal/forcing/deep-time",
397
- "tags": [
398
- "tidal"
399
- ],
400
- "operationId": "get_tidal_forcing_deep_time_endpoint_tidal_forcing_deep_time_get"
401
- },
402
394
  {
403
395
  "method": "GET",
404
396
  "path": "/tidal/forcing",
package/dist/index.js CHANGED
@@ -4,8 +4,9 @@ import path from "node:path";
4
4
  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
- import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
7
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
8
8
  import { initTools, toolRegistry, formatToolResponse } from "./tools/index.js";
9
+ import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
9
10
  function resolveServerVersion() {
10
11
  try {
11
12
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -34,6 +35,7 @@ const server = new Server({
34
35
  capabilities: {
35
36
  tools: {},
36
37
  prompts: {},
38
+ resources: {},
37
39
  },
38
40
  });
39
41
  // List available tools
@@ -92,6 +94,41 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
92
94
  ]
93
95
  };
94
96
  });
97
+ // List available resources
98
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
99
+ const bundle = getChartWheelBundle();
100
+ if (!bundle)
101
+ return { resources: [] };
102
+ return {
103
+ resources: [
104
+ {
105
+ uri: CHART_WHEEL_RESOURCE_URI,
106
+ name: "Chart Wheel Explorer",
107
+ description: "Interactive natal chart wheel with clickable planets, houses, and aspects.",
108
+ mimeType: CHART_WHEEL_MIME_TYPE,
109
+ },
110
+ ],
111
+ };
112
+ });
113
+ // Read resource
114
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
115
+ if (request.params.uri !== CHART_WHEEL_RESOURCE_URI) {
116
+ throw new Error(`Unknown resource: ${request.params.uri}`);
117
+ }
118
+ const bundle = getChartWheelBundle();
119
+ if (!bundle) {
120
+ throw new Error("Chart Wheel UI bundle not found. Run `npm run build:ui` to build it.");
121
+ }
122
+ return {
123
+ contents: [
124
+ {
125
+ uri: CHART_WHEEL_RESOURCE_URI,
126
+ mimeType: CHART_WHEEL_MIME_TYPE,
127
+ text: bundle,
128
+ },
129
+ ],
130
+ };
131
+ });
95
132
  // Handle tool calls
96
133
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
97
134
  const toolName = request.params.name;
@@ -21,9 +21,10 @@ import axios from "axios";
21
21
  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
- import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, isInitializeRequest, } from "@modelcontextprotocol/sdk/types.js";
24
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, isInitializeRequest, } from "@modelcontextprotocol/sdk/types.js";
25
25
  import { initTools, toolRegistry, formatToolResponse } from "./tools/index.js";
26
26
  import { BackendClient, runWithClient } from "./backend/client.js";
27
+ import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
27
28
  // ---------------------------------------------------------------------------
28
29
  // Helpers
29
30
  // ---------------------------------------------------------------------------
@@ -184,7 +185,7 @@ function createMcpServer() {
184
185
  "and more — powered by JPL DE440 ephemerides for sub-arcsecond accuracy.",
185
186
  },
186
187
  }, {
187
- capabilities: { tools: {}, prompts: {} },
188
+ capabilities: { tools: {}, prompts: {}, resources: {} },
188
189
  instructions: "Open Ephemeris is a precision astronomical computation engine. " +
189
190
  "Use format='llm' on any tool for compact, token-efficient output. " +
190
191
  "All computations use JPL DE440 ephemerides for sub-arcsecond accuracy. " +
@@ -240,6 +241,40 @@ function createMcpServer() {
240
241
  };
241
242
  }
242
243
  });
244
+ // --- Resource handlers ---
245
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
246
+ const bundle = getChartWheelBundle();
247
+ if (!bundle)
248
+ return { resources: [] };
249
+ return {
250
+ resources: [
251
+ {
252
+ uri: CHART_WHEEL_RESOURCE_URI,
253
+ name: "Chart Wheel Explorer",
254
+ description: "Interactive natal chart wheel with clickable planets, houses, and aspects.",
255
+ mimeType: CHART_WHEEL_MIME_TYPE,
256
+ },
257
+ ],
258
+ };
259
+ });
260
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
261
+ if (request.params.uri !== CHART_WHEEL_RESOURCE_URI) {
262
+ throw new Error(`Unknown resource: ${request.params.uri}`);
263
+ }
264
+ const bundle = getChartWheelBundle();
265
+ if (!bundle) {
266
+ throw new Error("Chart Wheel UI bundle not found. Run `npm run build:ui` to build it.");
267
+ }
268
+ return {
269
+ contents: [
270
+ {
271
+ uri: CHART_WHEEL_RESOURCE_URI,
272
+ mimeType: CHART_WHEEL_MIME_TYPE,
273
+ text: bundle,
274
+ },
275
+ ],
276
+ };
277
+ });
243
278
  return server;
244
279
  }
245
280
  const httpSessions = new Map();
@@ -0,0 +1,19 @@
1
+ /**
2
+ * chart-wheel-app.ts — MCP App tool registration for the Chart Wheel Explorer.
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_click — event 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
10
+ *
11
+ * Also exports resource helpers (getChartWheelBundle, etc.) for use in
12
+ * index.ts and server-sse.ts.
13
+ */
14
+ export declare const CHART_WHEEL_RESOURCE_URI = "ui://openephemeris/chart-wheel";
15
+ export declare const CHART_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
16
+ /** Read the pre-built HTML bundle. Returns null if not yet built. */
17
+ export declare function getChartWheelBundle(): string | null;
18
+ /** Reset cache (useful in dev watch mode or tests). */
19
+ export declare function clearBundleCache(): void;
@@ -0,0 +1,478 @@
1
+ /**
2
+ * chart-wheel-app.ts — MCP App tool registration for the Chart Wheel Explorer.
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_click — event 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
10
+ *
11
+ * Also exports resource helpers (getChartWheelBundle, etc.) for use in
12
+ * index.ts and server-sse.ts.
13
+ */
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { registerTool } from "../index.js";
18
+ import { getActiveClient } from "../../backend/client.js";
19
+ // ── Constants ─────────────────────────────────────────────────────────────
20
+ export const CHART_WHEEL_RESOURCE_URI = "ui://openephemeris/chart-wheel";
21
+ export const CHART_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
22
+ // Bundle path: at runtime (compiled), __dirname = dist/tools/apps/
23
+ // so ../../../dist/ui/ = dist/ui/ ✓
24
+ // In dev (tsx), __dirname = src/tools/apps/
25
+ // so ../../../dist/ui/ = dist/ui/ ✓ (UI pre-build always goes to dist/ui/)
26
+ const here = path.dirname(fileURLToPath(import.meta.url));
27
+ const BUNDLE_PATHS = [
28
+ // Works in both compiled (dist/tools/apps) and dev (src/tools/apps)
29
+ path.resolve(here, "..", "..", "..", "dist", "ui", "chart-wheel.html"),
30
+ ];
31
+ function findBundlePath() {
32
+ for (const p of BUNDLE_PATHS) {
33
+ if (fs.existsSync(p))
34
+ return p;
35
+ }
36
+ return null;
37
+ }
38
+ let cachedBundle = null;
39
+ /** Read the pre-built HTML bundle. Returns null if not yet built. */
40
+ export function getChartWheelBundle() {
41
+ if (cachedBundle)
42
+ return cachedBundle;
43
+ const bundlePath = findBundlePath();
44
+ if (!bundlePath)
45
+ return null;
46
+ try {
47
+ cachedBundle = fs.readFileSync(bundlePath, "utf-8");
48
+ return cachedBundle;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ /** Reset cache (useful in dev watch mode or tests). */
55
+ export function clearBundleCache() {
56
+ cachedBundle = null;
57
+ }
58
+ // ── House system helpers ───────────────────────────────────────────────────
59
+ const HOUSE_SYSTEM_MAP = {
60
+ placidus: "P", whole_sign: "W", equal: "E", koch: "K",
61
+ campanus: "C", regiomontanus: "R",
62
+ };
63
+ /** Build the nested request body expected by /ephemeris/natal-chart (POST). */
64
+ function buildNatalBody(datetime, lat, lon, houseSystem, timezone) {
65
+ const body = {
66
+ subject: {
67
+ name: "MCP Request",
68
+ birth_datetime: { iso: datetime },
69
+ birth_location: {
70
+ latitude: { decimal: lat ?? 0 },
71
+ longitude: { decimal: lon ?? 0 },
72
+ },
73
+ },
74
+ configuration: {
75
+ house_system: HOUSE_SYSTEM_MAP[houseSystem] ?? "P",
76
+ },
77
+ };
78
+ if (timezone) {
79
+ body.subject.birth_location.timezone = { iana_name: timezone };
80
+ }
81
+ return body;
82
+ }
83
+ // ── Tool: explore_natal_chart ──────────────────────────────────────────────
84
+ registerTool({
85
+ name: "explore_natal_chart",
86
+ description: "Generate an interactive natal chart wheel with clickable planets, houses, and aspects. " +
87
+ "Returns an embedded visual chart explorer that lets you click any planet, house, or aspect " +
88
+ "line for instant astrological interpretation. " +
89
+ "Supports house system switching (Placidus, Whole Sign, Equal, Koch). " +
90
+ "The chart is computed using NASA JPL DE440 ephemerides for sub-arcsecond precision. " +
91
+ "Use this instead of ephemeris_chart_wheel for a richer, interactive experience in " +
92
+ "MCP Apps-capable hosts (Claude Desktop). Falls back to static SVG in other hosts.",
93
+ inputSchema: {
94
+ type: "object",
95
+ properties: {
96
+ datetime: {
97
+ type: "string",
98
+ description: "ISO 8601 datetime string, e.g. '1990-04-15T14:30:00'. " +
99
+ "Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.",
100
+ },
101
+ timezone: {
102
+ type: "string",
103
+ description: "IANA timezone name (e.g. 'America/New_York'). Used if datetime has no UTC offset.",
104
+ },
105
+ latitude: {
106
+ type: "number",
107
+ description: "Birth latitude in decimal degrees (positive = North).",
108
+ },
109
+ longitude: {
110
+ type: "number",
111
+ description: "Birth longitude in decimal degrees (positive = East).",
112
+ },
113
+ location: {
114
+ type: "string",
115
+ description: "Location name for display only (e.g. 'New York, NY').",
116
+ },
117
+ house_system: {
118
+ type: "string",
119
+ enum: ["placidus", "whole_sign", "equal", "koch", "regiomontanus", "campanus"],
120
+ description: "House system to use (default: placidus).",
121
+ },
122
+ },
123
+ required: ["datetime", "latitude", "longitude"],
124
+ },
125
+ annotations: {
126
+ title: "Interactive Chart Wheel Explorer",
127
+ readOnlyHint: true,
128
+ destructiveHint: false,
129
+ idempotentHint: true,
130
+ openWorldHint: false,
131
+ },
132
+ handler: async (args) => {
133
+ const client = getActiveClient();
134
+ const houseSystem = args.house_system ?? "placidus";
135
+ const datetime = String(args.datetime);
136
+ const lat = args.latitude;
137
+ const lon = args.longitude;
138
+ 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
163
+ const summary = buildChartSummary(chartData, String(args.location ?? `${lat}, ${lon}`));
164
+ // Check if the UI bundle is available
165
+ const bundleAvailable = Boolean(getChartWheelBundle());
166
+ if (bundleAvailable) {
167
+ // Return: text fallback + JSON payload + resource reference
168
+ return {
169
+ content: [
170
+ { type: "text", text: summary },
171
+ { type: "text", text: JSON.stringify(uiPayload) },
172
+ {
173
+ type: "resource",
174
+ resource: {
175
+ uri: CHART_WHEEL_RESOURCE_URI,
176
+ mimeType: CHART_WHEEL_MIME_TYPE,
177
+ text: "[Chart Wheel Explorer UI]",
178
+ },
179
+ },
180
+ ],
181
+ _meta: {
182
+ ui: { resourceUri: CHART_WHEEL_RESOURCE_URI },
183
+ },
184
+ };
185
+ }
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 };
195
+ },
196
+ });
197
+ // ── Tool: chart_wheel_on_planet_click ──────────────────────────────────────
198
+ registerTool({
199
+ name: "chart_wheel_on_planet_click",
200
+ description: "Event handler called when the user clicks a planet in the interactive chart wheel. " +
201
+ "Provides rich contextual interpretation of the planet's placement — sign, house, " +
202
+ "aspects, dignity, and what it means for the native. " +
203
+ "This tool is called automatically by the chart UI; you do not need to call it directly.",
204
+ inputSchema: {
205
+ type: "object",
206
+ properties: {
207
+ planet: { type: "string", description: "Planet name (e.g. 'saturn')" },
208
+ longitude: { type: "number", description: "Ecliptic longitude in degrees" },
209
+ sign: { type: "string", description: "Zodiac sign the planet occupies" },
210
+ house: { type: "number", description: "House the planet occupies (1–12)" },
211
+ retrograde: { type: "boolean", description: "Whether the planet is retrograde" },
212
+ speed: { type: "number", description: "Daily motion in degrees" },
213
+ },
214
+ required: ["planet", "longitude"],
215
+ },
216
+ annotations: { title: "Planet Interpretation", readOnlyHint: true, openWorldHint: false },
217
+ handler: async (args) => {
218
+ const planet = String(args.planet);
219
+ const sign = args.sign ? String(args.sign) : zodSignFromLon(Number(args.longitude));
220
+ const house = args.house ? ` in the ${ordinal(Number(args.house))} house` : "";
221
+ const retro = args.retrograde ? " (retrograde ℞)" : "";
222
+ const degree = `${Math.floor(Number(args.longitude) % 30)}° ${sign}`;
223
+ return {
224
+ content: [{
225
+ type: "text",
226
+ text: `**${capitalize(planet)} at ${degree}${house}${retro}**\n\n` +
227
+ interpretPlanetPlacement(planet, sign, Number(args.house ?? 0), Boolean(args.retrograde)),
228
+ }],
229
+ };
230
+ },
231
+ });
232
+ // ── Tool: chart_wheel_on_house_click ──────────────────────────────────────
233
+ registerTool({
234
+ name: "chart_wheel_on_house_click",
235
+ description: "Event handler called when the user clicks a house cusp or house number in the chart wheel. " +
236
+ "Returns the astrological meaning of that house — life area, themes, and the influence " +
237
+ "of its ruling sign and planet. " +
238
+ "This tool is called automatically by the chart UI; you do not need to call it directly.",
239
+ inputSchema: {
240
+ type: "object",
241
+ properties: {
242
+ house_number: { type: "number", description: "House number (1–12)" },
243
+ sign: { type: "string", description: "Sign on the house cusp" },
244
+ cusp_longitude: { type: "number", description: "Cusp longitude in degrees" },
245
+ ruler: { type: "string", description: "Ruling planet of the house sign" },
246
+ },
247
+ required: ["house_number"],
248
+ },
249
+ annotations: { title: "House Interpretation", readOnlyHint: true, openWorldHint: false },
250
+ handler: async (args) => {
251
+ const num = Number(args.house_number);
252
+ const sign = args.sign ? String(args.sign) : "";
253
+ const ruler = args.ruler ? String(args.ruler) : "";
254
+ return {
255
+ content: [{
256
+ type: "text",
257
+ text: `**House ${num}${sign ? ` — ${sign} on the cusp` : ""}${ruler ? ` (ruled by ${ruler})` : ""}**\n\n` +
258
+ HOUSE_MEANINGS[num] +
259
+ (sign ? `\n\n${capitalize(sign)} on this cusp emphasizes ${SIGN_HOUSE_FLAVOR[sign.toLowerCase()] ?? "its natural qualities"} in the matters of this house.` : ""),
260
+ }],
261
+ };
262
+ },
263
+ });
264
+ // ── Tool: chart_wheel_on_aspect_click ────────────────────────────────────
265
+ registerTool({
266
+ name: "chart_wheel_on_aspect_click",
267
+ description: "Event handler called when the user clicks an aspect line between planets in the chart wheel. " +
268
+ "Returns the astrological interpretation of that aspect — its nature, energy, challenges, " +
269
+ "and integration advice. " +
270
+ "This tool is called automatically by the chart UI; you do not need to call it directly.",
271
+ inputSchema: {
272
+ type: "object",
273
+ properties: {
274
+ planet1: { type: "string" },
275
+ planet2: { type: "string" },
276
+ aspect_type: { type: "string", description: "Conjunction, opposition, trine, square, sextile, etc." },
277
+ orb: { type: "number", description: "Orb in degrees" },
278
+ applying: { type: "boolean", description: "True if aspect is applying (intensifying)" },
279
+ angle: { type: "number", description: "Exact aspect angle" },
280
+ },
281
+ required: ["planet1", "planet2"],
282
+ },
283
+ annotations: { title: "Aspect Interpretation", readOnlyHint: true, openWorldHint: false },
284
+ handler: async (args) => {
285
+ const p1 = capitalize(String(args.planet1));
286
+ const p2 = capitalize(String(args.planet2));
287
+ const type = args.aspect_type ? String(args.aspect_type) : "aspect";
288
+ const orb = args.orb != null ? ` (${Number(args.orb).toFixed(1)}° orb)` : "";
289
+ const timing = args.applying != null ? (args.applying ? " — applying" : " — separating") : "";
290
+ return {
291
+ content: [{
292
+ type: "text",
293
+ text: `**${p1} ${capitalize(type)} ${p2}${orb}${timing}**\n\n` +
294
+ interpretAspect(String(args.planet1), String(args.planet2), type, Boolean(args.applying)),
295
+ }],
296
+ };
297
+ },
298
+ });
299
+ // ── Tool: chart_wheel_recalculate ─────────────────────────────────────────
300
+ registerTool({
301
+ name: "chart_wheel_recalculate",
302
+ description: "Recalculates the natal chart using a different house system. " +
303
+ "Called automatically by the chart wheel UI when the user switches house systems. " +
304
+ "Returns updated chart data including new house cusps and planet house assignments.",
305
+ inputSchema: {
306
+ type: "object",
307
+ properties: {
308
+ datetime: { type: "string", description: "ISO 8601 datetime string" },
309
+ timezone: { type: "string" },
310
+ latitude: { type: "number" },
311
+ longitude: { type: "number" },
312
+ location: { type: "string" },
313
+ house_system: {
314
+ type: "string",
315
+ enum: ["placidus", "whole_sign", "equal", "koch", "regiomontanus", "campanus"],
316
+ },
317
+ },
318
+ required: ["datetime", "latitude", "longitude", "house_system"],
319
+ },
320
+ annotations: { title: "Recalculate Chart", readOnlyHint: true, openWorldHint: false },
321
+ handler: async (args) => {
322
+ const client = getActiveClient();
323
+ const houseSystem = String(args.house_system);
324
+ const datetime = String(args.datetime);
325
+ const lat = args.latitude;
326
+ const lon = args.longitude;
327
+ const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
328
+ const [chartData, svgBinary] = await Promise.all([
329
+ client.post("/ephemeris/natal-chart", natalBody),
330
+ client.post(`/visualization/chart-wheel?format=svg&size=800`, { ...natalBody, style: "light" }).catch(() => null),
331
+ ]);
332
+ let svgString = null;
333
+ if (svgBinary && typeof svgBinary === "object" && svgBinary.encoding === "base64" && svgBinary.data_base64) {
334
+ svgString = Buffer.from(svgBinary.data_base64, "base64").toString("utf-8");
335
+ }
336
+ const uiPayload = {
337
+ ...chartData,
338
+ _svg_base: svgString,
339
+ _birth_params: {
340
+ datetime,
341
+ timezone: args.timezone ?? null,
342
+ latitude: lat ?? null,
343
+ longitude: lon ?? null,
344
+ location: args.location ?? null,
345
+ },
346
+ house_system: houseSystem,
347
+ };
348
+ return {
349
+ content: [{ type: "text", text: JSON.stringify(uiPayload) }],
350
+ };
351
+ },
352
+ });
353
+ // ── Interpretation helpers ────────────────────────────────────────────────
354
+ function capitalize(s) {
355
+ return s.charAt(0).toUpperCase() + s.slice(1);
356
+ }
357
+ function ordinal(n) {
358
+ const s = ["th", "st", "nd", "rd"];
359
+ const v = n % 100;
360
+ return n + (s[(v - 20) % 10] || s[v] || s[0]);
361
+ }
362
+ function zodSignFromLon(lon) {
363
+ const signs = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
364
+ "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"];
365
+ return signs[Math.floor(((lon % 360) + 360) % 360 / 30)];
366
+ }
367
+ function buildChartSummary(data, location) {
368
+ // The natal API returns planets as a keyed object { sun: {...}, moon: {...} }.
369
+ // Normalise to an array before processing.
370
+ const raw = data.planets ?? {};
371
+ const planetsArray = Array.isArray(raw)
372
+ ? raw
373
+ : Object.entries(raw).map(([name, p]) => ({
374
+ name,
375
+ sign: p.sign,
376
+ house: p.house,
377
+ retrograde: Boolean(p.retrograde),
378
+ }));
379
+ const lines = planetsArray.slice(0, 10).map((p) => `• **${capitalize(p.name)}** in ${p.sign ?? "?"} (H${p.house ?? "?"})${p.retrograde ? " ℞" : ""}`);
380
+ return (`**Natal Chart — ${location}**\n\n` +
381
+ `Key Placements:\n${lines.join("\n")}\n\n` +
382
+ "Click any planet, house, or aspect line for interpretation.");
383
+ }
384
+ function interpretPlanetPlacement(planet, sign, house, retrograde) {
385
+ const PLANET_THEMES = {
386
+ sun: "identity, vitality, and ego expression",
387
+ moon: "emotional nature, instincts, and early conditioning",
388
+ mercury: "communication style, thinking patterns, and learning",
389
+ venus: "love language, values, aesthetics, and relationships",
390
+ mars: "drive, ambition, aggression, and desire",
391
+ jupiter: "expansion, luck, philosophy, and higher meaning",
392
+ saturn: "discipline, limitations, karmic lessons, and mastery",
393
+ uranus: "innovation, rebellion, sudden change, and originality",
394
+ neptune: "dreams, spirituality, dissolution, and transcendence",
395
+ pluto: "transformation, power, death/rebirth, and the unconscious",
396
+ };
397
+ const theme = PLANET_THEMES[planet.toLowerCase()] ?? "its archetypal themes";
398
+ const houseContext = house ? ` Its energy is channeled through the ${ordinal(house)} house, the sphere of ${HOUSE_AREAS[house] ?? "life experience"}.` : "";
399
+ const retroContext = retrograde
400
+ ? " Being retrograde, this planet's energy turns inward — its themes are processed more privately and may require more deliberate integration."
401
+ : "";
402
+ return (`${capitalize(planet)} rules ${theme}. In ${capitalize(sign)}, it expresses through ${SIGN_QUALITIES[sign.toLowerCase()] ?? "this sign's qualities"}.` +
403
+ houseContext +
404
+ retroContext);
405
+ }
406
+ function interpretAspect(planet1, planet2, type, applying) {
407
+ const ASPECT_NATURE = {
408
+ conjunction: "creates an intense fusion of these two planetary energies. Their themes blend and amplify each other, for better or worse depending on the planets involved.",
409
+ opposition: "creates a polarity that demands integration. The challenge is finding balance between these two forces rather than projecting one onto others.",
410
+ trine: "creates an easy, flowing exchange of energy between these planets. Natural talents and opportunities arise where these themes intersect.",
411
+ square: "creates productive tension that demands action. This friction is uncomfortable but drives growth and achievement when channeled constructively.",
412
+ sextile: "creates an opportunistic connection. The gifts here require some effort to activate but reward that effort generously.",
413
+ quincunx: "creates an awkward inconjunction where these energies struggle to integrate. Ongoing adjustment and flexibility are required.",
414
+ semisquare: "creates a mild but persistent irritant that, consciously worked with, builds resilience and skill.",
415
+ };
416
+ const nature = ASPECT_NATURE[type.toLowerCase()] ?? "creates a notable connection between these energies.";
417
+ const timing = applying
418
+ ? "This is an applying aspect, meaning its themes are intensifying and moving toward full expression."
419
+ : "This is a separating aspect — its peak influence has passed, and its lessons are being integrated.";
420
+ return (`The ${type} between ${capitalize(planet1)} and ${capitalize(planet2)} ${nature}\n\n${timing}`);
421
+ }
422
+ // ── Static lookup tables ──────────────────────────────────────────────────
423
+ const HOUSE_AREAS = {
424
+ 1: "self, appearance, and beginnings",
425
+ 2: "money, values, and material resources",
426
+ 3: "communication, siblings, and local travel",
427
+ 4: "home, family, roots, and psychological foundations",
428
+ 5: "creativity, romance, children, and pleasure",
429
+ 6: "work, health, daily routines, and service",
430
+ 7: "partnerships, marriage, and open enemies",
431
+ 8: "transformation, shared resources, sex, and death",
432
+ 9: "philosophy, higher education, travel, and belief",
433
+ 10: "career, public reputation, and authority",
434
+ 11: "friends, groups, hopes, and social causes",
435
+ 12: "solitude, the unconscious, karma, and hidden matters",
436
+ };
437
+ const HOUSE_MEANINGS = {
438
+ 1: "The 1st house (Ascendant) governs the self: physical appearance, first impressions, personality, and the mask worn for the world. It marks the beginning of life's journey and how we project ourselves outward.",
439
+ 2: "The 2nd house governs personal resources: money, possessions, earning capacity, and core values. It shows what the native finds security in and how they relate to material comfort.",
440
+ 3: "The 3rd house governs communication, the intellect, siblings, neighbors, and short-distance travel. It reflects how the mind processes and exchanges information.",
441
+ 4: "The 4th house (IC) governs home, family of origin, roots, and the inner emotional world. It represents foundations — both the house you grew up in and your psychological home base.",
442
+ 5: "The 5th house governs creativity, self-expression, romance, children, play, and risk-taking. It is the house of the heart's joy and authentic creative output.",
443
+ 6: "The 6th house governs daily work, health, routines, and service. It reflects the native's relationship with discipline, the body, and supporting others through practical means.",
444
+ 7: "The 7th house (Descendant) governs committed partnerships — marriage, business partners, and close one-on-one relationships. It also shows qualities we project onto others or attract through opposition.",
445
+ 8: "The 8th house governs transformation, other people's resources, inheritances, taxes, sexuality, and the cycle of death and rebirth. It is the house of deep psychological change.",
446
+ 9: "The 9th house governs philosophy, religion, higher education, long-distance travel, and the search for meaning. It expands the mind beyond the familiar.",
447
+ 10: "The 10th house (Midheaven) governs career, public reputation, authority, and life calling. It represents the highest visible achievement and how the world sees the native professionally.",
448
+ 11: "The 11th house governs friendships, groups, social networks, collective causes, and long-term hopes. It reflects where we find belonging in the wider community.",
449
+ 12: "The 12th house governs solitude, the unconscious, hidden enemies, spiritual matters, and undoing. It holds what is repressed, sacrificed, or developed through inner work and retreat.",
450
+ };
451
+ const SIGN_QUALITIES = {
452
+ aries: "directness, initiative, and pioneering courage",
453
+ taurus: "steady determination, sensory pleasure, and practical patience",
454
+ gemini: "curiosity, adaptability, and intellectual versatility",
455
+ cancer: "emotional depth, nurturing instincts, and protective sensitivity",
456
+ leo: "bold self-expression, generosity, and regal confidence",
457
+ virgo: "precision, discernment, and devoted service to improvement",
458
+ libra: "harmony-seeking, relational grace, and aesthetic refinement",
459
+ scorpio: "intense focus, emotional depth, and transformative power",
460
+ sagittarius: "philosophical expansiveness, optimism, and adventurous seeking",
461
+ capricorn: "disciplined ambition, structural mastery, and long-term commitment",
462
+ aquarius: "innovative originality, humanitarian vision, and detached idealism",
463
+ pisces: "compassionate empathy, spiritual receptivity, and imaginative flow",
464
+ };
465
+ const SIGN_HOUSE_FLAVOR = {
466
+ aries: "initiative and directness",
467
+ taurus: "patience and material stability",
468
+ gemini: "intellectual curiosity and communication",
469
+ cancer: "emotional nurturing and memory",
470
+ leo: "creative expression and leadership",
471
+ virgo: "analytical precision and service",
472
+ libra: "balance and relationship dynamics",
473
+ scorpio: "depth, intensity, and transformation",
474
+ sagittarius: "expansion and philosophical seeking",
475
+ capricorn: "discipline, structure, and achievement",
476
+ aquarius: "innovation, groups, and humanitarian ideals",
477
+ pisces: "compassion, dreams, and dissolution of boundaries",
478
+ };
package/dist/tools/dev.js CHANGED
@@ -68,7 +68,6 @@ registerTool({
68
68
  " GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
69
69
  " GET /eclipse/solar/local — Local solar eclipse (query: lat, lon)\n" +
70
70
  " GET /tidal/forcing — Gravitational tidal forcing index\n" +
71
- " GET /tidal/forcing/deep-time — Extended tidal deep-time analysis\n" +
72
71
  " POST /acg/power-lines — Astrocartography power lines (lat/lon GeoJSON)\n" +
73
72
  " POST /acg/hits — ACG power at a specific location\n" +
74
73
  " GET /calendar/astrology/moon-phases — Moon phase calendar for a date range\n" +