@openephemeris/mcp-server 4.14.2 → 4.15.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,37 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [4.15.0] — 2026-09-03
11
+
12
+ ### Added
13
+ - **`electional_angle_crossings` gains `natal_datetime`.** Pass a zoned natal instant and the search
14
+ finds when the **secondary-progressed** positions of Sun, Moon, Mercury, Venus, Mars and the lunar
15
+ nodes reach an angle at the place — the same body set and day-for-a-year formula the hybrid CCG
16
+ transit map draws with — while slower bodies stay transits. Progressed crossings carry
17
+ `progressed: true` and `search_summary.progression` names the mode, the natal instant and the
18
+ bodies progressed. Omit the parameter for the plain transit search, which is unchanged.
19
+
20
+ ## [4.14.3] — 2026-09-02
21
+
22
+ ### Fixed
23
+ - **`ephemeris_transits` silently dropped requested `asc`/`mc`/`dsc`/`ic` natal points.** The tool
24
+ matched `natal_points` against `/ephemeris/natal-chart`'s angle field names
25
+ (`ascendant`/`midheaven`/`descendant`/`imum_coeli`), which never matched the short forms the
26
+ tool itself documents — so a search for transits to the Ascendant silently returned zero hits,
27
+ with `success: true` and no indication anything was dropped. Added alias resolution both ways,
28
+ plus an `unresolved_points` field so a genuine mismatch surfaces instead of hiding.
29
+ - **`ephemeris_progressed_chart` hardcoded `subject.name` to `"Progressed Subject"`** on both the
30
+ natal and progressed blocks of the response, so no consumer could tell them apart by name. Now
31
+ an optional `name` argument, defaulting to a neutral label.
32
+
33
+ ### Added
34
+ - `/ephemeris/progressed` responses now include `metadata.angle_method`
35
+ (`chart_for_progressed_moment` or `solar_arc_directed`), naming which algorithm produced the
36
+ progressed angles — previously undeclared and silently implied by
37
+ `progression_options.method`, which only ever described planetary progression.
38
+
39
+ ---
40
+
10
41
  ## [4.14.2] — 2026-09-01
11
42
 
12
43
  ### Fixed
@@ -1,7 +1,7 @@
1
1
  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
- import { WINDOW_DATE_DESC } from "../datetime.js";
4
+ import { DATETIME_DESC, WINDOW_DATE_DESC, localToUtcIso, timezoneProperty } from "../datetime.js";
5
5
  // All electional endpoints are GET endpoints with query params.
6
6
  // GET /electional/find-window — already existed, keeping it
