@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/CHANGELOG.md +57 -0
- package/LICENSE +21 -21
- package/dist/backend/client.d.ts +9 -0
- package/dist/backend/client.js +32 -5
- package/dist/index.js +12 -11
- package/dist/oauth/session-utils.d.ts +59 -0
- package/dist/oauth/session-utils.js +110 -0
- package/dist/server-sse.js +184 -20
- package/dist/tools/dev.js +1 -1
- package/dist/tools/index.d.ts +25 -0
- package/dist/tools/index.js +33 -0
- package/dist/tools/output-schemas.d.ts +1 -1
- package/dist/tools/output-schemas.js +1 -1
- package/dist/tools/specialized/acg.js +6 -2
- package/dist/tools/specialized/progressed.js +3 -1
- package/dist/ui/chart-wheel.html +15 -20
- package/dist/ui/moon-phase.html +28 -28
- package/package.json +4 -2
- package/dist/tools/apps/bazi-app.d.ts +0 -23
- package/dist/tools/apps/bazi-app.js +0 -226
- package/dist/tools/apps/transit-timeline-app.d.ts +0 -20
- package/dist/tools/apps/transit-timeline-app.js +0 -295
- package/dist/tools/apps/vedic-chart-app.d.ts +0 -17
- package/dist/tools/apps/vedic-chart-app.js +0 -228
- package/dist/ui/bazi.html +0 -213
- package/dist/ui/transit-timeline.html +0 -6874
- package/dist/ui/vedic-chart.html +0 -210
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openephemeris/mcp-server",
|
|
3
|
-
"version": "3.
|
|
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/
|
|
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;
|