@openephemeris/mcp-server 3.5.1 → 3.5.3

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,28 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [3.5.3] — 2026-04-10
11
+
12
+ ### Fixed
13
+ - `ephemeris_chart_wheel` and `ephemeris_bi_wheel` tools now correctly advertise the
14
+ `style` values accepted by the backend (`light`, `dark`, `mono`). The previous enum
15
+ (`modern`, `classic`, `dark`) caused every non-dark chart request to fail with `400`.
16
+ - SSE sessions are now fully isolated: each connection creates its own `BackendClient`
17
+ instance keyed to the connecting user's API key. Concurrent sessions no longer
18
+ overwrite each other's API key on the shared singleton, preventing cross-user
19
+ billing attribution errors.
20
+
21
+ ---
22
+
23
+ ## [3.5.1] — 2026-03-31
24
+
25
+ ### Changed
26
+ - Added `mcp-server`, `modelcontextprotocol`, `model-context-protocol` to package.json keywords for registry discovery
27
+ - Fixed `OE_API_KEY` → `OPENEPHEMERIS_API_KEY` in registry submission guide
28
+ - Corrected GitHub links from `MeridianMap` to `Spirit-River` org
29
+
30
+ ---
31
+
10
32
  ## [3.5.0] — 2026-03-28
11
33
 
12
34
  ### Added
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenEphemeris MCP Server
2
2
 
