@openephemeris/mcp-server 3.23.1 → 4.0.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/LICENSE +21 -21
  3. package/README.md +52 -3
  4. package/dist/analytics.js +37 -5
  5. package/dist/backend/client.d.ts +7 -0
  6. package/dist/backend/client.js +39 -38
  7. package/dist/index.js +64 -2
  8. package/dist/oauth/session-utils.d.ts +42 -18
  9. package/dist/oauth/session-utils.js +79 -0
  10. package/dist/server-sse.js +114 -14
  11. package/dist/tools/apps/bazi-app.js +12 -26
  12. package/dist/tools/apps/bi-wheel-app.js +2 -2
  13. package/dist/tools/apps/bodygraph-app.d.ts +5 -5
  14. package/dist/tools/apps/bodygraph-app.js +187 -225
  15. package/dist/tools/apps/chart-wheel-app.js +5 -3
  16. package/dist/tools/apps/location-tools.js +167 -18
  17. package/dist/tools/apps/moon-phase-app.js +10 -3
  18. package/dist/tools/apps/transit-timeline-app.js +6 -4
  19. package/dist/tools/apps/vedic-chart-app.js +15 -49
  20. package/dist/tools/datetime.d.ts +65 -0
  21. package/dist/tools/datetime.js +153 -0
  22. package/dist/tools/index.d.ts +45 -2
  23. package/dist/tools/index.js +78 -2
  24. package/dist/tools/specialized/account.d.ts +1 -0
  25. package/dist/tools/specialized/account.js +100 -0
  26. package/dist/tools/specialized/acg.js +16 -14
  27. package/dist/tools/specialized/bazi.d.ts +7 -1
  28. package/dist/tools/specialized/bazi.js +86 -19
  29. package/dist/tools/specialized/bi_wheel.js +5 -4
  30. package/dist/tools/specialized/chart_wheel.js +5 -8
  31. package/dist/tools/specialized/comparative.js +13 -5
  32. package/dist/tools/specialized/electional.js +7 -7
  33. package/dist/tools/specialized/ephemeris_core.js +13 -8
  34. package/dist/tools/specialized/ephemeris_extended.js +27 -17
  35. package/dist/tools/specialized/hd_bodygraph.js +8 -10
  36. package/dist/tools/specialized/hd_cycles.js +7 -14
  37. package/dist/tools/specialized/hd_group.js +20 -13
  38. package/dist/tools/specialized/human_design.js +11 -17
  39. package/dist/tools/specialized/moon.js +8 -2
  40. package/dist/tools/specialized/natal.js +7 -9
  41. package/dist/tools/specialized/progressed.js +12 -8
  42. package/dist/tools/specialized/relocation.js +9 -3
  43. package/dist/tools/specialized/returns.js +23 -11
  44. package/dist/tools/specialized/synastry.js +17 -6
  45. package/dist/tools/specialized/transits.js +9 -5
  46. package/dist/tools/specialized/vedic.js +5 -3
  47. package/dist/tools/specialized/venus_star_points.js +14 -9
  48. package/dist/ui/bazi.html +1063 -1049
  49. package/dist/ui/bi-wheel.html +4197 -4120
  50. package/dist/ui/bodygraph.html +3855 -3720
  51. package/dist/ui/chart-wheel.html +3779 -3706
  52. package/dist/ui/moon-phase.html +3228 -3145
  53. package/dist/ui/transit-timeline.html +199 -170
  54. package/dist/ui/vedic-chart.html +1116 -1098
  55. package/package.json +6 -3
  56. package/smithery.yaml +1 -1
