@openephemeris/mcp-server 4.6.0 → 4.7.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,44 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [4.7.0] — 2026-08-02
11
+
12
+ Historically correct birth-time conversion. IANA timezone databases — including
13
+ the one inside every JavaScript runtime — are only authoritative from 1970.
14
+ Before the US Uniform Time Act took effect in 1967, daylight saving was state
15
+ and municipal law, and a zone like `America/Chicago` models Chicago alone. A
16
+ 1961 Minnesota birth converted with standard timezone math lands an hour off,
17
+ which flips the Ascendant sign and the Human Design design-Moon gate.
18
+
19
+ ### Added
20
+
21
+ - **Pre-1970 births now resolve through the API's historical correction
22
+ overlay.** Any tool given a naive local birth time with a timezone and
23
+ coordinates — natal, Human Design (chart, composite, penta, cycles, groups),
24
+ Vedic, BaZi apps, ACG, moon tools, and the embedded chart apps — routes the
25
+ local→UTC conversion through the API's `/timezone/offset` `datetime_local`
26
+ mode, which applies primary-source-cited state and local law (with a
27
+ `tz_confidence` grade and rule citation) instead of assuming the reference
28
+ city's rules. Post-1970 conversions stay on the local, zero-latency path,
29
+ which is exactly as authoritative as the server. If the server is
30
+ unreachable, tools fall back to the previous local conversion rather than
31
+ failing the chart call.
32
+ - **`timezone_resolve`** consults the same historical path for pre-1970 dates
33
+ and reports `tzConfidence`, `tzRuleSource`, and `datetimeStatus` (ambiguous
34
+ DST-fold and nonexistent-gap times are called out explicitly).
35
+ - **`location_search`** tags each suggestion's historical offset with a
36
+ confidence grade and a note when a pre-1970 date may be affected by
37
+ state/local law divergence.
38
+
39
+ ### Fixed
40
+
41
+ - Three skills and one prompt still instructed the model to convert local
42
+ birth times to UTC itself with generic timezone math — the exact manual
43
+ conversion the historical overlay exists to prevent. They now direct all
44
+ conversion through the API.
45
+
46
+ ---
47
+
10
48
  ## [4.6.0] — 2026-07-30
11
49
 
12
50
  Fixes the tool that could not answer the question it is named for, closes the