3
- [![System Status](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fblacksmithops%2Fbetter-stack-shields%2Fmain%2Fbetterstack.json&label=Status&color=success)](https://status.openephemeris.com/)
3
+ [![System Status](https://img.shields.io/badge/Status-Operational-brightgreen)](https://status.openephemeris.com/)
4
4
 
5
5
  Model Context Protocol server for OpenEphemeris. This package exposes typed astrology tools and an allowlist-gated generic proxy (`dev.call`) for MCP-compatible clients.
6
6
 
@@ -224,8 +224,8 @@ OpenEphemeris API
224
224
 
225
225
  Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live tool registry.
226
226
 
227
- - Allowlisted operations: **20**
228
- - Methods: `GET=4`, `POST=16`, `PUT=0`, `PATCH=0`, `DELETE=0`
227
+ - Allowlisted operations: **22**
228
+ - Methods: `GET=4`, `POST=18`, `PUT=0`, `PATCH=0`, `DELETE=0`
229
229
  - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **48**
230
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`
231
231
  - Generic tools:
@@ -234,7 +234,7 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
234
234
 
235
235
  | Family | Operations | Example |
236
236
  |---|---:|---|
237
- | `acg` | 2 | `POST /acg/ccg`, `POST /acg/power-lines` |
237
+ | `acg` | 4 | `POST /acg/ccg`, `POST /acg/hits` |
238
238
  | `comparative` | 5 | `POST /comparative/composite`, `POST /comparative/composite/midpoint` |
239
239
  | `electional` | 4 | `GET /electional/aspect-search`, `GET /electional/find-window` |
240
240
  | `ephemeris` | 1 | `POST /ephemeris/relocation` |
@@ -19,6 +19,14 @@
19
19
  "method": "POST",
20
20
  "path": "/acg/ccg"
21
21
  },
22
+ {
23
+ "method": "POST",
24
+ "path": "/acg/hits"
25
+ },
26
+ {
27
+ "method": "POST",
28
+ "path": "/acg/local-space"
29
+ },
22
30
  {
23
31
  "method": "POST",
24
32
  "path": "/acg/power-lines"
@@ -96,8 +104,8 @@
96
104
  "path": "/visualization/chart-wheel"
97
105
  }
98
106
  ],
99
- "last_generated_at": "2026-03-29T02:31:11.230Z",
100
- "openapi_sha256": "c92376455af5f33481fd676a126d9b9436b30d5dd0c681d33258cf74a19ba18b",
107
+ "last_generated_at": "2026-04-03T20:52:05.270Z",
108
+ "openapi_sha256": "1203c3d6fd62c4af1b04ad983f9495a27f1799a13fcfd8aedc5189d118d5279a",
101
109
  "candidates_get": [
102
110
  {
103
111
  "method": "GET",
@@ -469,6 +477,14 @@
469
477
  ],
470
478
  "operationId": "features_acg_features_post"
471
479
  },
480
+ {
481
+ "method": "POST",
482
+ "path": "/acg/heatmap/composite",
483
+ "tags": [
484
+ "Astrocartography"
485
+ ],
486
+ "operationId": "AcgHeatmapCompositeAcgHeatmapCompositePost"
487
+ },
472
488
  {
473
489
  "method": "POST",
474
490
  "path": "/acg/hermetic-lines",
@@ -629,14 +645,6 @@
629
645
  ],
630
646
  "operationId": "calculate_harmonics_ephemeris_harmonics_post"
631
647
  },
632
- {
633
- "method": "POST",
634
- "path": "/ephemeris/heatmap/score",
635
- "tags": [
636
- "Ephemeris"
637
- ],
638
- "operationId": "HeatmapScoreEphemerisHeatmapScorePost"
639
- },
640
648
  {
641
649
  "method": "POST",
642
650
  "path": "/ephemeris/hermetic-lots",
@@ -979,6 +987,8 @@
979
987
  "errors": [],
980
988
  "warnings": [
981
989
  "Non-GET allowlisted: POST /acg/ccg",
990
+ "Non-GET allowlisted: POST /acg/hits",
991
+ "Non-GET allowlisted: POST /acg/local-space",
982
992
  "Non-GET allowlisted: POST /acg/power-lines",
983
993
  "Non-GET allowlisted: POST /comparative/composite/midpoint",
984
994
  "Non-GET allowlisted: POST /comparative/composite",
@@ -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
  // ---------------------------------------------------------------------------
@@ -144,7 +144,7 @@ async function main() {
144
144
  app.use((_req, res, next) => {
145
145
  res.setHeader("Access-Control-Allow-Origin", "*");
146
146
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
147
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-Meridian-API-Key");
147
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-OpenEphemeris-API-Key");
148
148
  if (_req.method === "OPTIONS") {
149
149
  res.status(204).end();
150
150
  return;
@@ -153,6 +153,8 @@ async function main() {
153
153
  });
154
154
  // Track active transports by session ID
155
155
  const transports = new Map();
156
+ // Per-session BackendClient — each connection authenticates independently.
157
+ const sessionClients = new Map();
156
158
  // Health check
157
159
  app.get("/health", (_req, res) => {
158
160
  res.json({ ok: true, version, transport: "sse", sessions: transports.size });
@@ -190,7 +192,7 @@ async function main() {
190
192
  // Extract API key from query param or header
191
193
  const apiKey = req.query.apiKey ||
192
194
  req.headers["x-api-key"] ||
193
- req.headers["x-meridian-api-key"] ||
195
+ req.headers["x-openephemeris-api-key"] ||
194
196
  req.headers["authorization"]?.replace(/^Bearer\s+/i, "");
195
197
  if (!apiKey) {
196
198
  res.status(401).json({
@@ -208,12 +210,16 @@ async function main() {
208
210
  });
209
211
  return;
210
212
  }
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
213
  const transport = new SSEServerTransport("/message", res);
215
214
  const sessionId = transport.sessionId;
216
215
  transports.set(sessionId, transport);
216
+ // Create a per-session BackendClient so concurrent SSE connections
217
+ // don't overwrite each other's API key on the shared singleton.
218
+ const sessionClient = new BackendClient({
219
+ baseURL: BACKEND_URL,
220
+ apiKey,
221
+ });
222
+ sessionClients.set(sessionId, sessionClient);
217
223
  const server = createMcpServer();
218
224
  // Guard: prevent double-close from crashing the process
219
225
  let closed = false;
@@ -222,6 +228,7 @@ async function main() {
222
228
  return;
223
229
  closed = true;
224
230
  transports.delete(sessionId);
231
+ sessionClients.delete(sessionId);
225
232
  try {
226
233
  server.close().catch(() => { });
227
234
  }
@@ -243,6 +250,7 @@ async function main() {
243
250
  app.post("/message", async (req, res) => {
244
251
  const sessionId = req.query.sessionId;
245
252
  const transport = transports.get(sessionId);
253
+ const sessionClient = sessionClients.get(sessionId);
246
254
  if (!transport) {
247
255
  res.status(400).json({
248
256
  error: "invalid_session",
@@ -250,7 +258,14 @@ async function main() {
250
258
  });
251
259
  return;
252
260
  }
253
- await transport.handlePostMessage(req, res);
261
+ // Run handlePostMessage inside the session's client context so all tool
262
+ // handlers reach getActiveClient() and get this user's BackendClient.
263
+ if (sessionClient) {
264
+ await runWithClient(sessionClient, () => transport.handlePostMessage(req, res));
265
+ }
266
+ else {
267
+ await transport.handlePostMessage(req, res);
268
+ }
254
269
  });
255
270
  app.listen(PORT, "0.0.0.0", () => {
256
271
  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";
@@ -165,7 +165,13 @@ registerTool({
165
165
  query.format = "llm";
166
166
  }
167
167
  }
168
- return await backendClient.request(method, pathname, {
168
+ // Auto-inject format=png for visualization endpoints when no format specified.
169
+ // MCP image handlers only accept PNG/JPG/GIF/WEBP — SVG won't render inline.
170
+ const isVisualizationPath = pathname.startsWith("/visualization/") || pathname.startsWith("/comparative/visualization/");
171
+ if (isVisualizationPath && !query.format) {
172
+ query.format = "png";
173
+ }
174
+ return await getActiveClient().request(method, pathname, {
169
175
  params: query,
170
176
  data: body,
171
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. " +
@@ -68,7 +68,7 @@ registerTool({
68
68
  body.query_lon = args.query_longitude;
69
69
  if (args.radius_deg != null)
70
70
  body.radius_deg = args.radius_deg;
71
- return await backendClient.post("/acg/power-lines", body);
71
+ return await getActiveClient().post("/acg/power-lines", body);
72
72
  },
73
73
  });
74
74
  // POST /acg/hits — OE-018
@@ -138,6 +138,6 @@ registerTool({
138
138
  if (args.include_aspects != null) {
139
139
  body.options = { include_aspects: args.include_aspects };
140
140
  }
141
- return await backendClient.post("/acg/hits", body);
141
+ return await getActiveClient().post("/acg/hits", body);
142
142
  },
143
143
  });
@@ -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,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: "ephemeris_next_eclipse",
5
5
  description: "Find the next solar or lunar eclipse visible from a given location. " +
@@ -45,6 +45,6 @@ registerTool({
45
45
  params.date = args.after_date;
46
46
  if (args.eclipse_type && args.eclipse_type !== "any")
47
47
  params.type = args.eclipse_type;
48
- return await backendClient.request("GET", "/eclipse/next-visible", { params, timeoutMs: 60_000 });
48
+ return await getActiveClient().request("GET", "/eclipse/next-visible", { params, timeoutMs: 60_000 });
49
49
  },
50
50
  });
@@ -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,7 +111,7 @@ 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
  });
@@ -160,7 +160,7 @@ registerTool({
160
160
  query.planets = args.planets;
161
161
  if (args.format)
162
162
  query.format = args.format;
163
- return await backendClient.request("GET", "/electional/station-tracker", {
163
+ return await getActiveClient().request("GET", "/electional/station-tracker", {
164
164
  data: {},
165
165
  params: query,
166
166
  });
@@ -202,7 +202,7 @@ registerTool({
202
202
  query.max_orb = args.max_orb;
203
203
  if (args.aspects)
204
204
  query.aspects = args.aspects;
205
- return await backendClient.request("GET", "/electional/aspect-search", {
205
+ return await getActiveClient().request("GET", "/electional/aspect-search", {
206
206
  data: {},
207
207
  params: query,
208
208
  });
@@ -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,5 +1,5 @@
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
5
  description: "Get the current Moon phase and void-of-course status. Returns the Moon's sign, phase name " +
@@ -40,8 +40,8 @@ registerTool({
40
40
  if (args.longitude != null)
41
41
  params.longitude = args.longitude;
42
42
  const [phase, voc] = await Promise.allSettled([
43
- backendClient.request("GET", "/ephemeris/moon/phase", { params }),
44
- backendClient.request("GET", "/ephemeris/moon/void-of-course", { params }),
43
+ getActiveClient().request("GET", "/ephemeris/moon/phase", { params }),
44
+ getActiveClient().request("GET", "/ephemeris/moon/void-of-course", { params }),
45
45
  ]);
46
46
  return {
47
47
  phase: phase.status === "fulfilled" ? phase.value : { error: phase.reason?.message },
@@ -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,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 /predictive/returns/solar
4
4
  registerTool({
5
5
  name: "ephemeris_solar_return",
@@ -57,7 +57,7 @@ registerTool({
57
57
  longitude: { decimal: args.return_longitude },
58
58
  };
59
59
  }
60
- return await backendClient.post("/predictive/returns/solar", body);
60
+ return await getActiveClient().post("/predictive/returns/solar", body);
61
61
  },
62
62
  });
63
63
  // POST /predictive/returns/lunar
@@ -103,7 +103,7 @@ registerTool({
103
103
  longitude: { decimal: args.birth_longitude },
104
104
  };
105
105
  }
106
- return await backendClient.post("/predictive/returns/lunar", body);
106
+ return await getActiveClient().post("/predictive/returns/lunar", body);
107
107
  },
108
108
  });
109
109
  // POST /predictive/returns (generic planetary return)
@@ -138,7 +138,7 @@ registerTool({
138
138
  },
139
139
  handler: async (args) => {
140
140
  validateRequired(args, ["body", "birth_datetime", "target_datetime"]);
141
- return await backendClient.post("/predictive/returns", {
141
+ return await getActiveClient().post("/predictive/returns", {
142
142
  body: args.body,
143
143
  birth_datetime: { iso: args.birth_datetime },
144
144
  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
  });
@@ -120,7 +120,7 @@ registerTool({
120
120
  const query = {};
121
121
  if (args.format)
122
122
  query.format = args.format;
123
- return await backendClient.request("POST", "/ephemeris/venus-star-points/phase", {
123
+ return await getActiveClient().request("POST", "/ephemeris/venus-star-points/phase", {
124
124
  data: { date: args.date },
125
125
  params: query,
126
126
  });
@@ -147,7 +147,7 @@ registerTool({
147
147
  const query = {};
148
148
  if (args.format)
149
149
  query.format = args.format;
150
- return await backendClient.request("POST", "/ephemeris/venus-star-points/elongations", {
150
+ return await getActiveClient().request("POST", "/ephemeris/venus-star-points/elongations", {
151
151
  data: { start_date: args.start_date, end_date: args.end_date },
152
152
  params: query,
153
153
  });
@@ -176,7 +176,7 @@ registerTool({
176
176
  const query = {};
177
177
  if (args.format)
178
178
  query.format = args.format;
179
- return await backendClient.request("POST", "/ephemeris/venus-star-points/stations", {
179
+ return await getActiveClient().request("POST", "/ephemeris/venus-star-points/stations", {
180
180
  data: {
181
181
  start_date: args.start_date,
182
182
  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.1",
3
+ "version": "3.5.3",
4
4
  "description": "Model Context Protocol server for the Open Ephemeris astronomical computation API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,6 +42,9 @@
42
42
  },
43
43
  "keywords": [
44
44
  "mcp",
45
+ "mcp-server",
46
+ "modelcontextprotocol",
47
+ "model-context-protocol",
45
48
  "astrology",
46
49
  "astronomy",
47
50
  "ephemeris",
@@ -52,22 +55,27 @@
52
55
  "swiss ephemeris"
53
56
  ],
54
57
  "author": "Open Ephemeris",
58
+ "homepage": "https://openephemeris.com",
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "https://github.com/Spirit-River/openephemeris-MCP.git"
62
+ },
55
63
  "license": "MIT",
56
64
  "engines": {
57
65
  "node": ">=18.0.0"
58
66
  },
59
67
  "dependencies": {
60
- "@modelcontextprotocol/sdk": "^1.24.3",
61
- "axios": "^1.13.2",
62
- "express": "^5.1.0",
63
- "zod": "^4.1.13"
68
+ "@modelcontextprotocol/sdk": "1.24.3",
69
+ "axios": "1.13.2",
70
+ "express": "5.1.0",
71
+ "zod": "4.1.13"
64
72
  },
65
73
  "devDependencies": {
66
- "@types/express": "^5.0.3",
67
- "@types/node": "^25.0.1",
68
- "tsx": "^4.21.0",
69
- "typescript": "^5.9.3",
70
- "vitest": "^4.0.15"
74
+ "@types/express": "5.0.3",
75
+ "@types/node": "25.0.1",
76
+ "tsx": "4.21.0",
77
+ "typescript": "5.9.3",
78
+ "vitest": "4.0.15"
71
79
  },
72
80
  "packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
73
81
  }