@openephemeris/mcp-server 3.5.2 → 3.6.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,47 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [3.6.0] — 2026-04-17
11
+
12
+ ### Added
13
+ - `ephemeris_next_lunar_phase` — new purpose-built tool for "when is the next new/full/quarter moon?" queries.
14
+ Replaces a 4–7 step multi-call chain with a single, credit-efficient call. Internally calculates a rolling
15
+ search window and filters the calendar API response so the LLM receives a clean, normalized answer.
16
+
17
+ ### Changed (Tool Descriptions & LLM Routing)
18
+ - `ephemeris_moon_phase` — sharpened description to clarify it is point-in-time only and cannot answer
19
+ "upcoming date" questions. Adds explicit `→ use ephemeris_next_lunar_phase` redirect.
20
+ - `ephemeris_next_eclipse` — latitude/longitude are now **optional**. Omitting them triggers a global eclipse
21
+ search (`/eclipse/solar/global` or `/eclipse/lunar/global`), allowing Claude to answer "when is the next
22
+ total solar eclipse?" without forcing the user to specify a location.
23
+ - `acg_power_lines` — added explicit ❌/✅ routing guidance distinguishing it from `acg_hits`. This prevents
24
+ Claude from fetching full global GeoJSON geometry when the user's question is about a specific city.
25
+ - `acg_hits` — symmetric routing guidance added (mirrors `acg_power_lines` change above).
26
+ - `ephemeris_solar_return` — `target_datetime` is now optional; defaults to `new Date().toISOString()` so
27
+ Claude can answer "What does my solar return look like?" for the current year without stalling for input.
28
+ - `ephemeris_lunar_return` — same treatment; defaults to today so "what's my next lunar return?" resolves
29
+ in one call.
30
+ - `electional_station_tracker` — `planets` parameter now accepts human-readable names (`mercury,venus,mars`)
31
+ in addition to numeric IDs. Names are mapped to IDs in the handler. Adds `USE THIS TOOL FOR:` examples
32
+ for "Mercury retrograde" queries to aid routing.
33
+ - `venus_phase` — `date` is now optional (was incorrectly marked required despite the description
34
+ saying "Defaults to now"). Handler auto-fills today's date when omitted.
35
+
36
+ ---
37
+
38
+ ## [3.5.3] — 2026-04-10
39
+
40
+ ### Fixed
41
+ - `ephemeris_chart_wheel` and `ephemeris_bi_wheel` tools now correctly advertise the
42
+ `style` values accepted by the backend (`light`, `dark`, `mono`). The previous enum
43
+ (`modern`, `classic`, `dark`) caused every non-dark chart request to fail with `400`.
44
+ - SSE sessions are now fully isolated: each connection creates its own `BackendClient`
45
+ instance keyed to the connecting user's API key. Concurrent sessions no longer
46
+ overwrite each other's API key on the shared singleton, preventing cross-user
47
+ billing attribution errors.
48
+
49
+ ---
50
+
10
51
  ## [3.5.1] — 2026-03-31
11
52
 
12
53
  ### Changed
package/README.md CHANGED
@@ -226,8 +226,8 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
226
226
 
227
227
  - Allowlisted operations: **22**
228
228
  - Methods: `GET=4`, `POST=18`, `PUT=0`, `PATCH=0`, `DELETE=0`
229
- - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **48**
230
- - Typed tools: `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_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_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`
229
+ - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **49**
230
+ - Typed tools: `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_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`
231
231
  - Generic tools:
232
232
 
233
233
  ### Allowlist Families
@@ -104,8 +104,8 @@
104
104
  "path": "/visualization/chart-wheel"
105
105
  }
106
106
  ],
