@openephemeris/mcp-server 3.2.11 → 3.2.13

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 (78) hide show
  1. package/dist/backend/client.d.ts +23 -1
  2. package/dist/backend/client.js +144 -25
  3. package/dist/index.js +94 -2
  4. package/dist/tools/dev.js +29 -21
  5. package/dist/tools/index.d.ts +8 -0
  6. package/dist/tools/index.js +30 -1
  7. package/dist/tools/specialized/eclipse.js +20 -26
  8. package/dist/tools/specialized/electional.js +4 -3
  9. package/dist/tools/specialized/human_design.js +27 -11
  10. package/dist/tools/specialized/moon.js +3 -2
  11. package/dist/tools/specialized/natal.js +36 -15
  12. package/dist/tools/specialized/relocation.js +14 -10
  13. package/dist/tools/specialized/returns.js +126 -0
  14. package/dist/tools/specialized/synastry.js +22 -12
  15. package/dist/tools/specialized/transits.js +78 -28
  16. package/package.json +3 -2
  17. package/dist/scripts/dev-allowlist.js +0 -287
  18. package/dist/scripts/pack-audit.d.ts +0 -1
  19. package/dist/scripts/pack-audit.js +0 -45
  20. package/dist/scripts/schema-packs.d.ts +0 -1
  21. package/dist/scripts/schema-packs.js +0 -150
  22. package/dist/scripts/smoke-dev-profile.d.ts +0 -1
  23. package/dist/scripts/smoke-dev-profile.js +0 -25
  24. package/dist/scripts/sync-readme.d.ts +0 -1
  25. package/dist/scripts/sync-readme.js +0 -141
  26. package/dist/scripts/test-client.d.ts +0 -1
  27. package/dist/scripts/test-client.js +0 -69
  28. package/dist/scripts/test-sse-client.d.ts +0 -1
  29. package/dist/scripts/test-sse-client.js +0 -221
  30. package/dist/src/backend/client.d.ts +0 -67
  31. package/dist/src/backend/client.js +0 -363
  32. package/dist/src/index.d.ts +0 -2
  33. package/dist/src/index.js +0 -184
  34. package/dist/src/schema-packs/llm.d.ts +0 -105
  35. package/dist/src/schema-packs/llm.js +0 -429
  36. package/dist/src/tools/dev.d.ts +0 -1
  37. package/dist/src/tools/dev.js +0 -195
  38. package/dist/src/tools/index.d.ts +0 -33
  39. package/dist/src/tools/index.js +0 -60
  40. package/dist/src/tools/specialized/eclipse.d.ts +0 -1
  41. package/dist/src/tools/specialized/eclipse.js +0 -50
  42. package/dist/src/tools/specialized/electional.d.ts +0 -1
  43. package/dist/src/tools/specialized/electional.js +0 -80
  44. package/dist/src/tools/specialized/human_design.d.ts +0 -1
  45. package/dist/src/tools/specialized/human_design.js +0 -69
  46. package/dist/src/tools/specialized/moon.d.ts +0 -1
  47. package/dist/src/tools/specialized/moon.js +0 -51
  48. package/dist/src/tools/specialized/natal.d.ts +0 -1
  49. package/dist/src/tools/specialized/natal.js +0 -92
  50. package/dist/src/tools/specialized/relocation.d.ts +0 -1
  51. package/dist/src/tools/specialized/relocation.js +0 -76
  52. package/dist/src/tools/specialized/synastry.d.ts +0 -1
  53. package/dist/src/tools/specialized/synastry.js +0 -73
  54. package/dist/src/tools/specialized/transits.d.ts +0 -1
  55. package/dist/src/tools/specialized/transits.js +0 -130
  56. package/dist/test/allowlist-and-tools.test.d.ts +0 -1
  57. package/dist/test/allowlist-and-tools.test.js +0 -96
  58. package/dist/test/backend-client.test.d.ts +0 -1
  59. package/dist/test/backend-client.test.js +0 -284
  60. package/dist/test/credentials.test.d.ts +0 -1
  61. package/dist/test/credentials.test.js +0 -143
  62. /package/dist/{src/auth → auth}/credentials.d.ts +0 -0
  63. /package/dist/{src/auth → auth}/credentials.js +0 -0
  64. /package/dist/{src/auth → auth}/device-auth.d.ts +0 -0
  65. /package/dist/{src/auth → auth}/device-auth.js +0 -0
  66. /package/dist/{src/server-sse.d.ts → server-sse.d.ts} +0 -0
  67. /package/dist/{src/server-sse.js → server-sse.js} +0 -0
  68. /package/dist/{src/tools → tools}/auth.d.ts +0 -0
  69. /package/dist/{src/tools → tools}/auth.js +0 -0
  70. /package/dist/{src/tools → tools}/specialized/bazi.d.ts +0 -0
  71. /package/dist/{src/tools → tools}/specialized/bazi.js +0 -0
  72. /package/dist/{src/tools → tools}/specialized/bi_wheel.d.ts +0 -0
  73. /package/dist/{src/tools → tools}/specialized/bi_wheel.js +0 -0
  74. /package/dist/{src/tools → tools}/specialized/chart_wheel.d.ts +0 -0
  75. /package/dist/{src/tools → tools}/specialized/chart_wheel.js +0 -0
  76. /package/dist/{scripts/dev-allowlist.d.ts → tools/specialized/returns.d.ts} +0 -0
  77. /package/dist/{src/tools → tools}/specialized/vedic.d.ts +0 -0
  78. /package/dist/{src/tools → tools}/specialized/vedic.js +0 -0
