@openephemeris/mcp-server 4.17.0 → 4.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 +52 -0
- package/README.md +10 -10
- package/dist/backend/client.d.ts +57 -1
- package/dist/backend/client.js +125 -15
- package/dist/index.js +8 -110
- package/dist/prompts.js +26 -24
- package/dist/server-sse.d.ts +18 -0
- package/dist/server-sse.js +48 -120
- package/dist/tools/apps/_render-token.d.ts +34 -0
- package/dist/tools/apps/_render-token.js +50 -0
- package/dist/tools/apps/bazi-app.d.ts +1 -1
- package/dist/tools/apps/bazi-app.js +39 -14
- package/dist/tools/apps/bi-wheel-app.d.ts +10 -3
- package/dist/tools/apps/bi-wheel-app.js +79 -119
- package/dist/tools/apps/bodygraph-app.d.ts +7 -4
- package/dist/tools/apps/bodygraph-app.js +101 -46
- package/dist/tools/apps/chart-wheel-app.d.ts +1 -1
- package/dist/tools/apps/chart-wheel-app.js +59 -18
- package/dist/tools/apps/location-tools.js +10 -3
- package/dist/tools/apps/moon-phase-app.d.ts +1 -1
- package/dist/tools/apps/moon-phase-app.js +13 -4
- package/dist/tools/apps/transit-timeline-app.d.ts +1 -1
- package/dist/tools/apps/transit-timeline-app.js +6 -6
- package/dist/tools/apps/ui-resource.d.ts +43 -0
- package/dist/tools/apps/ui-resource.js +66 -0
- package/dist/tools/apps/ui-resources.d.ts +22 -0
- package/dist/tools/apps/ui-resources.js +105 -0
- package/dist/tools/apps/vedic-chart-app.d.ts +1 -1
- package/dist/tools/apps/vedic-chart-app.js +13 -3
- package/dist/tools/datetime-historical.js +7 -2
- package/dist/tools/datetime.js +2 -1
- package/dist/tools/dev.js +7 -6
- package/dist/tools/specialized/electional.js +4 -3
- package/dist/tools/specialized/ephemeris_extended.js +19 -4
- package/dist/tools/specialized/hd_group.js +2 -2
- package/dist/tools/specialized/moon.d.ts +1 -1
- package/dist/tools/specialized/moon.js +51 -43
- package/dist/tools/specialized/progressed.js +2 -25
- package/dist/tools/specialized/transits.js +5 -5
- package/dist/ui/bazi.html +1199 -1161
- package/dist/ui/bi-wheel.html +573 -504
- package/dist/ui/bodygraph.html +437 -337
- package/dist/ui/chart-wheel.html +654 -584
- package/dist/ui/moon-phase.html +190 -155
- package/dist/ui/transit-timeline.html +865 -830
- package/dist/ui/vedic-chart.html +971 -934
- package/package.json +2 -1
package/dist/server-sse.d.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
+
/**
|
|
3
|
+
* The hosted server is multi-tenant: every tool call must be authenticated
|
|
4
|
+
* and metered as the connecting user. A credential in the process environment
|
|
5
|
+
* breaks that — the client interceptor sends X-Service-Key ahead of the user's
|
|
6
|
+
* key (every user unmetered), and an env API key / JWT would bill every
|
|
7
|
+
* session to one account.
|
|
8
|
+
*
|
|
9
|
+
* - A service key in the environment is a hard startup failure: there is no
|
|
10
|
+
* safe way to run with it present.
|
|
11
|
+
* - A user API key / JWT in the environment is logged loudly and ignored.
|
|
12
|
+
* - The module singleton (built from the environment at import, and what
|
|
13
|
+
* getActiveClient() falls back to outside a session context) is stripped of
|
|
14
|
+
* every credential either way; per-session clients are built with
|
|
15
|
+
* `inheritEnvCredentials: false`.
|
|
16
|
+
*
|
|
17
|
+
* Exported for tests. stdio mode never calls this.
|
|
18
|
+
*/
|
|
19
|
+
export declare function enforceHostedCredentialPolicy(env?: NodeJS.ProcessEnv): void;
|
|
2
20
|
/**
|
|
3
21
|
* Build the full Express app (auth gate, OAuth routes, /mcp Streamable HTTP,
|
|
4
22
|
* health, resources). Exported so tests can drive the REAL handlers with
|
package/dist/server-sse.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* Architecture:
|
|
9
9
|
* - The SSE server validates the user's API key at connection time by
|
|
10
10
|
* making a lightweight call to the Go backend.
|
|
11
|
-
* -
|
|
11
|
+
* - Each session gets its own BackendClient built from the validated key
|
|
12
|
+
* (never from env credentials — see enforceHostedCredentialPolicy), so
|
|
12
13
|
* all subsequent tool calls authenticate as the connecting user.
|
|
13
14
|
* - Usage credits are metered against the user's account and tier.
|
|
14
15
|
*/
|
|
@@ -26,17 +27,8 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema
|
|
|
26
27
|
import { initTools, toolRegistry, formatToolResponse, formatToolError, modelVisibleTools, parseToolSurface, describeSurface } from "./tools/index.js";
|
|
27
28
|
import { buildUiMeta } from "./tools/ui-meta.js";
|
|
28
29
|
import { SERVER_INSTRUCTIONS } from "./instructions.js";
|
|
29
|
-
import { BackendClient, runWithClient } from "./backend/client.js";
|
|
30
|
-
import {
|
|
31
|
-
import { BODYGRAPH_RESOURCE_URI, BODYGRAPH_MIME_TYPE, getBodygraphBundle, } from "./tools/apps/bodygraph-app.js";
|
|
32
|
-
import { BI_WHEEL_RESOURCE_URI, BI_WHEEL_MIME_TYPE, getBiWheelBundle, } from "./tools/apps/bi-wheel-app.js";
|
|
33
|
-
// NOTE: bazi, vedic-chart imports removed — tools disabled.
|
|
34
|
-
// NOTE: transit-timeline, bazi imports removed — tools disabled.
|
|
35
|
-
// NOTE: transit-timeline, vedic-chart imports removed — tools disabled.
|
|
36
|
-
import { MOON_PHASE_RESOURCE_URI, MOON_PHASE_MIME_TYPE, getMoonPhaseBundle, } from "./tools/apps/moon-phase-app.js";
|
|
37
|
-
import { TRANSIT_TIMELINE_RESOURCE_URI, TRANSIT_TIMELINE_MIME_TYPE, getTransitTimelineBundle, } from "./tools/apps/transit-timeline-app.js";
|
|
38
|
-
import { VEDIC_CHART_RESOURCE_URI, VEDIC_CHART_MIME_TYPE, getVedicChartBundle, } from "./tools/apps/vedic-chart-app.js";
|
|
39
|
-
import { BAZI_RESOURCE_URI, BAZI_MIME_TYPE, getBaziBundle, } from "./tools/apps/bazi-app.js";
|
|
30
|
+
import { BackendClient, backendClient, presentCredentialEnvVars, runWithClient } from "./backend/client.js";
|
|
31
|
+
import { listUiResources, readUiResource } from "./tools/apps/ui-resources.js";
|
|
40
32
|
import { oauthDiscoveryRouter, PROTECTED_RESOURCE_METADATA_URL } from "./oauth/discovery.js";
|
|
41
33
|
import { oauthDcrRouter } from "./oauth/dcr.js";
|
|
42
34
|
import { oauthTokenRouter } from "./oauth/token.js";
|
|
@@ -386,113 +378,15 @@ function createMcpServer(analyticsId = "anonymous", surface = "core", activity =
|
|
|
386
378
|
});
|
|
387
379
|
// --- Resource handlers ---
|
|
388
380
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
uri: CHART_WHEEL_RESOURCE_URI,
|
|
393
|
-
name: "Chart Wheel Explorer",
|
|
394
|
-
description: "Interactive natal chart wheel with clickable planets, houses, and aspects.",
|
|
395
|
-
mimeType: CHART_WHEEL_MIME_TYPE,
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
if (getBodygraphBundle()) {
|
|
399
|
-
resources.push({
|
|
400
|
-
uri: BODYGRAPH_RESOURCE_URI,
|
|
401
|
-
name: "Human Design Bodygraph Explorer",
|
|
402
|
-
description: "Interactive Human Design Bodygraph with clickable centers, gates, and channels.",
|
|
403
|
-
mimeType: BODYGRAPH_MIME_TYPE,
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
if (getBiWheelBundle()) {
|
|
407
|
-
resources.push({
|
|
408
|
-
uri: BI_WHEEL_RESOURCE_URI,
|
|
409
|
-
name: "Bi-Wheel Explorer",
|
|
410
|
-
description: "Interactive synastry or transit bi-wheel with cross-aspect lines and clickable planets.",
|
|
411
|
-
mimeType: BI_WHEEL_MIME_TYPE,
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
// NOTE: bazi resources omitted — tools disabled.
|
|
415
|
-
// NOTE: transit-timeline, vedic-chart resources omitted — tools disabled.
|
|
416
|
-
if (getMoonPhaseBundle()) {
|
|
417
|
-
resources.push({
|
|
418
|
-
uri: MOON_PHASE_RESOURCE_URI,
|
|
419
|
-
name: "Moon Phase Explorer",
|
|
420
|
-
description: "Interactive lunar phase dial showing illumination, sign, and void-of-course status.",
|
|
421
|
-
mimeType: MOON_PHASE_MIME_TYPE,
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
if (getTransitTimelineBundle()) {
|
|
425
|
-
resources.push({
|
|
426
|
-
uri: TRANSIT_TIMELINE_RESOURCE_URI,
|
|
427
|
-
name: "Transit Timeline Explorer",
|
|
428
|
-
description: "Interactive vertical timeline of upcoming transit hits grouped by month, with clickable events.",
|
|
429
|
-
mimeType: TRANSIT_TIMELINE_MIME_TYPE,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
if (getVedicChartBundle()) {
|
|
433
|
-
resources.push({
|
|
434
|
-
uri: VEDIC_CHART_RESOURCE_URI,
|
|
435
|
-
name: "Vedic Chart Explorer",
|
|
436
|
-
description: "Interactive Vedic (Jyotish) South Indian Rashi grid with clickable sign placements.",
|
|
437
|
-
mimeType: VEDIC_CHART_MIME_TYPE,
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
if (getBaziBundle()) {
|
|
441
|
-
resources.push({
|
|
442
|
-
uri: BAZI_RESOURCE_URI,
|
|
443
|
-
name: "BaZi Four Pillars Explorer",
|
|
444
|
-
description: "Interactive BaZi (Four Pillars of Destiny) chart with clickable Year/Month/Day/Hour pillars.",
|
|
445
|
-
mimeType: BAZI_MIME_TYPE,
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
return { resources };
|
|
381
|
+
// One registry for both transports: versioned ui:// URIs + resource _meta
|
|
382
|
+
// (prefersBorder, CSP, widgetDescription). See tools/apps/ui-resources.ts.
|
|
383
|
+
return { resources: listUiResources() };
|
|
449
384
|
});
|
|
450
385
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
451
386
|
const uri = request.params.uri;
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
throw new Error("Chart Wheel UI bundle not found. Run `npm run build:ui` to build it.");
|
|
456
|
-
return { contents: [{ uri: CHART_WHEEL_RESOURCE_URI, mimeType: CHART_WHEEL_MIME_TYPE, text: bundle }] };
|
|
457
|
-
}
|
|
458
|
-
if (uri === BODYGRAPH_RESOURCE_URI) {
|
|
459
|
-
const bundle = getBodygraphBundle();
|
|
460
|
-
if (!bundle)
|
|
461
|
-
throw new Error("Bodygraph UI bundle not found. Run `npm run build:ui` to build it.");
|
|
462
|
-
return { contents: [{ uri: BODYGRAPH_RESOURCE_URI, mimeType: BODYGRAPH_MIME_TYPE, text: bundle }] };
|
|
463
|
-
}
|
|
464
|
-
if (uri === BI_WHEEL_RESOURCE_URI) {
|
|
465
|
-
const bundle = getBiWheelBundle();
|
|
466
|
-
if (!bundle)
|
|
467
|
-
throw new Error("Bi-Wheel UI bundle not found. Run `npm run build:ui` to build it.");
|
|
468
|
-
return { contents: [{ uri: BI_WHEEL_RESOURCE_URI, mimeType: BI_WHEEL_MIME_TYPE, text: bundle }] };
|
|
469
|
-
}
|
|
470
|
-
// NOTE: bazi resources omitted — tools disabled.
|
|
471
|
-
// NOTE: transit-timeline, vedic-chart ReadResource handlers removed.
|
|
472
|
-
if (uri === MOON_PHASE_RESOURCE_URI) {
|
|
473
|
-
const bundle = getMoonPhaseBundle();
|
|
474
|
-
if (!bundle)
|
|
475
|
-
throw new Error("Moon Phase UI bundle not found. Run `npm run build:ui` to build it.");
|
|
476
|
-
return { contents: [{ uri: MOON_PHASE_RESOURCE_URI, mimeType: MOON_PHASE_MIME_TYPE, text: bundle }] };
|
|
477
|
-
}
|
|
478
|
-
if (uri === TRANSIT_TIMELINE_RESOURCE_URI) {
|
|
479
|
-
const bundle = getTransitTimelineBundle();
|
|
480
|
-
if (!bundle)
|
|
481
|
-
throw new Error("Transit Timeline UI bundle not found. Run `npm run build:ui` to build it.");
|
|
482
|
-
return { contents: [{ uri: TRANSIT_TIMELINE_RESOURCE_URI, mimeType: TRANSIT_TIMELINE_MIME_TYPE, text: bundle }] };
|
|
483
|
-
}
|
|
484
|
-
if (uri === VEDIC_CHART_RESOURCE_URI) {
|
|
485
|
-
const bundle = getVedicChartBundle();
|
|
486
|
-
if (!bundle)
|
|
487
|
-
throw new Error("Vedic Chart UI bundle not found. Run `npm run build:ui` to build it.");
|
|
488
|
-
return { contents: [{ uri: VEDIC_CHART_RESOURCE_URI, mimeType: VEDIC_CHART_MIME_TYPE, text: bundle }] };
|
|
489
|
-
}
|
|
490
|
-
if (uri === BAZI_RESOURCE_URI) {
|
|
491
|
-
const bundle = getBaziBundle();
|
|
492
|
-
if (!bundle)
|
|
493
|
-
throw new Error("BaZi UI bundle not found. Run `npm run build:ui` to build it.");
|
|
494
|
-
return { contents: [{ uri: BAZI_RESOURCE_URI, mimeType: BAZI_MIME_TYPE, text: bundle }] };
|
|
495
|
-
}
|
|
387
|
+
const result = readUiResource(uri);
|
|
388
|
+
if (result)
|
|
389
|
+
return result;
|
|
496
390
|
throw new Error(`Unknown resource: ${uri}`);
|
|
497
391
|
});
|
|
498
392
|
return server;
|
|
@@ -557,12 +451,43 @@ function startSseKeepalive(res) {
|
|
|
557
451
|
}, 25_000);
|
|
558
452
|
res.on("close", () => clearInterval(ping));
|
|
559
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* The hosted server is multi-tenant: every tool call must be authenticated
|
|
456
|
+
* and metered as the connecting user. A credential in the process environment
|
|
457
|
+
* breaks that — the client interceptor sends X-Service-Key ahead of the user's
|
|
458
|
+
* key (every user unmetered), and an env API key / JWT would bill every
|
|
459
|
+
* session to one account.
|
|
460
|
+
*
|
|
461
|
+
* - A service key in the environment is a hard startup failure: there is no
|
|
462
|
+
* safe way to run with it present.
|
|
463
|
+
* - A user API key / JWT in the environment is logged loudly and ignored.
|
|
464
|
+
* - The module singleton (built from the environment at import, and what
|
|
465
|
+
* getActiveClient() falls back to outside a session context) is stripped of
|
|
466
|
+
* every credential either way; per-session clients are built with
|
|
467
|
+
* `inheritEnvCredentials: false`.
|
|
468
|
+
*
|
|
469
|
+
* Exported for tests. stdio mode never calls this.
|
|
470
|
+
*/
|
|
471
|
+
export function enforceHostedCredentialPolicy(env = process.env) {
|
|
472
|
+
const { serviceKeys, userCredentials } = presentCredentialEnvVars(env);
|
|
473
|
+
if (serviceKeys.length > 0) {
|
|
474
|
+
throw new Error(`Refusing to start the hosted MCP HTTP server: ${serviceKeys.join(", ")} is set. ` +
|
|
475
|
+
`A service key bypasses per-user metering for every session. Unset it — the ` +
|
|
476
|
+
`hosted server authenticates each session with the user's own API key or OAuth token.`);
|
|
477
|
+
}
|
|
478
|
+
if (userCredentials.length > 0) {
|
|
479
|
+
console.error(`[SECURITY] ${userCredentials.join(", ")} is set in the hosted MCP server environment. ` +
|
|
480
|
+
`IGNORING it — sessions authenticate only with their own credential. Remove it from the deployment.`);
|
|
481
|
+
}
|
|
482
|
+
backendClient.clearCredentials();
|
|
483
|
+
}
|
|
560
484
|
/**
|
|
561
485
|
* Build the full Express app (auth gate, OAuth routes, /mcp Streamable HTTP,
|
|
562
486
|
* health, resources). Exported so tests can drive the REAL handlers with
|
|
563
487
|
* supertest instead of a re-implemented stub. Does not listen.
|
|
564
488
|
*/
|
|
565
489
|
export async function createSseApp() {
|
|
490
|
+
enforceHostedCredentialPolicy();
|
|
566
491
|
await initTools();
|
|
567
492
|
const app = express();
|
|
568
493
|
// NOTE: express.json() is applied per-route (jsonParser on /mcp POST), not
|
|
@@ -700,18 +625,19 @@ export async function createSseApp() {
|
|
|
700
625
|
serverInfo: {
|
|
701
626
|
name: "Open Ephemeris",
|
|
702
627
|
version,
|
|
703
|
-
description:
|
|
628
|
+
description: `NASA JPL DE440-backed astronomical computation engine for AI agents. ${tools.length} typed tools covering ` +
|
|
704
629
|
"natal charts, transit forecasting, Human Design bodygraphs, eclipses, astrocartography " +
|
|
705
630
|
"power lines, Venus Star Points, electional timing, synastry, composite charts, Vedic/Jyotish, " +
|
|
706
631
|
"Chinese BaZi, and more — powered by JPL DE440 ephemerides for sub-arcsecond " +
|
|
707
|
-
"zero-hallucination accuracy. Free Explorer tier
|
|
632
|
+
"zero-hallucination accuracy. Free Explorer tier: 150 one-time credits.",
|
|
708
633
|
iconUrl: "https://mcp.openephemeris.com/icon.png",
|
|
709
634
|
homepage: "https://openephemeris.com",
|
|
710
635
|
},
|
|
711
636
|
authentication: {
|
|
712
637
|
required: true,
|
|
713
638
|
schemes: ["apiKey"],
|
|
714
|
-
instructions: "
|
|
639
|
+
instructions: "Sign in with OAuth 2.1 (PKCE, dynamic client registration), or pass your Open Ephemeris API key " +
|
|
640
|
+
"via the X-API-Key header (or Authorization: Bearer opene-…). Every call is metered to that user. " +
|
|
715
641
|
"Get a free Explorer key at https://openephemeris.com/dashboard — no credit card required.",
|
|
716
642
|
},
|
|
717
643
|
tools,
|
|
@@ -871,7 +797,9 @@ export async function createSseApp() {
|
|
|
871
797
|
console.error(`[HTTP] Session initialized: ${id}`);
|
|
872
798
|
},
|
|
873
799
|
});
|
|
874
|
-
|
|
800
|
+
// inheritEnvCredentials:false — this client must authenticate as THIS
|
|
801
|
+
// session's user only, never as an env service key / API key / JWT.
|
|
802
|
+
const client = new BackendClient({ baseURL: BACKEND_URL, apiKey, jwt, inheritEnvCredentials: false });
|
|
875
803
|
const analyticsId = distinctIdFor(apiKey ?? jwt);
|
|
876
804
|
// Tool surface is fixed for the life of the session: we do not declare
|
|
877
805
|
// `tools.listChanged`, so a host has no obligation to re-fetch the list.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _render-token.ts — plumbing for the API's server-issued render token.
|
|
3
|
+
*
|
|
4
|
+
* The Go API stamps every charged visual on a render-family route
|
|
5
|
+
* (/human-design/chart + /visualization/bodygraph, /human-design/transit-chart,
|
|
6
|
+
* /human-design/composite, /chinese/bazi, /vedic/chart) with a short-lived
|
|
7
|
+
* token bound to the account and the chart-defining inputs
|
|
8
|
+
* (auth/render_token.go). Presenting it on a re-render of the SAME chart —
|
|
9
|
+
* the iframe's host light/dark reconciliation, a layout flip — makes that
|
|
10
|
+
* re-render free (base charge and visual surcharge). Anything else is charged
|
|
11
|
+
* normally, so passing a stale or foreign token is harmless.
|
|
12
|
+
*
|
|
13
|
+
* Flow: explore_* puts the token on the payload as `_render_token`; the iframe
|
|
14
|
+
* hands it back as the `render_token` argument of its recalc / refetch call;
|
|
15
|
+
* the tool sends it in the X-OE-Render-Token header.
|
|
16
|
+
*/
|
|
17
|
+
export declare const RENDER_TOKEN_HEADER = "X-OE-Render-Token";
|
|
18
|
+
/** Input-schema property for the app-only recalc args. Never set by the model. */
|
|
19
|
+
export declare const RENDER_TOKEN_PROPERTY: {
|
|
20
|
+
readonly type: "string";
|
|
21
|
+
readonly description: string;
|
|
22
|
+
};
|
|
23
|
+
/** Request headers presenting `token`, or undefined when there is none. */
|
|
24
|
+
export declare function renderTokenHeaders(token: unknown): Record<string, string> | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The token the API injected on the root <svg> of a /visualization/bodygraph
|
|
27
|
+
* response (a binary route, whose response headers the backend client does
|
|
28
|
+
* not surface).
|
|
29
|
+
*/
|
|
30
|
+
export declare function renderTokenFromSvg(svg: string | null | undefined): string | undefined;
|
|
31
|
+
/** `visual.render_token` from an include_visual JSON response. */
|
|
32
|
+
export declare function renderTokenFromVisual(visual: unknown): string | undefined;
|
|
33
|
+
/** Copy of tool args with the per-call token removed (for `_refetch.args`). */
|
|
34
|
+
export declare function withoutRenderToken<T extends Record<string, unknown>>(args: T): T;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _render-token.ts — plumbing for the API's server-issued render token.
|
|
3
|
+
*
|
|
4
|
+
* The Go API stamps every charged visual on a render-family route
|
|
5
|
+
* (/human-design/chart + /visualization/bodygraph, /human-design/transit-chart,
|
|
6
|
+
* /human-design/composite, /chinese/bazi, /vedic/chart) with a short-lived
|
|
7
|
+
* token bound to the account and the chart-defining inputs
|
|
8
|
+
* (auth/render_token.go). Presenting it on a re-render of the SAME chart —
|
|
9
|
+
* the iframe's host light/dark reconciliation, a layout flip — makes that
|
|
10
|
+
* re-render free (base charge and visual surcharge). Anything else is charged
|
|
11
|
+
* normally, so passing a stale or foreign token is harmless.
|
|
12
|
+
*
|
|
13
|
+
* Flow: explore_* puts the token on the payload as `_render_token`; the iframe
|
|
14
|
+
* hands it back as the `render_token` argument of its recalc / refetch call;
|
|
15
|
+
* the tool sends it in the X-OE-Render-Token header.
|
|
16
|
+
*/
|
|
17
|
+
export const RENDER_TOKEN_HEADER = "X-OE-Render-Token";
|
|
18
|
+
/** Input-schema property for the app-only recalc args. Never set by the model. */
|
|
19
|
+
export const RENDER_TOKEN_PROPERTY = {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Set automatically by the embedded app when it re-renders a chart it already paid for " +
|
|
22
|
+
"(e.g. to match the host's light/dark theme). Never pass this yourself.",
|
|
23
|
+
};
|
|
24
|
+
/** Request headers presenting `token`, or undefined when there is none. */
|
|
25
|
+
export function renderTokenHeaders(token) {
|
|
26
|
+
return typeof token === "string" && token.trim() !== ""
|
|
27
|
+
? { [RENDER_TOKEN_HEADER]: token.trim() }
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The token the API injected on the root <svg> of a /visualization/bodygraph
|
|
32
|
+
* response (a binary route, whose response headers the backend client does
|
|
33
|
+
* not surface).
|
|
34
|
+
*/
|
|
35
|
+
export function renderTokenFromSvg(svg) {
|
|
36
|
+
if (!svg)
|
|
37
|
+
return undefined;
|
|
38
|
+
const m = /<svg\b[^>]*\sdata-oe-render-token="([A-Za-z0-9._-]+)"/.exec(svg);
|
|
39
|
+
return m ? m[1] : undefined;
|
|
40
|
+
}
|
|
41
|
+
/** `visual.render_token` from an include_visual JSON response. */
|
|
42
|
+
export function renderTokenFromVisual(visual) {
|
|
43
|
+
const t = visual?.render_token;
|
|
44
|
+
return typeof t === "string" && t !== "" ? t : undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Copy of tool args with the per-call token removed (for `_refetch.args`). */
|
|
47
|
+
export function withoutRenderToken(args) {
|
|
48
|
+
const { render_token: _drop, ...rest } = args;
|
|
49
|
+
return rest;
|
|
50
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* bazi_annual_pillar, bazi_compatibility, bazi_chart) already exist in
|
|
17
17
|
* ../specialized/bazi.ts — this file is purely the interactive visual entry point.
|
|
18
18
|
*/
|
|
19
|
-
export declare const BAZI_RESOURCE_URI
|
|
19
|
+
export declare const BAZI_RESOURCE_URI: string;
|
|
20
20
|
export declare const BAZI_MIME_TYPE = "text/html;profile=mcp-app";
|
|
21
21
|
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
22
22
|
export declare function getBaziBundle(): string | null;
|
|
@@ -20,15 +20,17 @@ import fs from "node:fs";
|
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
23
|
+
import { UI_RESOURCE_MIME_TYPE, uiResourceUri } from "./ui-resource.js";
|
|
23
24
|
import { getActiveClient } from "../../backend/client.js";
|
|
24
25
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
25
26
|
// One BaZi component parser, shared with the specialized tools. The local copy
|
|
26
27
|
// this replaces used `new Date(naive)`, which resolves in the host process's
|
|
27
28
|
// timezone and silently shifted the hour pillar on any non-UTC server.
|
|
28
29
|
import { parseBaziArgs, buildBaziConventionFields, CONVENTION_PROPERTIES } from "../specialized/bazi.js";
|
|
30
|
+
import { RENDER_TOKEN_PROPERTY, renderTokenFromVisual, renderTokenHeaders } from "./_render-token.js";
|
|
29
31
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
30
|
-
export const BAZI_RESOURCE_URI = "
|
|
31
|
-
export const BAZI_MIME_TYPE =
|
|
32
|
+
export const BAZI_RESOURCE_URI = uiResourceUri("bazi");
|
|
33
|
+
export const BAZI_MIME_TYPE = UI_RESOURCE_MIME_TYPE;
|
|
32
34
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
33
35
|
const BUNDLE_PATHS = [
|
|
34
36
|
path.resolve(here, "..", "..", "..", "dist", "ui", "bazi.html"),
|
|
@@ -60,26 +62,39 @@ export function getBaziBundle() {
|
|
|
60
62
|
export function clearBaziBundleCache() {
|
|
61
63
|
cachedBundle = null;
|
|
62
64
|
}
|
|
65
|
+
const CONVENTION_ARG_KEYS = ["year_boundary", "day_boundary", "true_solar_time", "latitude", "longitude", "timezone"];
|
|
66
|
+
function pickConventionArgs(args) {
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const k of CONVENTION_ARG_KEYS) {
|
|
69
|
+
if (args?.[k] != null && args[k] !== "")
|
|
70
|
+
out[k] = args[k];
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
63
74
|
/**
|
|
64
75
|
* Fetch the BaZi chart via the include_visual intercept — one call returns
|
|
65
76
|
* both the structured pillar data (year/month/day/hour/day_master) and the
|
|
66
77
|
* Go-rendered SVG (bazi.RenderBaziChartSVG), same shape the strict
|
|
67
78
|
* /chinese/bazi handler returns with a `visual` key attached.
|
|
68
79
|
*/
|
|
69
|
-
async function fetchBaziChart(components, theme = "dark", conventionFields = {},
|
|
80
|
+
async function fetchBaziChart(components, theme = "dark", conventionFields = {}, renderToken) {
|
|
70
81
|
const client = getActiveClient();
|
|
71
82
|
const body = {
|
|
72
83
|
...components,
|
|
73
84
|
...conventionFields,
|
|
74
85
|
include_visual: true,
|
|
75
86
|
visual_config: { format: "svg", theme, size: 800 },
|
|
76
|
-
// Set only by bazi_recalculate's theme-reconcile path below — waives the
|
|
77
|
-
// visual surcharge server-side for a same-birth-data re-render (#551).
|
|
78
|
-
...(reconcile ? { _visual_reconcile: true } : {}),
|
|
79
87
|
};
|
|
80
|
-
|
|
88
|
+
// bazi_recalculate's theme reconcile presents the token the first render
|
|
89
|
+
// was issued; the API waives the whole re-render only when it verifies for
|
|
90
|
+
// this account and these exact inputs (the old self-serve
|
|
91
|
+
// `_visual_reconcile` flag is gone).
|
|
92
|
+
return await client.request("POST", "/chinese/bazi", {
|
|
93
|
+
data: body,
|
|
94
|
+
headers: renderTokenHeaders(renderToken),
|
|
95
|
+
});
|
|
81
96
|
}
|
|
82
|
-
function buildModelPayload(data, components, theme) {
|
|
97
|
+
function buildModelPayload(data, components, theme, conventionArgs = {}) {
|
|
83
98
|
return {
|
|
84
99
|
year: data.year,
|
|
85
100
|
month: data.month,
|
|
@@ -88,7 +103,9 @@ function buildModelPayload(data, components, theme) {
|
|
|
88
103
|
day_master: data.day_master,
|
|
89
104
|
_svg: data.visual?.data,
|
|
90
105
|
_birth_params: components,
|
|
106
|
+
...(Object.keys(conventionArgs).length ? { _convention_args: conventionArgs } : {}),
|
|
91
107
|
_theme: theme,
|
|
108
|
+
...(renderTokenFromVisual(data.visual) ? { _render_token: renderTokenFromVisual(data.visual) } : {}),
|
|
92
109
|
};
|
|
93
110
|
}
|
|
94
111
|
function buildSummary(payload) {
|
|
@@ -180,7 +197,7 @@ registerTool({
|
|
|
180
197
|
return { content: [{ type: "text", text: buildSummary(payload) }] };
|
|
181
198
|
}
|
|
182
199
|
const data = await fetchBaziChart(components, theme, conventionFields);
|
|
183
|
-
const payload = buildModelPayload(data, components, theme);
|
|
200
|
+
const payload = buildModelPayload(data, components, theme, pickConventionArgs(args));
|
|
184
201
|
const summary = buildSummary(payload);
|
|
185
202
|
// MCP Apps wire format: the UI is declared via `_meta.ui.resourceUri` and
|
|
186
203
|
// delivered through resources/read — NOT as a content block.
|
|
@@ -199,9 +216,9 @@ registerTool({
|
|
|
199
216
|
name: "bazi_recalculate",
|
|
200
217
|
description: "Recalculates a BaZi Four Pillars chart with new birth data or theme. " +
|
|
201
218
|
"App-only: called by the embedded chart itself to reconcile the initial " +
|
|
202
|
-
"server-rendered SVG to the host's actual light/dark theme.
|
|
203
|
-
"
|
|
204
|
-
"
|
|
219
|
+
"server-rendered SVG to the host's actual light/dark theme. Free when it " +
|
|
220
|
+
"presents the render_token of the chart it re-renders (same birth data and " +
|
|
221
|
+
"conventions); otherwise billed like a new chart (3 credits).",
|
|
205
222
|
inputSchema: {
|
|
206
223
|
type: "object",
|
|
207
224
|
properties: {
|
|
@@ -209,11 +226,15 @@ registerTool({
|
|
|
209
226
|
month: { type: "integer" },
|
|
210
227
|
day: { type: "integer" },
|
|
211
228
|
hour: { type: "integer" },
|
|
229
|
+
minute: { type: "integer" },
|
|
230
|
+
...CONVENTION_PROPERTIES,
|
|
231
|
+
timezone: { type: "string", description: "IANA timezone name the chart was cast in." },
|
|
212
232
|
theme: {
|
|
213
233
|
type: "string",
|
|
214
234
|
enum: ["light", "dark"],
|
|
215
235
|
description: "Render palette for the BaZi SVG. Mirrors the MCP host's light/dark color scheme.",
|
|
216
236
|
},
|
|
237
|
+
render_token: RENDER_TOKEN_PROPERTY,
|
|
217
238
|
},
|
|
218
239
|
required: ["year", "month", "day"],
|
|
219
240
|
},
|
|
@@ -223,8 +244,12 @@ registerTool({
|
|
|
223
244
|
handler: async (args) => {
|
|
224
245
|
const components = parseBaziArgs(args);
|
|
225
246
|
const theme = args.theme === "light" ? "light" : "dark";
|
|
226
|
-
|
|
227
|
-
|
|
247
|
+
// Re-cast under the SAME conventions as the original chart — dropping them
|
|
248
|
+
// (as this tool used to) rendered a different chart on any non-default
|
|
249
|
+
// year/day boundary, and would not match the render token either.
|
|
250
|
+
const conventionArgs = pickConventionArgs(args);
|
|
251
|
+
const data = await fetchBaziChart(components, theme, buildBaziConventionFields(conventionArgs), args.render_token);
|
|
252
|
+
const payload = buildModelPayload(data, components, theme, conventionArgs);
|
|
228
253
|
return {
|
|
229
254
|
content: [{ type: "text", text: JSON.stringify({ ...payload, server_version: SERVER_VERSION }) }],
|
|
230
255
|
};
|
|
@@ -12,20 +12,27 @@
|
|
|
12
12
|
* Supported modes (BiWheelMode):
|
|
13
13
|
* synastry — two natal charts (person1 inner, person2 outer)
|
|
14
14
|
* transit — natal inner + transiting planets outer
|
|
15
|
-
* progressed — natal inner + secondary progressions outer (POST /ephemeris/progressed)
|
|
15
|
+
* progressed — natal inner + secondary progressions outer (POST /ephemeris/progressed, method=secondary)
|
|
16
16
|
* solar_return — natal inner + solar return chart outer (POST /predictive/returns/solar)
|
|
17
17
|
* lunar_return — natal inner + lunar return chart outer (POST /predictive/returns/lunar)
|
|
18
|
-
* solar_arc — natal inner + solar arc directions outer (
|
|
18
|
+
* solar_arc — natal inner + solar arc directions outer (POST /ephemeris/progressed, method=solar_arc)
|
|
19
19
|
*
|
|
20
20
|
* Cross-aspects are computed client-side (UI) AND server-side (for summary/fallback).
|
|
21
21
|
* The payload cross_aspects array is capped at 30 tightest-orb aspects.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
import { getActiveClient } from "../../backend/client.js";
|
|
24
|
+
export declare const BI_WHEEL_RESOURCE_URI: string;
|
|
24
25
|
export declare const BI_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
|
|
25
26
|
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
26
27
|
export declare function getBiWheelBundle(): string | null;
|
|
27
28
|
export declare function clearBiWheelBundleCache(): void;
|
|
28
29
|
export type BiWheelMode = "synastry" | "transit" | "progressed" | "solar_return" | "lunar_return" | "solar_arc";
|
|
30
|
+
/**
|
|
31
|
+
* Route outer-wheel fetch to the correct API endpoint based on mode, and hand
|
|
32
|
+
* back a chart in the natal-chart shape (top-level planets/houses/angles):
|
|
33
|
+
* /ephemeris/progressed nests it under `data`, the return endpoints under `chart`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function fetchOuterChart(client: Pick<ReturnType<typeof getActiveClient>, "post">, mode: BiWheelMode, dt1: string, lat1: number | undefined, lon1: number | undefined, tz1: string | undefined, dt2: string, lat2: number | undefined, lon2: number | undefined, tz2: string | undefined, houseSystem?: string): Promise<Record<string, unknown>>;
|
|
29
36
|
interface PlanetPoint {
|
|
30
37
|
name: string;
|
|
31
38
|
longitude: number;
|