@openephemeris/mcp-server 4.16.0 → 4.18.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 (37) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +15 -15
  3. package/config/dev-allowlist.json +31 -19
  4. package/dist/backend/client.d.ts +57 -1
  5. package/dist/backend/client.js +125 -15
  6. package/dist/prompts.js +26 -24
  7. package/dist/server-sse.d.ts +18 -0
  8. package/dist/server-sse.js +41 -6
  9. package/dist/tools/apps/_render-token.d.ts +34 -0
  10. package/dist/tools/apps/_render-token.js +50 -0
  11. package/dist/tools/apps/bazi-app.js +36 -12
  12. package/dist/tools/apps/bi-wheel-app.d.ts +9 -2
  13. package/dist/tools/apps/bi-wheel-app.js +76 -117
  14. package/dist/tools/apps/bodygraph-app.js +88 -41
  15. package/dist/tools/apps/chart-wheel-app.js +56 -16
  16. package/dist/tools/apps/location-tools.js +10 -3
  17. package/dist/tools/apps/moon-phase-app.js +10 -2
  18. package/dist/tools/apps/transit-timeline-app.d.ts +3 -0
  19. package/dist/tools/apps/transit-timeline-app.js +77 -9
  20. package/dist/tools/apps/vedic-chart-app.js +10 -1
  21. package/dist/tools/datetime-historical.js +7 -2
  22. package/dist/tools/datetime.js +2 -1
  23. package/dist/tools/dev.js +7 -6
  24. package/dist/tools/specialized/electional.js +4 -3
  25. package/dist/tools/specialized/ephemeris_extended.js +19 -4
  26. package/dist/tools/specialized/hd_group.js +2 -2
  27. package/dist/tools/specialized/moon.d.ts +1 -1
  28. package/dist/tools/specialized/moon.js +51 -43
  29. package/dist/tools/specialized/progressed.js +2 -25
  30. package/dist/tools/specialized/transits.js +5 -5
  31. package/dist/ui/bazi.html +1523 -1522
  32. package/dist/ui/bi-wheel.html +406 -374
  33. package/dist/ui/bodygraph.html +83 -79
  34. package/dist/ui/chart-wheel.html +394 -361
  35. package/dist/ui/transit-timeline.html +3 -3
  36. package/dist/ui/vedic-chart.html +818 -818
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,52 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [4.18.0] — 2026-09-26
11
+
12
+ ### Fixed
13
+ - **`explore_bi_wheel` progressed, solar return and lunar return modes work again.** They sent
14
+ requests the API rejected ("Validation failed" / "birth_datetime is required"); all six modes now
15
+ return a chart.
16
+ - **Solar arc directions are exact.** `explore_bi_wheel` mode `solar_arc` used a flat ~1°-per-year
17
+ estimate and left the houses at their birth positions. It now uses the Sun's real arc from the API:
18
+ every planet, angle and house cusp is directed by the same arc, with correct signs and houses.
19
+ `ephemeris_progressed_chart` with `method: "solar_arc"` directs the angles and house cusps too.
20
+ - **Every lunar node is named Mean or True.** The charts show "North Node (Mean)", "North Node
21
+ (True)", "South Node (Mean)" and "South Node (True)" as four separate points. The two South Nodes
22
+ used to merge into one, and the mean North Node was shown as a bare "North Node". The Mean/True
23
+ toggle now applies to the South Node as well.
24
+ - **Bi-wheel recalculation keeps the house system you choose** for the outer chart too (it was always
25
+ Placidus).
26
+ - **Black Moon Lilith appears on the chart wheel, named Mean, True or Interpolated.** Asking
27
+ `explore_natal_chart` for `lilith` or `lilith_true` returned the chart without it. The three Liliths
28
+ are now shown as "Lilith (Mean)", "Lilith (True)" and "Lilith (Interpolated)" (new slug
29
+ `lilith_interpolated`), never merged into one.
30
+ - **`explore_natal_chart` with `bodies: ["all"]` works.** It was rejected because it asked for Vertex and
31
+ Part of Fortune as extra bodies; "all" now covers every body the chart can add.
32
+
33
+ ### Removed
34
+ - **`include_visual` on `ephemeris_progressed_chart`.** The API never draws a progressed chart
35
+ image, so the option returned no picture. It was never charged. To see progressions over the natal
36
+ chart, use `explore_bi_wheel` with mode `progressed`.
37
+
38
+ ## [4.17.0] — 2026-09-24
39
+
40
+ ### Added
41
+ - **Transit timelines show when each transit starts and ends.** `explore_transit_timeline` now asks
42
+ for orb windows (new `orb_deg` parameter, default 1°): every exact hit carries the in-orb window it
43
+ belongs to — the date the transit comes into orb and the date it leaves — and "pass 2 of 3" when a
44
+ retrograde brings the planet back over the same point without leaving the orb. Transits already in
45
+ orb at the start of the range are listed with whether they are applying or separating. The
46
+ interactive timeline shows the window on each card.
47
+ - **Two new API capabilities reachable through `dev_read_api`:** orb windows on
48
+ `POST /predictive/transits/search` (`orb_deg`, `aspect_orbs`, `natal_points`, `aspects`) and the new
49
+ `POST /human-design/transit-timeline` — every gate (and line) a transiting planet enters and leaves
50
+ over a date range, and when a transit completes one of your channels. 121 public endpoints.
51
+
52
+ ### Fixed
53
+ - **`dev_read_api` now reaches three endpoints it was missing:** `/ephemeris/draconic`,
54
+ `/ephemeris/prenatal-lunation` and `/predictive/primary-directions` (124 allowlisted operations).
55
+
10
56
  ## [4.16.0] — 2026-09-21
11
57
 
