@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
package/dist/server-sse.js
CHANGED
|
@@ -27,6 +27,15 @@ import { BackendClient, runWithClient } from "./backend/client.js";
|
|
|
27
27
|
import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
|
|
28
28
|
import { BODYGRAPH_RESOURCE_URI, BODYGRAPH_MIME_TYPE, getBodygraphBundle, } from "./tools/apps/bodygraph-app.js";
|
|
29
29
|
import { BI_WHEEL_RESOURCE_URI, BI_WHEEL_MIME_TYPE, getBiWheelBundle, } from "./tools/apps/bi-wheel-app.js";
|
|
30
|
+
import { TRANSIT_TIMELINE_RESOURCE_URI, TRANSIT_TIMELINE_MIME_TYPE, getTransitTimelineBundle, } from "./tools/apps/transit-timeline-app.js";
|
|
31
|
+
import { MOON_PHASE_RESOURCE_URI, MOON_PHASE_MIME_TYPE, getMoonPhaseBundle, } from "./tools/apps/moon-phase-app.js";
|
|
32
|
+
import { BAZI_RESOURCE_URI, BAZI_MIME_TYPE, getBaziBundle, } from "./tools/apps/bazi-app.js";
|
|
33
|
+
import { VEDIC_CHART_RESOURCE_URI, VEDIC_CHART_MIME_TYPE, getVedicChartBundle, } from "./tools/apps/vedic-chart-app.js";
|
|
34
|
+
import { oauthDiscoveryRouter } from "./oauth/discovery.js";
|
|
35
|
+
import { oauthDcrRouter } from "./oauth/dcr.js";
|
|
36
|
+
import { oauthTokenRouter } from "./oauth/token.js";
|
|
37
|
+
import { OAuthStore } from "./oauth/store.js";
|
|
38
|
+
import { createRateLimiter } from "./oauth/rate-limit.js";
|
|
30
39
|
// ---------------------------------------------------------------------------
|
|
31
40
|
// Helpers
|
|
32
41
|
// ---------------------------------------------------------------------------
|
|
@@ -82,18 +91,31 @@ async function validateApiKey(apiKey) {
|
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
/**
|
|
85
|
-
* Extract
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
94
|
+
* Extract authentication from a request, distinguishing JWTs from API keys.
|
|
95
|
+
*
|
|
96
|
+
* Detection heuristic:
|
|
97
|
+
* - Bearer tokens starting with "eyJ" → Supabase JWT (from OAuth flow)
|
|
98
|
+
* - Bearer tokens starting with "opene-" → API key
|
|
99
|
+
* - X-API-Key / X-OpenEphemeris-API-Key headers → API key
|
|
100
|
+
* - ?apiKey query param → API key
|
|
101
|
+
*
|
|
102
|
+
* Returns { apiKey?, jwt? } — at most one will be set.
|
|
90
103
|
*/
|
|
91
|
-
function
|
|
92
|
-
|
|
104
|
+
function extractAuth(req) {
|
|
105
|
+
// Explicit API key headers take precedence
|
|
106
|
+
const explicitKey = req.headers["x-api-key"] ||
|
|
93
107
|
req.headers["x-openephemeris-api-key"] ||
|
|
94
|
-
req.
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
req.query.apiKey;
|
|
109
|
+
if (explicitKey)
|
|
110
|
+
return { apiKey: explicitKey };
|
|
111
|
+
// Bearer token — distinguish JWT from API key by prefix
|
|
112
|
+
const bearer = req.headers["authorization"]?.replace(/^Bearer\s+/i, "");
|
|
113
|
+
if (bearer) {
|
|
114
|
+
if (bearer.startsWith("eyJ"))
|
|
115
|
+
return { jwt: bearer }; // Supabase JWT
|
|
116
|
+
return { apiKey: bearer }; // API key
|
|
117
|
+
}
|
|
118
|
+
return {};
|
|
97
119
|
}
|
|
98
120
|
// ---------------------------------------------------------------------------
|
|
99
121
|
// Helpers
|
|
@@ -282,6 +304,38 @@ function createMcpServer() {
|
|
|
282
304
|
mimeType: BI_WHEEL_MIME_TYPE,
|
|
283
305
|
});
|
|
284
306
|
}
|
|
307
|
+
if (getTransitTimelineBundle()) {
|
|
308
|
+
resources.push({
|
|
309
|
+
uri: TRANSIT_TIMELINE_RESOURCE_URI,
|
|
310
|
+
name: "Transit Timeline Explorer",
|
|
311
|
+
description: "Interactive Gantt-style transit timeline showing planetary aspects over time.",
|
|
312
|
+
mimeType: TRANSIT_TIMELINE_MIME_TYPE,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
if (getMoonPhaseBundle()) {
|
|
316
|
+
resources.push({
|
|
317
|
+
uri: MOON_PHASE_RESOURCE_URI,
|
|
318
|
+
name: "Moon Phase Explorer",
|
|
319
|
+
description: "Interactive lunar phase dial showing illumination, sign, and void-of-course status.",
|
|
320
|
+
mimeType: MOON_PHASE_MIME_TYPE,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (getBaziBundle()) {
|
|
324
|
+
resources.push({
|
|
325
|
+
uri: BAZI_RESOURCE_URI,
|
|
326
|
+
name: "BaZi Four Pillars Explorer",
|
|
327
|
+
description: "Interactive BaZi chart with pillars, Ten Gods, hidden stems, and Wu Xing balance.",
|
|
328
|
+
mimeType: BAZI_MIME_TYPE,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (getVedicChartBundle()) {
|
|
332
|
+
resources.push({
|
|
333
|
+
uri: VEDIC_CHART_RESOURCE_URI,
|
|
334
|
+
name: "Vedic Jyotish Chart",
|
|
335
|
+
description: "Interactive South Indian Rashi grid with planet placements, Lagna marker, and Nakshatra detail.",
|
|
336
|
+
mimeType: VEDIC_CHART_MIME_TYPE,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
285
339
|
return { resources };
|
|
286
340
|
});
|
|
287
341
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
@@ -304,6 +358,30 @@ function createMcpServer() {
|
|
|
304
358
|
throw new Error("Bi-Wheel UI bundle not found. Run `npm run build:ui` to build it.");
|
|
305
359
|
return { contents: [{ uri: BI_WHEEL_RESOURCE_URI, mimeType: BI_WHEEL_MIME_TYPE, text: bundle }] };
|
|
306
360
|
}
|
|
361
|
+
if (uri === TRANSIT_TIMELINE_RESOURCE_URI) {
|
|
362
|
+
const bundle = getTransitTimelineBundle();
|
|
363
|
+
if (!bundle)
|
|
364
|
+
throw new Error("Transit Timeline UI bundle not found. Run `npm run build:ui` to build it.");
|
|
365
|
+
return { contents: [{ uri: TRANSIT_TIMELINE_RESOURCE_URI, mimeType: TRANSIT_TIMELINE_MIME_TYPE, text: bundle }] };
|
|
366
|
+
}
|
|
367
|
+
if (uri === MOON_PHASE_RESOURCE_URI) {
|
|
368
|
+
const bundle = getMoonPhaseBundle();
|
|
369
|
+
if (!bundle)
|
|
370
|
+
throw new Error("Moon Phase UI bundle not found. Run `npm run build:ui` to build it.");
|
|
371
|
+
return { contents: [{ uri: MOON_PHASE_RESOURCE_URI, mimeType: MOON_PHASE_MIME_TYPE, text: bundle }] };
|
|
372
|
+
}
|
|
373
|
+
if (uri === BAZI_RESOURCE_URI) {
|
|
374
|
+
const bundle = getBaziBundle();
|
|
375
|
+
if (!bundle)
|
|
376
|
+
throw new Error("BaZi UI bundle not found. Run `npm run build:ui` to build it.");
|
|
377
|
+
return { contents: [{ uri: BAZI_RESOURCE_URI, mimeType: BAZI_MIME_TYPE, text: bundle }] };
|
|
378
|
+
}
|
|
379
|
+
if (uri === VEDIC_CHART_RESOURCE_URI) {
|
|
380
|
+
const bundle = getVedicChartBundle();
|
|
381
|
+
if (!bundle)
|
|
382
|
+
throw new Error("Vedic Chart UI bundle not found. Run `npm run build:ui` to build it.");
|
|
383
|
+
return { contents: [{ uri: VEDIC_CHART_RESOURCE_URI, mimeType: VEDIC_CHART_MIME_TYPE, text: bundle }] };
|
|
384
|
+
}
|
|
307
385
|
throw new Error(`Unknown resource: ${uri}`);
|
|
308
386
|
});
|
|
309
387
|
return server;
|
|
@@ -330,6 +408,19 @@ async function main() {
|
|
|
330
408
|
const transports = new Map();
|
|
331
409
|
// Per-session BackendClient — each connection authenticates independently.
|
|
332
410
|
const sessionClients = new Map();
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
// OAuth 2.1 routes — must be mounted BEFORE JSON body parser
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
const oauthStore = new OAuthStore();
|
|
415
|
+
const tokenRateLimiter = createRateLimiter({
|
|
416
|
+
windowMs: 60_000,
|
|
417
|
+
maxRequests: 10,
|
|
418
|
+
trustProxy: true,
|
|
419
|
+
});
|
|
420
|
+
app.use(oauthDiscoveryRouter);
|
|
421
|
+
app.use(express.json({ limit: "1mb" }), oauthDcrRouter);
|
|
422
|
+
app.use("/oauth/token", tokenRateLimiter);
|
|
423
|
+
app.use(express.urlencoded({ extended: false }), oauthTokenRouter(oauthStore));
|
|
333
424
|
// Health check
|
|
334
425
|
app.get("/health", (_req, res) => {
|
|
335
426
|
res.json({
|
|
@@ -407,22 +498,26 @@ async function main() {
|
|
|
407
498
|
});
|
|
408
499
|
return;
|
|
409
500
|
}
|
|
410
|
-
// Auth — required on Initialize
|
|
411
|
-
const apiKey =
|
|
412
|
-
if (!apiKey) {
|
|
501
|
+
// Auth — required on Initialize (supports both API keys and OAuth JWTs)
|
|
502
|
+
const { apiKey, jwt } = extractAuth(req);
|
|
503
|
+
if (!apiKey && !jwt) {
|
|
413
504
|
res.status(401).json({
|
|
414
|
-
error: "
|
|
415
|
-
message: "
|
|
505
|
+
error: "auth_required",
|
|
506
|
+
message: "Authentication required. Pass an API key via X-API-Key header, or a Bearer token via Authorization header. Get a free key at https://openephemeris.com/dashboard",
|
|
416
507
|
});
|
|
417
508
|
return;
|
|
418
509
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
510
|
+
// Validate API keys against the Go backend; JWTs are validated downstream
|
|
511
|
+
// by the Go sidecar's ValidateSupabaseJWT when tool calls are proxied.
|
|
512
|
+
if (apiKey) {
|
|
513
|
+
const valid = await validateApiKey(apiKey);
|
|
514
|
+
if (!valid) {
|
|
515
|
+
res.status(403).json({
|
|
516
|
+
error: "invalid_api_key",
|
|
517
|
+
message: "The provided API key is invalid or inactive. Check your key at https://openephemeris.com/dashboard?tab=apikeys",
|
|
518
|
+
});
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
426
521
|
}
|
|
427
522
|
// Create per-session transport, server, and client
|
|
428
523
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -432,7 +527,7 @@ async function main() {
|
|
|
432
527
|
console.error(`[HTTP] Session initialized: ${id}`);
|
|
433
528
|
},
|
|
434
529
|
});
|
|
435
|
-
const client = new BackendClient({ baseURL: BACKEND_URL, apiKey });
|
|
530
|
+
const client = new BackendClient({ baseURL: BACKEND_URL, apiKey, jwt });
|
|
436
531
|
const server = createMcpServer();
|
|
437
532
|
transport.onclose = () => {
|
|
438
533
|
if (transport.sessionId) {
|
|
@@ -511,23 +606,25 @@ async function main() {
|
|
|
511
606
|
// ---------------------------------------------------------------------------
|
|
512
607
|
// SSE endpoint — clients GET /sse to establish a stream
|
|
513
608
|
app.get("/sse", async (req, res) => {
|
|
514
|
-
// Extract
|
|
515
|
-
const apiKey =
|
|
516
|
-
if (!apiKey) {
|
|
609
|
+
// Extract auth from query param or header (supports API keys + OAuth JWTs)
|
|
610
|
+
const { apiKey, jwt } = extractAuth(req);
|
|
611
|
+
if (!apiKey && !jwt) {
|
|
517
612
|
res.status(401).json({
|
|
518
|
-
error: "
|
|
519
|
-
message: "
|
|
613
|
+
error: "auth_required",
|
|
614
|
+
message: "Authentication required. Pass an API key via X-API-Key header or query param, or a Bearer JWT via Authorization header. Get a free key at https://openephemeris.com/dashboard?tab=apikeys",
|
|
520
615
|
});
|
|
521
616
|
return;
|
|
522
617
|
}
|
|
523
|
-
// Validate
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
618
|
+
// Validate API keys against the Go backend; JWTs validated downstream.
|
|
619
|
+
if (apiKey) {
|
|
620
|
+
const valid = await validateApiKey(apiKey);
|
|
621
|
+
if (!valid) {
|
|
622
|
+
res.status(403).json({
|
|
623
|
+
error: "invalid_api_key",
|
|
624
|
+
message: "The provided API key is invalid or inactive. Check your key at https://openephemeris.com/dashboard?tab=apikeys",
|
|
625
|
+
});
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
531
628
|
}
|
|
532
629
|
const transport = new SSEServerTransport("/message", res);
|
|
533
630
|
const sessionId = transport.sessionId;
|
|
@@ -537,6 +634,7 @@ async function main() {
|
|
|
537
634
|
const sessionClient = new BackendClient({
|
|
538
635
|
baseURL: BACKEND_URL,
|
|
539
636
|
apiKey,
|
|
637
|
+
jwt,
|
|
540
638
|
});
|
|
541
639
|
sessionClients.set(sessionId, sessionClient);
|
|
542
640
|
const server = createMcpServer();
|
|
@@ -0,0 +1,23 @@
|
|
|
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;
|
|
@@ -0,0 +1,224 @@
|
|
|
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
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
24
|
+
export const BAZI_RESOURCE_URI = "ui://openephemeris/bazi";
|
|
25
|
+
export const BAZI_MIME_TYPE = "text/html;profile=mcp-app";
|
|
26
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const BUNDLE_PATHS = [
|
|
28
|
+
path.resolve(here, "..", "..", "..", "dist", "ui", "bazi.html"),
|
|
29
|
+
];
|
|
30
|
+
function findBundlePath() {
|
|
31
|
+
for (const p of BUNDLE_PATHS) {
|
|
32
|
+
if (fs.existsSync(p))
|
|
33
|
+
return p;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
let cachedBundle = null;
|
|
38
|
+
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
39
|
+
export function getBaziBundle() {
|
|
40
|
+
if (cachedBundle)
|
|
41
|
+
return cachedBundle;
|
|
42
|
+
const bundlePath = findBundlePath();
|
|
43
|
+
if (!bundlePath)
|
|
44
|
+
return null;
|
|
45
|
+
try {
|
|
46
|
+
cachedBundle = fs.readFileSync(bundlePath, "utf-8");
|
|
47
|
+
return cachedBundle;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Reset cache (useful in dev watch mode or tests). */
|
|
54
|
+
export function clearBaziBundleCache() {
|
|
55
|
+
cachedBundle = null;
|
|
56
|
+
}
|
|
57
|
+
function parseBaziArgs(args) {
|
|
58
|
+
let { year, month, day, hour } = args;
|
|
59
|
+
if (args.datetime && (!year || !month || !day)) {
|
|
60
|
+
const dt = new Date(args.datetime);
|
|
61
|
+
if (!isNaN(dt.getTime())) {
|
|
62
|
+
year = dt.getUTCFullYear();
|
|
63
|
+
month = dt.getUTCMonth() + 1;
|
|
64
|
+
day = dt.getUTCDate();
|
|
65
|
+
if (hour == null)
|
|
66
|
+
hour = dt.getUTCHours();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!year || !month || !day) {
|
|
70
|
+
throw new Error("Provide year/month/day fields, or a datetime ISO string. " +
|
|
71
|
+
"Example: year=1987, month=7, day=15 OR datetime='1987-07-15T14:00:00Z'");
|
|
72
|
+
}
|
|
73
|
+
const out = { year, month, day };
|
|
74
|
+
if (hour != null)
|
|
75
|
+
out.hour = hour;
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
// ── Summary builder ──────────────────────────────────────────────────────────
|
|
79
|
+
function buildBaziSummary(data) {
|
|
80
|
+
const chart = data.chart;
|
|
81
|
+
const balance = data.element_balance;
|
|
82
|
+
const tenGods = data.ten_gods;
|
|
83
|
+
const dm = chart?.day_master;
|
|
84
|
+
const dmStr = dm ? (dm.stem || "?") + " " + (dm.element || "?") : "Unknown";
|
|
85
|
+
let summary = "**BaZi Four Pillars** — Day Master: **" + dmStr + "**\n\n";
|
|
86
|
+
// Pillars summary
|
|
87
|
+
const keys = ["year", "month", "day", "hour"];
|
|
88
|
+
const labels = ["Year", "Month", "Day", "Hour"];
|
|
89
|
+
if (chart) {
|
|
90
|
+
summary += "| Pillar | Stem | Branch | Animal |\n|--------|------|--------|--------|\n";
|
|
91
|
+
for (let i = 0; i < keys.length; i++) {
|
|
92
|
+
const p = chart[keys[i]];
|
|
93
|
+
if (p) {
|
|
94
|
+
summary +=
|
|
95
|
+
"| " + labels[i] +
|
|
96
|
+
" | " + (p.stem?.name || "?") +
|
|
97
|
+
" (" + (p.stem?.element || "") + ")" +
|
|
98
|
+
" | " + (p.branch?.name || "?") +
|
|
99
|
+
" | " + (p.branch?.animal || "") +
|
|
100
|
+
" |\n";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
summary += "\n";
|
|
104
|
+
}
|
|
105
|
+
// Element balance
|
|
106
|
+
if (balance?.elements) {
|
|
107
|
+
const strength = balance.day_master_strength || "unknown";
|
|
108
|
+
summary += "**Day Master Strength**: " + strength + "\n";
|
|
109
|
+
if (balance.yong_shen_element) {
|
|
110
|
+
summary += "**Useful God (用神)**: " + balance.yong_shen_element + "\n";
|
|
111
|
+
}
|
|
112
|
+
summary += "\n";
|
|
113
|
+
}
|
|
114
|
+
summary += "Click pillars on the chart for Ten Gods detail and interpretation.";
|
|
115
|
+
return summary;
|
|
116
|
+
}
|
|
117
|
+
// ── Tool: explore_bazi_chart ─────────────────────────────────────────────────
|
|
118
|
+
registerTool({
|
|
119
|
+
name: "explore_bazi_chart",
|
|
120
|
+
description: "Generate an interactive BaZi Four Pillars (四柱命盘) visualization. " +
|
|
121
|
+
"Displays the Year, Month, Day, and Hour pillars with Heavenly Stems, " +
|
|
122
|
+
"Earthly Branches, Hidden Stems, Ten Gods analysis, and a weighted " +
|
|
123
|
+
"Wu Xing (Five Elements) balance bar.\n\n" +
|
|
124
|
+
"Returns a rich interactive chart with:\n" +
|
|
125
|
+
" • Four-column pillar grid with Chinese characters and element colours\n" +
|
|
126
|
+
" • Day Master emphasis and badge\n" +
|
|
127
|
+
" • Hidden Stems with strength indicators (main/secondary/residual)\n" +
|
|
128
|
+
" • Ten Gods classification for each pillar\n" +
|
|
129
|
+
" • Wu Xing balance bar with percentages and Yong Shen (Useful God)\n" +
|
|
130
|
+
" • Click pillars for detailed Ten Gods drawer\n\n" +
|
|
131
|
+
"CREDIT COST: 3 credits (batches three BaZi endpoints).\n\n" +
|
|
132
|
+
"Use this for a rich, interactive BaZi experience in MCP Apps-capable hosts (Claude Desktop). " +
|
|
133
|
+
"Falls back to a markdown table summary in other hosts.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
year: {
|
|
138
|
+
type: "integer",
|
|
139
|
+
description: "Gregorian birth year, e.g. 1987.",
|
|
140
|
+
},
|
|
141
|
+
month: {
|
|
142
|
+
type: "integer",
|
|
143
|
+
description: "Birth month (1–12).",
|
|
144
|
+
},
|
|
145
|
+
day: {
|
|
146
|
+
type: "integer",
|
|
147
|
+
description: "Birth day of month (1–31).",
|
|
148
|
+
},
|
|
149
|
+
hour: {
|
|
150
|
+
type: "integer",
|
|
151
|
+
description: "Birth hour (0–23). Optional, defaults to 12 (noon). " +
|
|
152
|
+
"Chinese shí hours are 2-hour blocks — precision within a 2-hour window is sufficient.",
|
|
153
|
+
},
|
|
154
|
+
datetime: {
|
|
155
|
+
type: "string",
|
|
156
|
+
description: "Alternative to year/month/day: ISO 8601 datetime (e.g. '1987-07-15T14:00:00Z'). " +
|
|
157
|
+
"year/month/day/hour are extracted automatically.",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: [],
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
},
|
|
163
|
+
annotations: {
|
|
164
|
+
title: "Interactive BaZi Four Pillars Explorer",
|
|
165
|
+
readOnlyHint: true,
|
|
166
|
+
destructiveHint: false,
|
|
167
|
+
idempotentHint: true,
|
|
168
|
+
openWorldHint: false,
|
|
169
|
+
},
|
|
170
|
+
_meta: {
|
|
171
|
+
ui: {
|
|
172
|
+
resourceUri: BAZI_RESOURCE_URI,
|
|
173
|
+
visibility: ["model", "app"],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
handler: async (args) => {
|
|
177
|
+
const components = parseBaziArgs(args);
|
|
178
|
+
const client = getActiveClient();
|
|
179
|
+
const body = {
|
|
180
|
+
year: components.year,
|
|
181
|
+
month: components.month,
|
|
182
|
+
day: components.day,
|
|
183
|
+
};
|
|
184
|
+
if (components.hour != null)
|
|
185
|
+
body.hour = components.hour;
|
|
186
|
+
// Batch all three BaZi endpoints in parallel
|
|
187
|
+
const [chartResult, tenGodsResult, balanceResult] = await Promise.allSettled([
|
|
188
|
+
client.request("POST", "/chinese/bazi", { data: body }),
|
|
189
|
+
client.request("POST", "/chinese/bazi/ten-gods", { data: body }),
|
|
190
|
+
client.request("POST", "/chinese/bazi/element-balance", { data: body }),
|
|
191
|
+
]);
|
|
192
|
+
const chartData = chartResult.status === "fulfilled"
|
|
193
|
+
? chartResult.value
|
|
194
|
+
: { error: chartResult.reason?.message };
|
|
195
|
+
const tenGodsData = tenGodsResult.status === "fulfilled"
|
|
196
|
+
? tenGodsResult.value
|
|
197
|
+
: { error: tenGodsResult.reason?.message };
|
|
198
|
+
const balanceData = balanceResult.status === "fulfilled"
|
|
199
|
+
? balanceResult.value
|
|
200
|
+
: { error: balanceResult.reason?.message };
|
|
201
|
+
const uiPayload = {
|
|
202
|
+
chart: chartData,
|
|
203
|
+
ten_gods: tenGodsData,
|
|
204
|
+
element_balance: balanceData,
|
|
205
|
+
server_version: SERVER_VERSION,
|
|
206
|
+
};
|
|
207
|
+
const summary = buildBaziSummary(uiPayload);
|
|
208
|
+
const bundleAvailable = Boolean(getBaziBundle());
|
|
209
|
+
if (bundleAvailable) {
|
|
210
|
+
return {
|
|
211
|
+
content: [
|
|
212
|
+
{ type: "text", text: summary },
|
|
213
|
+
{ type: "text", text: JSON.stringify(uiPayload) },
|
|
214
|
+
],
|
|
215
|
+
_meta: {
|
|
216
|
+
"ui/resourceUri": BAZI_RESOURCE_URI,
|
|
217
|
+
ui: { resourceUri: BAZI_RESOURCE_URI },
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// Fallback: text summary only
|
|
222
|
+
return { content: [{ type: "text", text: summary }] };
|
|
223
|
+
},
|
|
224
|
+
});
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* bi-wheel-app.ts — MCP App tool registration for the Bi-Wheel Explorer.
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
5
|
-
* • explore_bi_wheel — primary entry point
|
|
6
|
-
*
|
|
4
|
+
* Registers 6 tools:
|
|
5
|
+
* • explore_bi_wheel — primary entry point [model + app]
|
|
6
|
+
* • bi_wheel_recalculate — re-fetch with new params [app-only]
|
|
7
7
|
* • bi_wheel_on_cross_aspect_click — cross-aspect click handler [app-only]
|
|
8
|
-
* • bi_wheel_on_planet_click — planet click handler
|
|
8
|
+
* • bi_wheel_on_planet_click — planet click handler [app-only]
|
|
9
|
+
* • bi_wheel_on_house_click — house cusp click handler [app-only]
|
|
10
|
+
* • bi_wheel_synopsis — full reading overview [model + app]
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
* synastry
|
|
12
|
-
* transit
|
|
12
|
+
* Supported modes (BiWheelMode):
|
|
13
|
+
* synastry — two natal charts (person1 inner, person2 outer)
|
|
14
|
+
* transit — natal inner + transiting planets outer
|
|
15
|
+
* progressed — natal inner + secondary progressions outer (POST /ephemeris/progressed)
|
|
16
|
+
* solar_return — natal inner + solar return chart outer (POST /predictive/returns/solar)
|
|
17
|
+
* lunar_return — natal inner + lunar return chart outer (POST /predictive/returns/lunar)
|
|
18
|
+
* solar_arc — natal inner + solar arc directions outer (client-side Naibod approximation)
|
|
13
19
|
*
|
|
14
20
|
* Cross-aspects are computed client-side (UI) AND server-side (for summary/fallback).
|
|
15
21
|
* The payload cross_aspects array is capped at 30 tightest-orb aspects.
|
|
@@ -19,6 +25,7 @@ export declare const BI_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
|
|
|
19
25
|
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
20
26
|
export declare function getBiWheelBundle(): string | null;
|
|
21
27
|
export declare function clearBiWheelBundleCache(): void;
|
|
28
|
+
export type BiWheelMode = "synastry" | "transit" | "progressed" | "solar_return" | "lunar_return" | "solar_arc";
|
|
22
29
|
interface PlanetPoint {
|
|
23
30
|
name: string;
|
|
24
31
|
longitude: number;
|