@@ -1,56 +1,50 @@
1
- import { registerTool } from "../index.js";
1
+ import { registerTool, validateRequired, validateCoordinates } from "../index.js";
2
2
  import { backendClient } from "../../backend/client.js";
3
3
  registerTool({
4
- name: "ephemeris.next_eclipse",
5
- description: "Find the next solar or lunar eclipse visible from a given location (or globally). " +
4
+ name: "ephemeris_next_eclipse",
5
+ description: "Find the next solar or lunar eclipse visible from a given location. " +
6
6
  "Returns the eclipse type, date/time of maximum, magnitude, duration of totality (if any), " +
7
- "and local contact times if coordinates are provided.\n\n" +
7
+ "and local contact times.\n\n" +
8
8
  "CREDIT COST: 1 credit per call.\n\n" +
9
9
  "EXAMPLE: Find the next solar eclipse visible from New York:\n" +
10
10
  " eclipse_type='solar', latitude=40.7128, longitude=-74.006\n\n" +
11
- "EXAMPLE: Find the next lunar eclipse globally:\n" +
12
- " eclipse_type='lunar'",
11
+ "EXAMPLE: Find the next lunar eclipse from London:\n" +
12
+ " eclipse_type='lunar', latitude=51.5074, longitude=-0.1278",
13
13
  inputSchema: {
14
14
  type: "object",
15
15
  properties: {
16
16
  eclipse_type: {
17
17
  type: "string",
18
- enum: ["solar", "lunar"],
19
- description: "Eclipse type to search for.",
18
+ enum: ["solar", "lunar", "any"],
19
+ description: "Eclipse type to search for. Use 'any' for whichever comes first.",
20
20
  },
21
21
  latitude: {
22
22
  type: "number",
23
- description: "Observer latitude in decimal degrees. If provided, returns local visibility and contact times.",
23
+ description: "Observer latitude in decimal degrees (required). Returns local visibility and contact times.",
24
24
  },
25
25
  longitude: {
26
26
  type: "number",
27
- description: "Observer longitude in decimal degrees.",
27
+ description: "Observer longitude in decimal degrees (required).",
28
28
  },
29
29
  after_date: {
30
30
  type: "string",
31
31
  description: "ISO 8601 date to search after (e.g. '2026-01-01'). Defaults to today if omitted.",
32
32
  },
33
33
  },
34
- required: ["eclipse_type"],
34
+ required: ["eclipse_type", "latitude", "longitude"],
35
35
  additionalProperties: false,
36
36
  },
37
37
  handler: async (args) => {
38
- const params = {};
39
- if (args.latitude != null)
40
- params.latitude = args.latitude;
41
- if (args.longitude != null)
42
- params.longitude = args.longitude;
38
+ validateRequired(args, ["eclipse_type", "latitude", "longitude"]);
39
+ validateCoordinates(args, "latitude", "longitude");
40
+ const params = {
41
+ lat: args.latitude,
42
+ lon: args.longitude,
43
+ };
43
44
  if (args.after_date)
44
45
  params.date = args.after_date;
45
- if (args.eclipse_type === "solar") {
46
- // Use local endpoint if coordinates provided, otherwise global
47
- if (args.latitude != null && args.longitude != null) {
48
- return await backendClient.request("GET", "/eclipse/solar/local", { params, timeoutMs: 60_000 });
49
- }
50
- return await backendClient.request("GET", "/eclipse/solar/global", { params, timeoutMs: 60_000 });
51
- }
52
- else {
53
- return await backendClient.request("GET", "/eclipse/lunar/global", { params, timeoutMs: 60_000 });
54
- }
46
+ if (args.eclipse_type && args.eclipse_type !== "any")
47
+ params.type = args.eclipse_type;
48
+ return await backendClient.request("GET", "/eclipse/next-visible", { params, timeoutMs: 60_000 });
55
49
  },
56
50
  });
