@openephemeris/mcp-server 4.7.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +35 -3
- package/dist/index.js +5 -2
- package/dist/oauth/token.js +30 -3
- package/dist/server-sse.js +7 -2
- package/dist/tools/apps/bazi-app.js +3 -3
- package/dist/tools/apps/bi-wheel-app.js +52 -18
- package/dist/tools/apps/bodygraph-app.js +4 -1
- package/dist/tools/index.d.ts +41 -2
- package/dist/tools/index.js +201 -4
- package/dist/tools/specialized/moon.js +8 -11
- package/dist/ui/chart-wheel.html +56 -53
- package/package.json +4 -2
- package/smithery.yaml +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [4.8.0] — 2026-08-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Toolsets by tradition.** Tool definitions are re-sent to the model on every
|
|
14
|
+
message, so callers who work in one tradition can now advertise just that
|
|
15
|
+
tradition instead of the general-purpose default: `OPENEPHEMERIS_TOOLS=hd`
|
|
16
|
+
(stdio) or `?profile=hd,bazi` (hosted). Eight toolsets — `astrology`, `moon`,
|
|
17
|
+
`hd`, `bazi`, `vedic`, `acg`, `electional`, `venus` — each including
|
|
18
|
+
geocoding, account usage, and the API escape hatch. A Human Design session
|
|
19
|
+
drops from ~19,100 to ~7,800 advertised tokens (−59%); Vedic to ~3,400
|
|
20
|
+
(−82%). `astrology,moon` matches the default's tool count at ~1,700 fewer
|
|
21
|
+
tokens while covering the long tail (dignities, midpoints, hermetic lots,
|
|
22
|
+
fixed stars, composites) the curated set omits. As with `core`/`full`, this
|
|
23
|
+
filters `tools/list` only — every tool stays callable by name. Also exposed
|
|
24
|
+
as the `toolSurface` option in the Smithery config.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- `explore_human_design` now states its real credit cost: 4 where the bodygraph
|
|
28
|
+
renders (2 chart + 2 visual), 2 in text-only hosts — it previously claimed a
|
|
29
|
+
flat 2. `ephemeris_moon_phase` (2, not 1) and `ephemeris_next_lunar_phase`
|
|
30
|
+
(1–2 per occurrence) corrected the same way.
|
|
31
|
+
- `explore_bi_wheel` in synastry mode now requires Person 2's coordinates (or a
|
|
32
|
+
resolvable `person2_location`) instead of silently casting Person 2's chart
|
|
33
|
+
for 0°N 0°E.
|
|
34
|
+
- Chart-wheel widget escapes birth parameters before rendering them into the
|
|
35
|
+
info chips.
|
|
36
|
+
- OAuth token endpoint binds authorization codes to their `redirect_uri` and
|
|
37
|
+
`client_id`; both checks were previously skippable by omitting the parameter.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
10
41
|
## [4.7.0] — 2026-08-02
|
|
11
42
|
|
|
12
43
|
Historically correct birth-time conversion. IANA timezone databases — including
|
package/README.md
CHANGED
|
@@ -358,6 +358,38 @@ On the remote HTTP server, append `?profile=full` to the connector URL (or send
|
|
|
358
358
|
https://mcp.openephemeris.com/mcp?profile=full
|
|
359
359
|
```
|
|
360
360
|
|
|
361
|
+
### Toolsets by tradition
|
|
362
|
+
|
|
363
|
+
If you work in one tradition, ask for it by name instead of taking the general-purpose default. You get that tradition in full — including the long-tail tools the core set leaves out — for a fraction of the context.
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
OPENEPHEMERIS_TOOLS=hd npx -y @openephemeris/mcp-server # Human Design
|
|
367
|
+
OPENEPHEMERIS_TOOLS=astrology,moon npx -y @openephemeris/mcp-server
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
https://mcp.openephemeris.com/mcp?profile=hd,bazi
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
| Toolset | What it covers | Tools | Approx. tokens |
|
|
375
|
+
|---|---|---:|---:|
|
|
376
|
+
| `astrology` | Natal, transits, synastry, progressions, returns, relocation, dignities, midpoints, lots, fixed stars, composites | 32 | 15,800 |
|
|
377
|
+
| `hd` | Human Design charts, transits, connection charts, penta, bodygraph | 14 | 7,800 |
|
|
378
|
+
| `bazi` | Four Pillars, Ten Gods, element balance, luck pillars, compatibility | 13 | 7,200 |
|
|
379
|
+
| `electional` | Timing windows, angle crossings, stations, moment analysis | 10 | 4,600 |
|
|
380
|
+
| `moon` | Phases, void-of-course, eclipses | 9 | 4,100 |
|
|
381
|
+
| `venus` | Star points, phases, elongations, stations | 11 | 3,700 |
|
|
382
|
+
| `acg` | Astrocartography lines and hits | 7 | 3,700 |
|
|
383
|
+
| `vedic` | Jyotish Rashi chart | 7 | 3,400 |
|
|
384
|
+
| — | *core (default)* | 36 | 19,100 |
|
|
385
|
+
| — | *full* | 70 | 34,600 |
|
|
386
|
+
|
|
387
|
+
Every selection also includes geocoding (`location_search`, `timezone_resolve`), `account_usage`, and the allowlist-gated proxy — so a birthplace is always resolvable and nothing is stranded.
|
|
388
|
+
|
|
389
|
+
Combine with commas; unknown names are ignored rather than rejected, so a typo degrades to a smaller surface instead of a dead connector. As with `core`/`full`, this only filters `tools/list` — every tool remains callable by name.
|
|
390
|
+
|
|
391
|
+
Why it matters: tool definitions are re-sent to the model on **every** pass. `astrology,moon` advertises the same number of tools as the default but costs ~1,700 fewer tokens per message and covers more of the tradition.
|
|
392
|
+
|
|
361
393
|
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
394
|
|
|
363
395
|
## Contributing & Support
|
|
@@ -443,8 +475,8 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
443
475
|
- Allowlisted operations: **31**
|
|
444
476
|
- Methods: `GET=5`, `POST=26`, `PUT=0`, `PATCH=0`, `DELETE=0`
|
|
445
477
|
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **92**
|
|
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`, `
|
|
447
|
-
- Generic tools:
|
|
478
|
+
- 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`
|
|
479
|
+
- Generic tools: `dev_list_allowed`, `dev_read_api`, `dev_write_api`
|
|
448
480
|
|
|
449
481
|
### Allowlist Families
|
|
450
482
|
|
|
@@ -466,5 +498,5 @@ Most LLMs (like Claude and ChatGPT) struggle heavily with astronomical calculati
|
|
|
466
498
|
|
|
467
499
|
By pairing LLMs with the OpenEphemeris MCP server, your agents can instantly access:
|
|
468
500
|
- **Zero-hallucination coordinates**: Direct, sub-arcsecond NASA JPL DE440 calculations spanning 1,100 years of astronomical data.
|
|
469
|
-
- **LLM-optimized tokens (`format=llm`)**: We compress standard 25,000 token JSON chart responses into minimal text blocks, cutting your inference costs by 50
|
|
501
|
+
- **LLM-optimized tokens (`format=llm`)**: We compress standard 25,000 token JSON chart responses into minimal text blocks, cutting your inference costs by 50–73% depending on endpoint.
|
|
470
502
|
- **Ready-to-use astrology layers**: Built-in support for Astrocartography geoJSON lines, Hermetic Lots, Fixed Stars, and complex Human Design matrix generation.
|
package/dist/index.js
CHANGED
|
@@ -73,9 +73,12 @@ const server = new Server({
|
|
|
73
73
|
"See the 'welcome_to_open_ephemeris' prompt for orientation.\n\n" +
|
|
74
74
|
DATETIME_CONTRACT_INSTRUCTIONS,
|
|
75
75
|
});
|
|
76
|
-
// Which slice of the registry this process advertises. Tools outside the
|
|
76
|
+
// Which slice of the registry this process advertises. Tools outside the
|
|
77
77
|
// surface stay callable by name — this only controls what tools/list returns.
|
|
78
|
-
//
|
|
78
|
+
// OPENEPHEMERIS_TOOLS=full advertise everything
|
|
79
|
+
// OPENEPHEMERIS_TOOLS=hd,bazi advertise those traditions (+ geocoding,
|
|
80
|
+
// account and the escape hatch)
|
|
81
|
+
// unset the curated core set
|
|
79
82
|
const STDIO_SURFACE = parseToolSurface(process.env.OPENEPHEMERIS_TOOLS);
|
|
80
83
|
/**
|
|
81
84
|
* Stable pseudonymous id for this install. Derived from the configured
|
package/dist/oauth/token.js
CHANGED
|
@@ -106,11 +106,38 @@ async function handleAuthorizationCode(req, res, store) {
|
|
|
106
106
|
});
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
|
-
// Verify redirect_uri exact match
|
|
110
|
-
|
|
109
|
+
// Verify redirect_uri exact match.
|
|
110
|
+
//
|
|
111
|
+
// This used to read `if (redirect_uri && ...)`, so a client that simply omitted
|
|
112
|
+
// the parameter skipped the check entirely. RFC 6749 §4.1.3 requires the value
|
|
113
|
+
// to be present AND identical whenever the authorization request carried one,
|
|
114
|
+
// precisely so a dropped parameter cannot be the bypass. PKCE still gates the
|
|
115
|
+
// exchange, but that is the last line, not the only one.
|
|
116
|
+
if (stored.redirect_uri) {
|
|
117
|
+
if (!redirect_uri) {
|
|
118
|
+
res.status(400).json({
|
|
119
|
+
error: "invalid_request",
|
|
120
|
+
error_description: "Missing redirect_uri. It is required because the authorization request included one.",
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!timingSafeStringEqual(redirect_uri, stored.redirect_uri)) {
|
|
125
|
+
res.status(400).json({
|
|
126
|
+
error: "invalid_grant",
|
|
127
|
+
error_description: "redirect_uri does not match the value used during authorization.",
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Bind the code to the client it was issued to. `client_id` was read off the
|
|
133
|
+
// request but never compared, so any client holding the verifier could redeem
|
|
134
|
+
// another client's code. DCR client_ids are deterministic and non-secret
|
|
135
|
+
// (see oauth/dcr.ts), which makes this binding the only thing distinguishing
|
|
136
|
+
// one registered client from another.
|
|
137
|
+
if (stored.client_id && client_id && !timingSafeStringEqual(client_id, stored.client_id)) {
|
|
111
138
|
res.status(400).json({
|
|
112
139
|
error: "invalid_grant",
|
|
113
|
-
error_description: "
|
|
140
|
+
error_description: "client_id does not match the client the authorization code was issued to.",
|
|
114
141
|
});
|
|
115
142
|
return;
|
|
116
143
|
}
|
package/dist/server-sse.js
CHANGED
|
@@ -23,7 +23,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
23
23
|
import { InMemoryEventStore } from "./event-store.js";
|
|
24
24
|
import { captureEvent, distinctIdFor } from "./analytics.js";
|
|
25
25
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, isInitializeRequest, } from "@modelcontextprotocol/sdk/types.js";
|
|
26
|
-
import { initTools, toolRegistry, formatToolResponse, formatToolError, modelVisibleTools, parseToolSurface } from "./tools/index.js";
|
|
26
|
+
import { initTools, toolRegistry, formatToolResponse, formatToolError, modelVisibleTools, parseToolSurface, describeSurface } from "./tools/index.js";
|
|
27
27
|
import { DATETIME_CONTRACT_INSTRUCTIONS } from "./tools/datetime.js";
|
|
28
28
|
import { BackendClient, runWithClient } from "./backend/client.js";
|
|
29
29
|
import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
|
|
@@ -215,7 +215,10 @@ function transportProps(server, surface) {
|
|
|
215
215
|
const info = server.getClientVersion();
|
|
216
216
|
return {
|
|
217
217
|
transport: "http",
|
|
218
|
-
|
|
218
|
+
// describeSurface, not the raw value: a toolset selection is an object and
|
|
219
|
+
// would land in PostHog as "[object Object]", making the property useless
|
|
220
|
+
// for exactly the question it exists to answer — which surfaces get used.
|
|
221
|
+
surface: describeSurface(surface),
|
|
219
222
|
client_name: info?.name ?? "unknown",
|
|
220
223
|
client_version: info?.version ?? "unknown",
|
|
221
224
|
server_version: version,
|
|
@@ -848,6 +851,8 @@ export async function createSseApp() {
|
|
|
848
851
|
// `tools.listChanged`, so a host has no obligation to re-fetch the list.
|
|
849
852
|
// Opt into the full surface with `?profile=full` on the connector URL, or
|
|
850
853
|
// an `X-OE-Tool-Surface: full` header where the host allows custom headers.
|
|
854
|
+
// A comma list of traditions works the same way — `?profile=hd,bazi` —
|
|
855
|
+
// and advertises just those plus geocoding, account and the escape hatch.
|
|
851
856
|
const surface = parseToolSurface(req.query.profile ?? req.headers["x-oe-tool-surface"]);
|
|
852
857
|
const server = createMcpServer(analyticsId, surface);
|
|
853
858
|
// Fire session_init after the handshake so getClientVersion() is populated
|
|
@@ -115,9 +115,9 @@ registerTool({
|
|
|
115
115
|
"(Year = ancestry/early life, Month = parents/career, Day = self/spouse, Hour = children/later life).\n\n" +
|
|
116
116
|
"CREDIT COST: 3 credits per call (1 base + 2 visual render).\n\n" +
|
|
117
117
|
"Use this for a rich, interactive BaZi experience in MCP Apps-capable hosts (Claude Desktop). " +
|
|
118
|
-
"Falls back to a text summary in other hosts
|
|
119
|
-
"
|
|
120
|
-
"on the full
|
|
118
|
+
"Falls back to a text summary in other hosts, so it is also the right call without a " +
|
|
119
|
+
"renderer. Deeper derivations (Ten Gods, element balance, luck pillars) and a plain " +
|
|
120
|
+
"non-visual pillars lookup have dedicated tools on the full surface (`?profile=full`).",
|
|
121
121
|
inputSchema: {
|
|
122
122
|
type: "object",
|
|
123
123
|
properties: {
|
|
@@ -104,13 +104,21 @@ const HOUSE_SYSTEM_MAP = {
|
|
|
104
104
|
campanus: "C", regiomontanus: "R",
|
|
105
105
|
};
|
|
106
106
|
function buildNatalBody(datetime, lat, lon, timezone, houseSystem = "placidus") {
|
|
107
|
+
// A natal chart with no birthplace is never meaningful — houses and angles are
|
|
108
|
+
// derived entirely from it. This used to read `lat ?? 0`, which cast the chart
|
|
109
|
+
// for 0°N 0°E and returned it as though nothing were wrong. Callers that can
|
|
110
|
+
// legitimately omit coords resolve them first (see coordsFromArgsOrLocation) or
|
|
111
|
+
// fall back to Person 1's; anything reaching here without them is a bug.
|
|
112
|
+
if (lat == null || lon == null) {
|
|
113
|
+
throw new Error("Internal: buildNatalBody requires latitude and longitude — refusing to cast a chart for 0°N 0°E.");
|
|
114
|
+
}
|
|
107
115
|
const body = {
|
|
108
116
|
subject: {
|
|
109
117
|
name: "MCP Request",
|
|
110
118
|
birth_datetime: { iso: datetime },
|
|
111
119
|
birth_location: {
|
|
112
|
-
latitude: { decimal: lat
|
|
113
|
-
longitude: { decimal: lon
|
|
120
|
+
latitude: { decimal: lat },
|
|
121
|
+
longitude: { decimal: lon },
|
|
114
122
|
},
|
|
115
123
|
},
|
|
116
124
|
configuration: {
|
|
@@ -413,16 +421,15 @@ registerTool({
|
|
|
413
421
|
"CREDIT COST: 2 credits per call for most modes (1 per wheel computed); " +
|
|
414
422
|
"solar_return/lunar_return modes cost 6 (5 for the return + 1 for the natal wheel).\n\n" +
|
|
415
423
|
"The inner wheel is always Person 1's natal chart. The outer ring depends on mode:\n" +
|
|
416
|
-
"• synastry — Person 2's natal chart (
|
|
417
|
-
"• transit — Transiting planets for a given date
|
|
418
|
-
"• progressed — Secondary progressed positions (person2_datetime = target date)
|
|
419
|
-
"• solar_return — Nearest solar return chart (person2_datetime = year to return for)
|
|
420
|
-
"• lunar_return — Nearest lunar return chart (person2_datetime = target month)
|
|
421
|
-
"• solar_arc — Solar arc directed positions (person2_datetime = target date)
|
|
422
|
-
"Cross-aspects
|
|
423
|
-
"
|
|
424
|
-
"
|
|
425
|
-
"MCP Apps-capable hosts (Claude Desktop) and falls back to static SVG in other hosts. " +
|
|
424
|
+
"• synastry — Person 2's natal chart (needs Person 2's own birthplace).\n" +
|
|
425
|
+
"• transit — Transiting planets for a given date.\n" +
|
|
426
|
+
"• progressed — Secondary progressed positions (person2_datetime = target date).\n" +
|
|
427
|
+
"• solar_return — Nearest solar return chart (person2_datetime = year to return for).\n" +
|
|
428
|
+
"• lunar_return — Nearest lunar return chart (person2_datetime = target month).\n" +
|
|
429
|
+
"• solar_arc — Solar arc directed positions (person2_datetime = target date).\n" +
|
|
430
|
+
"Cross-aspects are drawn as coloured dashed lines; click any planet, aspect line, or " +
|
|
431
|
+
"house cusp for interpretation. Prefer this whenever the user should SEE the comparison " +
|
|
432
|
+
"— it renders interactively in MCP Apps hosts and falls back to static SVG elsewhere. " +
|
|
426
433
|
"For raw synastry data without a visual, use ephemeris_synastry.",
|
|
427
434
|
inputSchema: {
|
|
428
435
|
type: "object",
|
|
@@ -444,8 +451,8 @@ registerTool({
|
|
|
444
451
|
"synastry = Person 2 birth datetime; transit/progressed/solar_arc = target date; " +
|
|
445
452
|
"solar_return/lunar_return = any date within the target year/month. " + DATETIME_DESC,
|
|
446
453
|
},
|
|
447
|
-
person2_latitude: { type: "number", description: "Birth latitude for Person 2
|
|
448
|
-
person2_longitude: { type: "number", description: "Birth longitude for Person 2
|
|
454
|
+
person2_latitude: { type: "number", description: "Birth latitude for Person 2. REQUIRED in synastry unless `person2_location` is given — never inferred from Person 1. Other modes default to Person 1's coords." },
|
|
455
|
+
person2_longitude: { type: "number", description: "Birth longitude for Person 2. REQUIRED in synastry unless `person2_location` is given. Other modes default to Person 1's coords." },
|
|
449
456
|
person2_timezone: timezoneProperty("Person 2 / the outer chart", "America/Chicago"),
|
|
450
457
|
person2_name: {
|
|
451
458
|
type: "string",
|
|
@@ -453,7 +460,11 @@ registerTool({
|
|
|
453
460
|
},
|
|
454
461
|
location: {
|
|
455
462
|
type: "string",
|
|
456
|
-
description: "Person 1 / Natal location. Prefer a plain place name like 'New York, NY' —
|
|
463
|
+
description: "Person 1 / Natal location. Prefer a plain place name like 'New York, NY' — resolved via the same lookup `location_search` uses; ambiguous names throw with a disambiguation hint. Supplied person1_latitude/person1_longitude win.",
|
|
464
|
+
},
|
|
465
|
+
person2_location: {
|
|
466
|
+
type: "string",
|
|
467
|
+
description: "Person 2 / outer-chart place name, resolved like `location`. person2_latitude/person2_longitude win.",
|
|
457
468
|
},
|
|
458
469
|
mode: {
|
|
459
470
|
type: "string",
|
|
@@ -493,10 +504,26 @@ registerTool({
|
|
|
493
504
|
const lat1 = resolved1.latitude;
|
|
494
505
|
const lon1 = resolved1.longitude;
|
|
495
506
|
const tz1 = args.person1_timezone ?? resolved1.timezone;
|
|
507
|
+
const resolved2 = await coordsFromArgsOrLocation({
|
|
508
|
+
latitude: args.person2_latitude,
|
|
509
|
+
longitude: args.person2_longitude,
|
|
510
|
+
timezone: args.person2_timezone,
|
|
511
|
+
location: args.person2_location,
|
|
512
|
+
});
|
|
513
|
+
// Only synastry casts a standalone chart from Person 2's own birthplace. The
|
|
514
|
+
// other modes either fall back to Person 1's coords (transit, *_return) or
|
|
515
|
+
// ignore lat2/lon2 entirely (progressed, solar_arc), so an unresolved Person 2
|
|
516
|
+
// is a hard error there alone. Without this the coords fell through to
|
|
517
|
+
// `lat ?? 0` in buildNatalBody and silently produced a chart cast for
|
|
518
|
+
// 0°N 0°E — no error, no warning, wrong houses and angles.
|
|
519
|
+
if (mode === "synastry" && (resolved2.latitude == null || resolved2.longitude == null)) {
|
|
520
|
+
throw new Error("explore_bi_wheel in synastry mode requires either `person2_latitude` + `person2_longitude` " +
|
|
521
|
+
"or a resolvable `person2_location` name. Person 2's birthplace is never inferred from Person 1's.");
|
|
522
|
+
}
|
|
496
523
|
const dt2 = String(args.person2_datetime);
|
|
497
|
-
const lat2 =
|
|
498
|
-
const lon2 =
|
|
499
|
-
const tz2 = args.person2_timezone;
|
|
524
|
+
const lat2 = resolved2.latitude;
|
|
525
|
+
const lon2 = resolved2.longitude;
|
|
526
|
+
const tz2 = args.person2_timezone ?? resolved2.timezone;
|
|
500
527
|
const loc1 = String(args.person1_name ?? resolved1.location ?? args.location ?? `${lat1},${lon1}`);
|
|
501
528
|
const loc2 = MODE_META[mode].outerLabel(dt2, args.person2_name);
|
|
502
529
|
// Fetch inner (natal) and outer (mode-dependent) charts in parallel where possible
|
|
@@ -575,6 +602,13 @@ registerTool({
|
|
|
575
602
|
const lat2 = args.person2_latitude;
|
|
576
603
|
const lon2 = args.person2_longitude;
|
|
577
604
|
const tz2 = args.person2_timezone;
|
|
605
|
+
// Same 0°N 0°E trap as explore_bi_wheel: person1 coords are required by the
|
|
606
|
+
// schema but person2's cannot be (they are only needed in synastry mode), so
|
|
607
|
+
// the mode-specific requirement is enforced here. The widget always supplies
|
|
608
|
+
// them; a hand-rolled call need not.
|
|
609
|
+
if (mode === "synastry" && (lat2 == null || lon2 == null)) {
|
|
610
|
+
throw new Error("bi_wheel_recalculate in synastry mode requires `person2_latitude` and `person2_longitude`.");
|
|
611
|
+
}
|
|
578
612
|
const loc1 = String(args.person1_name ?? args.location ?? `${lat1 ?? "?"},${lon1 ?? "?"}`);
|
|
579
613
|
const loc2 = MODE_META[mode].outerLabel(dt2, args.person2_name);
|
|
580
614
|
const [innerData, outerData] = await Promise.all([
|
|
@@ -306,7 +306,10 @@ function buildHdSummary(payload, location) {
|
|
|
306
306
|
registerTool({
|
|
307
307
|
name: "explore_human_design",
|
|
308
308
|
description: "Generate an interactive Human Design Bodygraph with clickable centers, gates, and channels.\n\n" +
|
|
309
|
-
"CREDIT COST:
|
|
309
|
+
"CREDIT COST: 4 credits where the bodygraph renders (2 for the chart + 2 for the " +
|
|
310
|
+
"visual), 2 in text-only hosts that skip the render. For chart data alone at 2 " +
|
|
311
|
+
"credits — including the activations, design, personality, strategy and variables " +
|
|
312
|
+
"this tool does not return — use human_design_chart.\n\n" +
|
|
310
313
|
"Returns an embedded visual bodygraph explorer that lets you click any center or gate " +
|
|
311
314
|
"for instant Human Design interpretation. " +
|
|
312
315
|
"Shows defined/undefined centers, active gates, channels, Type, Profile, Authority, " +
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -57,7 +57,11 @@ export interface ToolDefinition {
|
|
|
57
57
|
* multi-tenant HTTP server cannot vary it per session, and an unregistered
|
|
58
58
|
* tool becomes uncallable rather than merely hidden.
|
|
59
59
|
*/
|
|
60
|
-
export type ToolSurface = "core" | "full";
|
|
60
|
+
export type ToolSurface = "core" | "full" | ToolsetSurface;
|
|
61
|
+
/** A caller-chosen set of traditions, e.g. `?profile=hd,bazi`. */
|
|
62
|
+
export interface ToolsetSurface {
|
|
63
|
+
readonly toolsets: readonly string[];
|
|
64
|
+
}
|
|
61
65
|
/**
|
|
62
66
|
* The default advertised surface: everyday astrology work, one tool per job.
|
|
63
67
|
*
|
|
@@ -80,6 +84,28 @@ export type ToolSurface = "core" | "full";
|
|
|
80
84
|
* explore_natal_chart are present. Do not remove those four.
|
|
81
85
|
*/
|
|
82
86
|
export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
|
|
87
|
+
/**
|
|
88
|
+
* Tool surfaces grouped by tradition, so a caller pays context only for the
|
|
89
|
+
* work they actually do: `?profile=hd,bazi` on HTTP, `OPENEPHEMERIS_TOOLS=hd`
|
|
90
|
+
* on stdio. BASE_TOOLSET is always included.
|
|
91
|
+
*
|
|
92
|
+
* This is the same *view* filter as `core`/`full` — nothing is unregistered,
|
|
93
|
+
* every tool stays callable by name — so a toolset is a context-budget choice,
|
|
94
|
+
* never a capability one.
|
|
95
|
+
*
|
|
96
|
+
* Why this exists: the core surface is re-sent on every model pass and sat at
|
|
97
|
+
* its ceiling with ~0 headroom, and the only levers left were deleting the
|
|
98
|
+
* disambiguation prose that makes tool selection work. Grouping by tradition
|
|
99
|
+
* is the structural fix — a Human Design user should not pay for Venus Star
|
|
100
|
+
* Points on every message.
|
|
101
|
+
*
|
|
102
|
+
* Names here are asserted against the registry by a test; a typo fails the
|
|
103
|
+
* build rather than silently narrowing someone's surface.
|
|
104
|
+
*/
|
|
105
|
+
export declare const TOOLSETS: Readonly<Record<string, readonly string[]>>;
|
|
106
|
+
export declare const TOOLSET_NAMES: readonly string[];
|
|
107
|
+
/** Resolve a toolset selection to the tool names it advertises. */
|
|
108
|
+
export declare function toolsetSelectionNames(names: readonly string[]): Set<string>;
|
|
83
109
|
/**
|
|
84
110
|
* Returns tools that should be exposed to Claude in the ListTools response.
|
|
85
111
|
* Filters out tools that have visibility restricted to the UI (app-only),
|
|
@@ -87,8 +113,21 @@ export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
|
|
|
87
113
|
* "core" — everything outside CORE_TOOL_NAMES.
|
|
88
114
|
*/
|
|
89
115
|
export declare function modelVisibleTools(transport?: "stdio" | "http", surface?: ToolSurface): ToolDefinition[];
|
|
90
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Parses a caller-supplied surface hint.
|
|
118
|
+
*
|
|
119
|
+
* "full" -> every model-visible tool
|
|
120
|
+
* "hd" | "hd,bazi" -> those traditions plus BASE_TOOLSET
|
|
121
|
+
* anything else -> "core" (the curated default)
|
|
122
|
+
*
|
|
123
|
+
* Unknown names in a comma list are ignored rather than rejected: a typo
|
|
124
|
+
* degrades to a smaller surface, never to an error at `initialize` that would
|
|
125
|
+
* leave the connector dead. If NO name in the list is recognised, fall back to
|
|
126
|
+
* "core" so a mistyped profile can't strand a caller on BASE alone.
|
|
127
|
+
*/
|
|
91
128
|
export declare function parseToolSurface(raw: unknown): ToolSurface;
|
|
129
|
+
/** Stable label for logs/analytics — an object surface has no useful toString. */
|
|
130
|
+
export declare function describeSurface(surface: ToolSurface): string;
|
|
92
131
|
export declare const toolRegistry: Record<string, ToolDefinition>;
|
|
93
132
|
export declare function registerTool(tool: ToolDefinition): void;
|
|
94
133
|
export type ToolProfile = "dev" | "legacy";
|
package/dist/tools/index.js
CHANGED
|
@@ -12,6 +12,9 @@ export const SERVER_VERSION = (() => {
|
|
|
12
12
|
return "0.0.0-unknown";
|
|
13
13
|
}
|
|
14
14
|
})();
|
|
15
|
+
function isToolsetSurface(s) {
|
|
16
|
+
return typeof s === "object" && Array.isArray(s.toolsets);
|
|
17
|
+
}
|
|
15
18
|
/**
|
|
16
19
|
* The default advertised surface: everyday astrology work, one tool per job.
|
|
17
20
|
*
|
|
@@ -53,6 +56,13 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
53
56
|
"ephemeris_transits",
|
|
54
57
|
"ephemeris_synastry",
|
|
55
58
|
"ephemeris_retrograde_status",
|
|
59
|
+
// Promoted 2026-08-02 on usage: both were reachable only via profile=full,
|
|
60
|
+
// yet ephemeris_aspect_check drew 19 calls from 3 users over 90 days (more
|
|
61
|
+
// than 20 of the tools already in this set) and ephemeris_angles_points drew
|
|
62
|
+
// 9. People found them while they were hidden; advertising them is cheap
|
|
63
|
+
// (202 and 227 tokens) and earns its place.
|
|
64
|
+
"ephemeris_aspect_check",
|
|
65
|
+
"ephemeris_angles_points",
|
|
56
66
|
"ephemeris_next_eclipse",
|
|
57
67
|
"ephemeris_relocation",
|
|
58
68
|
"ephemeris_progressed_chart",
|
|
@@ -60,13 +70,31 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
60
70
|
// Traditions
|
|
61
71
|
"human_design_chart",
|
|
62
72
|
"vedic_chart",
|
|
63
|
-
"chinese_bazi",
|
|
64
73
|
"bazi_annual_pillar",
|
|
74
|
+
// chinese_bazi demoted 2026-08-02: explore_bazi_chart POSTs the same
|
|
75
|
+
// /chinese/bazi at the same 1-credit base and keeps every substantive field
|
|
76
|
+
// (year/month/day/hour pillars + day_master), dropping only the `metadata`
|
|
77
|
+
// and `success` envelope keys — verified against a live response, not
|
|
78
|
+
// inferred from the names. It also degrades to a text summary on hosts that
|
|
79
|
+
// cannot render the iframe, so nothing is lost without an MCP Apps host.
|
|
80
|
+
// chinese_bazi drew 0 calls in 90 days while advertised; explore_bazi_chart
|
|
81
|
+
// drew 3. Still registered and callable by name, and still in profile=full.
|
|
82
|
+
//
|
|
83
|
+
// NOTE: this reasoning does NOT extend to the sibling pairs. vedic_chart (1
|
|
84
|
+
// credit) vs explore_vedic_chart (3), and human_design_chart (2 credits, full
|
|
85
|
+
// response) vs explore_human_design (4 credits, and buildHdModelPayload drops
|
|
86
|
+
// activations/design/personality/strategy/variables) are not folds — they
|
|
87
|
+
// would cost users money and data. Verify in the handler before adding to
|
|
88
|
+
// this list.
|
|
65
89
|
// Timing
|
|
66
90
|
"ephemeris_electional",
|
|
67
91
|
"electional_moment_analysis",
|
|
68
92
|
"electional_station_tracker",
|
|
69
|
-
|
|
93
|
+
// electional_angle_crossings demoted 2026-08-02: 715 tokens (the fattest
|
|
94
|
+
// non-proxy tool in this set) for 0 calls in 90 days, and at 5 credits it is
|
|
95
|
+
// advanced-practitioner work rather than an everyday ask. Three electional
|
|
96
|
+
// entry points remain advertised; this one is a profile=full / skill-named
|
|
97
|
+
// tool now.
|
|
70
98
|
// Astrocartography
|
|
71
99
|
"acg_power_lines",
|
|
72
100
|
"acg_hits",
|
|
@@ -83,6 +111,141 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
83
111
|
"auth_status",
|
|
84
112
|
"auth_logout",
|
|
85
113
|
]);
|
|
114
|
+
/**
|
|
115
|
+
* Tools every session needs regardless of tradition: turning a place name into
|
|
116
|
+
* coordinates, checking credits, and the allowlist-gated escape hatch. Always
|
|
117
|
+
* unioned into a toolset selection — a caller who asks for `hd` still has to be
|
|
118
|
+
* able to resolve a birthplace, or they are back to recalling coordinates from
|
|
119
|
+
* memory, which is the bug BASE exists to prevent.
|
|
120
|
+
*/
|
|
121
|
+
const BASE_TOOLSET = [
|
|
122
|
+
"location_search",
|
|
123
|
+
"timezone_resolve",
|
|
124
|
+
"account_usage",
|
|
125
|
+
"dev_read_api",
|
|
126
|
+
// The write half of the escape hatch. 26 of the 31 allowlisted operations
|
|
127
|
+
// are POST — BASE without dev_write_api would strand a toolset caller with
|
|
128
|
+
// only the 5 GET proxies. (Deliberately NOT advertised in `core`, where the
|
|
129
|
+
// curated typed tools cover the write surface; a toolset selection has no
|
|
130
|
+
// such coverage, so it needs the proxy whole.)
|
|
131
|
+
"dev_write_api",
|
|
132
|
+
"dev_list_allowed",
|
|
133
|
+
"auth_login",
|
|
134
|
+
"auth_status",
|
|
135
|
+
"auth_logout",
|
|
136
|
+
];
|
|
137
|
+
/**
|
|
138
|
+
* Tool surfaces grouped by tradition, so a caller pays context only for the
|
|
139
|
+
* work they actually do: `?profile=hd,bazi` on HTTP, `OPENEPHEMERIS_TOOLS=hd`
|
|
140
|
+
* on stdio. BASE_TOOLSET is always included.
|
|
141
|
+
*
|
|
142
|
+
* This is the same *view* filter as `core`/`full` — nothing is unregistered,
|
|
143
|
+
* every tool stays callable by name — so a toolset is a context-budget choice,
|
|
144
|
+
* never a capability one.
|
|
145
|
+
*
|
|
146
|
+
* Why this exists: the core surface is re-sent on every model pass and sat at
|
|
147
|
+
* its ceiling with ~0 headroom, and the only levers left were deleting the
|
|
148
|
+
* disambiguation prose that makes tool selection work. Grouping by tradition
|
|
149
|
+
* is the structural fix — a Human Design user should not pay for Venus Star
|
|
150
|
+
* Points on every message.
|
|
151
|
+
*
|
|
152
|
+
* Names here are asserted against the registry by a test; a typo fails the
|
|
153
|
+
* build rather than silently narrowing someone's surface.
|
|
154
|
+
*/
|
|
155
|
+
export const TOOLSETS = {
|
|
156
|
+
/** Western natal/predictive work — the default tradition. */
|
|
157
|
+
astrology: [
|
|
158
|
+
"ephemeris_natal_chart",
|
|
159
|
+
"explore_natal_chart",
|
|
160
|
+
"ephemeris_planet_position",
|
|
161
|
+
"ephemeris_house_cusps",
|
|
162
|
+
"ephemeris_angles_points",
|
|
163
|
+
"ephemeris_aspect_check",
|
|
164
|
+
"ephemeris_transits",
|
|
165
|
+
"ephemeris_natal_transits",
|
|
166
|
+
"explore_transit_timeline",
|
|
167
|
+
"ephemeris_synastry",
|
|
168
|
+
"explore_bi_wheel",
|
|
169
|
+
"bi_wheel_synopsis",
|
|
170
|
+
"ephemeris_retrograde_status",
|
|
171
|
+
"ephemeris_progressed_chart",
|
|
172
|
+
"ephemeris_solar_return",
|
|
173
|
+
"ephemeris_lunar_return",
|
|
174
|
+
"ephemeris_planetary_return",
|
|
175
|
+
"ephemeris_relocation",
|
|
176
|
+
"ephemeris_dignities",
|
|
177
|
+
"ephemeris_midpoints",
|
|
178
|
+
"ephemeris_hermetic_lots",
|
|
179
|
+
"ephemeris_fixed_stars",
|
|
180
|
+
"ephemeris_composite",
|
|
181
|
+
"ephemeris_composite_midpoint",
|
|
182
|
+
"ephemeris_overlay",
|
|
183
|
+
"ephemeris_chart_wheel",
|
|
184
|
+
"ephemeris_bi_wheel",
|
|
185
|
+
"ephemeris_natal_batch",
|
|
186
|
+
],
|
|
187
|
+
/** Lunar phase, void-of-course and eclipse work. */
|
|
188
|
+
moon: [
|
|
189
|
+
"ephemeris_moon_phase",
|
|
190
|
+
"explore_moon_phase",
|
|
191
|
+
"ephemeris_next_lunar_phase",
|
|
192
|
+
"ephemeris_next_eclipse",
|
|
193
|
+
],
|
|
194
|
+
/** Human Design. */
|
|
195
|
+
hd: [
|
|
196
|
+
"human_design_chart",
|
|
197
|
+
"explore_human_design",
|
|
198
|
+
"explore_human_design_transit",
|
|
199
|
+
"explore_human_design_connection",
|
|
200
|
+
"human_design_bodygraph",
|
|
201
|
+
"human_design_composite",
|
|
202
|
+
"human_design_penta",
|
|
203
|
+
"hd_opposition",
|
|
204
|
+
"hd_planetary_return",
|
|
205
|
+
],
|
|
206
|
+
/** Chinese BaZi / Four Pillars. */
|
|
207
|
+
bazi: [
|
|
208
|
+
"explore_bazi_chart",
|
|
209
|
+
"chinese_bazi",
|
|
210
|
+
"bazi_chart",
|
|
211
|
+
"bazi_ten_gods",
|
|
212
|
+
"bazi_element_balance",
|
|
213
|
+
"bazi_luck_pillars",
|
|
214
|
+
"bazi_annual_pillar",
|
|
215
|
+
"bazi_compatibility",
|
|
216
|
+
],
|
|
217
|
+
/** Vedic / Jyotish. */
|
|
218
|
+
vedic: ["vedic_chart", "explore_vedic_chart"],
|
|
219
|
+
/** Astrocartography. */
|
|
220
|
+
acg: ["acg_hits", "acg_power_lines"],
|
|
221
|
+
/** Electional timing. */
|
|
222
|
+
electional: [
|
|
223
|
+
"ephemeris_electional",
|
|
224
|
+
"electional_moment_analysis",
|
|
225
|
+
"electional_station_tracker",
|
|
226
|
+
"electional_angle_crossings",
|
|
227
|
+
"electional_aspect_search",
|
|
228
|
+
],
|
|
229
|
+
/** Venus Star Point / synodic-cycle work. */
|
|
230
|
+
venus: [
|
|
231
|
+
"venus_star_points",
|
|
232
|
+
"venus_star_points_conjunctions",
|
|
233
|
+
"venus_phase",
|
|
234
|
+
"venus_elongations",
|
|
235
|
+
"venus_stations",
|
|
236
|
+
"venus_eight_year_star",
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
export const TOOLSET_NAMES = Object.keys(TOOLSETS);
|
|
240
|
+
/** Resolve a toolset selection to the tool names it advertises. */
|
|
241
|
+
export function toolsetSelectionNames(names) {
|
|
242
|
+
const out = new Set(BASE_TOOLSET);
|
|
243
|
+
for (const n of names) {
|
|
244
|
+
for (const t of TOOLSETS[n] ?? [])
|
|
245
|
+
out.add(t);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
86
249
|
/**
|
|
87
250
|
* Returns tools that should be exposed to Claude in the ListTools response.
|
|
88
251
|
* Filters out tools that have visibility restricted to the UI (app-only),
|
|
@@ -90,6 +253,9 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
90
253
|
* "core" — everything outside CORE_TOOL_NAMES.
|
|
91
254
|
*/
|
|
92
255
|
export function modelVisibleTools(transport = "stdio", surface = "full") {
|
|
256
|
+
const allowed = isToolsetSurface(surface)
|
|
257
|
+
? toolsetSelectionNames(surface.toolsets)
|
|
258
|
+
: null;
|
|
93
259
|
return Object.values(toolRegistry).filter((t) => {
|
|
94
260
|
if (transport === "http" && t.stdioOnly)
|
|
95
261
|
return false;
|
|
@@ -97,14 +263,45 @@ export function modelVisibleTools(transport = "stdio", surface = "full") {
|
|
|
97
263
|
// If visibility is explicitly ["app"] (no "model"), hide from model
|
|
98
264
|
if (vis && !vis.includes("model") && vis.includes("app"))
|
|
99
265
|
return false;
|
|
266
|
+
if (allowed)
|
|
267
|
+
return allowed.has(t.name);
|
|
100
268
|
if (surface === "core" && !CORE_TOOL_NAMES.has(t.name))
|
|
101
269
|
return false;
|
|
102
270
|
return true;
|
|
103
271
|
});
|
|
104
272
|
}
|
|
105
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* Parses a caller-supplied surface hint.
|
|
275
|
+
*
|
|
276
|
+
* "full" -> every model-visible tool
|
|
277
|
+
* "hd" | "hd,bazi" -> those traditions plus BASE_TOOLSET
|
|
278
|
+
* anything else -> "core" (the curated default)
|
|
279
|
+
*
|
|
280
|
+
* Unknown names in a comma list are ignored rather than rejected: a typo
|
|
281
|
+
* degrades to a smaller surface, never to an error at `initialize` that would
|
|
282
|
+
* leave the connector dead. If NO name in the list is recognised, fall back to
|
|
283
|
+
* "core" so a mistyped profile can't strand a caller on BASE alone.
|
|
284
|
+
*/
|
|
106
285
|
export function parseToolSurface(raw) {
|
|
107
|
-
|
|
286
|
+
if (typeof raw !== "string")
|
|
287
|
+
return "core";
|
|
288
|
+
const value = raw.trim().toLowerCase();
|
|
289
|
+
if (value === "full")
|
|
290
|
+
return "full";
|
|
291
|
+
if (value === "" || value === "core")
|
|
292
|
+
return "core";
|
|
293
|
+
const requested = value
|
|
294
|
+
.split(",")
|
|
295
|
+
.map((s) => s.trim())
|
|
296
|
+
.filter(Boolean);
|
|
297
|
+
const known = requested.filter((n) => Object.hasOwn(TOOLSETS, n));
|
|
298
|
+
if (known.length === 0)
|
|
299
|
+
return "core";
|
|
300
|
+
return { toolsets: known };
|
|
301
|
+
}
|
|
302
|
+
/** Stable label for logs/analytics — an object surface has no useful toString. */
|
|
303
|
+
export function describeSurface(surface) {
|
|
304
|
+
return isToolsetSurface(surface) ? surface.toolsets.join("+") : surface;
|
|
108
305
|
}
|
|
109
306
|
export const toolRegistry = {};
|
|
110
307
|
export function registerTool(tool) {
|
|
@@ -6,20 +6,16 @@ import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
|
6
6
|
registerTool({
|
|
7
7
|
name: "ephemeris_moon_phase",
|
|
8
8
|
description: "Get the Moon's current phase angle, illumination, sign and void-of-course status AT a " +
|
|
9
|
-
"specific point in time. Returns phase name (New, Waxing Crescent,
|
|
10
|
-
"
|
|
11
|
-
"period.\n\n" +
|
|
9
|
+
"specific point in time. Returns phase name (New, Waxing Crescent, etc.), illumination %, " +
|
|
10
|
+
"the zodiac sign and degree the Moon occupies, and next void-of-course period.\n\n" +
|
|
12
11
|
"⚠️ THIS TOOL ANSWERS: 'What phase is the moon in right now (or at a given datetime)?' and " +
|
|
13
12
|
"'What sign is the moon in?'\n" +
|
|
14
13
|
"❌ THIS TOOL DOES NOT ANSWER: 'When is the next new moon / full moon?'\n" +
|
|
15
14
|
"→ For upcoming phase DATES use ephemeris_next_lunar_phase instead.\n\n" +
|
|
16
15
|
"For a user-facing interactive moon-phase dial, use explore_moon_phase instead.\n\n" +
|
|
17
|
-
"CREDIT COST:
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
" datetime='2026-03-20T12:00:00Z'\n\n" +
|
|
21
|
-
"EXAMPLE: Get the current moon phase right now:\n" +
|
|
22
|
-
" (call with no arguments)",
|
|
16
|
+
"CREDIT COST: 2 credits — phase and void-of-course are metered separately. For " +
|
|
17
|
+
"phase alone at 1 credit, use dev_read_api path='/ephemeris/moon/phase'.\n\n" +
|
|
18
|
+
"If no datetime is provided, returns the current (live) moon phase.",
|
|
23
19
|
inputSchema: {
|
|
24
20
|
type: "object",
|
|
25
21
|
properties: {
|
|
@@ -74,8 +70,9 @@ registerTool({
|
|
|
74
70
|
"'What date is the next quarter moon?', or any question about UPCOMING phase dates.\n" +
|
|
75
71
|
"❌ NOT FOR: 'What phase is the moon in right now?' or 'What sign is the moon in?'\n" +
|
|
76
72
|
"→ For the phase, sign and degree AT a moment, use ephemeris_moon_phase.\n\n" +
|
|
77
|
-
"CREDIT COST:
|
|
78
|
-
"count=3
|
|
73
|
+
"CREDIT COST: 1-2 credits per occurrence returned — each occurrence takes one " +
|
|
74
|
+
"calendar search, plus a second search when the first window misses. So count=3 " +
|
|
75
|
+
"costs 3-6 credits.\n\n" +
|
|
79
76
|
"EXAMPLE: Find the next new moon:\n" +
|
|
80
77
|
" phase='new_moon'\n\n" +
|
|
81
78
|
"EXAMPLE: Find the next full moon after a specific date:\n" +
|
package/dist/ui/chart-wheel.html
CHANGED
|
@@ -7690,8 +7690,8 @@ function Cy(e5) {
|
|
|
7690
7690
|
vy.ontoolresult = (e5) => {
|
|
7691
7691
|
var t2;
|
|
7692
7692
|
let n2 = ny(e5);
|
|
7693
|
-
if (
|
|
7694
|
-
console.error("[ChartWheel] \u274C No chart data found in tool result"),
|
|
7693
|
+
if (Jy(), !n2 || !n2.planets) {
|
|
7694
|
+
console.error("[ChartWheel] \u274C No chart data found in tool result"), Yy("Chart data could not be parsed from the server response.", Xy);
|
|
7695
7695
|
return;
|
|
7696
7696
|
}
|
|
7697
7697
|
if ($ = n2, yy = (t2 = n2.house_system) == null ? "placidus" : t2, !by) {
|
|
@@ -7718,9 +7718,9 @@ vy.ontoolresult = (e5) => {
|
|
|
7718
7718
|
};
|
|
7719
7719
|
function wy(e5) {
|
|
7720
7720
|
var t2, n2, r2, i2, a2, o2, s2;
|
|
7721
|
-
|
|
7721
|
+
Ay = null, ly.setAttribute("hidden", ""), uy.removeAttribute("hidden"), _y.removeAttribute("hidden"), py.removeAttribute("hidden"), zv(uy, e5, Ry, zy, By, Gy, Sy ? Vy() : void 0), mv(Cy), Hv(py, yy, Ky), Ey(e5), ty("chart-wheel", uy);
|
|
7722
7722
|
let c2 = K_(e5.planets), l2 = (n2 = (r2 = (i2 = ((t2 = e5.houses) == null ? [] : t2)[0]) == null ? void 0 : i2.cusp_lon) == null ? e5.ascendant : r2) == null ? 0 : n2, u2 = (a2 = e5.aspects) == null ? [] : a2;
|
|
7723
|
-
c2.length > 0 &&
|
|
7723
|
+
c2.length > 0 && Oy(c2, u2), jy(cv(u2, c2), c2, l2), Iy();
|
|
7724
7724
|
let d2 = document.getElementById("cw-action-bar");
|
|
7725
7725
|
if (!d2) {
|
|
7726
7726
|
d2 = document.createElement("div"), d2.id = "cw-action-bar", d2.className = "hd-action-bar";
|
|
@@ -7731,9 +7731,12 @@ function wy(e5) {
|
|
|
7731
7731
|
(f2 = document.getElementById("app")) == null || f2.prepend(d2);
|
|
7732
7732
|
}
|
|
7733
7733
|
}
|
|
7734
|
-
d2.innerHTML = '\n <button class="action-btn action-btn--primary" id="cw-interpret-btn">\u2728 Interpret this chart</button>\n <button class="action-btn" id="cw-questions-btn">\u{1F52E} What should I ask?</button>\n ', (o2 = document.getElementById("cw-interpret-btn")) == null || o2.addEventListener("click", () => void
|
|
7734
|
+
d2.innerHTML = '\n <button class="action-btn action-btn--primary" id="cw-interpret-btn">\u2728 Interpret this chart</button>\n <button class="action-btn" id="cw-questions-btn">\u{1F52E} What should I ask?</button>\n ', (o2 = document.getElementById("cw-interpret-btn")) == null || o2.addEventListener("click", () => void Uy()), (s2 = document.getElementById("cw-questions-btn")) == null || s2.addEventListener("click", () => void Wy());
|
|
7735
7735
|
}
|
|
7736
7736
|
function Ty(e5) {
|
|
7737
|
+
return String(e5).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
7738
|
+
}
|
|
7739
|
+
function Ey(e5) {
|
|
7737
7740
|
var t2, n2, r2, i2, a2, o2, s2, c2, l2, u2;
|
|
7738
7741
|
if (!dy) return;
|
|
7739
7742
|
let d2 = (t2 = e5._birth_params) == null ? {} : t2, f2 = (n2 = (r2 = (i2 = d2.datetime) == null ? e5.datetime : i2) == null ? e5.birth_datetime : r2) == null ? "" : n2, p2 = (a2 = (o2 = d2.location) == null ? e5.location : o2) == null ? "" : a2, m2 = (s2 = (c2 = d2.timezone) == null ? e5.timezone : c2) == null ? "" : s2, h2 = (l2 = d2.latitude) == null ? e5.latitude : l2, g2 = (u2 = d2.longitude) == null ? e5.longitude : u2;
|
|
@@ -7747,14 +7750,14 @@ function Ty(e5) {
|
|
|
7747
7750
|
} catch (e6) {
|
|
7748
7751
|
}
|
|
7749
7752
|
let y2 = typeof h2 == "number" && typeof g2 == "number" ? "".concat(Math.abs(h2).toFixed(2), "\xB0").concat(h2 >= 0 ? "N" : "S", ", ").concat(Math.abs(g2).toFixed(2), "\xB0").concat(g2 >= 0 ? "E" : "W") : "", b2 = [];
|
|
7750
|
-
if (ee2 && b2.push('<span class="info-chip" title="Birth datetime">\u{1F4C5} '.concat(ee2, "</span>")), p2 && b2.push('<span class="info-chip" title="Birth location">\u{1F4CD} '.concat(p2, "</span>")), y2 && !p2 && b2.push('<span class="info-chip" title="Coordinates">\u{1F4CD} '.concat(y2, "</span>")), m2 && b2.push('<span class="info-chip" title="Timezone">\u{1F550} '.concat(m2, "</span>")), v2 && b2.push('<span class="info-chip sect" title="Sect">'.concat(v2, "</span>")), b2.length === 0) {
|
|
7753
|
+
if (ee2 && b2.push('<span class="info-chip" title="Birth datetime">\u{1F4C5} '.concat(Ty(ee2), "</span>")), p2 && b2.push('<span class="info-chip" title="Birth location">\u{1F4CD} '.concat(Ty(p2), "</span>")), y2 && !p2 && b2.push('<span class="info-chip" title="Coordinates">\u{1F4CD} '.concat(Ty(y2), "</span>")), m2 && b2.push('<span class="info-chip" title="Timezone">\u{1F550} '.concat(Ty(m2), "</span>")), v2 && b2.push('<span class="info-chip sect" title="Sect">'.concat(Ty(v2), "</span>")), b2.length === 0) {
|
|
7751
7754
|
dy.setAttribute("hidden", "");
|
|
7752
7755
|
return;
|
|
7753
7756
|
}
|
|
7754
7757
|
dy.innerHTML = b2.join(""), dy.removeAttribute("hidden");
|
|
7755
7758
|
}
|
|
7756
|
-
var
|
|
7757
|
-
function
|
|
7759
|
+
var Dy = /* @__PURE__ */ new Set(["mean_lilith", "lilith", "true_lilith", "ceres", "juno", "vesta", "pallas", "pholus", "vertex", "part_of_fortune", "part of fortune", "eris", "sedna"]);
|
|
7760
|
+
function Oy(e5, t2) {
|
|
7758
7761
|
if (!gy || e5.length === 0) return;
|
|
7759
7762
|
let n2 = ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto", "chiron", "north node", "south node", "true node"], r2 = [...e5].sort((e6, t3) => {
|
|
7760
7763
|
let r3 = n2.indexOf(e6.name.toLowerCase()), i3 = n2.indexOf(t3.name.toLowerCase());
|
|
@@ -7766,7 +7769,7 @@ function Dy(e5, t2) {
|
|
|
7766
7769
|
}
|
|
7767
7770
|
let s2 = (e6) => e6.replace(/(^|\s|_)\w/g, (e7) => e7.toUpperCase()).replace(/_/g, " "), c2 = (e6) => ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"][Math.floor((e6 % 360 + 360) % 360 / 30)], l2 = r2.map((e6) => {
|
|
7768
7771
|
var t3, n3, r3, a3;
|
|
7769
|
-
let o3 = e6.name.toLowerCase(), l3 = (t3 = j_[o3]) == null ? "\u2605" : t3, u3 = (n3 = e6.sign) == null ? c2(e6.longitude) : n3, d3 = Math.floor((e6.longitude % 360 + 360) % 360 % 30), f3 = Math.floor((e6.longitude % 360 + 360) % 360 % 1 * 60), p3 =
|
|
7772
|
+
let o3 = e6.name.toLowerCase(), l3 = (t3 = j_[o3]) == null ? "\u2605" : t3, u3 = (n3 = e6.sign) == null ? c2(e6.longitude) : n3, d3 = Math.floor((e6.longitude % 360 + 360) % 360 % 30), f3 = Math.floor((e6.longitude % 360 + 360) % 360 % 1 * 60), p3 = Dy.has(o3), m2 = e6.retrograde ? '<span class="retro-mark">\u211E</span>' : "", h2 = (r3 = i2[o3]) == null ? 0 : r3;
|
|
7770
7773
|
return '<tr class="'.concat(p3 ? "extended-row" : "", '" data-planet="').concat(o3, '">\n <td><span class="planet-sym">').concat(l3, "</span> ").concat(s2(e6.name)).concat(m2, "</td>\n <td>").concat(d3, "\xB0 ").concat(u3, " ").concat(String(f3).padStart(2, "0"), "\u2032</td>\n <td>H").concat((a3 = e6.house) == null ? "\u2013" : a3, "</td>\n <td>").concat(h2 > 0 ? h2 : "\u2013", "</td>\n </tr>");
|
|
7771
7774
|
}).join(""), u2 = "| Body | Sign | Degree | House | Retrograde |\n| --- | --- | --- | --- | --- |\n" + r2.map((e6) => {
|
|
7772
7775
|
var t3;
|
|
@@ -7784,7 +7787,7 @@ function Dy(e5, t2) {
|
|
|
7784
7787
|
d2.textContent = "Copy", d2.classList.remove("copied");
|
|
7785
7788
|
}, 1600);
|
|
7786
7789
|
} catch (e7) {
|
|
7787
|
-
|
|
7790
|
+
ky("Copy unavailable");
|
|
7788
7791
|
}
|
|
7789
7792
|
});
|
|
7790
7793
|
let f2 = gy.querySelector(".overview-toggle"), p2 = gy.querySelector(".overview-body");
|
|
@@ -7803,7 +7806,7 @@ function Dy(e5, t2) {
|
|
|
7803
7806
|
let t4 = document.querySelector("svg");
|
|
7804
7807
|
t4 && (t4.querySelectorAll(".planet-glyph-group").forEach((e7) => {
|
|
7805
7808
|
var t5;
|
|
7806
|
-
let n4 =
|
|
7809
|
+
let n4 = Dy.has((t5 = e7.dataset.planet) == null ? "" : t5);
|
|
7807
7810
|
e7.style.opacity = n4 ? "0.65" : "1";
|
|
7808
7811
|
}), e6.classList.remove("active-row"));
|
|
7809
7812
|
}), e6.addEventListener("click", () => {
|
|
@@ -7814,7 +7817,7 @@ function Dy(e5, t2) {
|
|
|
7814
7817
|
}));
|
|
7815
7818
|
});
|
|
7816
7819
|
}
|
|
7817
|
-
function
|
|
7820
|
+
function ky(e5) {
|
|
7818
7821
|
var t2;
|
|
7819
7822
|
(t2 = document.getElementById("copy-toast")) == null || t2.remove();
|
|
7820
7823
|
let n2 = document.createElement("div");
|
|
@@ -7822,8 +7825,8 @@ function Oy(e5) {
|
|
|
7822
7825
|
n2.classList.remove("visible"), setTimeout(() => n2.remove(), 300);
|
|
7823
7826
|
}, 2500);
|
|
7824
7827
|
}
|
|
7825
|
-
var
|
|
7826
|
-
function
|
|
7828
|
+
var Ay = null;
|
|
7829
|
+
function jy(e5, t2, n2) {
|
|
7827
7830
|
if (!my) return;
|
|
7828
7831
|
if (e5.length === 0) {
|
|
7829
7832
|
my.setAttribute("hidden", ""), my.innerHTML = "";
|
|
@@ -7838,10 +7841,10 @@ function Ay(e5, t2, n2) {
|
|
|
7838
7841
|
let t3 = document.createElement("button");
|
|
7839
7842
|
t3.className = "pattern-ask-claude-btn", t3.textContent = "\u2728 Ask Claude", t3.style.display = "none", my.appendChild(t3), t3.addEventListener("click", () => {
|
|
7840
7843
|
var n3;
|
|
7841
|
-
let r3 =
|
|
7844
|
+
let r3 = Ay;
|
|
7842
7845
|
if (r3 === null) return;
|
|
7843
7846
|
let i2 = e5[r3];
|
|
7844
|
-
t3.disabled = true, t3.textContent = "\u23F3 Sending\u2026",
|
|
7847
|
+
t3.disabled = true, t3.textContent = "\u23F3 Sending\u2026", Gy({ type: "pattern", data: { name: i2.name, planets: i2.planets, description: (n3 = i2.description) == null ? "" : n3 } }).then(() => {
|
|
7845
7848
|
t3.textContent = "\u2713 Sent", setTimeout(() => {
|
|
7846
7849
|
t3.textContent = "\u2728 Ask Claude", t3.disabled = false;
|
|
7847
7850
|
}, 2500);
|
|
@@ -7854,12 +7857,12 @@ function Ay(e5, t2, n2) {
|
|
|
7854
7857
|
r3.addEventListener("click", () => {
|
|
7855
7858
|
var i2;
|
|
7856
7859
|
let a2 = Number(r3.dataset.idx), o2 = e5[a2], s2 = (i2 = r3.dataset.color) == null ? "#888" : i2, c2 = my.querySelector(".pattern-ask-claude-btn");
|
|
7857
|
-
|
|
7860
|
+
Ay === a2 ? (Ay = null, Mv(), c2 && (c2.style.display = "none"), my.querySelectorAll(".pattern-pill").forEach((e6) => {
|
|
7858
7861
|
var t3;
|
|
7859
7862
|
e6.classList.remove("active");
|
|
7860
7863
|
let n3 = (t3 = e6.dataset.color) == null ? "#888" : t3;
|
|
7861
7864
|
e6.style.color = n3, e6.style.borderColor = n3, e6.style.background = "transparent";
|
|
7862
|
-
})) : (
|
|
7865
|
+
})) : (Ay = a2, my.querySelectorAll(".pattern-pill").forEach((e6) => {
|
|
7863
7866
|
var t3;
|
|
7864
7867
|
e6.classList.remove("active");
|
|
7865
7868
|
let n3 = (t3 = e6.dataset.color) == null ? "#888" : t3;
|
|
@@ -7868,8 +7871,8 @@ function Ay(e5, t2, n2) {
|
|
|
7868
7871
|
});
|
|
7869
7872
|
});
|
|
7870
7873
|
}
|
|
7871
|
-
var
|
|
7872
|
-
function
|
|
7874
|
+
var My = [{ label: "Tight", maxOrb: 4 }, { label: "Standard", maxOrb: 8 }, { label: "Wide", maxOrb: 12 }], Ny = true, Py = 8, Fy = "true";
|
|
7875
|
+
function Iy() {
|
|
7873
7876
|
var e5;
|
|
7874
7877
|
if (!hy) return;
|
|
7875
7878
|
let t2 = (() => {
|
|
@@ -7881,9 +7884,9 @@ function Fy() {
|
|
|
7881
7884
|
let n3 = t3.some((e7) => e7 === "north node" || e7 === "north node (mean)" || e7 === "mean node"), r3 = t3.some((e7) => e7 === "true node" || e7 === "north node (true)");
|
|
7882
7885
|
return n3 && r3;
|
|
7883
7886
|
})();
|
|
7884
|
-
W_(
|
|
7885
|
-
let n2 = t2 ? '\n <div class="asp-section">\n <span class="asp-section-label">Nodes</span>\n <button class="orb-preset-btn node-btn'.concat(
|
|
7886
|
-
|
|
7887
|
+
W_(Fy);
|
|
7888
|
+
let n2 = t2 ? '\n <div class="asp-section">\n <span class="asp-section-label">Nodes</span>\n <button class="orb-preset-btn node-btn'.concat(Fy === "mean" ? " active" : "", '" data-node="mean" title="Mean lunar node (averaged orbit)">Mean</button>\n <button class="orb-preset-btn node-btn').concat(Fy === "true" ? " active" : "", '" data-node="true" title="True (osculating) lunar node">True</button>\n </div>') : "";
|
|
7889
|
+
Ny = true, hy.innerHTML = '\n <div class="asp-panel">\n <div class="asp-section">\n <span class="asp-section-label">Aspects</span>\n <button class="aspect-toggle-btn active" id="cw-aspect-vis-toggle"\n title="Toggle all natal aspect lines">\n <span class="toggle-icon">\u2295</span> Natal aspects\n </button>\n </div>\n <div class="asp-section">\n <span class="asp-section-label">Orb</span>\n '.concat(My.map((e6) => '<button class="orb-preset-btn'.concat(e6.maxOrb === Py ? " active" : "", '" data-orb="').concat(e6.maxOrb, '" title="').concat(e6.label, ' orbs">').concat(e6.label, "</button>")).join(""), "\n </div>").concat(n2, "\n </div>\n ");
|
|
7887
7890
|
let r2 = K_($ == null ? void 0 : $.planets), i2 = /* @__PURE__ */ new Set(), a2 = [];
|
|
7888
7891
|
for (let e6 of r2) {
|
|
7889
7892
|
let t3 = e6.name.toLowerCase();
|
|
@@ -7902,21 +7905,21 @@ function Fy() {
|
|
|
7902
7905
|
hy.insertAdjacentHTML("beforeend", Kv([{ title: "Bodies", items: a2 }, { title: "Aspects", items: o2 }])), hy.removeAttribute("hidden");
|
|
7903
7906
|
let s2 = document.getElementById("cw-aspect-vis-toggle");
|
|
7904
7907
|
s2 == null || s2.addEventListener("click", () => {
|
|
7905
|
-
|
|
7908
|
+
Ny = !Ny, Lv(Ny), s2.classList.toggle("active", Ny), s2.querySelector(".toggle-icon").textContent = Ny ? "\u2295" : "\u2296";
|
|
7906
7909
|
}), hy.querySelectorAll(".orb-preset-btn:not(.node-btn)").forEach((e6) => {
|
|
7907
7910
|
e6.addEventListener("click", () => {
|
|
7908
7911
|
var t3;
|
|
7909
7912
|
let n3 = parseFloat((t3 = e6.dataset.orb) == null ? "8" : t3);
|
|
7910
|
-
n3 !==
|
|
7913
|
+
n3 !== Py && (Py = n3, hy.querySelectorAll(".orb-preset-btn:not(.node-btn)").forEach((e7) => e7.classList.toggle("active", e7.dataset.orb === String(n3))), Rv(n3));
|
|
7911
7914
|
});
|
|
7912
7915
|
}), hy.querySelectorAll(".node-btn").forEach((e6) => {
|
|
7913
7916
|
e6.addEventListener("click", () => {
|
|
7914
7917
|
let t3 = e6.dataset.node === "mean" ? "mean" : "true";
|
|
7915
|
-
t3 ===
|
|
7918
|
+
t3 === Fy || !$ || (Fy = t3, W_(t3), wy($));
|
|
7916
7919
|
});
|
|
7917
7920
|
});
|
|
7918
7921
|
}
|
|
7919
|
-
async function
|
|
7922
|
+
async function Ly(e5, t2) {
|
|
7920
7923
|
if (xy) try {
|
|
7921
7924
|
let n2 = "[Chart Wheel] Selected ".concat(e5, " \u2014 ").concat(Object.entries(t2).filter(([, e6]) => e6 != null && e6 !== false && e6 !== "").map(([e6, t3]) => "".concat(e6, ": ").concat(t3)).join(", "));
|
|
7922
7925
|
await vy.updateModelContext({ content: [{ type: "text", text: n2 }] });
|
|
@@ -7924,24 +7927,24 @@ async function Iy(e5, t2) {
|
|
|
7924
7927
|
console.warn("[ChartWheel] \u26A0\uFE0F updateModelContext failed:", e6);
|
|
7925
7928
|
}
|
|
7926
7929
|
}
|
|
7927
|
-
async function
|
|
7930
|
+
async function Ry(e5) {
|
|
7928
7931
|
var t2, n2, r2, i2;
|
|
7929
|
-
|
|
7932
|
+
Ay = null, my == null || my.querySelectorAll(".pattern-pill").forEach((e6) => {
|
|
7930
7933
|
var t3;
|
|
7931
7934
|
e6.classList.remove("active");
|
|
7932
7935
|
let n3 = (t3 = e6.dataset.color) == null ? "#888" : t3;
|
|
7933
7936
|
e6.style.color = n3, e6.style.borderColor = n3, e6.style.background = "transparent";
|
|
7934
|
-
}), await
|
|
7937
|
+
}), await Ly("planet", { planet: e5.name, longitude: e5.longitude, sign: (t2 = e5.sign) == null ? null : t2, house: (n2 = e5.house) == null ? null : n2, retrograde: (r2 = e5.retrograde) == null ? false : r2, speed: (i2 = e5.speed) == null ? null : i2 });
|
|
7935
7938
|
}
|
|
7936
|
-
async function
|
|
7939
|
+
async function zy(e5) {
|
|
7937
7940
|
var t2, n2;
|
|
7938
|
-
await
|
|
7941
|
+
await Ly("house", { house_number: e5.number, sign: (t2 = e5.sign) == null ? null : t2, cusp_longitude: e5.cusp_lon, ruler: (n2 = e5.ruler) == null ? null : n2 });
|
|
7939
7942
|
}
|
|
7940
|
-
async function
|
|
7943
|
+
async function By(e5) {
|
|
7941
7944
|
var t2, n2, r2, i2;
|
|
7942
|
-
await
|
|
7945
|
+
await Ly("aspect", { planet1: e5.planet1, planet2: e5.planet2, aspect_type: (t2 = e5.type) == null ? null : t2, orb: (n2 = e5.orb) == null ? null : n2, applying: (r2 = e5.applying) == null ? null : r2, angle: (i2 = e5.angle) == null ? null : i2 });
|
|
7943
7946
|
}
|
|
7944
|
-
function
|
|
7947
|
+
function Vy() {
|
|
7945
7948
|
return async (e5) => {
|
|
7946
7949
|
try {
|
|
7947
7950
|
var t2, n2;
|
|
@@ -7952,30 +7955,30 @@ function By() {
|
|
|
7952
7955
|
}
|
|
7953
7956
|
};
|
|
7954
7957
|
}
|
|
7955
|
-
async function
|
|
7958
|
+
async function Hy(e5) {
|
|
7956
7959
|
try {
|
|
7957
7960
|
(await vy.sendMessage({ role: "user", content: [{ type: "text", text: e5 }] })).isError && console.error("[ChartWheel] Host rejected sendMessage");
|
|
7958
7961
|
} catch (e6) {
|
|
7959
7962
|
console.error("[ChartWheel] sendMessage failed:", e6);
|
|
7960
7963
|
}
|
|
7961
7964
|
}
|
|
7962
|
-
async function
|
|
7965
|
+
async function Uy() {
|
|
7963
7966
|
var e5, t2, n2, r2;
|
|
7964
7967
|
if (!$) return;
|
|
7965
7968
|
let i2 = $, a2 = (e5 = i2._birth_params) == null ? {} : e5, o2 = (t2 = (n2 = a2.name) == null ? a2.location : n2) == null ? "this person" : t2, s2 = K_(i2.planets), c2 = ((r2 = i2.aspects) == null ? [] : r2).filter((e6) => {
|
|
7966
7969
|
var t3;
|
|
7967
7970
|
return ["conjunction", "opposition", "trine", "square", "sextile"].includes((t3 = e6.type) == null ? "" : t3);
|
|
7968
7971
|
}).slice(0, 8).map((e6) => " \u2022 ".concat(e6.planet1, " ").concat(e6.type, " ").concat(e6.planet2, " (").concat(Number(e6.orb).toFixed(1), "\xB0, ").concat(e6.applying ? "applying" : "separating", ")")).join("\n") || " \u2022 No major aspects found";
|
|
7969
|
-
await
|
|
7972
|
+
await Hy("Please give a complete natal chart interpretation for ".concat(o2, ".\n\nKey planet placements:\n").concat(s2.slice(0, 5).map((e6) => {
|
|
7970
7973
|
var t3;
|
|
7971
7974
|
return " \u2022 ".concat(e6.name, " in ").concat((t3 = e6.sign) == null ? "?" : t3).concat(e6.house ? ", House ".concat(e6.house) : "").concat(e6.retrograde ? " \u211E" : "");
|
|
7972
7975
|
}).join("\n"), "\n\nMajor aspects:\n").concat(c2, "\n\nCover: (1) Core identity and soul purpose (Sun, Moon, Ascendant), (2) The chart's dominant element, modality, and overall signature, (3) The most significant aspects and their psychological themes, (4) Key life areas of focus and challenge, (5) A practical synthesis \u2014 who is this person and what is their path?"));
|
|
7973
7976
|
}
|
|
7974
|
-
async function
|
|
7977
|
+
async function Wy() {
|
|
7975
7978
|
var e5, t2, n2, r2;
|
|
7976
7979
|
if (!$) return;
|
|
7977
7980
|
let i2 = $, a2 = (e5 = i2._birth_params) == null ? {} : e5;
|
|
7978
|
-
await
|
|
7981
|
+
await Hy("I'm exploring the natal chart for ".concat((t2 = (n2 = a2.name) == null ? a2.location : n2) == null ? "this chart" : t2, ". Key placements: ").concat(K_(i2.planets).slice(0, 4).map((e6) => {
|
|
7979
7982
|
var t3;
|
|
7980
7983
|
return "".concat(e6.name, " in ").concat((t3 = e6.sign) == null ? "?" : t3).concat(e6.house ? " H".concat(e6.house) : "");
|
|
7981
7984
|
}).join(", ") || "unknown placements", ". Major aspects: ").concat(((r2 = i2.aspects) == null ? [] : r2).filter((e6) => {
|
|
@@ -7983,7 +7986,7 @@ async function Uy() {
|
|
|
7983
7986
|
return ["conjunction", "opposition", "trine", "square", "sextile"].includes((t3 = e6.type) == null ? "" : t3);
|
|
7984
7987
|
}).slice(0, 5).map((e6) => "".concat(e6.planet1, " ").concat(e6.type, " ").concat(e6.planet2, " (").concat(Number(e6.orb).toFixed(1), "\xB0)")).join(", ") || "none detected", ".\n\nBased on this chart, please suggest 3-4 specific, insightful questions I could ask to understand this person more deeply \u2014 such as how particular placements manifest in real life, the most significant aspect dynamics, or how to integrate the chart's core tensions. Present each as a tappable question I can ask you."));
|
|
7985
7988
|
}
|
|
7986
|
-
async function
|
|
7989
|
+
async function Gy(e5) {
|
|
7987
7990
|
let t2 = "";
|
|
7988
7991
|
if (e5.type === "planet") {
|
|
7989
7992
|
var n2;
|
|
@@ -8018,7 +8021,7 @@ async function Wy(e5) {
|
|
|
8018
8021
|
throw console.error("[ChartWheel] \u274C sendMessage failed:", e6), e6;
|
|
8019
8022
|
}
|
|
8020
8023
|
}
|
|
8021
|
-
async function
|
|
8024
|
+
async function Ky(e5) {
|
|
8022
8025
|
if (!($ != null && $._birth_params)) {
|
|
8023
8026
|
Uv(py, yy);
|
|
8024
8027
|
return;
|
|
@@ -8031,21 +8034,21 @@ async function Gy(e5) {
|
|
|
8031
8034
|
console.error("[Chart] Recalculate failed:", e6), Uv(py, yy);
|
|
8032
8035
|
}
|
|
8033
8036
|
}
|
|
8034
|
-
var
|
|
8035
|
-
function
|
|
8036
|
-
|
|
8037
|
+
var qy;
|
|
8038
|
+
function Jy() {
|
|
8039
|
+
qy && (clearTimeout(qy), qy = void 0);
|
|
8037
8040
|
}
|
|
8038
|
-
function
|
|
8039
|
-
|
|
8041
|
+
function Yy(e5, t2) {
|
|
8042
|
+
Jy(), ly.setAttribute("hidden", ""), iy({ container: uy, message: e5, onRetry: t2 });
|
|
8040
8043
|
}
|
|
8041
|
-
function
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
+
function Xy() {
|
|
8045
|
+
Jy(), uy.setAttribute("hidden", ""), ly.removeAttribute("hidden"), qy = setTimeout(() => {
|
|
8046
|
+
Yy("The chart didn't load. Tap to try again.", Xy);
|
|
8044
8047
|
}, 3e4), vy.sendMessage({ role: "user", content: [{ type: "text", text: "Please regenerate the chart wheel." }] }).catch(() => {
|
|
8045
8048
|
});
|
|
8046
8049
|
}
|
|
8047
|
-
oy(vy, { loadingEl: ly, contentEl: uy, statusFn: Cy, paramControls: () => by }),
|
|
8048
|
-
|
|
8050
|
+
oy(vy, { loadingEl: ly, contentEl: uy, statusFn: Cy, paramControls: () => by }), qy = setTimeout(() => {
|
|
8051
|
+
Yy("Chart data is taking longer than expected.", Xy);
|
|
8049
8052
|
}, 3e4), (async () => {
|
|
8050
8053
|
try {
|
|
8051
8054
|
await vy.connect();
|
|
@@ -8054,7 +8057,7 @@ oy(vy, { loadingEl: ly, contentEl: uy, statusFn: Cy, paramControls: () => by }),
|
|
|
8054
8057
|
$ && wy($);
|
|
8055
8058
|
}), $ && wy($);
|
|
8056
8059
|
} catch (e5) {
|
|
8057
|
-
console.error("[ChartWheel] \u274C app.connect() FAILED:", e5),
|
|
8060
|
+
console.error("[ChartWheel] \u274C app.connect() FAILED:", e5), Yy("The chart didn't load. Tap to try again.", Xy);
|
|
8058
8061
|
}
|
|
8059
8062
|
})();
|
|
8060
8063
|
</script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openephemeris/mcp-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"description": "Model Context Protocol server for the Open Ephemeris astronomical computation API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"check:schema-packs": "tsx scripts/schema-packs.ts --check",
|
|
42
42
|
"sync:readme": "tsx scripts/sync-readme.ts --write",
|
|
43
43
|
"check:readme": "tsx scripts/sync-readme.ts --check",
|
|
44
|
+
"check:counts": "tsx scripts/check-counts.ts",
|
|
44
45
|
"check:pack": "tsx scripts/pack-audit.ts",
|
|
45
46
|
"regen:plugin": "tsx scripts/plugin-audit.ts --write",
|
|
46
47
|
"check:plugin": "tsx scripts/plugin-audit.ts --check",
|
|
@@ -48,7 +49,8 @@
|
|
|
48
49
|
"check:skills": "node scripts/build-skills.mjs --check",
|
|
49
50
|
"sync:public": "node scripts/sync-public-repo.mjs --write",
|
|
50
51
|
"check:public": "node scripts/sync-public-repo.mjs --check",
|
|
51
|
-
"
|
|
52
|
+
"check:public:strict": "node scripts/sync-public-repo.mjs --check --require-mirror",
|
|
53
|
+
"verify:release": "npm run check:dev-allowlist && npm run check:schema-packs && npm run check:readme && npm run check:counts && npm run check:skills && npm run check:plugin && npm run check:public:strict && npm run typecheck && npm test && npm run build && npm run check:pack",
|
|
52
54
|
"prepack": "npm run build",
|
|
53
55
|
"prepublishOnly": "npm run verify:release",
|
|
54
56
|
"capture:fixtures": "tsx scripts/capture-harness-fixtures.ts"
|
package/smithery.yaml
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
name: Open Ephemeris
|
|
9
9
|
description: >
|
|
10
|
-
NASA JPL DE440-backed astronomical computation engine for AI agents.
|
|
10
|
+
NASA JPL DE440-backed astronomical computation engine for AI agents. Typed tools spanning
|
|
11
11
|
natal charts, transit forecasting, Human Design charts, eclipses, astrocartography power
|
|
12
12
|
lines, Venus Star Points, electional timing windows, synastry, composite charts, Vedic
|
|
13
13
|
charts, Chinese BaZi, and more — powered by JPL DE440 ephemerides for sub-arcsecond
|
|
14
14
|
zero-hallucination accuracy. Includes an LLM-optimized output format
|
|
15
|
-
(format=llm) that compresses chart responses by
|
|
15
|
+
(format=llm) that compresses chart responses by 50–73%. Free Explorer tier available.
|
|
16
16
|
iconUrl: https://mcp.openephemeris.com/icon.png
|
|
17
17
|
homepage: https://openephemeris.com
|
|
18
18
|
|
|
@@ -38,7 +38,7 @@ configSchema:
|
|
|
38
38
|
- llm
|
|
39
39
|
description: >
|
|
40
40
|
Default output format for all tools. 'llm' returns compact,
|
|
41
|
-
token-optimized output (
|
|
41
|
+
token-optimized output (50–73% smaller). 'json' returns full verbose JSON.
|
|
42
42
|
Individual tool calls can override this setting.
|
|
43
43
|
default: json
|
|
44
44
|
baseUrl:
|
|
@@ -49,6 +49,16 @@ configSchema:
|
|
|
49
49
|
default production endpoint (https://api.openephemeris.com). Useful
|
|
50
50
|
for self-hosted or staging deployments.
|
|
51
51
|
default: ""
|
|
52
|
+
toolSurface:
|
|
53
|
+
type: string
|
|
54
|
+
title: Tool Surface
|
|
55
|
+
description: >
|
|
56
|
+
Which tools to advertise. Leave blank for the curated default set,
|
|
57
|
+
'full' for the whole catalog, or a comma list of traditions —
|
|
58
|
+
astrology, moon, hd, bazi, vedic, acg, electional, venus — to pay
|
|
59
|
+
context only for the work you do (e.g. 'hd,bazi'). Every tool stays
|
|
60
|
+
callable by name regardless of surface.
|
|
61
|
+
default: ""
|
|
52
62
|
|
|
53
63
|
# How to start the server locally (stdio transport for Claude Desktop / Cursor)
|
|
54
64
|
startCommand:
|
|
@@ -61,5 +71,6 @@ startCommand:
|
|
|
61
71
|
OPENEPHEMERIS_API_KEY: config.apiKey,
|
|
62
72
|
...(config.format && config.format !== "json" ? { OPENEPHEMERIS_FORMAT: config.format } : {}),
|
|
63
73
|
...(config.baseUrl ? { OPENEPHEMERIS_BACKEND_URL: config.baseUrl } : {}),
|
|
74
|
+
...(config.toolSurface ? { OPENEPHEMERIS_TOOLS: config.toolSurface } : {}),
|
|
64
75
|
}
|
|
65
76
|
})
|