@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.
Files changed (61) hide show
  1. package/README.md +2 -2
  2. package/config/dev-allowlist.json +28 -4
  3. package/dist/index.js +45 -0
  4. package/dist/oauth/dcr.d.ts +11 -0
  5. package/dist/oauth/dcr.js +49 -0
  6. package/dist/oauth/discovery.d.ts +11 -0
  7. package/dist/oauth/discovery.js +40 -0
  8. package/dist/oauth/pkce.d.ts +18 -0
  9. package/dist/oauth/pkce.js +34 -0
  10. package/dist/oauth/rate-limit.d.ts +20 -0
  11. package/dist/oauth/rate-limit.js +65 -0
  12. package/dist/oauth/store.d.ts +68 -0
  13. package/dist/oauth/store.js +247 -0
  14. package/dist/oauth/supabase-jwt.d.ts +39 -0
  15. package/dist/oauth/supabase-jwt.js +194 -0
  16. package/dist/oauth/token.d.ts +21 -0
  17. package/dist/oauth/token.js +213 -0
  18. package/dist/prompts.js +171 -159
  19. package/dist/server-sse.js +134 -36
  20. package/dist/tools/apps/bazi-app.d.ts +23 -0
  21. package/dist/tools/apps/bazi-app.js +224 -0
  22. package/dist/tools/apps/bi-wheel-app.d.ts +14 -7
  23. package/dist/tools/apps/bi-wheel-app.js +293 -108
  24. package/dist/tools/apps/bodygraph-app.js +38 -3
  25. package/dist/tools/apps/moon-phase-app.d.ts +19 -0
  26. package/dist/tools/apps/moon-phase-app.js +190 -0
  27. package/dist/tools/apps/transit-timeline-app.d.ts +20 -0
  28. package/dist/tools/apps/transit-timeline-app.js +243 -0
  29. package/dist/tools/apps/vedic-chart-app.d.ts +17 -0
  30. package/dist/tools/apps/vedic-chart-app.js +226 -0
  31. package/dist/tools/index.js +4 -0
  32. package/dist/tools/specialized/acg.js +2 -2
  33. package/dist/tools/specialized/bazi.js +367 -44
  34. package/dist/tools/specialized/bi_wheel.js +1 -1
  35. package/dist/tools/specialized/chart_wheel.js +1 -1
  36. package/dist/tools/specialized/comparative.js +4 -4
  37. package/dist/tools/specialized/eclipse.js +1 -1
  38. package/dist/tools/specialized/electional.js +4 -4
  39. package/dist/tools/specialized/ephemeris_core.js +2 -2
  40. package/dist/tools/specialized/ephemeris_extended.js +8 -8
  41. package/dist/tools/specialized/hd_bodygraph.js +1 -1
  42. package/dist/tools/specialized/hd_cycles.js +2 -2
  43. package/dist/tools/specialized/hd_group.js +2 -2
  44. package/dist/tools/specialized/human_design.js +1 -1
  45. package/dist/tools/specialized/moon.js +2 -2
  46. package/dist/tools/specialized/natal.js +1 -1
  47. package/dist/tools/specialized/progressed.js +1 -1
  48. package/dist/tools/specialized/relocation.js +1 -1
  49. package/dist/tools/specialized/returns.js +3 -3
  50. package/dist/tools/specialized/synastry.js +1 -1
  51. package/dist/tools/specialized/transits.js +1 -1
  52. package/dist/tools/specialized/vedic.js +1 -1
  53. package/dist/tools/specialized/venus_star_points.js +6 -6
  54. package/dist/ui/bazi.html +213 -0
  55. package/dist/ui/bi-wheel.html +762 -489
  56. package/dist/ui/bodygraph.html +1788 -1649
  57. package/dist/ui/chart-wheel.html +1772 -1593
  58. package/dist/ui/moon-phase.html +6758 -0
  59. package/dist/ui/transit-timeline.html +6835 -0
  60. package/dist/ui/vedic-chart.html +210 -0
  61. 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
- // keep the maps
238
- const personality_activations = !Array.isArray(rawP) && typeof rawP === 'object' ? rawP : undefined;
239
- const design_activations = !Array.isArray(rawD) && typeof rawD === 'object' ? rawD : undefined;
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;