@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 +22 -1
- package/README.md +140 -12
- package/config/dev-allowlist.json +2 -2
- package/dist/server-sse.js +271 -19
- 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
|
@@ -1,11 +1,115 @@
|
|
|
1
1
|
# OpenEphemeris MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://smithery.ai/servers/open-ephemeris/openephemeris)
|
|
4
|
+
[](https://www.npmjs.com/package/@openephemeris/mcp-server)
|
|
3
5
|
[](https://status.openephemeris.com/)
|
|
4
6
|
|
|
5
|
-
Model Context Protocol server for OpenEphemeris
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
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-
|
|
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, 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
|
-
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
|
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
|
|
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.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
|
-
"
|
|
55
|
+
"ephemeris"
|
|
56
56
|
],
|
|
57
57
|
"author": "Open Ephemeris",
|
|
58
58
|
"homepage": "https://openephemeris.com",
|