@openephemeris/mcp-server 3.17.0 → 3.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openephemeris/mcp-server",
3
- "version": "3.17.0",
3
+ "version": "3.19.0",
4
4
  "description": "Model Context Protocol server for the Open Ephemeris astronomical computation API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -20,7 +20,7 @@
20
20
  "test": "test"
21
21
  },
22
22
  "scripts": {
23
- "build:ui": "cd src/ui/chart-wheel && npx vite build && cd ../../.. && cd src/ui/bodygraph && npx vite build && cd ../../.. && cd src/ui/bi-wheel && npx vite build && cd ../../.. && cd src/ui/transit-timeline && npx vite build && cd ../../.. && cd src/ui/moon-phase && npx vite build && cd ../../.. && cd src/ui/bazi && npx vite build && cd ../../.. && cd src/ui/vedic-chart && npx vite build",
23
+ "build:ui": "cd src/ui/chart-wheel && npx vite build && cd ../../.. && cd src/ui/bodygraph && npx vite build && cd ../../.. && cd src/ui/bi-wheel && npx vite build && cd ../../.. && cd src/ui/moon-phase && npx vite build",
24
24
  "build": "npm run build:ui && tsc -p tsconfig.build.json",
25
25
  "prebuild": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true})\"",
26
26
  "dev": "tsx watch src/index.ts",
@@ -33,6 +33,8 @@
33
33
  "validate:visual": "node scripts/visual-check.mjs",
34
34
  "test:integration": "tsx scripts/test-client.ts",
35
35
  "test:sse": "tsx scripts/test-sse-client.ts",
36
+ "test:published": "tsx scripts/test-published.ts",
37
+ "test:mcp-http": "tsx scripts/test-mcp-http.ts",
36
38
  "typecheck": "tsc --noEmit",
37
39
  "regen:dev-allowlist": "tsx scripts/dev-allowlist.ts --write",
38
40
  "check:dev-allowlist": "tsx scripts/dev-allowlist.ts --check",
