@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/LICENSE +21 -21
  3. package/README.md +75 -3
  4. package/dist/analytics.js +37 -5
  5. package/dist/backend/client.d.ts +7 -0
  6. package/dist/backend/client.js +39 -38
  7. package/dist/index.js +64 -2
  8. package/dist/oauth/session-utils.d.ts +42 -18
  9. package/dist/oauth/session-utils.js +79 -0
  10. package/dist/prompts.js +55 -42
  11. package/dist/server-sse.js +114 -14
  12. package/dist/tools/apps/bazi-app.js +15 -28
  13. package/dist/tools/apps/bi-wheel-app.js +14 -11
  14. package/dist/tools/apps/bodygraph-app.d.ts +5 -5
  15. package/dist/tools/apps/bodygraph-app.js +159 -212
  16. package/dist/tools/apps/chart-wheel-app.js +21 -20
  17. package/dist/tools/apps/location-tools.js +167 -18
  18. package/dist/tools/apps/moon-phase-app.js +10 -3
  19. package/dist/tools/apps/transit-timeline-app.js +6 -4
  20. package/dist/tools/apps/vedic-chart-app.js +15 -49
  21. package/dist/tools/datetime.d.ts +65 -0
  22. package/dist/tools/datetime.js +153 -0
  23. package/dist/tools/dev.js +4 -3
  24. package/dist/tools/index.d.ts +45 -2
  25. package/dist/tools/index.js +81 -2
  26. package/dist/tools/specialized/account.d.ts +1 -0
  27. package/dist/tools/specialized/account.js +100 -0
  28. package/dist/tools/specialized/acg.js +16 -14
  29. package/dist/tools/specialized/bazi.d.ts +7 -1
  30. package/dist/tools/specialized/bazi.js +89 -23
  31. package/dist/tools/specialized/bi_wheel.js +5 -4
  32. package/dist/tools/specialized/chart_wheel.js +5 -8
  33. package/dist/tools/specialized/comparative.js +40 -21
  34. package/dist/tools/specialized/electional.js +13 -10
  35. package/dist/tools/specialized/ephemeris_core.js +13 -8
  36. package/dist/tools/specialized/ephemeris_extended.js +60 -53
  37. package/dist/tools/specialized/hd_bodygraph.js +8 -10
  38. package/dist/tools/specialized/hd_cycles.js +7 -14
  39. package/dist/tools/specialized/hd_group.js +20 -13
  40. package/dist/tools/specialized/human_design.js +11 -17
  41. package/dist/tools/specialized/moon.js +14 -6
  42. package/dist/tools/specialized/natal.js +7 -9
  43. package/dist/tools/specialized/progressed.js +12 -8
  44. package/dist/tools/specialized/relocation.js +9 -3
  45. package/dist/tools/specialized/returns.js +23 -11
  46. package/dist/tools/specialized/synastry.js +17 -6
  47. package/dist/tools/specialized/transits.js +9 -5
  48. package/dist/tools/specialized/vedic.js +5 -3
  49. package/dist/tools/specialized/venus_star_points.js +14 -9
  50. package/dist/ui/bazi.html +1063 -1049
  51. package/dist/ui/bi-wheel.html +4188 -4128
  52. package/dist/ui/bodygraph.html +3673 -3616
  53. package/dist/ui/chart-wheel.html +3769 -3713
  54. package/dist/ui/moon-phase.html +3219 -3153
  55. package/dist/ui/transit-timeline.html +199 -170
  56. package/dist/ui/vedic-chart.html +1116 -1098
  57. package/package.json +3 -2
  58. 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
- arguments: { datetime: "1990-04-15T14:30:00", latitude: 41.8781, longitude: -87.6298, format: "llm" },
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`): **90**
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 the remote MCP server.
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 (Fly secrets / env):
10
- * POSTHOG_API_KEY — the project API key (phc_...); absent → all no-ops.
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
- return process.env.POSTHOG_API_KEY || undefined;
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) {
@@ -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;
@@ -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
- try {
147
- const flow = new DeviceAuthFlow();
148
- const startResult = await flow.start();
149
- this._authFlowResult = {
150
- verification_uri: startResult.verification_uri,
151
- user_code: startResult.user_code,
152
- verification_uri_complete: startResult.verification_uri_complete,
153
- };
154
- // Print to stderr so it's visible in MCP host logs
155
- console.error(`\n🔗 OpenEphemeris: Account connection required.\n` +
156
- ` Visit: ${startResult.verification_uri}\n` +
157
- ` Code: ${startResult.user_code}\n` +
158
- ` Or: ${startResult.verification_uri_complete}\n`);
159
- // Poll in background (don't await)
160
- this._pendingAuthFlow = flow
161
- .poll(startResult.device_code, (attempt) => {
162
- if (attempt % 12 === 0) {
163
- console.error(` Still waiting... Enter code ${startResult.user_code} at ${startResult.verification_uri}`);
164
- }
165
- })
166
- .then(() => {
167
- console.error(`✅ Authenticated! Subsequent requests will use your account.`);
168
- this._authFlowResult = null;
169
- })
170
- .catch((err) => {
171
- console.error(`❌ Device auth failed: ${err.message}`);
172
- this._pendingAuthFlow = null; // Allow retry
173
- });
174
- }
175
- catch (err) {
176
- console.error(`Could not start device auth: ${err.message}`);
177
- // Don't block — the request will go out unauthenticated and
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 (429, 502, 503, 504, network errors)
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
  }