@@ -1,7 +1,7 @@
1
- import { registerTool } from "../index.js";
1
+ import { registerTool, validateRequired } from "../index.js";
2
2
  import { backendClient } from "../../backend/client.js";
3
3
  registerTool({
4
- name: "ephemeris.electional",
4
+ name: "ephemeris_electional",
5
5
  description: "Find optimal planetary timing windows (electional astrology). Scans a date range to find the " +
6
6
  "best times for an event based on essential dignity, aspect quality, sect, and void-of-course " +
7
7
  "moon penalties. Evaluates every hour and clusters the best continuous windows.\n\n" +
@@ -48,13 +48,14 @@ registerTool({
48
48
  format: {
49
49
  type: "string",
50
50
  enum: ["json", "llm"],
51
- description: "Output format. 'llm' = compact token-efficient output (Startup tier).",
51
+ description: "Output format. 'llm' = compact token-efficient output (available on all tiers).",
52
52
  },
53
53
  },
54
54
  required: ["start_date", "end_date", "latitude", "longitude"],
55
55
  additionalProperties: false,
56
56
  },
57
57
  handler: async (args) => {
58
+ validateRequired(args, ["start_date", "end_date", "latitude", "longitude"]);
58
59
  const query = {
59
60
  start_date: args.start_date,
60
61
  end_date: args.end_date,
@@ -1,22 +1,35 @@
1
- import { registerTool } from "../index.js";
1
+ import { registerTool, validateRequired } from "../index.js";
2
2
  import { backendClient } from "../../backend/client.js";
3
+ /**
4
+ * Ensure a datetime string has a timezone offset for Go's time.Time parsing.
5
+ * Go's encoding/json only accepts RFC 3339 (must have Z or +HH:MM offset).
6
+ */
7
+ function ensureTimezone(dt) {
8
+ if (!dt)
9
+ return dt;
10
+ // Already has timezone indicator
11
+ if (/[Zz]$/.test(dt) || /[+-]\d{2}:\d{2}$/.test(dt) || /[+-]\d{4}$/.test(dt))
12
+ return dt;
13
+ return dt + "Z";
14
+ }
3
15
  registerTool({
4
- name: "human_design.chart",
5
- description: "Calculate a full Human Design bodygraph chart from birth data. Returns the person's Type " +
16
+ name: "human_design_chart",
17
+ description: "Calculate a full Human Design I Ching hexagram chart from birth data. Returns the person's Type " +
6
18
  "(Generator, Manifesting Generator, Projector, Manifestor, Reflector), Strategy, Authority, " +
7
19
  "Profile (e.g. 1/3, 2/4), defined and undefined Centers, activated Gates and Channels, " +
8
20
  "Incarnation Cross, and both Personality (conscious) and Design (unconscious) planetary positions.\n\n" +
9
21
  "CREDIT COST: 1 credit per call.\n\n" +
10
22
  "Human Design uses two calculation moments: the birth time (Personality) and ~88° of Sun motion " +
11
23
  "before birth (~3 months prior, the Design calculation). The API handles this automatically.\n\n" +
12
- "EXAMPLE: Get the Human Design chart for someone born April 15, 1990 at 2:30 PM in Chicago:\n" +
13
- " datetime='1990-04-15T14:30:00', latitude=41.8781, longitude=-87.6298",
24
+ "IMPORTANT: The datetime should be in UTC. If you have local birth time, convert to UTC first.\n\n" +
25
+ "EXAMPLE: Get the Human Design chart for someone born April 15, 1990 at 7:30 PM UTC:\n" +
26
+ " datetime='1990-04-15T19:30:00Z', latitude=41.8781, longitude=-87.6298",
14
27
  inputSchema: {
15
28
  type: "object",
16
29
  properties: {
17
30
  datetime: {
18
31
  type: "string",
19
- description: "ISO 8601 birth datetime (local time at birth location), e.g. '1990-04-15T14:30:00'.",
32
+ description: "ISO 8601 birth datetime in UTC, e.g. '1990-04-15T19:30:00Z'. Must include 'Z' or timezone offset.",
20
33
  },
21
34
  latitude: {
22
35
  type: "number",
@@ -29,19 +42,22 @@ registerTool({
29
42
  format: {
30
43
  type: "string",
31
44
  enum: ["json", "llm"],
32
- description: "Output format. 'llm' returns compact array projection for token efficiency (Pro tier). " +
45
+ description: "Output format. 'llm' returns compact array projection for token efficiency (available on all tiers). " +
33
46
  "'json' returns verbose full output.",
34
47
  },
35
48
  },
36
- required: ["datetime", "latitude", "longitude"],
49
+ required: ["datetime"],
37
50
  additionalProperties: false,
38
51
  },
39
52
  handler: async (args) => {
53
+ validateRequired(args, ["datetime"]);
40
54
  const body = {
41
- datetime: args.datetime,
42
- latitude: args.latitude,
43
- longitude: args.longitude,
55
+ birth_datetime_utc: ensureTimezone(args.datetime),
44
56
  };
57
+ if (args.latitude != null)
58
+ body.latitude = args.latitude;
59
+ if (args.longitude != null)
60
+ body.longitude = args.longitude;
45
61
  const query = {};
46
62
  if (args.format)
47
63
  query.format = args.format;
@@ -1,7 +1,7 @@
1
- import { registerTool } from "../index.js";
1
+ import { registerTool, validateCoordinates } from "../index.js";
2
2
  import { backendClient } from "../../backend/client.js";
3
3
  registerTool({
4
- name: "ephemeris.moon_phase",
4
+ name: "ephemeris_moon_phase",
5
5
  description: "Get the current Moon phase and void-of-course status. Returns the Moon's sign, phase name " +
6
6
  "(New, Waxing Crescent, First Quarter, Waxing Gibbous, Full, Waning Gibbous, Last Quarter, " +
7
7
  "Waning Crescent), exact phase angle, illumination percentage, and the next void-of-course period.\n\n" +
@@ -31,6 +31,7 @@ registerTool({
31
31
  additionalProperties: false,
32
32
  },
33
33
  handler: async (args) => {
34
+ validateCoordinates(args, "latitude", "longitude");
34
35
  const params = {};
35
36
  if (args.datetime)
36
37
  params.datetime = args.datetime;
@@ -1,12 +1,18 @@
1
- import { registerTool } from "../index.js";
1
+ import { registerTool, validateRequired } from "../index.js";
2
2
  import { backendClient } from "../../backend/client.js";
3
3
  const DATETIME_DESC = "ISO 8601 datetime string, e.g. '1990-04-15T14:30:00' (local time at birth location). " +
4
4
  "Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.";
5
+ /** Map human-readable house system names to Swiss Ephemeris single-letter codes */
6
+ const HOUSE_SYSTEM_MAP = {
7
+ placidus: "P", whole_sign: "W", equal: "E", koch: "K",
8
+ campanus: "C", regiomontanus: "R", porphyry: "O",
9
+ alcabitius: "B", morinus: "M",
10
+ };
5
11
  registerTool({
6
- name: "ephemeris.natal_chart",
12
+ name: "ephemeris_natal_chart",
7
13
  description: "Calculate a full natal (birth) chart for a person. Returns planetary positions, house cusps, " +
8
14
  "aspects, and chart patterns. Use format='llm' for a compact, token-efficient output ideal for " +
9
- "interpretation (requires Pro tier). The result includes all major planets, angles (ASC/MC/DSC/IC), " +
15
+ "interpretation (available on all tiers). The result includes all major planets, angles (ASC/MC/DSC/IC), " +
10
16
  "essential dignities, retrograde status, house system data, and major aspect grid.\n\n" +
11
17
  "CREDIT COST: 1 credit per call.\n\n" +
12
18
  "EXAMPLE: Calculate the natal chart for someone born April 15, 1990 at 2:30 PM in Chicago:\n" +
@@ -34,32 +40,47 @@ registerTool({
34
40
  format: {
35
41
  type: "string",
36
42
  enum: ["json", "llm"],
37
- description: "Output format. 'llm' returns a compact array-based projection optimized for LLM token efficiency (Pro tier required). 'json' returns full verbose JSON.",
43
+ description: "Output format. 'llm' returns a compact array-based projection optimized for LLM token efficiency (available on all tiers). 'json' returns full verbose JSON.",
38
44
  },
39
45
  include_arabic_parts: {
40
46
  type: "boolean",
41
- description: "If true, include Hermetic Lots (Arabic Parts) in the response.",
47
+ description: "Reserved for future use. Hermetic Lots / Arabic Parts are currently available via the dedicated /ephemeris/hermetic-lots endpoint.",
42
48
  },
43
49
  include_fixed_stars: {
44
50
  type: "boolean",
45
- description: "If true, include fixed star conjunctions in the response.",
51
+ description: "Reserved for future use. Fixed star positions are currently available via the dedicated /ephemeris/fixed-stars endpoint.",
46
52
  },
47
53
  },
48
54
  required: ["datetime", "latitude", "longitude"],
49
55
  additionalProperties: false,
50
56
  },
51
57
  handler: async (args) => {
58
+ validateRequired(args, ["datetime", "latitude", "longitude"]);
52
59
  const body = {
53
- datetime: args.datetime,
54
- latitude: args.latitude,
55
- longitude: args.longitude,
60
+ subject: {
61
+ name: "MCP Request",
62
+ birth_datetime: { iso: args.datetime },
63
+ birth_location: {
64
+ latitude: { decimal: args.latitude },
65
+ longitude: { decimal: args.longitude }
66
+ },
67
+ }
56
68
  };
57
- if (args.house_system)
58
- body.house_system = args.house_system;
59
- if (args.include_arabic_parts)
60
- body.include_arabic_parts = args.include_arabic_parts;
61
- if (args.include_fixed_stars)
62
- body.include_fixed_stars = args.include_fixed_stars;
69
+ if (args.house_system) {
70
+ const code = HOUSE_SYSTEM_MAP[args.house_system] ?? args.house_system;
71
+ body.configuration = { house_system: code };
72
+ }
73
+ if (args.include_fixed_stars) {
74
+ body.configuration = {
75
+ ...(body.configuration || {}),
76
+ fixed_star_options: { include: true }
77
+ };
78
+ }
79
+ if (args.include_arabic_parts) {
80
+ body.options = {
81
+ include_hermetic_lots: true,
82
+ };
83
+ }
63
84
  const query = {};
64
85
  if (args.format)
65
86
  query.format = args.format;
@@ -1,7 +1,7 @@
1
- import { registerTool } from "../index.js";
1
+ import { registerTool, validateRequired } from "../index.js";
2
2
  import { backendClient } from "../../backend/client.js";
3
3
  registerTool({
4
- name: "ephemeris.relocation",
4
+ name: "ephemeris_relocation",
5
5
  description: "Calculate a relocation chart — the same natal planetary positions re-cast for a different " +
6
6
  "geographic location. Used to understand how living in a different city shifts house placements " +
7
7
  "and angles, without changing the planetary longitudes in the chart.\n\n" +
@@ -40,23 +40,27 @@ registerTool({
40
40
  format: {
41
41
  type: "string",
42
42
  enum: ["json", "llm"],
43
- description: "Output format. 'llm' = compact token-efficient output (Startup tier).",
43
+ description: "Output format. 'llm' = compact token-efficient output (available on all tiers).",
44
44
  },
45
45
  },
46
46
  required: ["natal_datetime", "natal_latitude", "natal_longitude", "relocation_latitude", "relocation_longitude"],
47
47
  additionalProperties: false,
48
48
  },
49
49
  handler: async (args) => {
50
+ validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude", "relocation_latitude", "relocation_longitude"]);
50
51
  const body = {
51
52
  natal: {
52
- datetime: args.natal_datetime,
53
- latitude: args.natal_latitude,
54
- longitude: args.natal_longitude,
55
- },
56
- relocation: {
57
- latitude: args.relocation_latitude,
58
- longitude: args.relocation_longitude,
53
+ subject: {
54
+ name: "MCP Request",
55
+ birth_datetime: { iso: args.natal_datetime },
56
+ birth_location: {
57
+ latitude: { decimal: args.natal_latitude },
58
+ longitude: { decimal: args.natal_longitude },
59
+ },
60
+ },
59
61
  },
62
+ relocation_lat: args.relocation_latitude,
63
+ relocation_lon: args.relocation_longitude,
60
64
  };
61
65
  if (args.house_system)
62
66
  body.house_system = args.house_system;
@@ -0,0 +1,126 @@
1
+ import { registerTool, validateRequired } from "../index.js";
2
+ import { backendClient } from "../../backend/client.js";
3
+ // POST /predictive/returns/solar
4
+ registerTool({
5
+ name: "ephemeris_solar_return",
6
+ description: "Calculate the exact Solar Return date/time — when the Sun returns to its natal ecliptic longitude. " +
7
+ "Used to cast the Solar Return chart for the year ahead.\n\n" +
8
+ "CREDIT COST: 1 credit per call.\n\n" +
9
+ "EXAMPLE: Solar return for someone born 1985-06-21 near their 2026 birthday:\n" +
10
+ " birth_datetime='1985-06-21T14:00:00', birth_latitude=51.5, birth_longitude=-0.12,\n" +
11
+ " target_datetime='2026-06-01'",
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ birth_datetime: {
16
+ type: "string",
17
+ description: "ISO 8601 birth date/time (e.g. '1985-06-21T14:00:00').",
18
+ },
19
+ birth_latitude: {
20
+ type: "number",
21
+ description: "Birth latitude in decimal degrees.",
22
+ },
23
+ birth_longitude: {
24
+ type: "number",
25
+ description: "Birth longitude in decimal degrees.",
26
+ },
27
+ target_datetime: {
28
+ type: "string",
29
+ description: "Date near which to find the Solar Return (ISO 8601). Defaults to now.",
30
+ },
31
+ return_latitude: {
32
+ type: "number",
33
+ description: "Location latitude for the return chart (optional). Defaults to birth location.",
34
+ },
35
+ return_longitude: {
36
+ type: "number",
37
+ description: "Location longitude for the return chart (optional). Defaults to birth location.",
38
+ },
39
+ },
40
+ required: ["birth_datetime", "birth_latitude", "birth_longitude"],
41
+ additionalProperties: false,
42
+ },
43
+ handler: async (args) => {
44
+ validateRequired(args, ["birth_datetime", "birth_latitude", "birth_longitude"]);
45
+ const body = {
46
+ birth_datetime: {
47
+ iso: args.birth_datetime,
48
+ },
49
+ birth_location: {
50
+ latitude: { decimal: args.birth_latitude },
51
+ longitude: { decimal: args.birth_longitude },
52
+ },
53
+ };
54
+ if (args.target_datetime) {
55
+ body.target_datetime = { iso: args.target_datetime };
56
+ }
57
+ if (args.return_latitude != null && args.return_longitude != null) {
58
+ body.return_location = {
59
+ latitude: args.return_latitude,
60
+ longitude: args.return_longitude,
61
+ };
62
+ }
63
+ return await backendClient.post("/predictive/returns/solar", body);
64
+ },
65
+ });
66
+ // POST /predictive/returns/lunar
67
+ registerTool({
68
+ name: "ephemeris_lunar_return",
69
+ description: "Calculate the exact Lunar Return date/time — when the Moon returns to its natal ecliptic longitude (~every 27.3 days). " +
70
+ "Used to cast the monthly Lunar Return chart.\n\n" +
71
+ "CREDIT COST: 1 credit per call.\n\n" +
72
+ "EXAMPLE: Next lunar return after 2026-03-15 for someone born 1990-04-15:\n" +
73
+ " birth_datetime='1990-04-15T14:30:00', birth_latitude=41.88, birth_longitude=-87.63,\n" +
74
+ " target_datetime='2026-03-15'",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ birth_datetime: {
79
+ type: "string",
80
+ description: "ISO 8601 birth date/time.",
81
+ },
82
+ birth_latitude: {
83
+ type: "number",
84
+ description: "Birth latitude in decimal degrees.",
85
+ },
86
+ birth_longitude: {
87
+ type: "number",
88
+ description: "Birth longitude in decimal degrees.",
89
+ },
90
+ target_datetime: {
91
+ type: "string",
92
+ description: "Date near which to find the Lunar Return (ISO 8601). Defaults to now.",
93
+ },
94
+ return_latitude: {
95
+ type: "number",
96
+ description: "Location latitude for return chart (optional). Defaults to birth location.",
97
+ },
98
+ return_longitude: {
99
+ type: "number",
100
+ description: "Location longitude for return chart (optional). Defaults to birth location.",
101
+ },
102
+ },
103
+ required: ["birth_datetime", "birth_latitude", "birth_longitude"],
104
+ additionalProperties: false,
105
+ },
106
+ handler: async (args) => {
107
+ validateRequired(args, ["birth_datetime", "birth_latitude", "birth_longitude"]);
108
+ const body = {
109
+ birth_datetime: { iso: args.birth_datetime },
110
+ birth_location: {
111
+ latitude: { decimal: args.birth_latitude },
112
+ longitude: { decimal: args.birth_longitude },
113
+ },
114
+ };
115
+ if (args.target_datetime) {
116
+ body.target_datetime = { iso: args.target_datetime };
117
+ }
118
+ if (args.return_latitude != null && args.return_longitude != null) {
119
+ body.return_location = {
120
+ latitude: args.return_latitude,
121
+ longitude: args.return_longitude,
122
+ };
123
+ }
124
+ return await backendClient.post("/predictive/returns/lunar", body);
125
+ },
126
+ });
@@ -1,11 +1,11 @@
1
- import { registerTool } from "../index.js";
1
+ import { registerTool, validateRequired } from "../index.js";
2
2
  import { backendClient } from "../../backend/client.js";
3
3
  registerTool({
4
- name: "ephemeris.synastry",
4
+ name: "ephemeris_synastry",
5
5
  description: "Calculate a synastry chart comparing two people's natal charts. Returns inter-aspects " +
6
6
  "(planetary connections between the two charts), composite points, and relationship indicators. " +
7
7
  "Use this for compatibility analysis, relationship timing, or partnership insights.\n\n" +
8
- "CREDIT COST: 5 credits per call.\n\n" +
8
+ "CREDIT COST: 3 credits per call.\n\n" +
9
9
  "EXAMPLE: Compare two people's charts:\n" +
10
10
  " person_a_datetime='1990-04-15T14:30:00', person_a_latitude=41.8781, person_a_longitude=-87.6298,\n" +
11
11
  " person_b_datetime='1988-09-22T08:15:00', person_b_latitude=34.0522, person_b_longitude=-118.2437",
@@ -26,7 +26,7 @@ registerTool({
26
26
  format: {
27
27
  type: "string",
28
28
  enum: ["json", "llm"],
29
- description: "Output format. 'llm' is compact and token-efficient (Pro tier).",
29
+ description: "Output format. 'llm' is compact and token-efficient (available on all tiers).",
30
30
  },
31
31
  },
32
32
  required: [
@@ -36,16 +36,26 @@ registerTool({
36
36
  additionalProperties: false,
37
37
  },
38
38
  handler: async (args) => {
39
+ validateRequired(args, [
40
+ "person_a_datetime", "person_a_latitude", "person_a_longitude",
41
+ "person_b_datetime", "person_b_latitude", "person_b_longitude",
42
+ ]);
39
43
  const body = {
40
- natal_a: {
41
- datetime: args.person_a_datetime,
42
- latitude: args.person_a_latitude,
43
- longitude: args.person_a_longitude,
44
+ subject_a: {
45
+ name: "Person A",
46
+ birth_datetime: { iso: args.person_a_datetime },
47
+ birth_location: {
48
+ latitude: { decimal: args.person_a_latitude },
49
+ longitude: { decimal: args.person_a_longitude },
50
+ },
44
51
  },
45
- natal_b: {
46
- datetime: args.person_b_datetime,
47
- latitude: args.person_b_latitude,
48
- longitude: args.person_b_longitude,
52
+ subject_b: {
53
+ name: "Person B",
54
+ birth_datetime: { iso: args.person_b_datetime },
55
+ birth_location: {
56
+ latitude: { decimal: args.person_b_latitude },
57
+ longitude: { decimal: args.person_b_longitude },
58
+ },
49
59
  },
50
60
  };
51
61
  if (args.house_system)
@@ -1,12 +1,14 @@
1
- import { registerTool } from "../index.js";
1
+ import { registerTool, validateRequired } from "../index.js";
2
2
  import { backendClient } from "../../backend/client.js";
3
3
  registerTool({
4
- name: "ephemeris.transits",
4
+ name: "ephemeris_transits",
5
5
  description: "Search for astrological transit events affecting a natal chart over a date range. " +
6
6
  "Returns a list of exact transit moments — when transiting planets form specified aspects " +
7
7
  "to natal planet positions. Ideal for generating horoscope timelines, event forecasting, or " +
8
8
  "finding optimal timing windows.\n\n" +
9
- "CREDIT COST: 5 credits per call.\n\n" +
9
+ "HOW IT WORKS: This tool first calculates the natal chart to extract your actual planetary " +
10
+ "ecliptic longitudes, then searches for transiting planets crossing those exact degrees.\n\n" +
11
+ "CREDIT COST: 6 credits per call (1 for natal + 5 for transit search).\n\n" +
10
12
  "EXAMPLE: Find all Saturn transits to the natal Sun/Moon for the next 6 months:\n" +
11
13
  " natal_datetime='1990-04-15T14:30:00', natal_latitude=41.8781, natal_longitude=-87.6298,\n" +
12
14
  " start_date='2026-01-01', end_date='2026-06-30', transiting_planets=['saturn']",
@@ -42,39 +44,87 @@ registerTool({
42
44
  natal_points: {
43
45
  type: "array",
44
46
  items: { type: "string" },
45
- description: "Natal point IDs to receive transits. E.g. ['sun', 'moon', 'asc', 'mc']. Omit for all core points.",
46
- },
47
- aspects: {
48
- type: "array",
49
- items: { type: "string", enum: ["conjunction", "opposition", "trine", "square", "sextile"] },
50
- description: "Aspect types to include in the search. Defaults to all major aspects.",
51
- },
52
- orb: {
53
- type: "number",
54
- description: "Maximum orb in degrees (default: 1.0).",
47
+ description: "Natal point IDs whose exact longitudes should be targeted. E.g. ['sun', 'moon', 'asc', 'mc']. Omit for all core points.",
55
48
  },
56
49
  },
57
50
  required: ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"],
58
51
  additionalProperties: false,
59
52
  },
60
53
  handler: async (args) => {
61
- const body = {
62
- natal: {
63
- datetime: args.natal_datetime,
64
- latitude: args.natal_latitude,
65
- longitude: args.natal_longitude,
54
+ validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"]);
55
+ // ── Step 1: Compute natal chart to extract real planetary longitudes ──
56
+ const natalBody = {
57
+ subject: {
58
+ name: "Transit Natal Subject",
59
+ birth_datetime: { iso: args.natal_datetime },
60
+ birth_location: {
61
+ latitude: { decimal: args.natal_latitude },
62
+ longitude: { decimal: args.natal_longitude }
63
+ },
66
64
  },
67
- start_date: args.start_date,
68
- end_date: args.end_date,
65
+ };
66
+ const natalResult = await backendClient.post("/ephemeris/natal-chart", natalBody);
67
+ // Extract ecliptic longitudes from natal chart planets
68
+ const targetDegrees = [];
69
+ const natalPositionMap = {};
70
+ // The natal response typically has a `planets` array or object
71
+ const planets = natalResult?.planets || natalResult?.data?.planets || [];
72
+ const planetArray = Array.isArray(planets) ? planets : Object.values(planets);
73
+ // Filter to requested natal points if specified
74
+ const wantedPoints = args.natal_points
75
+ ? new Set(args.natal_points.map((p) => p.toLowerCase()))
76
+ : null;
77
+ for (const planet of planetArray) {
78
+ const name = (planet.name || planet.id || "").toLowerCase();
79
+ const lon = planet.longitude ?? planet.ecliptic_longitude ?? planet.lon;
80
+ if (typeof lon !== "number" || !isFinite(lon))
81
+ continue;
82
+ if (wantedPoints && !wantedPoints.has(name))
83
+ continue;
84
+ targetDegrees.push(Math.round(lon * 100) / 100); // 2 decimal places
85
+ natalPositionMap[name] = lon;
86
+ }
87
+ // Also extract angles (ASC, MC, DSC, IC) if present
88
+ const angles = natalResult?.angles || natalResult?.data?.angles;
89
+ if (angles) {
90
+ const angleEntries = Array.isArray(angles) ? angles : Object.entries(angles).map(([k, v]) => ({ name: k, ...(typeof v === 'object' ? v : { longitude: v }) }));
91
+ for (const angle of angleEntries) {
92
+ const name = (angle.name || angle.id || "").toLowerCase();
93
+ const lon = angle.longitude ?? angle.ecliptic_longitude ?? angle.lon;
94
+ if (typeof lon !== "number" || !isFinite(lon))
95
+ continue;
96
+ if (wantedPoints && !wantedPoints.has(name))
97
+ continue;
98
+ targetDegrees.push(Math.round(lon * 100) / 100);
99
+ natalPositionMap[name] = lon;
100
+ }
101
+ }
102
+ if (targetDegrees.length === 0) {
103
+ throw new Error("Could not extract natal planet longitudes from the natal chart calculation. " +
104
+ "Please verify your natal_datetime, natal_latitude, and natal_longitude are correct.");
105
+ }
106
+ // ── Step 2: Search transits against those real natal longitudes ──
107
+ let startStr = args.start_date;
108
+ if (/^\d{4}-\d{2}-\d{2}$/.test(startStr)) {
109
+ startStr += "T00:00:00Z";
110
+ }
111
+ let endStr = args.end_date;
112
+ if (/^\d{4}-\d{2}-\d{2}$/.test(endStr)) {
113
+ endStr += "T23:59:59Z";
114
+ }
115
+ const transitBody = {
116
+ start_date: startStr,
117
+ end_date: endStr,
118
+ target_degrees: targetDegrees,
69
119
  };
70
120
  if (args.transiting_planets)
71
- body.transiting_planets = args.transiting_planets;
72
- if (args.natal_points)
73
- body.natal_points = args.natal_points;
74
- if (args.aspects)
75
- body.aspects = args.aspects;
76
- if (args.orb != null)
77
- body.orb = args.orb;
78
- return await backendClient.request("POST", "/predictive/transits/search", { data: body });
121
+ transitBody.planet_names = args.transiting_planets;
122
+ const transitResult = await backendClient.request("POST", "/predictive/transits/search", { data: transitBody });
123
+ // Return combined context so the LLM knows what natal positions were targeted
124
+ return {
125
+ natal_positions: natalPositionMap,
126
+ target_degrees_used: targetDegrees,
127
+ transit_results: transitResult,
128
+ };
79
129
  },
80
130
  });