@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 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 Swiss Ephemeris path instead of simplified approximation
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 Swiss Ephemeris lookups). OpenEphemeris serves as a **secure, remote math engine**.
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-22T01:16:37.771Z",
112
- "openapi_sha256": "c649b6a9ba32e1576f9f12d2d78f1706d5bc279d7733a29f1e69a8ca042024d6",
111
+ "last_generated_at": "2026-04-22T15:19:21.508Z",
112
+ "openapi_sha256": "0c29ad2e37fd08d48800ec54059e262fbbdb681b844e00fed9274611bae678ac",
113
113
  "candidates_get": [
114
114
  {
115
115
  "method": "GET",
@@ -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 { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
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({ ok: true, version, transport: "sse", sessions: transports.size });
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.query.apiKey ||
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 Ela Map for geographic rendering.",
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 Swiss Ephemeris single-letter codes */
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.8.0",
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
- "swiss ephemeris"
55
+ "ephemeris"
56
56
  ],
57
57
  "author": "Open Ephemeris",
58
58
  "homepage": "https://openephemeris.com",