12
58
  ### Changed
package/README.md CHANGED
@@ -212,7 +212,7 @@ The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable
212
212
 
213
213
  - Missing/invalid credentials (`401`): tool call fails with a message that points users to sign up/sign in at `https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount`, then create/manage keys in `https://openephemeris.com/dashboard?tab=account`.
214
214
  - Tier-gated endpoint (`403`): tool call returns an upgrade-required message with `https://openephemeris.com/pay` and dashboard billing/key management link.
215
- - Monthly quota exhausted (`402`): tool call returns usage quota guidance with both dashboard (`/dashboard?tab=account`) and upgrade (`/pay`) links.
215
+ - Out of credits (`402`): tool call returns a one-tap top-up link (Explorer's 150 free credits are one-time and do not reset; plan allowances renew each billing period) plus the dashboard usage link. Failed calls (any `4xx`/`5xx`) are refunded.
216
216
  - Burst/rate limit (`429`): tool call returns retry guidance and links to dashboard usage monitoring.
217
217
 
218
218
  ## What You Can Ask
@@ -256,13 +256,13 @@ instead of the picture, so nothing breaks, you just don't get the wheel.
256
256
  | Tool | What opens | What you can click | Credits |
257
257
  |---|---|---|---|
258
258
  | `explore_natal_chart` | Natal wheel — planets, houses, aspects, angles | Planets, houses, aspect lines; recalculate with new settings | 1 |
259
- | `explore_bi_wheel` | Two charts on one wheel: transits, synastry, progressions | Either wheel's planets, houses, and the aspects between them | 2 |
260
- | `explore_human_design` | Human Design bodygraph, with a mandala view toggle | Centers, gates, channels, planets, variables | 2 |
261
- | `explore_human_design_transit` | Today's planets laid over a natal bodygraph | Transit-activated channels | 3 |
262
- | `explore_human_design_connection` | Two bodygraphs combined, every shared channel classified | Connection channels by type | 3 |
259
+ | `explore_bi_wheel` | Two charts on one wheel: transits, synastry, progressions | Either wheel's planets, houses, and the aspects between them | 2 (6 for solar/lunar return) |
260
+ | `explore_human_design` | Human Design bodygraph, with a mandala view toggle | Centers, gates, channels, planets, variables | 4 |
261
+ | `explore_human_design_transit` | Today's planets laid over a natal bodygraph | Transit-activated channels | 5 |
262
+ | `explore_human_design_connection` | Two bodygraphs combined, every shared channel classified | Connection channels by type | 5 |
263
263
  | `explore_vedic_chart` | South Indian Rashi grid — sidereal placements and Lagna | Each rashi, for its placements and nakshatras | 3 |
264
264
  | `explore_bazi_chart` | Four Pillars (四柱命盘) — Year, Month, Day, Hour | Each pillar | 3 |
265
- | `explore_transit_timeline` | Upcoming transit hits in date order | Individual hits | 6 |
265
+ | `explore_transit_timeline` | Upcoming transit hits in date order | Individual hits | 6 for up to 1 year (priced by span) |
266
266
  | `explore_moon_phase` | Moon dial — illumination, phase, sign, void-of-course | Recalculate for another moment | 3 |
267
267
 
268
268
  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.
@@ -300,10 +300,10 @@ Screenshots of each are on the way.
300
300
  | Lunar return | `ephemeris_lunar_return` | Explorer |
301
301
  | Planetary return | `ephemeris_planetary_return` | Explorer |
302
302
  | Astrocartography lines | `acg_power_lines` | Developer |
303
- | ACG hits at location | `acg_hits` | Scale |
303
+ | ACG hits at location | `acg_hits` | Developer |
304
304
  | Venus Star Points | `venus_star_points` + 4 more | Explorer |
305
- | Chart wheel image | `ephemeris_chart_wheel` | Developer |
306
- | Bi-wheel image | `ephemeris_bi_wheel` | Developer |
305
+ | Chart wheel image | `ephemeris_chart_wheel` | Explorer |
306
+ | Bi-wheel image | `ephemeris_bi_wheel` | Explorer |
307
307
  | Dignities / Midpoints / Fixed stars | `ephemeris_dignities`, `ephemeris_midpoints`, `ephemeris_fixed_stars` | Explorer |
308
308
 
309
309
  ## Tooling Model
@@ -335,7 +335,7 @@ Screenshots of each are on the way.
335
335
  | `OPENEPHEMERIS_PROFILE` | No | `dev` by default |
336
336
  | `OPENEPHEMERIS_TOOLS` | No | `core` (default) advertises a focused everyday tool set; `full` advertises every tool. See [Tool surface](#tool-surface) |
337
337
  | `OPENEPHEMERIS_TELEMETRY` | No | Set to `0`/`false`/`off` to disable anonymous usage reporting. `DO_NOT_TRACK=1` also works. See [Telemetry](#telemetry) |
338
- | `OPENEPHEMERIS_SERVICE_KEY` | No | Internal service auth |
338
+ | `OPENEPHEMERIS_SERVICE_KEY` | No | Internal service auth (stdio only; the hosted server refuses to start with one and always authenticates as the signed-in user) |
339
339
  | `OPENEPHEMERIS_JWT` | No | Bearer token auth |
340
340
  | `OPENEPHEMERIS_DEV_ALLOWLIST_PATH` | No | Override allowlist file path |
341
341
  | `MCP_USER_ID` | No | Per-instance user identifier |
@@ -490,8 +490,8 @@ When you update the MCP server logic (handlers, bug fixes, hardening), you shoul
490
490
 
491
491
  Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live tool registry.
492
492
 
493
- - Allowlisted operations: **120**
494
- - Methods: `GET=44`, `POST=76`, `PUT=0`, `PATCH=0`, `DELETE=0`
493
+ - Allowlisted operations: **124**
494
+ - Methods: `GET=44`, `POST=80`, `PUT=0`, `PATCH=0`, `DELETE=0`
495
495
  - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **92**
496
496
  - 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`, `electional_angle_crossings`, `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`
497
497
  - Generic tools: `dev_list_allowed`, `dev_read_api`, `dev_write_api`
@@ -507,11 +507,11 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
507
507
  | `comparative` | 5 | `POST /comparative/composite`, `POST /comparative/composite/midpoint` |
508
508
  | `eclipse` | 6 | `GET /eclipse/besselian-elements`, `GET /eclipse/lunar/global` |
509
509
  | `electional` | 6 | `GET /electional/angle-crossings`, `GET /electional/aspect-search` |
510
- | `ephemeris` | 36 | `GET /ephemeris/agro/calendar`, `GET /ephemeris/agro/daily` |
510
+ | `ephemeris` | 38 | `GET /ephemeris/agro/calendar`, `GET /ephemeris/agro/daily` |
511
511
  | `health` | 2 | `GET /health`, `GET /health/detailed` |
512
- | `human-design` | 8 | `POST /human-design/chart`, `POST /human-design/composite` |
512
+ | `human-design` | 9 | `POST /human-design/chart`, `POST /human-design/composite` |
513
513
  | `location` | 2 | `GET /location/autocomplete`, `GET /location/reverse` |
514
- | `predictive` | 10 | `POST /predictive/returns`, `POST /predictive/returns/lunar` |
514
+ | `predictive` | 11 | `POST /predictive/primary-directions`, `POST /predictive/returns` |
515
515
  | `root` | 1 | `GET /` |
516
516
  | `tidal` | 2 | `GET /tidal/forcing`, `GET /tidal/forcing/deep-time` |
517
517
  | `time` | 6 | `GET /time/delta-t`, `GET /time/equation-of-time` |
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": "astromcp-dev-allowlist-v1",
3
3
  "description": "Allowlist derived from the Go sidecar openapi.json. Every entry in allow[] corresponds to a real Go endpoint. Run 'npm run check:dev-allowlist' to validate freshness.",
4
- "generated_from": "C:\\src\\openephemeris\\.claude\\worktrees\\mcp-allowlist-gaps-b8befc\\apps\\api\\go-sidecar\\openapi.json",
4
+ "generated_from": "C:\\src\\openephemeris\\.claude\\worktrees\\adoring-rhodes-871fd7\\apps\\api\\go-sidecar\\openapi.json",
5
5
  "deny": {
6
6
  "path_prefixes": [
7
7
  "/auth",
@@ -235,6 +235,10 @@
235
235
  "method": "POST",
236
236
  "path": "/ephemeris/dispositor-tree"
237
237
  },
238
+ {
239
+ "method": "POST",
240
+ "path": "/ephemeris/draconic"
241
+ },
238
242
  {
239
243
  "method": "POST",
240
244
  "path": "/ephemeris/fixed-stars"
@@ -291,6 +295,10 @@
291
295
  "method": "POST",
292
296
  "path": "/ephemeris/planet-position"
293
297
  },
298
+ {
299
+ "method": "POST",
300
+ "path": "/ephemeris/prenatal-lunation"
301
+ },
294
302
  {
295
303
  "method": "POST",
296
304
  "path": "/ephemeris/progressed"
@@ -383,6 +391,10 @@
383
391
  "method": "POST",
384
392
  "path": "/human-design/transit-chart"
385
393
  },
394
+ {
395
+ "method": "POST",
396
+ "path": "/human-design/transit-timeline"
397
+ },
386
398
  {
387
399
  "method": "POST",
388
400
  "path": "/human-design/transit"
@@ -395,6 +407,10 @@
395
407
  "method": "GET",
396
408
  "path": "/location/reverse"
397
409
  },
410
+ {
411
+ "method": "POST",
412
+ "path": "/predictive/primary-directions"
413
+ },
398
414
  {
399
415
  "method": "POST",
400
416
  "path": "/predictive/returns/lunar"
@@ -496,8 +512,8 @@
496
512
  "path": "/visualization/chart-wheel"
497
513
  }
498
514
  ],
499
- "last_generated_at": "2026-08-31T23:51:24.873Z",
500
- "openapi_sha256": "4347724c414ef94075a3db63016a4935ca7b533ecf18f37fac1ca16c22ca0195",
515
+ "last_generated_at": "2026-09-24T18:29:22.473Z",
516
+ "openapi_sha256": "f5927cecddb0f8629f7c94d380ca878e6780bc491f85490bacb2508e1b721b3d",
501
517
  "candidates_get": [
502
518
  {
503
519
  "method": "GET",
@@ -893,22 +909,6 @@
893
909
  ],
894
910
  "operationId": "declination_lines_acg_declination_lines_post"
895
911
  },
896
- {
897
- "method": "POST",
898
- "path": "/acg/features",
899
- "tags": [
900
- "ACG"
901
- ],
902
- "operationId": "features_acg_features_post"
903
- },
904
- {
905
- "method": "POST",
906
- "path": "/acg/heatmap/composite",
907
- "tags": [
908
- "Astrocartography"
909
- ],
910
- "operationId": "AcgHeatmapCompositeAcgHeatmapCompositePost"
911
- },
912
912
  {
913
913
  "method": "POST",
914
914
  "path": "/acg/hermetic-lines",
@@ -1341,6 +1341,14 @@
1341
1341
  ],
1342
1342
  "operationId": "human_design_transit_chart_human_design_transit_chart_post"
1343
1343
  },
1344
+ {
1345
+ "method": "POST",
1346
+ "path": "/human-design/transit-timeline",
1347
+ "tags": [
1348
+ "Human Design"
1349
+ ],
1350
+ "operationId": "human_design_transit_timeline_human_design_transit_timeline_post"
1351
+ },
1344
1352
  {
1345
1353
  "method": "POST",
1346
1354
  "path": "/human-design/transit",
@@ -1538,6 +1546,7 @@
1538
1546
  "Non-GET allowlisted: POST /ephemeris/declinations",
1539
1547
  "Non-GET allowlisted: POST /ephemeris/dignities",
1540
1548
  "Non-GET allowlisted: POST /ephemeris/dispositor-tree",
1549
+ "Non-GET allowlisted: POST /ephemeris/draconic",
1541
1550
  "Non-GET allowlisted: POST /ephemeris/fixed-stars",
1542
1551
  "Non-GET allowlisted: POST /ephemeris/harmonics",
1543
1552
  "Non-GET allowlisted: POST /ephemeris/hermetic-lots",
@@ -1547,6 +1556,7 @@
1547
1556
  "Non-GET allowlisted: POST /ephemeris/natal-chart",
1548
1557
  "Non-GET allowlisted: POST /ephemeris/natal/batch",
1549
1558
  "Non-GET allowlisted: POST /ephemeris/planet-position",
1559
+ "Non-GET allowlisted: POST /ephemeris/prenatal-lunation",
1550
1560
  "Non-GET allowlisted: POST /ephemeris/progressed",
1551
1561
  "Non-GET allowlisted: POST /ephemeris/relocation",
1552
1562
  "Non-GET allowlisted: POST /ephemeris/retrograde-status",
@@ -1563,7 +1573,9 @@
1563
1573
  "Non-GET allowlisted: POST /human-design/cycles/solar-return",
1564
1574
  "Non-GET allowlisted: POST /human-design/penta",
1565
1575
  "Non-GET allowlisted: POST /human-design/transit-chart",
1576
+ "Non-GET allowlisted: POST /human-design/transit-timeline",
1566
1577
  "Non-GET allowlisted: POST /human-design/transit",
1578
+ "Non-GET allowlisted: POST /predictive/primary-directions",
1567
1579
  "Non-GET allowlisted: POST /predictive/returns/lunar",
1568
1580
  "Non-GET allowlisted: POST /predictive/returns/solar",
1569
1581
  "Non-GET allowlisted: POST /predictive/returns",
@@ -6,7 +6,29 @@ export interface BackendConfig {
6
6
  apiKey?: string;
7
7
  /** Back-compat: previous name for JWT token (Bearer). */
8
8
  authToken?: string;
9
+ /**
10
+ * When false, the client uses ONLY the credentials passed in this config and
11
+ * never falls back to the process environment (OPENEPHEMERIS_SERVICE_KEY,
12
+ * OPENEPHEMERIS_API_KEY, OPENEPHEMERIS_JWT and their legacy aliases).
13
+ *
14
+ * The hosted HTTP server builds one client per user session and MUST pass
15
+ * false: the interceptor sends X-Service-Key ahead of the user's own key, so
16
+ * an env credential inherited by a per-session client would make every
17
+ * user's calls unmetered (service key) or billed to one account (API key /
18
+ * JWT). Defaults to true so the stdio/local server keeps reading its config
19
+ * from the environment exactly as before.
20
+ */
21
+ inheritEnvCredentials?: boolean;
9
22
  }
23
+ /** Env vars holding a service key — bypasses metering; never valid on the hosted server. */
24
+ export declare const SERVICE_KEY_ENV_VARS: readonly ["OPENEPHEMERIS_SERVICE_KEY", "ASTROMCP_SERVICE_KEY", "MERIDIAN_SERVICE_KEY"];
25
+ /** Env vars holding one user's credential — on the hosted server they would bill every session to one account. */
26
+ export declare const USER_CREDENTIAL_ENV_VARS: readonly ["OPENEPHEMERIS_API_KEY", "ASTROMCP_API_KEY", "MERIDIAN_API_KEY", "OPENEPHEMERIS_JWT", "ASTROMCP_JWT", "MERIDIAN_AUTH_TOKEN"];
27
+ /** Names of the credential env vars that are set (non-empty) in `env`. */
28
+ export declare function presentCredentialEnvVars(env?: NodeJS.ProcessEnv): {
29
+ serviceKeys: string[];
30
+ userCredentials: string[];
31
+ };
10
32
  export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
11
33
  export interface BackendRequestOptions {
12
34
  params?: Record<string, unknown>;
@@ -25,8 +47,33 @@ export declare const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboar
25
47
  export declare const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
26
48
  export declare const UPGRADE_URL = "https://openephemeris.com/pricing";
27
49
  /** One-tap $5 → 150-credit top-up (signs the user in if needed, then redirects to the prefilled Stripe Payment Link). */
28
- export declare const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5";
50
+ export declare const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5&src=mcp_402";
51
+ /**
52
+ * The API's 402 carries `upgrade.topup_url`: the same /topup link, signed with
53
+ * the caller's account so it opens Stripe checkout without a sign-in step
54
+ * (most people hitting the wall are in ChatGPT/Claude and not signed in on the
55
+ * site). Use it when present, re-tagged src=mcp_402 so the funnel can tell MCP
56
+ * clicks from raw API ones; otherwise fall back to the unsigned link.
57
+ */
58
+ export declare function resolveTopupUrl(apiTopupUrl: unknown): string;
29
59
  export declare const WALLET_TOPUP_URL = "https://openephemeris.com/wallet";
60
+ /**
61
+ * Whether an error body (typically a 422) is really the credit wall: the
62
+ * detail / type / field codes mention insufficient credits or an exceeded
63
+ * quota. Checked over the whole serialized body because the signal can sit in
64
+ * `detail`, `type`, or a nested `errors[].code`.
65
+ */
66
+ export declare function looksLikeCreditExhaustion(data: unknown, msg?: string): boolean;
67
+ /**
68
+ * True for errors that mean "this account cannot be charged / is not allowed"
69
+ * — the credit wall (402), missing auth (401), or a tier gate (403). Tools
70
+ * that fan out or degrade gracefully must rethrow these instead of swallowing
71
+ * them: a placeholder result at zero credits hides the paywall (and its
72
+ * top-up link) from the assistant and reads as real data.
73
+ */
74
+ export declare function isBillingOrAuthError(err: unknown): err is BackendError;
75
+ /** Rethrows the first billing/auth BackendError among settled results, if any. */
76
+ export declare function rethrowBillingOrAuth(results: PromiseSettledResult<unknown>[]): void;
30
77
  export declare class BackendError extends Error {
31
78
  readonly status: number;
32
79
  readonly code: string;
@@ -59,6 +106,15 @@ export declare class BackendClient {
59
106
  * (JWT is the lowest-priority credential in the interceptor).
60
107
  */
61
108
  setJwt(jwt: string): void;
109
+ /**
110
+ * Drop every credential this client holds (service key, API key, JWT). The
111
+ * hosted HTTP server calls this on the module singleton at startup: the
112
+ * singleton is what getActiveClient() falls back to outside a session
113
+ * context, and it was built from the environment at import time.
114
+ */
115
+ clearCredentials(): void;
116
+ /** Which configured credential the interceptor will send (diagnostics/tests). */
117
+ credentialKind(): "service_key" | "api_key" | "jwt" | null;
62
118
  /**
63
119
  * Start the device auth flow in the background if not already running.
64
120
  * Called automatically when no credentials are found in the interceptor.
@@ -2,6 +2,29 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import axios, { AxiosError } from "axios";
3
3
  import { CredentialManager } from "../auth/credentials.js";
4
4
  import { DeviceAuthFlow } from "../auth/device-auth.js";
5
+ /** Env vars holding a service key — bypasses metering; never valid on the hosted server. */
6
+ export const SERVICE_KEY_ENV_VARS = [
7
+ "OPENEPHEMERIS_SERVICE_KEY",
8
+ "ASTROMCP_SERVICE_KEY",
9
+ "MERIDIAN_SERVICE_KEY",
10
+ ];
11
+ /** Env vars holding one user's credential — on the hosted server they would bill every session to one account. */
12
+ export const USER_CREDENTIAL_ENV_VARS = [
13
+ "OPENEPHEMERIS_API_KEY",
14
+ "ASTROMCP_API_KEY",
15
+ "MERIDIAN_API_KEY",
16
+ "OPENEPHEMERIS_JWT",
17
+ "ASTROMCP_JWT",
18
+ "MERIDIAN_AUTH_TOKEN",
19
+ ];
20
+ /** Names of the credential env vars that are set (non-empty) in `env`. */
21
+ export function presentCredentialEnvVars(env = process.env) {
22
+ const isSet = (name) => typeof env[name] === "string" && env[name].trim() !== "";
23
+ return {
24
+ serviceKeys: SERVICE_KEY_ENV_VARS.filter(isSet),
25
+ userCredentials: USER_CREDENTIAL_ENV_VARS.filter(isSet),
26
+ };
27
+ }
5
28
  const DEFAULT_TIMEOUT_MS = 45_000;
6
29
  const RETRY_DELAYS_MS = [1_000, 2_000, 4_000]; // Three retries with backoff
7
30
  // NOTE: 429 is intentionally NOT retryable. Every expensive compute endpoint is
@@ -23,8 +46,64 @@ export const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboard?tab=ac
23
46
  export const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
24
47
  export const UPGRADE_URL = "https://openephemeris.com/pricing";
25
48
  /** One-tap $5 → 150-credit top-up (signs the user in if needed, then redirects to the prefilled Stripe Payment Link). */
26
- export const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5";
49
+ export const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5&src=mcp_402";
50
+ /**
51
+ * The API's 402 carries `upgrade.topup_url`: the same /topup link, signed with
52
+ * the caller's account so it opens Stripe checkout without a sign-in step
53
+ * (most people hitting the wall are in ChatGPT/Claude and not signed in on the
54
+ * site). Use it when present, re-tagged src=mcp_402 so the funnel can tell MCP
55
+ * clicks from raw API ones; otherwise fall back to the unsigned link.
56
+ */
57
+ export function resolveTopupUrl(apiTopupUrl) {
58
+ if (typeof apiTopupUrl !== "string")
59
+ return TOPUP_URL;
60
+ try {
61
+ const url = new URL(apiTopupUrl);
62
+ if (url.protocol !== "https:" || url.hostname !== "openephemeris.com" || url.pathname !== "/topup") {
63
+ return TOPUP_URL;
64
+ }
65
+ url.searchParams.set("src", "mcp_402");
66
+ return url.toString();
67
+ }
68
+ catch {
69
+ return TOPUP_URL;
70
+ }
71
+ }
27
72
  export const WALLET_TOPUP_URL = "https://openephemeris.com/wallet";
73
+ const CREDIT_EXHAUSTION_RE = /credits?_insufficient|insufficient[ _-]?credits?|not enough credits|out of credits|credits?[ _-]exhausted|quota[ _-]exceeded|usage_quota|requires \d+ credits/i;
74
+ /**
75
+ * Whether an error body (typically a 422) is really the credit wall: the
76
+ * detail / type / field codes mention insufficient credits or an exceeded
77
+ * quota. Checked over the whole serialized body because the signal can sit in
78
+ * `detail`, `type`, or a nested `errors[].code`.
79
+ */
80
+ export function looksLikeCreditExhaustion(data, msg) {
81
+ let text = msg ?? "";
82
+ try {
83
+ text += " " + (typeof data === "string" ? data : JSON.stringify(data ?? ""));
84
+ }
85
+ catch {
86
+ // unserializable body — fall back to the message alone
87
+ }
88
+ return CREDIT_EXHAUSTION_RE.test(text);
89
+ }
90
+ /**
91
+ * True for errors that mean "this account cannot be charged / is not allowed"
92
+ * — the credit wall (402), missing auth (401), or a tier gate (403). Tools
93
+ * that fan out or degrade gracefully must rethrow these instead of swallowing
94
+ * them: a placeholder result at zero credits hides the paywall (and its
95
+ * top-up link) from the assistant and reads as real data.
96
+ */
97
+ export function isBillingOrAuthError(err) {
98
+ return err instanceof BackendError && (err.status === 402 || err.status === 401 || err.status === 403);
99
+ }
100
+ /** Rethrows the first billing/auth BackendError among settled results, if any. */
101
+ export function rethrowBillingOrAuth(results) {
102
+ for (const r of results) {
103
+ if (r.status === "rejected" && isBillingOrAuthError(r.reason))
104
+ throw r.reason;
105
+ }
106
+ }
28
107
  function sleep(ms) {
29
108
  return new Promise((resolve) => setTimeout(resolve, ms));
30
109
  }
@@ -53,22 +132,24 @@ export class BackendClient {
53
132
  _authFlowResult = null;
54
133
  constructor(config) {
55
134
  this.userId = config.userId || "anonymous";
135
+ // See BackendConfig.inheritEnvCredentials — per-session hosted clients opt out.
136
+ const env = config.inheritEnvCredentials === false ? {} : process.env;
56
137
  this.jwt =
57
138
  config.jwt ||
58
139
  config.authToken ||
59
- process.env.OPENEPHEMERIS_JWT ||
60
- process.env.ASTROMCP_JWT ||
61
- process.env.MERIDIAN_AUTH_TOKEN;
140
+ env.OPENEPHEMERIS_JWT ||
141
+ env.ASTROMCP_JWT ||
142
+ env.MERIDIAN_AUTH_TOKEN;
62
143
  this.serviceKey =
63
144
  config.serviceKey ||
64
- process.env.OPENEPHEMERIS_SERVICE_KEY ||
65
- process.env.ASTROMCP_SERVICE_KEY ||
66
- process.env.MERIDIAN_SERVICE_KEY;
145
+ env.OPENEPHEMERIS_SERVICE_KEY ||
146
+ env.ASTROMCP_SERVICE_KEY ||
147
+ env.MERIDIAN_SERVICE_KEY;
67
148
  this.apiKey =
68
149
  config.apiKey ||
69
- process.env.OPENEPHEMERIS_API_KEY ||
70
- process.env.ASTROMCP_API_KEY ||
71
- process.env.MERIDIAN_API_KEY;
150
+ env.OPENEPHEMERIS_API_KEY ||
151
+ env.ASTROMCP_API_KEY ||
152
+ env.MERIDIAN_API_KEY;
72
153
  this.credentialManager = new CredentialManager();
73
154
  this.client = axios.create({
74
155
  baseURL: config.baseURL ||
@@ -138,6 +219,27 @@ export class BackendClient {
138
219
  setJwt(jwt) {
139
220
  this.jwt = jwt;
140
221
  }
222
+ /**
223
+ * Drop every credential this client holds (service key, API key, JWT). The
224
+ * hosted HTTP server calls this on the module singleton at startup: the
225
+ * singleton is what getActiveClient() falls back to outside a session
226
+ * context, and it was built from the environment at import time.
227
+ */
228
+ clearCredentials() {
229
+ this.serviceKey = undefined;
230
+ this.apiKey = undefined;
231
+ this.jwt = undefined;
232
+ }
233
+ /** Which configured credential the interceptor will send (diagnostics/tests). */
234
+ credentialKind() {
235
+ if (this.serviceKey)
236
+ return "service_key";
237
+ if (this.apiKey)
238
+ return "api_key";
239
+ if (this.jwt)
240
+ return "jwt";
241
+ return null;
242
+ }
141
243
  /**
142
244
  * Start the device auth flow in the background if not already running.
143
245
  * Called automatically when no credentials are found in the interceptor.
@@ -270,7 +372,12 @@ export class BackendClient {
270
372
  `through Claude's connector settings, your session expired: in Claude's connector ` +
271
373
  `settings, toggle OpenEphemeris off and on to reconnect — this only takes a moment.`, 401, "auth_required", false, LOGIN_SIGNUP_URL);
272
374
  }
273
- if (status === 402) {
375
+ // A 422 whose body says the caller ran out of credits is the credit wall
376
+ // wearing a validation status (the Go visual add-on reservation returned
377
+ // 422 `credits_insufficient` before it moved to 402). Route it through
378
+ // the same 402 path so the assistant gets the one credit-wall message
379
+ // and top-up link, not "Validation error: ...".
380
+ if (status === 402 || (status === 422 && looksLikeCreditExhaustion(data, msg))) {
274
381
  // PRICING SSOT: apps/web/config/billing-plans.ts — Explorer grant
275
382
  // (EXPLORER.creditsNumeric = 150, one-time), Pro/`developer`
276
383
  // (monthlyPrice = 29, credits = 75,000), and CREDIT_TOPUPS
@@ -289,18 +396,21 @@ export class BackendClient {
289
396
  // One direct link, not a dashboard tab: /topup?pack=payg_5 signs the
290
397
  // user in if needed and redirects straight to the prefilled Stripe
291
398
  // Payment Link for the $5 → 150-credit pack.
399
+ const topupUrl = resolveTopupUrl(dataObj.upgrade?.topup_url);
292
400
  let upsellMsg = "";
293
- let actionUrl = TOPUP_URL;
401
+ let actionUrl = topupUrl;
294
402
  if (currentTier === 'explorer') {
295
403
  upsellMsg =
296
- `Let the user know warmly: Their 150 free credits are used up. ` +
297
- `$5 gets 150 more — one tap: ${TOPUP_URL}. ` +
404
+ `Let the user know warmly: Their 150 free credits are used up (they do not reset). ` +
405
+ `$5 gets 150 more — one tap, no sign-in, opens checkout directly: ${topupUrl} ` +
406
+ `(show this exact link to the user as a clickable link). ` +
298
407
  `Or Pro ($29/mo, 75,000 credits): ${UPGRADE_URL}?plan=developer`;
299
408
  }
300
409
  else if (currentTier === 'payg') {
301
410
  upsellMsg =
302
411
  `Let the user know warmly: Their wallet balance is empty. ` +
303
- `$5 gets 150 more credits — one tap: ${TOPUP_URL}. ` +
412
+ `$5 gets 150 more credits — one tap, no sign-in, opens checkout directly: ${topupUrl} ` +
413
+ `(show this exact link to the user as a clickable link). ` +
304
414
  `Or Pro ($29/mo, 75,000 credits): ${UPGRADE_URL}?plan=developer`;
305
415
  }
306
416
  else {
package/dist/prompts.js CHANGED
@@ -36,8 +36,8 @@ export const PROMPTS = [
36
36
  "| `solar_return_year_ahead` | Annual Solar Return chart — themes for the coming birthday year |\n" +
37
37
  "| `developer_api_integration` | Help a dev integrate the Open Ephemeris API into their app |\n\n" +
38
38
  "## Before Calling Any Tool — Three Rules\n" +
39
- "1. **Coordinates first**: Convert city names to decimal lat/lon before calling tools " +
40
- "(e.g. Chicago → 41.8781, -87.6298). Positive = North/East; negative = South/West.\n" +
39
+ "1. **Coordinates first**: Resolve place names with `location_search` — never recall " +
40
+ "coordinates from memory. Positive = North/East; negative = South/West.\n" +
41
41
  "2. **Datetime format**: Pass birth times as local ISO 8601 + separate `timezone` IANA name " +
42
42
  "(e.g. `America/Chicago`), OR as a UTC-offset datetime `1990-04-15T14:30:00-05:00`. " +
43
43
  "Never append `Z` to a local birth time — `Z` means UTC.\n" +
@@ -870,7 +870,7 @@ export const PROMPTS = [
870
870
  "| Feature | MCP Tool | REST Endpoint | Credits |\n" +
871
871
  "|---------|----------|---------------|---------|\n" +
872
872
  "| Natal chart | `ephemeris_natal_chart` | `POST /ephemeris/natal-chart` | 1 |\n" +
873
- "| Natal batch (up to 100) | `ephemeris_natal_batch` | `POST /ephemeris/natal/batch` | 1/subject |\n" +
873
+ "| Natal batch (up to 100, Startup+) | `ephemeris_natal_batch` | `POST /ephemeris/natal/batch` | 1/subject |\n" +
874
874
  "| Vedic / Jyotish chart | `vedic_chart` | `POST /vedic/chart` | 1 |\n" +
875
875
  "| Human Design | `human_design_chart` | `POST /human-design/chart` | 2 |\n" +
876
876
  "| BaZi Four Pillars | `chinese_bazi` | `POST /chinese/bazi` | 1 |\n" +
@@ -879,38 +879,39 @@ export const PROMPTS = [
879
879
  "| Midpoint composite | `ephemeris_composite_midpoint` | `POST /comparative/composite/midpoint` | 3 |\n" +
880
880
  "| House overlay | `ephemeris_overlay` | `POST /comparative/overlay` | 3 |\n" +
881
881
  "| Natal transits (now) | `ephemeris_natal_transits` | `POST /comparative/natal-transits` | 3 |\n" +
882
- "| Transit search (range) | `ephemeris_transits` | `POST /predictive/transits/search` | 6 |\n" +
882
+ "| Transit search (range) | `ephemeris_transits` | `POST /predictive/transits/search` | 5–70 by span (≤1y 5 … ≤40y 70); tool +1 for the natal chart |\n" +
883
883
  "| Relocation chart | `ephemeris_relocation` | `POST /ephemeris/relocation` | 1 |\n" +
884
884
  "| Solar return | `ephemeris_solar_return` | `POST /predictive/returns/solar` | 5 |\n" +
885
885
  "| Lunar return | `ephemeris_lunar_return` | `POST /predictive/returns/lunar` | 5 |\n" +
886
886
  "| Progressions | `ephemeris_progressed_chart` | `POST /ephemeris/progressed` | 1 |\n" +
887
- "| ACG power lines | `acg_power_lines` | `POST /acg/power-lines` | 10 |\n" +
888
- "| ACG city hits | `acg_hits` | `POST /acg/hits` | 15 |\n" +
887
+ "| ACG power lines (Pro+) | `acg_power_lines` | `POST /acg/power-lines` | 10 |\n" +
888
+ "| ACG city hits (Pro+) | `acg_hits` | `POST /acg/hits` | 10 |\n" +
889
889
  "| Eclipse finder | `ephemeris_next_eclipse` | `GET /eclipse/next-visible` | 1 |\n" +
890
- "| Moon phase (current) | `ephemeris_moon_phase` | `GET /ephemeris/moon/phase` | 1 |\n" +
891
- "| Next lunar phase | `ephemeris_next_lunar_phase` | `GET /calendar/astrology/moon-phases` | 1 |\n" +
892
- "| Electional windows | `ephemeris_electional` | `GET /electional/find-window` | 5 |\n" +
893
- "| Electional score | `electional_moment_analysis` | `GET /electional/moment-analysis` | 1 |\n" +
890
+ "| Moon phase (current) | `ephemeris_moon_phase` | `GET /ephemeris/moon/phase` + `/void-of-course` | 2 |\n" +
891
+ "| Next lunar phase | `ephemeris_next_lunar_phase` | `GET /calendar/astrology/moon-phases` | 2 per occurrence |\n" +
892
+ "| Electional windows (Pro+) | `ephemeris_electional` | `GET /electional/find-window` | 5 / 8 / 12 for ≤30 / 60 / 120 days |\n" +
893
+ "| Electional score | `electional_moment_analysis` | `GET /electional/moment-analysis` | 5 |\n" +
894
894
  "| Dignities | `ephemeris_dignities` | `POST /ephemeris/dignities` | 1 |\n" +
895
895
  "| Hermetic lots | `ephemeris_hermetic_lots` | `POST /ephemeris/hermetic-lots` | 1 |\n" +
896
896
  "| Midpoints | `ephemeris_midpoints` | `POST /ephemeris/midpoints` | 1 |\n\n" +
897
897
  "## Step 3 — Critical Implementation Patterns\n\n" +
898
898
  "### ⚠️ Datetime Handling — Most Common Source of Bugs\n" +
899
899
  "```\n" +
900
- "# CORRECT — Local birth time + IANA timezone (Western astrology)\n" +
900
+ "# CORRECT — local wall-clock time + IANA zone (server applies historical DST)\n" +
901
901
  "POST /ephemeris/natal-chart\n" +
902
- "{ \"datetime\": \"1990-04-15T14:30:00\", \"timezone\": \"America/Chicago\" }\n\n" +
903
- "# ALSO CORRECT — Local time with UTC offset embedded\n" +
904
- "{ \"datetime\": \"1990-04-15T14:30:00-05:00\" }\n\n" +
905
- "# CORRECT — UTC required for HD and Vedic\n" +
902
+ "{ \"subject\": { \"name\": \"A\", \"birth_datetime\": { \"iso\": \"1990-04-15T14:30:00\" },\n" +
903
+ " \"birth_location\": { \"latitude\": { \"decimal\": 41.88 }, \"longitude\": { \"decimal\": -87.63 },\n" +
904
+ " \"timezone\": { \"iana_name\": \"America/Chicago\" } } } }\n\n" +
906
905
  "POST /human-design/chart\n" +
907
- "{ \"datetime\": \"1990-04-15T19:30:00Z\" } // Z = UTC\n\n" +
908
- "# WRONG — Never append Z to a local time\n" +
909
- "{ \"datetime\": \"1990-04-15T14:30:00Z\" } // ❌ reads as UTC 2:30pm, not Central 2:30pm\n" +
906
+ "{ \"birth_datetime_local\": \"1990-04-15T14:30:00\", \"timezone\": { \"iana_name\": \"America/Chicago\" },\n" +
907
+ " \"latitude\": 41.88, \"longitude\": -87.63 }\n\n" +
908
+ "# ALSO CORRECT — an explicit instant (offset or Z): trusted as-is\n" +
909
+ "\"1990-04-15T14:30:00-05:00\" or \"1990-04-15T19:30:00Z\"\n\n" +
910
+ "# WRONG — Z on a local time reads as UTC 2:30pm, not Central 2:30pm (no error raised)\n" +
911
+ "\"1990-04-15T14:30:00Z\"\n" +
910
912
  "```\n\n" +
911
- "**Rule of thumb**:\n" +
912
- "- Natal charts (Western, BaZi, electional): local time + `timezone` IANA name\n" +
913
- "- Human Design, Vedic: always UTC with Z\n\n" +
913
+ "**Rule of thumb**: on every endpoint, prefer local time + IANA zone. A zone-less clock time " +
914
+ "with no zone is a hard 400; a bare date resolves to 12:00 UTC.\n\n" +
914
915
  "### Coordinates\n" +
915
916
  "- Decimal degrees, at least 4 decimal places\n" +
916
917
  "- Positive = North/East; Negative = South/West\n" +
@@ -938,9 +939,10 @@ export const PROMPTS = [
938
939
  "### Credit Optimization\n" +
939
940
  "- **Batch natal charts**: use `POST /ephemeris/natal/batch` for multi-user onboarding (1 credit/subject vs N calls)\n" +
940
941
  "- **Current transits**: prefer `ephemeris_natal_transits` (3 credits) over " +
941
- "`ephemeris_transits` (6 credits) when you only need right-now, not a date range\n" +
942
- "- **ACG**: call `acg_hits` (city-level, 15 credits) only for specific targets; " +
943
- "use `acg_power_lines` (10 credits) for global maps\n" +
942
+ "`ephemeris_transits` (6+ credits, priced by span) when you only need right-now, not a date range\n" +
943
+ "- **Range searches**: priced by the span you ask for, before compute; a span over the plan cap " +
944
+ "(transit search: Explorer/PAYG 1y, Pro 5y, Startup 10y, Scale 40y) is a 400 `search_span_limit` — split it\n" +
945
+ "- **Failed calls are free**: every 4xx/5xx response is refunded\n" +
944
946
  "- **Format matters**: `format: 'llm'` reduces token usage in your LLM calls significantly\n\n" +
945
947
  "## Step 4 — Generate a Code Scaffold\n" +
946
948
  "Ask: 'What language would you like the scaffold in? And what is the first feature you want to implement?'\n\n" +