@openephemeris/mcp-server 3.24.0 → 4.1.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 +173 -0
- package/LICENSE +21 -21
- package/README.md +75 -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/prompts.js +55 -42
- package/dist/server-sse.js +114 -14
- package/dist/tools/apps/bazi-app.js +15 -28
- package/dist/tools/apps/bi-wheel-app.js +14 -11
- package/dist/tools/apps/bodygraph-app.d.ts +5 -5
- package/dist/tools/apps/bodygraph-app.js +159 -212
- package/dist/tools/apps/chart-wheel-app.js +21 -20
- 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/dev.js +4 -3
- package/dist/tools/index.d.ts +45 -2
- package/dist/tools/index.js +81 -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 +89 -23
- package/dist/tools/specialized/bi_wheel.js +5 -4
- package/dist/tools/specialized/chart_wheel.js +5 -8
- package/dist/tools/specialized/comparative.js +40 -21
- package/dist/tools/specialized/electional.js +13 -10
- package/dist/tools/specialized/ephemeris_core.js +13 -8
- package/dist/tools/specialized/ephemeris_extended.js +60 -53
- 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 +14 -6
- 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 +4188 -4128
- package/dist/ui/bodygraph.html +3673 -3616
- package/dist/ui/chart-wheel.html +3769 -3713
- package/dist/ui/moon-phase.html +3219 -3153
- package/dist/ui/transit-timeline.html +199 -170
- package/dist/ui/vedic-chart.html +1116 -1098
- package/package.json +3 -2
- package/smithery.yaml +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,179 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [4.1.0] — 2026-07-26
|
|
11
|
+
|
|
12
|
+
Follow-up to 4.0.0. The new `400` was telling REST callers to do something that, on
|
|
13
|
+
most endpoints, was impossible. It now isn't — the remedy it names is real everywhere.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`date_time.timezone` — every datetime in the API now accepts its own zone.** 4.0.0's
|
|
18
|
+
rejection message offered two fixes: put an offset on the value, or "keep the local
|
|
19
|
+
wall-clock time and pass the zone alongside it". The second one only worked on requests
|
|
20
|
+
built around a `subject`, because the zone lived on `subject.birth_location.timezone`.
|
|
21
|
+
Endpoints that take a bare `date_time` — `/ephemeris/angles-points`, `/ephemeris/house-cusps`,
|
|
22
|
+
`/ephemeris/planet-position`, `/ephemeris/dignities`, `/ephemeris/midpoints`,
|
|
23
|
+
`/ephemeris/retrograde-status`, `/ephemeris/fixed-stars`, `/ephemeris/hermetic-lots`
|
|
24
|
+
and `/ephemeris/lunar-phase` — had nowhere to put a zone, so a caller following the
|
|
25
|
+
error's own advice got the same `400` back. Nine endpoints, one impossible instruction.
|
|
26
|
+
|
|
27
|
+
The zone now belongs to the datetime rather than to the request, so this works
|
|
28
|
+
everywhere a datetime is accepted:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{ "date_time": { "iso": "1987-07-15T09:01:00",
|
|
32
|
+
"timezone": { "iana_name": "America/Chicago" } } }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Purely additive — an omitted `timezone` behaves exactly as before, and where a request
|
|
36
|
+
already carries a zone (a subject's birth location) that remains the fallback. A zone
|
|
37
|
+
already written on the string always wins, so a stray `timezone` can never move an
|
|
38
|
+
instant that the caller had already pinned.
|
|
39
|
+
|
|
40
|
+
- **`docs/datetime-contract.md`** — one authoritative statement of the rule: when a zone
|
|
41
|
+
is required, the date-only and `*_utc` carve-outs, how to pass a zone on each request
|
|
42
|
+
shape, and what the response echoes. The reason four endpoint families drifted to four
|
|
43
|
+
different readings of "ISO 8601 date-time" is that no such document existed.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **The `400` no longer advertises a remedy the endpoint doesn't have.** Fields that are
|
|
48
|
+
a bare datetime *string* rather than an object — the ACG `epoch` family, the Venus date
|
|
49
|
+
ranges, the `datetime` query parameter on the `GET /ephemeris/moon/*` endpoints — have
|
|
50
|
+
no `timezone` companion and cannot grow one. Their rejection message now names only the
|
|
51
|
+
fix that exists: put the zone on the value. Being told to do something impossible is
|
|
52
|
+
worse than a terse error.
|
|
53
|
+
|
|
54
|
+
- **The OpenAPI spec now states the contract where callers actually read it.** The
|
|
55
|
+
`date_time` fields said "ISO 8601 date-time" and nothing more, so the only place the
|
|
56
|
+
rule appeared was in the error you got for breaking it. The `iso`, `components` and
|
|
57
|
+
`timezone` fields, the `epoch`/date-range string fields, and the spec's own
|
|
58
|
+
"Supported Formats" section now all state it. That section had been listing a zone-less
|
|
59
|
+
`"2000-01-01T12:00:00"` as an accepted input — the exact value the API rejects.
|
|
60
|
+
|
|
61
|
+
- **`/timezone/offset` documented behaviour it has never had.** Its `datetime_utc` field
|
|
62
|
+
claimed a naive value was "interpreted as UTC". The field is a strict RFC 3339 instant,
|
|
63
|
+
so a zone-less value was never interpreted at all — it failed to parse. Now documented
|
|
64
|
+
as what it is.
|
|
65
|
+
|
|
66
|
+
- **The spec's front-page example used field names that do not exist** (`iso_string`,
|
|
67
|
+
a `timezone.name`, and latitude/longitude directly under `subject`). Replaced with the
|
|
68
|
+
real request shape.
|
|
69
|
+
|
|
70
|
+
- **Four tool modules were still teaching the pre-4.0.0 contract.** `chart_wheel` told the
|
|
71
|
+
model to "include timezone offset **if known**"; `ephemeris_composite`,
|
|
72
|
+
`ephemeris_composite_midpoint`, `ephemeris_overlay`, `ephemeris_natal_transits`, the
|
|
73
|
+
electional search tools and `explore_bi_wheel` described their datetimes as plain
|
|
74
|
+
"(ISO 8601)" with no zone requirement. 4.0.0 migrated the rest of the surface and missed
|
|
75
|
+
these. They now use the same canonical description as every other tool, and the composite
|
|
76
|
+
tools reject a zone-less datetime locally instead of spending a credit to learn it.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- The tool-surface token ceilings are **unchanged** (core 20,000 / full 36,500). The
|
|
81
|
+
descriptions above cost ~0.4k core / ~1.0k full and fit inside the existing budget;
|
|
82
|
+
measured after the change, core is 18.7k and full is 34.8k.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## [4.0.0] — 2026-07-26
|
|
87
|
+
|
|
88
|
+
**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.
|
|
89
|
+
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- **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.
|
|
95
|
+
|
|
96
|
+
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.
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
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.
|
|
101
|
+
|
|
102
|
+
- **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.
|
|
103
|
+
|
|
104
|
+
- **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.
|
|
105
|
+
|
|
106
|
+
- **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.
|
|
107
|
+
|
|
108
|
+
- **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.)
|
|
109
|
+
|
|
110
|
+
- **`ephemeris_natal_transits` declared a `transit_timezone` parameter that the handler never read.** It is now applied.
|
|
111
|
+
|
|
112
|
+
- **`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.
|
|
113
|
+
|
|
114
|
+
### Changed
|
|
115
|
+
|
|
116
|
+
- 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.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## [3.26.0] — 2026-07-25
|
|
121
|
+
|
|
122
|
+
A leaner default tool list, and a sweep of documentation and billing corrections found while auditing it.
|
|
123
|
+
|
|
124
|
+
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.
|
|
125
|
+
|
|
126
|
+
### Added
|
|
127
|
+
|
|
128
|
+
- **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.
|
|
129
|
+
|
|
130
|
+
**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.
|
|
131
|
+
|
|
132
|
+
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.
|
|
133
|
+
|
|
134
|
+
- **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.
|
|
135
|
+
|
|
136
|
+
**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.
|
|
137
|
+
|
|
138
|
+
- **`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.
|
|
139
|
+
|
|
140
|
+
### Fixed
|
|
141
|
+
|
|
142
|
+
- **`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).
|
|
143
|
+
|
|
144
|
+
- **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.
|
|
145
|
+
|
|
146
|
+
- **`/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.
|
|
147
|
+
|
|
148
|
+
- **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.
|
|
149
|
+
|
|
150
|
+
- **`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.
|
|
151
|
+
|
|
152
|
+
- **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.
|
|
153
|
+
|
|
154
|
+
- **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.
|
|
155
|
+
|
|
156
|
+
- **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.
|
|
157
|
+
|
|
158
|
+
- **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.
|
|
159
|
+
|
|
160
|
+
### Notes
|
|
161
|
+
|
|
162
|
+
Registered tool count is unchanged at 91 — this release changes what is advertised, not what exists.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## [3.25.0] — 2026-07-22
|
|
167
|
+
|
|
168
|
+
New account-usage tool, remote-transport session-security hardening, plus text-encoding and telemetry fixes. Registered tool count: 90 → 91.
|
|
169
|
+
|
|
170
|
+
### Added
|
|
171
|
+
- **`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.
|
|
172
|
+
|
|
173
|
+
### Security
|
|
174
|
+
- **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`.
|
|
175
|
+
- **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).
|
|
176
|
+
|
|
177
|
+
### Fixed
|
|
178
|
+
- **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.
|
|
179
|
+
- **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.
|
|
180
|
+
- **`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.
|
|
181
|
+
- **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).
|
|
182
|
+
|
|
10
183
|
## [3.24.0] — 2026-07-21
|
|
11
184
|
|
|
12
185
|
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.
|
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
|
@@ -93,7 +93,15 @@ await mcpClient.connect(transport)
|
|
|
93
93
|
const { tools } = await mcpClient.listTools()
|
|
94
94
|
const result = await mcpClient.callTool({
|
|
95
95
|
name: "ephemeris_natal_chart",
|
|
96
|
-
|
|
96
|
+
// A datetime that states a clock time must state its zone: either pass
|
|
97
|
+
// `timezone` alongside the local time, or put a Z/±HH:MM offset on the value.
|
|
98
|
+
arguments: {
|
|
99
|
+
datetime: "1990-04-15T14:30:00",
|
|
100
|
+
timezone: "America/Chicago",
|
|
101
|
+
latitude: 41.8781,
|
|
102
|
+
longitude: -87.6298,
|
|
103
|
+
format: "llm",
|
|
104
|
+
},
|
|
97
105
|
})
|
|
98
106
|
```
|
|
99
107
|
|
|
@@ -219,6 +227,32 @@ The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable
|
|
|
219
227
|
"What is the sidereal time and delta-T right now?"
|
|
220
228
|
```
|
|
221
229
|
|
|
230
|
+
## Interactive Charts
|
|
231
|
+
|
|
232
|
+
Nine of the tools don't answer with JSON. They open a chart in the conversation — a real one, drawn from the same calculation, that you can click around in.
|
|
233
|
+
|
|
234
|
+
This matters more than it sounds. A natal chart returned as JSON is a list of numbers you have to already understand to read. The same chart rendered as a wheel is something you can point at. Click a planet and you get that placement explained; click a house and you get what's in it. The chart stays on screen while you keep talking, and it doesn't cost another credit to keep looking at it.
|
|
235
|
+
|
|
236
|
+
These need a host that supports MCP Apps — Claude Desktop is the main one today. In a client without app support the same tools still work; you get the underlying data instead of the picture, so nothing breaks, you just don't get the wheel.
|
|
237
|
+
|
|
238
|
+
| Tool | What opens | What you can click | Credits |
|
|
239
|
+
|---|---|---|---|
|
|
240
|
+
| `explore_natal_chart` | Natal wheel — planets, houses, aspects, angles | Planets, houses, aspect lines; recalculate with new settings | 1 |
|
|
241
|
+
| `explore_bi_wheel` | Two charts on one wheel: transits, synastry, progressions | Either wheel's planets, houses, and the aspects between them | 2 |
|
|
242
|
+
| `explore_human_design` | Human Design bodygraph, with a mandala view toggle | Centers, gates, channels, planets, variables | 2 |
|
|
243
|
+
| `explore_human_design_transit` | Today's planets laid over a natal bodygraph | Transit-activated channels | 3 |
|
|
244
|
+
| `explore_human_design_connection` | Two bodygraphs combined, every shared channel classified | Connection channels by type | 3 |
|
|
245
|
+
| `explore_vedic_chart` | South Indian Rashi grid — sidereal placements and Lagna | Each rashi, for its placements and nakshatras | 3 |
|
|
246
|
+
| `explore_bazi_chart` | Four Pillars (四柱命盘) — Year, Month, Day, Hour | Each pillar | 3 |
|
|
247
|
+
| `explore_transit_timeline` | Upcoming transit hits in date order | Individual hits | 6 |
|
|
248
|
+
| `explore_moon_phase` | Moon dial — illumination, phase, sign, void-of-course | Recalculate for another moment | 3 |
|
|
249
|
+
|
|
250
|
+
Ask for these the way you'd ask a person: *"show me my chart"*, *"put today's transits over my Human Design"*, *"what's the moon doing right now"*. The model picks the app.
|
|
251
|
+
|
|
252
|
+
Two things worth knowing. The chart wheel and bi-wheel accept a click on an aspect line, not just on the two planets it joins — so "why does this line matter" is one click rather than a paragraph of setup. And the bodygraph's mandala toggle rearranges the whole chart into concentric rings without another API call, so switching views is free.
|
|
253
|
+
|
|
254
|
+
Screenshots of each are on the way.
|
|
255
|
+
|
|
222
256
|
## Tools at a Glance
|
|
223
257
|
|
|
224
258
|
| Category | Tool | Tier |
|
|
@@ -281,6 +315,8 @@ The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable
|
|
|
281
315
|
| `ASTROMCP_API_KEY` | No | Legacy alias for `OPENEPHEMERIS_API_KEY` (checked as fallback) |
|
|
282
316
|
| `OPENEPHEMERIS_BACKEND_URL` | No | Defaults to `https://api.openephemeris.com` |
|
|
283
317
|
| `OPENEPHEMERIS_PROFILE` | No | `dev` by default |
|
|
318
|
+
| `OPENEPHEMERIS_TOOLS` | No | `core` (default) advertises a focused everyday tool set; `full` advertises every tool. See [Tool surface](#tool-surface) |
|
|
319
|
+
| `OPENEPHEMERIS_TELEMETRY` | No | Set to `0`/`false`/`off` to disable anonymous usage reporting. `DO_NOT_TRACK=1` also works. See [Telemetry](#telemetry) |
|
|
284
320
|
| `OPENEPHEMERIS_SERVICE_KEY` | No | Internal service auth |
|
|
285
321
|
| `OPENEPHEMERIS_JWT` | No | Bearer token auth |
|
|
286
322
|
| `OPENEPHEMERIS_DEV_ALLOWLIST_PATH` | No | Override allowlist file path |
|
|
@@ -288,6 +324,42 @@ The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable
|
|
|
288
324
|
|
|
289
325
|
Legacy aliases (`ASTROMCP_*`, `MERIDIAN_*`) remain supported.
|
|
290
326
|
|
|
327
|
+
## Telemetry
|
|
328
|
+
|
|
329
|
+
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.
|
|
330
|
+
|
|
331
|
+
**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.
|
|
332
|
+
|
|
333
|
+
**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.
|
|
334
|
+
|
|
335
|
+
**To turn it off** — either works, checked before anything is sent:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
OPENEPHEMERIS_TELEMETRY=0
|
|
339
|
+
# or the cross-tool standard
|
|
340
|
+
DO_NOT_TRACK=1
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Tool surface
|
|
344
|
+
|
|
345
|
+
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.
|
|
346
|
+
|
|
347
|
+
**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.
|
|
348
|
+
|
|
349
|
+
To advertise the full catalog:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
OPENEPHEMERIS_TOOLS=full npx -y @openephemeris/mcp-server
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
On the remote HTTP server, append `?profile=full` to the connector URL (or send `X-OE-Tool-Surface: full`):
|
|
356
|
+
|
|
357
|
+
```
|
|
358
|
+
https://mcp.openephemeris.com/mcp?profile=full
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
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.
|
|
362
|
+
|
|
291
363
|
## Contributing & Support
|
|
292
364
|
|
|
293
365
|
- **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.
|
|
@@ -370,8 +442,8 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
370
442
|
|
|
371
443
|
- Allowlisted operations: **28**
|
|
372
444
|
- Methods: `GET=4`, `POST=24`, `PUT=0`, `PATCH=0`, `DELETE=0`
|
|
373
|
-
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **
|
|
374
|
-
- 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`
|
|
445
|
+
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **91**
|
|
446
|
+
- 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`
|
|
375
447
|
- Generic tools:
|
|
376
448
|
|
|
377
449
|
### 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
|
}
|