107
- "last_generated_at": "2026-04-02T14:37:22.841Z",
108
- "openapi_sha256": "fd75c2b6eae40884622067c1489e61a2d35b9cce9cd57943e989210a6728a197",
107
+ "last_generated_at": "2026-04-16T23:21:31.665Z",
108
+ "openapi_sha256": "2349bd8e3e36dad609b75cc61acb1722206990581085d986b0cdad79317296aa",
109
109
  "candidates_get": [
110
110
  {
111
111
  "method": "GET",
@@ -65,3 +65,13 @@ export declare class BackendClient {
65
65
  getUserId(): string;
66
66
  }
67
67
  export declare const backendClient: BackendClient;
68
+ /**
69
+ * Run `fn` in an async context where `getActiveClient()` returns `client`.
70
+ * Used by the SSE server to scope each connection to its own BackendClient.
71
+ */
72
+ export declare function runWithClient<T>(client: BackendClient, fn: () => T): T;
73
+ /**
74
+ * Returns the per-session BackendClient when called inside `runWithClient`,
75
+ * otherwise falls back to the module-level singleton (stdio mode).
76
+ */
77
+ export declare function getActiveClient(): BackendClient;
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import axios, { AxiosError } from "axios";
2
3
  import { CredentialManager } from "../auth/credentials.js";
3
4
  import { DeviceAuthFlow } from "../auth/device-auth.js";
@@ -352,7 +353,8 @@ export class BackendClient {
352
353
  return this.userId;
353
354
  }
354
355
  }
355
- // Singleton instance
356
+ // Singleton instance — used by stdio server and as the fallback when no
357
+ // session context is active.
356
358
  export const backendClient = new BackendClient({
357
359
  baseURL: process.env.OPENEPHEMERIS_BACKEND_URL ||
358
360
  process.env.ASTROMCP_BACKEND_URL ||
@@ -363,3 +365,24 @@ export const backendClient = new BackendClient({
363
365
  serviceKey: process.env.OPENEPHEMERIS_SERVICE_KEY || process.env.ASTROMCP_SERVICE_KEY || process.env.MERIDIAN_SERVICE_KEY,
364
366
  apiKey: process.env.OPENEPHEMERIS_API_KEY || process.env.ASTROMCP_API_KEY || process.env.MERIDIAN_API_KEY,
365
367
  });
368
+ // ---------------------------------------------------------------------------
369
+ // Per-session client context (SSE isolation)
370
+ // ---------------------------------------------------------------------------
371
+ // Each SSE connection creates its own BackendClient keyed to the user's API
372
+ // key. AsyncLocalStorage propagates that client through the entire async call
373
+ // chain of a tool invocation without touching the shared singleton.
374
+ const _sessionClientStore = new AsyncLocalStorage();
375
+ /**
376
+ * Run `fn` in an async context where `getActiveClient()` returns `client`.
377
+ * Used by the SSE server to scope each connection to its own BackendClient.
378
+ */
379
+ export function runWithClient(client, fn) {
380
+ return _sessionClientStore.run(client, fn);
381
+ }
382
+ /**
383
+ * Returns the per-session BackendClient when called inside `runWithClient`,
384
+ * otherwise falls back to the module-level singleton (stdio mode).
385
+ */
386
+ export function getActiveClient() {
387
+ return _sessionClientStore.getStore() ?? backendClient;
388
+ }
@@ -21,7 +21,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
21
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
22
22
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
23
  import { initTools, toolRegistry, formatToolResponse } from "./tools/index.js";
24
- import { backendClient } from "./backend/client.js";
24
+ import { BackendClient, runWithClient } from "./backend/client.js";
25
25
  // ---------------------------------------------------------------------------
26
26
  // Helpers
27
27
  // ---------------------------------------------------------------------------
@@ -61,6 +61,7 @@ async function validateApiKey(apiKey) {
61
61
  try {
62
62
  const resp = await axios.get(`${BACKEND_URL}/ephemeris/moon/phase`, {
63
63
  headers: { "X-API-Key": apiKey },
64
+ params: { datetime: new Date().toISOString() }, // required param per OpenAPI spec
64
65
  timeout: 5_000,
65
66
  validateStatus: () => true, // don't throw on any status
66
67
  });
@@ -153,6 +154,8 @@ async function main() {
153
154
  });
154
155
  // Track active transports by session ID
155
156
  const transports = new Map();
157
+ // Per-session BackendClient — each connection authenticates independently.
158
+ const sessionClients = new Map();
156
159
  // Health check
157
160
  app.get("/health", (_req, res) => {
158
161
  res.json({ ok: true, version, transport: "sse", sessions: transports.size });
@@ -208,12 +211,16 @@ async function main() {
208
211
  });
209
212
  return;
210
213
  }
211
- // Inject the validated API key into the singleton BackendClient so all
212
- // tool calls authenticate as this user and usage is metered correctly.
213
- backendClient.setApiKey(apiKey);
214
214
  const transport = new SSEServerTransport("/message", res);
215
215
  const sessionId = transport.sessionId;
216
216
  transports.set(sessionId, transport);
217
+ // Create a per-session BackendClient so concurrent SSE connections
218
+ // don't overwrite each other's API key on the shared singleton.
219
+ const sessionClient = new BackendClient({
220
+ baseURL: BACKEND_URL,
221
+ apiKey,
222
+ });
223
+ sessionClients.set(sessionId, sessionClient);
217
224
  const server = createMcpServer();
218
225
  // Guard: prevent double-close from crashing the process
219
226
  let closed = false;
@@ -222,6 +229,7 @@ async function main() {
222
229
  return;
223
230
  closed = true;
224
231
  transports.delete(sessionId);
232
+ sessionClients.delete(sessionId);
225
233
  try {
226
234
  server.close().catch(() => { });
227
235
  }
@@ -243,6 +251,7 @@ async function main() {
243
251
  app.post("/message", async (req, res) => {
244
252
  const sessionId = req.query.sessionId;
245
253
  const transport = transports.get(sessionId);
254
+ const sessionClient = sessionClients.get(sessionId);
246
255
  if (!transport) {
247
256
  res.status(400).json({
248
257
  error: "invalid_session",
@@ -250,7 +259,14 @@ async function main() {
250
259
  });
251
260
  return;
252
261
  }
253
- await transport.handlePostMessage(req, res);
262
+ // Run handlePostMessage inside the session's client context so all tool
263
+ // handlers reach getActiveClient() and get this user's BackendClient.
264
+ if (sessionClient) {
265
+ await runWithClient(sessionClient, () => transport.handlePostMessage(req, res));
266
+ }
267
+ else {
268
+ await transport.handlePostMessage(req, res);
269
+ }
254
270
  });
255
271
  app.listen(PORT, "0.0.0.0", () => {
256
272
  console.log(`OpenEphemeris MCP SSE server v${version} listening on port ${PORT}`);
package/dist/tools/dev.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "./index.js";
2
- import { backendClient } from "../backend/client.js";
2
+ import { getActiveClient } from "../backend/client.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -171,7 +171,7 @@ registerTool({
171
171
  if (isVisualizationPath && !query.format) {
172
172
  query.format = "png";
173
173
  }
174
- return await backendClient.request(method, pathname, {
174
+ return await getActiveClient().request(method, pathname, {
175
175
  params: query,
176
176
  data: body,
177
177
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  const ACG_BODY_DESCRIPTION = "List of celestial bodies for line calculation. " +
4
4
  "E.g. ['Sun', 'Moon', 'Venus', 'Mars', 'Jupiter', 'Saturn']. " +
5
5
  "Aliases: 'NorthNode'/'Node'/'Rahu' → MeanNode, 'SouthNode'/'Ketu' → SouthNode. " +
@@ -11,6 +11,9 @@ registerTool({
11
11
  "Returns GeoJSON LineStrings tracing each planetary angle line around the globe. " +
12
12
  "These are the latitudinal lines where a planet was rising (AC), setting (DC), " +
13
13
  "culminating (MC), or anti-culminating (IC) at birth.\n\n" +
14
+ "❌ NOT FOR: 'Is [city] good for me?' or 'What planets affect me in Tokyo?' " +
15
+ "→ For a specific city/location analysis, use acg_hits instead (faster and more relevant).\n" +
16
+ "✅ USE FOR: Getting the full global GeoJSON line geometry for map rendering or bulk geographic analysis.\n\n" +
14
17
  "CREDIT COST: 10 credits per call.\n\n" +
15
18
  "EXAMPLE: Saturn and Jupiter power lines for a chart born 1990-04-15 in Chicago:\n" +
16
19
  " birth_datetime='1990-04-15T14:30:00', birth_latitude=41.8781, birth_longitude=-87.6298,\n" +
@@ -68,7 +71,7 @@ registerTool({
68
71
  body.query_lon = args.query_longitude;
69
72
  if (args.radius_deg != null)
70
73
  body.radius_deg = args.radius_deg;
71
- return await backendClient.post("/acg/power-lines", body);
74
+ return await getActiveClient().post("/acg/power-lines", body);
72
75
  },
73
76
  });
74
77
  // POST /acg/hits — OE-018
@@ -76,6 +79,9 @@ registerTool({
76
79
  name: "acg_hits",
77
80
  description: "Find all Astrocartography lines (power lines + aspect lines) passing near a specific location. " +
78
81
  "Returns features sorted by distance, making it easy to interpret planetary influences at a place.\n\n" +
82
+ "✅ USE THIS TOOL FOR: 'Is [city] good for me?', 'What planets affect me in Tokyo?', " +
83
+ "'What ACG lines run through Paris for my chart?', 'Which cities are under my Jupiter line?'\n" +
84
+ "❌ NOT FOR: Full global map geometry → use acg_power_lines for that instead.\n\n" +
79
85
  "CREDIT COST: 15 credits per call.\n\n" +
80
86
  "EXAMPLE: All ACG lines within 3° of Paris for a chart born 1990-04-15 in Chicago:\n" +
81
87
  " birth_datetime='1990-04-15T14:30:00', birth_latitude=41.8781, birth_longitude=-87.6298,\n" +
@@ -138,6 +144,6 @@ registerTool({
138
144
  if (args.include_aspects != null) {
139
145
  body.options = { include_aspects: args.include_aspects };
140
146
  }
141
- return await backendClient.post("/acg/hits", body);
147
+ return await getActiveClient().post("/acg/hits", body);
142
148
  },
143
149
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  registerTool({
4
4
  name: "chinese_bazi",
5
5
  description: "Calculate a Chinese Ba Zi (Four Pillars of Destiny) chart. Returns the Year, Month, Day, " +
@@ -62,6 +62,6 @@ registerTool({
62
62
  const body = { year, month, day };
63
63
  if (hour != null)
64
64
  body.hour = hour;
65
- return await backendClient.request("POST", "/chinese/bazi", { data: body });
65
+ return await getActiveClient().request("POST", "/chinese/bazi", { data: body });
66
66
  },
67
67
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  const DATETIME_DESC = "ISO 8601 datetime string, e.g. '1990-04-15T14:30:00' (local time at birth location). " +
4
4
  "Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.";
5
5
  const HOUSE_SYSTEM_MAP = {
@@ -31,8 +31,8 @@ registerTool({
31
31
  },
32
32
  style: {
33
33
  type: "string",
34
- enum: ["modern", "classic", "dark"],
35
- description: "Aesthetic style of the chart. Defaults to 'modern'.",
34
+ enum: ["light", "dark", "mono"],
35
+ description: "Aesthetic style of the chart: 'light' (default, warm cream background), 'dark' (deep navy), or 'mono' (black-and-white print-ready).",
36
36
  }
37
37
  },
38
38
  required: ["datetime_a", "latitude_a", "longitude_a", "datetime_b", "latitude_b", "longitude_b"],
@@ -69,6 +69,6 @@ registerTool({
69
69
  body.configuration = { house_system: mappedCode };
70
70
  }
71
71
  const styleParam = args.style ? `&style=${args.style}` : "";
72
- return await backendClient.post(`/visualization/bi-wheel?format=png&size=800${styleParam}`, body);
72
+ return await getActiveClient().post(`/visualization/bi-wheel?format=png&size=800${styleParam}`, body);
73
73
  },
74
74
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  const DATETIME_DESC = "ISO 8601 datetime string, e.g. '1990-04-15T14:30:00' (local time at birth location). " +
4
4
  "Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.";
5
5
  const HOUSE_SYSTEM_MAP = {
@@ -39,8 +39,8 @@ registerTool({
39
39
  },
40
40
  style: {
41
41
  type: "string",
42
- enum: ["modern", "classic", "dark"],
43
- description: "Aesthetic style of the chart. Defaults to 'modern'.",
42
+ enum: ["light", "dark", "mono"],
43
+ description: "Aesthetic style of the chart: 'light' (default, warm cream background), 'dark' (deep navy), or 'mono' (black-and-white print-ready).",
44
44
  }
45
45
  },
46
46
  required: ["datetime", "latitude", "longitude"],
@@ -66,6 +66,6 @@ registerTool({
66
66
  body.configuration = { house_system: mappedCode };
67
67
  }
68
68
  const styleParam = args.style ? `&style=${args.style}` : "";
69
- return await backendClient.post(`/visualization/chart-wheel?format=png&size=800${styleParam}`, body);
69
+ return await getActiveClient().post(`/visualization/chart-wheel?format=png&size=800${styleParam}`, body);
70
70
  },
71
71
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  function buildSubject(name, datetime, lat, lon, timezone) {
4
4
  const subj = {
5
5
  name,
@@ -50,7 +50,7 @@ registerTool({
50
50
  const query = {};
51
51
  if (args.format)
52
52
  query.format = args.format;
53
- return await backendClient.request("POST", "/comparative/composite", {
53
+ return await getActiveClient().request("POST", "/comparative/composite", {
54
54
  params: query,
55
55
  data: {
56
56
  subjects: [
@@ -94,7 +94,7 @@ registerTool({
94
94
  const query = {};
95
95
  if (args.format)
96
96
  query.format = args.format;
97
- return await backendClient.request("POST", "/comparative/composite/midpoint", {
97
+ return await getActiveClient().request("POST", "/comparative/composite/midpoint", {
98
98
  params: query,
99
99
  data: {
100
100
  subjects: [
@@ -138,7 +138,7 @@ registerTool({
138
138
  const query = {};
139
139
  if (args.format)
140
140
  query.format = args.format;
141
- return await backendClient.request("POST", "/comparative/overlay", {
141
+ return await getActiveClient().request("POST", "/comparative/overlay", {
142
142
  params: query,
143
143
  data: {
144
144
  subjects: [
@@ -182,7 +182,7 @@ registerTool({
182
182
  const query = {};
183
183
  if (args.format)
184
184
  query.format = args.format;
185
- return await backendClient.request("POST", "/comparative/natal-transits", {
185
+ return await getActiveClient().request("POST", "/comparative/natal-transits", {
186
186
  params: query,
187
187
  data: { subjects }
188
188
  });
@@ -1,14 +1,20 @@
1
1
  import { registerTool, validateRequired, validateCoordinates } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  registerTool({
4
4
  name: "ephemeris_next_eclipse",
5
- description: "Find the next solar or lunar eclipse visible from a given location. " +
6
- "Returns the eclipse type, date/time of maximum, magnitude, duration of totality (if any), " +
7
- "and local contact times.\n\n" +
5
+ description: "Find the next solar or lunar eclipse. " +
6
+ "Returns the eclipse type, date/time of maximum, magnitude, and duration of totality (if any).\n\n" +
7
+ "📍 LOCATION OPTIONAL:\n" +
8
+ " • WITH latitude+longitude → returns local contact times and visibility for that specific location.\n" +
9
+ " • WITHOUT latitude+longitude → returns the next global eclipse of that type (no location needed).\n\n" +
10
+ "USE THIS TOOL FOR: 'When is the next solar eclipse?', 'When is the next total lunar eclipse?', " +
11
+ "'Will there be an eclipse visible from Tokyo?'\n\n" +
8
12
  "CREDIT COST: 1 credit per call.\n\n" +
9
- "EXAMPLE: Find the next solar eclipse visible from New York:\n" +
13
+ "EXAMPLE: Next solar eclipse globally (no location needed):\n" +
14
+ " eclipse_type='solar'\n\n" +
15
+ "EXAMPLE: Next solar eclipse visible from New York:\n" +
10
16
  " eclipse_type='solar', latitude=40.7128, longitude=-74.006\n\n" +
11
- "EXAMPLE: Find the next lunar eclipse from London:\n" +
17
+ "EXAMPLE: Next lunar eclipse from London:\n" +
12
18
  " eclipse_type='lunar', latitude=51.5074, longitude=-0.1278",
13
19
  inputSchema: {
14
20
  type: "object",
@@ -16,35 +22,47 @@ registerTool({
16
22
  eclipse_type: {
17
23
  type: "string",
18
24
  enum: ["solar", "lunar", "any"],
19
- description: "Eclipse type to search for. Use 'any' for whichever comes first.",
25
+ description: "Eclipse type to search for. Use 'any' for whichever comes first (solar or lunar).",
20
26
  },
21
27
  latitude: {
22
28
  type: "number",
23
- description: "Observer latitude in decimal degrees (required). Returns local visibility and contact times.",
29
+ description: "Observer latitude in decimal degrees (optional). When provided with longitude, returns local contact times and visibility.",
24
30
  },
25
31
  longitude: {
26
32
  type: "number",
27
- description: "Observer longitude in decimal degrees (required).",
33
+ description: "Observer longitude in decimal degrees (optional, pair with latitude).",
28
34
  },
29
35
  after_date: {
30
36
  type: "string",
31
37
  description: "ISO 8601 date to search after (e.g. '2026-01-01'). Defaults to today if omitted.",
32
38
  },
33
39
  },
34
- required: ["eclipse_type", "latitude", "longitude"],
40
+ required: ["eclipse_type"],
35
41
  additionalProperties: false,
36
42
  },
37
43
  handler: async (args) => {
38
- validateRequired(args, ["eclipse_type", "latitude", "longitude"]);
44
+ validateRequired(args, ["eclipse_type"]);
39
45
  validateCoordinates(args, "latitude", "longitude");
40
- const params = {
41
- lat: args.latitude,
42
- lon: args.longitude,
43
- };
44
- if (args.after_date)
45
- params.date = args.after_date;
46
- if (args.eclipse_type && args.eclipse_type !== "any")
47
- params.type = args.eclipse_type;
48
- return await backendClient.request("GET", "/eclipse/next-visible", { params, timeoutMs: 60_000 });
46
+ const hasLocation = args.latitude != null && args.longitude != null;
47
+ if (hasLocation) {
48
+ // Local visibility query
49
+ const params = {
50
+ lat: args.latitude,
51
+ lon: args.longitude,
52
+ };
53
+ if (args.after_date)
54
+ params.date = args.after_date;
55
+ if (args.eclipse_type && args.eclipse_type !== "any")
56
+ params.type = args.eclipse_type;
57
+ return await getActiveClient().request("GET", "/eclipse/next-visible", { params, timeoutMs: 60_000 });
58
+ }
59
+ else {
60
+ // Global query — route to global endpoint
61
+ const params = {};
62
+ if (args.after_date)
63
+ params.date = args.after_date;
64
+ const endpoint = args.eclipse_type === "lunar" ? "/eclipse/lunar/global" : "/eclipse/solar/global";
65
+ return await getActiveClient().request("GET", endpoint, { params, timeoutMs: 60_000 });
66
+ }
49
67
  },
50
68
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  // All electional endpoints are GET endpoints with query params.
4
4
  // GET /electional/find-window — already existed, keeping it
5
5
  registerTool({
@@ -74,7 +74,7 @@ registerTool({
74
74
  query.avoid_voc = args.avoid_voc;
75
75
  if (args.format)
76
76
  query.format = args.format;
77
- return await backendClient.request("GET", "/electional/find-window", {
77
+ return await getActiveClient().request("GET", "/electional/find-window", {
78
78
  data: {},
79
79
  params: query,
80
80
  });
@@ -111,27 +111,30 @@ registerTool({
111
111
  query.date = args.date;
112
112
  if (args.format)
113
113
  query.format = args.format;
114
- return await backendClient.request("GET", "/electional/moment-analysis", {
114
+ return await getActiveClient().request("GET", "/electional/moment-analysis", {
115
115
  data: {},
116
116
  params: query,
117
117
  });
118
118
  },
119
119
  });
120
- // GET /electional/station-tracker
121
120
  registerTool({
122
121
  name: "electional_station_tracker",
123
122
  description: "Find all upcoming retrograde and direct stations for planets in a date range. " +
124
- "Returns exact station times, longitudes, and signs. Essential for timing around " +
125
- "Mercury retrograde, Venus retrograde, etc.\n\n" +
123
+ "Returns exact station times, longitudes, and signs.\n\n" +
124
+ "USE THIS TOOL FOR: 'When does Mercury go retrograde?', 'Is Venus retrograde this year?', " +
125
+ "'What planets station this month?', 'When does Mars go direct?'\n\n" +
126
+ "All required fields have smart defaults (searches the next 90 days from today).\n\n" +
126
127
  "CREDIT COST: 3 credits per call.\n\n" +
127
- "EXAMPLE: Stations in the next 3 months:\n" +
128
- " start_date='2026-03-01', end_date='2026-06-01'",
128
+ "EXAMPLE: Mercury and Venus stations in the next 3 months (all defaults):\n" +
129
+ " (no args required, will auto-scan next 90 days for all inner planets)\n\n" +
130
+ "EXAMPLE: Outer planet stations in 2026:\n" +
131
+ " start_date='2026-01-01', end_date='2026-12-31', planets='jupiter,saturn,uranus,neptune,pluto'",
129
132
  inputSchema: {
130
133
  type: "object",
131
134
  properties: {
132
135
  start_date: {
133
136
  type: "string",
134
- description: "Start date (ISO 8601). Defaults to now.",
137
+ description: "Start date (ISO 8601). Defaults to today.",
135
138
  },
136
139
  end_date: {
137
140
  type: "string",
@@ -139,7 +142,9 @@ registerTool({
139
142
  },
140
143
  planets: {
141
144
  type: "string",
142
- description: "Comma-separated planet IDs (e.g., '2,3,4'). Defaults to Mercury-Saturn.",
145
+ description: "Comma-separated planet names to track. " +
146
+ "E.g. 'mercury,venus,mars' or 'jupiter,saturn,uranus,neptune,pluto'. " +
147
+ "Defaults to Mercury through Saturn (inner + classical planets).",
143
148
  },
144
149
  format: {
145
150
  type: "string",
@@ -152,15 +157,29 @@ registerTool({
152
157
  },
153
158
  handler: async (args) => {
154
159
  const query = {};
160
+ // Map human-readable planet names to IDs if needed
161
+ const PLANET_NAME_MAP = {
162
+ mercury: "2", venus: "3", mars: "4",
163
+ jupiter: "5", saturn: "6", uranus: "7",
164
+ neptune: "8", pluto: "9",
165
+ };
155
166
  if (args.start_date)
156
167
  query.start_date = args.start_date;
157
168
  if (args.end_date)
158
169
  query.end_date = args.end_date;
159
- if (args.planets)
160
- query.planets = args.planets;
170
+ if (args.planets) {
171
+ // Accept both names ('mercury,venus') and numeric IDs ('2,3')
172
+ query.planets = args.planets
173
+ .split(",")
174
+ .map((p) => {
175
+ const name = p.trim().toLowerCase();
176
+ return PLANET_NAME_MAP[name] ?? p.trim();
177
+ })
178
+ .join(",");
179
+ }
161
180
  if (args.format)
162
181
  query.format = args.format;
163
- return await backendClient.request("GET", "/electional/station-tracker", {
182
+ return await getActiveClient().request("GET", "/electional/station-tracker", {
164
183
  data: {},
165
184
  params: query,
166
185
  });
@@ -202,7 +221,7 @@ registerTool({
202
221
  query.max_orb = args.max_orb;
203
222
  if (args.aspects)
204
223
  query.aspects = args.aspects;
205
- return await backendClient.request("GET", "/electional/aspect-search", {
224
+ return await getActiveClient().request("GET", "/electional/aspect-search", {
206
225
  data: {},
207
226
  params: query,
208
227
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  // POST /ephemeris/planet-position — OE-016
4
4
  registerTool({
5
5
  name: "ephemeris_planet_position",
@@ -44,7 +44,7 @@ registerTool({
44
44
  body.latitude = args.latitude;
45
45
  if (args.longitude != null)
46
46
  body.longitude = args.longitude;
47
- return await backendClient.post("/ephemeris/planet-position", body);
47
+ return await getActiveClient().post("/ephemeris/planet-position", body);
48
48
  },
49
49
  });
50
50
  // POST /ephemeris/house-cusps — OE-017
@@ -89,6 +89,6 @@ registerTool({
89
89
  };
90
90
  if (args.house_systems)
91
91
  body.house_systems = args.house_systems;
92
- return await backendClient.post("/ephemeris/house-cusps", body);
92
+ return await getActiveClient().post("/ephemeris/house-cusps", body);
93
93
  },
94
94
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  function buildSubject(name, datetime, lat, lon) {
4
4
  return {
5
5
  name,
@@ -47,7 +47,7 @@ registerTool({
47
47
  const items = args.subjects.map((s) => ({
48
48
  subject: buildSubject(s.name, s.datetime, s.latitude, s.longitude),
49
49
  }));
50
- return await backendClient.post("/ephemeris/natal/batch", { items });
50
+ return await getActiveClient().post("/ephemeris/natal/batch", { items });
51
51
  },
52
52
  });
53
53
  // POST /ephemeris/dignities
@@ -66,7 +66,7 @@ registerTool({
66
66
  },
67
67
  handler: async (args) => {
68
68
  validateRequired(args, ["datetime"]);
69
- return await backendClient.post("/ephemeris/dignities", {
69
+ return await getActiveClient().post("/ephemeris/dignities", {
70
70
  date_time: { iso: args.datetime },
71
71
  });
72
72
  },
@@ -93,7 +93,7 @@ registerTool({
93
93
  };
94
94
  if (args.planet_id != null)
95
95
  body.planet_id = args.planet_id;
96
- return await backendClient.post("/ephemeris/retrograde-status", body);
96
+ return await getActiveClient().post("/ephemeris/retrograde-status", body);
97
97
  },
98
98
  });
99
99
  // POST /ephemeris/midpoints
@@ -114,7 +114,7 @@ registerTool({
114
114
  },
115
115
  handler: async (args) => {
116
116
  validateRequired(args, ["datetime", "latitude", "longitude"]);
117
- return await backendClient.post("/ephemeris/midpoints", {
117
+ return await getActiveClient().post("/ephemeris/midpoints", {
118
118
  date_time: { iso: args.datetime },
119
119
  latitude: args.latitude,
120
120
  longitude: args.longitude,
@@ -150,7 +150,7 @@ registerTool({
150
150
  body.star_names = args.star_names;
151
151
  if (args.orb != null)
152
152
  body.orb = args.orb;
153
- return await backendClient.post("/ephemeris/fixed-stars", body);
153
+ return await getActiveClient().post("/ephemeris/fixed-stars", body);
154
154
  },
155
155
  });
156
156
  // POST /ephemeris/hermetic-lots
@@ -171,7 +171,7 @@ registerTool({
171
171
  },
172
172
  handler: async (args) => {
173
173
  validateRequired(args, ["datetime", "latitude", "longitude"]);
174
- return await backendClient.post("/ephemeris/hermetic-lots", {
174
+ return await getActiveClient().post("/ephemeris/hermetic-lots", {
175
175
  date_time: { iso: args.datetime },
176
176
  latitude: args.latitude,
177
177
  longitude: args.longitude,
@@ -196,7 +196,7 @@ registerTool({
196
196
  },
197
197
  handler: async (args) => {
198
198
  validateRequired(args, ["datetime", "latitude", "longitude"]);
199
- return await backendClient.post("/ephemeris/angles-points", {
199
+ return await getActiveClient().post("/ephemeris/angles-points", {
200
200
  date_time: { iso: args.datetime },
201
201
  latitude: args.latitude,
202
202
  longitude: args.longitude,
@@ -229,6 +229,6 @@ registerTool({
229
229
  };
230
230
  if (args.max_orb != null)
231
231
  body.max_orb = args.max_orb;
232
- return await backendClient.post("/ephemeris/aspect-check", body);
232
+ return await getActiveClient().post("/ephemeris/aspect-check", body);
233
233
  },
234
234
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  /**
4
4
  * Ensure a datetime string has a timezone offset for Go's time.Time parsing.
5
5
  */
@@ -51,7 +51,7 @@ registerTool({
51
51
  },
52
52
  handler: async (args) => {
53
53
  validateRequired(args, ["planet", "datetime", "return_year"]);
54
- return await backendClient.request("POST", "/human-design/cycles/return", {
54
+ return await getActiveClient().request("POST", "/human-design/cycles/return", {
55
55
  data: {
56
56
  planet: args.planet,
57
57
  birth_datetime_utc: ensureTimezone(args.datetime),
@@ -101,7 +101,7 @@ registerTool({
101
101
  },
102
102
  handler: async (args) => {
103
103
  validateRequired(args, ["planet", "datetime", "target_year"]);
104
- return await backendClient.request("POST", "/human-design/cycles/opposition", {
104
+ return await getActiveClient().request("POST", "/human-design/cycles/opposition", {
105
105
  data: {
106
106
  planet: args.planet,
107
107
  birth_datetime_utc: ensureTimezone(args.datetime),
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  // POST /human-design/composite — OE-027
4
4
  registerTool({
5
5
  name: "human_design_composite",
@@ -40,7 +40,7 @@ registerTool({
40
40
  };
41
41
  if (args.format)
42
42
  body.format = args.format;
43
- return await backendClient.post("/human-design/composite", body);
43
+ return await getActiveClient().post("/human-design/composite", body);
44
44
  },
45
45
  });
46
46
  // POST /human-design/penta — OE-027
@@ -102,6 +102,6 @@ registerTool({
102
102
  };
103
103
  if (args.format)
104
104
  body.format = args.format;
105
- return await backendClient.post("/human-design/penta", body);
105
+ return await getActiveClient().post("/human-design/penta", body);
106
106
  },
107
107
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  /**
4
4
  * Ensure a datetime string has a timezone offset for Go's time.Time parsing.
5
5
  * Go's encoding/json only accepts RFC 3339 (must have Z or +HH:MM offset).
@@ -61,7 +61,7 @@ registerTool({
61
61
  const query = {};
62
62
  if (args.format)
63
63
  query.format = args.format;
64
- return await backendClient.request("POST", "/human-design/chart", {
64
+ return await getActiveClient().request("POST", "/human-design/chart", {
65
65
  data: body,
66
66
  params: query,
67
67
  });
@@ -1,10 +1,13 @@
1
1
  import { registerTool, validateCoordinates } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  registerTool({
4
4
  name: "ephemeris_moon_phase",
5
- description: "Get the current Moon phase and void-of-course status. Returns the Moon's sign, phase name " +
6
- "(New, Waxing Crescent, First Quarter, Waxing Gibbous, Full, Waning Gibbous, Last Quarter, " +
7
- "Waning Crescent), exact phase angle, illumination percentage, and the next void-of-course period.\n\n" +
5
+ description: "Get the Moon's current phase angle, illumination, and void-of-course status AT a specific " +
6
+ "point in time. Returns phase name (New, Waxing Crescent, First Quarter, etc.), exact angle, " +
7
+ "illumination %, and next void-of-course period.\n\n" +
8
+ "⚠️ THIS TOOL ANSWERS: 'What phase is the moon in right now (or at a given datetime)?'\n" +
9
+ "❌ THIS TOOL DOES NOT ANSWER: 'When is the next new moon / full moon?'\n" +
10
+ "→ For upcoming phase DATES use ephemeris_next_lunar_phase instead.\n\n" +
8
11
  "CREDIT COST: 1 credit per call.\n\n" +
9
12
  "If no datetime is provided, returns the current (live) moon phase.\n\n" +
10
13
  "EXAMPLE: Get moon phase for a specific date/time:\n" +
@@ -40,8 +43,8 @@ registerTool({
40
43
  if (args.longitude != null)
41
44
  params.longitude = args.longitude;
42
45
  const [phase, voc] = await Promise.allSettled([
43
- backendClient.request("GET", "/ephemeris/moon/phase", { params }),
44
- backendClient.request("GET", "/ephemeris/moon/void-of-course", { params }),
46
+ getActiveClient().request("GET", "/ephemeris/moon/phase", { params }),
47
+ getActiveClient().request("GET", "/ephemeris/moon/void-of-course", { params }),
45
48
  ]);
46
49
  return {
47
50
  phase: phase.status === "fulfilled" ? phase.value : { error: phase.reason?.message },
@@ -49,3 +52,89 @@ registerTool({
49
52
  };
50
53
  },
51
54
  });
55
+ registerTool({
56
+ name: "ephemeris_next_lunar_phase",
57
+ description: "Find the next occurrence of a specific Moon phase after a given date. " +
58
+ "Returns the exact UTC datetime, zodiac sign, and degree.\n\n" +
59
+ "✅ USE THIS TOOL FOR: 'When is the next new moon?', 'When is the next full moon?', " +
60
+ "'What date is the next quarter moon?', or any question about UPCOMING phase dates.\n\n" +
61
+ "CREDIT COST: 1 credit per call.\n\n" +
62
+ "EXAMPLE: Find the next new moon:\n" +
63
+ " phase='new_moon'\n\n" +
64
+ "EXAMPLE: Find the next full moon after a specific date:\n" +
65
+ " phase='full_moon', after_date='2026-06-01'\n\n" +
66
+ "EXAMPLE: Find the next 3 full moons:\n" +
67
+ " phase='full_moon', count=3",
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {
71
+ phase: {
72
+ type: "string",
73
+ enum: ["new_moon", "full_moon", "first_quarter", "last_quarter"],
74
+ description: "The lunar phase to find. " +
75
+ "'new_moon' = conjunction (0° Sun-Moon, start of cycle). " +
76
+ "'full_moon' = opposition (180°). " +
77
+ "'first_quarter' = 90° waxing. " +
78
+ "'last_quarter' = 270° waning.",
79
+ },
80
+ after_date: {
81
+ type: "string",
82
+ description: "ISO 8601 date to search from (e.g. '2026-06-01'). Defaults to today (UTC) if omitted.",
83
+ },
84
+ count: {
85
+ type: "integer",
86
+ description: "Number of upcoming occurrences to return (1-12). Default 1.",
87
+ },
88
+ },
89
+ required: ["phase"],
90
+ additionalProperties: false,
91
+ },
92
+ handler: async (args) => {
93
+ // Map our clean enum to display names the calendar API returns
94
+ const phaseNameMap = {
95
+ new_moon: "New Moon",
96
+ full_moon: "Full Moon",
97
+ first_quarter: "First Quarter",
98
+ last_quarter: "Last Quarter",
99
+ };
100
+ const phaseLabel = phaseNameMap[args.phase] ?? args.phase;
101
+ const count = Math.max(1, Math.min(12, args.count ?? 1));
102
+ // Build a search window wide enough to capture `count` occurrences.
103
+ // Lunar cycle is ~29.5 days, so count * 35 days guarantees coverage.
104
+ const startDate = args.after_date
105
+ ? args.after_date.substring(0, 10)
106
+ : new Date().toISOString().substring(0, 10);
107
+ const endMs = new Date(startDate + "T00:00:00Z").getTime() + count * 35 * 24 * 60 * 60 * 1000;
108
+ const endDate = new Date(endMs).toISOString().substring(0, 10);
109
+ const result = await getActiveClient().request("GET", "/calendar/astrology/moon-phases", {
110
+ params: { start_date: startDate, end_date: endDate },
111
+ });
112
+ // Normalise response shape — API may return array, or { phases: [...] }, or { data: [...] }
113
+ const allPhases = Array.isArray(result)
114
+ ? result
115
+ : result?.phases ?? result?.data ?? result?.events ?? [];
116
+ const matching = allPhases
117
+ .filter((p) => {
118
+ const name = (p.phase_name ?? p.name ?? p.phase ?? "").toLowerCase();
119
+ return (name === phaseLabel.toLowerCase() ||
120
+ name === args.phase.replace("_", " ").toLowerCase());
121
+ })
122
+ .slice(0, count);
123
+ if (matching.length === 0) {
124
+ // Return full calendar as fallback so the LLM can inspect it
125
+ return {
126
+ requested_phase: phaseLabel,
127
+ search_window: { start_date: startDate, end_date: endDate },
128
+ result_count: 0,
129
+ note: "No matching phase found in window. Full calendar returned for inspection.",
130
+ full_calendar: allPhases,
131
+ };
132
+ }
133
+ return {
134
+ requested_phase: phaseLabel,
135
+ after_date: startDate,
136
+ results: matching,
137
+ result_count: matching.length,
138
+ };
139
+ },
140
+ });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  const DATETIME_DESC = "ISO 8601 datetime string, e.g. '1990-04-15T14:30:00' (local time at birth location). " +
4
4
  "Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.";
5
5
  /** Map human-readable house system names to Swiss Ephemeris single-letter codes */
@@ -91,7 +91,7 @@ registerTool({
91
91
  const query = {};
92
92
  if (args.format)
93
93
  query.format = args.format;
94
- return await backendClient.request("POST", "/ephemeris/natal-chart", {
94
+ return await getActiveClient().request("POST", "/ephemeris/natal-chart", {
95
95
  data: body,
96
96
  params: query,
97
97
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  registerTool({
4
4
  name: "ephemeris_progressed_chart",
5
5
  description: "Calculate a Secondary Progressed (or Solar Arc / Tertiary) chart. " +
@@ -60,6 +60,6 @@ registerTool({
60
60
  if (args.include_aspects != null) {
61
61
  body.options = { include_aspects: args.include_aspects };
62
62
  }
63
- return await backendClient.post("/ephemeris/progressed", body);
63
+ return await getActiveClient().post("/ephemeris/progressed", body);
64
64
  },
65
65
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  registerTool({
4
4
  name: "ephemeris_relocation",
5
5
  description: "Calculate a relocation chart — the same natal planetary positions re-cast for a different " +
@@ -67,7 +67,7 @@ registerTool({
67
67
  const query = {};
68
68
  if (args.format)
69
69
  query.format = args.format;
70
- return await backendClient.request("POST", "/ephemeris/relocation", {
70
+ return await getActiveClient().request("POST", "/ephemeris/relocation", {
71
71
  data: body,
72
72
  params: query,
73
73
  });
@@ -1,23 +1,28 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  // POST /predictive/returns/solar
4
4
  registerTool({
5
5
  name: "ephemeris_solar_return",
6
- description: "Calculate the exact Solar Return date/time — when the Sun returns to its natal ecliptic longitude. " +
7
- "Used to cast the Solar Return chart for the year ahead.\n\n" +
6
+ description: "Calculate the exact Solar Return date/time — when the Sun returns to its natal ecliptic longitude " +
7
+ "(happens once per year near the birthday). Returns the Solar Return chart for the year ahead.\n\n" +
8
8
  "CREDIT COST: 5 credits per call.\n\n" +
9
- "EXAMPLE: Solar return for someone born 1985-06-21 near their 2026 birthday:\n" +
10
- " birth_datetime='1985-06-21T14:00:00', target_datetime='2026-06-01'",
9
+ "TARGET DATE: Provide target_datetime near the desired birthday year. " +
10
+ "If omitted, defaults to the current year's solar return.\n\n" +
11
+ "EXAMPLE: Solar return for someone born 1985-06-21 (current year):\n" +
12
+ " birth_datetime='1985-06-21T14:00:00Z'\n\n" +
13
+ "EXAMPLE: Solar return for 2027 specifically:\n" +
14
+ " birth_datetime='1985-06-21T14:00:00Z', target_datetime='2027-01-01T00:00:00Z'",
11
15
  inputSchema: {
12
16
  type: "object",
13
17
  properties: {
14
18
  birth_datetime: {
15
19
  type: "string",
16
- description: "ISO 8601 birth date/time. MUST include offset or Z (e.g. '1985-06-21T14:00:00-05:00').",
20
+ description: "ISO 8601 birth date/time. Include offset or Z (e.g. '1985-06-21T14:00:00-05:00').",
17
21
  },
18
22
  target_datetime: {
19
23
  type: "string",
20
- description: "Date/time near which to find the Solar Return (ISO 8601). MUST include offset or Z. Required.",
24
+ description: "Date/time near which to find the Solar Return (ISO 8601 with offset or Z). " +
25
+ "Defaults to approximately 1 year from now if omitted.",
21
26
  },
22
27
  birth_latitude: {
23
28
  type: "number",
@@ -29,21 +34,23 @@ registerTool({
29
34
  },
30
35
  return_latitude: {
31
36
  type: "number",
32
- description: "Location latitude for the return chart (optional).",
37
+ description: "Location latitude for the return chart (optional, if different from birth location).",
33
38
  },
34
39
  return_longitude: {
35
40
  type: "number",
36
41
  description: "Location longitude for the return chart (optional).",
37
42
  },
38
43
  },
39
- required: ["birth_datetime", "target_datetime"],
44
+ required: ["birth_datetime"],
40
45
  additionalProperties: false,
41
46
  },
42
47
  handler: async (args) => {
43
- validateRequired(args, ["birth_datetime", "target_datetime"]);
48
+ validateRequired(args, ["birth_datetime"]);
49
+ // Default target to current year if not provided
50
+ const targetDt = args.target_datetime ?? new Date().toISOString();
44
51
  const body = {
45
52
  birth_datetime: { iso: args.birth_datetime },
46
- target_datetime: { iso: args.target_datetime },
53
+ target_datetime: { iso: targetDt },
47
54
  };
48
55
  if (args.birth_latitude != null && args.birth_longitude != null) {
49
56
  body.location = {
@@ -57,7 +64,7 @@ registerTool({
57
64
  longitude: { decimal: args.return_longitude },
58
65
  };
59
66
  }
60
- return await backendClient.post("/predictive/returns/solar", body);
67
+ return await getActiveClient().post("/predictive/returns/solar", body);
61
68
  },
62
69
  });
63
70
  // POST /predictive/returns/lunar
@@ -66,18 +73,23 @@ registerTool({
66
73
  description: "Calculate the exact Lunar Return date/time — when the Moon returns to its natal ecliptic longitude (~every 27.3 days). " +
67
74
  "Used to cast the monthly Lunar Return chart.\n\n" +
68
75
  "CREDIT COST: 5 credits per call.\n\n" +
69
- "EXAMPLE: Next lunar return after 2026-03-15 for someone born 1990-04-15:\n" +
70
- " birth_datetime='1990-04-15T14:30:00', target_datetime='2026-03-15'",
76
+ "TARGET DATE: Provide target_datetime near the month you want. " +
77
+ "If omitted, defaults to the next upcoming lunar return from today.\n\n" +
78
+ "EXAMPLE: Next lunar return after today for someone born 1990-04-15:\n" +
79
+ " birth_datetime='1990-04-15T14:30:00Z'\n\n" +
80
+ "EXAMPLE: Lunar return nearest March 2026:\n" +
81
+ " birth_datetime='1990-04-15T14:30:00Z', target_datetime='2026-03-15T00:00:00Z'",
71
82
  inputSchema: {
72
83
  type: "object",
73
84
  properties: {
74
85
  birth_datetime: {
75
86
  type: "string",
76
- description: "ISO 8601 birth date/time. MUST include offset or Z.",
87
+ description: "ISO 8601 birth date/time. Include offset or Z.",
77
88
  },
78
89
  target_datetime: {
79
90
  type: "string",
80
- description: "Date/time near which to find the Lunar Return (ISO 8601). MUST include offset or Z. Required.",
91
+ description: "Date/time near which to find the Lunar Return (ISO 8601 with offset or Z). " +
92
+ "Defaults to today if omitted (finds next return).",
81
93
  },
82
94
  birth_latitude: {
83
95
  type: "number",
@@ -88,14 +100,15 @@ registerTool({
88
100
  description: "Birth longitude in decimal degrees (optional).",
89
101
  },
90
102
  },
91
- required: ["birth_datetime", "target_datetime"],
103
+ required: ["birth_datetime"],
92
104
  additionalProperties: false,
93
105
  },
94
106
  handler: async (args) => {
95
- validateRequired(args, ["birth_datetime", "target_datetime"]);
107
+ validateRequired(args, ["birth_datetime"]);
108
+ const targetDt = args.target_datetime ?? new Date().toISOString();
96
109
  const body = {
97
110
  birth_datetime: { iso: args.birth_datetime },
98
- target_datetime: { iso: args.target_datetime },
111
+ target_datetime: { iso: targetDt },
99
112
  };
100
113
  if (args.birth_latitude != null && args.birth_longitude != null) {
101
114
  body.location = {
@@ -103,7 +116,7 @@ registerTool({
103
116
  longitude: { decimal: args.birth_longitude },
104
117
  };
105
118
  }
106
- return await backendClient.post("/predictive/returns/lunar", body);
119
+ return await getActiveClient().post("/predictive/returns/lunar", body);
107
120
  },
108
121
  });
109
122
  // POST /predictive/returns (generic planetary return)
@@ -138,7 +151,7 @@ registerTool({
138
151
  },
139
152
  handler: async (args) => {
140
153
  validateRequired(args, ["body", "birth_datetime", "target_datetime"]);
141
- return await backendClient.post("/predictive/returns", {
154
+ return await getActiveClient().post("/predictive/returns", {
142
155
  body: args.body,
143
156
  birth_datetime: { iso: args.birth_datetime },
144
157
  target_datetime: { iso: args.target_datetime },
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  registerTool({
4
4
  name: "ephemeris_synastry",
5
5
  description: "Calculate a synastry chart comparing two people's natal charts. Returns inter-aspects " +
@@ -63,7 +63,7 @@ registerTool({
63
63
  const query = {};
64
64
  if (args.format)
65
65
  query.format = args.format;
66
- return await backendClient.request("POST", "/comparative/synastry", {
66
+ return await getActiveClient().request("POST", "/comparative/synastry", {
67
67
  data: body,
68
68
  params: query,
69
69
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  registerTool({
4
4
  name: "ephemeris_transits",
5
5
  description: "Search for astrological transit events affecting a natal chart over a date range. " +
@@ -76,7 +76,7 @@ registerTool({
76
76
  },
77
77
  },
78
78
  };
79
- const natalResult = await backendClient.post("/ephemeris/natal-chart", natalBody);
79
+ const natalResult = await getActiveClient().post("/ephemeris/natal-chart", natalBody);
80
80
  // Extract ecliptic longitudes from natal chart planets
81
81
  const targetDegrees = [];
82
82
  const natalPositionMap = {};
@@ -141,7 +141,7 @@ registerTool({
141
141
  // Also include aspect metadata for labeling in results
142
142
  transitBody.search_criteria = { aspect_angle: args.aspect_angle };
143
143
  }
144
- const transitResult = await backendClient.request("POST", "/predictive/transits/search", { data: transitBody });
144
+ const transitResult = await getActiveClient().request("POST", "/predictive/transits/search", { data: transitBody });
145
145
  // Return combined context so the LLM knows what natal positions were targeted
146
146
  return {
147
147
  natal_positions: natalPositionMap,
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired, validateCoordinates } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  registerTool({
4
4
  name: "vedic_chart",
5
5
  description: "Calculate a Vedic (Jyotish) natal chart with sidereal positions. Returns planet placements " +
@@ -43,6 +43,6 @@ registerTool({
43
43
  };
44
44
  if (args.ayanamsa)
45
45
  body.ayanamsa = args.ayanamsa;
46
- return await backendClient.request("POST", "/vedic/chart", { data: body });
46
+ return await getActiveClient().request("POST", "/vedic/chart", { data: body });
47
47
  },
48
48
  });
@@ -1,5 +1,5 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
- import { backendClient } from "../../backend/client.js";
2
+ import { getActiveClient } from "../../backend/client.js";
3
3
  function buildSubject(datetime, lat, lon) {
4
4
  return {
5
5
  name: "Subject",
@@ -34,7 +34,7 @@ registerTool({
34
34
  const query = {};
35
35
  if (args.format)
36
36
  query.format = args.format;
37
- return await backendClient.request("POST", "/ephemeris/venus-star-points", {
37
+ return await getActiveClient().request("POST", "/ephemeris/venus-star-points", {
38
38
  data: { subject: buildSubject(args.datetime, args.latitude, args.longitude) },
39
39
  params: query,
40
40
  });
@@ -63,7 +63,7 @@ registerTool({
63
63
  const query = {};
64
64
  if (args.format)
65
65
  query.format = args.format;
66
- return await backendClient.request("POST", "/ephemeris/venus-star-points/conjunctions", {
66
+ return await getActiveClient().request("POST", "/ephemeris/venus-star-points/conjunctions", {
67
67
  data: { start_date: args.start_date, end_date: args.end_date },
68
68
  params: query,
69
69
  });
@@ -92,7 +92,7 @@ registerTool({
92
92
  const query = {};
93
93
  if (args.format)
94
94
  query.format = args.format;
95
- return await backendClient.request("POST", "/ephemeris/venus-star-points/eight-year-star", {
95
+ return await getActiveClient().request("POST", "/ephemeris/venus-star-points/eight-year-star", {
96
96
  data: { date: args.date },
97
97
  params: query,
98
98
  });
@@ -104,24 +104,25 @@ registerTool({
104
104
  description: "Get the current Venus phase — morning star, evening star, or combust. Returns Venus " +
105
105
  "longitude, Sun longitude, elongation, retrograde/cazimi status.\n\n" +
106
106
  "CREDIT COST: 1 credit per call.\n\n" +
107
- "EXAMPLE: Venus phase right now:\n" +
107
+ "EXAMPLE: Venus phase right now (omit date):\n" +
108
+ " (no args required)\n\n" +
109
+ "EXAMPLE: Venus phase on a specific date:\n" +
108
110
  " date='2026-03-20'",
109
111
  inputSchema: {
110
112
  type: "object",
111
113
  properties: {
112
- date: { type: "string", description: "Date to check (ISO 8601). Defaults to now." },
114
+ date: { type: "string", description: "Date to check (ISO 8601). Omit for today's phase." },
113
115
  format: { type: "string", enum: ["json", "llm"], description: "Output format." },
114
116
  },
115
- required: ["date"],
117
+ required: [],
116
118
  additionalProperties: false,
117
119
  },
118
120
  handler: async (args) => {
119
- validateRequired(args, ["date"]);
120
121
  const query = {};
121
122
  if (args.format)
122
123
  query.format = args.format;
123
- return await backendClient.request("POST", "/ephemeris/venus-star-points/phase", {
124
- data: { date: args.date },
124
+ return await getActiveClient().request("POST", "/ephemeris/venus-star-points/phase", {
125
+ data: { date: args.date ?? new Date().toISOString().split("T")[0] },
125
126
  params: query,
126
127
  });
127
128
  },
@@ -147,7 +148,7 @@ registerTool({
147
148
  const query = {};
148
149
  if (args.format)
149
150
  query.format = args.format;
150
- return await backendClient.request("POST", "/ephemeris/venus-star-points/elongations", {
151
+ return await getActiveClient().request("POST", "/ephemeris/venus-star-points/elongations", {
151
152
  data: { start_date: args.start_date, end_date: args.end_date },
152
153
  params: query,
153
154
  });
@@ -176,7 +177,7 @@ registerTool({
176
177
  const query = {};
177
178
  if (args.format)
178
179
  query.format = args.format;
179
- return await backendClient.request("POST", "/ephemeris/venus-star-points/stations", {
180
+ return await getActiveClient().request("POST", "/ephemeris/venus-star-points/stations", {
180
181
  data: {
181
182
  start_date: args.start_date,
182
183
  end_date: args.end_date || "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openephemeris/mcp-server",
3
- "version": "3.5.2",
3
+ "version": "3.6.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",