@openephemeris/mcp-server 3.13.9 → 3.13.10
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/README.md +2 -2
- package/config/dev-allowlist.json +28 -4
- package/dist/index.js +45 -0
- package/dist/oauth/dcr.d.ts +11 -0
- package/dist/oauth/dcr.js +49 -0
- package/dist/oauth/discovery.d.ts +11 -0
- package/dist/oauth/discovery.js +40 -0
- package/dist/oauth/pkce.d.ts +18 -0
- package/dist/oauth/pkce.js +34 -0
- package/dist/oauth/rate-limit.d.ts +20 -0
- package/dist/oauth/rate-limit.js +65 -0
- package/dist/oauth/store.d.ts +68 -0
- package/dist/oauth/store.js +247 -0
- package/dist/oauth/supabase-jwt.d.ts +39 -0
- package/dist/oauth/supabase-jwt.js +194 -0
- package/dist/oauth/token.d.ts +21 -0
- package/dist/oauth/token.js +213 -0
- package/dist/prompts.js +171 -159
- package/dist/server-sse.js +134 -36
- package/dist/tools/apps/bazi-app.d.ts +23 -0
- package/dist/tools/apps/bazi-app.js +224 -0
- package/dist/tools/apps/bi-wheel-app.d.ts +14 -7
- package/dist/tools/apps/bi-wheel-app.js +293 -108
- package/dist/tools/apps/bodygraph-app.js +38 -3
- package/dist/tools/apps/moon-phase-app.d.ts +19 -0
- package/dist/tools/apps/moon-phase-app.js +190 -0
- package/dist/tools/apps/transit-timeline-app.d.ts +20 -0
- package/dist/tools/apps/transit-timeline-app.js +243 -0
- package/dist/tools/apps/vedic-chart-app.d.ts +17 -0
- package/dist/tools/apps/vedic-chart-app.js +226 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/specialized/acg.js +2 -2
- package/dist/tools/specialized/bazi.js +367 -44
- package/dist/tools/specialized/bi_wheel.js +1 -1
- package/dist/tools/specialized/chart_wheel.js +1 -1
- package/dist/tools/specialized/comparative.js +4 -4
- package/dist/tools/specialized/eclipse.js +1 -1
- package/dist/tools/specialized/electional.js +4 -4
- package/dist/tools/specialized/ephemeris_core.js +2 -2
- package/dist/tools/specialized/ephemeris_extended.js +8 -8
- package/dist/tools/specialized/hd_bodygraph.js +1 -1
- package/dist/tools/specialized/hd_cycles.js +2 -2
- package/dist/tools/specialized/hd_group.js +2 -2
- package/dist/tools/specialized/human_design.js +1 -1
- package/dist/tools/specialized/moon.js +2 -2
- package/dist/tools/specialized/natal.js +1 -1
- package/dist/tools/specialized/progressed.js +1 -1
- package/dist/tools/specialized/relocation.js +1 -1
- package/dist/tools/specialized/returns.js +3 -3
- package/dist/tools/specialized/synastry.js +1 -1
- package/dist/tools/specialized/transits.js +1 -1
- package/dist/tools/specialized/vedic.js +1 -1
- package/dist/tools/specialized/venus_star_points.js +6 -6
- package/dist/ui/bazi.html +213 -0
- package/dist/ui/bi-wheel.html +762 -489
- package/dist/ui/bodygraph.html +1788 -1649
- package/dist/ui/chart-wheel.html +1772 -1593
- package/dist/ui/moon-phase.html +6758 -0
- package/dist/ui/transit-timeline.html +6835 -0
- package/dist/ui/vedic-chart.html +210 -0
- package/package.json +2 -2
|
@@ -234,9 +234,44 @@ function buildHdModelPayload(data, birthParams) {
|
|
|
234
234
|
const dGateList = Array.isArray(rawD) ? rawD : Object.values(rawD);
|
|
235
235
|
const personality_gates = [...new Set(extractGateNums(pGateList))].sort((a, b) => a - b);
|
|
236
236
|
const design_gates = [...new Set(extractGateNums(dGateList))].sort((a, b) => a - b);
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
// Convert activation arrays or maps → planet-keyed Record<string, ActivationSummary>
|
|
238
|
+
// The Go API returns arrays like [{ planet: "Sun", gate: 62, line: 1, color: 3, tone: 4 }].
|
|
239
|
+
// The renderer needs a map keyed by planet name: { "Sun": { gate, line, color, tone } }.
|
|
240
|
+
function toActivationMap(raw) {
|
|
241
|
+
if (!raw || (typeof raw !== 'object'))
|
|
242
|
+
return undefined;
|
|
243
|
+
// Already a planet-keyed map (non-array object)
|
|
244
|
+
if (!Array.isArray(raw)) {
|
|
245
|
+
const obj = raw;
|
|
246
|
+
// Make sure it looks like planet-keyed (values have 'gate' property)
|
|
247
|
+
const first = Object.values(obj)[0];
|
|
248
|
+
if (first && typeof first === 'object' && 'gate' in first) {
|
|
249
|
+
return obj;
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
// Array of activation objects — key by the 'planet' field
|
|
254
|
+
const arr = raw;
|
|
255
|
+
if (arr.length === 0)
|
|
256
|
+
return undefined;
|
|
257
|
+
const map = {};
|
|
258
|
+
for (const item of arr) {
|
|
259
|
+
if (!item || typeof item !== 'object')
|
|
260
|
+
continue;
|
|
261
|
+
const planetName = String(item.planet ?? item.name ?? '');
|
|
262
|
+
if (!planetName)
|
|
263
|
+
continue;
|
|
264
|
+
map[planetName] = {
|
|
265
|
+
gate: Number(item.gate ?? 0),
|
|
266
|
+
line: Number(item.line ?? 0),
|
|
267
|
+
color: Number(item.color ?? 0),
|
|
268
|
+
tone: Number(item.tone ?? 0),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return Object.keys(map).length > 0 ? map : undefined;
|
|
272
|
+
}
|
|
273
|
+
const personality_activations = toActivationMap(rawP);
|
|
274
|
+
const design_activations = toActivationMap(rawD);
|
|
240
275
|
return {
|
|
241
276
|
type,
|
|
242
277
|
profile,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* moon-phase-app.ts — MCP App tool registration for the Moon Phase Explorer.
|
|
3
|
+
*
|
|
4
|
+
* Registers 1 tool:
|
|
5
|
+
* • explore_moon_phase — primary entry point, returns data + UI resource [model]
|
|
6
|
+
*
|
|
7
|
+
* The dial is rendered client-side from structured JSON. The tool calls the
|
|
8
|
+
* existing moon phase and VOC endpoints, merges the results, and returns them
|
|
9
|
+
* with a UI resource link so the iframe can render a circular phase dial.
|
|
10
|
+
*
|
|
11
|
+
* Also exports resource helpers (getMoonPhaseBundle, etc.) for use in
|
|
12
|
+
* index.ts and server-sse.ts.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MOON_PHASE_RESOURCE_URI = "ui://openephemeris/moon-phase";
|
|
15
|
+
export declare const MOON_PHASE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
16
|
+
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
17
|
+
export declare function getMoonPhaseBundle(): string | null;
|
|
18
|
+
/** Reset cache (useful in dev watch mode or tests). */
|
|
19
|
+
export declare function clearMoonPhaseBundleCache(): void;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* moon-phase-app.ts — MCP App tool registration for the Moon Phase Explorer.
|
|
3
|
+
*
|
|
4
|
+
* Registers 1 tool:
|
|
5
|
+
* • explore_moon_phase — primary entry point, returns data + UI resource [model]
|
|
6
|
+
*
|
|
7
|
+
* The dial is rendered client-side from structured JSON. The tool calls the
|
|
8
|
+
* existing moon phase and VOC endpoints, merges the results, and returns them
|
|
9
|
+
* with a UI resource link so the iframe can render a circular phase dial.
|
|
10
|
+
*
|
|
11
|
+
* Also exports resource helpers (getMoonPhaseBundle, etc.) for use in
|
|
12
|
+
* index.ts and server-sse.ts.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { registerTool, validateCoordinates, SERVER_VERSION } from "../index.js";
|
|
18
|
+
import { getActiveClient } from "../../backend/client.js";
|
|
19
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
20
|
+
export const MOON_PHASE_RESOURCE_URI = "ui://openephemeris/moon-phase";
|
|
21
|
+
export const MOON_PHASE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
22
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const BUNDLE_PATHS = [
|
|
24
|
+
path.resolve(here, "..", "..", "..", "dist", "ui", "moon-phase.html"),
|
|
25
|
+
];
|
|
26
|
+
function findBundlePath() {
|
|
27
|
+
for (const p of BUNDLE_PATHS) {
|
|
28
|
+
if (fs.existsSync(p))
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
let cachedBundle = null;
|
|
34
|
+
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
35
|
+
export function getMoonPhaseBundle() {
|
|
36
|
+
if (cachedBundle)
|
|
37
|
+
return cachedBundle;
|
|
38
|
+
const bundlePath = findBundlePath();
|
|
39
|
+
if (!bundlePath)
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
cachedBundle = fs.readFileSync(bundlePath, "utf-8");
|
|
43
|
+
return cachedBundle;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Reset cache (useful in dev watch mode or tests). */
|
|
50
|
+
export function clearMoonPhaseBundleCache() {
|
|
51
|
+
cachedBundle = null;
|
|
52
|
+
}
|
|
53
|
+
// ── Summary builder ──────────────────────────────────────────────────────────
|
|
54
|
+
function buildMoonSummary(data) {
|
|
55
|
+
const phase = data.phase;
|
|
56
|
+
const voc = data.void_of_course;
|
|
57
|
+
const phaseName = String(phase?.phase_name ?? phase?.name ?? "Unknown");
|
|
58
|
+
const illumination = typeof phase?.illumination === "number" ? Math.round(phase.illumination * 100) : "?";
|
|
59
|
+
const moonSign = String(phase?.moon_sign ?? phase?.sign ?? "");
|
|
60
|
+
const isVoc = Boolean(voc?.is_void ?? voc?.is_void_of_course ?? false);
|
|
61
|
+
let summary = "**Moon Phase** — " + phaseName + "\n\n" +
|
|
62
|
+
"Illumination: **" + illumination + "%**\n";
|
|
63
|
+
if (moonSign) {
|
|
64
|
+
summary += "Moon Sign: **" + moonSign + "**\n";
|
|
65
|
+
}
|
|
66
|
+
if (isVoc) {
|
|
67
|
+
summary += "\n⚠️ **Void of Course** — The Moon is currently VOC.";
|
|
68
|
+
if (voc?.ends)
|
|
69
|
+
summary += " Ends at " + String(voc.ends) + ".";
|
|
70
|
+
if (voc?.next_sign)
|
|
71
|
+
summary += " Next sign: " + String(voc.next_sign) + ".";
|
|
72
|
+
summary += "\n";
|
|
73
|
+
}
|
|
74
|
+
summary += "\nClick cards on the dial for details and interpretation.";
|
|
75
|
+
return summary;
|
|
76
|
+
}
|
|
77
|
+
// ── Tool: explore_moon_phase ─────────────────────────────────────────────────
|
|
78
|
+
registerTool({
|
|
79
|
+
name: "explore_moon_phase",
|
|
80
|
+
description: "Generate an interactive Moon Phase dial showing the current lunar illumination, " +
|
|
81
|
+
"phase name, zodiac sign, and void-of-course status as a beautiful circular visualization.\n\n" +
|
|
82
|
+
"Returns a visual dial with:\n" +
|
|
83
|
+
" • SVG crescent Moon showing real-time illumination percentage\n" +
|
|
84
|
+
" • Phase name and waxing/waning indicator\n" +
|
|
85
|
+
" • Current Moon sign with degree\n" +
|
|
86
|
+
" • Void-of-Course status with timing details\n" +
|
|
87
|
+
" • Lunar age (days in the synodic cycle)\n" +
|
|
88
|
+
" • Upcoming New Moon and Full Moon dates\n\n" +
|
|
89
|
+
"Use this for a rich, interactive lunar phase experience in MCP Apps-capable hosts (Claude Desktop). " +
|
|
90
|
+
"Falls back to a text summary in other hosts.",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
datetime: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "ISO 8601 datetime to query. If omitted, returns the current live moon phase (UTC now).",
|
|
97
|
+
},
|
|
98
|
+
latitude: {
|
|
99
|
+
type: "number",
|
|
100
|
+
description: "Observer latitude (optional, used for local void-of-course calculations).",
|
|
101
|
+
},
|
|
102
|
+
longitude: {
|
|
103
|
+
type: "number",
|
|
104
|
+
description: "Observer longitude (optional, used for local void-of-course calculations).",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
required: [],
|
|
108
|
+
additionalProperties: false,
|
|
109
|
+
},
|
|
110
|
+
annotations: {
|
|
111
|
+
title: "Interactive Moon Phase Explorer",
|
|
112
|
+
readOnlyHint: true,
|
|
113
|
+
destructiveHint: false,
|
|
114
|
+
idempotentHint: true,
|
|
115
|
+
openWorldHint: false,
|
|
116
|
+
},
|
|
117
|
+
_meta: {
|
|
118
|
+
ui: {
|
|
119
|
+
resourceUri: MOON_PHASE_RESOURCE_URI,
|
|
120
|
+
visibility: ["model", "app"],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
handler: async (args) => {
|
|
124
|
+
validateCoordinates(args, "latitude", "longitude");
|
|
125
|
+
const client = getActiveClient();
|
|
126
|
+
const params = {};
|
|
127
|
+
params.datetime = args.datetime ?? new Date().toISOString();
|
|
128
|
+
if (args.latitude != null)
|
|
129
|
+
params.latitude = args.latitude;
|
|
130
|
+
if (args.longitude != null)
|
|
131
|
+
params.longitude = args.longitude;
|
|
132
|
+
// Fetch phase and VOC in parallel
|
|
133
|
+
const [phase, voc] = await Promise.allSettled([
|
|
134
|
+
client.request("GET", "/ephemeris/moon/phase", { params }),
|
|
135
|
+
client.request("GET", "/ephemeris/moon/void-of-course", { params }),
|
|
136
|
+
]);
|
|
137
|
+
const mergedData = {
|
|
138
|
+
phase: phase.status === "fulfilled" ? phase.value : { error: phase.reason?.message },
|
|
139
|
+
void_of_course: voc.status === "fulfilled" ? voc.value : { error: voc.reason?.message },
|
|
140
|
+
};
|
|
141
|
+
// Flatten for the UI — merge phase fields to top level
|
|
142
|
+
const phaseData = mergedData.phase;
|
|
143
|
+
const vocData = mergedData.void_of_course;
|
|
144
|
+
const uiPayload = {
|
|
145
|
+
phase_name: phaseData?.phase_name ?? phaseData?.name ?? "Unknown",
|
|
146
|
+
illumination: phaseData?.illumination ?? 0,
|
|
147
|
+
age_days: phaseData?.age_days ?? phaseData?.age ?? null,
|
|
148
|
+
waxing: phaseData?.waxing ??
|
|
149
|
+
// Fallback chain: elongation < 180° = waxing, else age_days < 14.75 = waxing.
|
|
150
|
+
// Never use illumination alone — it's identical for mirror-symmetric waxing/waning phases.
|
|
151
|
+
(typeof phaseData?.elongation === "number"
|
|
152
|
+
? phaseData.elongation < 180
|
|
153
|
+
: typeof phaseData?.age_days === "number"
|
|
154
|
+
? phaseData.age_days < 14.75
|
|
155
|
+
: true), // safe default: assume waxing
|
|
156
|
+
moon_sign: phaseData?.moon_sign ?? phaseData?.sign ?? "",
|
|
157
|
+
moon_longitude: phaseData?.moon_longitude ?? phaseData?.longitude ?? null,
|
|
158
|
+
sun_longitude: phaseData?.sun_longitude ?? null,
|
|
159
|
+
elongation: phaseData?.elongation ?? null,
|
|
160
|
+
moon_latitude: phaseData?.moon_latitude ?? null,
|
|
161
|
+
moon_distance_km: phaseData?.moon_distance_km ?? phaseData?.distance_km ?? null,
|
|
162
|
+
next_new_moon: phaseData?.next_new_moon ?? null,
|
|
163
|
+
next_full_moon: phaseData?.next_full_moon ?? null,
|
|
164
|
+
voc_status: {
|
|
165
|
+
is_void: Boolean(vocData?.is_void ?? vocData?.is_void_of_course ?? false),
|
|
166
|
+
started: vocData?.started ?? vocData?.start ?? null,
|
|
167
|
+
ends: vocData?.ends ?? vocData?.end ?? null,
|
|
168
|
+
next_sign: vocData?.next_sign ?? null,
|
|
169
|
+
},
|
|
170
|
+
datetime: params.datetime,
|
|
171
|
+
server_version: SERVER_VERSION,
|
|
172
|
+
};
|
|
173
|
+
const summary = buildMoonSummary(mergedData);
|
|
174
|
+
const bundleAvailable = Boolean(getMoonPhaseBundle());
|
|
175
|
+
if (bundleAvailable) {
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{ type: "text", text: summary },
|
|
179
|
+
{ type: "text", text: JSON.stringify(uiPayload) },
|
|
180
|
+
],
|
|
181
|
+
_meta: {
|
|
182
|
+
"ui/resourceUri": MOON_PHASE_RESOURCE_URI,
|
|
183
|
+
ui: { resourceUri: MOON_PHASE_RESOURCE_URI },
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// Fallback: text summary only
|
|
188
|
+
return { content: [{ type: "text", text: summary }] };
|
|
189
|
+
},
|
|
190
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transit-timeline-app.ts — MCP App tool registration for the Transit Timeline Explorer.
|
|
3
|
+
*
|
|
4
|
+
* Registers 1 tool:
|
|
5
|
+
* • explore_transit_timeline — primary entry point, returns data + UI resource [model]
|
|
6
|
+
*
|
|
7
|
+
* The timeline is rendered client-side from structured JSON — the tool reuses
|
|
8
|
+
* the existing `ephemeris_transits` handler logic to compute natal chart positions
|
|
9
|
+
* and search for transit events, then returns the data with a UI resource link
|
|
10
|
+
* so the iframe can render a Gantt-style 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;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transit-timeline-app.ts — MCP App tool registration for the Transit Timeline Explorer.
|
|
3
|
+
*
|
|
4
|
+
* Registers 1 tool:
|
|
5
|
+
* • explore_transit_timeline — primary entry point, returns data + UI resource [model]
|
|
6
|
+
*
|
|
7
|
+
* The timeline is rendered client-side from structured JSON — the tool reuses
|
|
8
|
+
* the existing `ephemeris_transits` handler logic to compute natal chart positions
|
|
9
|
+
* and search for transit events, then returns the data with a UI resource link
|
|
10
|
+
* so the iframe can render a Gantt-style timeline.
|
|
11
|
+
*
|
|
12
|
+
* Also exports resource helpers (getTransitTimelineBundle, etc.) for use in
|
|
13
|
+
* index.ts and server-sse.ts.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { registerTool, validateRequired, SERVER_VERSION } from "../index.js";
|
|
19
|
+
import { getActiveClient } from "../../backend/client.js";
|
|
20
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
21
|
+
export const TRANSIT_TIMELINE_RESOURCE_URI = "ui://openephemeris/transit-timeline";
|
|
22
|
+
export const TRANSIT_TIMELINE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
23
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const BUNDLE_PATHS = [
|
|
25
|
+
path.resolve(here, "..", "..", "..", "dist", "ui", "transit-timeline.html"),
|
|
26
|
+
];
|
|
27
|
+
function findBundlePath() {
|
|
28
|
+
for (const p of BUNDLE_PATHS) {
|
|
29
|
+
if (fs.existsSync(p))
|
|
30
|
+
return p;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
let cachedBundle = null;
|
|
35
|
+
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
36
|
+
export function getTransitTimelineBundle() {
|
|
37
|
+
if (cachedBundle)
|
|
38
|
+
return cachedBundle;
|
|
39
|
+
const bundlePath = findBundlePath();
|
|
40
|
+
if (!bundlePath)
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
cachedBundle = fs.readFileSync(bundlePath, "utf-8");
|
|
44
|
+
return cachedBundle;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Reset cache (useful in dev watch mode or tests). */
|
|
51
|
+
export function clearTransitTimelineBundleCache() {
|
|
52
|
+
cachedBundle = null;
|
|
53
|
+
}
|
|
54
|
+
// ── Summary builder ──────────────────────────────────────────────────────────
|
|
55
|
+
function buildTransitSummary(natalPositions, transitResult, aspectAngle, startDate, endDate) {
|
|
56
|
+
const transits = Array.isArray(transitResult)
|
|
57
|
+
? transitResult
|
|
58
|
+
: (transitResult?.events ?? transitResult?.transits ?? transitResult?.results ?? []);
|
|
59
|
+
const count = transits.length;
|
|
60
|
+
const planets = [...new Set(transits.map((t) => String(t.planet ?? "")).filter(Boolean))];
|
|
61
|
+
const natalPoints = Object.keys(natalPositions);
|
|
62
|
+
const aspectLabel = aspectAngle === 0 ? "conjunction" : aspectAngle === 180 ? "opposition" : aspectAngle === 90 ? "square" : aspectAngle === 120 ? "trine" : aspectAngle + "°";
|
|
63
|
+
let summary = "**Transit Timeline — " + startDate + " to " + endDate + "**\n\n" +
|
|
64
|
+
count + " transit event" + (count !== 1 ? "s" : "") + " found";
|
|
65
|
+
if (aspectAngle !== 0) {
|
|
66
|
+
summary += " (" + aspectLabel + " aspect)";
|
|
67
|
+
}
|
|
68
|
+
summary += ".\n";
|
|
69
|
+
if (planets.length > 0) {
|
|
70
|
+
summary += "Transiting planets: " + planets.join(", ") + "\n";
|
|
71
|
+
}
|
|
72
|
+
if (natalPoints.length > 0) {
|
|
73
|
+
summary += "Natal points targeted: " + natalPoints.join(", ") + "\n";
|
|
74
|
+
}
|
|
75
|
+
summary += "\nClick any transit bar on the timeline for details and interpretation.";
|
|
76
|
+
return summary;
|
|
77
|
+
}
|
|
78
|
+
// ── Tool: explore_transit_timeline ────────────────────────────────────────────
|
|
79
|
+
registerTool({
|
|
80
|
+
name: "explore_transit_timeline",
|
|
81
|
+
description: "Generate an interactive transit timeline visualization showing astrological transit events " +
|
|
82
|
+
"as a visual Gantt chart. Each transit appears as a colored bar on a time axis, grouped by " +
|
|
83
|
+
"transiting planet, with markers at exact aspect moments.\n\n" +
|
|
84
|
+
"This tool first calculates the natal chart to extract planetary longitudes, then searches " +
|
|
85
|
+
"for transiting planets crossing those degrees during the specified date range.\n\n" +
|
|
86
|
+
"SUPPORTED BODIES: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune, " +
|
|
87
|
+
"Pluto, Chiron, Pholus, Ceres, Pallas, Juno, Vesta, MeanNode.\n\n" +
|
|
88
|
+
"ASPECT ANGLES: 0 = conjunction/return (default), 180 = opposition, 90 = square, 120 = trine.\n\n" +
|
|
89
|
+
"Use this for a rich, interactive transit visualization in MCP Apps-capable hosts (Claude Desktop). " +
|
|
90
|
+
"Falls back to a text summary in other hosts.",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
natal_datetime: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "ISO 8601 birth datetime for the natal chart.",
|
|
97
|
+
},
|
|
98
|
+
natal_latitude: {
|
|
99
|
+
type: "number",
|
|
100
|
+
description: "Latitude of birth location in decimal degrees.",
|
|
101
|
+
},
|
|
102
|
+
natal_longitude: {
|
|
103
|
+
type: "number",
|
|
104
|
+
description: "Longitude of birth location in decimal degrees.",
|
|
105
|
+
},
|
|
106
|
+
start_date: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "Start of the transit search window, ISO 8601 date or datetime.",
|
|
109
|
+
},
|
|
110
|
+
end_date: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "End of the transit search window, ISO 8601 date or datetime.",
|
|
113
|
+
},
|
|
114
|
+
transiting_planets: {
|
|
115
|
+
type: "array",
|
|
116
|
+
items: { type: "string" },
|
|
117
|
+
description: "List of transiting planet IDs to search. Omit to search all outer planets.",
|
|
118
|
+
},
|
|
119
|
+
natal_points: {
|
|
120
|
+
type: "array",
|
|
121
|
+
items: { type: "string" },
|
|
122
|
+
description: "Natal point IDs whose longitudes should be targeted. Omit for all core points.",
|
|
123
|
+
},
|
|
124
|
+
aspect_angle: {
|
|
125
|
+
type: "number",
|
|
126
|
+
description: "Aspect angle in degrees. 0 = conjunction (default), 180 = opposition, 90 = square, 120 = trine.",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
required: ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"],
|
|
130
|
+
additionalProperties: false,
|
|
131
|
+
},
|
|
132
|
+
annotations: {
|
|
133
|
+
title: "Interactive Transit Timeline Explorer",
|
|
134
|
+
readOnlyHint: true,
|
|
135
|
+
destructiveHint: false,
|
|
136
|
+
idempotentHint: true,
|
|
137
|
+
openWorldHint: false,
|
|
138
|
+
},
|
|
139
|
+
_meta: {
|
|
140
|
+
ui: {
|
|
141
|
+
resourceUri: TRANSIT_TIMELINE_RESOURCE_URI,
|
|
142
|
+
visibility: ["model", "app"],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
handler: async (args) => {
|
|
146
|
+
validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"]);
|
|
147
|
+
const client = getActiveClient();
|
|
148
|
+
// Step 1: Compute natal chart to extract planetary longitudes
|
|
149
|
+
const natalBody = {
|
|
150
|
+
subject: {
|
|
151
|
+
name: "Transit Natal Subject",
|
|
152
|
+
birth_datetime: { iso: args.natal_datetime },
|
|
153
|
+
birth_location: {
|
|
154
|
+
latitude: { decimal: args.natal_latitude },
|
|
155
|
+
longitude: { decimal: args.natal_longitude },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
const natalResult = await client.request("POST", "/ephemeris/natal-chart", { data: natalBody });
|
|
160
|
+
const targetDegrees = [];
|
|
161
|
+
const natalPositionMap = {};
|
|
162
|
+
const planets = natalResult?.planets || natalResult?.data?.planets || [];
|
|
163
|
+
const planetArray = Array.isArray(planets) ? planets : Object.values(planets);
|
|
164
|
+
const wantedPoints = args.natal_points
|
|
165
|
+
? new Set(args.natal_points.map((p) => p.toLowerCase()))
|
|
166
|
+
: null;
|
|
167
|
+
for (const planet of planetArray) {
|
|
168
|
+
const name = (planet.name || planet.id || "").toLowerCase();
|
|
169
|
+
const lon = planet.longitude ?? planet.ecliptic_longitude ?? planet.lon;
|
|
170
|
+
if (typeof lon !== "number" || !isFinite(lon))
|
|
171
|
+
continue;
|
|
172
|
+
if (wantedPoints && !wantedPoints.has(name))
|
|
173
|
+
continue;
|
|
174
|
+
targetDegrees.push(Math.round(lon * 100) / 100);
|
|
175
|
+
natalPositionMap[name] = lon;
|
|
176
|
+
}
|
|
177
|
+
// Extract angles (ASC, MC)
|
|
178
|
+
const angles = natalResult?.angles || natalResult?.data?.angles;
|
|
179
|
+
if (angles) {
|
|
180
|
+
const angleEntries = Array.isArray(angles) ? angles : Object.entries(angles).map(([k, v]) => ({ name: k, ...(typeof v === 'object' ? v : { longitude: v }) }));
|
|
181
|
+
for (const angle of angleEntries) {
|
|
182
|
+
const name = (angle.name || angle.id || "").toLowerCase();
|
|
183
|
+
const lon = angle.longitude ?? angle.ecliptic_longitude ?? angle.lon;
|
|
184
|
+
if (typeof lon !== "number" || !isFinite(lon))
|
|
185
|
+
continue;
|
|
186
|
+
if (wantedPoints && !wantedPoints.has(name))
|
|
187
|
+
continue;
|
|
188
|
+
targetDegrees.push(Math.round(lon * 100) / 100);
|
|
189
|
+
natalPositionMap[name] = lon;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (targetDegrees.length === 0) {
|
|
193
|
+
throw new Error("Could not extract natal planet longitudes. Verify natal_datetime, natal_latitude, and natal_longitude.");
|
|
194
|
+
}
|
|
195
|
+
// Step 2: Search transits
|
|
196
|
+
let startStr = args.start_date;
|
|
197
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(startStr))
|
|
198
|
+
startStr += "T00:00:00Z";
|
|
199
|
+
let endStr = args.end_date;
|
|
200
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(endStr))
|
|
201
|
+
endStr += "T23:59:59Z";
|
|
202
|
+
const transitBody = {
|
|
203
|
+
start_date: startStr,
|
|
204
|
+
end_date: endStr,
|
|
205
|
+
target_degrees: targetDegrees,
|
|
206
|
+
};
|
|
207
|
+
if (args.transiting_planets)
|
|
208
|
+
transitBody.planet_names = args.transiting_planets;
|
|
209
|
+
let effectiveTargets = targetDegrees;
|
|
210
|
+
const aspectAngle = typeof args.aspect_angle === "number" ? args.aspect_angle : 0;
|
|
211
|
+
if (aspectAngle !== 0) {
|
|
212
|
+
effectiveTargets = targetDegrees.map((deg) => ((deg + aspectAngle) % 360 + 360) % 360);
|
|
213
|
+
transitBody.target_degrees = effectiveTargets;
|
|
214
|
+
transitBody.search_criteria = { aspect_angle: aspectAngle };
|
|
215
|
+
}
|
|
216
|
+
const transitResult = await client.request("POST", "/predictive/transits/search", { data: transitBody });
|
|
217
|
+
// Build payload
|
|
218
|
+
const payload = {
|
|
219
|
+
natal_positions: natalPositionMap,
|
|
220
|
+
target_degrees_used: effectiveTargets,
|
|
221
|
+
aspect_angle: aspectAngle,
|
|
222
|
+
transit_results: transitResult,
|
|
223
|
+
search_window: { start_date: args.start_date, end_date: args.end_date },
|
|
224
|
+
server_version: SERVER_VERSION,
|
|
225
|
+
};
|
|
226
|
+
const summary = buildTransitSummary(natalPositionMap, transitResult, aspectAngle, args.start_date, args.end_date);
|
|
227
|
+
const bundleAvailable = Boolean(getTransitTimelineBundle());
|
|
228
|
+
if (bundleAvailable) {
|
|
229
|
+
return {
|
|
230
|
+
content: [
|
|
231
|
+
{ type: "text", text: summary },
|
|
232
|
+
{ type: "text", text: JSON.stringify(payload) },
|
|
233
|
+
],
|
|
234
|
+
_meta: {
|
|
235
|
+
"ui/resourceUri": TRANSIT_TIMELINE_RESOURCE_URI,
|
|
236
|
+
ui: { resourceUri: TRANSIT_TIMELINE_RESOURCE_URI },
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// Fallback: text summary only
|
|
241
|
+
return { content: [{ type: "text", text: summary }] };
|
|
242
|
+
},
|
|
243
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vedic-chart-app.ts — MCP App tool registration for the Vedic Jyotish Chart Explorer.
|
|
3
|
+
*
|
|
4
|
+
* Registers 1 tool:
|
|
5
|
+
* • explore_vedic_chart — primary entry point [model + app visible]
|
|
6
|
+
* Calls POST /vedic/chart and returns a data payload + UI resource URI
|
|
7
|
+
* so Claude Desktop can render the interactive South Indian Rashi grid.
|
|
8
|
+
*
|
|
9
|
+
* Also exports resource helpers (getVedicChartBundle, etc.) for use in
|
|
10
|
+
* server-sse.ts resource listing and reading.
|
|
11
|
+
*/
|
|
12
|
+
export declare const VEDIC_CHART_RESOURCE_URI = "ui://openephemeris/vedic-chart";
|
|
13
|
+
export declare const VEDIC_CHART_MIME_TYPE = "text/html;profile=mcp-app";
|
|
14
|
+
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
15
|
+
export declare function getVedicChartBundle(): string | null;
|
|
16
|
+
/** Reset cache (useful in dev watch mode or tests). */
|
|
17
|
+
export declare function clearVedicChartBundleCache(): void;
|