@openephemeris/mcp-server 3.24.0 → 4.1.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 +173 -0
- package/LICENSE +21 -21
- package/README.md +75 -3
- package/dist/analytics.js +37 -5
- package/dist/backend/client.d.ts +7 -0
- package/dist/backend/client.js +39 -38
- package/dist/index.js +64 -2
- package/dist/oauth/session-utils.d.ts +42 -18
- package/dist/oauth/session-utils.js +79 -0
- package/dist/prompts.js +55 -42
- package/dist/server-sse.js +114 -14
- package/dist/tools/apps/bazi-app.js +15 -28
- package/dist/tools/apps/bi-wheel-app.js +14 -11
- package/dist/tools/apps/bodygraph-app.d.ts +5 -5
- package/dist/tools/apps/bodygraph-app.js +159 -212
- package/dist/tools/apps/chart-wheel-app.js +21 -20
- package/dist/tools/apps/location-tools.js +167 -18
- package/dist/tools/apps/moon-phase-app.js +10 -3
- package/dist/tools/apps/transit-timeline-app.js +6 -4
- package/dist/tools/apps/vedic-chart-app.js +15 -49
- package/dist/tools/datetime.d.ts +65 -0
- package/dist/tools/datetime.js +153 -0
- package/dist/tools/dev.js +4 -3
- package/dist/tools/index.d.ts +45 -2
- package/dist/tools/index.js +81 -2
- package/dist/tools/specialized/account.d.ts +1 -0
- package/dist/tools/specialized/account.js +100 -0
- package/dist/tools/specialized/acg.js +16 -14
- package/dist/tools/specialized/bazi.d.ts +7 -1
- package/dist/tools/specialized/bazi.js +89 -23
- package/dist/tools/specialized/bi_wheel.js +5 -4
- package/dist/tools/specialized/chart_wheel.js +5 -8
- package/dist/tools/specialized/comparative.js +40 -21
- package/dist/tools/specialized/electional.js +13 -10
- package/dist/tools/specialized/ephemeris_core.js +13 -8
- package/dist/tools/specialized/ephemeris_extended.js +60 -53
- package/dist/tools/specialized/hd_bodygraph.js +8 -10
- package/dist/tools/specialized/hd_cycles.js +7 -14
- package/dist/tools/specialized/hd_group.js +20 -13
- package/dist/tools/specialized/human_design.js +11 -17
- package/dist/tools/specialized/moon.js +14 -6
- package/dist/tools/specialized/natal.js +7 -9
- package/dist/tools/specialized/progressed.js +12 -8
- package/dist/tools/specialized/relocation.js +9 -3
- package/dist/tools/specialized/returns.js +23 -11
- package/dist/tools/specialized/synastry.js +17 -6
- package/dist/tools/specialized/transits.js +9 -5
- package/dist/tools/specialized/vedic.js +5 -3
- package/dist/tools/specialized/venus_star_points.js +14 -9
- package/dist/ui/bazi.html +1063 -1049
- package/dist/ui/bi-wheel.html +4188 -4128
- package/dist/ui/bodygraph.html +3673 -3616
- package/dist/ui/chart-wheel.html +3769 -3713
- package/dist/ui/moon-phase.html +3219 -3153
- package/dist/ui/transit-timeline.html +199 -170
- package/dist/ui/vedic-chart.html +1116 -1098
- package/package.json +3 -2
- package/smithery.yaml +1 -1
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { registerTool } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_IMAGE_AND_JSON } from "../output-schemas.js";
|
|
4
|
-
function parseBaziArgs(args) {
|
|
4
|
+
export function parseBaziArgs(args, datetimeField = "datetime") {
|
|
5
5
|
let { year, month, day, hour } = args;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
6
|
+
const datetime = args[datetimeField];
|
|
7
|
+
if (datetime && (!year || !month || !day)) {
|
|
8
|
+
const parts = baziLocalComponents(datetime, args.timezone, datetimeField);
|
|
9
|
+
year = parts.year;
|
|
10
|
+
month = parts.month;
|
|
11
|
+
day = parts.day;
|
|
12
|
+
if (hour == null)
|
|
13
|
+
hour = parts.hour;
|
|
15
14
|
}
|
|
16
15
|
if (!year || !month || !day) {
|
|
17
16
|
throw new Error("Provide year/month/day fields, or a datetime ISO string. " +
|
|
@@ -22,6 +21,49 @@ function parseBaziArgs(args) {
|
|
|
22
21
|
out.hour = hour;
|
|
23
22
|
return out;
|
|
24
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Extract the LOCAL calendar/clock components a BaZi chart is built from.
|
|
26
|
+
*
|
|
27
|
+
* BaZi pillars are a function of local wall-clock time at the birth place — the
|
|
28
|
+
* hour pillar is a two-hour shí block on the local clock — so unlike a western
|
|
29
|
+
* chart there is no UTC instant to resolve to, and a zone-less datetime is
|
|
30
|
+
* exactly right here. What was wrong was HOW those components were read: the
|
|
31
|
+
* previous implementation did `new Date("1987-07-15T14:00:00")`, which JS parses
|
|
32
|
+
* in the *host process's* timezone, then read `.getUTCHours()`. On any server
|
|
33
|
+
* not running in UTC that silently shifted the hour pillar, and near midnight
|
|
34
|
+
* the day pillar too. Reading the components lexically removes the dependency on
|
|
35
|
+
* where the server happens to be.
|
|
36
|
+
*
|
|
37
|
+
* A datetime that carries its own zone (`Z` / `±HH:MM`) is not local wall-clock
|
|
38
|
+
* time. It is converted into `timezone` when one is supplied; without one, its
|
|
39
|
+
* own stated zone is taken as the local clock, which is the long-standing
|
|
40
|
+
* behaviour and the only reading available.
|
|
41
|
+
*/
|
|
42
|
+
function baziLocalComponents(datetime, timezone, field) {
|
|
43
|
+
const value = String(datetime).trim();
|
|
44
|
+
const zoned = /([Zz]|[+-]\d{2}:?\d{2})$/.test(value);
|
|
45
|
+
if (zoned && timezone) {
|
|
46
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
47
|
+
timeZone: timezone,
|
|
48
|
+
hour12: false,
|
|
49
|
+
year: "numeric", month: "2-digit", day: "2-digit",
|
|
50
|
+
hour: "2-digit", minute: "2-digit",
|
|
51
|
+
}).formatToParts(new Date(value));
|
|
52
|
+
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
53
|
+
return { year: get("year"), month: get("month"), day: get("day"), hour: get("hour") % 24 };
|
|
54
|
+
}
|
|
55
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}))?/.exec(value);
|
|
56
|
+
if (!m) {
|
|
57
|
+
throw new Error(`\`${field}\` is "${value}", which is not an ISO 8601 date or datetime. ` +
|
|
58
|
+
"Example: '1987-07-15T14:00:00' (local time at the birth place).");
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
year: Number(m[1]),
|
|
62
|
+
month: Number(m[2]),
|
|
63
|
+
day: Number(m[3]),
|
|
64
|
+
hour: m[4] != null ? Number(m[4]) : undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
25
67
|
// Shared datetime input schema fragment — used across all BaZiRequest tools.
|
|
26
68
|
const DATETIME_PROPERTIES = {
|
|
27
69
|
year: {
|
|
@@ -43,8 +85,17 @@ const DATETIME_PROPERTIES = {
|
|
|
43
85
|
},
|
|
44
86
|
datetime: {
|
|
45
87
|
type: "string",
|
|
46
|
-
description: "Alternative to year/month/day: ISO 8601 datetime
|
|
47
|
-
"
|
|
88
|
+
description: "Alternative to year/month/day: ISO 8601 datetime read as LOCAL wall-clock time at the " +
|
|
89
|
+
"birth place — BaZi pillars are local by definition, so a zone-less value like " +
|
|
90
|
+
"'1987-07-15T14:00:00' is correct here and is NOT converted to UTC. " +
|
|
91
|
+
"If you pass an instant instead ('1987-07-15T19:00:00Z' or with an offset), also pass " +
|
|
92
|
+
"`timezone` so the local hour can be derived. Use this OR the individual fields.",
|
|
93
|
+
},
|
|
94
|
+
timezone: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "IANA timezone name for the birth place, e.g. 'Asia/Shanghai'. Only needed when " +
|
|
97
|
+
"`datetime` carries a 'Z' or ±HH:MM offset, to convert that instant back to the local " +
|
|
98
|
+
"clock the pillars are built from.",
|
|
48
99
|
},
|
|
49
100
|
};
|
|
50
101
|
// Shared visual input schema fragment — mirrors natal.ts pattern.
|
|
@@ -93,10 +144,9 @@ registerTool({
|
|
|
93
144
|
"Includes the Day Master element and basic metadata.\n\n" +
|
|
94
145
|
"CREDIT COST: 1 credit (3 credits when include_visual=true).\n\n" +
|
|
95
146
|
"Set include_visual=true to receive a rendered SVG chart alongside the text data.\n\n" +
|
|
96
|
-
"For deep analysis,
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
" • bazi_luck_pillars() — 8 Da Yun 10-year luck cycles\n\n" +
|
|
147
|
+
"For deep analysis — Ten Gods (十神) per pillar, weighted Wu Xing (五行) element balance, " +
|
|
148
|
+
"Da Yun 10-year luck cycles — dedicated tools are available on the full tool surface " +
|
|
149
|
+
"(`?profile=full` on HTTP, OPENEPHEMERIS_TOOLS=full on stdio).\n\n" +
|
|
100
150
|
"EXAMPLE: BaZi chart for someone born July 15, 1987 at 2 PM:\n" +
|
|
101
151
|
" year=1987, month=7, day=15, hour=14",
|
|
102
152
|
inputSchema: {
|
|
@@ -132,7 +182,7 @@ registerTool({
|
|
|
132
182
|
" RESOURCE: Indirect Resource (偏印), Direct Resource (正印)\n\n" +
|
|
133
183
|
"The Day Pillar itself has no Ten God (it IS the Day Master). " +
|
|
134
184
|
"Hidden stems in the Day Branch still receive Ten God labels.\n\n" +
|
|
135
|
-
"CREDIT COST:
|
|
185
|
+
"CREDIT COST: 3 credits (5 credits when include_visual=true).\n\n" +
|
|
136
186
|
"Set include_visual=true to receive an SVG Four Pillars chart alongside the Ten Gods data.\n\n" +
|
|
137
187
|
"EXAMPLE: Ten Gods for someone born July 15, 1987 at 2 PM:\n" +
|
|
138
188
|
" year=1987, month=7, day=15, hour=14",
|
|
@@ -167,7 +217,7 @@ registerTool({
|
|
|
167
217
|
"Also returns:\n" +
|
|
168
218
|
" • Day Master strength: 'strong' (旺, ≥50% own+resource elements) or 'weak' (弱)\n" +
|
|
169
219
|
" • Yong Shen (用神): the favorable element — what the chart needs most\n\n" +
|
|
170
|
-
"CREDIT COST:
|
|
220
|
+
"CREDIT COST: 3 credits (5 credits when include_visual=true).\n\n" +
|
|
171
221
|
"Set include_visual=true to receive an SVG Four Pillars chart alongside the element data.\n\n" +
|
|
172
222
|
"EXAMPLE: Element balance for 1987-07-15 at 14:00:\n" +
|
|
173
223
|
" year=1987, month=7, day=15, hour=14",
|
|
@@ -204,7 +254,7 @@ registerTool({
|
|
|
204
254
|
"gender is REQUIRED — the direction of luck pillars is gender-dependent.\n\n" +
|
|
205
255
|
"Returns: starting_age, direction, direction_reason, and 8 pillars each with:\n" +
|
|
206
256
|
" stem, branch, Chinese characters, element, start_age, end_age\n\n" +
|
|
207
|
-
"CREDIT COST:
|
|
257
|
+
"CREDIT COST: 3 credits per call.\n\n" +
|
|
208
258
|
"EXAMPLE: Luck pillars for a female born July 15, 1987 at 2 PM:\n" +
|
|
209
259
|
" year=1987, month=7, day=15, hour=14, gender='female'",
|
|
210
260
|
inputSchema: {
|
|
@@ -251,7 +301,7 @@ registerTool({
|
|
|
251
301
|
" • Identify the energetic quality of any given year\n" +
|
|
252
302
|
" • Determine a person's birth year pillar for compatibility context\n" +
|
|
253
303
|
" • Find the NaYin element for year or day interpretations\n\n" +
|
|
254
|
-
"CREDIT COST:
|
|
304
|
+
"CREDIT COST: 3 credits per call.\n\n" +
|
|
255
305
|
"EXAMPLE: Year pillar for 2025:\n" +
|
|
256
306
|
" year=2025",
|
|
257
307
|
inputSchema: {
|
|
@@ -292,7 +342,7 @@ registerTool({
|
|
|
292
342
|
" -6 Three Penalties (三刑 Sān Xíng) — branch penalty formations\n\n" +
|
|
293
343
|
"Assessment grades:\n" +
|
|
294
344
|
" 90–100: excellent | 70–89: good | 50–69: moderate | below 50: challenging\n\n" +
|
|
295
|
-
"CREDIT COST:
|
|
345
|
+
"CREDIT COST: 3 credits per call.\n\n" +
|
|
296
346
|
"EXAMPLE: Compatibility between two people:\n" +
|
|
297
347
|
" chart_a_year=1987, chart_a_month=7, chart_a_day=15, chart_a_hour=14\n" +
|
|
298
348
|
" chart_b_year=1990, chart_b_month=3, chart_b_day=22, chart_b_hour=8",
|
|
@@ -306,7 +356,14 @@ registerTool({
|
|
|
306
356
|
chart_a_hour: { type: "integer", description: "Chart A birth hour (0–23). Optional." },
|
|
307
357
|
chart_a_datetime: {
|
|
308
358
|
type: "string",
|
|
309
|
-
description: "Chart A alternative: ISO 8601 datetime
|
|
359
|
+
description: "Chart A alternative: ISO 8601 datetime read as LOCAL wall-clock time at the birth " +
|
|
360
|
+
"place — a zone-less value is correct here and is not converted to UTC. If it carries " +
|
|
361
|
+
"a 'Z' or offset, also pass chart_a_timezone. Extracts year/month/day/hour automatically.",
|
|
362
|
+
},
|
|
363
|
+
chart_a_timezone: {
|
|
364
|
+
type: "string",
|
|
365
|
+
description: "IANA timezone for Chart A's birth place, e.g. 'Asia/Shanghai'. Only needed when " +
|
|
366
|
+
"chart_a_datetime carries a 'Z' or ±HH:MM offset.",
|
|
310
367
|
},
|
|
311
368
|
// Chart B
|
|
312
369
|
chart_b_year: { type: "integer", description: "Chart B birth year." },
|
|
@@ -315,7 +372,14 @@ registerTool({
|
|
|
315
372
|
chart_b_hour: { type: "integer", description: "Chart B birth hour (0–23). Optional." },
|
|
316
373
|
chart_b_datetime: {
|
|
317
374
|
type: "string",
|
|
318
|
-
description: "Chart B alternative: ISO 8601 datetime
|
|
375
|
+
description: "Chart B alternative: ISO 8601 datetime read as LOCAL wall-clock time at the birth " +
|
|
376
|
+
"place — a zone-less value is correct here and is not converted to UTC. If it carries " +
|
|
377
|
+
"a 'Z' or offset, also pass chart_b_timezone. Extracts year/month/day/hour automatically.",
|
|
378
|
+
},
|
|
379
|
+
chart_b_timezone: {
|
|
380
|
+
type: "string",
|
|
381
|
+
description: "IANA timezone for Chart B's birth place, e.g. 'Asia/Shanghai'. Only needed when " +
|
|
382
|
+
"chart_b_datetime carries a 'Z' or ±HH:MM offset.",
|
|
319
383
|
},
|
|
320
384
|
},
|
|
321
385
|
additionalProperties: false,
|
|
@@ -329,6 +393,7 @@ registerTool({
|
|
|
329
393
|
day: args.chart_a_day,
|
|
330
394
|
hour: args.chart_a_hour,
|
|
331
395
|
datetime: args.chart_a_datetime,
|
|
396
|
+
timezone: args.chart_a_timezone,
|
|
332
397
|
});
|
|
333
398
|
const chartBComponents = parseBaziArgs({
|
|
334
399
|
year: args.chart_b_year,
|
|
@@ -336,6 +401,7 @@ registerTool({
|
|
|
336
401
|
day: args.chart_b_day,
|
|
337
402
|
hour: args.chart_b_hour,
|
|
338
403
|
datetime: args.chart_b_datetime,
|
|
404
|
+
timezone: args.chart_b_timezone,
|
|
339
405
|
});
|
|
340
406
|
return await getActiveClient().request("POST", "/chinese/bazi/compatibility", {
|
|
341
407
|
data: {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { registerTool, validateRequired, pickEnum } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_IMAGE } from "../output-schemas.js";
|
|
4
|
-
|
|
5
|
-
"Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.";
|
|
4
|
+
import { DATETIME_DESC, assertZonedDatetime, timezoneProperty } from "../datetime.js";
|
|
6
5
|
const HOUSE_SYSTEM_MAP = {
|
|
7
6
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
8
7
|
campanus: "C", regiomontanus: "R", porphyry: "O",
|
|
@@ -19,11 +18,11 @@ registerTool({
|
|
|
19
18
|
type: "object",
|
|
20
19
|
properties: {
|
|
21
20
|
datetime_a: { type: "string", description: DATETIME_DESC },
|
|
22
|
-
timezone_a:
|
|
21
|
+
timezone_a: timezoneProperty("subject A's location", "America/Denver"),
|
|
23
22
|
latitude_a: { type: "number", description: "Latitude for subject A" },
|
|
24
23
|
longitude_a: { type: "number", description: "Longitude for subject A" },
|
|
25
24
|
datetime_b: { type: "string", description: DATETIME_DESC },
|
|
26
|
-
timezone_b:
|
|
25
|
+
timezone_b: timezoneProperty("subject B's location", "Europe/London"),
|
|
27
26
|
latitude_b: { type: "number", description: "Latitude for subject B" },
|
|
28
27
|
longitude_b: { type: "number", description: "Longitude for subject B" },
|
|
29
28
|
house_system: {
|
|
@@ -44,6 +43,8 @@ registerTool({
|
|
|
44
43
|
annotations: { title: "Bi-Wheel Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
45
44
|
handler: async (args) => {
|
|
46
45
|
validateRequired(args, ["datetime_a", "latitude_a", "longitude_a", "datetime_b", "latitude_b", "longitude_b"]);
|
|
46
|
+
assertZonedDatetime("datetime_a", args.datetime_a, args.timezone_a, "timezone_a");
|
|
47
|
+
assertZonedDatetime("datetime_b", args.datetime_b, args.timezone_b, "timezone_b");
|
|
47
48
|
const body = {
|
|
48
49
|
subject_a: {
|
|
49
50
|
name: "Subject A",
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { registerTool, validateRequired, pickEnum } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_IMAGE } from "../output-schemas.js";
|
|
4
|
-
|
|
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
|
const HOUSE_SYSTEM_MAP = {
|
|
7
6
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
8
7
|
campanus: "C", regiomontanus: "R", porphyry: "O",
|
|
@@ -13,8 +12,8 @@ registerTool({
|
|
|
13
12
|
description: "Generate a classic astrological Chart Wheel image (SVG) for a person/event. This draws a standard circular chart wheel with planets, aspects, and house cusps. The tool returns a native SVG image that Claude displays inline in the conversation — no external tools needed.\n\n" +
|
|
14
13
|
"For a user-facing interactive chart wheel, use explore_natal_chart instead.\n\n" +
|
|
15
14
|
"CREDIT COST: 2 credits per call.\n\n" +
|
|
16
|
-
"EXAMPLE:
|
|
17
|
-
" datetime='1990-04-15T14:30:00', latitude=41.8781, longitude=-87.6298",
|
|
15
|
+
"EXAMPLE: born 15 April 1990 at 2:30 PM local time in Chicago:\n" +
|
|
16
|
+
" datetime='1990-04-15T14:30:00', timezone='America/Chicago', latitude=41.8781, longitude=-87.6298",
|
|
18
17
|
inputSchema: {
|
|
19
18
|
type: "object",
|
|
20
19
|
properties: {
|
|
@@ -22,10 +21,7 @@ registerTool({
|
|
|
22
21
|
type: "string",
|
|
23
22
|
description: DATETIME_DESC,
|
|
24
23
|
},
|
|
25
|
-
timezone:
|
|
26
|
-
type: "string",
|
|
27
|
-
description: "IANA timezone name, e.g. 'America/Denver'. Use this if passing local time without a UTC offset in the datetime string.",
|
|
28
|
-
},
|
|
24
|
+
timezone: TIMEZONE_PROPERTY,
|
|
29
25
|
latitude: {
|
|
30
26
|
type: "number",
|
|
31
27
|
description: "Geographic latitude of birth location in decimal degrees (positive = North).",
|
|
@@ -52,6 +48,7 @@ registerTool({
|
|
|
52
48
|
annotations: { title: "Chart Wheel SVG", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
53
49
|
handler: async (args) => {
|
|
54
50
|
validateRequired(args, ["datetime", "latitude", "longitude"]);
|
|
51
|
+
assertZonedDatetime("datetime", args.datetime, args.timezone);
|
|
55
52
|
const body = {
|
|
56
53
|
subject: {
|
|
57
54
|
name: "Chart Wheel Request",
|
|
@@ -1,6 +1,11 @@
|
|
|
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, assertZonedDatetime, localToUtcIso, timezoneProperty } from "../datetime.js";
|
|
5
|
+
/** Reject a zone-less person datetime here, before a credit is spent on it. */
|
|
6
|
+
function assertPersonZoned(prefix, args) {
|
|
7
|
+
assertZonedDatetime(`${prefix}_datetime`, args[`${prefix}_datetime`], args[`${prefix}_timezone`], `${prefix}_timezone`);
|
|
8
|
+
}
|
|
4
9
|
function buildSubject(name, datetime, lat, lon, timezone) {
|
|
5
10
|
const subj = {
|
|
6
11
|
name,
|
|
@@ -22,17 +27,19 @@ registerTool({
|
|
|
22
27
|
"uses midpoints of each planet pair to derive a single relationship chart.\n\n" +
|
|
23
28
|
"CREDIT COST: 3 credits per call.\n\n" +
|
|
24
29
|
"EXAMPLE: Composite for two people:\n" +
|
|
25
|
-
" person_a_datetime='1990-04-15T14:30:00',
|
|
26
|
-
"
|
|
30
|
+
" person_a_datetime='1990-04-15T14:30:00', person_a_timezone='America/Chicago',\n" +
|
|
31
|
+
" person_a_latitude=41.88, person_a_longitude=-87.63,\n" +
|
|
32
|
+
" person_b_datetime='1988-09-22T08:15:00', person_b_timezone='America/Los_Angeles',\n" +
|
|
33
|
+
" person_b_latitude=34.05, person_b_longitude=-118.24",
|
|
27
34
|
inputSchema: {
|
|
28
35
|
type: "object",
|
|
29
36
|
properties: {
|
|
30
|
-
person_a_datetime: { type: "string", description:
|
|
31
|
-
person_a_timezone:
|
|
37
|
+
person_a_datetime: { type: "string", description: DATETIME_DESC },
|
|
38
|
+
person_a_timezone: timezoneProperty("Person A's birth location", "America/Denver"),
|
|
32
39
|
person_a_latitude: { type: "number", description: "Person A birth latitude." },
|
|
33
40
|
person_a_longitude: { type: "number", description: "Person A birth longitude." },
|
|
34
|
-
person_b_datetime: { type: "string", description:
|
|
35
|
-
person_b_timezone:
|
|
41
|
+
person_b_datetime: { type: "string", description: DATETIME_DESC },
|
|
42
|
+
person_b_timezone: timezoneProperty("Person B's birth location", "Europe/London"),
|
|
36
43
|
person_b_latitude: { type: "number", description: "Person B birth latitude." },
|
|
37
44
|
person_b_longitude: { type: "number", description: "Person B birth longitude." },
|
|
38
45
|
format: { type: "string", enum: ["json", "llm"], description: "Use 'llm' for token-efficient LLM projection." },
|
|
@@ -50,6 +57,8 @@ registerTool({
|
|
|
50
57
|
"person_a_datetime", "person_a_latitude", "person_a_longitude",
|
|
51
58
|
"person_b_datetime", "person_b_latitude", "person_b_longitude",
|
|
52
59
|
]);
|
|
60
|
+
assertPersonZoned("person_a", args);
|
|
61
|
+
assertPersonZoned("person_b", args);
|
|
53
62
|
const query = {};
|
|
54
63
|
if (args.format)
|
|
55
64
|
query.format = args.format;
|
|
@@ -73,12 +82,12 @@ registerTool({
|
|
|
73
82
|
inputSchema: {
|
|
74
83
|
type: "object",
|
|
75
84
|
properties: {
|
|
76
|
-
person_a_datetime: { type: "string", description:
|
|
77
|
-
person_a_timezone:
|
|
85
|
+
person_a_datetime: { type: "string", description: DATETIME_DESC },
|
|
86
|
+
person_a_timezone: timezoneProperty("Person A's birth location", "America/Denver"),
|
|
78
87
|
person_a_latitude: { type: "number", description: "Person A birth latitude." },
|
|
79
88
|
person_a_longitude: { type: "number", description: "Person A birth longitude." },
|
|
80
|
-
person_b_datetime: { type: "string", description:
|
|
81
|
-
person_b_timezone:
|
|
89
|
+
person_b_datetime: { type: "string", description: DATETIME_DESC },
|
|
90
|
+
person_b_timezone: timezoneProperty("Person B's birth location", "Europe/London"),
|
|
82
91
|
person_b_latitude: { type: "number", description: "Person B birth latitude." },
|
|
83
92
|
person_b_longitude: { type: "number", description: "Person B birth longitude." },
|
|
84
93
|
format: { type: "string", enum: ["json", "llm"], description: "Use 'llm' for token-efficient LLM projection." },
|
|
@@ -96,6 +105,8 @@ registerTool({
|
|
|
96
105
|
"person_a_datetime", "person_a_latitude", "person_a_longitude",
|
|
97
106
|
"person_b_datetime", "person_b_latitude", "person_b_longitude",
|
|
98
107
|
]);
|
|
108
|
+
assertPersonZoned("person_a", args);
|
|
109
|
+
assertPersonZoned("person_b", args);
|
|
99
110
|
const query = {};
|
|
100
111
|
if (args.format)
|
|
101
112
|
query.format = args.format;
|
|
@@ -119,12 +130,12 @@ registerTool({
|
|
|
119
130
|
inputSchema: {
|
|
120
131
|
type: "object",
|
|
121
132
|
properties: {
|
|
122
|
-
person_a_datetime: { type: "string", description:
|
|
123
|
-
person_a_timezone:
|
|
133
|
+
person_a_datetime: { type: "string", description: DATETIME_DESC },
|
|
134
|
+
person_a_timezone: timezoneProperty("Person A's birth location", "America/Denver"),
|
|
124
135
|
person_a_latitude: { type: "number", description: "Person A birth latitude." },
|
|
125
136
|
person_a_longitude: { type: "number", description: "Person A birth longitude." },
|
|
126
|
-
person_b_datetime: { type: "string", description:
|
|
127
|
-
person_b_timezone:
|
|
137
|
+
person_b_datetime: { type: "string", description: DATETIME_DESC },
|
|
138
|
+
person_b_timezone: timezoneProperty("Person B's birth location", "Europe/London"),
|
|
128
139
|
person_b_latitude: { type: "number", description: "Person B birth latitude." },
|
|
129
140
|
person_b_longitude: { type: "number", description: "Person B birth longitude." },
|
|
130
141
|
format: { type: "string", enum: ["json", "llm"], description: "Use 'llm' for token-efficient LLM projection." },
|
|
@@ -142,6 +153,8 @@ registerTool({
|
|
|
142
153
|
"person_a_datetime", "person_a_latitude", "person_a_longitude",
|
|
143
154
|
"person_b_datetime", "person_b_latitude", "person_b_longitude",
|
|
144
155
|
]);
|
|
156
|
+
assertPersonZoned("person_a", args);
|
|
157
|
+
assertPersonZoned("person_b", args);
|
|
145
158
|
const query = {};
|
|
146
159
|
if (args.format)
|
|
147
160
|
query.format = args.format;
|
|
@@ -163,16 +176,17 @@ registerTool({
|
|
|
163
176
|
"are aspecting natal positions. Essential for predictive astrology.\n\n" +
|
|
164
177
|
"CREDIT COST: 3 credits per call.\n\n" +
|
|
165
178
|
"EXAMPLE: Current transits to someone born 1990-04-15:\n" +
|
|
166
|
-
" natal_datetime='1990-04-15T14:30:00',
|
|
179
|
+
" natal_datetime='1990-04-15T14:30:00', natal_timezone='America/Chicago',\n" +
|
|
180
|
+
" natal_latitude=41.88, natal_longitude=-87.63",
|
|
167
181
|
inputSchema: {
|
|
168
182
|
type: "object",
|
|
169
183
|
properties: {
|
|
170
|
-
natal_datetime: { type: "string", description:
|
|
171
|
-
natal_timezone:
|
|
184
|
+
natal_datetime: { type: "string", description: DATETIME_DESC },
|
|
185
|
+
natal_timezone: timezoneProperty("the natal birth location", "America/Denver"),
|
|
172
186
|
natal_latitude: { type: "number", description: "Natal birth latitude." },
|
|
173
187
|
natal_longitude: { type: "number", description: "Natal birth longitude." },
|
|
174
|
-
transit_datetime: { type: "string", description: "Transit moment
|
|
175
|
-
transit_timezone:
|
|
188
|
+
transit_datetime: { type: "string", description: "Transit moment. Defaults to now. " + DATETIME_DESC },
|
|
189
|
+
transit_timezone: timezoneProperty("the transit moment", "America/Denver"),
|
|
176
190
|
format: { type: "string", enum: ["json", "llm"], description: "Use 'llm' for token-efficient LLM projection." },
|
|
177
191
|
},
|
|
178
192
|
required: ["natal_datetime", "natal_latitude", "natal_longitude"],
|
|
@@ -182,14 +196,19 @@ registerTool({
|
|
|
182
196
|
annotations: { title: "Transits to Natal Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
183
197
|
handler: async (args) => {
|
|
184
198
|
validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude"]);
|
|
199
|
+
assertPersonZoned("natal", args);
|
|
185
200
|
// Backend expects: { subject: {...}, transit_datetime?: {...} }
|
|
186
201
|
// NOT a subjects[] array — natal-transits is a single-subject endpoint
|
|
187
202
|
const body = {
|
|
188
203
|
subject: buildSubject("Natal", args.natal_datetime, args.natal_latitude, args.natal_longitude, args.natal_timezone),
|
|
189
204
|
};
|
|
190
205
|
if (args.transit_datetime) {
|
|
191
|
-
// transit_datetime is a DateTimeInput on the backend
|
|
192
|
-
|
|
206
|
+
// transit_datetime is a DateTimeInput on the backend, and unlike the
|
|
207
|
+
// subject it has no timezone companion — so , which
|
|
208
|
+
// was declared but never read, is resolved here.
|
|
209
|
+
body.transit_datetime = {
|
|
210
|
+
iso: localToUtcIso("transit_datetime", args.transit_datetime, args.transit_timezone, "transit_timezone"),
|
|
211
|
+
};
|
|
193
212
|
}
|
|
194
213
|
const query = {};
|
|
195
214
|
if (args.format)
|
|
@@ -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 { WINDOW_DATE_DESC } from "../datetime.js";
|
|
4
5
|
// All electional endpoints are GET endpoints with query params.
|
|
5
6
|
// GET /electional/find-window — already existed, keeping it
|
|
6
7
|
registerTool({
|
|
@@ -17,15 +18,15 @@ registerTool({
|
|
|
17
18
|
properties: {
|
|
18
19
|
start_date: {
|
|
19
20
|
type: "string",
|
|
20
|
-
description: "
|
|
21
|
+
description: "Start of the search window, e.g. '2026-03-01'. " + WINDOW_DATE_DESC,
|
|
21
22
|
},
|
|
22
23
|
end_date: {
|
|
23
24
|
type: "string",
|
|
24
|
-
description: "
|
|
25
|
+
description: "End of the search window. " + WINDOW_DATE_DESC,
|
|
25
26
|
},
|
|
26
27
|
latitude: {
|
|
27
28
|
type: "number",
|
|
28
|
-
description: "Latitude of location in decimal degrees (positive = North).",
|
|
29
|
+
description: "Latitude of location in decimal degrees (positive = North). Resolve from a place name with location_search; never recall coordinates from memory.",
|
|
29
30
|
},
|
|
30
31
|
longitude: {
|
|
31
32
|
type: "number",
|
|
@@ -89,15 +90,15 @@ registerTool({
|
|
|
89
90
|
description: "Analyze the astrological quality of a specific moment: planet positions, aspects, " +
|
|
90
91
|
"void of course status, lunar phase, day ruler, and an overall electional score (0-100). " +
|
|
91
92
|
"Perfect for evaluating whether 'right now' or a specific date/time is good for action.\n\n" +
|
|
92
|
-
"CREDIT COST:
|
|
93
|
+
"CREDIT COST: 5 credits per call.\n\n" +
|
|
93
94
|
"EXAMPLE: Analyze March 21, 2026 at noon:\n" +
|
|
94
|
-
" date='2026-03-21T12:00:
|
|
95
|
+
" date='2026-03-21T12:00:00Z'",
|
|
95
96
|
inputSchema: {
|
|
96
97
|
type: "object",
|
|
97
98
|
properties: {
|
|
98
99
|
date: {
|
|
99
100
|
type: "string",
|
|
100
|
-
description: "ISO 8601 datetime to analyze (e.g., '2026-03-21T12:00:00'). Defaults to now.",
|
|
101
|
+
description: "ISO 8601 datetime to analyze, with a zone (e.g., '2026-03-21T12:00:00Z' or '2026-03-21T08:00:00-04:00'). Defaults to now.",
|
|
101
102
|
},
|
|
102
103
|
format: {
|
|
103
104
|
type: "string",
|
|
@@ -128,8 +129,10 @@ registerTool({
|
|
|
128
129
|
"Returns exact station times, longitudes, and signs.\n\n" +
|
|
129
130
|
"USE THIS TOOL FOR: 'When does Mercury go retrograde?', 'Is Venus retrograde this year?', " +
|
|
130
131
|
"'What planets station this month?', 'When does Mars go direct?'\n\n" +
|
|
132
|
+
"❌ NOT FOR: 'Is Mercury retrograde right now?' — that is the state at a single instant, " +
|
|
133
|
+
"so use ephemeris_retrograde_status (1 credit for one planet, vs 5 here).\n\n" +
|
|
131
134
|
"All required fields have smart defaults (searches the next 90 days from today).\n\n" +
|
|
132
|
-
"CREDIT COST:
|
|
135
|
+
"CREDIT COST: 5 credits per call.\n\n" +
|
|
133
136
|
"EXAMPLE: Mercury and Venus stations in the next 3 months (all defaults):\n" +
|
|
134
137
|
" (no args required, will auto-scan next 90 days for all inner planets)\n\n" +
|
|
135
138
|
"EXAMPLE: Outer planet stations in 2026:\n" +
|
|
@@ -198,15 +201,15 @@ registerTool({
|
|
|
198
201
|
description: "Find all active aspects between planets at a specific moment. Returns aspect type, " +
|
|
199
202
|
"orb, quality score, and whether it's applying or separating. Great for checking " +
|
|
200
203
|
"the 'weather' of a given day.\n\n" +
|
|
201
|
-
"CREDIT COST:
|
|
204
|
+
"CREDIT COST: 5 credits per call.\n\n" +
|
|
202
205
|
"EXAMPLE: What aspects are active on March 21, 2026?\n" +
|
|
203
|
-
" date='2026-03-21T12:00:
|
|
206
|
+
" date='2026-03-21T12:00:00Z'",
|
|
204
207
|
inputSchema: {
|
|
205
208
|
type: "object",
|
|
206
209
|
properties: {
|
|
207
210
|
date: {
|
|
208
211
|
type: "string",
|
|
209
|
-
description: "
|
|
212
|
+
description: "Datetime to check. Defaults to now. " + WINDOW_DATE_DESC,
|
|
210
213
|
},
|
|
211
214
|
max_orb: {
|
|
212
215
|
type: "number",
|
|
@@ -1,14 +1,17 @@
|
|
|
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
5
|
// POST /ephemeris/planet-position — OE-016
|
|
5
6
|
registerTool({
|
|
6
7
|
name: "ephemeris_planet_position",
|
|
7
8
|
description: "Get the precise ecliptic longitude, latitude, distance, speed, and retrograde status " +
|
|
8
9
|
"for a single planet/body at a given date and time. " +
|
|
9
10
|
"Planet IDs: 0=Sun, 1=Moon, 2=Mercury, 3=Venus, 4=Mars, 5=Jupiter, 6=Saturn, " +
|
|
10
|
-
"7=Uranus, 8=Neptune, 9=Pluto, 10=North Node, 11=
|
|
11
|
-
"15=Chiron, 17=Ceres, 18=Pallas, 19=Juno, 20=Vesta.\n\n" +
|
|
11
|
+
"7=Uranus, 8=Neptune, 9=Pluto, 10=North Node (Mean), 11=North Node (True), " +
|
|
12
|
+
"12=Lilith (Mean), 15=Chiron, 17=Ceres, 18=Pallas, 19=Juno, 20=Vesta.\n\n" +
|
|
13
|
+
"SOUTH NODE: there is no South Node id. The South Node is always exactly " +
|
|
14
|
+
"opposite the North Node — request id 10 or 11 and add 180° (mod 360).\n\n" +
|
|
12
15
|
"CREDIT COST: 1 credit per call.\n\n" +
|
|
13
16
|
"EXAMPLE: Where is Mars on 2026-03-20 at noon UTC?\n" +
|
|
14
17
|
" planet_id=4, datetime='2026-03-20T12:00:00Z'",
|
|
@@ -17,12 +20,13 @@ registerTool({
|
|
|
17
20
|
properties: {
|
|
18
21
|
planet_id: {
|
|
19
22
|
type: "integer",
|
|
20
|
-
description: "Planet/body ID (0=Sun, 1=Moon, 2=Mercury, 3=Venus, 4=Mars, 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune, 9=Pluto, 10=
|
|
23
|
+
description: "Planet/body ID (0=Sun, 1=Moon, 2=Mercury, 3=Venus, 4=Mars, 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune, 9=Pluto, 10=North Node (Mean), 11=North Node (True), 12=Lilith (Mean), 15=Chiron, 17=Ceres, 18=Pallas, 19=Juno, 20=Vesta). There is no South Node id — take the North Node and add 180°.",
|
|
21
24
|
},
|
|
22
25
|
datetime: {
|
|
23
26
|
type: "string",
|
|
24
|
-
description:
|
|
27
|
+
description: DATETIME_DESC,
|
|
25
28
|
},
|
|
29
|
+
timezone: TIMEZONE_PROPERTY,
|
|
26
30
|
latitude: {
|
|
27
31
|
type: "number",
|
|
28
32
|
description: "Observer latitude for topocentric position (optional). Omit for geocentric.",
|
|
@@ -41,7 +45,7 @@ registerTool({
|
|
|
41
45
|
validateRequired(args, ["planet_id", "datetime"]);
|
|
42
46
|
const body = {
|
|
43
47
|
planet_id: args.planet_id,
|
|
44
|
-
date_time: { iso: args.datetime },
|
|
48
|
+
date_time: { iso: localToUtcIso("datetime", args.datetime, args.timezone) },
|
|
45
49
|
};
|
|
46
50
|
if (args.latitude != null)
|
|
47
51
|
body.latitude = args.latitude;
|
|
@@ -64,11 +68,12 @@ registerTool({
|
|
|
64
68
|
properties: {
|
|
65
69
|
datetime: {
|
|
66
70
|
type: "string",
|
|
67
|
-
description:
|
|
71
|
+
description: DATETIME_DESC,
|
|
68
72
|
},
|
|
73
|
+
timezone: TIMEZONE_PROPERTY,
|
|
69
74
|
latitude: {
|
|
70
75
|
type: "number",
|
|
71
|
-
description: "Observer latitude in decimal degrees.",
|
|
76
|
+
description: "Observer latitude in decimal degrees. Resolve from a place name with location_search; never recall coordinates from memory.",
|
|
72
77
|
},
|
|
73
78
|
longitude: {
|
|
74
79
|
type: "number",
|
|
@@ -88,7 +93,7 @@ registerTool({
|
|
|
88
93
|
handler: async (args) => {
|
|
89
94
|
validateRequired(args, ["datetime", "latitude", "longitude"]);
|
|
90
95
|
const body = {
|
|
91
|
-
date_time: { iso: args.datetime },
|
|
96
|
+
date_time: { iso: localToUtcIso("datetime", args.datetime, args.timezone) },
|
|
92
97
|
latitude: args.latitude,
|
|
93
98
|
longitude: args.longitude,
|
|
94
99
|
};
|