@openephemeris/mcp-server 3.6.1 → 3.9.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 +36 -1
- package/README.md +6 -6
- package/config/dev-allowlist.json +23 -3
- package/dist/backend/client.js +7 -3
- package/dist/server-sse.js +161 -9
- package/dist/tools/index.js +2 -0
- package/dist/tools/specialized/acg.js +21 -1
- package/dist/tools/specialized/comparative.js +8 -5
- package/dist/tools/specialized/ephemeris_extended.js +68 -10
- package/dist/tools/specialized/hd_bodygraph.d.ts +1 -0
- package/dist/tools/specialized/hd_bodygraph.js +51 -0
- package/dist/tools/specialized/moon.js +20 -6
- package/dist/tools/specialized/natal.js +1 -1
- package/dist/tools/specialized/returns.js +21 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,41 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [3.9.0] — 2026-04-22
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Streamable HTTP transport** (`POST/GET/DELETE /mcp`) — implements the MCP 2025-11-25
|
|
14
|
+
specification. New integrations (Smithery, cursor, Claude Web) should prefer
|
|
15
|
+
`https://mcp.openephemeris.com/mcp` over the legacy SSE endpoint. The SSE endpoint
|
|
16
|
+
(`/sse`) is retained for backward compatibility.
|
|
17
|
+
- **`/.well-known/mcp/server-card.json`** — static server card served at the standard
|
|
18
|
+
well-known path. Allows Smithery and other registries to scan tool metadata without
|
|
19
|
+
requiring an authenticated session, enabling automatic listing and discovery.
|
|
20
|
+
- **`extractApiKey()` helper** — shared auth extraction (X-API-Key / Authorization: Bearer
|
|
21
|
+
/ X-OpenEphemeris-API-Key / ?apiKey) used by both transports, eliminating duplication.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `smithery.yaml` upgraded to modern `configSchema` + `commandFunction` format. Smithery
|
|
25
|
+
now renders a typed API key input field in its UI instead of a raw env map.
|
|
26
|
+
- `/health` now reports both `sse_sessions` and `http_sessions` counts, and lists
|
|
27
|
+
`transports: ["sse", "streamable-http"]` for client introspection.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## [3.8.0] — 2026-04-21
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- `human_design_bodygraph` — new dedicated tool for generating Human Design Bodygraph images
|
|
35
|
+
(PNG or SVG). Renders all 9 centers, 36 channels, and 64 gates with Personality/Design
|
|
36
|
+
color-coding. Supports `light`, `dark`, and `mono` styles. Developer tier, 2 credits.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- **HD Bodygraph: missing 10-57 channel** — The "Channel of Perfected Form" (Gate 10 ↔ Gate 57)
|
|
40
|
+
was silently dropped from the visualization geometry while the math engine still reported it as
|
|
41
|
+
active. Charts with both gates activated now correctly render the channel.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
10
45
|
## [3.7.0] — 2026-04-19
|
|
11
46
|
|
|
12
47
|
### Changed
|
|
@@ -82,7 +117,7 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
82
117
|
|
|
83
118
|
|
|
84
119
|
### Fixed
|
|
85
|
-
- Chiron position fallback for edge-date calculations now uses full
|
|
120
|
+
- Chiron position fallback for edge-date calculations now uses full high-precision ephemeris path instead of simplified approximation
|
|
86
121
|
- SSE server chart wheel responses now correctly deliver native MCP image blocks instead of raw base64 JSON
|
|
87
122
|
|
|
88
123
|
### Changed
|
package/README.md
CHANGED
|
@@ -224,10 +224,10 @@ 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: **
|
|
228
|
-
- Methods: `GET=4`, `POST=
|
|
229
|
-
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **
|
|
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`
|
|
227
|
+
- Allowlisted operations: **23**
|
|
228
|
+
- Methods: `GET=4`, `POST=19`, `PUT=0`, `PATCH=0`, `DELETE=0`
|
|
229
|
+
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **52**
|
|
230
|
+
- Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `chinese_bazi`, `dev_call`, `dev_list_allowed`, `electional_aspect_search`, `electional_moment_analysis`, `electional_station_tracker`, `ephemeris_angles_points`, `ephemeris_aspect_check`, `ephemeris_bi_wheel`, `ephemeris_chart_wheel`, `ephemeris_composite`, `ephemeris_composite_midpoint`, `ephemeris_dignities`, `ephemeris_electional`, `ephemeris_fixed_stars`, `ephemeris_hermetic_lots`, `ephemeris_house_cusps`, `ephemeris_lunar_return`, `ephemeris_midpoints`, `ephemeris_moon_phase`, `ephemeris_natal_batch`, `ephemeris_natal_chart`, `ephemeris_natal_transits`, `ephemeris_next_eclipse`, `ephemeris_next_lunar_phase`, `ephemeris_overlay`, `ephemeris_planet_position`, `ephemeris_planetary_return`, `ephemeris_progressed_chart`, `ephemeris_relocation`, `ephemeris_retrograde_status`, `ephemeris_solar_return`, `ephemeris_synastry`, `ephemeris_transits`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `vedic_chart`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
|
|
231
231
|
- Generic tools:
|
|
232
232
|
|
|
233
233
|
### Allowlist Families
|
|
@@ -239,12 +239,12 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
239
239
|
| `electional` | 4 | `GET /electional/aspect-search`, `GET /electional/find-window` |
|
|
240
240
|
| `ephemeris` | 1 | `POST /ephemeris/relocation` |
|
|
241
241
|
| `predictive` | 6 | `POST /predictive/returns`, `POST /predictive/returns/lunar` |
|
|
242
|
-
| `visualization` |
|
|
242
|
+
| `visualization` | 3 | `POST /visualization/bi-wheel`, `POST /visualization/bodygraph` |
|
|
243
243
|
<!-- GENERATED:RUNTIME_SNAPSHOT:END -->
|
|
244
244
|
|
|
245
245
|
## Why OpenEphemeris for AI Agents?
|
|
246
246
|
|
|
247
|
-
Most LLMs (like Claude and ChatGPT) struggle heavily with astronomical calculations (trigonometry, Julian date conversions, and
|
|
247
|
+
Most LLMs (like Claude and ChatGPT) struggle heavily with astronomical calculations (trigonometry, Julian date conversions, and planetary lookups). OpenEphemeris serves as a **secure, remote math engine**.
|
|
248
248
|
|
|
249
249
|
By pairing LLMs with the OpenEphemeris MCP server, your agents can instantly access:
|
|
250
250
|
- **Zero-hallucination coordinates**: Direct, sub-arcsecond NASA JPL DE440/DE441 calculations.
|
|
@@ -99,13 +99,17 @@
|
|
|
99
99
|
"method": "POST",
|
|
100
100
|
"path": "/visualization/bi-wheel"
|
|
101
101
|
},
|
|
102
|
+
{
|
|
103
|
+
"method": "POST",
|
|
104
|
+
"path": "/visualization/bodygraph"
|
|
105
|
+
},
|
|
102
106
|
{
|
|
103
107
|
"method": "POST",
|
|
104
108
|
"path": "/visualization/chart-wheel"
|
|
105
109
|
}
|
|
106
110
|
],
|
|
107
|
-
"last_generated_at": "2026-04-
|
|
108
|
-
"openapi_sha256": "
|
|
111
|
+
"last_generated_at": "2026-04-22T15:19:21.508Z",
|
|
112
|
+
"openapi_sha256": "0c29ad2e37fd08d48800ec54059e262fbbdb681b844e00fed9274611bae678ac",
|
|
109
113
|
"candidates_get": [
|
|
110
114
|
{
|
|
111
115
|
"method": "GET",
|
|
@@ -477,7 +481,14 @@
|
|
|
477
481
|
],
|
|
478
482
|
"operationId": "features_acg_features_post"
|
|
479
483
|
},
|
|
480
|
-
|
|
484
|
+
{
|
|
485
|
+
"method": "POST",
|
|
486
|
+
"path": "/acg/heatmap/composite",
|
|
487
|
+
"tags": [
|
|
488
|
+
"Astrocartography"
|
|
489
|
+
],
|
|
490
|
+
"operationId": "AcgHeatmapCompositeAcgHeatmapCompositePost"
|
|
491
|
+
},
|
|
481
492
|
{
|
|
482
493
|
"method": "POST",
|
|
483
494
|
"path": "/acg/hermetic-lines",
|
|
@@ -966,6 +977,14 @@
|
|
|
966
977
|
],
|
|
967
978
|
"operationId": "generate_bi_wheel_visualization_bi_wheel_post"
|
|
968
979
|
},
|
|
980
|
+
{
|
|
981
|
+
"method": "POST",
|
|
982
|
+
"path": "/visualization/bodygraph",
|
|
983
|
+
"tags": [
|
|
984
|
+
"visualization"
|
|
985
|
+
],
|
|
986
|
+
"operationId": "GenerateBodygraphVisualization_visualization_bodygraph_post"
|
|
987
|
+
},
|
|
969
988
|
{
|
|
970
989
|
"method": "POST",
|
|
971
990
|
"path": "/visualization/chart-wheel",
|
|
@@ -996,6 +1015,7 @@
|
|
|
996
1015
|
"Non-GET allowlisted: POST /predictive/time-lords/profections",
|
|
997
1016
|
"Non-GET allowlisted: POST /predictive/time-lords/zodiacal-releasing",
|
|
998
1017
|
"Non-GET allowlisted: POST /visualization/bi-wheel",
|
|
1018
|
+
"Non-GET allowlisted: POST /visualization/bodygraph",
|
|
999
1019
|
"Non-GET allowlisted: POST /visualization/chart-wheel"
|
|
1000
1020
|
]
|
|
1001
1021
|
}
|
package/dist/backend/client.js
CHANGED
|
@@ -8,6 +8,7 @@ const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
|
|
|
8
8
|
const RETRYABLE_CODES = new Set(["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN"]);
|
|
9
9
|
const BINARY_ENDPOINT_PREFIXES = [
|
|
10
10
|
"/visualization/bi-wheel",
|
|
11
|
+
"/visualization/bodygraph",
|
|
11
12
|
"/visualization/chart-wheel",
|
|
12
13
|
"/comparative/visualization/bi-wheel",
|
|
13
14
|
"/comparative/visualization/chart-wheel",
|
|
@@ -259,10 +260,13 @@ export class BackendClient {
|
|
|
259
260
|
`All chart positions, aspects, and timing data MUST come from the API.`, 402, "quota_exceeded", false, UPGRADE_URL);
|
|
260
261
|
}
|
|
261
262
|
if (status === 403) {
|
|
262
|
-
return new BackendError(
|
|
263
|
+
return new BackendError(`This feature is part of a higher-tier plan.\n\n` +
|
|
263
264
|
`${msg}\n\n` +
|
|
264
|
-
`
|
|
265
|
-
`
|
|
265
|
+
`Let the user know warmly: this particular capability is available on a ` +
|
|
266
|
+
`higher plan tier. Share the upgrade link (${UPGRADE_URL}) as a helpful suggestion, ` +
|
|
267
|
+
`not as a gate. Mention what they CAN do on their current plan, and frame the upgrade ` +
|
|
268
|
+
`as unlocking deeper possibilities — charts, visualizations, and advanced HD analysis ` +
|
|
269
|
+
`are all available with a plan upgrade.\n\n` +
|
|
266
270
|
`DO NOT attempt to generate astrological data from your own knowledge. ` +
|
|
267
271
|
`All chart positions, aspects, and timing data MUST come from the API.`, 403, "tier_required", false, UPGRADE_URL);
|
|
268
272
|
}
|
package/dist/server-sse.js
CHANGED
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
18
19
|
import express from "express";
|
|
19
20
|
import axios from "axios";
|
|
20
21
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
22
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
22
|
-
import {
|
|
23
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
24
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, isInitializeRequest, } from "@modelcontextprotocol/sdk/types.js";
|
|
23
25
|
import { initTools, toolRegistry, formatToolResponse } from "./tools/index.js";
|
|
24
26
|
import { BackendClient, runWithClient } from "./backend/client.js";
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
@@ -76,6 +78,20 @@ async function validateApiKey(apiKey) {
|
|
|
76
78
|
return true;
|
|
77
79
|
}
|
|
78
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Extract an API key from a request using priority order:
|
|
83
|
+
* 1. X-API-Key header
|
|
84
|
+
* 2. X-OpenEphemeris-API-Key header
|
|
85
|
+
* 3. Authorization: Bearer <token>
|
|
86
|
+
* 4. ?apiKey query param
|
|
87
|
+
*/
|
|
88
|
+
function extractApiKey(req) {
|
|
89
|
+
return (req.headers["x-api-key"] ||
|
|
90
|
+
req.headers["x-openephemeris-api-key"] ||
|
|
91
|
+
req.headers["authorization"]?.replace(/^Bearer\s+/i, "") ||
|
|
92
|
+
req.query.apiKey ||
|
|
93
|
+
undefined);
|
|
94
|
+
}
|
|
79
95
|
// ---------------------------------------------------------------------------
|
|
80
96
|
// MCP Server factory — one per SSE session
|
|
81
97
|
// ---------------------------------------------------------------------------
|
|
@@ -132,9 +148,7 @@ function createMcpServer() {
|
|
|
132
148
|
});
|
|
133
149
|
return server;
|
|
134
150
|
}
|
|
135
|
-
|
|
136
|
-
// Express + SSE wiring
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
151
|
+
const httpSessions = new Map();
|
|
138
152
|
async function main() {
|
|
139
153
|
await initTools();
|
|
140
154
|
const app = express();
|
|
@@ -158,7 +172,143 @@ async function main() {
|
|
|
158
172
|
const sessionClients = new Map();
|
|
159
173
|
// Health check
|
|
160
174
|
app.get("/health", (_req, res) => {
|
|
161
|
-
res.json({
|
|
175
|
+
res.json({
|
|
176
|
+
ok: true,
|
|
177
|
+
version,
|
|
178
|
+
transports: ["sse", "streamable-http"],
|
|
179
|
+
sse_sessions: transports.size,
|
|
180
|
+
http_sessions: httpSessions.size,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Static server card — lets Smithery (and other registries) scan tools
|
|
185
|
+
// without needing an authenticated session. Updated on every deploy.
|
|
186
|
+
// Spec: https://smithery.ai/docs/build/publish#server-scanning
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
app.get("/.well-known/mcp/server-card.json", (_req, res) => {
|
|
189
|
+
const tools = Object.values(toolRegistry).map((tool) => ({
|
|
190
|
+
name: tool.name,
|
|
191
|
+
description: tool.description,
|
|
192
|
+
inputSchema: tool.inputSchema,
|
|
193
|
+
}));
|
|
194
|
+
res.json({
|
|
195
|
+
serverInfo: {
|
|
196
|
+
name: "Open Ephemeris",
|
|
197
|
+
version,
|
|
198
|
+
description: "NASA JPL-backed astronomical computation engine for AI agents. 48+ typed tools covering natal charts, transit forecasting, Human Design, eclipses, astrocartography, BaZi, and more — powered by DE440/DE441 for sub-arcsecond accuracy.",
|
|
199
|
+
iconUrl: "https://mcp.openephemeris.com/icon.png",
|
|
200
|
+
homepage: "https://openephemeris.com",
|
|
201
|
+
},
|
|
202
|
+
authentication: {
|
|
203
|
+
required: true,
|
|
204
|
+
schemes: ["apiKey"],
|
|
205
|
+
instructions: "Pass your Open Ephemeris API key via the X-API-Key header. Get a free Explorer key at https://openephemeris.com/dashboard",
|
|
206
|
+
},
|
|
207
|
+
tools,
|
|
208
|
+
resources: [],
|
|
209
|
+
prompts: [],
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Streamable HTTP transport — MCP spec 2025-11-25
|
|
214
|
+
// Endpoint: POST/GET/DELETE https://mcp.openephemeris.com/mcp
|
|
215
|
+
// Clients send X-API-Key (or Authorization: Bearer) on the init request.
|
|
216
|
+
// Session ID is managed via mcp-session-id response header.
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Parse JSON bodies only for /mcp routes (keeps SSE routes safe)
|
|
219
|
+
const jsonParser = express.json({ limit: "4mb" });
|
|
220
|
+
// POST /mcp — initialize a new session OR resume an existing one
|
|
221
|
+
app.post("/mcp", jsonParser, async (req, res) => {
|
|
222
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
223
|
+
// Resume existing session
|
|
224
|
+
if (sessionId) {
|
|
225
|
+
const session = httpSessions.get(sessionId);
|
|
226
|
+
if (!session) {
|
|
227
|
+
res.status(404).json({
|
|
228
|
+
error: "session_not_found",
|
|
229
|
+
message: "Unknown or expired MCP session. Re-initialize to start a new one.",
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
await runWithClient(session.client, () => session.transport.handleRequest(req, res, req.body));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// New session — must be an initialize request
|
|
237
|
+
if (!isInitializeRequest(req.body)) {
|
|
238
|
+
res.status(400).json({
|
|
239
|
+
error: "expected_initialize",
|
|
240
|
+
message: "First request to /mcp must be an MCP Initialize request.",
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// Auth — required on Initialize
|
|
245
|
+
const apiKey = extractApiKey(req);
|
|
246
|
+
if (!apiKey) {
|
|
247
|
+
res.status(401).json({
|
|
248
|
+
error: "api_key_required",
|
|
249
|
+
message: "An Open Ephemeris API key is required. Pass it via X-API-Key header or Authorization: Bearer. Get a free key at https://openephemeris.com/dashboard",
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const valid = await validateApiKey(apiKey);
|
|
254
|
+
if (!valid) {
|
|
255
|
+
res.status(403).json({
|
|
256
|
+
error: "invalid_api_key",
|
|
257
|
+
message: "The provided API key is invalid or inactive. Check your key at https://openephemeris.com/dashboard?tab=apikeys",
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Create per-session transport, server, and client
|
|
262
|
+
const transport = new StreamableHTTPServerTransport({
|
|
263
|
+
sessionIdGenerator: randomUUID,
|
|
264
|
+
onsessioninitialized: (id) => {
|
|
265
|
+
httpSessions.set(id, { server, transport, client });
|
|
266
|
+
console.error(`[HTTP] Session initialized: ${id}`);
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
const client = new BackendClient({ baseURL: BACKEND_URL, apiKey });
|
|
270
|
+
const server = createMcpServer();
|
|
271
|
+
transport.onclose = () => {
|
|
272
|
+
if (transport.sessionId) {
|
|
273
|
+
httpSessions.delete(transport.sessionId);
|
|
274
|
+
console.error(`[HTTP] Session closed: ${transport.sessionId}`);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
await server.connect(transport);
|
|
278
|
+
await runWithClient(client, () => transport.handleRequest(req, res, req.body));
|
|
279
|
+
});
|
|
280
|
+
// GET /mcp — SSE stream leg of Streamable HTTP (server → client events)
|
|
281
|
+
app.get("/mcp", async (req, res) => {
|
|
282
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
283
|
+
if (!sessionId) {
|
|
284
|
+
res.status(400).json({
|
|
285
|
+
error: "session_id_required",
|
|
286
|
+
message: "mcp-session-id header is required for GET /mcp.",
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const session = httpSessions.get(sessionId);
|
|
291
|
+
if (!session) {
|
|
292
|
+
res.status(404).json({
|
|
293
|
+
error: "session_not_found",
|
|
294
|
+
message: "Unknown or expired MCP session.",
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
await session.transport.handleRequest(req, res);
|
|
299
|
+
});
|
|
300
|
+
// DELETE /mcp — client-initiated session teardown
|
|
301
|
+
app.delete("/mcp", (req, res) => {
|
|
302
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
303
|
+
if (sessionId) {
|
|
304
|
+
const session = httpSessions.get(sessionId);
|
|
305
|
+
if (session) {
|
|
306
|
+
httpSessions.delete(sessionId);
|
|
307
|
+
session.server.close().catch(() => { });
|
|
308
|
+
console.error(`[HTTP] Session deleted: ${sessionId}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
res.status(204).end();
|
|
162
312
|
});
|
|
163
313
|
// Serve the MCP icon (earth + star)
|
|
164
314
|
// Compiled JS at dist/src/server-sse.js: ../../assets
|
|
@@ -188,13 +338,15 @@ async function main() {
|
|
|
188
338
|
res.status(404).end();
|
|
189
339
|
}
|
|
190
340
|
});
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Legacy SSE transport — MCP spec pre-2025 (kept for backward compatibility)
|
|
343
|
+
// Endpoint: GET https://mcp.openephemeris.com/sse
|
|
344
|
+
// New integrations should prefer /mcp (Streamable HTTP above).
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
191
346
|
// SSE endpoint — clients GET /sse to establish a stream
|
|
192
347
|
app.get("/sse", async (req, res) => {
|
|
193
348
|
// Extract API key from query param or header
|
|
194
|
-
const apiKey = req
|
|
195
|
-
req.headers["x-api-key"] ||
|
|
196
|
-
req.headers["x-openephemeris-api-key"] ||
|
|
197
|
-
req.headers["authorization"]?.replace(/^Bearer\s+/i, "");
|
|
349
|
+
const apiKey = extractApiKey(req);
|
|
198
350
|
if (!apiKey) {
|
|
199
351
|
res.status(401).json({
|
|
200
352
|
error: "api_key_required",
|
package/dist/tools/index.js
CHANGED
|
@@ -38,9 +38,11 @@ export async function initTools(profile) {
|
|
|
38
38
|
await import("./specialized/ephemeris_core.js");
|
|
39
39
|
await import("./specialized/comparative.js");
|
|
40
40
|
await import("./specialized/hd_group.js");
|
|
41
|
+
await import("./specialized/hd_bodygraph.js");
|
|
41
42
|
await import("./specialized/hd_cycles.js");
|
|
42
43
|
await import("./specialized/ephemeris_extended.js");
|
|
43
44
|
await import("./specialized/venus_star_points.js");
|
|
45
|
+
await import("./specialized/acg.js");
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
@@ -71,7 +71,27 @@ registerTool({
|
|
|
71
71
|
body.query_lon = args.query_longitude;
|
|
72
72
|
if (args.radius_deg != null)
|
|
73
73
|
body.radius_deg = args.radius_deg;
|
|
74
|
-
|
|
74
|
+
const raw = await getActiveClient().post("/acg/power-lines", body);
|
|
75
|
+
// Strip coordinate geometry to prevent context-window blowouts.
|
|
76
|
+
// Full GeoJSON can be 200K+ tokens; condensed metadata is ~2K.
|
|
77
|
+
if (raw?.type === "FeatureCollection" && Array.isArray(raw.features)) {
|
|
78
|
+
const condensed = raw.features.map((f) => ({
|
|
79
|
+
planet: f.properties?.planet ?? f.properties?.body,
|
|
80
|
+
angle: f.properties?.angle ?? f.properties?.line_type,
|
|
81
|
+
...(f.properties?.label ? { label: f.properties.label } : {}),
|
|
82
|
+
...(f.properties?.zodiac_sign ? { zodiac_sign: f.properties.zodiac_sign } : {}),
|
|
83
|
+
...(f.properties?.longitude_at_equator != null
|
|
84
|
+
? { longitude_at_equator: f.properties.longitude_at_equator }
|
|
85
|
+
: {}),
|
|
86
|
+
}));
|
|
87
|
+
return {
|
|
88
|
+
type: "acg_power_lines_summary",
|
|
89
|
+
total_lines: condensed.length,
|
|
90
|
+
note: "Coordinate geometry stripped for LLM context efficiency. Use the full API or a map rendering client for geographic visualization.",
|
|
91
|
+
lines: condensed,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return raw;
|
|
75
95
|
},
|
|
76
96
|
});
|
|
77
97
|
// POST /acg/hits — OE-018
|
|
@@ -173,18 +173,21 @@ registerTool({
|
|
|
173
173
|
},
|
|
174
174
|
handler: async (args) => {
|
|
175
175
|
validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude"]);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
// Backend expects: { subject: {...}, transit_datetime?: {...} }
|
|
177
|
+
// NOT a subjects[] array — natal-transits is a single-subject endpoint
|
|
178
|
+
const body = {
|
|
179
|
+
subject: buildSubject("Natal", args.natal_datetime, args.natal_latitude, args.natal_longitude, args.natal_timezone),
|
|
180
|
+
};
|
|
179
181
|
if (args.transit_datetime) {
|
|
180
|
-
|
|
182
|
+
// transit_datetime is a DateTimeInput on the backend
|
|
183
|
+
body.transit_datetime = { iso: args.transit_datetime };
|
|
181
184
|
}
|
|
182
185
|
const query = {};
|
|
183
186
|
if (args.format)
|
|
184
187
|
query.format = args.format;
|
|
185
188
|
return await getActiveClient().request("POST", "/comparative/natal-transits", {
|
|
186
189
|
params: query,
|
|
187
|
-
data:
|
|
190
|
+
data: body,
|
|
188
191
|
});
|
|
189
192
|
},
|
|
190
193
|
});
|
|
@@ -72,11 +72,15 @@ registerTool({
|
|
|
72
72
|
},
|
|
73
73
|
});
|
|
74
74
|
// POST /ephemeris/retrograde-status
|
|
75
|
+
// NOTE: the backend handles one planet per call (planet_id is a non-nullable int
|
|
76
|
+
// that defaults to 0/Sun when omitted). We fan out to all 10 planets here so
|
|
77
|
+
// the documented "all planets" behaviour works correctly.
|
|
75
78
|
registerTool({
|
|
76
79
|
name: "ephemeris_retrograde_status",
|
|
77
80
|
description: "Get retrograde/direct status and speed for all planets at a given date/time. " +
|
|
78
|
-
"Returns is_retrograde flag, longitude speed, and station proximity.\n\n" +
|
|
79
|
-
"CREDIT COST: 1 credit per call
|
|
81
|
+
"Returns is_retrograde flag, longitude speed, and station proximity for every planet.\n\n" +
|
|
82
|
+
"CREDIT COST: 1 credit per call.\n\n" +
|
|
83
|
+
"Optionally pass planet_id (0-9) to query a single planet.",
|
|
80
84
|
inputSchema: {
|
|
81
85
|
type: "object",
|
|
82
86
|
properties: {
|
|
@@ -88,12 +92,34 @@ registerTool({
|
|
|
88
92
|
},
|
|
89
93
|
handler: async (args) => {
|
|
90
94
|
validateRequired(args, ["datetime"]);
|
|
91
|
-
const
|
|
95
|
+
const client = getActiveClient();
|
|
96
|
+
// If a specific planet is requested, delegate directly.
|
|
97
|
+
if (args.planet_id != null) {
|
|
98
|
+
return await client.post("/ephemeris/retrograde-status", {
|
|
99
|
+
date_time: { iso: args.datetime },
|
|
100
|
+
planet_id: args.planet_id,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Fan-out: the backend only handles one planet per call; query all 10 in parallel.
|
|
104
|
+
const PLANET_IDS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
105
|
+
const results = await Promise.all(PLANET_IDS.map((pid) => client.post("/ephemeris/retrograde-status", {
|
|
92
106
|
date_time: { iso: args.datetime },
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
planet_id: pid,
|
|
108
|
+
}).catch(() => null)));
|
|
109
|
+
// Merge into a keyed object: { planet_name: {...status} }
|
|
110
|
+
const merged = {};
|
|
111
|
+
for (const res of results) {
|
|
112
|
+
if (res && typeof res === "object" && res.planet_name) {
|
|
113
|
+
const r = res;
|
|
114
|
+
merged[r.planet_name] = {
|
|
115
|
+
planet_id: r.planet_id,
|
|
116
|
+
is_retrograde: r.is_retrograde,
|
|
117
|
+
longitude_speed: r.longitude_speed,
|
|
118
|
+
motion_type: r.motion_type,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { planets: merged, datetime: args.datetime, success: true };
|
|
97
123
|
},
|
|
98
124
|
});
|
|
99
125
|
// POST /ephemeris/midpoints
|
|
@@ -150,7 +176,38 @@ registerTool({
|
|
|
150
176
|
body.star_names = args.star_names;
|
|
151
177
|
if (args.orb != null)
|
|
152
178
|
body.orb = args.orb;
|
|
153
|
-
|
|
179
|
+
const raw = await getActiveClient().post("/ephemeris/fixed-stars", body);
|
|
180
|
+
// Bug 6 workaround: backend returns longitude as degrees-within-sign (0–30),
|
|
181
|
+
// not full ecliptic longitude (0–360). Compute full_longitude from sign name.
|
|
182
|
+
const SIGN_ORDER = {
|
|
183
|
+
Aries: 0, Taurus: 1, Gemini: 2, Cancer: 3, Leo: 4, Virgo: 5,
|
|
184
|
+
Libra: 6, Scorpio: 7, Sagittarius: 8, Capricorn: 9, Aquarius: 10, Pisces: 11,
|
|
185
|
+
};
|
|
186
|
+
function enrichStar(star) {
|
|
187
|
+
if (!star || typeof star !== "object")
|
|
188
|
+
return star;
|
|
189
|
+
const signLon = typeof star.longitude === "number" ? star.longitude : 0;
|
|
190
|
+
const signName = star.sign ?? star.sign_name ?? "";
|
|
191
|
+
const signIndex = SIGN_ORDER[signName] ?? -1;
|
|
192
|
+
const fullLon = signIndex >= 0 ? signIndex * 30 + signLon : signLon;
|
|
193
|
+
return {
|
|
194
|
+
...star,
|
|
195
|
+
longitude: fullLon, // ecliptic longitude 0–360
|
|
196
|
+
sign_longitude: signLon, // original within-sign degrees preserved
|
|
197
|
+
sign: signName || undefined,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// Normalise the response — backend may return array or { stars: [...] }
|
|
201
|
+
if (Array.isArray(raw)) {
|
|
202
|
+
return raw.map(enrichStar);
|
|
203
|
+
}
|
|
204
|
+
if (raw && typeof raw === "object") {
|
|
205
|
+
const starsKey = ["stars", "data", "fixed_stars", "results"].find((k) => Array.isArray(raw[k]));
|
|
206
|
+
if (starsKey) {
|
|
207
|
+
return { ...raw, [starsKey]: raw[starsKey].map(enrichStar) };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return raw;
|
|
154
211
|
},
|
|
155
212
|
});
|
|
156
213
|
// POST /ephemeris/hermetic-lots
|
|
@@ -223,9 +280,10 @@ registerTool({
|
|
|
223
280
|
},
|
|
224
281
|
handler: async (args) => {
|
|
225
282
|
validateRequired(args, ["longitude_1", "longitude_2"]);
|
|
283
|
+
// Backend field names are longitude1 / longitude2 (no underscore)
|
|
226
284
|
const body = {
|
|
227
|
-
|
|
228
|
-
|
|
285
|
+
longitude1: args.longitude_1,
|
|
286
|
+
longitude2: args.longitude_2,
|
|
229
287
|
};
|
|
230
288
|
if (args.max_orb != null)
|
|
231
289
|
body.max_orb = args.max_orb;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { registerTool, validateRequired } from "../index.js";
|
|
2
|
+
import { getActiveClient } from "../../backend/client.js";
|
|
3
|
+
/** Append 'Z' if no timezone offset is present in the datetime string. */
|
|
4
|
+
function ensureTimezone(dt) {
|
|
5
|
+
if (/[Zz]$/.test(dt) || /[+-]\d{2}:\d{2}$/.test(dt))
|
|
6
|
+
return dt;
|
|
7
|
+
return dt + "Z";
|
|
8
|
+
}
|
|
9
|
+
registerTool({
|
|
10
|
+
name: "human_design_bodygraph",
|
|
11
|
+
description: "Generate a Human Design Bodygraph image (PNG or SVG) from a birth datetime (UTC). " +
|
|
12
|
+
"Renders the full bodygraph circuit board showing all 9 centers, 36 channels, and 64 gates " +
|
|
13
|
+
"with Personality (conscious) and Design (unconscious) activations color-coded. " +
|
|
14
|
+
"Defined centers are filled with their HD doctrine color; open/undefined centers remain muted. " +
|
|
15
|
+
"The tool returns a raw image (base64) which most Vision-enabled AI agents can display and visually read.\n\n" +
|
|
16
|
+
"CREDIT COST: 2 credits per call.\n\n" +
|
|
17
|
+
"EXAMPLE: Generate a bodygraph for someone born April 15, 1990 at 19:30 UTC:\n" +
|
|
18
|
+
" datetime='1990-04-15T19:30:00Z'",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
datetime: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "ISO 8601 birth datetime in UTC, e.g. '1990-04-15T19:30:00Z'. " +
|
|
25
|
+
"Human Design chart calculation is time-sensitive — accuracy to the minute matters.",
|
|
26
|
+
},
|
|
27
|
+
style: {
|
|
28
|
+
type: "string",
|
|
29
|
+
enum: ["light", "dark", "mono"],
|
|
30
|
+
description: "Aesthetic style of the bodygraph: 'dark' (default, deep background), " +
|
|
31
|
+
"'light' (cream background), or 'mono' (black-and-white print-ready).",
|
|
32
|
+
},
|
|
33
|
+
format: {
|
|
34
|
+
type: "string",
|
|
35
|
+
enum: ["png", "svg"],
|
|
36
|
+
description: "Output format. Defaults to 'png'.",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ["datetime"],
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
},
|
|
42
|
+
handler: async (args) => {
|
|
43
|
+
validateRequired(args, ["datetime"]);
|
|
44
|
+
const body = {
|
|
45
|
+
birth_datetime_utc: ensureTimezone(args.datetime),
|
|
46
|
+
};
|
|
47
|
+
const format = args.format || "png";
|
|
48
|
+
const styleParam = args.style ? `&style=${args.style}` : "";
|
|
49
|
+
return await getActiveClient().post(`/visualization/bodygraph?format=${format}&size=800${styleParam}`, body);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -36,8 +36,9 @@ registerTool({
|
|
|
36
36
|
handler: async (args) => {
|
|
37
37
|
validateCoordinates(args, "latitude", "longitude");
|
|
38
38
|
const params = {};
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// Backend requires datetime even though the schema marks it optional.
|
|
40
|
+
// Default to current UTC time when caller omits it.
|
|
41
|
+
params.datetime = args.datetime ?? new Date().toISOString();
|
|
41
42
|
if (args.latitude != null)
|
|
42
43
|
params.latitude = args.latitude;
|
|
43
44
|
if (args.longitude != null)
|
|
@@ -109,10 +110,23 @@ registerTool({
|
|
|
109
110
|
const result = await getActiveClient().request("GET", "/calendar/astrology/moon-phases", {
|
|
110
111
|
params: { start_date: startDate, end_date: endDate },
|
|
111
112
|
});
|
|
112
|
-
// Normalise response shape —
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
// Normalise response shape — defensively handle every known API envelope shape.
|
|
114
|
+
// Crash "allPhases.filter is not a function" occurs when a property like
|
|
115
|
+
// result.data is truthy but is an object, not an array.
|
|
116
|
+
function extractArray(val) {
|
|
117
|
+
if (Array.isArray(val))
|
|
118
|
+
return val;
|
|
119
|
+
if (val && typeof val === "object") {
|
|
120
|
+
// one level deeper: { phases: [...] }, { data: [...] }, etc.
|
|
121
|
+
for (const key of ["phases", "data", "events", "moon_phases", "results", "items"]) {
|
|
122
|
+
const nested = val[key];
|
|
123
|
+
if (Array.isArray(nested))
|
|
124
|
+
return nested;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const allPhases = extractArray(result) ?? [];
|
|
116
130
|
const matching = allPhases
|
|
117
131
|
.filter((p) => {
|
|
118
132
|
const name = (p.phase_name ?? p.name ?? p.phase ?? "").toLowerCase();
|
|
@@ -2,7 +2,7 @@ import { registerTool, validateRequired } from "../index.js";
|
|
|
2
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
|
-
/** Map human-readable house system names to
|
|
5
|
+
/** Map human-readable house system names to standard single-letter codes */
|
|
6
6
|
const HOUSE_SYSTEM_MAP = {
|
|
7
7
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
8
8
|
campanus: "C", regiomontanus: "R", porphyry: "O",
|
|
@@ -127,9 +127,9 @@ registerTool({
|
|
|
127
127
|
"Uranus returns (~84 years), etc.\n\n" +
|
|
128
128
|
"CREDIT COST: 5 credits per call.\n\n" +
|
|
129
129
|
"EXAMPLE: Saturn return for birth 1990-04-15 near year 2019:\n" +
|
|
130
|
-
" body='saturn', birth_datetime='1990-04-15T14:30:
|
|
130
|
+
" body='saturn', birth_datetime='1990-04-15T14:30:00Z', target_datetime='2019-01-01T00:00:00Z'\n" +
|
|
131
131
|
"EXAMPLE: Chiron return for birth 1975-03-10 near age 50:\n" +
|
|
132
|
-
" body='chiron', birth_datetime='1975-03-10T08:00:
|
|
132
|
+
" body='chiron', birth_datetime='1975-03-10T08:00:00Z', target_datetime='2025-01-01T00:00:00Z'",
|
|
133
133
|
inputSchema: {
|
|
134
134
|
type: "object",
|
|
135
135
|
properties: {
|
|
@@ -145,16 +145,33 @@ registerTool({
|
|
|
145
145
|
type: "string",
|
|
146
146
|
description: "Date/time near which to find the return (ISO 8601). MUST include offset or Z. Required.",
|
|
147
147
|
},
|
|
148
|
+
birth_latitude: {
|
|
149
|
+
type: "number",
|
|
150
|
+
description: "Birth latitude in decimal degrees. Required to get a full return chart (house cusps, angles).",
|
|
151
|
+
},
|
|
152
|
+
birth_longitude: {
|
|
153
|
+
type: "number",
|
|
154
|
+
description: "Birth longitude in decimal degrees. Required to get a full return chart (house cusps, angles).",
|
|
155
|
+
},
|
|
148
156
|
},
|
|
149
157
|
required: ["body", "birth_datetime", "target_datetime"],
|
|
150
158
|
additionalProperties: false,
|
|
151
159
|
},
|
|
152
160
|
handler: async (args) => {
|
|
153
161
|
validateRequired(args, ["body", "birth_datetime", "target_datetime"]);
|
|
154
|
-
|
|
162
|
+
const requestBody = {
|
|
155
163
|
body: args.body,
|
|
156
164
|
birth_datetime: { iso: args.birth_datetime },
|
|
157
165
|
target_datetime: { iso: args.target_datetime },
|
|
158
|
-
}
|
|
166
|
+
};
|
|
167
|
+
// Forward birth coordinates so the backend can compute the full return chart.
|
|
168
|
+
// Without these, computeReturnChartData sees {lat:0, lon:0} and returns nil.
|
|
169
|
+
if (args.birth_latitude != null && args.birth_longitude != null) {
|
|
170
|
+
requestBody.location = {
|
|
171
|
+
latitude: { decimal: args.birth_latitude },
|
|
172
|
+
longitude: { decimal: args.birth_longitude },
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return await getActiveClient().post("/predictive/returns", requestBody);
|
|
159
176
|
},
|
|
160
177
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openephemeris/mcp-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.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",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"llm",
|
|
53
53
|
"claude",
|
|
54
54
|
"ai",
|
|
55
|
-
"
|
|
55
|
+
"ephemeris"
|
|
56
56
|
],
|
|
57
57
|
"author": "Open Ephemeris",
|
|
58
58
|
"homepage": "https://openephemeris.com",
|