@openephemeris/mcp-server 4.3.1 → 4.4.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,45 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [4.4.0] — 2026-07-30
11
+
12
+ Puts the location resolver on the natural path for the four `explore_*`
13
+ chart tools, and corrects two parameter descriptions that had drifted from
14
+ the code.
15
+
16
+ ### Added
17
+
18
+ - **`location` on `explore_natal_chart`, `explore_human_design`,
19
+ `explore_vedic_chart`, and `explore_bi_wheel` now resolves internally.**
20
+ Supply a plain place name — `location: "Portland, ME"` — and the tool
21
+ fills in the coordinates and (unless overridden) the timezone via the
22
+ same `/location/autocomplete` lookup `location_search` uses. Supplied
23
+ `latitude`/`longitude` always win. Ambiguous names (e.g. bare
24
+ `"portland"`) throw with a disambiguation hint rather than silently
25
+ picking the top hit — the same behaviour `location_search` reports as
26
+ `ambiguous: true`.
27
+ A prior smoke run measured `location_search` adoption at **1 in 8** when
28
+ it was documented but off the natural path; every request supplied
29
+ coordinates from model memory instead. Wiring `location` into the chart
30
+ tools themselves makes the resolver the minimum-effort call.
31
+
32
+ ### Fixed
33
+
34
+ - **`explore_human_design` no longer silently computes at 0°N 0°E.**
35
+ When neither `latitude`/`longitude` nor a resolvable `location` is
36
+ supplied, the call is now rejected with a clear message. Previously a
37
+ bare `datetime` produced a plausible-looking bodygraph anchored on the
38
+ Gulf of Guinea — a location-sensitive system silently defaulting to a
39
+ wrong location is the failure class the Phase-0 audits keep catching.
40
+ - **`include_fixed_stars` and `include_arabic_parts` descriptions on
41
+ `ephemeris_natal_chart` no longer say "Reserved for future use".** Both
42
+ parameters have always been wired to their respective configuration
43
+ keys (`configuration.fixed_star_options.include: true` and
44
+ `options.include_hermetic_lots: true`); only the descriptions were
45
+ stale.
46
+
47
+ ---
48
+
10
49
  ## [4.3.1] — 2026-07-29
11
50
 
12
51
  Two rendering fixes surfaced by the Phase-0 v4.3 audit, plus a companion
