@openephemeris/mcp-server 4.12.0 → 4.13.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 CHANGED
@@ -7,6 +7,74 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [4.13.0] — 2026-08-27
11
+
12
+ ### Added
13
+ - **Sidereal charts on `ephemeris_natal_chart`.** New `zodiac_type`
14
+ (`tropical` | `sidereal`) and `ayanamsa` (Lahiri, Fagan-Bradley,
15
+ Krishnamurti, Raman, Yukteshwar). Sidereal subtracts the ayanamsa from every
16
+ longitude, so signs, dignities and element balance all move by roughly a
17
+ whole sign; aspects and house numbers do not change, because a uniform
18
+ offset preserves angular separation and which side of a cusp a body falls
19
+ on. The response names the zodiac and the ayanamsa that produced it. Naming
20
+ an ayanamsa without asking for sidereal is now an error rather than a
21
+ quietly tropical chart.
22
+
23
+ The API had accepted `zodiac_type` for as long as the natal contract has
24
+ existed and read it nowhere, so a request for a sidereal chart came back
25
+ tropical — not imprecise, a different chart — with nothing in the payload
26
+ saying which zodiac you had been given. That is fixed on the server, which
27
+ is the only reason the parameter is exposed here.
28
+
29
+ - **Whole-chart analytics on `ephemeris_natal_chart`.** `analytics` accepts any
30
+ of `element_balance`, `hemisphere_emphasis`, `aspect_patterns` and
31
+ `harmonics`. No extra credits. The endpoint has always computed these on
32
+ request and this server never asked for them, so they were unreachable from
33
+ MCP no matter what a caller wanted.
34
+
35
+ - **`include_minor_aspects` on `ephemeris_natal_chart`.** Adds quincunx,
36
+ semisextile, semisquare, sesquiquadrate, quintile and biquintile to the
37
+ aspect grid. Minors use their own orbs, so this widens the grid without
38
+ changing any major aspect.
39
+
40
+ - **`latitude` / `longitude` on `electional_moment_analysis`.** Both optional —
41
+ the tool is documented as taking no arguments at all and still does — but
42
+ without them sect falls back to diurnal, so a night moment is scored with
43
+ the day triplicity rulers. Pass both whenever the place is known.
44
+
45
+ ### Removed
46
+ - **`include_fixed_stars` on `ephemeris_natal_chart`.** It never did anything.
47
+ The natal endpoint reads no fixed-star configuration on any of the keys this
48
+ flag was routed to, so the response was identical either way. Tool
49
+ definitions are re-sent on every model pass, which means a parameter that
50
+ does nothing is still paid for by every user on every request. Fixed stars
51
+ come from `ephemeris_fixed_stars`, which also reports which chart points sit
52
+ conjunct them. Unknown arguments are not rejected, so a caller still sending
53
+ the old flag is ignored exactly as it was before.
54
+
55
+ - **The `alcabitius` and `morinus` house systems.** Advertised on three tools
56
+ and broken on all of them: the codes sent for them are accepted by no code
57
+ path, so every call naming one came back a validation error. The seven
58
+ systems that remain are the whole set the engine implements.
59
+
60
+ ### Fixed
61
+ - **`dev_read_api` advertised fifteen POST endpoints it rejects.** It is
62
+ GET-only, and it shared one reference block with the write proxy, so most of
63
+ its description documented calls it cannot make. It is on the default tool
64
+ surface, so every client paid for that text on every model pass. Each proxy
65
+ now lists only the calls it accepts.
66
+
67
+ - **The compact `llm` output could not express a minor aspect.** Its aspect
68
+ dictionary declared five entries while the engine can now emit eleven, so a
69
+ minor aspect had no valid code to travel under. All eleven are declared.
70
+
71
+ ### Changed
72
+ - **Endpoint count corrected to 120** across the bundled skills, and the
73
+ plugin skill's fixed-star guidance now points at `ephemeris_fixed_stars`
74
+ rather than a flag that never worked.
75
+
76
+ ---
77
+
10
78
  ## [4.12.0] — 2026-08-13
11
79
 
12
80
  ### Fixed
@@ -375,6 +443,27 @@ the code.
375
443
  `options.include_hermetic_lots: true`); only the descriptions were
376
444
  stale.
377
445
 
446
+ > **Correction (2026-08-23).** Half of the entry above is wrong, and it is
447
+ > left standing rather than deleted so the record shows what was claimed.
448
+ >
449
+ > `include_arabic_parts` is accurate: the natal endpoint reads
450
+ > `include_hermetic_lots` at the top level, under `options`, and under
451
+ > `enhanced_options`, and has done so all along.
452
+ >
453
+ > `include_fixed_stars` was **not** wired and still is not. The natal
454
+ > endpoint reads no fixed-star configuration at any of the three keys —
455
+ > not `configuration.fixed_star_options`, not
456
+ > `enhanced_options.include_fixed_stars`, not
457
+ > `options.include_fixed_stars` — so setting the flag changes nothing in
458
+ > the response and never has. What made it look wired is that the
459
+ > identical `configuration.fixed_star_options` block genuinely does work
460
+ > on the chart-wheel and bi-wheel renderers, which share the request
461
+ > shape but not the code path.
462
+ >
463
+ > Until the natal endpoint honours it, `POST /ephemeris/fixed-stars` is
464
+ > the way to get fixed-star positions; it accepts the same star names,
465
+ > groups, and magnitude limits and returns conjunctions to the angles.
466
+
378
467
  ---
