@openephemeris/mcp-server 3.8.0 → 3.9.1

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
@@ -1,11 +1,115 @@
1
1
  # OpenEphemeris MCP Server
2
2
 
3
+ [![smithery badge](https://smithery.ai/badge/open-ephemeris/openephemeris)](https://smithery.ai/servers/open-ephemeris/openephemeris)
4
+ [![npm version](https://img.shields.io/npm/v/@openephemeris/mcp-server)](https://www.npmjs.com/package/@openephemeris/mcp-server)
3
5
  [![System Status](https://img.shields.io/badge/Status-Operational-brightgreen)](https://status.openephemeris.com/)
4
6
 
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.
7
+ Model Context Protocol server for OpenEphemeris 50+ typed astrology tools powered by NASA JPL DE440/DE441 ephemerides. Zero hallucination on planetary positions, dates, and degrees.
8
+
9
+ **Hosted endpoint:** `https://mcp.openephemeris.com/mcp` (Streamable HTTP, MCP 2025-11-25 spec)
6
10
 
7
11
  ## Quick Start
8
12
 
13
+ ### Install via Smithery (recommended)
14
+
15
+ The fastest way to connect any MCP-compatible client:
16
+
17
+ ```bash
18
+ npx -y @smithery/cli install @open-ephemeris/openephemeris --client claude
19
+ ```
20
+
21
+ Or browse the listing and copy connection snippets: **[smithery.ai/servers/open-ephemeris/openephemeris](https://smithery.ai/servers/open-ephemeris/openephemeris)**
22
+
23
+ ---
24
+
25
+ ### Connect via AI SDK (Vercel AI SDK)
26
+
27
+ ```typescript
28
+ import Smithery from "@smithery/api"
29
+ import { createMCPClient } from "@ai-sdk/mcp"
30
+ import { generateText } from "ai"
31
+ import { anthropic } from "@ai-sdk/anthropic"
32
+ import { createConnection } from "@smithery/api/mcp"
33
+
34
+ const smithery = new Smithery()
35
+
36
+ const conn = await smithery.connections.create("{your-namespace}", {
37
+ mcpUrl: "https://server.smithery.ai/open-ephemeris/openephemeris",
38
+ headers: {
39
+ apiKey: "your-openephemeris-api-key", // get one free at openephemeris.com/dashboard
40
+ },
41
+ })
42
+
43
+ const { transport } = await createConnection({
44
+ client: smithery,
45
+ namespace: "{your-namespace}",
46
+ connectionId: conn.connectionId,
47
+ })
48
+
49
+ const mcpClient = await createMCPClient({ transport })
50
+ const tools = await mcpClient.tools()
51
+
52
+ const { text } = await generateText({
53
+ model: anthropic("claude-sonnet-4-20250514"),
54
+ tools,
55
+ prompt: "Calculate a natal chart for someone born April 15, 1990 at 2:30 PM in Chicago.",
56
+ })
57
+
58
+ await mcpClient.close()
59
+ ```
60
+
61
+ ### Connect via MCP SDK (TypeScript)
62
+
63
+ ```typescript
64
+ import Smithery from "@smithery/api"
65
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js"
66
+ import { createConnection } from "@smithery/api/mcp"
67
+
68
+ const smithery = new Smithery()
69
+
70
+ const conn = await smithery.connections.create("{your-namespace}", {
71
+ mcpUrl: "https://server.smithery.ai/open-ephemeris/openephemeris",
72
+ headers: {
73
+ apiKey: "your-openephemeris-api-key",
74
+ },
75
+ })
76
+
77
+ const { transport } = await createConnection({
78
+ client: smithery,
79
+ namespace: "{your-namespace}",
80
+ connectionId: conn.connectionId,
81
+ })
82
+
83
+ const mcpClient = new Client(
84
+ { name: "my-app", version: "1.0.0" },
85
+ { capabilities: {} }
86
+ )
87
+ await mcpClient.connect(transport)
88
+
89
+ const { tools } = await mcpClient.listTools()
90
+ const result = await mcpClient.callTool({
91
+ name: "ephemeris_natal_chart",
92
+ arguments: { datetime: "1990-04-15T14:30:00", latitude: 41.8781, longitude: -87.6298, format: "llm" },
93
+ })
94
+ ```
95
+
96
+ ### Connect directly (Streamable HTTP, no Smithery)
97
+
98
+ ```typescript
99
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
100
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js"
101
+
102
+ const transport = new StreamableHTTPClientTransport(
103
+ new URL("https://mcp.openephemeris.com/mcp"),
104
+ { requestInit: { headers: { "X-API-Key": "your-openephemeris-api-key" } } }
105
+ )
106
+
107
+ const client = new Client({ name: "my-app", version: "1.0.0" }, { capabilities: {} })
108
+ await client.connect(transport)
109
+ ```
110
+
111
+ ---
112
+
9
113
  ### One-click install (Cursor)
10
114
 
11
115
  <!-- GENERATED:CURSOR_INSTALL:BEGIN -->
@@ -54,10 +158,12 @@ Cursor deeplink payload:
54
158
 
55
159
  | Client | Install mode | Config location |
56
160
  |---|---|---|
161
+ | Smithery | One-click | [smithery.ai](https://smithery.ai/servers/open-ephemeris/openephemeris) |
57
162
  | Cursor | One-click deeplink or manual | `~/.cursor/mcp.json` |
58
163
  | Claude Desktop (macOS) | Manual | `~/Library/Application Support/Claude/claude_desktop_config.json` |
59
164
  | Claude Desktop (Windows) | Manual | `%APPDATA%\Claude\claude_desktop_config.json` |
60
165
  | Windsurf | Manual | `~/.codeium/windsurf/mcp_config.json` (or legacy `~/.codeium/mcp_config.json`) |
166
+ | Claude Web / ChatGPT / remote clients | Hosted URL | `https://mcp.openephemeris.com/mcp` |
61
167
 
62
168
  ### Client install walkthroughs
63
169
 
@@ -73,9 +179,13 @@ Cursor deeplink payload:
73
179
  - Add the `mcpServers.openephemeris` block from "Manual install".
74
180
  - Restart Windsurf.
75
181
 
76
- ### Remote-only clients (ChatGPT Developer Mode, Antigravity, etc.)
182
+ ### Remote-only clients (Claude Web, ChatGPT, etc.)
183
+
184
+ The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable HTTP support (MCP 2025-11-25 spec). Remote-only clients can connect directly — no bridge/proxy required:
77
185
 
78
- This package runs as a local stdio MCP server. Remote-only clients require a hosted MCP endpoint (Streamable HTTP/SSE), so you must run this server behind an MCP bridge/proxy and expose it over HTTPS before adding it in those clients.
186
+ - **Claude Web**: Add `https://mcp.openephemeris.com/mcp` as a remote MCP server with `X-API-Key: your-key` header
187
+ - **Via Smithery**: Use the [Smithery listing](https://smithery.ai/servers/open-ephemeris/openephemeris) for managed connections with any client
188
+ - **Legacy SSE**: `https://mcp.openephemeris.com/sse` remains available for SSE-only clients
79
189
 
80
190
  ### Auth and upgrade behavior in MCP clients
81
191
 
@@ -209,14 +319,32 @@ When you update the MCP server logic (handlers, bug fixes, hardening), you shoul
209
319
  ## Architecture
210
320
 
211
321
  ```text
212
- MCP Client (Cursor / Claude / Windsurf / other stdio clients)
213
- | stdio JSON-RPC
214
- v
215
- openephemeris-mcp (Node.js)
216
- | typed tools + dev.call/dev.list_allowed
217
- | auth chain: Service Key > API Key > JWT
218
- v
219
- OpenEphemeris API
322
+ ┌─────────────────────────────────────────────────────────┐
323
+ │ MCP Clients │
324
+ │ Smithery Gateway · Claude Web · ChatGPT · Remote apps │
325
+ └──────────────────┬──────────────────────────────────────┘
326
+ Streamable HTTP (MCP 2025-11-25)
327
+ https://mcp.openephemeris.com/mcp
328
+
329
+ ┌─────────────────────────────────────────────────────────┐
330
+ │ Cursor · Claude Desktop · Windsurf │
331
+ └──────────────────┬──────────────────────────────────────┘
332
+ │ stdio JSON-RPC
333
+ │ npx @openephemeris/mcp-server
334
+
335
+ ┌────▼────────────────────┐
336
+ │ openephemeris-mcp │
337
+ │ Node.js MCP Server │
338
+ │ 52 typed tools │
339
+ │ auth: Key > JWT │
340
+ └────────────┬────────────┘
341
+ │ HTTPS
342
+
343
+ ┌────────────────────────┐
344
+ │ OpenEphemeris API │
345
+ │ api.openephemeris.com │
346
+ │ JPL DE440/DE441 │
347
+ └────────────────────────┘
220
348
  ```
221
349
 
222
350
  <!-- GENERATED:RUNTIME_SNAPSHOT:BEGIN -->
@@ -244,7 +372,7 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
244
372
 
245
373
  ## Why OpenEphemeris for AI Agents?
246
374
 
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**.
375
+ 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
376
 
249
377
  By pairing LLMs with the OpenEphemeris MCP server, your agents can instantly access:
250
378
  - **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, ListPromptsRequestSchema, GetPromptRequestSchema, 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,32 +78,140 @@ 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
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // Helpers
97
+ // ---------------------------------------------------------------------------
98
+ /** Convert snake_case tool name → Human Title (e.g. "ephemeris_natal_chart" → "Natal Chart") */
99
+ function humanTitle(name) {
100
+ // Strip common prefixes that don't add value in a title context
101
+ const stripped = name
102
+ .replace(/^ephemeris_/, "")
103
+ .replace(/^acg_/, "ACG ")
104
+ .replace(/^hd_/, "Human Design ")
105
+ .replace(/^human_design_/, "Human Design ")
106
+ .replace(/^electional_/, "Electional ")
107
+ .replace(/^venus_/, "Venus ")
108
+ .replace(/^vedic_/, "Vedic ")
109
+ .replace(/^bazi_/, "BaZi ")
110
+ .replace(/^chinese_/, "Chinese ");
111
+ return stripped
112
+ .split("_")
113
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
114
+ .join(" ");
115
+ }
116
+ /** Build MCP-spec tool annotations for a tool */
117
+ function buildAnnotations(tool) {
118
+ return {
119
+ title: humanTitle(tool.name),
120
+ readOnlyHint: true,
121
+ destructiveHint: false,
122
+ idempotentHint: true,
123
+ openWorldHint: false,
124
+ ...(tool.annotations ?? {}),
125
+ };
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // Welcome prompt — orientation guide for LLMs connecting via MCP
79
129
  // ---------------------------------------------------------------------------
80
- // MCP Server factory — one per SSE session
130
+ const WELCOME_PROMPT = {
131
+ name: "welcome_to_open_ephemeris",
132
+ description: "An orientation guide that introduces the Open Ephemeris MCP server, " +
133
+ "its capabilities, and how to use its tools effectively. " +
134
+ "Invoke this prompt when a user first connects or asks 'what can you do?'",
135
+ arguments: [],
136
+ };
137
+ const WELCOME_PROMPT_CONTENT = [
138
+ {
139
+ role: "assistant",
140
+ content: {
141
+ type: "text",
142
+ text: "🌟 **Welcome to Open Ephemeris** — a NASA JPL-backed astronomical computation engine.\n\n" +
143
+ "I have access to 50+ precision tools covering:\n\n" +
144
+ "• **Natal Charts** — full planetary positions, house cusps, aspects, dignities\n" +
145
+ "• **Transits & Progressions** — real-time and secondary progressions\n" +
146
+ "• **Synastry & Composites** — relationship compatibility analysis\n" +
147
+ "• **Human Design** — bodygraph charts, gates, channels, types & profiles\n" +
148
+ "• **Astrocartography (ACG)** — power lines, city hits, local space lines\n" +
149
+ "• **Eclipses** — solar/lunar eclipse lookup with visibility data\n" +
150
+ "• **Vedic/Jyotish** — D-charts, dashas, nakshatras, Ashtakavarga\n" +
151
+ "• **Chinese BaZi** — Four Pillars, Ten Gods, Luck Pillars\n" +
152
+ "• **Venus Star Points** — 8-year Venus cycle phases\n" +
153
+ "• **Electional Astrology** — station tracking, timing windows\n" +
154
+ "• **Chart Wheels** — visual PNG/SVG wheels and bi-wheels\n" +
155
+ "• **Time Lords** — Firdaria, Profections, Zodiacal Releasing\n\n" +
156
+ "**Tips:** Use `format=\"llm\"` on any tool to get ~50% smaller, token-optimized output. " +
157
+ "All computations are sub-arcsecond precision using JPL DE440/DE441 ephemerides.\n\n" +
158
+ "Just tell me what you'd like to explore!",
159
+ },
160
+ },
161
+ ];
162
+ // ---------------------------------------------------------------------------
163
+ // MCP Server factory — one per SSE/HTTP session
81
164
  // ---------------------------------------------------------------------------
82
165
  function createMcpServer() {
83
166
  const server = new Server({
84
167
  name: "openephemeris-mcp",
168
+ title: "Open Ephemeris",
85
169
  version,
170
+ websiteUrl: "https://openephemeris.com",
86
171
  icons: [
87
172
  {
88
173
  src: "https://mcp.openephemeris.com/icon.png",
89
174
  mimeType: "image/png",
90
175
  }
91
- ]
92
- }, { capabilities: { tools: {} } });
176
+ ],
177
+ // description is in the MCP spec (spec.types.d.ts Implementation interface)
178
+ // but the SDK's Zod schema doesn't include it yet — pass via spread
179
+ ...{
180
+ description: "NASA JPL-backed astronomical computation engine for AI agents. " +
181
+ "50+ typed tools covering natal charts, transit forecasting, Human Design " +
182
+ "bodygraphs, eclipses, astrocartography power lines, Venus Star Points, " +
183
+ "electional timing, synastry, composite charts, Vedic/Jyotish, Chinese BaZi, " +
184
+ "and more — powered by JPL DE440/DE441 ephemerides for sub-arcsecond accuracy.",
185
+ },
186
+ }, {
187
+ capabilities: { tools: {}, prompts: {} },
188
+ instructions: "Open Ephemeris is a precision astronomical computation engine. " +
189
+ "Use format='llm' on any tool for compact, token-efficient output. " +
190
+ "All computations use JPL DE440/DE441 ephemerides for sub-arcsecond accuracy. " +
191
+ "See the 'welcome_to_open_ephemeris' prompt for a full capability overview.",
192
+ });
193
+ // --- Tool handlers ---
93
194
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
94
195
  tools: Object.values(toolRegistry).map((tool) => ({
95
196
  name: tool.name,
96
197
  description: tool.description,
97
198
  inputSchema: tool.inputSchema,
98
- annotations: tool.annotations ?? {
99
- title: tool.name,
100
- readOnlyHint: true,
101
- destructiveHint: false,
102
- },
199
+ annotations: buildAnnotations(tool),
103
200
  })),
104
201
  }));
202
+ // --- Prompt handlers ---
203
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
204
+ prompts: [WELCOME_PROMPT],
205
+ }));
206
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
207
+ if (request.params.name !== WELCOME_PROMPT.name) {
208
+ throw new Error(`Unknown prompt: ${request.params.name}`);
209
+ }
210
+ return {
211
+ description: WELCOME_PROMPT.description,
212
+ messages: WELCOME_PROMPT_CONTENT,
213
+ };
214
+ });
105
215
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
106
216
  const toolName = request.params.name;
