@openephemeris/mcp-server 3.13.10 → 3.14.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 +39 -0
- package/README.md +3 -3
- package/dist/index.js +5 -43
- package/dist/oauth/discovery.d.ts +1 -0
- package/dist/oauth/discovery.js +8 -3
- package/dist/oauth/store.d.ts +20 -0
- package/dist/oauth/store.js +117 -3
- package/dist/oauth/token.js +4 -2
- package/dist/prompts.js +9 -0
- package/dist/server-sse.js +71 -64
- package/dist/tools/apps/bazi-app.js +2 -0
- package/dist/tools/apps/bi-wheel-app.js +55 -6
- package/dist/tools/apps/bodygraph-app.js +68 -10
- package/dist/tools/apps/chart-wheel-app.js +53 -4
- package/dist/tools/apps/location-tools.js +3 -0
- package/dist/tools/apps/moon-phase-app.js +75 -21
- package/dist/tools/apps/transit-timeline-app.js +75 -23
- package/dist/tools/apps/vedic-chart-app.js +2 -0
- package/dist/tools/auth.js +4 -0
- package/dist/tools/dev.js +3 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.js +8 -7
- package/dist/tools/output-schemas.d.ts +37 -0
- package/dist/tools/output-schemas.js +130 -0
- package/dist/tools/specialized/acg.js +3 -0
- package/dist/tools/specialized/bazi.js +8 -0
- package/dist/tools/specialized/bi_wheel.js +2 -0
- package/dist/tools/specialized/chart_wheel.js +2 -0
- package/dist/tools/specialized/comparative.js +5 -0
- package/dist/tools/specialized/eclipse.js +2 -0
- package/dist/tools/specialized/electional.js +5 -0
- package/dist/tools/specialized/ephemeris_core.js +3 -0
- package/dist/tools/specialized/ephemeris_extended.js +9 -0
- package/dist/tools/specialized/hd_bodygraph.js +2 -0
- package/dist/tools/specialized/hd_cycles.js +3 -0
- package/dist/tools/specialized/hd_group.js +3 -0
- package/dist/tools/specialized/human_design.js +2 -0
- package/dist/tools/specialized/moon.js +3 -0
- package/dist/tools/specialized/natal.js +2 -0
- package/dist/tools/specialized/progressed.js +2 -0
- package/dist/tools/specialized/relocation.js +2 -0
- package/dist/tools/specialized/returns.js +4 -0
- package/dist/tools/specialized/synastry.js +2 -0
- package/dist/tools/specialized/transits.js +52 -43
- package/dist/tools/specialized/vedic.js +2 -0
- package/dist/tools/specialized/venus_star_points.js +7 -0
- package/dist/ui/bazi.html +1 -1
- package/dist/ui/bi-wheel.html +3514 -3121
- package/dist/ui/bodygraph.html +1308 -1261
- package/dist/ui/chart-wheel.html +3404 -3116
- package/dist/ui/moon-phase.html +2622 -2616
- package/dist/ui/transit-timeline.html +2695 -2656
- package/dist/ui/vedic-chart.html +1 -1
- package/package.json +14 -11
- package/smithery.yaml +16 -1
|
@@ -20,6 +20,7 @@ import path from "node:path";
|
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
22
22
|
import { getActiveClient } from "../../backend/client.js";
|
|
23
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
23
24
|
// ── Constants ─────────────────────────────────────────────────────────────
|
|
24
25
|
export const CHART_WHEEL_RESOURCE_URI = "ui://openephemeris/chart-wheel";
|
|
25
26
|
export const CHART_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -139,9 +140,18 @@ registerTool({
|
|
|
139
140
|
enum: ["placidus", "whole_sign", "equal", "koch", "regiomontanus", "campanus"],
|
|
140
141
|
description: "House system to use (default: placidus).",
|
|
141
142
|
},
|
|
143
|
+
bodies: {
|
|
144
|
+
type: "array",
|
|
145
|
+
items: { type: "string" },
|
|
146
|
+
description: "Optional list of body names to include. " +
|
|
147
|
+
"Defaults to 13 classical bodies (Sun through Pluto + Chiron + Nodes). " +
|
|
148
|
+
"Use 'all' as a single item to include every available body (Lilith, Ceres, Juno, Vesta, Pallas, Vertex, etc.). " +
|
|
149
|
+
"Example: ['sun','moon','lilith','vertex'].",
|
|
150
|
+
},
|
|
142
151
|
},
|
|
143
152
|
required: ["datetime", "latitude", "longitude"],
|
|
144
153
|
},
|
|
154
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
145
155
|
annotations: {
|
|
146
156
|
title: "Interactive Chart Wheel Explorer",
|
|
147
157
|
readOnlyHint: true,
|
|
@@ -162,6 +172,17 @@ registerTool({
|
|
|
162
172
|
const lat = args.latitude;
|
|
163
173
|
const lon = args.longitude;
|
|
164
174
|
const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
|
|
175
|
+
// Determine which bodies are requested
|
|
176
|
+
const requestedBodies = args.bodies;
|
|
177
|
+
const wantsAll = requestedBodies?.some(b => b.toLowerCase() === "all");
|
|
178
|
+
const wantsExtended = wantsAll || requestedBodies?.some(b => EXTENDED_BODIES.has(b.toLowerCase()));
|
|
179
|
+
// Enable extended body computation on the API side when needed
|
|
180
|
+
if (wantsExtended) {
|
|
181
|
+
const config = natalBody.configuration;
|
|
182
|
+
config.include_asteroids = true;
|
|
183
|
+
config.include_lilith = true;
|
|
184
|
+
config.include_chiron = true;
|
|
185
|
+
}
|
|
165
186
|
// Fetch natal chart JSON. The chart is rendered client-side in the UI iframe,
|
|
166
187
|
// so we do NOT call the /visualization/chart-wheel endpoint — that was the
|
|
167
188
|
// source of the ~90 second blocking delay.
|
|
@@ -175,7 +196,7 @@ registerTool({
|
|
|
175
196
|
latitude: lat ?? null,
|
|
176
197
|
longitude: lon ?? null,
|
|
177
198
|
location: args.location ?? null,
|
|
178
|
-
}, houseSystem);
|
|
199
|
+
}, houseSystem, undefined, requestedBodies);
|
|
179
200
|
const bundleAvailable = Boolean(getChartWheelBundle());
|
|
180
201
|
if (bundleAvailable) {
|
|
181
202
|
return {
|
|
@@ -216,6 +237,7 @@ registerTool({
|
|
|
216
237
|
},
|
|
217
238
|
required: ["planet", "longitude"],
|
|
218
239
|
},
|
|
240
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
219
241
|
annotations: { title: "Planet Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
220
242
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
221
243
|
handler: async (args) => {
|
|
@@ -250,6 +272,7 @@ registerTool({
|
|
|
250
272
|
},
|
|
251
273
|
required: ["house_number"],
|
|
252
274
|
},
|
|
275
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
253
276
|
annotations: { title: "House Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
254
277
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
255
278
|
handler: async (args) => {
|
|
@@ -285,6 +308,7 @@ registerTool({
|
|
|
285
308
|
},
|
|
286
309
|
required: ["planet1", "planet2"],
|
|
287
310
|
},
|
|
311
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
288
312
|
annotations: { title: "Aspect Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
289
313
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
290
314
|
handler: async (args) => {
|
|
@@ -333,6 +357,7 @@ registerTool({
|
|
|
333
357
|
},
|
|
334
358
|
required: ["datetime", "latitude", "longitude", "house_system"],
|
|
335
359
|
},
|
|
360
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
336
361
|
annotations: { title: "Recalculate Chart", readOnlyHint: true, openWorldHint: false },
|
|
337
362
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
338
363
|
handler: async (args) => {
|
|
@@ -391,6 +416,13 @@ const CLASSICAL_PLANETS = new Set([
|
|
|
391
416
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
392
417
|
"chiron", "north_node", "south_node", "true_node", "asc", "mc",
|
|
393
418
|
]);
|
|
419
|
+
const EXTENDED_BODIES = new Set([
|
|
420
|
+
"mean_lilith", "lilith", "true_lilith",
|
|
421
|
+
"ceres", "juno", "vesta", "pallas", "pholus",
|
|
422
|
+
"vertex", "part_of_fortune",
|
|
423
|
+
"eris", "sedna",
|
|
424
|
+
]);
|
|
425
|
+
const ALL_KNOWN_BODIES = new Set([...CLASSICAL_PLANETS, ...EXTENDED_BODIES]);
|
|
394
426
|
/**
|
|
395
427
|
* Build a lean, SVG-free payload for Claude's text context.
|
|
396
428
|
* Strips SVG, filters to classical planets only, and packages birth params.
|
|
@@ -422,7 +454,13 @@ function computeAspects(planets) {
|
|
|
422
454
|
diff = 360 - diff;
|
|
423
455
|
for (const asp of ASPECTS) {
|
|
424
456
|
const orb = Math.abs(diff - asp.angle);
|
|
425
|
-
|
|
457
|
+
// Tighten orbs when one or both bodies are extended (asteroids, etc.)
|
|
458
|
+
const p1Extended = EXTENDED_BODIES.has(p1.name.toLowerCase());
|
|
459
|
+
const p2Extended = EXTENDED_BODIES.has(p2.name.toLowerCase());
|
|
460
|
+
const effectiveOrb = (p1Extended || p2Extended)
|
|
461
|
+
? Math.min(asp.orb * 0.5, 3) // half standard, capped at 3°
|
|
462
|
+
: asp.orb;
|
|
463
|
+
if (orb <= effectiveOrb) {
|
|
426
464
|
// Applying: combined longitudinal speed closing the orb
|
|
427
465
|
const applying = ((p1.speed ?? 1) - (p2.speed ?? 0)) < 0 ? diff < asp.angle : diff > asp.angle;
|
|
428
466
|
results.push({
|
|
@@ -440,7 +478,7 @@ function computeAspects(planets) {
|
|
|
440
478
|
}
|
|
441
479
|
return results;
|
|
442
480
|
}
|
|
443
|
-
function buildModelPayload(chartData, birthParams, houseSystem, svgBase) {
|
|
481
|
+
function buildModelPayload(chartData, birthParams, houseSystem, svgBase, bodyFilter) {
|
|
444
482
|
// ── 1. Normalize planets: keyed object → typed PlanetData array ──────────
|
|
445
483
|
// The natal API returns { sun: { longitude, sign_name, is_retrograde, ... }, moon: {...} }.
|
|
446
484
|
// The chart wheel UI expects PlanetData[] with canonical field names.
|
|
@@ -448,7 +486,18 @@ function buildModelPayload(chartData, birthParams, houseSystem, svgBase) {
|
|
|
448
486
|
let planetsArray = [];
|
|
449
487
|
if (rawPlanets && typeof rawPlanets === "object" && !Array.isArray(rawPlanets)) {
|
|
450
488
|
planetsArray = Object.entries(rawPlanets)
|
|
451
|
-
.filter(([k]) =>
|
|
489
|
+
.filter(([k]) => {
|
|
490
|
+
const key = k.toLowerCase();
|
|
491
|
+
// If specific bodies were requested, use that filter
|
|
492
|
+
if (bodyFilter) {
|
|
493
|
+
const wantsAll = bodyFilter.some(b => b.toLowerCase() === "all");
|
|
494
|
+
if (wantsAll)
|
|
495
|
+
return ALL_KNOWN_BODIES.has(key);
|
|
496
|
+
return bodyFilter.some(b => b.toLowerCase() === key);
|
|
497
|
+
}
|
|
498
|
+
// Default: classical planets only
|
|
499
|
+
return CLASSICAL_PLANETS.has(key);
|
|
500
|
+
})
|
|
452
501
|
.map(([name, p]) => ({
|
|
453
502
|
name: name.toLowerCase(),
|
|
454
503
|
longitude: (p.longitude ?? p.lon ?? 0),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registerTool, validateRequired } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
3
4
|
registerTool({
|
|
4
5
|
name: "location_search",
|
|
5
6
|
description: "App-only tool for getting location autocomplete suggestions. Returns latitude, longitude, and IANA timezone.",
|
|
@@ -14,6 +15,7 @@ registerTool({
|
|
|
14
15
|
required: ["query"],
|
|
15
16
|
additionalProperties: false,
|
|
16
17
|
},
|
|
18
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
17
19
|
_meta: {
|
|
18
20
|
ui: {
|
|
19
21
|
visibility: ["app"],
|
|
@@ -38,6 +40,7 @@ registerTool({
|
|
|
38
40
|
required: ["latitude", "longitude"],
|
|
39
41
|
additionalProperties: false,
|
|
40
42
|
},
|
|
43
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
41
44
|
_meta: {
|
|
42
45
|
ui: {
|
|
43
46
|
visibility: ["app"],
|
|
@@ -16,6 +16,7 @@ import path from "node:path";
|
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { registerTool, validateCoordinates, SERVER_VERSION } from "../index.js";
|
|
18
18
|
import { getActiveClient } from "../../backend/client.js";
|
|
19
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
19
20
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
20
21
|
export const MOON_PHASE_RESOURCE_URI = "ui://openephemeris/moon-phase";
|
|
21
22
|
export const MOON_PHASE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -54,15 +55,26 @@ export function clearMoonPhaseBundleCache() {
|
|
|
54
55
|
function buildMoonSummary(data) {
|
|
55
56
|
const phase = data.phase;
|
|
56
57
|
const voc = data.void_of_course;
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
58
|
+
const meta = (phase?.metadata ?? {});
|
|
59
|
+
const phaseName = String(phase?.phase_name ?? phase?.name ?? "Unknown").replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
|
|
60
|
+
const illumination = typeof phase?.illumination === "number" ? Math.round(phase.illumination) : "?";
|
|
61
|
+
const moonSign = String(meta?.moon_sign ?? phase?.moon_sign ?? phase?.sign ?? voc?.current_sign ?? "");
|
|
60
62
|
const isVoc = Boolean(voc?.is_void ?? voc?.is_void_of_course ?? false);
|
|
63
|
+
const speed = typeof meta?.longitude_speed === "number" ? meta.longitude_speed : null;
|
|
64
|
+
const daysToFull = typeof meta?.days_to_full === "number" ? meta.days_to_full :
|
|
65
|
+
typeof phase?.days_to_full === "number" ? phase.days_to_full : null;
|
|
61
66
|
let summary = "**Moon Phase** — " + phaseName + "\n\n" +
|
|
62
67
|
"Illumination: **" + illumination + "%**\n";
|
|
63
68
|
if (moonSign) {
|
|
64
69
|
summary += "Moon Sign: **" + moonSign + "**\n";
|
|
65
70
|
}
|
|
71
|
+
if (speed !== null) {
|
|
72
|
+
const tempo = speed > 14.5 ? "fast" : speed < 12.0 ? "slow" : "average";
|
|
73
|
+
summary += "Speed: **" + speed.toFixed(2) + "°/day** (" + tempo + ")\n";
|
|
74
|
+
}
|
|
75
|
+
if (daysToFull !== null) {
|
|
76
|
+
summary += "Full Moon in: **" + daysToFull.toFixed(1) + " days**\n";
|
|
77
|
+
}
|
|
66
78
|
if (isVoc) {
|
|
67
79
|
summary += "\n⚠️ **Void of Course** — The Moon is currently VOC.";
|
|
68
80
|
if (voc?.ends)
|
|
@@ -107,6 +119,7 @@ registerTool({
|
|
|
107
119
|
required: [],
|
|
108
120
|
additionalProperties: false,
|
|
109
121
|
},
|
|
122
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
110
123
|
annotations: {
|
|
111
124
|
title: "Interactive Moon Phase Explorer",
|
|
112
125
|
readOnlyHint: true,
|
|
@@ -129,38 +142,79 @@ registerTool({
|
|
|
129
142
|
params.latitude = args.latitude;
|
|
130
143
|
if (args.longitude != null)
|
|
131
144
|
params.longitude = args.longitude;
|
|
132
|
-
// Fetch phase and
|
|
133
|
-
const [phase, voc] = await Promise.allSettled([
|
|
145
|
+
// Fetch phase, VOC, and moon aspects in parallel
|
|
146
|
+
const [phase, voc, aspects] = await Promise.allSettled([
|
|
134
147
|
client.request("GET", "/ephemeris/moon/phase", { params }),
|
|
135
148
|
client.request("GET", "/ephemeris/moon/void-of-course", { params }),
|
|
149
|
+
client.request("GET", "/ephemeris/moon/aspects", { params }),
|
|
136
150
|
]);
|
|
137
151
|
const mergedData = {
|
|
138
152
|
phase: phase.status === "fulfilled" ? phase.value : { error: phase.reason?.message },
|
|
139
153
|
void_of_course: voc.status === "fulfilled" ? voc.value : { error: voc.reason?.message },
|
|
154
|
+
aspects: aspects.status === "fulfilled" ? aspects.value : null,
|
|
140
155
|
};
|
|
141
156
|
// Flatten for the UI — merge phase fields to top level
|
|
142
157
|
const phaseData = mergedData.phase;
|
|
143
158
|
const vocData = mergedData.void_of_course;
|
|
159
|
+
const aspectsData = mergedData.aspects;
|
|
160
|
+
// Extract enriched metadata fields from the GET response
|
|
161
|
+
const meta = (phaseData?.metadata ?? {});
|
|
162
|
+
const moonSign = String(meta?.moon_sign ?? phaseData?.moon_sign ?? phaseData?.sign ?? vocData?.current_sign ?? "");
|
|
163
|
+
const moonSpeedRaw = meta?.longitude_speed;
|
|
164
|
+
const moonSpeed = typeof moonSpeedRaw === "number" ? moonSpeedRaw : null;
|
|
165
|
+
const angularDiameter = typeof meta?.angular_diameter === "number" ? meta.angular_diameter : null;
|
|
166
|
+
const distanceKm = typeof meta?.moon_distance_km === "number" ? meta.moon_distance_km : null;
|
|
167
|
+
const moonLat = typeof meta?.moon_lat === "number" ? meta.moon_lat : null;
|
|
168
|
+
const ageDays = typeof meta?.age_days === "number" ? meta.age_days :
|
|
169
|
+
(typeof phaseData?.age_days === "number" ? phaseData.age_days : null);
|
|
170
|
+
// days_to_new / days_to_full can come as the union value or from the top-level field
|
|
171
|
+
const extractDays = (field) => {
|
|
172
|
+
if (typeof field === "number")
|
|
173
|
+
return field;
|
|
174
|
+
if (field && typeof field === "object") {
|
|
175
|
+
const v = Object.values(field)[0];
|
|
176
|
+
if (typeof v === "number")
|
|
177
|
+
return v;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
const daysToNew = extractDays(phaseData?.days_to_new);
|
|
182
|
+
const daysToFull = extractDays(phaseData?.days_to_full);
|
|
183
|
+
// Top 3 applying aspects by tightest orb
|
|
184
|
+
const rawAspects = (aspectsData?.aspects ?? aspectsData?.data ?? []);
|
|
185
|
+
const applyingAspects = Array.isArray(rawAspects)
|
|
186
|
+
? rawAspects
|
|
187
|
+
.filter((a) => a.applying === true || a.type === "applying")
|
|
188
|
+
.sort((a, b) => Math.abs(a.orb ?? 99) - Math.abs(b.orb ?? 99))
|
|
189
|
+
.slice(0, 3)
|
|
190
|
+
.map((a) => ({
|
|
191
|
+
planet: a.planet ?? a.body ?? a.planet_name ?? "",
|
|
192
|
+
aspect: a.aspect ?? a.aspect_name ?? a.type_name ?? "",
|
|
193
|
+
orb: typeof a.orb === "number" ? Math.abs(a.orb) : null,
|
|
194
|
+
}))
|
|
195
|
+
: [];
|
|
144
196
|
const uiPayload = {
|
|
145
|
-
phase_name: phaseData?.phase_name ?? phaseData?.name ?? "Unknown",
|
|
146
|
-
illumination: phaseData?.illumination
|
|
147
|
-
|
|
197
|
+
phase_name: String(phaseData?.phase_name ?? phaseData?.name ?? "Unknown").replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()),
|
|
198
|
+
illumination: typeof phaseData?.illumination === "number" ? phaseData.illumination / 100.0 : 0,
|
|
199
|
+
phase_angle: typeof phaseData?.phase_angle === "number" ? phaseData.phase_angle : null,
|
|
200
|
+
age_days: ageDays,
|
|
201
|
+
days_to_new: daysToNew,
|
|
202
|
+
days_to_full: daysToFull,
|
|
148
203
|
waxing: phaseData?.waxing ??
|
|
149
204
|
// Fallback chain: elongation < 180° = waxing, else age_days < 14.75 = waxing.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
? phaseData.age_days < 14.75
|
|
205
|
+
(typeof phaseData?.phase_angle === "number"
|
|
206
|
+
? phaseData.phase_angle < 180
|
|
207
|
+
: typeof ageDays === "number"
|
|
208
|
+
? ageDays < 14.75
|
|
155
209
|
: true), // safe default: assume waxing
|
|
156
|
-
moon_sign:
|
|
157
|
-
moon_longitude: phaseData?.moon_longitude ?? phaseData?.longitude ?? null,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
210
|
+
moon_sign: moonSign,
|
|
211
|
+
moon_longitude: meta?.moon_lon ?? phaseData?.moon_longitude ?? phaseData?.longitude ?? null,
|
|
212
|
+
moon_latitude: moonLat,
|
|
213
|
+
moon_distance_km: distanceKm,
|
|
214
|
+
angular_diameter: angularDiameter,
|
|
215
|
+
moon_speed: moonSpeed,
|
|
216
|
+
sun_longitude: meta?.sun_lon ?? phaseData?.sun_longitude ?? null,
|
|
217
|
+
applying_aspects: applyingAspects,
|
|
164
218
|
voc_status: {
|
|
165
219
|
is_void: Boolean(vocData?.is_void ?? vocData?.is_void_of_course ?? false),
|
|
166
220
|
started: vocData?.started ?? vocData?.start ?? null,
|
|
@@ -17,6 +17,7 @@ import path from "node:path";
|
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { registerTool, validateRequired, SERVER_VERSION } from "../index.js";
|
|
19
19
|
import { getActiveClient } from "../../backend/client.js";
|
|
20
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
20
21
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
21
22
|
export const TRANSIT_TIMELINE_RESOURCE_URI = "ui://openephemeris/transit-timeline";
|
|
22
23
|
export const TRANSIT_TIMELINE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -129,6 +130,7 @@ registerTool({
|
|
|
129
130
|
required: ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"],
|
|
130
131
|
additionalProperties: false,
|
|
131
132
|
},
|
|
133
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
132
134
|
annotations: {
|
|
133
135
|
title: "Interactive Transit Timeline Explorer",
|
|
134
136
|
readOnlyHint: true,
|
|
@@ -145,6 +147,19 @@ registerTool({
|
|
|
145
147
|
handler: async (args) => {
|
|
146
148
|
validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"]);
|
|
147
149
|
const client = getActiveClient();
|
|
150
|
+
// Guard: reject excessively wide date ranges before hitting the backend.
|
|
151
|
+
// The Go backend clamps Explorer tier to 1 year, but larger tiers can still
|
|
152
|
+
// trigger compute-bound 45-second timeouts on very wide windows.
|
|
153
|
+
const startMs = new Date(args.start_date).getTime();
|
|
154
|
+
const endMs = new Date(args.end_date).getTime();
|
|
155
|
+
if (!isNaN(startMs) && !isNaN(endMs)) {
|
|
156
|
+
const spanDays = (endMs - startMs) / (1000 * 60 * 60 * 24);
|
|
157
|
+
if (spanDays > 5 * 365) {
|
|
158
|
+
throw new Error(`Date range too wide (${Math.round(spanDays / 365)} years). ` +
|
|
159
|
+
`The transit timeline works best with windows under 2 years — try narrowing ` +
|
|
160
|
+
`start_date and end_date to a 1–2 year window and re-run.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
148
163
|
// Step 1: Compute natal chart to extract planetary longitudes
|
|
149
164
|
const natalBody = {
|
|
150
165
|
subject: {
|
|
@@ -161,32 +176,57 @@ registerTool({
|
|
|
161
176
|
const natalPositionMap = {};
|
|
162
177
|
const planets = natalResult?.planets || natalResult?.data?.planets || [];
|
|
163
178
|
const planetArray = Array.isArray(planets) ? planets : Object.values(planets);
|
|
179
|
+
// ── Core natal points to target (conjunction search) ─────────────────────
|
|
180
|
+
// When no natal_points filter is given, we limit to 7 core bodies to keep
|
|
181
|
+
// target_degrees count manageable. 4 outer planets × 10+ targets creates a
|
|
182
|
+
// combinatoric explosion (40+ serial SearchTransit calls on the backend).
|
|
183
|
+
const CORE_BODIES_DEFAULT = new Set([
|
|
184
|
+
"sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn",
|
|
185
|
+
]);
|
|
164
186
|
const wantedPoints = args.natal_points
|
|
165
187
|
? new Set(args.natal_points.map((p) => p.toLowerCase()))
|
|
166
|
-
:
|
|
188
|
+
: CORE_BODIES_DEFAULT;
|
|
167
189
|
for (const planet of planetArray) {
|
|
168
190
|
const name = (planet.name || planet.id || "").toLowerCase();
|
|
169
191
|
const lon = planet.longitude ?? planet.ecliptic_longitude ?? planet.lon;
|
|
170
192
|
if (typeof lon !== "number" || !isFinite(lon))
|
|
171
193
|
continue;
|
|
172
|
-
if (
|
|
194
|
+
if (!wantedPoints.has(name))
|
|
173
195
|
continue;
|
|
174
196
|
targetDegrees.push(Math.round(lon * 100) / 100);
|
|
175
197
|
natalPositionMap[name] = lon;
|
|
176
198
|
}
|
|
177
|
-
// Extract angles (ASC, MC)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
// Extract angles (ASC, MC) — only if explicitly requested via natal_points
|
|
200
|
+
if (args.natal_points) {
|
|
201
|
+
const angles = natalResult?.angles || natalResult?.data?.angles;
|
|
202
|
+
if (angles) {
|
|
203
|
+
const angleEntries = Array.isArray(angles)
|
|
204
|
+
? angles
|
|
205
|
+
: Object.entries(angles).map(([k, v]) => ({ name: k, ...(typeof v === "object" ? v : { longitude: v }) }));
|
|
206
|
+
for (const angle of angleEntries) {
|
|
207
|
+
const name = (angle.name || angle.id || "").toLowerCase();
|
|
208
|
+
const lon = angle.longitude ?? angle.ecliptic_longitude ?? angle.lon;
|
|
209
|
+
if (typeof lon !== "number" || !isFinite(lon))
|
|
210
|
+
continue;
|
|
211
|
+
if (!wantedPoints.has(name))
|
|
212
|
+
continue;
|
|
213
|
+
targetDegrees.push(Math.round(lon * 100) / 100);
|
|
214
|
+
natalPositionMap[name] = lon;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Safety cap: never send more than 12 target degrees to the backend
|
|
219
|
+
// (prevents combinatoric timeouts when many planets + many targets overlap).
|
|
220
|
+
const MAX_TARGETS = 12;
|
|
221
|
+
if (targetDegrees.length > MAX_TARGETS) {
|
|
222
|
+
targetDegrees.splice(MAX_TARGETS);
|
|
223
|
+
// Prune natalPositionMap to match
|
|
224
|
+
const keepNames = new Set(Object.entries(natalPositionMap)
|
|
225
|
+
.filter(([, v]) => targetDegrees.includes(Math.round(v * 100) / 100))
|
|
226
|
+
.map(([k]) => k));
|
|
227
|
+
for (const k of Object.keys(natalPositionMap)) {
|
|
228
|
+
if (!keepNames.has(k))
|
|
229
|
+
delete natalPositionMap[k];
|
|
190
230
|
}
|
|
191
231
|
}
|
|
192
232
|
if (targetDegrees.length === 0) {
|
|
@@ -199,31 +239,43 @@ registerTool({
|
|
|
199
239
|
let endStr = args.end_date;
|
|
200
240
|
if (/^\d{4}-\d{2}-\d{2}$/.test(endStr))
|
|
201
241
|
endStr += "T23:59:59Z";
|
|
242
|
+
const aspectAngle = typeof args.aspect_angle === "number" ? args.aspect_angle : 0;
|
|
243
|
+
let effectiveTargets = targetDegrees;
|
|
244
|
+
if (aspectAngle !== 0) {
|
|
245
|
+
effectiveTargets = targetDegrees.map((deg) => ((deg + aspectAngle) % 360 + 360) % 360);
|
|
246
|
+
}
|
|
202
247
|
const transitBody = {
|
|
203
248
|
start_date: startStr,
|
|
204
249
|
end_date: endStr,
|
|
205
|
-
target_degrees:
|
|
250
|
+
target_degrees: effectiveTargets,
|
|
206
251
|
};
|
|
207
252
|
if (args.transiting_planets)
|
|
208
253
|
transitBody.planet_names = args.transiting_planets;
|
|
209
|
-
let effectiveTargets = targetDegrees;
|
|
210
|
-
const aspectAngle = typeof args.aspect_angle === "number" ? args.aspect_angle : 0;
|
|
211
254
|
if (aspectAngle !== 0) {
|
|
212
|
-
effectiveTargets = targetDegrees.map((deg) => ((deg + aspectAngle) % 360 + 360) % 360);
|
|
213
|
-
transitBody.target_degrees = effectiveTargets;
|
|
214
255
|
transitBody.search_criteria = { aspect_angle: aspectAngle };
|
|
215
256
|
}
|
|
216
|
-
|
|
257
|
+
// Give the backend a 90s window — explorer tier allows 30s Go compute
|
|
258
|
+
// timeout, but the node-to-Fly round-trip including queuing can extend this.
|
|
259
|
+
const transitResult = await client.request("POST", "/predictive/transits/search", { data: transitBody, timeoutMs: 90_000 });
|
|
260
|
+
// Normalize the API response into a flat array so the iframe UI always
|
|
261
|
+
// receives an iterable — some backend response shapes wrap events in a
|
|
262
|
+
// container object rather than returning a bare array.
|
|
263
|
+
const transitArray = Array.isArray(transitResult)
|
|
264
|
+
? transitResult
|
|
265
|
+
: (transitResult?.events ??
|
|
266
|
+
transitResult?.transits ??
|
|
267
|
+
transitResult?.results ??
|
|
268
|
+
[]);
|
|
217
269
|
// Build payload
|
|
218
270
|
const payload = {
|
|
219
271
|
natal_positions: natalPositionMap,
|
|
220
272
|
target_degrees_used: effectiveTargets,
|
|
221
273
|
aspect_angle: aspectAngle,
|
|
222
|
-
transit_results:
|
|
274
|
+
transit_results: transitArray,
|
|
223
275
|
search_window: { start_date: args.start_date, end_date: args.end_date },
|
|
224
276
|
server_version: SERVER_VERSION,
|
|
225
277
|
};
|
|
226
|
-
const summary = buildTransitSummary(natalPositionMap,
|
|
278
|
+
const summary = buildTransitSummary(natalPositionMap, transitArray, aspectAngle, args.start_date, args.end_date);
|
|
227
279
|
const bundleAvailable = Boolean(getTransitTimelineBundle());
|
|
228
280
|
if (bundleAvailable) {
|
|
229
281
|
return {
|
|
@@ -14,6 +14,7 @@ import path from "node:path";
|
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
16
16
|
import { getActiveClient } from "../../backend/client.js";
|
|
17
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
17
18
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
18
19
|
export const VEDIC_CHART_RESOURCE_URI = "ui://openephemeris/vedic-chart";
|
|
19
20
|
export const VEDIC_CHART_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -149,6 +150,7 @@ registerTool({
|
|
|
149
150
|
required: ["datetime", "latitude", "longitude"],
|
|
150
151
|
additionalProperties: false,
|
|
151
152
|
},
|
|
153
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
152
154
|
annotations: {
|
|
153
155
|
title: "Interactive Vedic Jyotish Chart",
|
|
154
156
|
readOnlyHint: true,
|
package/dist/tools/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { registerTool } from "./index.js";
|
|
2
2
|
import { CredentialManager } from "../auth/credentials.js";
|
|
3
3
|
import { DeviceAuthFlow } from "../auth/device-auth.js";
|
|
4
|
+
import { OUTPUT_SCHEMA_AUTH } from "./output-schemas.js";
|
|
4
5
|
const credentialManager = new CredentialManager();
|
|
5
6
|
// ────────────────────────────────────────────────────────
|
|
6
7
|
// auth.login — Start the device authorization flow
|
|
@@ -17,6 +18,7 @@ registerTool({
|
|
|
17
18
|
properties: {},
|
|
18
19
|
additionalProperties: false,
|
|
19
20
|
},
|
|
21
|
+
outputSchema: OUTPUT_SCHEMA_AUTH,
|
|
20
22
|
annotations: {
|
|
21
23
|
title: "Connect Account",
|
|
22
24
|
readOnlyHint: false,
|
|
@@ -95,6 +97,7 @@ registerTool({
|
|
|
95
97
|
properties: {},
|
|
96
98
|
additionalProperties: false,
|
|
97
99
|
},
|
|
100
|
+
outputSchema: OUTPUT_SCHEMA_AUTH,
|
|
98
101
|
annotations: {
|
|
99
102
|
title: "Auth Status",
|
|
100
103
|
readOnlyHint: true,
|
|
@@ -179,6 +182,7 @@ registerTool({
|
|
|
179
182
|
properties: {},
|
|
180
183
|
additionalProperties: false,
|
|
181
184
|
},
|
|
185
|
+
outputSchema: OUTPUT_SCHEMA_AUTH,
|
|
182
186
|
annotations: {
|
|
183
187
|
title: "Disconnect Account",
|
|
184
188
|
readOnlyHint: false,
|
package/dist/tools/dev.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registerTool, validateRequired } from "./index.js";
|
|
2
2
|
import { getActiveClient } from "../backend/client.js";
|
|
3
|
+
import { OUTPUT_SCHEMA_JSON } from "./output-schemas.js";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -123,6 +124,7 @@ registerTool({
|
|
|
123
124
|
required: ["method", "path"],
|
|
124
125
|
additionalProperties: false,
|
|
125
126
|
},
|
|
127
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
126
128
|
handler: async (args) => {
|
|
127
129
|
validateRequired(args, ["path"]);
|
|
128
130
|
const methodStr = String(args.method || "GET").toUpperCase();
|
|
@@ -189,6 +191,7 @@ registerTool({
|
|
|
189
191
|
properties: {},
|
|
190
192
|
additionalProperties: false,
|
|
191
193
|
},
|
|
194
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
192
195
|
handler: async () => {
|
|
193
196
|
const allowlist = loadAllowlist();
|
|
194
197
|
return {
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ export interface ToolDefinition {
|
|
|
5
5
|
name: string;
|
|
6
6
|
description: string;
|
|
7
7
|
inputSchema: z.ZodType<any> | Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* MCP spec 2025-11-25 — JSON Schema describing the structured output of this tool.
|
|
10
|
+
* Smithery uses this to score "Output schemas" in the quality rubric.
|
|
11
|
+
* All tools return MCP content block arrays; specialized tools declare richer shapes.
|
|
12
|
+
*/
|
|
13
|
+
outputSchema?: Record<string, unknown>;
|
|
8
14
|
annotations?: {
|
|
9
15
|
title?: string;
|
|
10
16
|
readOnlyHint?: boolean;
|
package/dist/tools/index.js
CHANGED
|
@@ -47,7 +47,11 @@ export async function initTools(profile) {
|
|
|
47
47
|
// Always register the generic proxy tools (dev.call + dev.list_allowed).
|
|
48
48
|
await import("./dev.js");
|
|
49
49
|
if (resolvedProfile === "dev") {
|
|
50
|
-
//
|
|
50
|
+
// ── FULL BUILD: all specialized tools ───────────────────────────
|
|
51
|
+
// Payload size theory was disproven. The "Failed to call tool"
|
|
52
|
+
// bug was caused by Claude Desktop's MCP App iframe handler
|
|
53
|
+
// corruption. Iframe tools are purged; all specialized tools are restored.
|
|
54
|
+
// ────────────────────────────────────────────────────────────────
|
|
51
55
|
await import("./specialized/natal.js");
|
|
52
56
|
await import("./specialized/transits.js");
|
|
53
57
|
await import("./specialized/moon.js");
|
|
@@ -70,15 +74,12 @@ export async function initTools(profile) {
|
|
|
70
74
|
await import("./specialized/ephemeris_extended.js");
|
|
71
75
|
await import("./specialized/venus_star_points.js");
|
|
72
76
|
await import("./specialized/acg.js");
|
|
73
|
-
|
|
77
|
+
await import("./apps/location-tools.js");
|
|
78
|
+
// Re-enable requested MCP apps (iframes)
|
|
74
79
|
await import("./apps/chart-wheel-app.js");
|
|
75
|
-
await import("./apps/bodygraph-app.js");
|
|
76
80
|
await import("./apps/bi-wheel-app.js");
|
|
77
|
-
await import("./apps/
|
|
81
|
+
await import("./apps/bodygraph-app.js");
|
|
78
82
|
await import("./apps/moon-phase-app.js");
|
|
79
|
-
await import("./apps/bazi-app.js");
|
|
80
|
-
await import("./apps/vedic-chart-app.js");
|
|
81
|
-
await import("./apps/location-tools.js");
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* output-schemas.ts — Shared JSON Schema output schema definitions for MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* MCP spec 2025-11-25 supports `outputSchema` on tool definitions. Smithery
|
|
5
|
+
* uses this field to score the "Output schemas" quality criterion (10.37pt / 57 tools).
|
|
6
|
+
*
|
|
7
|
+
* All tools return an MCP content block response. Most return a single JSON text
|
|
8
|
+
* block; visual tools return an image block or an image + text block.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* import { OUTPUT_SCHEMA_JSON, OUTPUT_SCHEMA_IMAGE } from "../output-schemas.js";
|
|
12
|
+
* registerTool({ ..., outputSchema: OUTPUT_SCHEMA_JSON });
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Standard tool output schema: a single JSON text block.
|
|
16
|
+
* Used by all data-returning tools (natal chart, transits, synastry, etc.).
|
|
17
|
+
*/
|
|
18
|
+
export declare const OUTPUT_SCHEMA_JSON: Record<string, unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* Visual tool output schema: a single SVG or PNG image block.
|
|
21
|
+
* Used by chart wheel (direct image), BaZi chart, Vedic chart, etc.
|
|
22
|
+
*/
|
|
23
|
+
export declare const OUTPUT_SCHEMA_IMAGE: Record<string, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* Dual output schema: an image block followed by a JSON text block.
|
|
26
|
+
* Used by tools with include_visual=true (natal chart SVG + data, bi-wheel, etc.).
|
|
27
|
+
*/
|
|
28
|
+
export declare const OUTPUT_SCHEMA_IMAGE_AND_JSON: Record<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Interactive app tool output schema: a text block containing MCP App metadata.
|
|
31
|
+
* Used by explore_* tools that return an embedded UI resource reference.
|
|
32
|
+
*/
|
|
33
|
+
export declare const OUTPUT_SCHEMA_APP: Record<string, unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Auth tool output schema: a JSON text block confirming auth status or credentials.
|
|
36
|
+
*/
|
|
37
|
+
export declare const OUTPUT_SCHEMA_AUTH: Record<string, unknown>;
|