@@ -0,0 +1,34 @@
1
+ export interface ResolvedLocation {
2
+ latitude: number;
3
+ longitude: number;
4
+ timezone: string | null;
5
+ displayName: string;
6
+ placeId: string | null;
7
+ }
8
+ /**
9
+ * Resolve a place name to lat/lon/tz. Throws if ambiguous or unresolvable.
10
+ *
11
+ * Ambiguity is detected the same way `location_search` reports it — a
12
+ * count of *rivals* for the top short_name, unless the query itself
13
+ * qualifies the place (e.g. "portland uk", "dallas texas").
14
+ */
15
+ export declare function resolveLocationOrThrow(location: string): Promise<ResolvedLocation>;
16
+ /**
17
+ * Convenience wrapper: fills in missing lat/lon (and optionally timezone)
18
+ * from `location` when the caller left them unset. Returns the effective
19
+ * coordinates + a display-safe location string. Coords passed in win —
20
+ * this only fires when both are unset. If only one of lat/lon is set,
21
+ * throws (partial input is a user error).
22
+ */
23
+ export declare function coordsFromArgsOrLocation(args: {
24
+ latitude?: unknown;
25
+ longitude?: unknown;
26
+ timezone?: unknown;
27
+ location?: unknown;
28
+ }): Promise<{
29
+ latitude: number | undefined;
30
+ longitude: number | undefined;
31
+ timezone: string | undefined;
32
+ location: string | undefined;
33
+ resolvedFromLocation: boolean;
34
+ }>;
@@ -0,0 +1,100 @@
1
+ // Internal location resolver used by `explore_*` tools when the caller
2
+ // supplies a place name (`location`) but omits `latitude`/`longitude`.
3
+ //
4
+ // The point is to put the resolver ON the model's natural path rather than
5
+ // hoping tool-description prose ("call location_search first") convinces
6
+ // the model to detour when it already has plausible coordinates in memory.
7
+ // A live smoke test measured that path at 1-in-8 adoption vs 8-in-8 for
8
+ // the same guidance when the request-shape *forced* the model's hand
9
+ // (naive datetime → 400).
10
+ //
11
+ // Ambiguity handling MUST mirror `location_search` — if the top hit has
12
+ // rival cities of the same short name and the query didn't already qualify
13
+ // it (e.g. "portland uk"), throw rather than silently pick suggestions[0].
14
+ // A quiet default to Portland, OR when the user meant Portland, ME
15
+ // produces a chart nothing downstream can detect as wrong. Same failure
16
+ // class as the naive-UTC ASC and the unlabelled cusp longitudes.
17
+ import { getActiveClient } from "../../backend/client.js";
18
+ /**
19
+ * Resolve a place name to lat/lon/tz. Throws if ambiguous or unresolvable.
20
+ *
21
+ * Ambiguity is detected the same way `location_search` reports it — a
22
+ * count of *rivals* for the top short_name, unless the query itself
23
+ * qualifies the place (e.g. "portland uk", "dallas texas").
24
+ */
25
+ export async function resolveLocationOrThrow(location) {
26
+ const query = location.trim();
27
+ if (query === "") {
28
+ throw new Error("location is empty");
29
+ }
30
+ const raw = (await getActiveClient().request("GET", "/location/autocomplete", {
31
+ params: { query },
32
+ }));
33
+ const list = Array.isArray(raw?.suggestions) ? raw.suggestions : [];
34
+ if (list.length === 0) {
35
+ throw new Error(`location "${query}" did not match any known place. Call location_search directly to inspect suggestions, ` +
36
+ `or supply latitude/longitude explicitly.`);
37
+ }
38
+ const norm = (v) => String(v ?? "").trim().toLowerCase();
39
+ const top = list[0];
40
+ const rivals = list.filter((s) => norm(s.short_name) === norm(top.short_name));
41
+ const q = norm(query);
42
+ const qTokens = new Set(q.split(/[^a-z0-9]+/).filter(Boolean));
43
+ const qualified = (norm(top.region) !== "" && q.includes(norm(top.region))) ||
44
+ (norm(top.country_code) !== "" && qTokens.has(norm(top.country_code)));
45
+ const ambiguous = rivals.length > 1 && !qualified;
46
+ if (ambiguous) {
47
+ const options = rivals
48
+ .slice(0, 4)
49
+ .map((s) => `${s.display_name ?? s.short_name} (${s.country_code ?? "??"})`)
50
+ .join("; ");
51
+ throw new Error(`location "${query}" is ambiguous — matches ${rivals.length} places (${options}${rivals.length > 4 ? ", …" : ""}). Call location_search directly and pass the disambiguated place, or supply latitude/longitude explicitly.`);
52
+ }
53
+ if (top.latitude == null || top.longitude == null) {
54
+ throw new Error(`location "${query}" matched "${top.display_name ?? top.short_name}" but the record is missing coordinates. ` +
55
+ `Supply latitude/longitude explicitly.`);
56
+ }
57
+ return {
58
+ latitude: Number(top.latitude),
59
+ longitude: Number(top.longitude),
60
+ timezone: top.timezone ?? null,
61
+ displayName: String(top.display_name ?? top.short_name ?? query),
62
+ placeId: top.place_id ?? null,
63
+ };
64
+ }
65
+ /**
66
+ * Convenience wrapper: fills in missing lat/lon (and optionally timezone)
67
+ * from `location` when the caller left them unset. Returns the effective
68
+ * coordinates + a display-safe location string. Coords passed in win —
69
+ * this only fires when both are unset. If only one of lat/lon is set,
70
+ * throws (partial input is a user error).
71
+ */
72
+ export async function coordsFromArgsOrLocation(args) {
73
+ const lat = args.latitude;
74
+ const lon = args.longitude;
75
+ const tz = args.timezone;
76
+ const loc = args.location;
77
+ const latSet = lat != null;
78
+ const lonSet = lon != null;
79
+ if (latSet && lonSet) {
80
+ return { latitude: lat, longitude: lon, timezone: tz, location: loc, resolvedFromLocation: false };
81
+ }
82
+ if (latSet !== lonSet) {
83
+ throw new Error("latitude and longitude must both be supplied (or both omitted with a `location` name).");
84
+ }
85
+ if (!loc || String(loc).trim() === "") {
86
+ // Neither coords nor location — leave undefined; the tool's own
87
+ // validation decides whether that's acceptable (some tools, like
88
+ // explore_human_design, historically defaulted to 0°N 0°E, which
89
+ // is a silent-wrong we want to end).
90
+ return { latitude: undefined, longitude: undefined, timezone: tz, location: loc, resolvedFromLocation: false };
91
+ }
92
+ const resolved = await resolveLocationOrThrow(String(loc));
93
+ return {
94
+ latitude: resolved.latitude,
95
+ longitude: resolved.longitude,
96
+ timezone: tz ?? resolved.timezone ?? undefined,
97
+ location: resolved.displayName,
98
+ resolvedFromLocation: true,
99
+ };
100
+ }
@@ -27,6 +27,7 @@ import { registerTool, SERVER_VERSION } from "../index.js";
27
27
  import { getActiveClient } from "../../backend/client.js";
