@openephemeris/mcp-server 3.23.1 → 4.0.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 +105 -0
- package/LICENSE +21 -21
- package/README.md +52 -3
- package/dist/analytics.js +37 -5
- package/dist/backend/client.d.ts +7 -0
- package/dist/backend/client.js +39 -38
- package/dist/index.js +64 -2
- package/dist/oauth/session-utils.d.ts +42 -18
- package/dist/oauth/session-utils.js +79 -0
- package/dist/server-sse.js +114 -14
- package/dist/tools/apps/bazi-app.js +12 -26
- package/dist/tools/apps/bi-wheel-app.js +2 -2
- package/dist/tools/apps/bodygraph-app.d.ts +5 -5
- package/dist/tools/apps/bodygraph-app.js +187 -225
- package/dist/tools/apps/chart-wheel-app.js +5 -3
- package/dist/tools/apps/location-tools.js +167 -18
- package/dist/tools/apps/moon-phase-app.js +10 -3
- package/dist/tools/apps/transit-timeline-app.js +6 -4
- package/dist/tools/apps/vedic-chart-app.js +15 -49
- package/dist/tools/datetime.d.ts +65 -0
- package/dist/tools/datetime.js +153 -0
- package/dist/tools/index.d.ts +45 -2
- package/dist/tools/index.js +78 -2
- package/dist/tools/specialized/account.d.ts +1 -0
- package/dist/tools/specialized/account.js +100 -0
- package/dist/tools/specialized/acg.js +16 -14
- package/dist/tools/specialized/bazi.d.ts +7 -1
- package/dist/tools/specialized/bazi.js +86 -19
- package/dist/tools/specialized/bi_wheel.js +5 -4
- package/dist/tools/specialized/chart_wheel.js +5 -8
- package/dist/tools/specialized/comparative.js +13 -5
- package/dist/tools/specialized/electional.js +7 -7
- package/dist/tools/specialized/ephemeris_core.js +13 -8
- package/dist/tools/specialized/ephemeris_extended.js +27 -17
- package/dist/tools/specialized/hd_bodygraph.js +8 -10
- package/dist/tools/specialized/hd_cycles.js +7 -14
- package/dist/tools/specialized/hd_group.js +20 -13
- package/dist/tools/specialized/human_design.js +11 -17
- package/dist/tools/specialized/moon.js +8 -2
- package/dist/tools/specialized/natal.js +7 -9
- package/dist/tools/specialized/progressed.js +12 -8
- package/dist/tools/specialized/relocation.js +9 -3
- package/dist/tools/specialized/returns.js +23 -11
- package/dist/tools/specialized/synastry.js +17 -6
- package/dist/tools/specialized/transits.js +9 -5
- package/dist/tools/specialized/vedic.js +5 -3
- package/dist/tools/specialized/venus_star_points.js +14 -9
- package/dist/ui/bazi.html +1063 -1049
- package/dist/ui/bi-wheel.html +4197 -4120
- package/dist/ui/bodygraph.html +3855 -3720
- package/dist/ui/chart-wheel.html +3779 -3706
- package/dist/ui/moon-phase.html +3228 -3145
- package/dist/ui/transit-timeline.html +199 -170
- package/dist/ui/vedic-chart.html +1116 -1098
- package/package.json +6 -3
- package/smithery.yaml +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,111 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [4.0.0] — 2026-07-26
|
|
11
|
+
|
|
12
|
+
**Breaking.** A datetime that states a clock time without a zone is now rejected instead of silently assumed to be UTC. Callers who relied on the old behaviour were receiving charts computed for the wrong instant, so the break is the fix — but it is a break, and it is why this is a major version.
|
|
13
|
+
|
|
14
|
+
This release also carries everything from 3.26.0, which was written up but never published to npm (`latest` was 3.24.0). Nothing is lost; the 3.26.0 entry below is part of what ships here.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A birth time without a timezone was silently read as UTC, producing the wrong houses and angles.** This is the headline fix, and it is a correctness bug rather than a cosmetic one.
|
|
19
|
+
|
|
20
|
+
Given `datetime='1987-07-15T09:01:00'` with Dallas coordinates — 9:01 AM local, which is 14:01 UTC in July — the API computed the chart for 09:01 **UTC**: five hours early. Every planet keeps its sign at that error scale, so the output passed casual inspection, while the Ascendant landed in Gemini instead of Leo (77.97° vs 143.12°), the Midheaven moved nearly three signs, and every house placement was wrong. In Human Design the same input returned profile 2/4 instead of 2/5 and a Design Sun on gate 42 line 4 instead of line 5.
|
|
21
|
+
|
|
22
|
+
A datetime that states a clock time but not the zone that clock is in does not name a moment, so the server no longer guesses. It is now a `400` with an RFC 7807 `ambiguous_datetime` problem that names **both** fixes — put a `Z`/`±HH:MM` offset on the value, or pass the local time together with a `timezone`. Rejecting is the right default for a chart-correctness product: a loud failure costs one retry, a silent five-hour shift costs the whole reading.
|
|
23
|
+
|
|
24
|
+
A date with no clock time (`'1987-07-15'`) is unaffected and still resolves to 12:00 UTC — there is nothing ambiguous about it, and every search-window parameter in the API depends on it.
|
|
25
|
+
|
|
26
|
+
- **Every tool that takes a datetime now takes a `timezone`.** Previously only four did (`ephemeris_natal_chart`, `ephemeris_chart_wheel`, `ephemeris_bi_wheel`, and the `explore_*` apps), which left callers of `ephemeris_synastry`, `ephemeris_relocation`, the return tools, the ACG tools, `ephemeris_angles_points`, `ephemeris_house_cusps` and the rest with **no correct way to express a local birth time at all**. There is now a test that fails if a tool gains a datetime parameter without one.
|
|
27
|
+
|
|
28
|
+
- **The Human Design tools documented `"Must include 'Z' or timezone offset"` and then silently appended a `Z` themselves.** Four separate copies of an `ensureTimezone` helper turned any zone-less value into a UTC assertion the caller never made — which is precisely what made the error invisible. `human_design_composite` and `human_design_penta` skipped even that and passed the naive string straight through. All of them now take a `timezone`, resolve it properly (including historical DST — 1987 Chicago is UTC-5, not UTC-6), and refuse to invent a zone.
|
|
29
|
+
|
|
30
|
+
- **The documentation taught the bug.** `ephemeris_natal_chart`'s example was `datetime='1990-04-15T14:30:00'` with Chicago coordinates, no offset and no timezone — copied verbatim by models reading it. `acg_power_lines`, `acg_hits` and `ephemeris_progressed_chart` each carried a naive example that directly contradicted their own parameter description warning against naive input. Every ISO example across the tool surface now states its zone, and a repo test fails the build if a new one appears.
|
|
31
|
+
|
|
32
|
+
- **BaZi charts depended on the server's own timezone.** `parseBaziArgs` did `new Date('1987-07-15T14:00:00')`, which JavaScript resolves in the *host process's* zone, then read `.getUTCHours()`. On any server not running in UTC this shifted the hour pillar, and near midnight the day pillar. Components are now read from the string itself. (BaZi pillars are built from local wall-clock time by definition, so a zone-less datetime is correct there — it was the reading that was wrong, not the input.)
|
|
33
|
+
|
|
34
|
+
- **`ephemeris_natal_transits` declared a `transit_timezone` parameter that the handler never read.** It is now applied.
|
|
35
|
+
|
|
36
|
+
- **`vedic_chart_recalculate` dropped the `timezone` that `explore_vedic_chart` accepts**, so re-rendering the same chart from the iframe interpreted the birth time as UTC and produced a different chart than the one on screen.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Responses now echo the instant they were actually computed for: `resolved_utc`, plus `datetime_zone_source` (`offset` / `timezone` / `date_only`) and the zone itself. When a chart looks wrong, the first question is which moment it was cast for, and the answer is now in the response.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## [3.26.0] — 2026-07-25
|
|
45
|
+
|
|
46
|
+
A leaner default tool list, and a sweep of documentation and billing corrections found while auditing it.
|
|
47
|
+
|
|
48
|
+
The server has grown to 91 registered tools, 69 of which were advertised to the model on every single connection. That is roughly 28,000 tokens of tool definitions consumed before the user has said anything, and a 69-item menu to choose from on questions that almost always want one of about a dozen tools. This release changes what gets *advertised* without changing what exists.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **Tool surfaces — a focused default, with the full catalog one flag away.** `tools/list` now returns a curated core set (32 tools over HTTP, 35 over stdio) instead of everything: one interactive app per tradition, the primary data tool per domain, geocoding, account usage, and the allowlist-gated generic proxy. Measured tool-definition payload drops from ~27,800 to ~15,100 tokens.
|
|
53
|
+
|
|
54
|
+
**No capability was removed.** This is a filter on `tools/list` alone — every tool remains registered and remains callable by name. If a client knows the tool it wants, it works whether or not it was listed. There is a regression test asserting exactly this, because a surface filter that quietly became a capability cut would be a very easy mistake to ship.
|
|
55
|
+
|
|
56
|
+
Opt into the full catalog with `OPENEPHEMERIS_TOOLS=full` (stdio) or `?profile=full` / `X-OE-Tool-Surface: full` (remote HTTP). The surface is fixed at session initialize — this server does not advertise `tools.listChanged`, so switching means reconnecting. The public server card at `/.well-known/mcp/server-card.json` deliberately still lists everything: it is a registry catalog, not model context.
|
|
57
|
+
|
|
58
|
+
- **Usage reporting now covers the stdio transport, and identifies the connecting client.** Telemetry previously existed only on the remote HTTP server, so the entire npm install base — every Claude Desktop, Cursor and Windsurf user — was invisible. Both transports now emit the same three events with the same properties (`transport`, `surface`, `client_name`, `client_version`, `server_version`), so a single query can compare them. `client_name` comes from the MCP initialize handshake and is the only way to know which hosts this package actually runs in.
|
|
59
|
+
|
|
60
|
+
**This is disclosed and opt-out-able**, because it runs on your machine: see [Telemetry](README.md#telemetry). Tool name, duration, error status, client name and a one-way hash of your API key are sent. Your key, birth data, coordinates, tool arguments and tool results never are. Disable with `OPENEPHEMERIS_TELEMETRY=0` or the cross-tool standard `DO_NOT_TRACK=1`; both are checked before anything is sent, and are covered by tests.
|
|
61
|
+
|
|
62
|
+
- **`location_search` and `timezone_resolve` are now visible to the model.** Both existed but were marked app-only, so the model could not call them — which is why prompts had to geocode through `dev_read_api /location/autocomplete` instead. Turning a birth city into coordinates and an IANA timezone is now a first-class two-step.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **`ephemeris_planet_position` documented the wrong body for `planet_id=11`.** The description advertised `10=North Node, 11=South Node`. The API actually returns `10` = North Node (Mean) and `11` = North Node (**True**) — there is no South Node id at all. A request for a South Node returned the North Node with no error and no warning: a silently wrong answer, roughly 180° off. The description now documents the real ids and explains that the South Node is the North Node opposed (add 180°, mod 360).
|
|
67
|
+
|
|
68
|
+
- **Ten tools misstated their credit cost.** Costs are metered server-side on the URL path, so billing was always correct — but the descriptions the model reads were not, which meant plans and cost estimates built on them were wrong. Corrected: the BaZi analytical tools (`bazi_ten_gods`, `bazi_element_balance`, `bazi_luck_pillars`, `bazi_annual_pillar` — 1 → 3; `bazi_compatibility` — 2 → 3), the electional tools (`electional_moment_analysis` and `electional_aspect_search` — 2 → 5; `electional_station_tracker` — 3 → 5), and the Venus tools, which were **overstated** at 2 and actually cost 1. `ephemeris_retrograde_status` now discloses that its all-planets sweep fans out to ten backend calls and costs 10 credits, not 1.
|
|
69
|
+
|
|
70
|
+
- **`/chinese/bazi/chart` charged 4 credits while documenting 3.** The handler reserved base + visual render, but the route is mounted behind the usage-tracking middleware, which had already debited the base credit — so every BaZi chart render double-charged its base credit. The handler now reserves only the visual surcharge, matching the pattern already used by the `include_visual` wrapper. Actual cost is now 3, as documented.
|
|
71
|
+
|
|
72
|
+
- **Progressed chart recalculation in the chart-wheel app posted to `/predictive/progressed`, which does not exist.** The real path is `/ephemeris/progressed`; every progressed recalculate from the iframe was 404ing.
|
|
73
|
+
|
|
74
|
+
- **`ephemeris_lunar_return` and `ephemeris_planetary_return` declared an output schema promising an image** they can never return — neither tool sends `include_visual`. Both now declare the JSON-only schema they actually produce.
|
|
75
|
+
|
|
76
|
+
- **Tool-error telemetry could not distinguish a backend outage from a client-side argument error.** Errors thrown locally carry no HTTP status, so they were all recorded as `status: "unknown"` alongside genuine backend failures. Error events now carry `error_kind` (`backend` / `local`) and the error code.
|
|
77
|
+
|
|
78
|
+
- **WCAG AA contrast restored in the dark widget palette.** The 3.24.0 theme unification left five of the seven embedded apps — bodygraph, moon-phase, transit-timeline, vedic-chart and bazi — with muted and secondary text below the minimum contrast ratio. Those bundles ship inside this package (`dist/ui`), so anyone rendering the iframe apps saw it. The visual gate that should have caught it now audits **both** palettes per app and state; previously it only ever rendered one, which is how a whole-palette regression stayed green.
|
|
79
|
+
|
|
80
|
+
- **A release could not pass its own gate.** `npm publish` runs `verify:release` → `check:public`, which byte-compares the files mirrored to the public repo. Neither side pinned line endings, so a Windows checkout rewrote them to CRLF and the check reported permanent drift on files nobody had edited. Both repos now pin LF.
|
|
81
|
+
|
|
82
|
+
- **Stale references in the skill packs and plugin docs.** The setup and API-reference skills still described `dev.call` / `dev.list_allowed`, names retired in 3.15.0 (the tools are `dev_read_api`, `dev_write_api`, `dev_list_allowed`). The plugin skill listed `electional_ingress_calendar`, which is not a tool this server has ever registered. Endpoint counts corrected to 118.
|
|
83
|
+
|
|
84
|
+
### Notes
|
|
85
|
+
|
|
86
|
+
Registered tool count is unchanged at 91 — this release changes what is advertised, not what exists.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## [3.25.0] — 2026-07-22
|
|
91
|
+
|
|
92
|
+
New account-usage tool, remote-transport session-security hardening, plus text-encoding and telemetry fixes. Registered tool count: 90 → 91.
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
- **`account_usage`** — a free (0-credit) tool that answers "how many credits do I have left?" directly in chat: plan tier, billing period, credits used / included / remaining, percent of quota used, total API calls, and subscription status with renewal date, plus the right next step (wallet top-up or plan upgrade) for the user's tier. Accepts an optional `month` (YYYY-MM) to review past periods.
|
|
96
|
+
|
|
97
|
+
### Security
|
|
98
|
+
- **Sessions are now bound to the credential that created them.** Previously the resume path (`POST /mcp` with an `mcp-session-id`) accepted *any* syntactically valid API key or unexpired Bearer token — it never checked that the presented credential matched the one that initialized the session. A leaked or guessed session id combined with a *different* valid credential could attach to another user's session. Each session now stores a SHA-256 hash of a stable binding token (the raw API key, or the JWT `sub`/subject for OAuth so legitimate ~hourly token refresh still matches) and every resume must present a credential whose hash matches, compared in constant time. Mismatches get a `403 application/problem+json`.
|
|
99
|
+
- **The SSE stream leg (`GET /mcp`) now requires authentication.** It previously checked only the `mcp-session-id` header with no credential at all, so a leaked session id alone let an attacker attach to another user's server→client event stream. It now requires a credential (401 without one) and the same credential-binding match as resume (403 on mismatch).
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
- **Bodygraph gate/center text encoding repaired** — the Human Design bodygraph click-handler tools contained double-encoded UTF-8 (mojibake) in em-dashes, ranges, and status emoji across all 64 gate hexagram names and the center descriptions; they now render correctly.
|
|
103
|
+
- **Tool-error telemetry now records HTTP status** — the remote server's error events always reported "unknown" instead of the actual status (402 paywall / 429 rate-limit), making those uncountable in analytics.
|
|
104
|
+
- **`autoStartDeviceAuth` race (stdio device-auth):** concurrent first requests could each launch a device-auth flow (two codes, two poll loops) because the in-flight guard was only assigned after the async start resolved. The guard promise is now assigned synchronously before the first await.
|
|
105
|
+
- **Corrected a stale code comment** claiming HTTP 429 responses are retried — they are deliberately not (retrying a metered POST would double-charge; only idempotent GETs over 502/503/504 + transient transport codes retry).
|
|
106
|
+
|
|
107
|
+
## [3.24.0] — 2026-07-21
|
|
108
|
+
|
|
109
|
+
Mandala layout for the bodygraph iframe app — an in-iframe toggle between the classic graph and the concentric-rings mandala view, plus a metered house-ring opt-in.
|
|
110
|
+
|
|
111
|
+
### Added
|
|
112
|
+
- **Mandala layout toggle** in the Human Design Bodygraph Explorer — a power-user view alongside the default graph layout, reusing the existing theme-recalculate round-trip. The render cache now keys on layout × theme × houses, so toggling any axis back and forth never re-bills within a session. Requests an explicit hexagram-free ring set (the decorative hexagram ring cells no longer carry `data-gate` at all, fixing a hover mislabel and roughly halving the keyboard tab-stop count).
|
|
113
|
+
- **House rings — a distinct, explicitly metered opt-in** — a location-gated "Add house rings (+1 credit)" checkbox, off by default even when the mandala is on, disabled without a birth location or outside the mandala layout.
|
|
114
|
+
|
|
10
115
|
## [3.23.0] — 2026-07-19
|
|
11
116
|
|
|
12
117
|
Coverage build-out (4 → 7 live iframe apps), tool-data bug fixes from live probing, and Human Design trademark hygiene. Registered tool count: 84 → 90.
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025-2026 Open Ephemeris
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Open Ephemeris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
[](https://smithery.ai/servers/open-ephemeris/openephemeris)
|
|
4
4
|
[](https://www.npmjs.com/package/@openephemeris/mcp-server)
|
|
5
5
|
[](https://status.openephemeris.com/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://ssd.jpl.nasa.gov/planets/eph_export.html)
|
|
6
8
|
|
|
7
|
-

|
|
8
10
|
|
|
9
11
|
Model Context Protocol server for OpenEphemeris — typed astrology tools powered by the NASA JPL DE440 ephemeris. Zero hallucination on planetary positions, dates, and degrees. Covers 1,100 years of astronomical data.
|
|
10
12
|
|
|
@@ -279,6 +281,8 @@ The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable
|
|
|
279
281
|
| `ASTROMCP_API_KEY` | No | Legacy alias for `OPENEPHEMERIS_API_KEY` (checked as fallback) |
|
|
280
282
|
| `OPENEPHEMERIS_BACKEND_URL` | No | Defaults to `https://api.openephemeris.com` |
|
|
281
283
|
| `OPENEPHEMERIS_PROFILE` | No | `dev` by default |
|
|
284
|
+
| `OPENEPHEMERIS_TOOLS` | No | `core` (default) advertises a focused everyday tool set; `full` advertises every tool. See [Tool surface](#tool-surface) |
|
|
285
|
+
| `OPENEPHEMERIS_TELEMETRY` | No | Set to `0`/`false`/`off` to disable anonymous usage reporting. `DO_NOT_TRACK=1` also works. See [Telemetry](#telemetry) |
|
|
282
286
|
| `OPENEPHEMERIS_SERVICE_KEY` | No | Internal service auth |
|
|
283
287
|
| `OPENEPHEMERIS_JWT` | No | Bearer token auth |
|
|
284
288
|
| `OPENEPHEMERIS_DEV_ALLOWLIST_PATH` | No | Override allowlist file path |
|
|
@@ -286,6 +290,51 @@ The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable
|
|
|
286
290
|
|
|
287
291
|
Legacy aliases (`ASTROMCP_*`, `MERIDIAN_*`) remain supported.
|
|
288
292
|
|
|
293
|
+
## Telemetry
|
|
294
|
+
|
|
295
|
+
This server reports anonymous usage so we know which tools are worth maintaining and which are broken. Three events: session start, tool call, tool error.
|
|
296
|
+
|
|
297
|
+
**What is sent:** the tool name, how long it took, error status, which MCP client connected (e.g. Claude Desktop, Cursor) and its version, the server version, and a one-way SHA-256 prefix of your API key used as a stable anonymous id.
|
|
298
|
+
|
|
299
|
+
**What is never sent:** your API key or token, birth data, dates, names, coordinates, tool arguments, or tool results. No request or response bodies, ever.
|
|
300
|
+
|
|
301
|
+
**To turn it off** — either works, checked before anything is sent:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
OPENEPHEMERIS_TELEMETRY=0
|
|
305
|
+
# or the cross-tool standard
|
|
306
|
+
DO_NOT_TRACK=1
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Tool surface
|
|
310
|
+
|
|
311
|
+
By default the server advertises a **focused core set** of everyday tools rather than the entire catalog. Large tool lists cost context and make model tool-selection worse, so the default is tuned for real conversations: one interactive app per tradition, the primary data tool per domain, geocoding, and the allowlist-gated generic proxy.
|
|
312
|
+
|
|
313
|
+
**Nothing is removed.** The surface is a filter on `tools/list` only — every tool stays registered and stays callable by name. If you know the tool you want, call it and it works, listed or not.
|
|
314
|
+
|
|
315
|
+
To advertise the full catalog:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
OPENEPHEMERIS_TOOLS=full npx -y @openephemeris/mcp-server
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
On the remote HTTP server, append `?profile=full` to the connector URL (or send `X-OE-Tool-Surface: full`):
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
https://mcp.openephemeris.com/mcp?profile=full
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The surface is fixed when the session initializes — this server does not advertise `tools.listChanged`, so switching requires reconnecting. `dev_list_allowed` enumerates every operation reachable through the generic proxy regardless of surface.
|
|
328
|
+
|
|
329
|
+
## Contributing & Support
|
|
330
|
+
|
|
331
|
+
- **Something wrong with a result?** [Open an issue](https://github.com/openephemeris/openephemeris-MCP/issues/new/choose) — include the tool, your inputs, and what you expected.
|
|
332
|
+
- **Want to contribute?** See [CONTRIBUTING.md](CONTRIBUTING.md). Integration examples and new skills are the most useful things you can add.
|
|
333
|
+
- **Found a security problem?** Please report it privately — see [SECURITY.md](SECURITY.md).
|
|
334
|
+
- **Tools timing out?** Check [status.openephemeris.com](https://status.openephemeris.com) first.
|
|
335
|
+
|
|
336
|
+
If this saved you from an LLM confidently inventing a Saturn position, a ⭐ helps other people find it.
|
|
337
|
+
|
|
289
338
|
## Legal
|
|
290
339
|
|
|
291
340
|
This package is licensed under the [MIT License](./LICENSE). However, use of this package to access the OpenEphemeris API constitutes use of the Service and is governed by the [OpenEphemeris Terms of Service](https://openephemeris.com/terms). By using this package, you agree to those terms. See also the [Privacy Policy](https://openephemeris.com/privacy) and [Acceptable Use Policy](https://openephemeris.com/acceptable-use).
|
|
@@ -359,8 +408,8 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
359
408
|
|
|
360
409
|
- Allowlisted operations: **28**
|
|
361
410
|
- Methods: `GET=4`, `POST=24`, `PUT=0`, `PATCH=0`, `DELETE=0`
|
|
362
|
-
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **
|
|
363
|
-
- Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bazi_annual_pillar`, `bazi_chart`, `bazi_compatibility`, `bazi_element_balance`, `bazi_luck_pillars`, `bazi_recalculate`, `bazi_ten_gods`, `bi_wheel_on_cross_aspect_click`, `bi_wheel_on_house_click`, `bi_wheel_on_planet_click`, `bi_wheel_recalculate`, `bi_wheel_synopsis`, `bodygraph_recalculate`, `chart_wheel_on_aspect_click`, `chart_wheel_on_house_click`, `chart_wheel_on_planet_click`, `chart_wheel_recalculate`, `chinese_bazi`, `dev_list_allowed`, `dev_read_api`, `dev_write_api`, `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`, `explore_bazi_chart`, `explore_bi_wheel`, `explore_human_design`, `explore_human_design_connection`, `explore_human_design_transit`, `explore_moon_phase`, `explore_natal_chart`, `explore_transit_timeline`, `explore_vedic_chart`, `hd_on_center_click`, `hd_on_channel_click`, `hd_on_connection_channel_click`, `hd_on_gate_click`, `hd_on_planet_click`, `hd_on_transit_channel_click`, `hd_on_variable_click`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `location_search`, `moon_phase_recalculate`, `timezone_resolve`, `vedic_chart`, `vedic_chart_recalculate`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
|
|
411
|
+
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **91**
|
|
412
|
+
- Typed tools: `account_usage`, `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bazi_annual_pillar`, `bazi_chart`, `bazi_compatibility`, `bazi_element_balance`, `bazi_luck_pillars`, `bazi_recalculate`, `bazi_ten_gods`, `bi_wheel_on_cross_aspect_click`, `bi_wheel_on_house_click`, `bi_wheel_on_planet_click`, `bi_wheel_recalculate`, `bi_wheel_synopsis`, `bodygraph_recalculate`, `chart_wheel_on_aspect_click`, `chart_wheel_on_house_click`, `chart_wheel_on_planet_click`, `chart_wheel_recalculate`, `chinese_bazi`, `dev_list_allowed`, `dev_read_api`, `dev_write_api`, `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`, `explore_bazi_chart`, `explore_bi_wheel`, `explore_human_design`, `explore_human_design_connection`, `explore_human_design_transit`, `explore_moon_phase`, `explore_natal_chart`, `explore_transit_timeline`, `explore_vedic_chart`, `hd_on_center_click`, `hd_on_channel_click`, `hd_on_connection_channel_click`, `hd_on_gate_click`, `hd_on_planet_click`, `hd_on_transit_channel_click`, `hd_on_variable_click`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `location_search`, `moon_phase_recalculate`, `timezone_resolve`, `vedic_chart`, `vedic_chart_recalculate`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
|
|
364
413
|
- Generic tools:
|
|
365
414
|
|
|
366
415
|
### Allowlist Families
|
package/dist/analytics.js
CHANGED
|
@@ -1,23 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* analytics.ts — fire-and-forget PostHog events for
|
|
2
|
+
* analytics.ts — fire-and-forget PostHog events for both MCP transports.
|
|
3
3
|
*
|
|
4
4
|
* The launch was previously unmeasurable from the MCP side: no way to see how
|
|
5
5
|
* many sessions initialized, which tools drove usage, or how many users hit
|
|
6
6
|
* the 402 paywall. These events close that gap. Everything is best-effort:
|
|
7
7
|
* analytics must never slow down or fail a tool call.
|
|
8
8
|
*
|
|
9
|
-
* Configuration
|
|
10
|
-
* POSTHOG_API_KEY —
|
|
9
|
+
* Configuration:
|
|
10
|
+
* POSTHOG_API_KEY — project API key (phc_...). On the Fly-hosted remote
|
|
11
|
+
* server this comes from a secret. On stdio (the npm
|
|
12
|
+
* package running on a user's machine) there is no env to
|
|
13
|
+
* set, so DEFAULT_INGEST_KEY below is compiled in.
|
|
11
14
|
* POSTHOG_HOST — capture host, default https://us.i.posthog.com.
|
|
12
15
|
*
|
|
16
|
+
* OPT-OUT — respected on every transport, checked before anything is sent:
|
|
17
|
+
* OPENEPHEMERIS_TELEMETRY=0|false|off|no
|
|
18
|
+
* DO_NOT_TRACK=1 (de-facto cross-tool standard)
|
|
19
|
+
* Disclosed in README.md under "Telemetry".
|
|
20
|
+
*
|
|
13
21
|
* Identity: we never send raw API keys or JWTs. The distinct_id is a SHA-256
|
|
14
22
|
* prefix of the credential, stable per user across sessions but useless for
|
|
15
|
-
* authentication.
|
|
23
|
+
* authentication. No birth data, coordinates, names, or tool arguments are
|
|
24
|
+
* ever sent — only the tool NAME, duration, error status, and client name.
|
|
16
25
|
*/
|
|
17
26
|
import { createHash } from "node:crypto";
|
|
18
27
|
const POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
|
|
28
|
+
/**
|
|
29
|
+
* Public, write-only PostHog project ingestion key.
|
|
30
|
+
*
|
|
31
|
+
* This is the same key class that ships in the openephemeris.com browser
|
|
32
|
+
* bundle (NEXT_PUBLIC_POSTHOG_KEY) — it can only append events, cannot read
|
|
33
|
+
* anything back, and is public by design. It is compiled in so the stdio
|
|
34
|
+
* package (which has no env to configure) is measurable at all; without it
|
|
35
|
+
* the entire npm install base is invisible.
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_INGEST_KEY = "phc_tDomsYBMTZyFOb2DYKpWprUr4ATJ4v0QQh3fYqDdh9P";
|
|
38
|
+
/** True when the user has opted out of telemetry by either supported signal. */
|
|
39
|
+
function telemetryDisabled() {
|
|
40
|
+
const flag = (process.env.OPENEPHEMERIS_TELEMETRY ?? "").trim().toLowerCase();
|
|
41
|
+
if (flag === "0" || flag === "false" || flag === "off" || flag === "no")
|
|
42
|
+
return true;
|
|
43
|
+
const dnt = (process.env.DO_NOT_TRACK ?? "").trim();
|
|
44
|
+
return dnt === "1" || dnt.toLowerCase() === "true";
|
|
45
|
+
}
|
|
19
46
|
function apiKey() {
|
|
20
|
-
|
|
47
|
+
if (telemetryDisabled())
|
|
48
|
+
return undefined;
|
|
49
|
+
const configured = process.env.POSTHOG_API_KEY;
|
|
50
|
+
if (configured)
|
|
51
|
+
return configured;
|
|
52
|
+
return DEFAULT_INGEST_KEY.startsWith("phc_") ? DEFAULT_INGEST_KEY : undefined;
|
|
21
53
|
}
|
|
22
54
|
/** Stable, non-reversible identity from a credential. */
|
|
23
55
|
export function distinctIdFor(credential) {
|
package/dist/backend/client.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export interface BinaryBackendResponse {
|
|
|
21
21
|
encoding: "base64";
|
|
22
22
|
data_base64: string;
|
|
23
23
|
}
|
|
24
|
+
export declare const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboard?tab=account";
|
|
25
|
+
export declare const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
|
|
26
|
+
export declare const UPGRADE_URL = "https://openephemeris.com/pricing";
|
|
27
|
+
export declare const WALLET_TOPUP_URL = "https://openephemeris.com/wallet";
|
|
24
28
|
export declare class BackendError extends Error {
|
|
25
29
|
readonly status: number;
|
|
26
30
|
readonly code: string;
|
|
@@ -59,6 +63,9 @@ export declare class BackendClient {
|
|
|
59
63
|
* Idempotent — only starts once per client lifetime.
|
|
60
64
|
*/
|
|
61
65
|
private autoStartDeviceAuth;
|
|
66
|
+
/** The actual device-auth start + background poll. Kicked off by (and stored
|
|
67
|
+
* on) autoStartDeviceAuth so the guard is race-safe. */
|
|
68
|
+
private _runDeviceAuth;
|
|
62
69
|
private expectsBinaryResponse;
|
|
63
70
|
private toBuffer;
|
|
64
71
|
private decodePayload;
|
package/dist/backend/client.js
CHANGED
|
@@ -19,9 +19,10 @@ const BINARY_ENDPOINT_PREFIXES = [
|
|
|
19
19
|
"/comparative/visualization/bi-wheel",
|
|
20
20
|
"/comparative/visualization/chart-wheel",
|
|
21
21
|
];
|
|
22
|
-
const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboard?tab=account";
|
|
23
|
-
const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
|
|
24
|
-
const UPGRADE_URL = "https://openephemeris.com/pricing";
|
|
22
|
+
export const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboard?tab=account";
|
|
23
|
+
export const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
|
|
24
|
+
export const UPGRADE_URL = "https://openephemeris.com/pricing";
|
|
25
|
+
export const WALLET_TOPUP_URL = "https://openephemeris.com/wallet";
|
|
25
26
|
function sleep(ms) {
|
|
26
27
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
28
|
}
|
|
@@ -143,40 +144,38 @@ export class BackendClient {
|
|
|
143
144
|
async autoStartDeviceAuth() {
|
|
144
145
|
if (this._pendingAuthFlow)
|
|
145
146
|
return; // Already started
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// the 401 response will surface the error message to the user.
|
|
179
|
-
}
|
|
147
|
+
// Assign the in-flight promise SYNCHRONOUSLY — before the first await inside
|
|
148
|
+
// _runDeviceAuth — so concurrent callers in the same tick observe a non-null
|
|
149
|
+
// _pendingAuthFlow and bail instead of each launching a duplicate device-auth
|
|
150
|
+
// flow (which would print two codes and open two poll loops). On any failure
|
|
151
|
+
// we reset to null so a later request can retry.
|
|
152
|
+
this._pendingAuthFlow = this._runDeviceAuth().catch((err) => {
|
|
153
|
+
console.error(`❌ Device auth failed: ${err?.message ?? err}`);
|
|
154
|
+
this._pendingAuthFlow = null; // Allow retry
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** The actual device-auth start + background poll. Kicked off by (and stored
|
|
158
|
+
* on) autoStartDeviceAuth so the guard is race-safe. */
|
|
159
|
+
async _runDeviceAuth() {
|
|
160
|
+
const flow = new DeviceAuthFlow();
|
|
161
|
+
const startResult = await flow.start();
|
|
162
|
+
this._authFlowResult = {
|
|
163
|
+
verification_uri: startResult.verification_uri,
|
|
164
|
+
user_code: startResult.user_code,
|
|
165
|
+
verification_uri_complete: startResult.verification_uri_complete,
|
|
166
|
+
};
|
|
167
|
+
// Print to stderr so it's visible in MCP host logs
|
|
168
|
+
console.error(`\n🔗 OpenEphemeris: Account connection required.\n` +
|
|
169
|
+
` Visit: ${startResult.verification_uri}\n` +
|
|
170
|
+
` Code: ${startResult.user_code}\n` +
|
|
171
|
+
` Or: ${startResult.verification_uri_complete}\n`);
|
|
172
|
+
await flow.poll(startResult.device_code, (attempt) => {
|
|
173
|
+
if (attempt % 12 === 0) {
|
|
174
|
+
console.error(` Still waiting... Enter code ${startResult.user_code} at ${startResult.verification_uri}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
console.error(`✅ Authenticated! Subsequent requests will use your account.`);
|
|
178
|
+
this._authFlowResult = null;
|
|
180
179
|
}
|
|
181
180
|
expectsBinaryResponse(path, options) {
|
|
182
181
|
const normalizedPath = path.split("?")[0].trim().toLowerCase();
|
|
@@ -361,7 +360,9 @@ export class BackendClient {
|
|
|
361
360
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
362
361
|
const expectsBinary = this.expectsBinaryResponse(path, options);
|
|
363
362
|
let lastError = new Error("Unknown error");
|
|
364
|
-
// Attempt + retries on transient failures
|
|
363
|
+
// Attempt + retries on transient failures. Retryable set is deliberately
|
|
364
|
+
// GET-only over {502, 503, 504} + transient transport codes — NOT 429 (see
|
|
365
|
+
// the RETRYABLE_STATUSES NOTE above: retrying a metered POST double-charges).
|
|
365
366
|
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
366
367
|
try {
|
|
367
368
|
const response = await this.client.request({
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
-
import { initTools, toolRegistry, formatToolResponse, formatToolError, modelVisibleTools } from "./tools/index.js";
|
|
8
|
+
import { initTools, toolRegistry, formatToolResponse, formatToolError, modelVisibleTools, parseToolSurface, SERVER_VERSION } from "./tools/index.js";
|
|
9
|
+
import { captureEvent, distinctIdFor } from "./analytics.js";
|
|
9
10
|
import { listPrompts, getPromptContent } from "./prompts.js";
|
|
10
11
|
// ── MCP App resource imports ────────────────────────────────────────────────
|
|
11
12
|
import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
|
|
@@ -62,10 +63,44 @@ const server = new Server({
|
|
|
62
63
|
},
|
|
63
64
|
},
|
|
64
65
|
});
|
|
66
|
+
// Which slice of the registry this process advertises. Tools outside the core
|
|
67
|
+
// surface stay callable by name — this only controls what tools/list returns.
|
|
68
|
+
// Set OPENEPHEMERIS_TOOLS=full to advertise everything.
|
|
69
|
+
const STDIO_SURFACE = parseToolSurface(process.env.OPENEPHEMERIS_TOOLS);
|
|
70
|
+
/**
|
|
71
|
+
* Stable pseudonymous id for this install. Derived from the configured
|
|
72
|
+
* credential exactly like the remote server, so one user shows up as the same
|
|
73
|
+
* person whether they connect over stdio or HTTP. Falls back to "anonymous"
|
|
74
|
+
* when no credential is configured yet.
|
|
75
|
+
*/
|
|
76
|
+
function analyticsId() {
|
|
77
|
+
return distinctIdFor(process.env.OPENEPHEMERIS_API_KEY ||
|
|
78
|
+
process.env.ASTROMCP_API_KEY ||
|
|
79
|
+
process.env.OPENEPHEMERIS_JWT ||
|
|
80
|
+
undefined);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Properties attached to every stdio event.
|
|
84
|
+
*
|
|
85
|
+
* `client_name` is the host that connected (Claude Desktop, Cursor, Windsurf,
|
|
86
|
+
* …) from the MCP initialize handshake — this is the only way to know which
|
|
87
|
+
* hosts the npm install base actually runs in, and therefore which of them
|
|
88
|
+
* ever exercise the iframe apps.
|
|
89
|
+
*/
|
|
90
|
+
function transportProps() {
|
|
91
|
+
const info = server.getClientVersion();
|
|
92
|
+
return {
|
|
93
|
+
transport: "stdio",
|
|
94
|
+
surface: STDIO_SURFACE,
|
|
95
|
+
client_name: info?.name ?? "unknown",
|
|
96
|
+
client_version: info?.version ?? "unknown",
|
|
97
|
+
server_version: SERVER_VERSION,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
65
100
|
// List available tools
|
|
66
101
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
67
102
|
return {
|
|
68
|
-
tools: modelVisibleTools().map((tool) => ({
|
|
103
|
+
tools: modelVisibleTools("stdio", STDIO_SURFACE).map((tool) => ({
|
|
69
104
|
name: tool.name,
|
|
70
105
|
description: tool.description,
|
|
71
106
|
inputSchema: tool.inputSchema,
|
|
@@ -231,12 +266,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
231
266
|
try {
|
|
232
267
|
const result = await tool.handler(request.params.arguments ?? {});
|
|
233
268
|
const durationMs = Date.now() - startTime;
|
|
269
|
+
captureEvent("mcp_tool_call", analyticsId(), {
|
|
270
|
+
tool: toolName,
|
|
271
|
+
duration_ms: durationMs,
|
|
272
|
+
...transportProps(),
|
|
273
|
+
});
|
|
234
274
|
return formatToolResponse(toolName, result, durationMs);
|
|
235
275
|
}
|
|
236
276
|
catch (error) {
|
|
237
277
|
const durationMs = Date.now() - startTime;
|
|
238
278
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
239
279
|
console.error(`[MCP] ❌ Failed: ${toolName} (${durationMs}ms) - ${errorMessage}`);
|
|
280
|
+
const err = error;
|
|
281
|
+
captureEvent("mcp_tool_error", analyticsId(), {
|
|
282
|
+
tool: toolName,
|
|
283
|
+
status: err?.status ?? "unknown",
|
|
284
|
+
error_kind: err?.status != null ? "backend" : "local",
|
|
285
|
+
code: err?.code ?? "none",
|
|
286
|
+
...transportProps(),
|
|
287
|
+
});
|
|
240
288
|
// isError: true for every failure so the host recovers, EXCEPT the stdio
|
|
241
289
|
// device-auth-pending case (model must relay the verification link).
|
|
242
290
|
return formatToolError(error);
|
|
@@ -246,6 +294,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
246
294
|
async function main() {
|
|
247
295
|
await initTools();
|
|
248
296
|
const transport = new StdioServerTransport();
|
|
297
|
+
// oninitialized fires after the handshake, so getClientVersion() is
|
|
298
|
+
// populated — this is the one place we learn which host we are running in.
|
|
299
|
+
server.oninitialized = () => {
|
|
300
|
+
const info = server.getClientVersion();
|
|
301
|
+
console.error(`[MCP] Connected to ${info?.name ?? "unknown host"} ${info?.version ?? ""}`.trim());
|
|
302
|
+
captureEvent("mcp_session_init", analyticsId(), {
|
|
303
|
+
auth: process.env.OPENEPHEMERIS_API_KEY || process.env.ASTROMCP_API_KEY
|
|
304
|
+
? "api_key"
|
|
305
|
+
: process.env.OPENEPHEMERIS_JWT
|
|
306
|
+
? "jwt"
|
|
307
|
+
: "none",
|
|
308
|
+
...transportProps(),
|
|
309
|
+
});
|
|
310
|
+
};
|
|
249
311
|
await server.connect(transport);
|
|
250
312
|
console.error("Open Ephemeris MCP server running on stdio");
|
|
251
313
|
}
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* src/oauth/session-utils.ts — Pure helpers for session reliability.
|
|
3
|
-
*
|
|
4
|
-
* Two concerns live here so they can be unit-tested without spinning up the
|
|
5
|
-
* full SSE server:
|
|
6
|
-
*
|
|
7
|
-
* 1. JWT expiry decoding — read the `exp` claim WITHOUT signature
|
|
8
|
-
* verification. The signature is validated downstream by the Go API's
|
|
9
|
-
* ValidateSupabaseJWT; here we only need to know whether a token is
|
|
10
|
-
* already expired so the /mcp gate can trigger the host's OAuth refresh
|
|
11
|
-
* (401 invalid_token) instead of letting it die as a confusing tool error.
|
|
12
|
-
*
|
|
13
|
-
* 2. Machine-scoped session IDs — Fly runs min_machines_running >= 2, but the
|
|
14
|
-
* HTTP/SSE session store is a per-process Map. A resume request can land on
|
|
15
|
-
* a different machine and 404. We embed the FLY_MACHINE_ID into generated
|
|
16
|
-
* session IDs so an incoming request can be re-routed to the owning machine
|
|
17
|
-
* via the `fly-replay` header. Local/dev (no FLY_MACHINE_ID) is a no-op.
|
|
18
|
-
*/
|
|
19
1
|
/**
|
|
20
2
|
* Decode a JWT's payload without verifying the signature and return its `exp`
|
|
21
3
|
* claim (seconds since epoch), or null if the token is malformed / has no exp.
|
|
@@ -57,3 +39,45 @@ export declare function replayTargetMachine(sessionId: string, opts: {
|
|
|
57
39
|
selfMachineId?: string;
|
|
58
40
|
alreadyReplayed: boolean;
|
|
59
41
|
}): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Decode a JWT's `sub` (subject) claim without verifying the signature.
|
|
44
|
+
* The subject is the stable Supabase user id — it survives token refresh,
|
|
45
|
+
* whereas the raw JWT string rotates roughly hourly. Returns null when the
|
|
46
|
+
* token is malformed or carries no usable string `sub`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function decodeJwtSub(token: string): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Compute the STABLE binding token for a presented credential — the value a
|
|
51
|
+
* session is bound to at init and re-checked on every resume:
|
|
52
|
+
*
|
|
53
|
+
* - API key → the raw key. A given key is re-presented verbatim on every
|
|
54
|
+
* request, so binding to it is exact.
|
|
55
|
+
* - JWT → the `sub` claim (Supabase user id), NOT the raw token. claude.ai
|
|
56
|
+
* rotates the Bearer JWT ~hourly via /oauth/token; binding to the raw token
|
|
57
|
+
* would 403 every legitimate refresh. The subject is constant across
|
|
58
|
+
* refreshes for the same user, so binding survives rotation while still
|
|
59
|
+
* rejecting a different user's token.
|
|
60
|
+
* - JWT with no decodable `sub` → fall back to the raw token (best effort;
|
|
61
|
+
* Supabase access tokens always carry `sub`, so this is a degenerate case).
|
|
62
|
+
*
|
|
63
|
+
* Returns null when neither credential is present.
|
|
64
|
+
*/
|
|
65
|
+
export declare function credentialBindingToken(auth: {
|
|
66
|
+
apiKey?: string;
|
|
67
|
+
jwt?: string;
|
|
68
|
+
}): string | null;
|
|
69
|
+
/** SHA-256 hex digest of a binding token — what we store on the session record. */
|
|
70
|
+
export declare function hashBindingToken(token: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Compute the stored/compared binding hash for a request's auth, or null when
|
|
73
|
+
* the request carries no credential.
|
|
74
|
+
*/
|
|
75
|
+
export declare function credentialBindingHash(auth: {
|
|
76
|
+
apiKey?: string;
|
|
77
|
+
jwt?: string;
|
|
78
|
+
}): string | null;
|
|
79
|
+
/**
|
|
80
|
+
* Constant-time comparison of two binding hashes. Both are fixed-length hex
|
|
81
|
+
* digests; a length mismatch (or a null presented hash) is a non-match.
|
|
82
|
+
*/
|
|
83
|
+
export declare function bindingHashMatches(stored: string, presented: string | null): boolean;
|