7
7
  registerTool({
@@ -279,6 +279,10 @@ registerTool({
279
279
  "Crossing times use the same in-mundo geometry as the astrocartography line engine, so casting a " +
280
280
  "map at a returned moment puts that line through the query point. Circumpolar cases (a body that " +
281
281
  "never rises or never sets at that latitude) simply return no event.\n\n" +
282
+ "PROGRESSED (hybrid) lines: pass natal_datetime to search the SECONDARY-PROGRESSED positions of " +
283
+ "Sun, Moon, Mercury, Venus, Mars and the lunar nodes — the same body set and formula the CCG hybrid " +
284
+ "transit map (/acg/ccg mode=ccg_hybrid) draws with; every other body stays a transit. Progressed " +
285
+ "crossings carry progressed: true and search_summary.progression names what was progressed.\n\n" +
282
286
  "CREDIT COST: 5 credits per call.\n\n" +
283
287
  "EXAMPLE: When does Mercury cross the Descendant over Warsaw on 2026-07-30?\n" +
284
288
  " latitude=52.2297, longitude=21.0122, bodies='Mercury', angles='DC',\n" +
@@ -320,6 +324,14 @@ registerTool({
320
324
  type: "number",
321
325
  description: "Maximum number of crossing events to return (default 5000).",
322
326
  },
327
+ natal_datetime: {
328
+ type: "string",
329
+ description: "Optional natal (birth) instant. When given, Sun, Moon, Mercury, Venus, Mars and the lunar " +
330
+ "nodes are secondary-progressed from it (day-for-a-year) and the search finds when their " +
331
+ "PROGRESSED position reaches the angle — the line a hybrid CCG map paints. Slow bodies " +
332
+ "(Jupiter outward) are unaffected. Omit for the plain transit search. " + DATETIME_DESC,
333
+ },
334
+ natal_timezone: timezoneProperty("the natal birth location", "Europe/London"),
323
335
  format: {
324
336
  type: "string",
325
337
  enum: ["json", "llm"],
@@ -349,6 +361,11 @@ registerTool({
349
361
  query.refraction_correction = args.refraction_correction;
350
362
  if (args.max_results !== undefined)
351
363
  query.max_results = args.max_results;
364
+ // A naive birth time plus its zone is a legitimate call (the datetime
365
+ // contract); the API's GET takes one zoned instant, so resolve here.
366
+ if (args.natal_datetime) {
367
+ query.natal_datetime = localToUtcIso("natal_datetime", args.natal_datetime, args.natal_timezone, "natal_timezone");
368
+ }
352
369
  if (args.format)
353
370
  query.format = args.format;
354
371
  return await getActiveClient().request("GET", "/electional/angle-crossings", {
@@ -36,6 +36,10 @@ registerTool({
36
36
  enum: ["secondary", "solar_arc", "tertiary"],
37
37
  description: "Progression method. Defaults to 'secondary' (1 day = 1 year).",
38
38
  },
39
+ name: {
40
+ type: "string",
41
+ description: "Optional label for the subject, echoed back in both natal_subject and progressed_subject. Defaults to a generic label if omitted.",
42
+ },
39
43
  include_aspects: {
40
44
  type: "boolean",
41
45
  description: "Whether to include aspect grid in the response. Default false.",
@@ -67,7 +71,10 @@ registerTool({
67
71
  assertZonedDatetime("target_datetime", args.target_datetime);
68
72
  const body = {
69
73
  subject: {
70
- name: "Progressed Subject",
74
+ // Echoed verbatim into both natal_subject.name and progressed_subject.name
75
+ // by the API (same person, two instants) — never a role-specific label
76
+ // like "Progressed Subject", which would be wrong on the natal block.
77
+ name: args.name || "Subject",
71
78
  birth_datetime: { iso: args.birth_datetime },
72
79
  birth_location: {
73
80
  latitude: { decimal: args.birth_latitude },
@@ -96,6 +96,7 @@ registerTool({
96
96
  const wantedPoints = args.natal_points
97
97
  ? new Set(args.natal_points.map((p) => p.toLowerCase()))
98
98
  : CORE_BODIES_DEFAULT;
99
+ const resolvedPoints = new Set();
99
100
  for (const planet of planetArray) {
100
101
  const name = (planet.name || planet.id || "").toLowerCase();
101
102
  const lon = planet.longitude ?? planet.ecliptic_longitude ?? planet.lon;
@@ -105,26 +106,54 @@ registerTool({
105
106
  continue;
106
107
  targetDegrees.push(Math.round(lon * 100) / 100);
107
108
  natalPositionMap[name] = lon;
109
+ resolvedPoints.add(name);
108
110
  }
109
- // Extract angles (ASC, MC) only when explicitly requested
111
+ // Extract angles (ASC, MC, DSC, IC) only when explicitly requested.
112
+ // /ephemeris/natal-chart names these "ascendant", "midheaven", "descendant",
113
+ // "imum_coeli" — but the short forms below are what this tool documents and
114
+ // what natal_points is usually given as, so map both directions or a caller
115
+ // asking for "asc" silently gets nothing back (no error, no natal_positions
116
+ // entry) even though the point exists in the natal chart.
110
117
  if (args.natal_points) {
118
+ const ANGLE_ALIASES = {
119
+ ascendant: "asc",
120
+ asc: "ascendant",
121
+ midheaven: "mc",
122
+ mc: "midheaven",
123
+ descendant: "dsc",
124
+ dsc: "descendant",
125
+ desc: "descendant",
126
+ imum_coeli: "ic",
127
+ ic: "imum_coeli",
128
+ };
111
129
  const angles = natalResult?.angles || natalResult?.data?.angles;
112
130
  if (angles) {
113
131
  const angleEntries = Array.isArray(angles)
114
132
  ? angles
115
133
  : Object.entries(angles).map(([k, v]) => ({ name: k, ...(typeof v === "object" ? v : { longitude: v }) }));
116
134
  for (const angle of angleEntries) {
117
- const name = (angle.name || angle.id || "").toLowerCase();
135
+ const apiName = (angle.name || angle.id || "").toLowerCase();
118
136
  const lon = angle.longitude ?? angle.ecliptic_longitude ?? angle.lon;
119
137
  if (typeof lon !== "number" || !isFinite(lon))
120
138
  continue;
121
- if (!wantedPoints.has(name))
139
+ const matchedAs = wantedPoints.has(apiName)
140
+ ? apiName
141
+ : (ANGLE_ALIASES[apiName] && wantedPoints.has(ANGLE_ALIASES[apiName]))
142
+ ? ANGLE_ALIASES[apiName]
143
+ : null;
144
+ if (!matchedAs)
122
145
  continue;
123
146
  targetDegrees.push(Math.round(lon * 100) / 100);
124
- natalPositionMap[name] = lon;
147
+ natalPositionMap[matchedAs] = lon;
148
+ resolvedPoints.add(matchedAs);
125
149
  }
126
150
  }
127
151
  }
152
+ // Any requested point that never turned up in natal-chart planets or angles —
153
+ // surface it instead of silently returning fewer targets than asked for.
154
+ const unresolvedPoints = args.natal_points
155
+ ? args.natal_points.filter((p) => !resolvedPoints.has(p.toLowerCase()))
156
+ : [];
128
157
  // Hard cap: never send more than 12 target degrees
129
158
  const MAX_TARGETS = 12;
130
159
  if (targetDegrees.length > MAX_TARGETS) {
@@ -162,6 +191,7 @@ registerTool({
162
191
  // Return combined context so the LLM knows what natal positions were targeted
163
192
  return {
164
193
  natal_positions: natalPositionMap,
194
+ ...(unresolvedPoints.length > 0 ? { unresolved_points: unresolvedPoints } : {}),
165
195
  target_degrees_used: effectiveTargets,
166
196
  aspect_angle: args.aspect_angle ?? 0,
167
197
  transit_results: transitResult,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openephemeris/mcp-server",
3
- "version": "4.14.2",
3
+ "version": "4.15.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",