package/dist/prompts.js CHANGED
@@ -377,8 +377,9 @@ export const PROMPTS = [
377
377
  "even 10–15 minutes can shift the Type, Authority, or Profile. If truly unknown, " +
378
378
  "proceed with solar noon but flag that results may be imprecise.\n" +
379
379
  "4. **Birth city and country** — needed for timezone conversion\n\n" +
380
- "## Step 2 — CRITICAL: Convert to UTC\n" +
381
- "Human Design requires **UTC datetime**. The tool rejects local times without a UTC offset.\n\n" +
380
+ "## Step 2 — Resolve Coordinates and Timezone\n" +
381
+ "Human Design needs the exact birth instant; the tool resolves it from local time + " +
382
+ "timezone + coordinates automatically (historically correct even pre-1970).\n\n" +
382
383
  "**Resolve the birth place with `location_search`** — pass `query: 'Birth City, Country'` " +
383
384
  "and `date` set to the birth date (`YYYY-MM-DD`). One call returns decimal lat/lon, the " +
384
385
  "IANA timezone, and `utcOffsetAtDate` — the offset that actually applied at that place on " +
@@ -25,7 +25,7 @@ import { fileURLToPath } from "node:url";
25
25
  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
- import { localToUtcIso } from "../datetime.js";
28
+ import { localToUtcIsoHistorical } from "../datetime-historical.js";
29
29
  import { coordsFromArgsOrLocation } from "./_location-resolver.js";
30
30
  // ── Constants ─────────────────────────────────────────────────────────────
31
31
  export const BODYGRAPH_RESOURCE_URI = "ui://openephemeris/bodygraph";
@@ -372,9 +372,9 @@ registerTool({
372
372
  location: args.location,
373
373
  });
374
374
  const timezone = args.timezone ?? resolved.timezone;
375
- const datetime = localToUtcIso("datetime", String(args.datetime), timezone);
376
375
  const lat = resolved.latitude;
377
376
  const lon = resolved.longitude;
377
+ const datetime = await localToUtcIsoHistorical("datetime", String(args.datetime), timezone, { latitude: lat, longitude: lon });
378
378
  if (lat == null || lon == null) {
379
379
  throw new Error("explore_human_design requires either `latitude` + `longitude` or a resolvable `location` name. " +
380
380
  "Human Design is sensitive to birth location; a chart at 0°N 0°E is silently wrong.");
@@ -473,10 +473,11 @@ registerTool({
473
473
  handler: async (args) => {
474
474
  const client = getActiveClient();
475
475
  const timezone = args.timezone;
476
- // Convert local-time input → UTC using IANA timezone when provided
477
- const datetime = localToUtcIso("datetime", String(args.datetime), timezone);
478
476
  const lat = args.latitude;
479
477
  const lon = args.longitude;
478
+ // Convert local-time input → UTC using IANA timezone when provided;
479
+ // pre-1970 births route through the API's historical correction overlay.
480
+ const datetime = await localToUtcIsoHistorical("datetime", String(args.datetime), timezone, { latitude: lat, longitude: lon });
480
481
  const theme = args.theme === "light" ? "light" : "dark";
481
482
  const layout = args.layout === "mandala" ? "mandala" : undefined;
482
483
  const rings = typeof args.rings === "string" ? args.rings : undefined;
@@ -1365,9 +1366,9 @@ registerTool({
1365
1366
  location: args.location,
1366
1367
  });
1367
1368
  const timezone = args.timezone ?? resolved.timezone;
1368
- const natalIso = localToUtcIso("datetime", String(args.datetime), timezone);
1369
1369
  const lat = resolved.latitude;
1370
1370
  const lon = resolved.longitude;
1371
+ const natalIso = await localToUtcIsoHistorical("datetime", String(args.datetime), timezone, { latitude: lat, longitude: lon });
1371
1372
  if (lat == null || lon == null) {
1372
1373
  throw new Error("explore_human_design_transit requires either `latitude` + `longitude` or a resolvable `location` name. " +
1373
1374
  "Human Design is sensitive to birth location; a chart at 0°N 0°E is silently wrong.");
@@ -1389,7 +1390,7 @@ registerTool({
1389
1390
  };
1390
1391
  if (args.transit_datetime) {
1391
1392
  body.transit_datetime = {
1392
- iso: localToUtcIso("transit_datetime", String(args.transit_datetime), args.timezone),
1393
+ iso: await localToUtcIsoHistorical("transit_datetime", String(args.transit_datetime), args.timezone, { latitude: lat, longitude: lon }),
1393
1394
  };
1394
1395
  }
1395
1396
  const bundleAvailable = Boolean(getBodygraphBundle());
@@ -1520,7 +1521,7 @@ registerTool({
1520
1521
  "Human Design is sensitive to birth location; a chart at 0°N 0°E is silently wrong.");
1521
1522
  }
1522
1523
  return {
1523
- birth_datetime_utc: localToUtcIso(`${label}.datetime`, String(p?.datetime), tz, `${label}.timezone`),
1524
+ birth_datetime_utc: await localToUtcIsoHistorical(`${label}.datetime`, String(p?.datetime), tz, { latitude: lat, longitude: lon }, `${label}.timezone`),
1524
1525
  latitude: lat,
1525
1526
  longitude: lon,
1526
1527
  };
@@ -1,6 +1,15 @@
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 { TZDATA_AUTHORITATIVE_FROM_YEAR } from "../datetime-historical.js";
5
+ /** Render minutes east of UTC as `±HH:MM`. */
6
+ function formatOffsetMinutes(offsetMinutes) {
7
+ const sign = offsetMinutes < 0 ? "-" : "+";
8
+ const abs = Math.abs(offsetMinutes);
9
+ const hh = String(Math.floor(abs / 60)).padStart(2, "0");
10
+ const mm = String(Math.round(abs % 60)).padStart(2, "0");
11
+ return `${sign}${hh}:${mm}`;
12
+ }
4
13
  /**
5
14
  * The UTC offset of an IANA zone is a function of place AND date: 1987 US DST
6
15
  * rules are not today's rules, and America/Chicago ran on CDT through the whole
@@ -143,6 +152,8 @@ registerTool({
143
152
  // Map the API's snake_case response to the camelCase shape the app UI consumes.
144
153
  const list = Array.isArray(raw?.suggestions) ? raw.suggestions : [];
145
154
  const date = args.date != null && String(args.date).trim() !== "" ? String(args.date).trim() : null;
155
+ const dateYear = date ? Number(date.slice(0, 4)) : null;
156
+ const preTzdataEra = dateYear != null && Number.isFinite(dateYear) && dateYear < TZDATA_AUTHORITATIVE_FROM_YEAR;
146
157
  const suggestions = list.map((s) => {
147
158
  const mapped = {
148
159
  displayName: s.display_name,
@@ -154,8 +165,17 @@ registerTool({
154
165
  longitude: s.longitude,
155
166
  timezone: s.timezone,
156
167
  };
157
- if (date && s.timezone)
158
- Object.assign(mapped, offsetAtLocalNoon(s.timezone, date) ?? {});
168
+ if (date && s.timezone) {
169
+ const offset = offsetAtLocalNoon(s.timezone, date);
170
+ if (offset) {
171
+ // These offsets come from local tzdata, which is only
172
+ // authoritative from 1970 — before that it models the
173
+ // zone's reference city, not state/local law.
174
+ Object.assign(mapped, offset, {
175
+ tzConfidence: preTzdataEra ? "historical_estimate" : "authoritative",
176
+ });
177
+ }
178
+ }
159
179
  return mapped;
160
180
  });
161
181
  // Ambiguity signal. "portland" really does match Oregon, Maine, Texas,
@@ -180,6 +200,11 @@ registerTool({
180
200
  return {
181
201
  suggestions,
182
202
  matchCount: suggestions.length,
203
+ ...(preTzdataEra
204
+ ? {
205
+ historicalNote: "Pre-1970 date: tzdata offsets are reference-city estimates. For the chosen place, call timezone_resolve with the same date (or submit the naive local time + timezone + coordinates to the chart endpoint) to apply the API's historical correction overlay.",
206
+ }
207
+ : {}),
183
208
  ambiguous,
184
209
  ...(ambiguous
185
210
  ? {
@@ -203,7 +228,7 @@ registerTool({
203
228
  longitude: { type: "number" },
204
229
  date: {
205
230
  type: "string",
206
- description: "Optional birth/event date as 'YYYY-MM-DD'. When given, the result also carries utcOffsetAtDate / utcOffsetMinutes / isDst for that date under that zone's historical DST rules. Free: adds no API call and no credits.",
231
+ description: "Optional birth/event date 'YYYY-MM-DD'. Adds utcOffsetAtDate / utcOffsetMinutes / isDst / tzConfidence. Post-1970 resolves locally (free); pre-1970 consults the API's historical correction overlay (1 extra credit).",
207
232
  },
208
233
  },
209
234
  required: ["latitude", "longitude"],
@@ -228,9 +253,46 @@ registerTool({
228
253
  const date = args.date != null && String(args.date).trim() !== "" ? String(args.date).trim() : null;
229
254
  const tz = typeof result?.timezone === "string" ? result.timezone : null;
230
255
  if (date && tz) {
256
+ // Pre-1970 dates: tzdata (Node's and Go's alike) models the zone's
257
+ // reference city only, so ask the API — its historical correction
258
+ // overlay knows where state law diverged (e.g. Minnesota 1959-66).
259
+ // Costs one extra credit, only for pre-1970 dates.
260
+ const year = Number(date.slice(0, 4));
261
+ if (Number.isFinite(year) && year < TZDATA_AUTHORITATIVE_FROM_YEAR) {
262
+ try {
263
+ const off = (await getActiveClient().request("POST", "/timezone/offset", {
264
+ data: { lat: args.latitude, lon: args.longitude, datetime_local: `${date.slice(0, 10)}T12:00:00` },
265
+ }));
266
+ if (typeof off?.offset_seconds === "number") {
267
+ const minutes = off.offset_seconds / 60;
268
+ return {
269
+ ...result,
270
+ utcOffsetAtDate: formatOffsetMinutes(minutes),
271
+ utcOffsetMinutes: minutes,
272
+ isDst: Boolean(off.is_dst),
273
+ date,
274
+ tzConfidence: typeof off.tz_confidence === "string" ? off.tz_confidence : "historical_estimate",
275
+ ...(typeof off.tz_rule_source === "string" ? { tzRuleSource: off.tz_rule_source } : {}),
276
+ ...(typeof off.datetime_status === "string" ? { datetimeStatus: off.datetime_status } : {}),
277
+ };
278
+ }
279
+ }
280
+ catch {
281
+ // Server path unavailable — fall through to the local
282
+ // tzdata estimate below, flagged as such.
283
+ }
284
+ }
231
285
  const offset = offsetAtLocalNoon(tz, date);
232
- if (offset)
233
- return { ...result, ...offset, date };
286
+ if (offset) {
287
+ return {
288
+ ...result,
289
+ ...offset,
290
+ date,
291
+ tzConfidence: Number.isFinite(year) && year < TZDATA_AUTHORITATIVE_FROM_YEAR
292
+ ? "historical_estimate"
293
+ : "authoritative",
294
+ };
295
+ }
234
296
  }
235
297
  return result;
236
298
  },
@@ -17,7 +17,8 @@ import { fileURLToPath } from "node:url";
17
17
  import { registerTool, validateCoordinates, SERVER_VERSION } from "../index.js";
18
18
  import { getActiveClient } from "../../backend/client.js";
19
19
  import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
20
- import { TIMEZONE_PROPERTY, localToUtcIso } from "../datetime.js";
20
+ import { TIMEZONE_PROPERTY } from "../datetime.js";
21
+ import { localToUtcIsoHistorical } from "../datetime-historical.js";
21
22
  // ── Constants ─────────────────────────────────────────────────────────────────
22
23
  export const MOON_PHASE_RESOURCE_URI = "ui://openephemeris/moon-phase";
23
24
  export const MOON_PHASE_MIME_TYPE = "text/html;profile=mcp-app";
@@ -98,7 +99,7 @@ async function computeMoonData(args) {
98
99
  const client = getActiveClient();
99
100
  const params = {};
100
101
  params.datetime = args.datetime
101
- ? localToUtcIso("datetime", String(args.datetime), args.timezone)
102
+ ? await localToUtcIsoHistorical("datetime", String(args.datetime), args.timezone, { latitude: args.latitude, longitude: args.longitude })
102
103
  : new Date().toISOString();
103
104
  if (args.latitude != null)
104
105
  params.latitude = args.latitude;
@@ -23,7 +23,7 @@ import { fileURLToPath } from "node:url";
23
23
  import { registerTool, validateRequired, validateCoordinates, SERVER_VERSION } from "../index.js";
24
24
  import { getActiveClient } from "../../backend/client.js";
25
25
  import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
26
- import { localToUtcIso } from "../datetime.js";
26
+ import { localToUtcIsoHistorical } from "../datetime-historical.js";
27
27
  import { coordsFromArgsOrLocation } from "./_location-resolver.js";
28
28
  // ── Constants ─────────────────────────────────────────────────────────────────
29
29
  export const VEDIC_CHART_RESOURCE_URI = "ui://openephemeris/vedic-chart";
@@ -165,9 +165,9 @@ registerTool({
165
165
  validateCoordinates(argsWithResolved, "latitude", "longitude");
166
166
  const client = getActiveClient();
167
167
  const timezone = args.timezone ?? resolved.timezone;
168
- const datetime = localToUtcIso("datetime", String(args.datetime), timezone);
169
168
  const lat = Number(resolved.latitude);
170
169
  const lon = Number(resolved.longitude);
170
+ const datetime = await localToUtcIsoHistorical("datetime", String(args.datetime), timezone, { latitude: lat, longitude: lon });
171
171
  const ayanamsa = args.ayanamsa;
172
172
  const location = String(resolved.location ?? args.location ?? `${lat}, ${lon}`).slice(0, 120);
173
173
  const bundleAvailable = Boolean(getVedicChartBundle());
@@ -244,9 +244,9 @@ registerTool({
244
244
  _meta: { ui: { resourceUri: VEDIC_CHART_RESOURCE_URI, visibility: ["app"] } },
245
245
  handler: async (args) => {
246
246
  const client = getActiveClient();
247
- const datetime = localToUtcIso("datetime", String(args.datetime), args.timezone);
248
247
  const lat = args.latitude != null ? Number(args.latitude) : undefined;
249
248
  const lon = args.longitude != null ? Number(args.longitude) : undefined;
249
+ const datetime = await localToUtcIsoHistorical("datetime", String(args.datetime), args.timezone, { latitude: lat, longitude: lon });
250
250
  const ayanamsa = args.ayanamsa;
251
251
  const theme = args.theme === "light" ? "light" : "dark";
252
252
  const body = {
@@ -0,0 +1,19 @@
1
+ /** Where IANA tzdata stops being reference-city best effort. */
2
+ export declare const TZDATA_AUTHORITATIVE_FROM_YEAR = 1970;
3
+ export interface BirthCoords {
4
+ latitude?: number | null;
5
+ longitude?: number | null;
6
+ }
7
+ /** True when a naive local datetime needs the server-side historical path. */
8
+ export declare function needsHistoricalResolution(dt: string, coords?: BirthCoords): boolean;
9
+ /**
10
+ * `localToUtcIso`, but historically correct when it matters.
11
+ *
12
+ * Same contract as `localToUtcIso` (naive value with no `tz` throws the
13
+ * ambiguous-datetime error; zone-suffixed and date-only values pass through).
14
+ * For a pre-1970 naive value with known coordinates, the conversion is done
15
+ * by the API's `/timezone/offset` `datetime_local` mode so the historical
16
+ * correction overlay applies. Everything else — including every post-1970
17
+ * conversion — stays on the local, zero-latency Intl path.
18
+ */
19
+ export declare function localToUtcIsoHistorical(field: string, dt: string, tz?: string, coords?: BirthCoords, timezoneField?: string): Promise<string>;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Historically-correct local → UTC conversion.
3
+ *
4
+ * IANA tzdata — Node's Intl and the Go engine's tzdata alike — is only
5
+ * authoritative from 1970. Before the US Uniform Time Act (effective 1967),
6
+ * DST was a state and municipal matter, and tzdata models each zone's
7
+ * reference city only. `localToUtcIso`'s Intl math is therefore silently an
8
+ * hour off for births like Robbinsdale, MN 1961-10-09 17:56 (Minnesota was on
9
+ * CST while America/Chicago says CDT), which flips the Ascendant sign and the
10
+ * HD design-Moon gate.
11
+ *
12
+ * The Go API owns the correction: `POST /timezone/offset` with
13
+ * `datetime_local` + coordinates resolves through its historical-correction
14
+ * overlay (apps/api/go-sidecar/internal/historicaltz) and returns the true
15
+ * instant plus a `tz_confidence` grade. This module routes the conversions
16
+ * that need it — pre-1970, naive, with known coordinates — through that
17
+ * endpoint, and leaves every other case on the local Intl path, which is
18
+ * exactly as authoritative as the server for 1970+.
19
+ *
20
+ * Fallback discipline: if the server call fails or resolves a different zone
21
+ * than the caller explicitly named, we fall back to `localToUtcIso` — the
22
+ * pre-existing behavior — rather than failing the chart call. Best effort
23
+ * beats an outage; the server remains the single authority whenever it is
24
+ * reachable.
25
+ */
26
+ import { getActiveClient } from "../backend/client.js";
27
+ import { hasZoneSuffix, isNaiveClockTime, localToUtcIso } from "./datetime.js";
28
+ /** Where IANA tzdata stops being reference-city best effort. */
29
+ export const TZDATA_AUTHORITATIVE_FROM_YEAR = 1970;
30
+ /** True when a naive local datetime needs the server-side historical path. */
31
+ export function needsHistoricalResolution(dt, coords) {
32
+ const value = (dt ?? "").trim();
33
+ if (!isNaiveClockTime(value) || hasZoneSuffix(value))
34
+ return false;
35
+ const year = Number(value.slice(0, 4));
36
+ return (Number.isFinite(year) &&
37
+ year < TZDATA_AUTHORITATIVE_FROM_YEAR &&
38
+ typeof coords?.latitude === "number" &&
39
+ typeof coords?.longitude === "number");
40
+ }
41
+ /**
42
+ * `localToUtcIso`, but historically correct when it matters.
43
+ *
44
+ * Same contract as `localToUtcIso` (naive value with no `tz` throws the
45
+ * ambiguous-datetime error; zone-suffixed and date-only values pass through).
46
+ * For a pre-1970 naive value with known coordinates, the conversion is done
47
+ * by the API's `/timezone/offset` `datetime_local` mode so the historical
48
+ * correction overlay applies. Everything else — including every post-1970
49
+ * conversion — stays on the local, zero-latency Intl path.
50
+ */
51
+ export async function localToUtcIsoHistorical(field, dt, tz, coords, timezoneField = "timezone") {
52
+ const value = (dt ?? "").trim();
53
+ if (!needsHistoricalResolution(value, coords) || !tz || tz.trim() === "") {
54
+ // Includes the naive-without-tz case, which must throw the same
55
+ // ambiguous-datetime error localToUtcIso throws.
56
+ return localToUtcIso(field, dt, tz, timezoneField);
57
+ }
58
+ try {
59
+ const res = (await getActiveClient().request("POST", "/timezone/offset", {
60
+ data: {
61
+ lat: coords.latitude,
62
+ lon: coords.longitude,
63
+ datetime_local: value,
64
+ },
65
+ }));
66
+ // Respect an explicit caller zone: the server resolves the zone from
67
+ // the coordinates, so only trust its instant when the zones agree.
68
+ if (res?.resolved_utc && (!res.timezone || res.timezone === tz.trim())) {
69
+ return res.resolved_utc;
70
+ }
71
+ }
72
+ catch {
73
+ // Server unreachable or rejected the request — fall through to the
74
+ // local conversion rather than failing the chart call.
75
+ }
76
+ return localToUtcIso(field, dt, tz, timezoneField);
77
+ }
@@ -1,7 +1,8 @@
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 { DATETIME_DESC, TIMEZONE_PROPERTY, localToUtcIso } from "../datetime.js";
4
+ import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
5
+ import { localToUtcIsoHistorical } from "../datetime-historical.js";
5
6
  const ACG_BODY_DESCRIPTION = "List of celestial bodies for line calculation. " +
6
7
  "E.g. ['Sun', 'Moon', 'Venus', 'Mars', 'Jupiter', 'Saturn']. " +
7
8
  "Aliases: 'NorthNode'/'Node'/'Rahu' → MeanNode, 'SouthNode'/'Ketu' → SouthNode. " +
@@ -67,7 +68,7 @@ registerTool({
67
68
  birthplace_lat: args.birth_latitude,
68
69
  birthplace_lon: args.birth_longitude,
69
70
  },
70
- epoch: localToUtcIso("birth_datetime", args.birth_datetime, args.timezone),
71
+ epoch: await localToUtcIsoHistorical("birth_datetime", args.birth_datetime, args.timezone, { latitude: args.birth_latitude, longitude: args.birth_longitude }),
71
72
  };
72
73
  if (args.bodies)
73
74
  body.bodies = args.bodies;
@@ -164,7 +165,7 @@ registerTool({
164
165
  birthplace_lat: args.birth_latitude,
165
166
  birthplace_lon: args.birth_longitude,
166
167
  },
167
- epoch: localToUtcIso("birth_datetime", args.birth_datetime, args.timezone),
168
+ epoch: await localToUtcIsoHistorical("birth_datetime", args.birth_datetime, args.timezone, { latitude: args.birth_latitude, longitude: args.birth_longitude }),
168
169
  query_lat: args.query_latitude,
169
170
  query_lon: args.query_longitude,
170
171
  };
@@ -1,7 +1,8 @@
1
1
  import { registerTool, validateRequired, pickEnum } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
3
  import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
4
- import { DATETIME_DESC, TIMEZONE_PROPERTY, localToUtcIso } from "../datetime.js";
4
+ import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
5
+ import { localToUtcIsoHistorical } from "../datetime-historical.js";
5
6
  registerTool({
6
7
  name: "human_design_bodygraph",
7
8
  description: "Generate a Human Design Bodygraph image (SVG) from a birth datetime (UTC). " +
@@ -12,7 +13,7 @@ registerTool({
12
13
  "For a user-facing interactive bodygraph explorer, use explore_human_design instead.\n\n" +
13
14
  "CREDIT COST: 2 credits per call.\n\n" +
14
15
  "EXAMPLE (local birth time + zone): born 15 April 1990 at 14:30 in Chicago:\n" +
15
- " datetime='1990-04-15T14:30:00', timezone='America/Chicago'\n" +
16
+ " datetime='1990-04-15T14:30:00', timezone='America/Chicago', latitude=41.8781, longitude=-87.6298\n" +
16
17
  "EXAMPLE (already in UTC):\n" +
17
18
  " datetime='1990-04-15T19:30:00Z'",
18
19
  inputSchema: {
@@ -24,6 +25,14 @@ registerTool({
24
25
  " Human Design is time-sensitive — accuracy to the minute matters.",
25
26
  },
26
27
  timezone: TIMEZONE_PROPERTY,
28
+ latitude: {
29
+ type: "number",
30
+ description: "Optional birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times (e.g. state-level DST deviations); has no effect on the bodygraph itself.",
31
+ },
32
+ longitude: {
33
+ type: "number",
34
+ description: "Optional birth longitude in decimal degrees. See latitude.",
35
+ },
27
36
  style: {
28
37
  type: "string",
29
38
  enum: ["light", "dark", "mono"],
@@ -44,7 +53,7 @@ registerTool({
44
53
  handler: async (args) => {
45
54
  validateRequired(args, ["datetime"]);
46
55
  const body = {
47
- birth_datetime_utc: localToUtcIso("datetime", args.datetime, args.timezone),
56
+ birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
48
57
  };
49
58
  const format = pickEnum(args.format, ["svg", "png"]) || "svg";
50
59
  const style = pickEnum(args.style, ["light", "dark", "mono"]);
@@ -1,7 +1,21 @@
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 { DATETIME_DESC, TIMEZONE_PROPERTY, localToUtcIso } from "../datetime.js";
4
+ import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
5
+ import { localToUtcIsoHistorical } from "../datetime-historical.js";
6
+ /** Optional birth coordinates, present only to feed the historical timezone
7
+ * correction for pre-1970 naive local birth times — not sent to the Go
8
+ * endpoint, which is non-relocational. */
9
+ const OPTIONAL_BIRTH_COORDS = {
10
+ latitude: {
11
+ type: "number",
12
+ description: "Optional birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times; has no other effect.",
13
+ },
14
+ longitude: {
15
+ type: "number",
16
+ description: "Optional birth longitude in decimal degrees. See latitude.",
17
+ },
18
+ };
5
19
  registerTool({
6
20
  name: "hd_planetary_return",
7
21
  description: "Calculate a Human Design planetary return chart — full HD chart " +
@@ -29,6 +43,7 @@ registerTool({
29
43
  description: DATETIME_DESC,
30
44
  },
31
45
  timezone: TIMEZONE_PROPERTY,
46
+ ...OPTIONAL_BIRTH_COORDS,
32
47
  return_year: {
33
48
  type: "integer",
34
49
  description: "Year to find the return near (e.g. 2020 for a Saturn return at ~age 29).",
@@ -49,7 +64,7 @@ registerTool({
49
64
  return await getActiveClient().request("POST", "/human-design/cycles/return", {
50
65
  data: {
51
66
  planet: args.planet,
52
- birth_datetime_utc: localToUtcIso("datetime", args.datetime, args.timezone),
67
+ birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
53
68
  return_year: args.return_year,
54
69
  format: args.format,
55
70
  include_chiron: args.include_chiron,
@@ -82,6 +97,7 @@ registerTool({
82
97
  description: DATETIME_DESC,
83
98
  },
84
99
  timezone: TIMEZONE_PROPERTY,
100
+ ...OPTIONAL_BIRTH_COORDS,
85
101
  target_year: {
86
102
  type: "integer",
87
103
  description: "Year to search for the opposition near.",
@@ -102,7 +118,7 @@ registerTool({
102
118
  return await getActiveClient().request("POST", "/human-design/cycles/opposition", {
103
119
  data: {
104
120
  planet: args.planet,
105
- birth_datetime_utc: localToUtcIso("datetime", args.datetime, args.timezone),
121
+ birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
106
122
  target_year: args.target_year,
107
123
  format: args.format,
108
124
  include_chiron: args.include_chiron,
@@ -1,7 +1,8 @@
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 { DATETIME_DESC, localToUtcIso, timezoneProperty } from "../datetime.js";
4
+ import { DATETIME_DESC, timezoneProperty } from "../datetime.js";
5
+ import { localToUtcIsoHistorical } from "../datetime-historical.js";
5
6
  // POST /human-design/composite — OE-027
6
7
  registerTool({
7
8
  name: "human_design_composite",
@@ -21,11 +22,27 @@ registerTool({
21
22
  description: DATETIME_DESC,
22
23
  },
23
24
  person_a_timezone: timezoneProperty("Person A's birth location"),
25
+ person_a_latitude: {
26
+ type: "number",
27
+ description: "Optional Person A birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times; has no other effect.",
28
+ },
29
+ person_a_longitude: {
30
+ type: "number",
31
+ description: "Optional Person A birth longitude in decimal degrees. See person_a_latitude.",
32
+ },
24
33
  person_b_datetime: {
25
34
  type: "string",
26
35
  description: DATETIME_DESC,
27
36
  },
28
37
  person_b_timezone: timezoneProperty("Person B's birth location", "America/Los_Angeles"),
38
+ person_b_latitude: {
39
+ type: "number",
40
+ description: "Optional Person B birth latitude in decimal degrees. See person_a_latitude.",
41
+ },
42
+ person_b_longitude: {
43
+ type: "number",
44
+ description: "Optional Person B birth longitude in decimal degrees.",
45
+ },
29
46
  format: {
30
47
  type: "string",
31
48
  enum: ["json", "llm"],
@@ -41,10 +58,10 @@ registerTool({
41
58
  validateRequired(args, ["person_a_datetime", "person_b_datetime"]);
42
59
  const body = {
43
60
  subject_1: {
44
- birth_datetime_utc: localToUtcIso("person_a_datetime", args.person_a_datetime, args.person_a_timezone, "person_a_timezone"),
61
+ birth_datetime_utc: await localToUtcIsoHistorical("person_a_datetime", args.person_a_datetime, args.person_a_timezone, { latitude: args.person_a_latitude, longitude: args.person_a_longitude }, "person_a_timezone"),
45
62
  },
46
63
  subject_2: {
47
- birth_datetime_utc: localToUtcIso("person_b_datetime", args.person_b_datetime, args.person_b_timezone, "person_b_timezone"),
64
+ birth_datetime_utc: await localToUtcIsoHistorical("person_b_datetime", args.person_b_datetime, args.person_b_timezone, { latitude: args.person_b_latitude, longitude: args.person_b_longitude }, "person_b_timezone"),
48
65
  },
49
66
  };
50
67
  if (args.format)
@@ -81,6 +98,14 @@ registerTool({
81
98
  name: { type: "string", description: "Member's name." },
82
99
  datetime: { type: "string", description: DATETIME_DESC },
83
100
  timezone: timezoneProperty("this member's birth location"),
101
+ latitude: {
102
+ type: "number",
103
+ description: "Optional birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times; has no other effect.",
104
+ },
105
+ longitude: {
106
+ type: "number",
107
+ description: "Optional birth longitude in decimal degrees. See latitude.",
108
+ },
84
109
  },
85
110
  required: ["id", "name", "datetime"],
86
111
  },
@@ -101,13 +126,13 @@ registerTool({
101
126
  annotations: { title: "HD Penta Group Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
102
127
  handler: async (args) => {
103
128
  validateRequired(args, ["group_name", "members"]);
104
- const members = args.members.map((m, i) => ({
129
+ const members = await Promise.all(args.members.map(async (m, i) => ({
105
130
  id: m.id,
106
131
  name: m.name,
107
132
  birth_data: {
108
- birth_datetime_utc: localToUtcIso(`members[${i}].datetime`, m.datetime, m.timezone, `members[${i}].timezone`),
133
+ birth_datetime_utc: await localToUtcIsoHistorical(`members[${i}].datetime`, m.datetime, m.timezone, { latitude: m.latitude, longitude: m.longitude }, `members[${i}].timezone`),
109
134
  },
110
- }));
135
+ })));
111
136
  const body = {
112
137
  group_name: args.group_name,
113
138
  members,
@@ -1,7 +1,8 @@
1
1
  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
- import { DATETIME_DESC, TIMEZONE_PROPERTY, localToUtcIso } from "../datetime.js";
4
+ import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
5
+ import { localToUtcIsoHistorical } from "../datetime-historical.js";
5
6
  registerTool({
6
7
  name: "human_design_chart",
7
8
  description: "Calculate a full Human Design I Ching hexagram chart from birth data. Returns the person's Type " +
@@ -65,7 +66,7 @@ registerTool({
65
66
  handler: async (args) => {
66
67
  validateRequired(args, ["datetime"]);
67
68
  const body = {
68
- birth_datetime_utc: localToUtcIso("datetime", args.datetime, args.timezone),
69
+ birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
69
70
  };
70
71
  if (args.latitude != null)
71
72
  body.latitude = args.latitude;
@@ -1,7 +1,8 @@
1
1
  import { registerTool, validateCoordinates } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
3
  import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
4
- import { TIMEZONE_PROPERTY, localToUtcIso } from "../datetime.js";
4
+ import { TIMEZONE_PROPERTY } from "../datetime.js";
5
+ import { localToUtcIsoHistorical } from "../datetime-historical.js";
5
6
  registerTool({
6
7
  name: "ephemeris_moon_phase",
7
8
  description: "Get the Moon's current phase angle, illumination, sign and void-of-course status AT a " +
@@ -49,7 +50,7 @@ registerTool({
49
50
  // Backend requires datetime even though the schema marks it optional.
50
51
  // Default to current UTC time when caller omits it.
51
52
  params.datetime = args.datetime
52
- ? localToUtcIso("datetime", args.datetime, args.timezone)
53
+ ? await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude })
53
54
  : new Date().toISOString();
54
55
  if (args.latitude != null)
55
56
  params.latitude = args.latitude;