@openephemeris/mcp-server 3.13.8 → 3.13.9

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.
@@ -65,6 +65,54 @@ function ensureTimezone(dt) {
65
65
  return dt;
66
66
  return dt + "Z";
67
67
  }
68
+ /**
69
+ * Convert a local datetime string (no offset) to a UTC ISO 8601 string.
70
+ * Uses the IANA timezone to compute the correct UTC offset.
71
+ * Falls back to appending 'Z' (treating input as UTC) when no timezone is given
72
+ * or when the Intl API cannot resolve the zone.
73
+ *
74
+ * @param dt - "YYYY-MM-DDTHH:MM:SS" (no offset)
75
+ * @param tz - IANA timezone name, e.g. "America/New_York"
76
+ */
77
+ function localToUtcIso(dt, tz) {
78
+ // Already has an offset / Z — pass through as-is
79
+ if (!dt || /[Zz]$/.test(dt) || /[+-]\d{2}:\d{2}$/.test(dt))
80
+ return ensureTimezone(dt);
81
+ if (!tz)
82
+ return dt + "Z"; // treat as UTC
83
+ try {
84
+ // Parse the local date components
85
+ const [datePart, timePart = "00:00:00"] = dt.split("T");
86
+ const [year, month, day] = datePart.split("-").map(Number);
87
+ const [hour, min, sec = 0] = timePart.split(":").map(Number);
88
+ // Build a Date assuming the inputs are LOCAL in the given timezone.
89
+ // Strategy: create a UTC candidate and shift by the Intl-reported offset.
90
+ // We iterate twice because DST can cause the offset to change at boundary datetimes.
91
+ const candidateUtcMs = Date.UTC(year, month - 1, day, hour, min, sec);
92
+ const getOffsetMs = (utcMs) => {
93
+ const fmtParts = new Intl.DateTimeFormat("en-US", {
94
+ timeZone: tz,
95
+ year: "numeric", month: "2-digit", day: "2-digit",
96
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
97
+ hour12: false,
98
+ }).formatToParts(new Date(utcMs));
99
+ const get = (t) => Number(fmtParts.find(p => p.type === t)?.value ?? 0);
100
+ const localizedUtcMs = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
101
+ return utcMs - localizedUtcMs; // positive = timezone behind UTC
102
+ };
103
+ // First pass: rough offset
104
+ const offsetMs1 = getOffsetMs(candidateUtcMs);
105
+ const correctedUtcMs1 = candidateUtcMs + offsetMs1;
106
+ // Second pass: refine around DST boundary
107
+ const offsetMs2 = getOffsetMs(correctedUtcMs1);
108
+ const finalUtcMs = candidateUtcMs + offsetMs2;
109
+ return new Date(finalUtcMs).toISOString();
110
+ }
111
+ catch {
112
+ // Intl failed (bad timezone string etc.) — fall back to naive UTC
113
+ return dt + "Z";
114
+ }
115
+ }
68
116
  const CENTER_ORDER = [
69
117
  "Head", "Ajna", "Throat", "G", "Sacral",
70
118
  "Heart", "Spleen", "Solar Plexus", "Root",
@@ -202,7 +250,13 @@ function buildHdModelPayload(data, birthParams) {
202
250
  design_gates,
203
251
  personality_activations,
204
252
  design_activations,
205
- _birth_params: birthParams,
253
+ _birth_params: {
254
+ datetime: birthParams.datetime,
255
+ location: birthParams.location,
256
+ timezone: birthParams.timezone ?? null,
257
+ latitude: birthParams.latitude ?? null,
258
+ longitude: birthParams.longitude ?? null,
259
+ },
206
260
  };
207
261
  }
208
262
  /**
@@ -275,15 +329,18 @@ registerTool({
275
329
  },
276
330
  handler: async (args) => {
277
331
  const client = getActiveClient();
278
- const datetime = ensureTimezone(String(args.datetime));
332
+ const timezone = args.timezone;
333
+ const datetime = localToUtcIso(String(args.datetime), timezone);
279
334
  const location = String(args.location ?? (args.latitude != null ? `${args.latitude}, ${args.longitude}` : "Unknown")).slice(0, 120);
335
+ const lat = args.latitude;
336
+ const lon = args.longitude;
280
337
  const body = {
281
338
  birth_datetime_utc: datetime,
282
339
  };
283
- if (args.latitude != null)
284
- body.latitude = args.latitude;
285
- if (args.longitude != null)
286
- body.longitude = args.longitude;
340
+ if (lat != null)
341
+ body.latitude = lat;
342
+ if (lon != null)
343
+ body.longitude = lon;
287
344
  const chartData = await client.request("POST", "/human-design/chart", {
288
345
  data: body,
289
346
  });
@@ -293,6 +350,9 @@ registerTool({
293
350
  const modelPayload = buildHdModelPayload(chart, {
294
351
  datetime,
295
352
  location: args.location ?? null,
353
+ timezone: timezone ?? null,
354
+ latitude: lat ?? null,
355
+ longitude: lon ?? null,
296
356
  });
297
357
  const summary = buildHdSummary(modelPayload, location);
298
358
  const bundleAvailable = Boolean(getBodygraphBundle());
@@ -334,14 +394,18 @@ registerTool({
334
394
  _meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
335
395
  handler: async (args) => {
336
396
  const client = getActiveClient();
337
- const datetime = ensureTimezone(String(args.datetime));
397
+ const timezone = args.timezone;
398
+ // Convert local-time input → UTC using IANA timezone when provided
399
+ const datetime = localToUtcIso(String(args.datetime), timezone);
400
+ const lat = args.latitude;
401
+ const lon = args.longitude;
338
402
  const body = {
339
403
  birth_datetime_utc: datetime,
340
404
  };
341
- if (args.latitude != null)
342
- body.latitude = args.latitude;
343
- if (args.longitude != null)
344
- body.longitude = args.longitude;
405
+ if (lat != null)
406
+ body.latitude = lat;
407
+ if (lon != null)
408
+ body.longitude = lon;
345
409
  const chartData = await client.request("POST", "/human-design/chart", {
346
410
  data: body,
347
411
  });
@@ -349,6 +413,9 @@ registerTool({
349
413
  const modelPayload = buildHdModelPayload(chart, {
350
414
  datetime,
351
415
  location: args.location ?? null,
416
+ timezone: timezone ?? null,
417
+ latitude: lat ?? null,
418
+ longitude: lon ?? null,
352
419
  });
353
420
  return {
354
421
  content: [{ type: "text", text: JSON.stringify({ ...modelPayload, server_version: SERVER_VERSION }) }],