107
217
  const tool = toolRegistry[toolName];
@@ -132,9 +242,7 @@ function createMcpServer() {
132
242
  });
133
243
  return server;
134
244
  }
135
- // ---------------------------------------------------------------------------
136
- // Express + SSE wiring
137
- // ---------------------------------------------------------------------------
245
+ const httpSessions = new Map();
138
246
  async function main() {
139
247
  await initTools();
140
248
  const app = express();
@@ -144,8 +252,8 @@ async function main() {
144
252
  // CORS — required for Claude Web cross-origin SSE connections
145
253
  app.use((_req, res, next) => {
146
254
  res.setHeader("Access-Control-Allow-Origin", "*");
147
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
148
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-OpenEphemeris-API-Key");
255
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
256
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-OpenEphemeris-API-Key, mcp-session-id");
149
257
  if (_req.method === "OPTIONS") {
150
258
  res.status(204).end();
151
259
  return;
@@ -158,7 +266,149 @@ async function main() {
158
266
  const sessionClients = new Map();
159
267
  // Health check
160
268
  app.get("/health", (_req, res) => {
161
- res.json({ ok: true, version, transport: "sse", sessions: transports.size });
269
+ res.json({
270
+ ok: true,
271
+ version,
272
+ transports: ["sse", "streamable-http"],
273
+ sse_sessions: transports.size,
274
+ http_sessions: httpSessions.size,
275
+ });
276
+ });
277
+ // ---------------------------------------------------------------------------
278
+ // Static server card — lets Smithery (and other registries) scan tools
279
+ // without needing an authenticated session. Updated on every deploy.
280
+ // Spec: https://smithery.ai/docs/build/publish#server-scanning
281
+ // ---------------------------------------------------------------------------
282
+ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
283
+ const tools = Object.values(toolRegistry).map((tool) => ({
284
+ name: tool.name,
285
+ description: tool.description,
286
+ inputSchema: tool.inputSchema,
287
+ annotations: buildAnnotations(tool),
288
+ }));
289
+ res.json({
290
+ serverInfo: {
291
+ name: "Open Ephemeris",
292
+ version,
293
+ description: "NASA JPL-backed astronomical computation engine for AI agents. 50+ typed tools covering " +
294
+ "natal charts, transit forecasting, Human Design bodygraphs, eclipses, astrocartography " +
295
+ "power lines, Venus Star Points, electional timing, synastry, composite charts, Vedic/Jyotish, " +
296
+ "Chinese BaZi, and more — powered by JPL DE440/DE441 ephemerides for sub-arcsecond " +
297
+ "zero-hallucination accuracy. Free Explorer tier available.",
298
+ iconUrl: "https://mcp.openephemeris.com/icon.png",
299
+ homepage: "https://openephemeris.com",
300
+ },
301
+ authentication: {
302
+ required: true,
303
+ schemes: ["apiKey"],
304
+ instructions: "Pass your Open Ephemeris API key via the X-API-Key header. " +
305
+ "Get a free Explorer key at https://openephemeris.com/dashboard — no credit card required.",
306
+ },
307
+ tools,
308
+ resources: [],
309
+ prompts: [WELCOME_PROMPT],
310
+ });
311
+ });
312
+ // ---------------------------------------------------------------------------
313
+ // Streamable HTTP transport — MCP spec 2025-11-25
314
+ // Endpoint: POST/GET/DELETE https://mcp.openephemeris.com/mcp
315
+ // Clients send X-API-Key (or Authorization: Bearer) on the init request.
316
+ // Session ID is managed via mcp-session-id response header.
317
+ // ---------------------------------------------------------------------------
318
+ // Parse JSON bodies only for /mcp routes (keeps SSE routes safe)
319
+ const jsonParser = express.json({ limit: "4mb" });
320
+ // POST /mcp — initialize a new session OR resume an existing one
321
+ app.post("/mcp", jsonParser, async (req, res) => {
322
+ const sessionId = req.headers["mcp-session-id"];
323
+ // Resume existing session
324
+ if (sessionId) {
325
+ const session = httpSessions.get(sessionId);
326
+ if (!session) {
327
+ res.status(404).json({
328
+ error: "session_not_found",
329
+ message: "Unknown or expired MCP session. Re-initialize to start a new one.",
330
+ });
331
+ return;
332
+ }
333
+ await runWithClient(session.client, () => session.transport.handleRequest(req, res, req.body));
334
+ return;
335
+ }
336
+ // New session — must be an initialize request
337
+ if (!isInitializeRequest(req.body)) {
338
+ res.status(400).json({
339
+ error: "expected_initialize",
340
+ message: "First request to /mcp must be an MCP Initialize request.",
341
+ });
342
+ return;
343
+ }
344
+ // Auth — required on Initialize
345
+ const apiKey = extractApiKey(req);
346
+ if (!apiKey) {
347
+ res.status(401).json({
348
+ error: "api_key_required",
349
+ 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",
350
+ });
351
+ return;
352
+ }
353
+ const valid = await validateApiKey(apiKey);
354
+ if (!valid) {
355
+ res.status(403).json({
356
+ error: "invalid_api_key",
357
+ message: "The provided API key is invalid or inactive. Check your key at https://openephemeris.com/dashboard?tab=apikeys",
358
+ });
359
+ return;
360
+ }
361
+ // Create per-session transport, server, and client
362
+ const transport = new StreamableHTTPServerTransport({
363
+ sessionIdGenerator: randomUUID,
364
+ onsessioninitialized: (id) => {
365
+ httpSessions.set(id, { server, transport, client });
366
+ console.error(`[HTTP] Session initialized: ${id}`);
367
+ },
368
+ });
369
+ const client = new BackendClient({ baseURL: BACKEND_URL, apiKey });
370
+ const server = createMcpServer();
371
+ transport.onclose = () => {
372
+ if (transport.sessionId) {
373
+ httpSessions.delete(transport.sessionId);
374
+ console.error(`[HTTP] Session closed: ${transport.sessionId}`);
375
+ }
376
+ };
377
+ await server.connect(transport);
378
+ await runWithClient(client, () => transport.handleRequest(req, res, req.body));
379
+ });
380
+ // GET /mcp — SSE stream leg of Streamable HTTP (server → client events)
381
+ app.get("/mcp", async (req, res) => {
382
+ const sessionId = req.headers["mcp-session-id"];
383
+ if (!sessionId) {
384
+ res.status(400).json({
385
+ error: "session_id_required",
386
+ message: "mcp-session-id header is required for GET /mcp.",
387
+ });
388
+ return;
389
+ }
390
+ const session = httpSessions.get(sessionId);
391
+ if (!session) {
392
+ res.status(404).json({
393
+ error: "session_not_found",
394
+ message: "Unknown or expired MCP session.",
395
+ });
396
+ return;
397
+ }
398
+ await session.transport.handleRequest(req, res);
399
+ });
400
+ // DELETE /mcp — client-initiated session teardown
401
+ app.delete("/mcp", (req, res) => {
402
+ const sessionId = req.headers["mcp-session-id"];
403
+ if (sessionId) {
404
+ const session = httpSessions.get(sessionId);
405
+ if (session) {
406
+ httpSessions.delete(sessionId);
407
+ session.server.close().catch(() => { });
408
+ console.error(`[HTTP] Session deleted: ${sessionId}`);
409
+ }
410
+ }
411
+ res.status(204).end();
162
412
  });
163
413
  // Serve the MCP icon (earth + star)
164
414
  // Compiled JS at dist/src/server-sse.js: ../../assets
@@ -188,13 +438,15 @@ async function main() {
188
438
  res.status(404).end();
189
439
  }
190
440
  });
441
+ // ---------------------------------------------------------------------------
442
+ // Legacy SSE transport — MCP spec pre-2025 (kept for backward compatibility)
443
+ // Endpoint: GET https://mcp.openephemeris.com/sse
444
+ // New integrations should prefer /mcp (Streamable HTTP above).
445
+ // ---------------------------------------------------------------------------
191
446
  // SSE endpoint — clients GET /sse to establish a stream
192
447
  app.get("/sse", async (req, res) => {
193
448
  // 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, "");
449
+ const apiKey = extractApiKey(req);
198
450
  if (!apiKey) {
199
451
  res.status(401).json({
200
452
  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.1",
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",