28
28
  import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
29
29
  import { DATETIME_DESC, timezoneProperty } from "../datetime.js";
30
+ import { coordsFromArgsOrLocation } from "./_location-resolver.js";
30
31
  // ── Constants ─────────────────────────────────────────────────────────────────
31
32
  export const BI_WHEEL_RESOURCE_URI = "ui://openephemeris/bi-wheel";
32
33
  export const BI_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
@@ -430,8 +431,8 @@ registerTool({
430
431
  type: "string",
431
432
  description: "Birth datetime for Person 1 / Natal chart. " + DATETIME_DESC,
432
433
  },
433
- person1_latitude: { type: "number", description: "Birth latitude for Person 1 (decimal degrees, positive = North). Resolve from a place name with location_search; never recall coordinates from memory." },
434
- person1_longitude: { type: "number", description: "Birth longitude for Person 1 (decimal degrees, positive = East)." },
434
+ person1_latitude: { type: "number", description: "Birth latitude for Person 1 (decimal degrees, positive = North). Optional if `location` is a place name — the resolver fills it in. Never recall coordinates from memory." },
435
+ person1_longitude: { type: "number", description: "Birth longitude for Person 1 (decimal degrees, positive = East). Optional if `location` is a place name." },
435
436
  person1_timezone: timezoneProperty("Person 1's birth location", "America/New_York"),
436
437
  person1_name: {
437
438
  type: "string",
@@ -452,7 +453,7 @@ registerTool({
452
453
  },
453
454
  location: {
454
455
  type: "string",
455
- description: "Label for Person 1 / Natal location (display only, e.g. 'New York, NY'). This is a caption, NOT a geocoder — it does not set the chart location. Supply latitude/longitude too (resolve them with location_search), or the chart is computed at 0N 0E.",
456
+ description: "Person 1 / Natal location. Prefer a plain place name like 'New York, NY' — the server resolves it via the same lookup `location_search` uses (unambiguous names → coords + timezone; ambiguous names throw with a disambiguation hint). Supplied person1_latitude/person1_longitude always win.",
456
457
  },
457
458
  mode: {
458
459
  type: "string",
@@ -460,8 +461,7 @@ registerTool({
460
461
  description: "Chart comparison mode. Defaults to 'synastry'.",
461
462
  },
462
463
  },
463
- required: ["person1_datetime", "person2_datetime",
464
- "person1_latitude", "person1_longitude"],
464
+ required: ["person1_datetime", "person2_datetime"],
465
465
  },
466
466
  outputSchema: OUTPUT_SCHEMA_JSON,
467
467
  annotations: {
@@ -480,15 +480,24 @@ registerTool({
480
480
  handler: async (args) => {
481
481
  const client = getActiveClient();
482
482
  const mode = args.mode ?? "synastry";
483
+ const resolved1 = await coordsFromArgsOrLocation({
484
+ latitude: args.person1_latitude,
485
+ longitude: args.person1_longitude,
486
+ timezone: args.person1_timezone,
487
+ location: args.location,
488
+ });
489
+ if (resolved1.latitude == null || resolved1.longitude == null) {
490
+ throw new Error("explore_bi_wheel requires either `person1_latitude` + `person1_longitude` or a resolvable `location` name.");
491
+ }
483
492
  const dt1 = String(args.person1_datetime);
484
- const lat1 = args.person1_latitude;
485
- const lon1 = args.person1_longitude;
486
- const tz1 = args.person1_timezone;
493
+ const lat1 = resolved1.latitude;
494
+ const lon1 = resolved1.longitude;
495
+ const tz1 = args.person1_timezone ?? resolved1.timezone;
487
496
  const dt2 = String(args.person2_datetime);
488
497
  const lat2 = args.person2_latitude;
489
498
  const lon2 = args.person2_longitude;
490
499
  const tz2 = args.person2_timezone;
491
- const loc1 = String(args.person1_name ?? args.location ?? `${lat1 ?? "?"},${lon1 ?? "?"}`);
500
+ const loc1 = String(args.person1_name ?? resolved1.location ?? args.location ?? `${lat1},${lon1}`);
492
501
  const loc2 = MODE_META[mode].outerLabel(dt2, args.person2_name);
493
502
  // Fetch inner (natal) and outer (mode-dependent) charts in parallel where possible
494
503
  const [innerData, outerData] = await Promise.all([
@@ -26,6 +26,7 @@ import { registerTool, SERVER_VERSION } from "../index.js";
26
26
  import { getActiveClient } from "../../backend/client.js";
27
27
  import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
28
28
  import { localToUtcIso } from "../datetime.js";
29
+ import { coordsFromArgsOrLocation } from "./_location-resolver.js";
29
30
  // ── Constants ─────────────────────────────────────────────────────────────
30
31
  export const BODYGRAPH_RESOURCE_URI = "ui://openephemeris/bodygraph";
31
32
  // Appended to every model-visible HD tool description — trademark hygiene.
@@ -326,15 +327,15 @@ registerTool({
326
327
  },
327
328
  latitude: {
328
329
  type: "number",
329
- description: "Birth latitude in decimal degrees (positive = North). Optional for HD charts. Resolve from a place name with location_search; never recall coordinates from memory.",
330
+ description: "Birth latitude in decimal degrees (positive = North). Optional if `location` is a place name — the resolver fills it in. Never recall coordinates from memory; either supply a place name or resolve with location_search first.",
330
331
  },
331
332
  longitude: {
332
333
  type: "number",
333
- description: "Birth longitude in decimal degrees (positive = East). Optional for HD charts.",
334
+ description: "Birth longitude in decimal degrees (positive = East). Optional if `location` is a place name.",
334
335
  },
335
336
  location: {
336
337
  type: "string",
337
- description: "Location name for display only (e.g. 'New York, NY'). This is a caption, NOT a geocoder — it does not set the chart location. Supply latitude/longitude too (resolve them with location_search), or the chart is computed at 0N 0E.",
338
+ description: "Birth location. Prefer a plain place name like 'New York, NY' — the server resolves it via the same lookup `location_search` uses (unambiguous names → coords + timezone; ambiguous names throw with a disambiguation hint). If neither location nor lat/lon is supplied the call is rejected — no more silent 0°N 0°E charts.",
338
339
  },
339
340
  timezone: {
340
341
  type: "string",
@@ -361,18 +362,29 @@ registerTool({
361
362
  },
362
363
  handler: async (args) => {
363
364
  const client = getActiveClient();
364
- const timezone = args.timezone;
365
+ // Resolve `location` → lat/lon/tz when the caller left coords unset.
366
+ // If neither is provided we throw rather than silently computing at
367
+ // 0°N 0°E (a plausible-looking chart nothing downstream can detect).
368
+ const resolved = await coordsFromArgsOrLocation({
369
+ latitude: args.latitude,
370
+ longitude: args.longitude,
371
+ timezone: args.timezone,
372
+ location: args.location,
373
+ });
374
+ const timezone = args.timezone ?? resolved.timezone;
365
375
  const datetime = localToUtcIso("datetime", String(args.datetime), timezone);
366
- const location = String(args.location ?? (args.latitude != null ? `${args.latitude}, ${args.longitude}` : "Unknown")).slice(0, 120);
367
- const lat = args.latitude;
368
- const lon = args.longitude;
376
+ const lat = resolved.latitude;
377
+ const lon = resolved.longitude;
378
+ if (lat == null || lon == null) {
379
+ throw new Error("explore_human_design requires either `latitude` + `longitude` or a resolvable `location` name. " +
380
+ "Human Design is sensitive to birth location; a chart at 0°N 0°E is silently wrong.");
381
+ }
382
+ const location = String(resolved.location ?? args.location ?? `${lat}, ${lon}`).slice(0, 120);
369
383
  const body = {
370
384
  birth_datetime_utc: datetime,
371
385
  };
372
- if (lat != null)
373
- body.latitude = lat;
374
- if (lon != null)
375
- body.longitude = lon;
386
+ body.latitude = lat;
387
+ body.longitude = lon;
376
388
  const bundleAvailable = Boolean(getBodygraphBundle());
377
389
  // Fetch chart data and Go-rendered SVG in parallel. SVG only fetched when
378
390
  // the iframe bundle is available (text-fallback hosts have no use for it,
@@ -390,7 +402,7 @@ registerTool({
390
402
  // Build payload once — summary and model data both derive from the same object
391
403
  const modelPayload = buildHdModelPayload(chart, {
392
404
  datetime,
393
- location: args.location ?? null,
405
+ location: resolved.location ?? args.location ?? null,
394
406
  timezone: timezone ?? null,
395
407
  latitude: lat ?? null,
396
408
  longitude: lon ?? null,
@@ -22,6 +22,7 @@ import { registerTool, SERVER_VERSION } from "../index.js";
22
22
  import { getActiveClient } from "../../backend/client.js";
23
23
  import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
24
24
  import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
25
+ import { coordsFromArgsOrLocation } from "./_location-resolver.js";
25
26
  // ── Constants ─────────────────────────────────────────────────────────────
26
27
  export const CHART_WHEEL_RESOURCE_URI = "ui://openephemeris/chart-wheel";
27
28
  export const CHART_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
@@ -125,15 +126,15 @@ registerTool({
125
126
  timezone: TIMEZONE_PROPERTY,
126
127
  latitude: {
127
128
  type: "number",
128
- description: "Birth latitude in decimal degrees (positive = North). Resolve from a place name with location_search; never recall coordinates from memory.",
129
+ description: "Birth latitude in decimal degrees (positive = North). Optional if `location` is a place name — the resolver fills it in. If you supply lat/lon directly, resolve them with location_search first; never recall coordinates from memory.",
129
130
  },
130
131
  longitude: {
131
132
  type: "number",
132
- description: "Birth longitude in decimal degrees (positive = East).",
133
+ description: "Birth longitude in decimal degrees (positive = East). Optional if `location` is a place name.",
133
134
  },
134
135
  location: {
135
136
  type: "string",
136
- description: "Location name for display only (e.g. 'New York, NY'). This is a caption, NOT a geocoder — it does not set the chart location. Supply latitude/longitude too (resolve them with location_search), or the chart is computed at 0N 0E.",
137
+ description: "Birth location. Prefer a plain place name like 'New York, NY' — the server resolves it via the same lookup `location_search` uses (unambiguous names → coords + timezone; ambiguous names throw with a disambiguation hint). If you already have coords, either omit this or use it as a display caption; supplied coords always win.",
137
138
  },
138
139
  house_system: {
139
140
  type: "string",
@@ -149,7 +150,7 @@ registerTool({
149
150
  "Example: ['sun','moon','lilith','vertex'].",
150
151
  },
151
152
  },
152
- required: ["datetime", "latitude", "longitude"],
153
+ required: ["datetime"],
153
154
  },
154
155
  outputSchema: OUTPUT_SCHEMA_JSON,
155
156
  annotations: {
@@ -169,9 +170,23 @@ registerTool({
169
170
  const client = getActiveClient();
170
171
  const houseSystem = args.house_system ?? "placidus";
171
172
  const datetime = String(args.datetime);
172
- const lat = args.latitude;
173
- const lon = args.longitude;
174
- const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
173
+ const resolved = await coordsFromArgsOrLocation({
174
+ latitude: args.latitude,
175
+ longitude: args.longitude,
176
+ timezone: args.timezone,
177
+ location: args.location,
178
+ });
179
+ const lat = resolved.latitude;
180
+ const lon = resolved.longitude;
181
+ if (lat == null || lon == null) {
182
+ throw new Error("explore_natal_chart requires either `latitude` + `longitude` or a resolvable `location` name. " +
183
+ "The server tried to compute a chart with no coordinates and refused — a chart at 0°N 0°E is silently wrong.");
184
+ }
185
+ // A location resolved from the place name carries its own tz. The
186
+ // caller's explicit timezone still wins when supplied.
187
+ const effectiveTimezone = args.timezone ?? resolved.timezone;
188
+ const effectiveLocation = resolved.location ?? args.location;
189
+ const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, effectiveTimezone);
175
190
  // Determine which bodies are requested
176
191
  const requestedBodies = args.bodies;
177
192
  const wantsAll = requestedBodies?.some(b => b.toLowerCase() === "all");
@@ -188,14 +203,14 @@ registerTool({
188
203
  // source of the ~90 second blocking delay.
189
204
  const chartData = await client.post("/ephemeris/natal-chart", natalBody);
190
205
  // Build human-readable summary for the LLM context
191
- const summary = buildChartSummary(chartData, String(args.location ?? `${lat}, ${lon}`), houseSystem);
206
+ const summary = buildChartSummary(chartData, String(effectiveLocation ?? `${lat}, ${lon}`), houseSystem);
192
207
  // Build the UI payload (planets normalised to array, server aspects mapped)
193
208
  const modelPayload = buildModelPayload(chartData, {
194
209
  datetime,
195
- timezone: args.timezone ?? null,
210
+ timezone: effectiveTimezone ?? null,
196
211
  latitude: lat ?? null,
197
212
  longitude: lon ?? null,
198
- location: args.location ?? null,
213
+ location: effectiveLocation ?? null,
199
214
  }, houseSystem, undefined, requestedBodies);
200
215
  const bundleAvailable = Boolean(getChartWheelBundle());
201
216
  if (bundleAvailable) {
@@ -24,6 +24,7 @@ import { registerTool, validateRequired, validateCoordinates, SERVER_VERSION } f
24
24
  import { getActiveClient } from "../../backend/client.js";
25
25
  import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
26
26
  import { localToUtcIso } from "../datetime.js";
27
+ import { coordsFromArgsOrLocation } from "./_location-resolver.js";
27
28
  // ── Constants ─────────────────────────────────────────────────────────────────
28
29
  export const VEDIC_CHART_RESOURCE_URI = "ui://openephemeris/vedic-chart";
29
30
  export const VEDIC_CHART_MIME_TYPE = "text/html;profile=mcp-app";
@@ -112,15 +113,15 @@ registerTool({
112
113
  },
113
114
  latitude: {
114
115
  type: "number",
115
- description: "Birth latitude in decimal degrees (positive = North). Resolve from a place name with location_search; never recall coordinates from memory.",
116
+ description: "Birth latitude in decimal degrees (positive = North). Optional if `location` is a place name — the resolver fills it in. Never recall coordinates from memory; either supply a place name or resolve with location_search first.",
116
117
  },
117
118
  longitude: {
118
119
  type: "number",
119
- description: "Birth longitude in decimal degrees (positive = East).",
120
+ description: "Birth longitude in decimal degrees (positive = East). Optional if `location` is a place name.",
120
121
  },
121
122
  location: {
122
123
  type: "string",
123
- description: "Location name for display only (e.g. 'Mumbai, India'). This is a caption, NOT a geocoder — it does not set the chart location. Supply latitude/longitude too (resolve them with location_search), or the chart is computed at 0N 0E.",
124
+ description: "Birth location. Prefer a plain place name like 'Mumbai, India' — the server resolves it via the same lookup `location_search` uses (unambiguous names → coords + timezone; ambiguous names throw with a disambiguation hint). Supplied coords always win.",
124
125
  },
125
126
  timezone: {
126
127
  type: "string",
@@ -133,7 +134,7 @@ registerTool({
133
134
  description: "Ayanamsa system for sidereal conversion. Defaults to 'lahiri'.",
134
135
  },
135
136
  },
136
- required: ["datetime", "latitude", "longitude"],
137
+ required: ["datetime"],
137
138
  },
138
139
  outputSchema: OUTPUT_SCHEMA_JSON,
139
140
  annotations: {
@@ -150,15 +151,25 @@ registerTool({
150
151
  },
151
152
  },
152
153
  handler: async (args) => {
153
- validateRequired(args, ["datetime", "latitude", "longitude"]);
154
- validateCoordinates(args, "latitude", "longitude");
154
+ validateRequired(args, ["datetime"]);
155
+ const resolved = await coordsFromArgsOrLocation({
156
+ latitude: args.latitude,
157
+ longitude: args.longitude,
158
+ timezone: args.timezone,
159
+ location: args.location,
160
+ });
161
+ if (resolved.latitude == null || resolved.longitude == null) {
162
+ throw new Error("explore_vedic_chart requires either `latitude` + `longitude` or a resolvable `location` name.");
163
+ }
164
+ const argsWithResolved = { ...args, latitude: resolved.latitude, longitude: resolved.longitude };
165
+ validateCoordinates(argsWithResolved, "latitude", "longitude");
155
166
  const client = getActiveClient();
156
- const timezone = args.timezone;
167
+ const timezone = args.timezone ?? resolved.timezone;
157
168
  const datetime = localToUtcIso("datetime", String(args.datetime), timezone);
158
- const lat = Number(args.latitude);
159
- const lon = Number(args.longitude);
169
+ const lat = Number(resolved.latitude);
170
+ const lon = Number(resolved.longitude);
160
171
  const ayanamsa = args.ayanamsa;
161
- const location = String(args.location ?? `${lat}, ${lon}`).slice(0, 120);
172
+ const location = String(resolved.location ?? args.location ?? `${lat}, ${lon}`).slice(0, 120);
162
173
  const bundleAvailable = Boolean(getVedicChartBundle());
163
174
  const theme = "dark"; // app shell default; iframe reconciles to host theme on load
164
175
  const body = {
@@ -175,7 +186,7 @@ registerTool({
175
186
  const chartData = await client.request("POST", "/vedic/chart", { data: body });
176
187
  const modelPayload = buildVedicModelPayload(chartData, {
177
188
  datetime,
178
- location: args.location ?? null,
189
+ location: resolved.location ?? args.location ?? null,
179
190
  timezone: timezone ?? null,
180
191
  latitude: lat,
181
192
  longitude: lon,
@@ -47,11 +47,11 @@ registerTool({
47
47
  },
48
48
  include_arabic_parts: {
49
49
  type: "boolean",
50
- description: "Reserved for future use. Hermetic Lots / Arabic Parts are currently available via the dedicated /ephemeris/hermetic-lots endpoint.",
50
+ 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
51
  },
52
52
  include_fixed_stars: {
53
53
  type: "boolean",
54
- description: "Reserved for future use. Fixed star positions are currently available via the dedicated /ephemeris/fixed-stars endpoint.",
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.",
55
55
  },
56
56
  include_visual: {
57
57
  type: "boolean",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openephemeris/mcp-server",
3
- "version": "4.3.1",
3
+ "version": "4.4.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",