@openephemeris/mcp-server 3.21.1 → 3.23.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 +41 -0
- package/LICENSE +21 -21
- package/README.md +2 -2
- package/dist/index.js +45 -0
- package/dist/server-sse.js +52 -3
- package/dist/tools/apps/bazi-app.d.ts +24 -0
- package/dist/tools/apps/bazi-app.js +231 -0
- package/dist/tools/apps/bi-wheel-app.js +3 -1
- package/dist/tools/apps/bodygraph-app.js +55 -10
- package/dist/tools/apps/moon-phase-app.js +141 -93
- package/dist/tools/apps/transit-timeline-app.d.ts +20 -0
- package/dist/tools/apps/transit-timeline-app.js +302 -0
- package/dist/tools/apps/vedic-chart-app.d.ts +25 -0
- package/dist/tools/apps/vedic-chart-app.js +297 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/specialized/bi_wheel.js +1 -0
- package/dist/tools/specialized/chart_wheel.js +1 -0
- package/dist/tools/specialized/hd_bodygraph.js +1 -0
- package/dist/tools/specialized/human_design.js +1 -0
- package/dist/tools/specialized/moon.js +1 -0
- package/dist/tools/specialized/natal.js +6 -1
- package/dist/ui/bazi.html +6928 -0
- package/dist/ui/bi-wheel.html +2091 -2031
- package/dist/ui/bodygraph.html +286 -221
- package/dist/ui/chart-wheel.html +552 -453
- package/dist/ui/moon-phase.html +310 -42
- package/dist/ui/transit-timeline.html +6911 -0
- package/dist/ui/vedic-chart.html +7043 -0
- package/package.json +2 -2
|
@@ -27,6 +27,8 @@ import { getActiveClient } from "../../backend/client.js";
|
|
|
27
27
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
28
28
|
// ── Constants ─────────────────────────────────────────────────────────────
|
|
29
29
|
export const BODYGRAPH_RESOURCE_URI = "ui://openephemeris/bodygraph";
|
|
30
|
+
// Appended to every model-visible HD tool description — trademark hygiene.
|
|
31
|
+
const HD_DISCLAIMER = " Human Design was originated by Ra Uru Hu; OpenEphemeris is an independent calculation service, not affiliated with Jovian Archive.";
|
|
30
32
|
export const BODYGRAPH_MIME_TYPE = "text/html;profile=mcp-app";
|
|
31
33
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
32
34
|
const BUNDLE_PATHS = [
|
|
@@ -346,11 +348,13 @@ function buildHdModelPayload(data, birthParams) {
|
|
|
346
348
|
function buildHdSummary(payload, location) {
|
|
347
349
|
const definedList = payload.centers.filter((c) => c.defined).map((c) => c.name);
|
|
348
350
|
const undefinedList = payload.centers.filter((c) => !c.defined).map((c) => c.name);
|
|
351
|
+
const channelList = payload.active_channels.length > 0 ? payload.active_channels.join(", ") : "None";
|
|
349
352
|
return (`**Human Design Chart \u2014 ${location}**\n\n` +
|
|
350
353
|
`Type: **${payload.type}** | Profile: **${payload.profile}** | Authority: **${payload.authority}**\n` +
|
|
351
354
|
`Definition: ${payload.definition} | Cross: ${payload.incarnation_cross}\n\n` +
|
|
352
355
|
`Defined Centers: ${definedList.length > 0 ? definedList.join(", ") : "None"}\n` +
|
|
353
|
-
`Undefined Centers: ${undefinedList.length > 0 ? undefinedList.join(", ") : "None"}\n
|
|
356
|
+
`Undefined Centers: ${undefinedList.length > 0 ? undefinedList.join(", ") : "None"}\n` +
|
|
357
|
+
`Channels (${payload.active_channels.length}): ${channelList}\n\n` +
|
|
354
358
|
"Click any center, gate, or channel on the bodygraph for interpretation.");
|
|
355
359
|
}
|
|
356
360
|
// ── Tool: explore_human_design ────────────────────────────────────────────
|
|
@@ -363,8 +367,10 @@ registerTool({
|
|
|
363
367
|
"Shows defined/undefined centers, active gates, channels, Type, Profile, Authority, " +
|
|
364
368
|
"and Incarnation Cross. " +
|
|
365
369
|
"The chart is calculated using NASA JPL DE440 ephemerides for both Personality and Design positions. " +
|
|
366
|
-
"Use this for a
|
|
367
|
-
"
|
|
370
|
+
"Use this instead of human_design_chart or human_design_bodygraph for a richer, interactive HD " +
|
|
371
|
+
"experience in MCP Apps-capable hosts (Claude Desktop). " +
|
|
372
|
+
"Falls back to a text summary in other hosts." +
|
|
373
|
+
HD_DISCLAIMER,
|
|
368
374
|
inputSchema: {
|
|
369
375
|
type: "object",
|
|
370
376
|
properties: {
|
|
@@ -1331,7 +1337,8 @@ registerTool({
|
|
|
1331
1337
|
"Highlights the channels a transit temporarily COMPLETES with the natal chart and any centers it " +
|
|
1332
1338
|
"newly defines — the core of a Human Design transit reading. " +
|
|
1333
1339
|
"Returns an interactive overlay bodygraph in MCP Apps-capable hosts (Claude Desktop), with a text " +
|
|
1334
|
-
"summary fallback elsewhere. Premium (Pro tier). Calculated with NASA JPL DE440 ephemerides."
|
|
1340
|
+
"summary fallback elsewhere. Premium (Pro tier). Calculated with NASA JPL DE440 ephemerides." +
|
|
1341
|
+
HD_DISCLAIMER,
|
|
1335
1342
|
inputSchema: {
|
|
1336
1343
|
type: "object",
|
|
1337
1344
|
properties: {
|
|
@@ -1348,6 +1355,12 @@ registerTool({
|
|
|
1348
1355
|
type: "string",
|
|
1349
1356
|
description: "Transit moment, ISO 8601. Defaults to now (UTC) when omitted.",
|
|
1350
1357
|
},
|
|
1358
|
+
theme: {
|
|
1359
|
+
type: "string",
|
|
1360
|
+
enum: ["light", "dark"],
|
|
1361
|
+
description: "Visual theme for the overlay bodygraph. Set automatically by the embedded app to match " +
|
|
1362
|
+
"the host; defaults to dark. You normally never need to pass this.",
|
|
1363
|
+
},
|
|
1351
1364
|
},
|
|
1352
1365
|
required: ["datetime"],
|
|
1353
1366
|
},
|
|
@@ -1384,8 +1397,14 @@ registerTool({
|
|
|
1384
1397
|
body.transit_datetime = { iso: ensureTimezone(String(args.transit_datetime)) };
|
|
1385
1398
|
}
|
|
1386
1399
|
const bundleAvailable = Boolean(getBodygraphBundle());
|
|
1387
|
-
|
|
1400
|
+
// The Go endpoint defaults visual_config.theme to "light", which mismatches
|
|
1401
|
+
// dark MCP hosts — mirror the natal path's explicit dark default. The
|
|
1402
|
+
// iframe re-calls this tool with theme once it detects the host theme.
|
|
1403
|
+
const theme = args.theme === "light" ? "light" : "dark";
|
|
1404
|
+
if (bundleAvailable) {
|
|
1388
1405
|
body.include_visual = true;
|
|
1406
|
+
body.visual_config = { theme };
|
|
1407
|
+
}
|
|
1389
1408
|
const resp = await client.request("POST", "/human-design/transit-chart", { data: body });
|
|
1390
1409
|
const transit = (resp?.transit ?? {});
|
|
1391
1410
|
const completed = Array.isArray(transit.completed_channels) ? transit.completed_channels : [];
|
|
@@ -1400,7 +1419,14 @@ registerTool({
|
|
|
1400
1419
|
(newCenters.length
|
|
1401
1420
|
? `\n\nNewly-defined center${newCenters.length === 1 ? "" : "s"}: **${newCenters.join(", ")}** — a temporary shift while the transit holds.`
|
|
1402
1421
|
: "");
|
|
1403
|
-
const payload = {
|
|
1422
|
+
const payload = {
|
|
1423
|
+
...resp,
|
|
1424
|
+
_svg: svg,
|
|
1425
|
+
_theme: theme,
|
|
1426
|
+
// Lets the iframe re-request this overlay in the host's theme.
|
|
1427
|
+
_refetch: { tool: "explore_human_design_transit", args: { ...args } },
|
|
1428
|
+
server_version: SERVER_VERSION,
|
|
1429
|
+
};
|
|
1404
1430
|
if (bundleAvailable && svg) {
|
|
1405
1431
|
return {
|
|
1406
1432
|
content: [{ type: "text", text: summary }],
|
|
@@ -1417,10 +1443,13 @@ registerTool({
|
|
|
1417
1443
|
registerTool({
|
|
1418
1444
|
name: "explore_human_design_connection",
|
|
1419
1445
|
description: "Compare two people's Human Design charts and classify every connected channel by HD connection " +
|
|
1420
|
-
"theory
|
|
1446
|
+
"theory.\n\n" +
|
|
1447
|
+
"CREDIT COST: 3 credits per call.\n\n" +
|
|
1448
|
+
"Classifications: " +
|
|
1421
1449
|
"electromagnetic (attraction), companionship (sameness), dominance (one defines), and " +
|
|
1422
1450
|
"compromise (friction). Returns an interactive two-person overlay bodygraph in MCP Apps-capable " +
|
|
1423
|
-
"hosts, with a text summary fallback elsewhere. Premium (Pro tier). NASA JPL DE440 ephemerides."
|
|
1451
|
+
"hosts, with a text summary fallback elsewhere. Premium (Pro tier). NASA JPL DE440 ephemerides." +
|
|
1452
|
+
HD_DISCLAIMER,
|
|
1424
1453
|
inputSchema: {
|
|
1425
1454
|
type: "object",
|
|
1426
1455
|
properties: {
|
|
@@ -1446,6 +1475,12 @@ registerTool({
|
|
|
1446
1475
|
},
|
|
1447
1476
|
required: ["datetime"],
|
|
1448
1477
|
},
|
|
1478
|
+
theme: {
|
|
1479
|
+
type: "string",
|
|
1480
|
+
enum: ["light", "dark"],
|
|
1481
|
+
description: "Visual theme for the overlay bodygraph. Set automatically by the embedded app to match " +
|
|
1482
|
+
"the host; defaults to dark. You normally never need to pass this.",
|
|
1483
|
+
},
|
|
1449
1484
|
},
|
|
1450
1485
|
required: ["person_a", "person_b"],
|
|
1451
1486
|
},
|
|
@@ -1473,8 +1508,12 @@ registerTool({
|
|
|
1473
1508
|
subject_2: toSubject(args.person_b),
|
|
1474
1509
|
};
|
|
1475
1510
|
const bundleAvailable = Boolean(getBodygraphBundle());
|
|
1476
|
-
|
|
1511
|
+
// Mirror the transit tool: explicit dark default, host-theme refetch.
|
|
1512
|
+
const theme = args.theme === "light" ? "light" : "dark";
|
|
1513
|
+
if (bundleAvailable) {
|
|
1477
1514
|
body.include_visual = true;
|
|
1515
|
+
body.visual_config = { theme };
|
|
1516
|
+
}
|
|
1478
1517
|
const resp = await client.request("POST", "/human-design/composite", { data: body });
|
|
1479
1518
|
const connections = Array.isArray(resp?.connections) ? resp.connections : [];
|
|
1480
1519
|
const svg = resp?.visual?.data;
|
|
@@ -1490,7 +1529,13 @@ registerTool({
|
|
|
1490
1529
|
line("Dominance — one defines", "dominance") +
|
|
1491
1530
|
line("Compromise — friction", "compromise") +
|
|
1492
1531
|
(connections.length ? "" : " none.");
|
|
1493
|
-
const payload = {
|
|
1532
|
+
const payload = {
|
|
1533
|
+
...resp,
|
|
1534
|
+
_svg: svg,
|
|
1535
|
+
_theme: theme,
|
|
1536
|
+
_refetch: { tool: "explore_human_design_connection", args: { ...args } },
|
|
1537
|
+
server_version: SERVER_VERSION,
|
|
1538
|
+
};
|
|
1494
1539
|
if (bundleAvailable && svg) {
|
|
1495
1540
|
return {
|
|
1496
1541
|
content: [{ type: "text", text: summary }],
|
|
@@ -86,6 +86,106 @@ function buildMoonSummary(data) {
|
|
|
86
86
|
summary += "\nClick cards on the dial for details and interpretation.";
|
|
87
87
|
return summary;
|
|
88
88
|
}
|
|
89
|
+
// ── Shared payload builder ────────────────────────────────────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Fetch phase + void-of-course + aspects for the given args and build the
|
|
92
|
+
* flattened UI payload and human summary. Shared by explore_moon_phase and
|
|
93
|
+
* moon_phase_recalculate so both return an identical structuredContent shape.
|
|
94
|
+
*/
|
|
95
|
+
async function computeMoonData(args) {
|
|
96
|
+
validateCoordinates(args, "latitude", "longitude");
|
|
97
|
+
const client = getActiveClient();
|
|
98
|
+
const params = {};
|
|
99
|
+
params.datetime = args.datetime ?? new Date().toISOString();
|
|
100
|
+
if (args.latitude != null)
|
|
101
|
+
params.latitude = args.latitude;
|
|
102
|
+
if (args.longitude != null)
|
|
103
|
+
params.longitude = args.longitude;
|
|
104
|
+
// Fetch phase, VOC, and moon aspects in parallel
|
|
105
|
+
const [phase, voc, aspects] = await Promise.allSettled([
|
|
106
|
+
client.request("GET", "/ephemeris/moon/phase", { params }),
|
|
107
|
+
client.request("GET", "/ephemeris/moon/void-of-course", { params }),
|
|
108
|
+
client.request("GET", "/ephemeris/moon/aspects", { params }),
|
|
109
|
+
]);
|
|
110
|
+
const mergedData = {
|
|
111
|
+
phase: phase.status === "fulfilled" ? phase.value : { error: phase.reason?.message },
|
|
112
|
+
void_of_course: voc.status === "fulfilled" ? voc.value : { error: voc.reason?.message },
|
|
113
|
+
aspects: aspects.status === "fulfilled" ? aspects.value : null,
|
|
114
|
+
};
|
|
115
|
+
// Flatten for the UI — merge phase fields to top level
|
|
116
|
+
const phaseData = mergedData.phase;
|
|
117
|
+
const vocData = mergedData.void_of_course;
|
|
118
|
+
const aspectsData = mergedData.aspects;
|
|
119
|
+
// Extract enriched metadata fields from the GET response
|
|
120
|
+
const meta = (phaseData?.metadata ?? {});
|
|
121
|
+
const moonSign = String(meta?.moon_sign ?? phaseData?.moon_sign ?? phaseData?.sign ?? vocData?.current_sign ?? "");
|
|
122
|
+
const moonSpeedRaw = meta?.longitude_speed;
|
|
123
|
+
const moonSpeed = typeof moonSpeedRaw === "number" ? moonSpeedRaw : null;
|
|
124
|
+
const angularDiameter = typeof meta?.angular_diameter === "number" ? meta.angular_diameter : null;
|
|
125
|
+
const distanceKm = typeof meta?.moon_distance_km === "number" ? meta.moon_distance_km : null;
|
|
126
|
+
const moonLat = typeof meta?.moon_lat === "number" ? meta.moon_lat : null;
|
|
127
|
+
const ageDays = typeof meta?.age_days === "number" ? meta.age_days :
|
|
128
|
+
(typeof phaseData?.age_days === "number" ? phaseData.age_days : null);
|
|
129
|
+
// days_to_new / days_to_full can come as the union value or from the top-level field
|
|
130
|
+
const extractDays = (field) => {
|
|
131
|
+
if (typeof field === "number")
|
|
132
|
+
return field;
|
|
133
|
+
if (field && typeof field === "object") {
|
|
134
|
+
const v = Object.values(field)[0];
|
|
135
|
+
if (typeof v === "number")
|
|
136
|
+
return v;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
};
|
|
140
|
+
const daysToNew = extractDays(phaseData?.days_to_new);
|
|
141
|
+
const daysToFull = extractDays(phaseData?.days_to_full);
|
|
142
|
+
// Top 3 applying aspects by tightest orb
|
|
143
|
+
const rawAspects = (aspectsData?.aspects ?? aspectsData?.data ?? []);
|
|
144
|
+
const applyingAspects = Array.isArray(rawAspects)
|
|
145
|
+
? rawAspects
|
|
146
|
+
.filter((a) => a.applying === true || a.type === "applying")
|
|
147
|
+
.sort((a, b) => Math.abs(a.orb ?? 99) - Math.abs(b.orb ?? 99))
|
|
148
|
+
.slice(0, 3)
|
|
149
|
+
.map((a) => ({
|
|
150
|
+
planet: a.planet ?? a.body ?? a.planet_name ?? "",
|
|
151
|
+
aspect: a.aspect ?? a.aspect_name ?? a.type_name ?? "",
|
|
152
|
+
orb: typeof a.orb === "number" ? Math.abs(a.orb) : null,
|
|
153
|
+
}))
|
|
154
|
+
: [];
|
|
155
|
+
const uiPayload = {
|
|
156
|
+
phase_name: String(phaseData?.phase_name ?? phaseData?.name ?? "Unknown").replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()),
|
|
157
|
+
illumination: typeof phaseData?.illumination === "number" ? phaseData.illumination / 100.0 : 0,
|
|
158
|
+
phase_angle: typeof phaseData?.phase_angle === "number" ? phaseData.phase_angle : null,
|
|
159
|
+
age_days: ageDays,
|
|
160
|
+
days_to_new: daysToNew,
|
|
161
|
+
days_to_full: daysToFull,
|
|
162
|
+
waxing: phaseData?.waxing ??
|
|
163
|
+
// Fallback chain: elongation < 180° = waxing, else age_days < 14.75 = waxing.
|
|
164
|
+
(typeof phaseData?.phase_angle === "number"
|
|
165
|
+
? phaseData.phase_angle < 180
|
|
166
|
+
: typeof ageDays === "number"
|
|
167
|
+
? ageDays < 14.75
|
|
168
|
+
: true), // safe default: assume waxing
|
|
169
|
+
moon_sign: moonSign,
|
|
170
|
+
moon_longitude: meta?.moon_lon ?? phaseData?.moon_longitude ?? phaseData?.longitude ?? null,
|
|
171
|
+
moon_latitude: moonLat,
|
|
172
|
+
moon_distance_km: distanceKm,
|
|
173
|
+
angular_diameter: angularDiameter,
|
|
174
|
+
moon_speed: moonSpeed,
|
|
175
|
+
sun_longitude: meta?.sun_lon ?? phaseData?.sun_longitude ?? null,
|
|
176
|
+
applying_aspects: applyingAspects,
|
|
177
|
+
voc_status: {
|
|
178
|
+
is_void: Boolean(vocData?.is_void ?? vocData?.is_void_of_course ?? false),
|
|
179
|
+
started: vocData?.started ?? vocData?.start ?? null,
|
|
180
|
+
ends: vocData?.ends ?? vocData?.end ?? null,
|
|
181
|
+
next_sign: vocData?.next_sign ?? null,
|
|
182
|
+
},
|
|
183
|
+
datetime: params.datetime,
|
|
184
|
+
server_version: SERVER_VERSION,
|
|
185
|
+
};
|
|
186
|
+
const summary = buildMoonSummary(mergedData);
|
|
187
|
+
return { summary, uiPayload };
|
|
188
|
+
}
|
|
89
189
|
// ── Tool: explore_moon_phase ─────────────────────────────────────────────────
|
|
90
190
|
registerTool({
|
|
91
191
|
name: "explore_moon_phase",
|
|
@@ -99,8 +199,8 @@ registerTool({
|
|
|
99
199
|
" • Lunar age (days in the synodic cycle)\n" +
|
|
100
200
|
" • Upcoming New Moon and Full Moon dates\n\n" +
|
|
101
201
|
"CREDIT COST: 3 credits per call (phase + void-of-course + aspects, 1 each).\n\n" +
|
|
102
|
-
"Use this for a rich, interactive lunar phase experience in
|
|
103
|
-
"Falls back to a text summary in other hosts.",
|
|
202
|
+
"Use this instead of ephemeris_moon_phase for a rich, interactive lunar phase experience in " +
|
|
203
|
+
"MCP Apps-capable hosts (Claude Desktop). Falls back to a text summary in other hosts.",
|
|
104
204
|
inputSchema: {
|
|
105
205
|
type: "object",
|
|
106
206
|
properties: {
|
|
@@ -135,97 +235,7 @@ registerTool({
|
|
|
135
235
|
},
|
|
136
236
|
},
|
|
137
237
|
handler: async (args) => {
|
|
138
|
-
|
|
139
|
-
const client = getActiveClient();
|
|
140
|
-
const params = {};
|
|
141
|
-
params.datetime = args.datetime ?? new Date().toISOString();
|
|
142
|
-
if (args.latitude != null)
|
|
143
|
-
params.latitude = args.latitude;
|
|
144
|
-
if (args.longitude != null)
|
|
145
|
-
params.longitude = args.longitude;
|
|
146
|
-
// Fetch phase, VOC, and moon aspects in parallel
|
|
147
|
-
const [phase, voc, aspects] = await Promise.allSettled([
|
|
148
|
-
client.request("GET", "/ephemeris/moon/phase", { params }),
|
|
149
|
-
client.request("GET", "/ephemeris/moon/void-of-course", { params }),
|
|
150
|
-
client.request("GET", "/ephemeris/moon/aspects", { params }),
|
|
151
|
-
]);
|
|
152
|
-
const mergedData = {
|
|
153
|
-
phase: phase.status === "fulfilled" ? phase.value : { error: phase.reason?.message },
|
|
154
|
-
void_of_course: voc.status === "fulfilled" ? voc.value : { error: voc.reason?.message },
|
|
155
|
-
aspects: aspects.status === "fulfilled" ? aspects.value : null,
|
|
156
|
-
};
|
|
157
|
-
// Flatten for the UI — merge phase fields to top level
|
|
158
|
-
const phaseData = mergedData.phase;
|
|
159
|
-
const vocData = mergedData.void_of_course;
|
|
160
|
-
const aspectsData = mergedData.aspects;
|
|
161
|
-
// Extract enriched metadata fields from the GET response
|
|
162
|
-
const meta = (phaseData?.metadata ?? {});
|
|
163
|
-
const moonSign = String(meta?.moon_sign ?? phaseData?.moon_sign ?? phaseData?.sign ?? vocData?.current_sign ?? "");
|
|
164
|
-
const moonSpeedRaw = meta?.longitude_speed;
|
|
165
|
-
const moonSpeed = typeof moonSpeedRaw === "number" ? moonSpeedRaw : null;
|
|
166
|
-
const angularDiameter = typeof meta?.angular_diameter === "number" ? meta.angular_diameter : null;
|
|
167
|
-
const distanceKm = typeof meta?.moon_distance_km === "number" ? meta.moon_distance_km : null;
|
|
168
|
-
const moonLat = typeof meta?.moon_lat === "number" ? meta.moon_lat : null;
|
|
169
|
-
const ageDays = typeof meta?.age_days === "number" ? meta.age_days :
|
|
170
|
-
(typeof phaseData?.age_days === "number" ? phaseData.age_days : null);
|
|
171
|
-
// days_to_new / days_to_full can come as the union value or from the top-level field
|
|
172
|
-
const extractDays = (field) => {
|
|
173
|
-
if (typeof field === "number")
|
|
174
|
-
return field;
|
|
175
|
-
if (field && typeof field === "object") {
|
|
176
|
-
const v = Object.values(field)[0];
|
|
177
|
-
if (typeof v === "number")
|
|
178
|
-
return v;
|
|
179
|
-
}
|
|
180
|
-
return null;
|
|
181
|
-
};
|
|
182
|
-
const daysToNew = extractDays(phaseData?.days_to_new);
|
|
183
|
-
const daysToFull = extractDays(phaseData?.days_to_full);
|
|
184
|
-
// Top 3 applying aspects by tightest orb
|
|
185
|
-
const rawAspects = (aspectsData?.aspects ?? aspectsData?.data ?? []);
|
|
186
|
-
const applyingAspects = Array.isArray(rawAspects)
|
|
187
|
-
? rawAspects
|
|
188
|
-
.filter((a) => a.applying === true || a.type === "applying")
|
|
189
|
-
.sort((a, b) => Math.abs(a.orb ?? 99) - Math.abs(b.orb ?? 99))
|
|
190
|
-
.slice(0, 3)
|
|
191
|
-
.map((a) => ({
|
|
192
|
-
planet: a.planet ?? a.body ?? a.planet_name ?? "",
|
|
193
|
-
aspect: a.aspect ?? a.aspect_name ?? a.type_name ?? "",
|
|
194
|
-
orb: typeof a.orb === "number" ? Math.abs(a.orb) : null,
|
|
195
|
-
}))
|
|
196
|
-
: [];
|
|
197
|
-
const uiPayload = {
|
|
198
|
-
phase_name: String(phaseData?.phase_name ?? phaseData?.name ?? "Unknown").replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()),
|
|
199
|
-
illumination: typeof phaseData?.illumination === "number" ? phaseData.illumination / 100.0 : 0,
|
|
200
|
-
phase_angle: typeof phaseData?.phase_angle === "number" ? phaseData.phase_angle : null,
|
|
201
|
-
age_days: ageDays,
|
|
202
|
-
days_to_new: daysToNew,
|
|
203
|
-
days_to_full: daysToFull,
|
|
204
|
-
waxing: phaseData?.waxing ??
|
|
205
|
-
// Fallback chain: elongation < 180° = waxing, else age_days < 14.75 = waxing.
|
|
206
|
-
(typeof phaseData?.phase_angle === "number"
|
|
207
|
-
? phaseData.phase_angle < 180
|
|
208
|
-
: typeof ageDays === "number"
|
|
209
|
-
? ageDays < 14.75
|
|
210
|
-
: true), // safe default: assume waxing
|
|
211
|
-
moon_sign: moonSign,
|
|
212
|
-
moon_longitude: meta?.moon_lon ?? phaseData?.moon_longitude ?? phaseData?.longitude ?? null,
|
|
213
|
-
moon_latitude: moonLat,
|
|
214
|
-
moon_distance_km: distanceKm,
|
|
215
|
-
angular_diameter: angularDiameter,
|
|
216
|
-
moon_speed: moonSpeed,
|
|
217
|
-
sun_longitude: meta?.sun_lon ?? phaseData?.sun_longitude ?? null,
|
|
218
|
-
applying_aspects: applyingAspects,
|
|
219
|
-
voc_status: {
|
|
220
|
-
is_void: Boolean(vocData?.is_void ?? vocData?.is_void_of_course ?? false),
|
|
221
|
-
started: vocData?.started ?? vocData?.start ?? null,
|
|
222
|
-
ends: vocData?.ends ?? vocData?.end ?? null,
|
|
223
|
-
next_sign: vocData?.next_sign ?? null,
|
|
224
|
-
},
|
|
225
|
-
datetime: params.datetime,
|
|
226
|
-
server_version: SERVER_VERSION,
|
|
227
|
-
};
|
|
228
|
-
const summary = buildMoonSummary(mergedData);
|
|
238
|
+
const { summary, uiPayload } = await computeMoonData(args);
|
|
229
239
|
const bundleAvailable = Boolean(getMoonPhaseBundle());
|
|
230
240
|
if (bundleAvailable) {
|
|
231
241
|
// MCP Apps wire format: the UI is declared via `_meta.ui.resourceUri` and
|
|
@@ -249,3 +259,41 @@ registerTool({
|
|
|
249
259
|
return { content: [{ type: "text", text: summary }] };
|
|
250
260
|
},
|
|
251
261
|
});
|
|
262
|
+
// ── Tool: moon_phase_recalculate ─────────────────────────────────────────────
|
|
263
|
+
// App-only. Fired by the Moon Phase iframe's date control to recompute the dial
|
|
264
|
+
// for a chosen datetime. Mirrors bodygraph_recalculate: same endpoint set, same
|
|
265
|
+
// structuredContent shape as explore_moon_phase, returned directly to the iframe
|
|
266
|
+
// (ontoolresult does not fire for in-app tool calls).
|
|
267
|
+
registerTool({
|
|
268
|
+
name: "moon_phase_recalculate",
|
|
269
|
+
description: "Recalculates the Moon Phase dial for a new datetime.",
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: "object",
|
|
272
|
+
properties: {
|
|
273
|
+
datetime: {
|
|
274
|
+
type: "string",
|
|
275
|
+
description: "ISO 8601 datetime to query. If omitted, uses the current live moon phase (UTC now).",
|
|
276
|
+
},
|
|
277
|
+
latitude: {
|
|
278
|
+
type: "number",
|
|
279
|
+
description: "Observer latitude (optional, used for local void-of-course calculations).",
|
|
280
|
+
},
|
|
281
|
+
longitude: {
|
|
282
|
+
type: "number",
|
|
283
|
+
description: "Observer longitude (optional, used for local void-of-course calculations).",
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
required: [],
|
|
287
|
+
additionalProperties: false,
|
|
288
|
+
},
|
|
289
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
290
|
+
annotations: { title: "Recalculate Moon Phase", readOnlyHint: true, openWorldHint: false },
|
|
291
|
+
_meta: { ui: { resourceUri: MOON_PHASE_RESOURCE_URI, visibility: ["app"] } },
|
|
292
|
+
handler: async (args) => {
|
|
293
|
+
const { summary, uiPayload } = await computeMoonData(args);
|
|
294
|
+
return {
|
|
295
|
+
content: [{ type: "text", text: summary }],
|
|
296
|
+
structuredContent: uiPayload,
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transit-timeline-app.ts — MCP App tool registration for the Transit Timeline.
|
|
3
|
+
*
|
|
4
|
+
* Registers 1 tool:
|
|
5
|
+
* • explore_transit_timeline — primary entry point, returns data + UI resource.
|
|
6
|
+
*
|
|
7
|
+
* The tool mirrors the `ephemeris_transits` plumbing: it computes the natal
|
|
8
|
+
* chart to extract real planetary longitudes, searches predictive transits
|
|
9
|
+
* against those targets, then flattens the (planet@target → hits) result map
|
|
10
|
+
* into a single chronological array the iframe renders as a vertical timeline.
|
|
11
|
+
*
|
|
12
|
+
* Also exports resource helpers (getTransitTimelineBundle, etc.) for use in
|
|
13
|
+
* index.ts and server-sse.ts.
|
|
14
|
+
*/
|
|
15
|
+
export declare const TRANSIT_TIMELINE_RESOURCE_URI = "ui://openephemeris/transit-timeline";
|
|
16
|
+
export declare const TRANSIT_TIMELINE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
17
|
+
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
18
|
+
export declare function getTransitTimelineBundle(): string | null;
|
|
19
|
+
/** Reset cache (useful in dev watch mode or tests). */
|
|
20
|
+
export declare function clearTransitTimelineBundleCache(): void;
|