@@ -1,23 +0,0 @@
1
- /**
2
- * bazi-app.ts — MCP App tool registration for the BaZi Four Pillars Explorer.
3
- *
4
- * Registers 1 tool:
5
- * • explore_bazi_chart — primary entry point, returns data + UI resource [model]
6
- *
7
- * Batches three backend API calls in parallel:
8
- * POST /chinese/bazi → four pillars
9
- * POST /chinese/bazi/ten-gods → ten gods + hidden stems
10
- * POST /chinese/bazi/element-balance → wu xing balance + yong shen
11
- *
12
- * Merges results into a single UI payload and returns it with a resource URI
13
- * so the iframe can render the interactive Four Pillars visualization.
14
- *
15
- * Also exports resource helpers (getBaziBundle, etc.) for use in
16
- * index.ts and server-sse.ts.
17
- */
18
- export declare const BAZI_RESOURCE_URI = "ui://openephemeris/bazi";
19
- export declare const BAZI_MIME_TYPE = "text/html;profile=mcp-app";
20
- /** Read the pre-built HTML bundle. Returns null if not yet built. */
21
- export declare function getBaziBundle(): string | null;
22
- /** Reset cache (useful in dev watch mode or tests). */
23
- export declare function clearBaziBundleCache(): void;
@@ -1,226 +0,0 @@
1
- /**
2
- * bazi-app.ts — MCP App tool registration for the BaZi Four Pillars Explorer.
3
- *
4
- * Registers 1 tool:
5
- * • explore_bazi_chart — primary entry point, returns data + UI resource [model]
6
- *
7
- * Batches three backend API calls in parallel:
8
- * POST /chinese/bazi → four pillars
9
- * POST /chinese/bazi/ten-gods → ten gods + hidden stems
10
- * POST /chinese/bazi/element-balance → wu xing balance + yong shen
11
- *
12
- * Merges results into a single UI payload and returns it with a resource URI
13
- * so the iframe can render the interactive Four Pillars visualization.
14
- *
15
- * Also exports resource helpers (getBaziBundle, etc.) for use in
16
- * index.ts and server-sse.ts.
17
- */
18
- import fs from "node:fs";
19
- import path from "node:path";
20
- import { fileURLToPath } from "node:url";
21
- import { registerTool, SERVER_VERSION } from "../index.js";
22
- import { getActiveClient } from "../../backend/client.js";
23
- import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
24
- // ── Constants ─────────────────────────────────────────────────────────────────
25
- export const BAZI_RESOURCE_URI = "ui://openephemeris/bazi";
26
- export const BAZI_MIME_TYPE = "text/html;profile=mcp-app";
27
- const here = path.dirname(fileURLToPath(import.meta.url));
28
- const BUNDLE_PATHS = [
29
- path.resolve(here, "..", "..", "..", "dist", "ui", "bazi.html"),
30
- ];
31
- function findBundlePath() {
32
- for (const p of BUNDLE_PATHS) {
33
- if (fs.existsSync(p))
34
- return p;
35
- }
36
- return null;
37
- }
38
- let cachedBundle = null;
39
- /** Read the pre-built HTML bundle. Returns null if not yet built. */
40
- export function getBaziBundle() {
41
- if (cachedBundle)
42
- return cachedBundle;
43
- const bundlePath = findBundlePath();
44
- if (!bundlePath)
45
- return null;
46
- try {
47
- cachedBundle = fs.readFileSync(bundlePath, "utf-8");
48
- return cachedBundle;
49
- }
50
- catch {
51
- return null;
52
- }
53
- }
54
- /** Reset cache (useful in dev watch mode or tests). */
55
- export function clearBaziBundleCache() {
56
- cachedBundle = null;
57
- }
58
- function parseBaziArgs(args) {
59
- let { year, month, day, hour } = args;
60
- if (args.datetime && (!year || !month || !day)) {
61
- const dt = new Date(args.datetime);
62
- if (!isNaN(dt.getTime())) {
63
- year = dt.getUTCFullYear();
64
- month = dt.getUTCMonth() + 1;
65
- day = dt.getUTCDate();
66
- if (hour == null)
67
- hour = dt.getUTCHours();
68
- }
69
- }
70
- if (!year || !month || !day) {
71
- throw new Error("Provide year/month/day fields, or a datetime ISO string. " +
72
- "Example: year=1987, month=7, day=15 OR datetime='1987-07-15T14:00:00Z'");
73
- }
74
- const out = { year, month, day };
75
- if (hour != null)
76
- out.hour = hour;
77
- return out;
78
- }
79
- // ── Summary builder ──────────────────────────────────────────────────────────
80
- function buildBaziSummary(data) {
81
- const chart = data.chart;
82
- const balance = data.element_balance;
83
- const tenGods = data.ten_gods;
84
- const dm = chart?.day_master;
85
- const dmStr = dm ? (dm.stem || "?") + " " + (dm.element || "?") : "Unknown";
86
- let summary = "**BaZi Four Pillars** — Day Master: **" + dmStr + "**\n\n";
87
- // Pillars summary
88
- const keys = ["year", "month", "day", "hour"];
89
- const labels = ["Year", "Month", "Day", "Hour"];
90
- if (chart) {
91
- summary += "| Pillar | Stem | Branch | Animal |\n|--------|------|--------|--------|\n";
92
- for (let i = 0; i < keys.length; i++) {
93
- const p = chart[keys[i]];
94
- if (p) {
95
- summary +=
96
- "| " + labels[i] +
97
- " | " + (p.stem?.name || "?") +
98
- " (" + (p.stem?.element || "") + ")" +
99
- " | " + (p.branch?.name || "?") +
100
- " | " + (p.branch?.animal || "") +
101
- " |\n";
102
- }
103
- }
104
- summary += "\n";
105
- }
106
- // Element balance
107
- if (balance?.elements) {
108
- const strength = balance.day_master_strength || "unknown";
109
- summary += "**Day Master Strength**: " + strength + "\n";
110
- if (balance.yong_shen_element) {
111
- summary += "**Useful God (用神)**: " + balance.yong_shen_element + "\n";
112
- }
113
- summary += "\n";
114
- }
115
- summary += "Click pillars on the chart for Ten Gods detail and interpretation.";
116
- return summary;
117
- }
118
- // ── Tool: explore_bazi_chart ─────────────────────────────────────────────────
119
- registerTool({
120
- name: "explore_bazi_chart",
121
- description: "Generate an interactive BaZi Four Pillars (四柱命盘) visualization. " +
122
- "Displays the Year, Month, Day, and Hour pillars with Heavenly Stems, " +
123
- "Earthly Branches, Hidden Stems, Ten Gods analysis, and a weighted " +
124
- "Wu Xing (Five Elements) balance bar.\n\n" +
125
- "Returns a rich interactive chart with:\n" +
126
- " • Four-column pillar grid with Chinese characters and element colours\n" +
127
- " • Day Master emphasis and badge\n" +
128
- " • Hidden Stems with strength indicators (main/secondary/residual)\n" +
129
- " • Ten Gods classification for each pillar\n" +
130
- " • Wu Xing balance bar with percentages and Yong Shen (Useful God)\n" +
131
- " • Click pillars for detailed Ten Gods drawer\n\n" +
132
- "CREDIT COST: 3 credits (batches three BaZi endpoints).\n\n" +
133
- "Use this for a rich, interactive BaZi experience in MCP Apps-capable hosts (Claude Desktop). " +
134
- "Falls back to a markdown table summary in other hosts.",
135
- inputSchema: {
136
- type: "object",
137
- properties: {
138
- year: {
139
- type: "integer",
140
- description: "Gregorian birth year, e.g. 1987.",
141
- },
142
- month: {
143
- type: "integer",
144
- description: "Birth month (1–12).",
145
- },
146
- day: {
147
- type: "integer",
148
- description: "Birth day of month (1–31).",
149
- },
150
- hour: {
151
- type: "integer",
152
- description: "Birth hour (0–23). Optional, defaults to 12 (noon). " +
153
- "Chinese shí hours are 2-hour blocks — precision within a 2-hour window is sufficient.",
154
- },
155
- datetime: {
156
- type: "string",
157
- description: "Alternative to year/month/day: ISO 8601 datetime (e.g. '1987-07-15T14:00:00Z'). " +
158
- "year/month/day/hour are extracted automatically.",
159
- },
160
- },
161
- required: [],
162
- additionalProperties: false,
163
- },
164
- outputSchema: OUTPUT_SCHEMA_JSON,
165
- annotations: {
166
- title: "Interactive BaZi Four Pillars Explorer",
167
- readOnlyHint: true,
168
- destructiveHint: false,
169
- idempotentHint: true,
170
- openWorldHint: false,
171
- },
172
- _meta: {
173
- ui: {
174
- resourceUri: BAZI_RESOURCE_URI,
175
- visibility: ["model", "app"],
176
- },
177
- },
178
- handler: async (args) => {
179
- const components = parseBaziArgs(args);
180
- const client = getActiveClient();
181
- const body = {
182
- year: components.year,
183
- month: components.month,
184
- day: components.day,
185
- };
186
- if (components.hour != null)
187
- body.hour = components.hour;
188
- // Batch all three BaZi endpoints in parallel
189
- const [chartResult, tenGodsResult, balanceResult] = await Promise.allSettled([
190
- client.request("POST", "/chinese/bazi", { data: body }),
191
- client.request("POST", "/chinese/bazi/ten-gods", { data: body }),
192
- client.request("POST", "/chinese/bazi/element-balance", { data: body }),
193
- ]);
194
- const chartData = chartResult.status === "fulfilled"
195
- ? chartResult.value
196
- : { error: chartResult.reason?.message };
197
- const tenGodsData = tenGodsResult.status === "fulfilled"
198
- ? tenGodsResult.value
199
- : { error: tenGodsResult.reason?.message };
200
- const balanceData = balanceResult.status === "fulfilled"
201
- ? balanceResult.value
202
- : { error: balanceResult.reason?.message };
203
- const uiPayload = {
204
- chart: chartData,
205
- ten_gods: tenGodsData,
206
- element_balance: balanceData,
207
- server_version: SERVER_VERSION,
208
- };
209
- const summary = buildBaziSummary(uiPayload);
210
- const bundleAvailable = Boolean(getBaziBundle());
211
- if (bundleAvailable) {
212
- return {
213
- content: [
214
- { type: "text", text: summary },
215
- { type: "text", text: JSON.stringify(uiPayload) },
216
- ],
217
- _meta: {
218
- "ui/resourceUri": BAZI_RESOURCE_URI,
219
- ui: { resourceUri: BAZI_RESOURCE_URI },
220
- },
221
- };
222
- }
223
- // Fallback: text summary only
224
- return { content: [{ type: "text", text: summary }] };
225
- },
226
- });
@@ -1,20 +0,0 @@
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;
@@ -1,295 +0,0 @@
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
- import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
21
- // ── Constants ─────────────────────────────────────────────────────────────────
22
- export const TRANSIT_TIMELINE_RESOURCE_URI = "ui://openephemeris/transit-timeline";
23
- export const TRANSIT_TIMELINE_MIME_TYPE = "text/html;profile=mcp-app";
24
- const here = path.dirname(fileURLToPath(import.meta.url));
25
- const BUNDLE_PATHS = [
26
- path.resolve(here, "..", "..", "..", "dist", "ui", "transit-timeline.html"),
27
- ];
28
- function findBundlePath() {
29
- for (const p of BUNDLE_PATHS) {
30
- if (fs.existsSync(p))
31
- return p;
32
- }
33
- return null;
34
- }
35
- let cachedBundle = null;
36
- /** Read the pre-built HTML bundle. Returns null if not yet built. */
37
- export function getTransitTimelineBundle() {
38
- if (cachedBundle)
39
- return cachedBundle;
40
- const bundlePath = findBundlePath();
41
- if (!bundlePath)
42
- return null;
43
- try {
44
- cachedBundle = fs.readFileSync(bundlePath, "utf-8");
45
- return cachedBundle;
46
- }
47
- catch {
48
- return null;
49
- }
50
- }
51
- /** Reset cache (useful in dev watch mode or tests). */
52
- export function clearTransitTimelineBundleCache() {
53
- cachedBundle = null;
54
- }
55
- // ── Summary builder ──────────────────────────────────────────────────────────
56
- function buildTransitSummary(natalPositions, transitResult, aspectAngle, startDate, endDate) {
57
- const transits = Array.isArray(transitResult)
58
- ? transitResult
59
- : (transitResult?.events ?? transitResult?.transits ?? transitResult?.results ?? []);
60
- const count = transits.length;
61
- const planets = [...new Set(transits.map((t) => String(t.planet ?? "")).filter(Boolean))];
62
- const natalPoints = Object.keys(natalPositions);
63
- const aspectLabel = aspectAngle === 0 ? "conjunction" : aspectAngle === 180 ? "opposition" : aspectAngle === 90 ? "square" : aspectAngle === 120 ? "trine" : aspectAngle + "°";
64
- let summary = "**Transit Timeline — " + startDate + " to " + endDate + "**\n\n" +
65
- count + " transit event" + (count !== 1 ? "s" : "") + " found";
66
- if (aspectAngle !== 0) {
67
- summary += " (" + aspectLabel + " aspect)";
68
- }
69
- summary += ".\n";
70
- if (planets.length > 0) {
71
- summary += "Transiting planets: " + planets.join(", ") + "\n";
72
- }
73
- if (natalPoints.length > 0) {
74
- summary += "Natal points targeted: " + natalPoints.join(", ") + "\n";
75
- }
76
- summary += "\nClick any transit bar on the timeline for details and interpretation.";
77
- return summary;
78
- }
79
- // ── Tool: explore_transit_timeline ────────────────────────────────────────────
80
- registerTool({
81
- name: "explore_transit_timeline",
82
- description: "Generate an interactive transit timeline visualization showing astrological transit events " +
83
- "as a visual Gantt chart. Each transit appears as a colored bar on a time axis, grouped by " +
84
- "transiting planet, with markers at exact aspect moments.\n\n" +
85
- "This tool first calculates the natal chart to extract planetary longitudes, then searches " +
86
- "for transiting planets crossing those degrees during the specified date range.\n\n" +
87
- "SUPPORTED BODIES: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune, " +
88
- "Pluto, Chiron, Pholus, Ceres, Pallas, Juno, Vesta, MeanNode.\n\n" +
89
- "ASPECT ANGLES: 0 = conjunction/return (default), 180 = opposition, 90 = square, 120 = trine.\n\n" +
90
- "Use this for a rich, interactive transit visualization in MCP Apps-capable hosts (Claude Desktop). " +
91
- "Falls back to a text summary in other hosts.",
92
- inputSchema: {
93
- type: "object",
94
- properties: {
95
- natal_datetime: {
96
- type: "string",
97
- description: "ISO 8601 birth datetime for the natal chart.",
98
- },
99
- natal_latitude: {
100
- type: "number",
101
- description: "Latitude of birth location in decimal degrees.",
102
- },
103
- natal_longitude: {
104
- type: "number",
105
- description: "Longitude of birth location in decimal degrees.",
106
- },
107
- start_date: {
108
- type: "string",
109
- description: "Start of the transit search window, ISO 8601 date or datetime.",
110
- },
111
- end_date: {
112
- type: "string",
113
- description: "End of the transit search window, ISO 8601 date or datetime.",
114
- },
115
- transiting_planets: {
116
- type: "array",
117
- items: { type: "string" },
118
- description: "List of transiting planet IDs to search. Omit to search all outer planets.",
119
- },
120
- natal_points: {
121
- type: "array",
122
- items: { type: "string" },
123
- description: "Natal point IDs whose longitudes should be targeted. Omit for all core points.",
124
- },
125
- aspect_angle: {
126
- type: "number",
127
- description: "Aspect angle in degrees. 0 = conjunction (default), 180 = opposition, 90 = square, 120 = trine.",
128
- },
129
- },
130
- required: ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"],
131
- additionalProperties: false,
132
- },
133
- outputSchema: OUTPUT_SCHEMA_JSON,
134
- annotations: {
135
- title: "Interactive Transit Timeline Explorer",
136
- readOnlyHint: true,
137
- destructiveHint: false,
138
- idempotentHint: true,
139
- openWorldHint: false,
140
- },
141
- _meta: {
142
- ui: {
143
- resourceUri: TRANSIT_TIMELINE_RESOURCE_URI,
144
- visibility: ["model", "app"],
145
- },
146
- },
147
- handler: async (args) => {
148
- validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"]);
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
- }
163
- // Step 1: Compute natal chart to extract planetary longitudes
164
- const natalBody = {
165
- subject: {
166
- name: "Transit Natal Subject",
167
- birth_datetime: { iso: args.natal_datetime },
168
- birth_location: {
169
- latitude: { decimal: args.natal_latitude },
170
- longitude: { decimal: args.natal_longitude },
171
- },
172
- },
173
- };
174
- const natalResult = await client.request("POST", "/ephemeris/natal-chart", { data: natalBody });
175
- const targetDegrees = [];
176
- const natalPositionMap = {};
177
- const planets = natalResult?.planets || natalResult?.data?.planets || [];
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
- ]);
186
- const wantedPoints = args.natal_points
187
- ? new Set(args.natal_points.map((p) => p.toLowerCase()))
188
- : CORE_BODIES_DEFAULT;
189
- for (const planet of planetArray) {
190
- const name = (planet.name || planet.id || "").toLowerCase();
191
- const lon = planet.longitude ?? planet.ecliptic_longitude ?? planet.lon;
192
- if (typeof lon !== "number" || !isFinite(lon))
193
- continue;
194
- if (!wantedPoints.has(name))
195
- continue;
196
- targetDegrees.push(Math.round(lon * 100) / 100);
197
- natalPositionMap[name] = lon;
198
- }
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];
230
- }
231
- }
232
- if (targetDegrees.length === 0) {
233
- throw new Error("Could not extract natal planet longitudes. Verify natal_datetime, natal_latitude, and natal_longitude.");
234
- }
235
- // Step 2: Search transits
236
- let startStr = args.start_date;
237
- if (/^\d{4}-\d{2}-\d{2}$/.test(startStr))
238
- startStr += "T00:00:00Z";
239
- let endStr = args.end_date;
240
- if (/^\d{4}-\d{2}-\d{2}$/.test(endStr))
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
- }
247
- const transitBody = {
248
- start_date: startStr,
249
- end_date: endStr,
250
- target_degrees: effectiveTargets,
251
- };
252
- if (args.transiting_planets)
253
- transitBody.planet_names = args.transiting_planets;
254
- if (aspectAngle !== 0) {
255
- transitBody.search_criteria = { aspect_angle: aspectAngle };
256
- }
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
- []);
269
- // Build payload
270
- const payload = {
271
- natal_positions: natalPositionMap,
272
- target_degrees_used: effectiveTargets,
273
- aspect_angle: aspectAngle,
274
- transit_results: transitArray,
275
- search_window: { start_date: args.start_date, end_date: args.end_date },
276
- server_version: SERVER_VERSION,
277
- };
278
- const summary = buildTransitSummary(natalPositionMap, transitArray, aspectAngle, args.start_date, args.end_date);
279
- const bundleAvailable = Boolean(getTransitTimelineBundle());
280
- if (bundleAvailable) {
281
- return {
282
- content: [
283
- { type: "text", text: summary },
284
- { type: "text", text: JSON.stringify(payload) },
285
- ],
286
- _meta: {
287
- "ui/resourceUri": TRANSIT_TIMELINE_RESOURCE_URI,
288
- ui: { resourceUri: TRANSIT_TIMELINE_RESOURCE_URI },
289
- },
290
- };
291
- }
292
- // Fallback: text summary only
293
- return { content: [{ type: "text", text: summary }] };
294
- },
295
- });
@@ -1,17 +0,0 @@
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;