379
468
 
380
469
  ## [4.3.1] — 2026-07-29
package/README.md CHANGED
@@ -10,6 +10,8 @@
10
10
 
11
11
  Model Context Protocol server for OpenEphemeris — typed astrology tools powered by the NASA JPL DE440 ephemeris. Zero hallucination on planetary positions, dates, and degrees. Covers 1,100 years of astronomical data.
12
12
 
13
+ ![The catalog: 90 bodies and 124 fixed stars — asteroids, trans-Neptunians, Uranian points, computed in one engine](https://raw.githubusercontent.com/openephemeris/openephemeris-MCP/main/assets/catalog-90-bodies.png)
14
+
13
15
  **Hosted endpoint:** `https://mcp.openephemeris.com/mcp` (Streamable HTTP, MCP 2025-11-25 spec)
14
16
 
15
17
  ## Quick Start
package/dist/prompts.js CHANGED
@@ -16,7 +16,9 @@ export const PROMPTS = [
16
16
  description: "Orientation guide for the Open Ephemeris MCP. Understand available tools, key concepts, " +
17
17
  "and how to pick the right prompt for the task.",
18
18
  text: "Welcome to the **Open Ephemeris MCP Server** — an enterprise-grade astrological and astronomical " +
19
- "computation engine powered by NASA JPL DE440 ephemeris data (1550–2650 CE).\n\n" +
19
+ "computation engine powered by NASA JPL DE440 ephemeris data (1550–2650 CE for the Sun, " +
20
+ "Moon, and planets; 1600–2200 CE for the extended asteroid, centaur, and trans-Neptunian " +
21
+ "catalog).\n\n" +
20
22
  "## Available Workflows\n" +
21
23
  "There are specialized prompts for each of the following. Ask the user which they'd like, " +
22
24
  "or jump in directly if they've already told you:\n\n" +
@@ -3,8 +3,8 @@ export declare const LLM_V2_POINTS_SCHEMA: readonly ["id", "kind", "src", "lon",
3
3
  export declare const LLM_V2_ASPECTS_SCHEMA: readonly ["a_i", "b_i", "t", "orb", "orb_pct", "app", "str"];
4
4
  export declare const LLM_V2_DICT: {
5
5
  readonly sign_id: readonly ["ari", "tau", "gem", "can", "leo", "vir", "lib", "sco", "sag", "cap", "aqu", "pis"];
6
- readonly aspect_id: readonly ["con", "opp", "tri", "sqr", "sex"];
7
- readonly aspect_angle: readonly [0, 180, 120, 90, 60];
6
+ readonly aspect_id: readonly ["con", "opp", "tri", "sqr", "sex", "ssx", "ssq", "qui", "sqq", "bqu", "qcx"];
7
+ readonly aspect_angle: readonly [0, 180, 120, 90, 60, 30, 45, 72, 135, 144, 150];
8
8
  readonly kind_id: readonly ["planet", "angle", "node", "lilith", "asteroid", "other"];
9
9
  };
10
10
  export declare const LlmV2PayloadSchema: z.ZodObject<{
@@ -18,10 +18,19 @@ export const LLM_V2_POINTS_SCHEMA = [
18
18
  "on_cusp",
19
19
  ];
20
20
  export const LLM_V2_ASPECTS_SCHEMA = ["a_i", "b_i", "t", "orb", "orb_pct", "app", "str"];
21
+ // aspect_id / aspect_angle mirror the Go projection's dictionary
22
+ // (llmAspectIDs / llmAspectAngles in internal/api/handlers/llm_projection.go).
23
+ // The six minor aspects became detectable behind options.include_minor_aspects,
24
+ // so they are enumerated here too — a `t` value the pack does not declare fails
25
+ // DictZ, which is a strict tuple of literals, and check:schema-packs gates the
26
+ // release on it.
27
+ //
28
+ // APPEND ONLY, and in the Go dictionary's order: the index is the wire value of
29
+ // the aspects table's `t` column, so reordering re-points every stored row.
21
30
  export const LLM_V2_DICT = {
22
31
  sign_id: ["ari", "tau", "gem", "can", "leo", "vir", "lib", "sco", "sag", "cap", "aqu", "pis"],
23
- aspect_id: ["con", "opp", "tri", "sqr", "sex"],
24
- aspect_angle: [0, 180, 120, 90, 60],
32
+ aspect_id: ["con", "opp", "tri", "sqr", "sex", "ssx", "ssq", "qui", "sqq", "bqu", "qcx"],
33
+ aspect_angle: [0, 180, 120, 90, 60, 30, 45, 72, 135, 144, 150],
25
34
  kind_id: ["planet", "angle", "node", "lilith", "asteroid", "other"],
26
35
  };
27
36
  const PointsSchemaZ = z.tuple(LLM_V2_POINTS_SCHEMA.map((v) => z.literal(v)));
@@ -5,6 +5,17 @@ export interface ResolvedLocation {
5
5
  displayName: string;
6
6
  placeId: string | null;
7
7
  }
8
+ /**
9
+ * Whether the query itself already pins the top hit — "dallas texas",
10
+ * "san francisco, ca", "paris, france", "london uk" — so rival cities of
11
+ * the same short name are not actually ambiguous.
12
+ *
13
+ * Single source of truth for the ambiguity qualifier: both
14
+ * `resolveLocationOrThrow` and the `location_search` tool call this. The
15
+ * two call sites read differently-named fields (raw `region`/`country_code`
16
+ * vs mapped `region`/`countryCode`), so this takes plain strings.
17
+ */
18
+ export declare function isQualifiedForTopHit(query: string, region: unknown, countryCode: unknown): boolean;
8
19
  /**
9
20
  * Resolve a place name to lat/lon/tz. Throws if ambiguous or unresolvable.
10
21
  *
@@ -52,6 +52,29 @@ function countryNameFor(code) {
52
52
  // Colloquial forms Intl does not produce: it renders GB as "United Kingdom",
53
53
  // so a bare "uk" token would otherwise miss.
54
54
  const COUNTRY_ALIASES = { uk: "gb", usa: "us", uae: "ae" };
55
+ /**
56
+ * Whether the query itself already pins the top hit — "dallas texas",
57
+ * "san francisco, ca", "paris, france", "london uk" — so rival cities of
58
+ * the same short name are not actually ambiguous.
59
+ *
60
+ * Single source of truth for the ambiguity qualifier: both
61
+ * `resolveLocationOrThrow` and the `location_search` tool call this. The
62
+ * two call sites read differently-named fields (raw `region`/`country_code`
63
+ * vs mapped `region`/`countryCode`), so this takes plain strings.
64
+ */
65
+ export function isQualifiedForTopHit(query, region, countryCode) {
66
+ const norm = (v) => String(v ?? "").trim().toLowerCase();
67
+ const q = norm(query);
68
+ const qTokens = new Set(q.split(/[^a-z0-9]+/).filter(Boolean));
69
+ const topRegion = norm(region);
70
+ const topCountry = norm(countryCode);
71
+ const topCountryName = topCountry === "" ? "" : countryNameFor(topCountry);
72
+ return ((topRegion !== "" && q.includes(topRegion)) ||
73
+ (topRegion !== "" && [...qTokens].some((t) => US_STATE_ABBREVIATIONS[t] === topRegion)) ||
74
+ (topCountry !== "" && qTokens.has(topCountry)) ||
75
+ (topCountryName !== "" && q.includes(topCountryName)) ||
76
+ (topCountry !== "" && [...qTokens].some((t) => COUNTRY_ALIASES[t] === topCountry)));
77
+ }
55
78
  // `code` rides along to PostHog via the existing `code` property on
56
79
  // mcp_tool_error. The event deliberately records no message or stack, so
57
80
  // without a code every one of these lands as code:"none" / error_kind:"local"
@@ -82,16 +105,7 @@ export async function resolveLocationOrThrow(location) {
82
105
  const norm = (v) => String(v ?? "").trim().toLowerCase();
83
106
  const top = list[0];
84
107
  const rivals = list.filter((s) => norm(s.short_name) === norm(top.short_name));
85
- const q = norm(query);
86
- const qTokens = new Set(q.split(/[^a-z0-9]+/).filter(Boolean));
87
- const topRegion = norm(top.region);
88
- const topCountry = norm(top.country_code);
89
- const topCountryName = topCountry === "" ? "" : countryNameFor(topCountry);
90
- const qualified = (topRegion !== "" && q.includes(topRegion)) ||
91
- (topRegion !== "" && [...qTokens].some((t) => US_STATE_ABBREVIATIONS[t] === topRegion)) ||
92
- (topCountry !== "" && qTokens.has(topCountry)) ||
93
- (topCountryName !== "" && q.includes(topCountryName)) ||
94
- (topCountry !== "" && [...qTokens].some((t) => COUNTRY_ALIASES[t] === topCountry));
108
+ const qualified = isQualifiedForTopHit(query, top.region, top.country_code);
95
109
  const ambiguous = rivals.length > 1 && !qualified;
96
110
  if (ambiguous) {
97
111
  const options = rivals
@@ -88,7 +88,16 @@ function prettyHouseSystem(hs) {
88
88
  return hs.split("_").map(capitalize).join(" ");
89
89
  }
90
90
  /** Build the nested request body expected by /ephemeris/natal-chart (POST). */
91
- function buildNatalBody(datetime, lat, lon, houseSystem, timezone) {
91
+ function buildNatalBody(datetime, lat, lon, houseSystem, timezone, additionalObjects) {
92
+ const configuration = {
93
+ house_system: HOUSE_SYSTEM_MAP[houseSystem] ?? "P",
94
+ };
95
+ if (additionalObjects && additionalObjects.length > 0) {
96
+ // additional_objects is additive on top of the API's fixed base roster
97
+ // (classical planets, nodes, Chiron, major asteroids) — it does not
98
+ // replace it. See handler_ephemeris2.go's body-ID resolution.
99
+ configuration.additional_objects = additionalObjects;
100
+ }
92
101
  const body = {
93
102
  subject: {
94
103
  name: "MCP Request",
@@ -98,9 +107,7 @@ function buildNatalBody(datetime, lat, lon, houseSystem, timezone) {
98
107
  longitude: { decimal: lon ?? 0 },
99
108
  },
100
109
  },
101
- configuration: {
102
- house_system: HOUSE_SYSTEM_MAP[houseSystem] ?? "P",
103
- },
110
+ configuration,
104
111
  options: {
105
112
  include_aspects: true,
106
113
  },
@@ -149,8 +156,10 @@ registerTool({
149
156
  items: { type: "string" },
150
157
  description: "Optional list of body names to include. " +
151
158
  "Defaults to 13 classical bodies (Sun through Pluto + Chiron + Nodes). " +
152
- "Use 'all' as a single item to include every available body (Lilith, Ceres, Juno, Vesta, Pallas, Vertex, etc.). " +
153
- "Example: ['sun','moon','lilith','vertex'].",
159
+ "Use 'all' as a single item to include every available body (Lilith, Ceres, Juno, Vesta, Pallas, Vertex, " +
160
+ "the 8 trans-Neptunian objects — eris, sedna, makemake, haumea, quaoar, orcus, ixion, varuna — and the " +
161
+ "8 Uranian/Hamburg-School points — cupido, hades, zeus, kronos, apollon, admetos, vulkanus, poseidon). " +
162
+ "Example: ['sun','moon','lilith','sedna'].",
154
163
  },
155
164
  },
156
165
  required: ["datetime"],
@@ -189,18 +198,22 @@ registerTool({
189
198
  // caller's explicit timezone still wins when supplied.
190
199
  const effectiveTimezone = args.timezone ?? resolved.timezone;
191
200
  const effectiveLocation = resolved.location ?? args.location;
192
- const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, effectiveTimezone);
193
- // Determine which bodies are requested
201
+ // Determine which bodies are requested. Recognized extended slugs (TNOs,
202
+ // Uranian points, Lilith variants, etc.) are sent to the API as
203
+ // additional_objects so the API actually computes them — previously this
204
+ // only flipped legacy include_* booleans the natal-chart endpoint's
205
+ // schema doesn't have, so extended bodies (beyond the ones already in the
206
+ // API's fixed default roster) never reached the response. See #612-area
207
+ // catalog-expansion follow-up: TNO/Uranian bodies were addressable via
208
+ // /catalogs/bodies but unreachable from any chart-computation endpoint.
194
209
  const requestedBodies = args.bodies;
195
210
  const wantsAll = requestedBodies?.some(b => b.toLowerCase() === "all");
196
- const wantsExtended = wantsAll || requestedBodies?.some(b => EXTENDED_BODIES.has(b.toLowerCase()));
197
- // Enable extended body computation on the API side when needed
198
- if (wantsExtended) {
199
- const config = natalBody.configuration;
200
- config.include_asteroids = true;
201
- config.include_lilith = true;
202
- config.include_chiron = true;
203
- }
211
+ const additionalObjects = wantsAll
212
+ ? Array.from(EXTENDED_BODIES)
213
+ : (requestedBodies ?? [])
214
+ .map(b => b.toLowerCase())
215
+ .filter(b => EXTENDED_BODIES.has(b));
216
+ const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, effectiveTimezone, additionalObjects);
204
217
  // Fetch natal chart JSON. The chart is rendered client-side in the UI iframe,
205
218
  // so we do NOT call the /visualization/chart-wheel endpoint — that was the
206
219
  // source of the ~90 second blocking delay.
@@ -461,10 +474,15 @@ function canonicalizeBodyName(raw) {
461
474
  return lower;
462
475
  }
463
476
  const EXTENDED_BODIES = new Set([
464
- "mean_lilith", "lilith", "true_lilith",
477
+ // Slugs match the Go API's ephemerisIDForSlug (handler_wheel_resolver.go) so
478
+ // additional_objects entries actually resolve server-side.
479
+ "mean_lilith", "lilith", "lilith_true",
465
480
  "ceres", "juno", "vesta", "pallas", "pholus",
466
481
  "vertex", "part_of_fortune",
467
- "eris", "sedna",
482
+ // Trans-Neptunian objects (all 8 — #603 catalog expansion)
483
+ "eris", "sedna", "makemake", "haumea", "quaoar", "orcus", "ixion", "varuna",
484
+ // Uranian / Hamburg School hypothetical points (all 8 — #603 catalog expansion)
485
+ "cupido", "hades", "zeus", "kronos", "apollon", "admetos", "vulkanus", "poseidon",
468
486
  ]);
469
487
  const ALL_KNOWN_BODIES = new Set([...CLASSICAL_PLANETS, ...EXTENDED_BODIES]);
470
488
  /**
@@ -2,6 +2,7 @@ import { registerTool, validateRequired } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
3
  import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
4
4
  import { TZDATA_AUTHORITATIVE_FROM_YEAR } from "../datetime-historical.js";
5
+ import { isQualifiedForTopHit } from "./_location-resolver.js";
5
6
  /**
6
7
  * Lift the API's historical-correction provenance onto an MCP result.
7
8
  *
@@ -254,11 +255,7 @@ registerTool({
254
255
  const rivals = top ? suggestions.filter((s) => norm(s.shortName) === norm(top.shortName)) : [];
255
256
  // If the caller already pinned the place — "dallas texas", "portland uk" —
256
257
  // the top hit is what they asked for and there is nothing to ask about.
257
- const q = norm(args.query);
258
- const qTokens = new Set(q.split(/[^a-z0-9]+/).filter(Boolean));
259
- const qualified = !!top &&
260
- ((norm(top.region) !== "" && q.includes(norm(top.region))) ||
261
- (norm(top.countryCode) !== "" && qTokens.has(norm(top.countryCode))));
258
+ const qualified = !!top && isQualifiedForTopHit(args.query, top.region, top.countryCode);
262
259
  const ambiguous = rivals.length > 1 && !qualified;
263
260
  return {
264
261
  suggestions,
package/dist/tools/dev.js CHANGED
@@ -34,7 +34,14 @@ function isAllowedOperation(method, pathname, allow) {
34
34
  }
35
35
  // Shared reference block appended to both read/write proxy tools. Names the
36
36
  // target API (Open Ephemeris) explicitly — required by the directory for
37
- // freeform-path tools — plus credit costs and common calls.
37
+ // freeform-path tools — plus credit costs.
38
+ //
39
+ // The COMMON CALLS list is NOT shared, because it is method-specific and the
40
+ // two tools do not accept the same methods. It used to be: dev_read_api is
41
+ // GET-only (READ_METHODS), and it was spending most of its description listing
42
+ // fifteen POST endpoints it will reject — on the core surface, which every
43
+ // client pays for on every model pass. Each tool now lists the calls it can
44
+ // actually make.
38
45
  const DEV_API_REFERENCE = "Target API: Open Ephemeris REST API (https://api.openephemeris.com). " +
39
46
  "Call dev_list_allowed to see all currently available endpoint paths.\n\n" +
40
47
  "AUTH: Set OPENEPHEMERIS_API_KEY in your environment. See openephemeris.com/dashboard for active plan limits.\n\n" +
@@ -50,7 +57,27 @@ const DEV_API_REFERENCE = "Target API: Open Ephemeris REST API (https://api.open
50
57
  " • Catalog / metadata / health endpoints: 0 credits\n" +
51
58
  " • Compute surcharge: requests > 30s add 1 credit per 30s (predictive, acg, calendar, electional)\n" +
52
59
  " • format=llm (token-optimized output): available on all tiers\n\n" +
53
- "COMMON CALLS:\n" +
60
+ "BINARY RESPONSES:\n" +
61
+ " • Binary/image endpoints return {content_type, content_length, encoding, data_base64}\n" +
62
+ " so callers can decode bytes deterministically.\n\n" +
63
+ "ECLIPSE NOTE: Eclipse endpoints accept format=llm via the query param like other endpoints.\n\n" +
64
+ "format=llm NOTE: Add query: {format: 'llm'} to natal/synastry/composite/HD endpoints for " +
65
+ "compact columnar output optimized for LLM token budgets (availability depends on your current plan).";
66
+ const READ_COMMON_CALLS = "COMMON CALLS:\n" +
67
+ " GET /ephemeris/moon/phase — Current/queried moon phase\n" +
68
+ " GET /ephemeris/moon/void-of-course — Next void-of-course period\n" +
69
+ " GET /ephemeris/agro/daily — Biodynamic farming day quality\n" +
70
+ " GET /ephemeris/agro/calendar — Multi-day biodynamic calendar\n" +
71
+ " GET /ephemeris/agro/void-of-course — Biodynamic VoC periods\n" +
72
+ " GET /eclipse/next-visible — Next eclipse visible from a location (query: lat, lon, type=solar|lunar)\n" +
73
+ " GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
74
+ " GET /eclipse/solar/local — Local solar eclipse (query: lat, lon)\n" +
75
+ " GET /tidal/forcing — Gravitational tidal forcing index\n" +
76
+ " GET /calendar/astrology/moon-phases — Moon phase calendar for a date range\n" +
77
+ " GET /location/autocomplete — Geocode a place name (query: query=City Name)\n" +
78
+ " GET /chinese/zodiac — Chinese zodiac year element/animal\n" +
79
+ " GET /catalogs/bodies — List all supported celestial bodies\n";
80
+ const WRITE_COMMON_CALLS = "COMMON CALLS:\n" +
54
81
  " POST /ephemeris/natal-chart — Full natal chart (body: {subject: {name: 'Name', birth_datetime: {iso: '1990-04-15T14:30:00-05:00'}, birth_location: {latitude: {decimal: 40.0}, longitude: {decimal: -70.0}, timezone: {}}}})\n" +
55
82
  " POST /ephemeris/natal/batch — Up to 50 natal charts in one request\n" +
56
83
  " POST /ephemeris/relocation — Relocated chart (same natal, new location)\n" +
@@ -61,30 +88,11 @@ const DEV_API_REFERENCE = "Target API: Open Ephemeris REST API (https://api.open
61
88
  " POST /comparative/composite — Composite (midpoint) chart\n" +
62
89
  " POST /human-design/chart — Full HD chart (body: {birth_datetime_utc: '1990-04-15T19:30:00Z'}) — lat/lon optional\n" +
63
90
  " POST /time/julian-day — Convert date to JD (body: {year: 1987, month: 7, day: 15, hour: 14, minute: 1})\n" +
64
- " GET /ephemeris/moon/phase — Current/queried moon phase\n" +
65
- " GET /ephemeris/moon/void-of-course — Next void-of-course period\n" +
66
- " GET /ephemeris/agro/daily — Biodynamic farming day quality\n" +
67
- " GET /ephemeris/agro/calendar — Multi-day biodynamic calendar\n" +
68
- " GET /ephemeris/agro/void-of-course — Biodynamic VoC periods\n" +
69
- " GET /eclipse/next-visible — Next eclipse visible from a location (query: lat, lon, type=solar|lunar)\n" +
70
- " GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
71
- " GET /eclipse/solar/local — Local solar eclipse (query: lat, lon)\n" +
72
- " GET /tidal/forcing — Gravitational tidal forcing index\n" +
73
91
  " POST /acg/power-lines — Astrocartography power lines (lat/lon GeoJSON)\n" +
74
92
  " POST /acg/hits — ACG power at a specific location\n" +
75
- " GET /calendar/astrology/moon-phases — Moon phase calendar for a date range\n" +
76
- " GET /location/autocomplete — Geocode a place name (query: query=City Name)\n" +
77
93
  " POST /timezone/lookup — Resolve timezone + UTC offset for a location\n" +
78
94
  " POST /chinese/bazi — Chinese Ba Zi (Four Pillars) chart (body: {year, month, day, hour})\n" +
79
- " GET /chinese/zodiac — Chinese zodiac year element/animal\n" +
80
- " POST /vedic/chart — Vedic (Jyotish) natal chart (body: {datetime_utc, latitude, longitude})\n" +
81
- " GET /catalogs/bodies — List all supported celestial bodies\n\n" +
82
- "BINARY RESPONSES:\n" +
83
- " • Binary/image endpoints return {content_type, content_length, encoding, data_base64}\n" +
84
- " so callers can decode bytes deterministically.\n\n" +
85
- "ECLIPSE NOTE: Eclipse endpoints accept format=llm via the query param like other endpoints.\n\n" +
86
- "format=llm NOTE: Add query: {format: 'llm'} to natal/synastry/composite/HD endpoints for " +
87
- "compact columnar output optimized for LLM token budgets (availability depends on your current plan).";
95
+ " POST /vedic/chart — Vedic (Jyotish) natal chart (body: {datetime_utc, latitude, longitude})";
88
96
  // The read and write proxies are kept as separate tools (not one method-switching
89
97
  // tool) so that safe GET reads never share a surface with state-changing writes —
90
98
  // a hard requirement of the Anthropic connector directory.
@@ -193,7 +201,7 @@ registerTool({
193
201
  "first for common operations. Endpoints that compute via POST (natal-chart, synastry, etc.) " +
194
202
  "are covered by the typed tools; the generic POST proxy is only exposed on the full tool " +
195
203
  "surface (`?profile=full`).\n\n" +
196
- DEV_API_REFERENCE,
204
+ DEV_API_REFERENCE + "\n\n" + READ_COMMON_CALLS,
197
205
  inputSchema: makeProxyInputSchema(READ_METHODS),
198
206
  outputSchema: OUTPUT_SCHEMA_JSON,
199
207
  // GET-only: never mutates server state. openWorldHint false — the proxy
@@ -208,7 +216,7 @@ registerTool({
208
216
  "Most chart computations (natal-chart, synastry, composite, transits/search, returns, ACG) are " +
209
217
  "POST endpoints and use this tool. Use the typed tools first for common operations; use dev_read_api " +
210
218
  "for GET endpoints.\n\n" +
211
- DEV_API_REFERENCE,
219
+ DEV_API_REFERENCE + "\n\n" + WRITE_COMMON_CALLS,
212
220
  inputSchema: makeProxyInputSchema(WRITE_METHODS),
213
221
  outputSchema: OUTPUT_SCHEMA_JSON,
214
222
  // Issues POST/PUT/PATCH/DELETE — must not be advertised as read-only. But
@@ -2,10 +2,11 @@ import { registerTool, validateRequired, pickEnum } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
3
  import { OUTPUT_SCHEMA_IMAGE } from "../output-schemas.js";
4
4
  import { DATETIME_DESC, assertZonedDatetime, timezoneProperty } from "../datetime.js";
5
+ // The seven systems the engine accepts (internal/math/houses.go).
6
+ // Do not re-add alcabitius/morinus — see the note in natal.ts.
5
7
  const HOUSE_SYSTEM_MAP = {
6
8
  placidus: "P", whole_sign: "W", equal: "E", koch: "K",
7
9
  campanus: "C", regiomontanus: "R", porphyry: "O",
8
- alcabitius: "B", morinus: "M",
9
10
  };
10
11
  registerTool({
11
12
  name: "ephemeris_bi_wheel",
@@ -27,7 +28,7 @@ registerTool({
27
28
  longitude_b: { type: "number", description: "Longitude for subject B" },
28
29
  house_system: {
29
30
  type: "string",
30
- enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry", "alcabitius", "morinus"],
31
+ enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry"],
31
32
  description: "House system to use. Defaults to 'placidus' if omitted.",
32
33
  },
33
34
  style: {
@@ -2,10 +2,11 @@ import { registerTool, validateRequired, pickEnum } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
3
  import { OUTPUT_SCHEMA_IMAGE } from "../output-schemas.js";
4
4
  import { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime } from "../datetime.js";
5
+ // The seven systems the engine accepts (internal/math/houses.go).
6
+ // Do not re-add alcabitius/morinus — see the note in natal.ts.
5
7
  const HOUSE_SYSTEM_MAP = {
6
8
  placidus: "P", whole_sign: "W", equal: "E", koch: "K",
7
9
  campanus: "C", regiomontanus: "R", porphyry: "O",
8
- alcabitius: "B", morinus: "M",
9
10
  };
10
11
  registerTool({
11
12
  name: "ephemeris_chart_wheel",
@@ -32,7 +33,7 @@ registerTool({
32
33
  },
33
34
  house_system: {
34
35
  type: "string",
35
- enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry", "alcabitius", "morinus"],
36
+ enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry"],
36
37
  description: "House system to use. Defaults to 'placidus' if omitted.",
37
38
  },
38
39
  style: {
@@ -85,14 +85,27 @@ registerTool({
85
85
  },
86
86
  });
87
87
  // GET /electional/moment-analysis
88
+ //
89
+ // latitude/longitude are OPTIONAL here and required on ephemeris_electional
90
+ // above, deliberately. This tool is one of two ZERO_ARG_ENTRY_TOOLS
91
+ // (src/instructions.ts): the server instructions promise a host it "takes no
92
+ // arguments at all", and test/description-cross-references.test.ts asserts the
93
+ // schema keeps that promise. Requiring coordinates would turn the cold-open
94
+ // call — the first thing a model does for a user who has offered no data — into
95
+ // a validation error, which is the exact failure that list exists to prevent.
96
+ //
97
+ // The scoring risk is handled in the parameter descriptions instead: the Go
98
+ // handler falls back to a day chart without coordinates and says so in
99
+ // score_detail.sect_basis ("default_diurnal_no_coordinates"), so a caller that
100
+ // omits them gets a wrong-half-of-the-day score that is at least labelled.
88
101
  registerTool({
89
102
  name: "electional_moment_analysis",
90
103
  description: "Analyze the astrological quality of a specific moment: planet positions, aspects, " +
91
- "void of course status, lunar phase, day ruler, and an overall electional score (0-100). " +
104
+ "void of course status, lunar phase, day ruler, sect, and an overall electional score (0-100). " +
92
105
  "Perfect for evaluating whether 'right now' or a specific date/time is good for action.\n\n" +
93
106
  "CREDIT COST: 5 credits per call.\n\n" +
94
- "EXAMPLE: Analyze March 21, 2026 at noon:\n" +
95
- " date='2026-03-21T12:00:00Z'",
107
+ "EXAMPLE: Analyze March 21, 2026 at noon in New York:\n" +
108
+ " date='2026-03-21T12:00:00Z', latitude=40.7128, longitude=-74.0060",
96
109
  inputSchema: {
97
110
  type: "object",
98
111
  properties: {
@@ -100,6 +113,14 @@ registerTool({
100
113
  type: "string",
101
114
  description: "ISO 8601 datetime to analyze, with a zone (e.g., '2026-03-21T12:00:00Z' or '2026-03-21T08:00:00-04:00'). Defaults to now.",
102
115
  },
116
+ latitude: {
117
+ type: "number",
118
+ description: "Latitude of location in decimal degrees (positive = North). Resolve from a place name with location_search; never recall coordinates from memory. Optional, but pass both whenever the place is known — without them sect falls back to diurnal and a night moment is scored with the DAY triplicity rulers.",
119
+ },
120
+ longitude: {
121
+ type: "number",
122
+ description: "Longitude of location in decimal degrees (positive = East). Needed alongside latitude — day/night is a local fact that neither coordinate settles alone.",
123
+ },
103
124
  format: {
104
125
  type: "string",
105
126
  enum: ["json", "llm"],
@@ -115,6 +136,13 @@ registerTool({
115
136
  const query = {};
116
137
  if (args.date)
117
138
  query.date = args.date;
139
+ // `!== undefined`, not truthiness: latitude 0 (equator) and longitude 0
140
+ // (Greenwich) are real places, and a falsy check would drop them and
141
+ // silently score those elections as day charts.
142
+ if (args.latitude !== undefined)
143
+ query.latitude = args.latitude;
144
+ if (args.longitude !== undefined)
145
+ query.longitude = args.longitude;
118
146
  if (args.format)
119
147
  query.format = args.format;
120
148
  return await getActiveClient().request("GET", "/electional/moment-analysis", {
@@ -276,7 +276,9 @@ registerTool({
276
276
  registerTool({
277
277
  name: "ephemeris_angles_points",
278
278
  description: "Calculate chart angles and sensitive points (ASC, MC, DSC, IC, Vertex, " +
279
- "East Point, etc.) for a given date/time and location.\n\n" +
279
+ "Equatorial Ascendant/Descendant) for a given date/time and location. " +
280
+ "The equatorial ascendant is also returned under the key `east_point` — " +
281
+ "a synonym for the same point, not a separate one.\n\n" +
280
282
  "CREDIT COST: 1 credit per call.",
281
283
  inputSchema: {
282
284
  type: "object",
@@ -2,11 +2,25 @@ import { registerTool, validateRequired } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
3
  import { OUTPUT_SCHEMA_IMAGE_AND_JSON } from "../output-schemas.js";
4
4
  import { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime } from "../datetime.js";
5
- /** Map human-readable house system names to standard single-letter codes */
5
+ /**
6
+ * Map human-readable house system names to standard single-letter codes.
7
+ *
8
+ * These seven are the whole set. They are exactly what
9
+ * `CanonicalHouseSystemCode` accepts (`internal/math/houses.go`) and exactly
10
+ * what `GET /ephemeris/house-systems` returns.
11
+ *
12
+ * Do not add `alcabitius: "B"` or `morinus: "M"` back. They were advertised
13
+ * here through 4.4.0 and never worked: the codes "B" and "M" are accepted by
14
+ * no code path, so every call naming them came back
15
+ * `422 Validation error: house_system is invalid`. The engine does take the
16
+ * *slugs* "alcabitius"/"morinus", but only by silently aliasing them to Koch
17
+ * and Regiomontanus — so wiring the slug through would answer a request for
18
+ * one system with a different system's cusps. Neither is implemented; until
19
+ * one is, neither belongs in a schema.
20
+ */
6
21
  const HOUSE_SYSTEM_MAP = {
7
22
  placidus: "P", whole_sign: "W", equal: "E", koch: "K",
8
23
  campanus: "C", regiomontanus: "R", porphyry: "O",
9
- alcabitius: "B", morinus: "M",
10
24
  };
11
25
  registerTool({
12
26
  name: "ephemeris_natal_chart",
@@ -37,7 +51,7 @@ registerTool({
37
51
  },
38
52
  house_system: {
39
53
  type: "string",
40
- enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry", "alcabitius", "morinus"],
54
+ enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry"],
41
55
  description: "House system to use. Defaults to 'placidus' if omitted.",
42
56
  },
43
57
  format: {
@@ -49,9 +63,34 @@ registerTool({
49
63
  type: "boolean",
50
64
  description: "Include Hermetic Lots / Arabic Parts in the natal response (routed as `options.include_hermetic_lots: true`). For a standalone lots-only payload without a full natal chart, use `/ephemeris/hermetic-lots` instead.",
51
65
  },
52
- include_fixed_stars: {
66
+ zodiac_type: {
67
+ type: "string",
68
+ enum: ["tropical", "sidereal"],
69
+ description: "Zodiac frame. Default tropical. `sidereal` subtracts the ayanamsa (~24°) from every " +
70
+ "longitude, so signs, dignities and element balance all move; aspects and house numbers " +
71
+ "do not. The response names the frame it used.",
72
+ },
73
+ ayanamsa: {
74
+ type: "string",
75
+ enum: ["lahiri", "fagan_bradley", "krishnamurti", "raman", "yukteshwar"],
76
+ description: "Sidereal system. Default lahiri. Only read when zodiac_type is `sidereal`.",
77
+ },
78
+ analytics: {
79
+ type: "array",
80
+ items: {
81
+ type: "string",
82
+ enum: ["element_balance", "hemisphere_emphasis", "aspect_patterns", "harmonics"],
83
+ },
84
+ description: "Optional whole-chart analytics, off by default: element_balance (fire/earth/air/water plus " +
85
+ "modality counts over the classical ten, unweighted), hemisphere_emphasis (above/below and " +
86
+ "east/west from the angles), aspect_patterns (aspect matrix + detected patterns), harmonics. " +
87
+ "No extra credits. Absent from format='llm'.",
88
+ },
89
+ include_minor_aspects: {
53
90
  type: "boolean",
54
- description: "Include fixed-star positions in the natal response (routed as `configuration.fixed_star_options.include: true`). For a standalone fixed-stars payload without a full natal chart, use `/ephemeris/fixed-stars` instead.",
91
+ description: "Add quincunx, semisextile, semisquare, sesquiquadrate, quintile and biquintile to the aspect " +
92
+ "grid. Off by default; minors use their own orbs, so this widens the grid without changing " +
93
+ "any major aspect.",
55
94
  },
56
95
  include_visual: {
57
96
  type: "boolean",
@@ -98,10 +137,43 @@ registerTool({
98
137
  const code = HOUSE_SYSTEM_MAP[args.house_system] ?? args.house_system;
99
138
  body.configuration = { house_system: code };
100
139
  }
101
- if (args.include_fixed_stars) {
140
+ // Sidereal. The endpoint accepted zodiac_type on paper for as long as
141
+ // NatalConfig has existed and read it nowhere, so asking for a sidereal
142
+ // chart returned a tropical one with every body most of a whole sign
143
+ // out and nothing in the payload saying so. It is honoured now, which
144
+ // is the only reason this is exposed here.
145
+ if (args.zodiac_type === "sidereal") {
102
146
  body.configuration = {
103
147
  ...(body.configuration || {}),
104
- fixed_star_options: { include: true }
148
+ zodiac_type: "sidereal",
149
+ ...(args.ayanamsa ? { ayanamsa: args.ayanamsa } : {}),
150
+ };
151
+ }
152
+ else if (args.ayanamsa) {
153
+ // Naming a system and getting the tropical chart anyway is the same
154
+ // silence this parameter spent its whole life as. Say so instead.
155
+ throw new Error("ayanamsa only applies when zodiac_type is 'sidereal' — the tropical zodiac has no ayanamsa.");
156
+ }
157
+ // enhanced_options is where the Go engine keeps whole-chart analytics.
158
+ // The endpoint has always accepted these; nothing here ever sent them,
159
+ // so every one of these capabilities was unreachable from MCP.
160
+ if (Array.isArray(args.analytics) && args.analytics.length > 0) {
161
+ const want = new Set(args.analytics);
162
+ const enhanced = {};
163
+ if (want.has("element_balance"))
164
+ enhanced.element_balance = true;
165
+ if (want.has("hemisphere_emphasis"))
166
+ enhanced.hemisphere_emphasis = true;
167
+ if (want.has("aspect_patterns"))
168
+ enhanced.aspect_patterns = true;
169
+ if (want.has("harmonics"))
170
+ enhanced.include_harmonics = true;
171
+ body.enhanced_options = enhanced;
172
+ }
173
+ if (args.include_minor_aspects) {
174
+ body.options = {
175
+ ...body.options,
176
+ include_minor_aspects: true,
105
177
  };
106
178
  }
107
179
  if (args.include_arabic_parts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openephemeris/mcp-server",
3
- "version": "4.12.0",
3
+ "version": "4.13.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",
@@ -92,7 +92,7 @@
92
92
  "@types/adm-zip": "^0.5.8",
93
93
  "@types/express": "^5.0.3",
94
94
  "@types/node": "^25.0.1",
95
- "adm-zip": "^0.5.17",
95
+ "adm-zip": "^0.6.0",
96
96
  "cross-env": "^10.1.0",
97
97
  "tsx": "^4.21.0",
98
98
  "typescript": "^5.9.3",