@openephemeris/mcp-server 3.8.0 → 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 +22 -1
- package/README.md +1 -1
- package/config/dev-allowlist.json +2 -2
- package/dist/server-sse.js +161 -9
- package/dist/tools/specialized/acg.js +1 -1
- package/dist/tools/specialized/natal.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ 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
|
+
|
|
10
31
|
## [3.8.0] — 2026-04-21
|
|
11
32
|
|
|
12
33
|
### Added
|
|
@@ -96,7 +117,7 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
96
117
|
|
|
97
118
|
|
|
98
119
|
### Fixed
|
|
99
|
-
- 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
|
|
100
121
|
- SSE server chart wheel responses now correctly deliver native MCP image blocks instead of raw base64 JSON
|
|
101
122
|
|
|
102
123
|
### Changed
|
package/README.md
CHANGED
|
@@ -244,7 +244,7 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
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.
|
|
@@ -108,8 +108,8 @@
|
|
|
108
108
|
"path": "/visualization/chart-wheel"
|
|
109
109
|
}
|
|
110
110
|
],
|
|
111
|
-
"last_generated_at": "2026-04-
|
|
112
|
-
"openapi_sha256": "
|
|
111
|
+
"last_generated_at": "2026-04-22T15:19:21.508Z",
|
|
112
|
+
"openapi_sha256": "0c29ad2e37fd08d48800ec54059e262fbbdb681b844e00fed9274611bae678ac",
|
|
113
113
|
"candidates_get": [
|
|
114
114
|
{
|
|
115
115
|
"method": "GET",
|
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",
|
|
@@ -87,7 +87,7 @@ registerTool({
|
|
|
87
87
|
return {
|
|
88
88
|
type: "acg_power_lines_summary",
|
|
89
89
|
total_lines: condensed.length,
|
|
90
|
-
note: "Coordinate geometry stripped for LLM context efficiency. Use the full API or
|
|
90
|
+
note: "Coordinate geometry stripped for LLM context efficiency. Use the full API or a map rendering client for geographic visualization.",
|
|
91
91
|
lines: condensed,
|
|
92
92
|
};
|
|
93
93
|
}
|
|
@@ -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",
|
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",
|