@@ -1,16 +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
- /**
5
- * Ensure a datetime string has a timezone offset for Go's time.Time parsing.
6
- */
7
- function ensureTimezone(dt) {
8
- if (!dt)
9
- return dt;
10
- if (/[Zz]$/.test(dt) || /[+-]\d{2}:\d{2}$/.test(dt) || /[+-]\d{4}$/.test(dt))
11
- return dt;
12
- return dt + "Z";
13
- }
4
+ import { DATETIME_DESC, TIMEZONE_PROPERTY, localToUtcIso } from "../datetime.js";
14
5
  registerTool({
15
6
  name: "hd_planetary_return",
16
7
  description: "Calculate a Human Design planetary return chart — full HD chart " +
@@ -35,8 +26,9 @@ registerTool({
35
26
  },
36
27
  datetime: {
37
28
  type: "string",
38
- description: "ISO 8601 birth datetime in UTC, e.g. '1990-04-15T19:30:00Z'.",
29
+ description: DATETIME_DESC,
39
30
  },
31
+ timezone: TIMEZONE_PROPERTY,
40
32
  return_year: {
41
33
  type: "integer",
42
34
  description: "Year to find the return near (e.g. 2020 for a Saturn return at ~age 29).",
@@ -57,7 +49,7 @@ registerTool({
57
49
  return await getActiveClient().request("POST", "/human-design/cycles/return", {
58
50
  data: {
59
51
  planet: args.planet,
60
- birth_datetime_utc: ensureTimezone(args.datetime),
52
+ birth_datetime_utc: localToUtcIso("datetime", args.datetime, args.timezone),
61
53
  return_year: args.return_year,
62
54
  format: args.format,
63
55
  include_chiron: args.include_chiron,
@@ -87,8 +79,9 @@ registerTool({
87
79
  },
88
80
  datetime: {
89
81
  type: "string",
90
- description: "ISO 8601 birth datetime in UTC, e.g. '1983-07-15T12:00:00Z'.",
82
+ description: DATETIME_DESC,
91
83
  },
84
+ timezone: TIMEZONE_PROPERTY,
92
85
  target_year: {
93
86
  type: "integer",
94
87
  description: "Year to search for the opposition near.",
@@ -109,7 +102,7 @@ registerTool({
109
102
  return await getActiveClient().request("POST", "/human-design/cycles/opposition", {
110
103
  data: {
111
104
  planet: args.planet,
112
- birth_datetime_utc: ensureTimezone(args.datetime),
105
+ birth_datetime_utc: localToUtcIso("datetime", args.datetime, args.timezone),
113
106
  target_year: args.target_year,
114
107
  format: args.format,
115
108
  include_chiron: args.include_chiron,
@@ -1,25 +1,31 @@
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
5
  // POST /human-design/composite — OE-027
5
6
  registerTool({
6
7
  name: "human_design_composite",
7
8
  description: "Calculate a Human Design composite chart for two people. Merges both bodygraphs " +
8
9
  "to show shared channels, authority dynamics, and relationship type.\n\n" +
9
10
  "CREDIT COST: 4 credits per call.\n\n" +
10
- "EXAMPLE:\n" +
11
- " person_a_datetime='1990-04-15T14:30:00Z', person_b_datetime='1988-09-22T08:15:00Z'",
11
+ "EXAMPLE (local times + zones):\n" +
12
+ " person_a_datetime='1990-04-15T14:30:00', person_a_timezone='America/Chicago',\n" +
13
+ " person_b_datetime='1988-09-22T08:15:00', person_b_timezone='America/Los_Angeles'\n" +
14
+ "EXAMPLE (already in UTC):\n" +
15
+ " person_a_datetime='1990-04-15T19:30:00Z', person_b_datetime='1988-09-22T15:15:00Z'",
12
16
  inputSchema: {
13
17
  type: "object",
14
18
  properties: {
15
19
  person_a_datetime: {
16
20
  type: "string",
17
- description: "Person A birth datetime as ISO 8601 UTC (e.g. '1990-04-15T14:30:00Z').",
21
+ description: DATETIME_DESC,
18
22
  },
23
+ person_a_timezone: timezoneProperty("Person A's birth location"),
19
24
  person_b_datetime: {
20
25
  type: "string",
21
- description: "Person B birth datetime as ISO 8601 UTC.",
26
+ description: DATETIME_DESC,
22
27
  },
28
+ person_b_timezone: timezoneProperty("Person B's birth location", "America/Los_Angeles"),
23
29
  format: {
24
30
  type: "string",
25
31
  enum: ["json", "llm"],
@@ -35,10 +41,10 @@ registerTool({
35
41
  validateRequired(args, ["person_a_datetime", "person_b_datetime"]);
36
42
  const body = {
37
43
  subject_1: {
38
- birth_datetime_utc: args.person_a_datetime,
44
+ birth_datetime_utc: localToUtcIso("person_a_datetime", args.person_a_datetime, args.person_a_timezone, "person_a_timezone"),
39
45
  },
40
46
  subject_2: {
41
- birth_datetime_utc: args.person_b_datetime,
47
+ birth_datetime_utc: localToUtcIso("person_b_datetime", args.person_b_datetime, args.person_b_timezone, "person_b_timezone"),
42
48
  },
43
49
  };
44
50
  if (args.format)
@@ -52,12 +58,12 @@ registerTool({
52
58
  description: "Calculate a Human Design Penta (group) chart for 3-5 people. Shows functional " +
53
59
  "attributes, leadership dynamics, channels, redundancies, and a group stability score.\n\n" +
54
60
  "CREDIT COST: 6 credits per call.\n\n" +
55
- "EXAMPLE (3 people):\n" +
61
+ "EXAMPLE (3 people — each datetime states its zone):\n" +
56
62
  " group_name='Team Alpha',\n" +
57
63
  " members=[\n" +
58
- " {id: 'alice', name: 'Alice', datetime: '1990-04-15T14:30:00Z'},\n" +
59
- " {id: 'bob', name: 'Bob', datetime: '1988-09-22T08:15:00Z'},\n" +
60
- " {id: 'carol', name: 'Carol', datetime: '1995-01-10T11:00:00Z'}\n" +
64
+ " {id: 'alice', name: 'Alice', datetime: '1990-04-15T14:30:00', timezone: 'America/Chicago'},\n" +
65
+ " {id: 'bob', name: 'Bob', datetime: '1988-09-22T15:15:00Z'},\n" +
66
+ " {id: 'carol', name: 'Carol', datetime: '1995-01-10T11:00:00+00:00'}\n" +
61
67
  " ]",
62
68
  inputSchema: {
63
69
  type: "object",
@@ -73,7 +79,8 @@ registerTool({
73
79
  properties: {
74
80
  id: { type: "string", description: "Unique member ID." },
75
81
  name: { type: "string", description: "Member's name." },
76
- datetime: { type: "string", description: "Birth datetime as ISO 8601 UTC." },
82
+ datetime: { type: "string", description: DATETIME_DESC },
83
+ timezone: timezoneProperty("this member's birth location"),
77
84
  },
78
85
  required: ["id", "name", "datetime"],
79
86
  },
@@ -94,11 +101,11 @@ registerTool({
94
101
  annotations: { title: "HD Penta Group Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
95
102
  handler: async (args) => {
96
103
  validateRequired(args, ["group_name", "members"]);
97
- const members = args.members.map((m) => ({
104
+ const members = args.members.map((m, i) => ({
98
105
  id: m.id,
99
106
  name: m.name,
100
107
  birth_data: {
101
- birth_datetime_utc: m.datetime,
108
+ birth_datetime_utc: localToUtcIso(`members[${i}].datetime`, m.datetime, m.timezone, `members[${i}].timezone`),
102
109
  },
103
110
  }));
104
111
  const body = {
@@ -1,18 +1,7 @@
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
- /**
5
- * Ensure a datetime string has a timezone offset for Go's time.Time parsing.
6
- * Go's encoding/json only accepts RFC 3339 (must have Z or +HH:MM offset).
7
- */
8
- function ensureTimezone(dt) {
9
- if (!dt)
10
- return dt;
11
- // Already has timezone indicator
12
- if (/[Zz]$/.test(dt) || /[+-]\d{2}:\d{2}$/.test(dt) || /[+-]\d{4}$/.test(dt))
13
- return dt;
14
- return dt + "Z";
15
- }
4
+ import { DATETIME_DESC, TIMEZONE_PROPERTY, localToUtcIso } from "../datetime.js";
16
5
  registerTool({
17
6
  name: "human_design_chart",
18
7
  description: "Calculate a full Human Design I Ching hexagram chart from birth data. Returns the person's Type " +
@@ -23,19 +12,24 @@ registerTool({
23
12
  "CREDIT COST: 2 credits per call.\n\n" +
24
13
  "Human Design uses two calculation moments: the birth time (Personality) and ~88° of Sun motion " +
25
14
  "before birth (~3 months prior, the Design calculation). The API handles this automatically.\n\n" +
26
- "IMPORTANT: The datetime should be in UTC. If you have local birth time, convert to UTC first.\n\n" +
27
- "EXAMPLE: Get the Human Design chart for someone born April 15, 1990 at 7:30 PM UTC:\n" +
15
+ "The datetime must state its zone — pass an offset/'Z', or pass local time plus timezone. " +
16
+ "A zone-less datetime is rejected rather than assumed to be UTC: Human Design is minute-sensitive, " +
17
+ "and an hour of error changes the Profile and the Design Sun line.\n\n" +
18
+ "EXAMPLE (local birth time + zone): someone born 15 April 1990 at 14:30 in Chicago:\n" +
19
+ " datetime='1990-04-15T14:30:00', timezone='America/Chicago', latitude=41.8781, longitude=-87.6298\n" +
20
+ "EXAMPLE (already in UTC):\n" +
28
21
  " datetime='1990-04-15T19:30:00Z', latitude=41.8781, longitude=-87.6298",
29
22
  inputSchema: {
30
23
  type: "object",
31
24
  properties: {
32
25
  datetime: {
33
26
  type: "string",
34
- description: "ISO 8601 birth datetime in UTC, e.g. '1990-04-15T19:30:00Z'. Must include 'Z' or timezone offset.",
27
+ description: DATETIME_DESC,
35
28
  },
29
+ timezone: TIMEZONE_PROPERTY,
36
30
  latitude: {
37
31
  type: "number",
38
- description: "Latitude of birth location in decimal degrees (positive = North).",
32
+ description: "Latitude of birth location in decimal degrees (positive = North). Resolve from a place name with location_search; never recall coordinates from memory.",
39
33
  },
40
34
  longitude: {
41
35
  type: "number",
@@ -71,7 +65,7 @@ registerTool({
71
65
  handler: async (args) => {
72
66
  validateRequired(args, ["datetime"]);
73
67
  const body = {
74
- birth_datetime_utc: ensureTimezone(args.datetime),
68
+ birth_datetime_utc: localToUtcIso("datetime", args.datetime, args.timezone),
75
69
  };
76
70
  if (args.latitude != null)
77
71
  body.latitude = args.latitude;
@@ -1,6 +1,7 @@
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
5
  registerTool({
5
6
  name: "ephemeris_moon_phase",
6
7
  description: "Get the Moon's current phase angle, illumination, and void-of-course status AT a specific " +
@@ -21,8 +22,11 @@ registerTool({
21
22
  properties: {
22
23
  datetime: {
23
24
  type: "string",
24
- description: "ISO 8601 datetime to query. If omitted, returns the current live moon phase (UTC now).",
25
+ description: "ISO 8601 datetime to query, stating its zone ('2026-03-20T12:00:00Z' or " +
26
+ "'2026-03-20T08:00:00-04:00'), or a local time together with `timezone`. " +
27
+ "If omitted, returns the current live moon phase (UTC now).",
25
28
  },
29
+ timezone: TIMEZONE_PROPERTY,
26
30
  latitude: {
27
31
  type: "number",
28
32
  description: "Observer latitude (optional, used for local void-of-course calculations).",
@@ -42,7 +46,9 @@ registerTool({
42
46
  const params = {};
43
47
  // Backend requires datetime even though the schema marks it optional.
44
48
  // Default to current UTC time when caller omits it.
45
- params.datetime = args.datetime ?? new Date().toISOString();
49
+ params.datetime = args.datetime
50
+ ? localToUtcIso("datetime", args.datetime, args.timezone)
51
+ : new Date().toISOString();
46
52
  if (args.latitude != null)
47
53
  params.latitude = args.latitude;
48
54
  if (args.longitude != null)
@@ -1,8 +1,7 @@
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
- const DATETIME_DESC = "ISO 8601 datetime string, e.g. '1990-04-15T14:30:00' (local time at birth location). " +
5
- "Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.";
4
+ import { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime } from "../datetime.js";
6
5
  /** Map human-readable house system names to standard single-letter codes */
7
6
  const HOUSE_SYSTEM_MAP = {
8
7
  placidus: "P", whole_sign: "W", equal: "E", koch: "K",
@@ -17,8 +16,9 @@ registerTool({
17
16
  "essential dignities, retrograde status, house system data, and major aspect grid. Asteroids are automatically included. " +
18
17
  "Returns raw JSON. For a user-facing visual chart, use explore_natal_chart instead.\n\n" +
19
18
  "CREDIT COST: 1 credit per call.\n\n" +
20
- "EXAMPLE: Calculate the natal chart for someone born April 15, 1990 at 2:30 PM in Chicago:\n" +
21
- " datetime='1990-04-15T14:30:00', latitude=41.8781, longitude=-87.6298",
19
+ "EXAMPLE: born 15 April 1990 at 2:30 PM local time in Chicago — pass the local time and name the zone:\n" +
20
+ " datetime='1990-04-15T14:30:00', timezone='America/Chicago', latitude=41.8781, longitude=-87.6298\n" +
21
+ "Equivalently, put the zone on the datetime: datetime='1990-04-15T14:30:00-05:00' (or the UTC instant '1990-04-15T19:30:00Z').",
22
22
  inputSchema: {
23
23
  type: "object",
24
24
  properties: {
@@ -26,13 +26,10 @@ registerTool({
26
26
  type: "string",
27
27
  description: DATETIME_DESC,
28
28
  },
29
- timezone: {
30
- type: "string",
31
- description: "IANA timezone name, e.g. 'America/Denver'. Use this if passing local time without a UTC offset in the datetime string.",
32
- },
29
+ timezone: TIMEZONE_PROPERTY,
33
30
  latitude: {
34
31
  type: "number",
35
- description: "Geographic latitude of birth location in decimal degrees (positive = North).",
32
+ description: "Geographic latitude of birth location in decimal degrees (positive = North). Resolve from a place name with location_search; never recall coordinates from memory.",
36
33
  },
37
34
  longitude: {
38
35
  type: "number",
@@ -79,6 +76,7 @@ registerTool({
79
76
  annotations: { title: "Natal Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
80
77
  handler: async (args) => {
81
78
  validateRequired(args, ["datetime", "latitude", "longitude"]);
79
+ assertZonedDatetime("datetime", args.datetime, args.timezone);
82
80
  const body = {
83
81
  subject: {
84
82
  name: "MCP Request",
@@ -1,27 +1,27 @@
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, WINDOW_DATE_DESC, assertZonedDatetime } from "../datetime.js";
4
5
  registerTool({
5
6
  name: "ephemeris_progressed_chart",
6
7
  description: "Calculate a Secondary Progressed (or Solar Arc / Tertiary) chart. " +
7
8
  "Advances the natal chart symbolically — 1 day = 1 year (secondary), or using solar arc motion. " +
8
9
  "Returns progressed planet positions, house cusps, aspects, and retrograde status.\n\n" +
9
10
  "CREDIT COST: 1 credit per call.\n\n" +
10
- "EXAMPLE: Secondary progressions for someone born 1985-06-21, progressed to 2026-01-01:\n" +
11
- " birth_datetime='1985-06-21T14:00:00', birth_latitude=51.5, birth_longitude=-0.12,\n" +
12
- " target_datetime='2026-01-01', method='secondary'",
11
+ "EXAMPLE: Secondary progressions for someone born 1985-06-21 at 2:00 PM in London, progressed to 2026-01-01:\n" +
12
+ " birth_datetime='1985-06-21T14:00:00', timezone='Europe/London',\n" +
13
+ " birth_latitude=51.5, birth_longitude=-0.12, target_datetime='2026-01-01', method='secondary'",
13
14
  inputSchema: {
14
15
  type: "object",
15
16
  properties: {
16
17
  birth_datetime: {
17
18
  type: "string",
18
- description: "ISO 8601 natal birth date/time. MUST include a UTC offset (or 'Z'), " +
19
- "e.g. '1990-05-15T14:30:00-05:00'. A naive datetime (no offset) is " +
20
- "interpreted as UTC and will shift the progressed positions.",
19
+ description: DATETIME_DESC,
21
20
  },
21
+ timezone: TIMEZONE_PROPERTY,
22
22
  birth_latitude: {
23
23
  type: "number",
24
- description: "Birth latitude in decimal degrees.",
24
+ description: "Birth latitude in decimal degrees. Resolve from a place name with location_search; never recall coordinates from memory.",
25
25
  },
26
26
  birth_longitude: {
27
27
  type: "number",
@@ -29,7 +29,7 @@ registerTool({
29
29
  },
30
30
  target_datetime: {
31
31
  type: "string",
32
- description: "Target date to progress the chart to (ISO 8601).",
32
+ description: "Date to progress the chart to. " + WINDOW_DATE_DESC,
33
33
  },
34
34
  method: {
35
35
  type: "string",
@@ -63,6 +63,8 @@ registerTool({
63
63
  annotations: { title: "Progressed Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
64
64
  handler: async (args) => {
65
65
  validateRequired(args, ["birth_datetime", "birth_latitude", "birth_longitude", "target_datetime"]);
66
+ assertZonedDatetime("birth_datetime", args.birth_datetime, args.timezone);
67
+ assertZonedDatetime("target_datetime", args.target_datetime);
66
68
  const body = {
67
69
  subject: {
68
70
  name: "Progressed Subject",
@@ -70,6 +72,8 @@ registerTool({
70
72
  birth_location: {
71
73
  latitude: { decimal: args.birth_latitude },
72
74
  longitude: { decimal: args.birth_longitude },
75
+ // The server resolves the IANA zone, so historical DST stays correct.
76
+ ...(args.timezone ? { timezone: { iana_name: args.timezone } } : {}),
73
77
  },
74
78
  },
75
79
  target_datetime: { iso: args.target_datetime },
@@ -1,6 +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 { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime } from "../datetime.js";
4
5
  registerTool({
5
6
  name: "ephemeris_relocation",
6
7
  description: "Calculate a relocation chart — the same natal planetary positions re-cast for a different " +
@@ -8,18 +9,20 @@ registerTool({
8
9
  "and angles, without changing the planetary longitudes in the chart.\n\n" +
9
10
  "CREDIT COST: 1 credit per call.\n\n" +
10
11
  "EXAMPLE: How does moving from Chicago to London change someone's chart?\n" +
11
- " natal_datetime='1990-04-15T14:30:00', natal_latitude=41.8781, natal_longitude=-87.6298,\n" +
12
+ " natal_datetime='1990-04-15T14:30:00', timezone='America/Chicago',\n" +
13
+ " natal_latitude=41.8781, natal_longitude=-87.6298,\n" +
12
14
  " relocation_latitude=51.5074, relocation_longitude=-0.1278",
13
15
  inputSchema: {
14
16
  type: "object",
15
17
  properties: {
16
18
  natal_datetime: {
17
19
  type: "string",
18
- description: "ISO 8601 birth datetime (local time at birth location).",
20
+ description: DATETIME_DESC,
19
21
  },
22
+ timezone: TIMEZONE_PROPERTY,
20
23
  natal_latitude: {
21
24
  type: "number",
22
- description: "Latitude of birth location in decimal degrees.",
25
+ description: "Latitude of birth location in decimal degrees. Resolve from a place name with location_search; never recall coordinates from memory.",
23
26
  },
24
27
  natal_longitude: {
25
28
  type: "number",
@@ -51,6 +54,7 @@ registerTool({
51
54
  annotations: { title: "Relocation Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
52
55
  handler: async (args) => {
53
56
  validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude", "relocation_latitude", "relocation_longitude"]);
57
+ assertZonedDatetime("natal_datetime", args.natal_datetime, args.timezone);
54
58
  const body = {
55
59
  natal: {
56
60
  subject: {
@@ -59,6 +63,8 @@ registerTool({
59
63
  birth_location: {
60
64
  latitude: { decimal: args.natal_latitude },
61
65
  longitude: { decimal: args.natal_longitude },
66
+ // The server resolves the IANA zone, so historical DST stays correct.
67
+ ...(args.timezone ? { timezone: { iana_name: args.timezone } } : {}),
62
68
  },
63
69
  },
64
70
  },
@@ -1,6 +1,7 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
- import { OUTPUT_SCHEMA_IMAGE_AND_JSON } from "../output-schemas.js";
3
+ import { OUTPUT_SCHEMA_IMAGE_AND_JSON, OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
4
+ import { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime, localToUtcIso } from "../datetime.js";
4
5
  // POST /predictive/returns/solar
5
6
  registerTool({
6
7
  name: "ephemeris_solar_return",
@@ -18,8 +19,9 @@ registerTool({
18
19
  properties: {
19
20
  birth_datetime: {
20
21
  type: "string",
21
- description: "ISO 8601 birth date/time. Include offset or Z (e.g. '1985-06-21T14:00:00-05:00').",
22
+ description: DATETIME_DESC,
22
23
  },
24
+ timezone: TIMEZONE_PROPERTY,
23
25
  target_datetime: {
24
26
  type: "string",
25
27
  description: "Date/time near which to find the Solar Return (ISO 8601 with offset or Z). " +
@@ -64,11 +66,13 @@ registerTool({
64
66
  annotations: { title: "Solar Return", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
65
67
  handler: async (args) => {
66
68
  validateRequired(args, ["birth_datetime"]);
69
+ assertZonedDatetime("birth_datetime", args.birth_datetime, args.timezone);
70
+ assertZonedDatetime("target_datetime", args.target_datetime, args.timezone);
67
71
  // Default target to current year if not provided
68
72
  const targetDt = args.target_datetime ?? new Date().toISOString();
69
73
  const body = {
70
- birth_datetime: { iso: args.birth_datetime },
71
- target_datetime: { iso: targetDt },
74
+ birth_datetime: { iso: localToUtcIso("birth_datetime", args.birth_datetime, args.timezone) },
75
+ target_datetime: { iso: localToUtcIso("target_datetime", targetDt, args.timezone) },
72
76
  };
73
77
  if (args.birth_latitude != null && args.birth_longitude != null) {
74
78
  body.location = {
@@ -110,8 +114,9 @@ registerTool({
110
114
  properties: {
111
115
  birth_datetime: {
112
116
  type: "string",
113
- description: "ISO 8601 birth date/time. Include offset or Z.",
117
+ description: DATETIME_DESC,
114
118
  },
119
+ timezone: TIMEZONE_PROPERTY,
115
120
  target_datetime: {
116
121
  type: "string",
117
122
  description: "Date/time near which to find the Lunar Return (ISO 8601 with offset or Z). " +
@@ -129,14 +134,17 @@ registerTool({
129
134
  required: ["birth_datetime"],
130
135
  additionalProperties: false,
131
136
  },
132
- outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
137
+ // JSON only: this tool never sets include_visual, so it cannot return an image.
138
+ outputSchema: OUTPUT_SCHEMA_JSON,
133
139
  annotations: { title: "Lunar Return", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
134
140
  handler: async (args) => {
135
141
  validateRequired(args, ["birth_datetime"]);
142
+ assertZonedDatetime("birth_datetime", args.birth_datetime, args.timezone);
143
+ assertZonedDatetime("target_datetime", args.target_datetime, args.timezone);
136
144
  const targetDt = args.target_datetime ?? new Date().toISOString();
137
145
  const body = {
138
- birth_datetime: { iso: args.birth_datetime },
139
- target_datetime: { iso: targetDt },
146
+ birth_datetime: { iso: localToUtcIso("birth_datetime", args.birth_datetime, args.timezone) },
147
+ target_datetime: { iso: localToUtcIso("target_datetime", targetDt, args.timezone) },
140
148
  };
141
149
  if (args.birth_latitude != null && args.birth_longitude != null) {
142
150
  body.location = {
@@ -167,15 +175,16 @@ registerTool({
167
175
  },
168
176
  birth_datetime: {
169
177
  type: "string",
170
- description: "ISO 8601 birth date/time. MUST include offset or Z.",
178
+ description: DATETIME_DESC,
171
179
  },
180
+ timezone: TIMEZONE_PROPERTY,
172
181
  target_datetime: {
173
182
  type: "string",
174
183
  description: "Date/time near which to find the return (ISO 8601). MUST include offset or Z. Required.",
175
184
  },
176
185
  birth_latitude: {
177
186
  type: "number",
178
- description: "Birth latitude in decimal degrees. Required to get a full return chart (house cusps, angles).",
187
+ description: "Birth latitude in decimal degrees. Required to get a full return chart (house cusps, angles). Resolve from a place name with location_search; never recall coordinates from memory.",
179
188
  },
180
189
  birth_longitude: {
181
190
  type: "number",
@@ -185,10 +194,13 @@ registerTool({
185
194
  required: ["body", "birth_datetime", "target_datetime"],
186
195
  additionalProperties: false,
187
196
  },
188
- outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
197
+ // JSON only: this tool never sets include_visual, so it cannot return an image.
198
+ outputSchema: OUTPUT_SCHEMA_JSON,
189
199
  annotations: { title: "Planetary Return", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
190
200
  handler: async (args) => {
191
201
  validateRequired(args, ["body", "birth_datetime", "target_datetime"]);
202
+ assertZonedDatetime("birth_datetime", args.birth_datetime, args.timezone);
203
+ assertZonedDatetime("target_datetime", args.target_datetime, args.timezone);
192
204
  const requestBody = {
193
205
  body: args.body,
194
206
  birth_datetime: { iso: args.birth_datetime },
@@ -1,22 +1,27 @@
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, assertZonedDatetime, timezoneProperty } from "../datetime.js";
4
5
  registerTool({
5
6
  name: "ephemeris_synastry",
6
7
  description: "Calculate a synastry chart comparing two people's natal charts. Returns inter-aspects " +
7
8
  "(planetary connections between the two charts), composite points, and relationship indicators. " +
8
9
  "Use this for compatibility analysis, relationship timing, or partnership insights.\n\n" +
9
10
  "CREDIT COST: 3 credits per call.\n\n" +
10
- "EXAMPLE: Compare two people's charts:\n" +
11
- " person_a_datetime='1990-04-15T14:30:00', person_a_latitude=41.8781, person_a_longitude=-87.6298,\n" +
12
- " person_b_datetime='1988-09-22T08:15:00', person_b_latitude=34.0522, person_b_longitude=-118.2437",
11
+ "EXAMPLE: Compare two people's charts (local birth times, each with its zone):\n" +
12
+ " person_a_datetime='1990-04-15T14:30:00', person_a_timezone='America/Chicago',\n" +
13
+ " person_a_latitude=41.8781, person_a_longitude=-87.6298,\n" +
14
+ " person_b_datetime='1988-09-22T08:15:00', person_b_timezone='America/Los_Angeles',\n" +
15
+ " person_b_latitude=34.0522, person_b_longitude=-118.2437",
13
16
  inputSchema: {
14
17
  type: "object",
15
18
  properties: {
16
- person_a_datetime: { type: "string", description: "Person A birth datetime (ISO 8601)." },
17
- person_a_latitude: { type: "number", description: "Person A birth latitude." },
19
+ person_a_datetime: { type: "string", description: DATETIME_DESC },
20
+ person_a_timezone: timezoneProperty("Person A's birth location"),
21
+ person_a_latitude: { type: "number", description: "Person A birth latitude. Resolve from a place name with location_search; never recall coordinates from memory." },
18
22
  person_a_longitude: { type: "number", description: "Person A birth longitude." },
19
- person_b_datetime: { type: "string", description: "Person B birth datetime (ISO 8601)." },
23
+ person_b_datetime: { type: "string", description: DATETIME_DESC },
24
+ person_b_timezone: timezoneProperty("Person B's birth location", "America/Los_Angeles"),
20
25
  person_b_latitude: { type: "number", description: "Person B birth latitude." },
21
26
  person_b_longitude: { type: "number", description: "Person B birth longitude." },
22
27
  house_system: {
@@ -58,6 +63,8 @@ registerTool({
58
63
  "person_a_datetime", "person_a_latitude", "person_a_longitude",
59
64
  "person_b_datetime", "person_b_latitude", "person_b_longitude",
60
65
  ]);
66
+ assertZonedDatetime("person_a_datetime", args.person_a_datetime, args.person_a_timezone, "person_a_timezone");
67
+ assertZonedDatetime("person_b_datetime", args.person_b_datetime, args.person_b_timezone, "person_b_timezone");
61
68
  const body = {
62
69
  subject_a: {
63
70
  name: "Person A",
@@ -65,6 +72,9 @@ registerTool({
65
72
  birth_location: {
66
73
  latitude: { decimal: args.person_a_latitude },
67
74
  longitude: { decimal: args.person_a_longitude },
75
+ // The server resolves the zone; sending the IANA name keeps
76
+ // historical DST correct instead of resolving it here.
77
+ ...(args.person_a_timezone ? { timezone: { iana_name: args.person_a_timezone } } : {}),
68
78
  },
69
79
  },
70
80
  subject_b: {
@@ -73,6 +83,7 @@ registerTool({
73
83
  birth_location: {
74
84
  latitude: { decimal: args.person_b_latitude },
75
85
  longitude: { decimal: args.person_b_longitude },
86
+ ...(args.person_b_timezone ? { timezone: { iana_name: args.person_b_timezone } } : {}),
76
87
  },
77
88
  },
78
89
  };
@@ -1,6 +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 { DATETIME_DESC, WINDOW_DATE_DESC, timezoneProperty } from "../datetime.js";
4
5
  registerTool({
5
6
  name: "ephemeris_transits",
6
7
  description: "Search for astrological transit events affecting a natal chart over a date range. " +
@@ -22,11 +23,12 @@ registerTool({
22
23
  properties: {
23
24
  natal_datetime: {
24
25
  type: "string",
25
- description: "ISO 8601 birth datetime for the natal chart.",
26
+ description: DATETIME_DESC,
26
27
  },
28
+ natal_timezone: timezoneProperty("the natal birth location"),
27
29
  natal_latitude: {
28
30
  type: "number",
29
- description: "Latitude of birth location in decimal degrees.",
31
+ description: "Latitude of birth location in decimal degrees. Resolve from a place name with location_search; never recall coordinates from memory.",
30
32
  },
31
33
  natal_longitude: {
32
34
  type: "number",
@@ -34,11 +36,11 @@ registerTool({
34
36
  },
35
37
  start_date: {
36
38
  type: "string",
37
- description: "Start of the transit search window, ISO 8601 date or datetime (e.g. '2026-01-01').",
39
+ description: "Start of the transit search window. " + WINDOW_DATE_DESC,
38
40
  },
39
41
  end_date: {
40
42
  type: "string",
41
- description: "End of the transit search window, ISO 8601 date or datetime (e.g. '2026-06-30').",
43
+ description: "End of the transit search window. " + WINDOW_DATE_DESC,
42
44
  },
43
45
  transiting_planets: {
44
46
  type: "array",
@@ -72,7 +74,9 @@ registerTool({
72
74
  birth_datetime: { iso: args.natal_datetime },
73
75
  birth_location: {
74
76
  latitude: { decimal: args.natal_latitude },
75
- longitude: { decimal: args.natal_longitude }
77
+ longitude: { decimal: args.natal_longitude },
78
+ // The server resolves the IANA zone, so historical DST stays correct.
79
+ ...(args.natal_timezone ? { timezone: { iana_name: args.natal_timezone } } : {}),
76
80
  },
77
81
  },
78
82
  };
@@ -1,6 +1,7 @@
1
1
  import { registerTool, validateRequired, validateCoordinates } 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
5
  registerTool({
5
6
  name: "vedic_chart",
6
7
  description: "Calculate a Vedic (Jyotish) natal chart with sidereal positions. Returns planet placements " +
@@ -15,11 +16,12 @@ registerTool({
15
16
  properties: {
16
17
  datetime: {
17
18
  type: "string",
18
- description: "ISO 8601 birth datetime in UTC, e.g. '1990-01-15T08:30:00Z'. Include 'Z' or timezone offset.",
19
+ description: DATETIME_DESC,
19
20
  },
21
+ timezone: TIMEZONE_PROPERTY,
20
22
  latitude: {
21
23
  type: "number",
22
- description: "Birth latitude in decimal degrees (north positive).",
24
+ description: "Birth latitude in decimal degrees (north positive). Resolve from a place name with location_search; never recall coordinates from memory.",
23
25
  },
24
26
  longitude: {
25
27
  type: "number",
@@ -40,7 +42,7 @@ registerTool({
40
42
  validateRequired(args, ["datetime", "latitude", "longitude"]);
41
43
  validateCoordinates(args, "latitude", "longitude");
42
44
  const body = {
43
- datetime_utc: args.datetime,
45
+ datetime_utc: localToUtcIso("datetime", args.datetime, args.timezone),
44
46
  latitude: args.latitude,
45
47
  longitude: args.longitude,
46
48
  };