@openephemeris/mcp-server 3.13.10 → 3.14.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 +39 -0
- package/README.md +3 -3
- package/dist/index.js +5 -43
- package/dist/oauth/discovery.d.ts +1 -0
- package/dist/oauth/discovery.js +8 -3
- package/dist/oauth/store.d.ts +20 -0
- package/dist/oauth/store.js +117 -3
- package/dist/oauth/token.js +4 -2
- package/dist/prompts.js +9 -0
- package/dist/server-sse.js +71 -64
- package/dist/tools/apps/bazi-app.js +2 -0
- package/dist/tools/apps/bi-wheel-app.js +55 -6
- package/dist/tools/apps/bodygraph-app.js +68 -10
- package/dist/tools/apps/chart-wheel-app.js +53 -4
- package/dist/tools/apps/location-tools.js +3 -0
- package/dist/tools/apps/moon-phase-app.js +75 -21
- package/dist/tools/apps/transit-timeline-app.js +75 -23
- package/dist/tools/apps/vedic-chart-app.js +2 -0
- package/dist/tools/auth.js +4 -0
- package/dist/tools/dev.js +3 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.js +8 -7
- package/dist/tools/output-schemas.d.ts +37 -0
- package/dist/tools/output-schemas.js +130 -0
- package/dist/tools/specialized/acg.js +3 -0
- package/dist/tools/specialized/bazi.js +8 -0
- package/dist/tools/specialized/bi_wheel.js +2 -0
- package/dist/tools/specialized/chart_wheel.js +2 -0
- package/dist/tools/specialized/comparative.js +5 -0
- package/dist/tools/specialized/eclipse.js +2 -0
- package/dist/tools/specialized/electional.js +5 -0
- package/dist/tools/specialized/ephemeris_core.js +3 -0
- package/dist/tools/specialized/ephemeris_extended.js +9 -0
- package/dist/tools/specialized/hd_bodygraph.js +2 -0
- package/dist/tools/specialized/hd_cycles.js +3 -0
- package/dist/tools/specialized/hd_group.js +3 -0
- package/dist/tools/specialized/human_design.js +2 -0
- package/dist/tools/specialized/moon.js +3 -0
- package/dist/tools/specialized/natal.js +2 -0
- package/dist/tools/specialized/progressed.js +2 -0
- package/dist/tools/specialized/relocation.js +2 -0
- package/dist/tools/specialized/returns.js +4 -0
- package/dist/tools/specialized/synastry.js +2 -0
- package/dist/tools/specialized/transits.js +52 -43
- package/dist/tools/specialized/vedic.js +2 -0
- package/dist/tools/specialized/venus_star_points.js +7 -0
- package/dist/ui/bazi.html +1 -1
- package/dist/ui/bi-wheel.html +3514 -3121
- package/dist/ui/bodygraph.html +1308 -1261
- package/dist/ui/chart-wheel.html +3404 -3116
- package/dist/ui/moon-phase.html +2622 -2616
- package/dist/ui/transit-timeline.html +2695 -2656
- package/dist/ui/vedic-chart.html +1 -1
- package/package.json +14 -11
- package/smithery.yaml +16 -1
package/dist/server-sse.js
CHANGED
|
@@ -27,11 +27,9 @@ 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
|
-
|
|
30
|
+
// NOTE: transit-timeline, bazi, vedic-chart imports removed — tools disabled.
|
|
31
31
|
import { MOON_PHASE_RESOURCE_URI, MOON_PHASE_MIME_TYPE, getMoonPhaseBundle, } from "./tools/apps/moon-phase-app.js";
|
|
32
|
-
import {
|
|
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";
|
|
32
|
+
import { oauthDiscoveryRouter, PROTECTED_RESOURCE_METADATA_URL } from "./oauth/discovery.js";
|
|
35
33
|
import { oauthDcrRouter } from "./oauth/dcr.js";
|
|
36
34
|
import { oauthTokenRouter } from "./oauth/token.js";
|
|
37
35
|
import { OAuthStore } from "./oauth/store.js";
|
|
@@ -232,7 +230,15 @@ function createMcpServer() {
|
|
|
232
230
|
annotations: buildAnnotations(tool),
|
|
233
231
|
// Expose MCP Apps UI linkage so Claude Desktop can prefetch the resource
|
|
234
232
|
...(tool._meta?.ui?.resourceUri
|
|
235
|
-
? {
|
|
233
|
+
? {
|
|
234
|
+
_meta: {
|
|
235
|
+
"ui/resourceUri": tool._meta.ui.resourceUri,
|
|
236
|
+
ui: {
|
|
237
|
+
resourceUri: tool._meta.ui.resourceUri,
|
|
238
|
+
visibility: tool._meta.ui.visibility ?? ["model", "app"],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
}
|
|
236
242
|
: {}),
|
|
237
243
|
})),
|
|
238
244
|
}));
|
|
@@ -264,16 +270,18 @@ function createMcpServer() {
|
|
|
264
270
|
}
|
|
265
271
|
catch (error) {
|
|
266
272
|
const startTime = Date.now(); // Not strictly accurate for errors but acceptable for logging structure if needed
|
|
273
|
+
const durationMs = Date.now() - startTime;
|
|
267
274
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
268
|
-
|
|
275
|
+
const isRetryable = error.retryable === true || error.status === 401 || error.code === "auth_required";
|
|
276
|
+
console.error(`[MCP] ❌ Failed: ${toolName} (${durationMs}ms) - ${errorMessage}`);
|
|
269
277
|
return {
|
|
270
278
|
content: [
|
|
271
279
|
{
|
|
272
280
|
type: "text",
|
|
273
|
-
text:
|
|
281
|
+
text: `Tool execution failed:\n\n${errorMessage}\n\nPlease read the error above carefully and assist the user (e.g. by providing them the auth link or explaining the issue).`,
|
|
274
282
|
},
|
|
275
283
|
],
|
|
276
|
-
isError:
|
|
284
|
+
isError: !isRetryable,
|
|
277
285
|
};
|
|
278
286
|
}
|
|
279
287
|
});
|
|
@@ -304,14 +312,7 @@ function createMcpServer() {
|
|
|
304
312
|
mimeType: BI_WHEEL_MIME_TYPE,
|
|
305
313
|
});
|
|
306
314
|
}
|
|
307
|
-
|
|
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
|
+
// NOTE: transit-timeline, bazi, vedic-chart resources omitted — tools disabled.
|
|
315
316
|
if (getMoonPhaseBundle()) {
|
|
316
317
|
resources.push({
|
|
317
318
|
uri: MOON_PHASE_RESOURCE_URI,
|
|
@@ -320,22 +321,6 @@ function createMcpServer() {
|
|
|
320
321
|
mimeType: MOON_PHASE_MIME_TYPE,
|
|
321
322
|
});
|
|
322
323
|
}
|
|
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
|
-
}
|
|
339
324
|
return { resources };
|
|
340
325
|
});
|
|
341
326
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
@@ -358,30 +343,13 @@ function createMcpServer() {
|
|
|
358
343
|
throw new Error("Bi-Wheel UI bundle not found. Run `npm run build:ui` to build it.");
|
|
359
344
|
return { contents: [{ uri: BI_WHEEL_RESOURCE_URI, mimeType: BI_WHEEL_MIME_TYPE, text: bundle }] };
|
|
360
345
|
}
|
|
361
|
-
|
|
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
|
-
}
|
|
346
|
+
// NOTE: transit-timeline, bazi, vedic-chart ReadResource handlers removed.
|
|
367
347
|
if (uri === MOON_PHASE_RESOURCE_URI) {
|
|
368
348
|
const bundle = getMoonPhaseBundle();
|
|
369
349
|
if (!bundle)
|
|
370
350
|
throw new Error("Moon Phase UI bundle not found. Run `npm run build:ui` to build it.");
|
|
371
351
|
return { contents: [{ uri: MOON_PHASE_RESOURCE_URI, mimeType: MOON_PHASE_MIME_TYPE, text: bundle }] };
|
|
372
352
|
}
|
|
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
|
-
}
|
|
385
353
|
throw new Error(`Unknown resource: ${uri}`);
|
|
386
354
|
});
|
|
387
355
|
return server;
|
|
@@ -393,12 +361,38 @@ async function main() {
|
|
|
393
361
|
// NOTE: Do NOT use express.json() globally — SSEServerTransport.handlePostMessage
|
|
394
362
|
// reads the raw request stream itself. Pre-parsing with express.json() consumes it,
|
|
395
363
|
// causing "stream is not readable" errors.
|
|
396
|
-
// CORS
|
|
397
|
-
|
|
398
|
-
|
|
364
|
+
// CORS + Origin allowlist
|
|
365
|
+
// Per Anthropic remote-MCP review criteria, browser-originated requests must
|
|
366
|
+
// be validated against an allowlist. Native (no-Origin) clients like Claude
|
|
367
|
+
// Desktop and CLI tooling are permitted; unknown browser origins are rejected.
|
|
368
|
+
const ORIGIN_ALLOWLIST = [
|
|
369
|
+
/^https:\/\/([a-z0-9-]+\.)*claude\.ai$/i,
|
|
370
|
+
/^https:\/\/([a-z0-9-]+\.)*claude\.com$/i,
|
|
371
|
+
/^https:\/\/([a-z0-9-]+\.)*anthropic\.com$/i,
|
|
372
|
+
/^https:\/\/([a-z0-9-]+\.)*openephemeris\.com$/i,
|
|
373
|
+
/^http:\/\/localhost(:\d+)?$/i,
|
|
374
|
+
/^http:\/\/127\.0\.0\.1(:\d+)?$/i,
|
|
375
|
+
];
|
|
376
|
+
function isOriginAllowed(origin) {
|
|
377
|
+
return ORIGIN_ALLOWLIST.some((m) => typeof m === "string" ? m === origin : m.test(origin));
|
|
378
|
+
}
|
|
379
|
+
app.use((req, res, next) => {
|
|
380
|
+
const origin = req.headers.origin;
|
|
381
|
+
// Browser request → must match allowlist. Native client (no Origin) → allow.
|
|
382
|
+
if (origin) {
|
|
383
|
+
if (!isOriginAllowed(origin)) {
|
|
384
|
+
res.status(403).json({
|
|
385
|
+
error: "origin_not_allowed",
|
|
386
|
+
error_description: `Origin '${origin}' is not permitted.`,
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
391
|
+
res.setHeader("Vary", "Origin");
|
|
392
|
+
}
|
|
399
393
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
400
394
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-OpenEphemeris-API-Key, mcp-session-id");
|
|
401
|
-
if (
|
|
395
|
+
if (req.method === "OPTIONS") {
|
|
402
396
|
res.status(204).end();
|
|
403
397
|
return;
|
|
404
398
|
}
|
|
@@ -417,7 +411,13 @@ async function main() {
|
|
|
417
411
|
maxRequests: 10,
|
|
418
412
|
trustProxy: true,
|
|
419
413
|
});
|
|
414
|
+
const dcrRateLimiter = createRateLimiter({
|
|
415
|
+
windowMs: 60_000,
|
|
416
|
+
maxRequests: 5,
|
|
417
|
+
trustProxy: true,
|
|
418
|
+
});
|
|
420
419
|
app.use(oauthDiscoveryRouter);
|
|
420
|
+
app.use("/oauth/register", dcrRateLimiter);
|
|
421
421
|
app.use(express.json({ limit: "1mb" }), oauthDcrRouter);
|
|
422
422
|
app.use("/oauth/token", tokenRateLimiter);
|
|
423
423
|
app.use(express.urlencoded({ extended: false }), oauthTokenRouter(oauthStore));
|
|
@@ -441,6 +441,7 @@ async function main() {
|
|
|
441
441
|
name: tool.name,
|
|
442
442
|
description: tool.description,
|
|
443
443
|
inputSchema: tool.inputSchema,
|
|
444
|
+
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
444
445
|
annotations: buildAnnotations(tool),
|
|
445
446
|
}));
|
|
446
447
|
res.json({
|
|
@@ -490,17 +491,14 @@ async function main() {
|
|
|
490
491
|
await runWithClient(session.client, () => session.transport.handleRequest(req, res, req.body));
|
|
491
492
|
return;
|
|
492
493
|
}
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
});
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
// Auth — required on Initialize (supports both API keys and OAuth JWTs)
|
|
494
|
+
// Auth check FIRST — before validating request shape. RFC 9728 requires
|
|
495
|
+
// any unauthenticated POST to /mcp (even a probe with an empty/invalid
|
|
496
|
+
// body) to return 401 + WWW-Authenticate so OAuth clients (Claude Web)
|
|
497
|
+
// can bootstrap discovery. Returning 400 before auth causes the ofid_
|
|
498
|
+
// "Authorization with MCP server failed" error in Claude's connector UI.
|
|
502
499
|
const { apiKey, jwt } = extractAuth(req);
|
|
503
500
|
if (!apiKey && !jwt) {
|
|
501
|
+
res.set("WWW-Authenticate", `Bearer realm="OpenEphemeris MCP", resource_metadata="${PROTECTED_RESOURCE_METADATA_URL}"`);
|
|
504
502
|
res.status(401).json({
|
|
505
503
|
error: "auth_required",
|
|
506
504
|
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",
|
|
@@ -512,13 +510,22 @@ async function main() {
|
|
|
512
510
|
if (apiKey) {
|
|
513
511
|
const valid = await validateApiKey(apiKey);
|
|
514
512
|
if (!valid) {
|
|
515
|
-
res.
|
|
513
|
+
res.set("WWW-Authenticate", `Bearer realm="OpenEphemeris MCP", error="invalid_token", resource_metadata="${PROTECTED_RESOURCE_METADATA_URL}"`);
|
|
514
|
+
res.status(401).json({
|
|
516
515
|
error: "invalid_api_key",
|
|
517
516
|
message: "The provided API key is invalid or inactive. Check your key at https://openephemeris.com/dashboard?tab=apikeys",
|
|
518
517
|
});
|
|
519
518
|
return;
|
|
520
519
|
}
|
|
521
520
|
}
|
|
521
|
+
// New session — must be an initialize request
|
|
522
|
+
if (!isInitializeRequest(req.body)) {
|
|
523
|
+
res.status(400).json({
|
|
524
|
+
error: "expected_initialize",
|
|
525
|
+
message: "First request to /mcp must be an MCP Initialize request.",
|
|
526
|
+
});
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
522
529
|
// Create per-session transport, server, and client
|
|
523
530
|
const transport = new StreamableHTTPServerTransport({
|
|
524
531
|
sessionIdGenerator: randomUUID,
|
|
@@ -20,6 +20,7 @@ import path from "node:path";
|
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
22
22
|
import { getActiveClient } from "../../backend/client.js";
|
|
23
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
23
24
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
24
25
|
export const BAZI_RESOURCE_URI = "ui://openephemeris/bazi";
|
|
25
26
|
export const BAZI_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -160,6 +161,7 @@ registerTool({
|
|
|
160
161
|
required: [],
|
|
161
162
|
additionalProperties: false,
|
|
162
163
|
},
|
|
164
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
163
165
|
annotations: {
|
|
164
166
|
title: "Interactive BaZi Four Pillars Explorer",
|
|
165
167
|
readOnlyHint: true,
|
|
@@ -25,6 +25,7 @@ import path from "node:path";
|
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
27
27
|
import { getActiveClient } from "../../backend/client.js";
|
|
28
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
28
29
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
29
30
|
export const BI_WHEEL_RESOURCE_URI = "ui://openephemeris/bi-wheel";
|
|
30
31
|
export const BI_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -100,7 +101,7 @@ const HOUSE_SYSTEM_MAP = {
|
|
|
100
101
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
101
102
|
campanus: "C", regiomontanus: "R",
|
|
102
103
|
};
|
|
103
|
-
function buildNatalBody(datetime, lat, lon, timezone) {
|
|
104
|
+
function buildNatalBody(datetime, lat, lon, timezone, houseSystem = "placidus") {
|
|
104
105
|
const body = {
|
|
105
106
|
subject: {
|
|
106
107
|
name: "MCP Request",
|
|
@@ -111,7 +112,7 @@ function buildNatalBody(datetime, lat, lon, timezone) {
|
|
|
111
112
|
},
|
|
112
113
|
},
|
|
113
114
|
configuration: {
|
|
114
|
-
house_system: HOUSE_SYSTEM_MAP["placidus"],
|
|
115
|
+
house_system: HOUSE_SYSTEM_MAP[houseSystem] ?? HOUSE_SYSTEM_MAP["placidus"],
|
|
115
116
|
},
|
|
116
117
|
};
|
|
117
118
|
if (timezone) {
|
|
@@ -275,19 +276,39 @@ export function computeCrossAspects(planets1, planets2) {
|
|
|
275
276
|
return results.sort((a, b) => a.orb - b.orb).slice(0, 30);
|
|
276
277
|
}
|
|
277
278
|
// ── Planet normalisation ───────────────────────────────────────────────────────
|
|
279
|
+
// Canonical underscore names accepted by the renderer.
|
|
278
280
|
const CLASSICAL_PLANETS = new Set([
|
|
279
281
|
"sun", "moon", "mercury", "venus", "mars",
|
|
280
282
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
281
283
|
"chiron", "north_node", "south_node", "true_node", "asc", "mc",
|
|
282
284
|
]);
|
|
285
|
+
/**
|
|
286
|
+
* Normalise the Go backend's verbose planet key (e.g. "North Node (Mean)")
|
|
287
|
+
* to the canonical lowercase underscore form the renderer expects.
|
|
288
|
+
*/
|
|
289
|
+
function canonicalizePlanetName(raw) {
|
|
290
|
+
const lower = raw.toLowerCase();
|
|
291
|
+
// Various node/chiron aliases the Go PlanetName() function emits
|
|
292
|
+
if (lower === "north node (mean)" || lower === "north node (true)" || lower === "mean node" || lower === "true node")
|
|
293
|
+
return "north_node";
|
|
294
|
+
if (lower === "south node (mean)" || lower === "south node (true)")
|
|
295
|
+
return "south_node";
|
|
296
|
+
if (lower === "mean apogee" || lower === "black moon lilith" || lower === "lilith (mean)" || lower === "lilith (true)")
|
|
297
|
+
return "lilith";
|
|
298
|
+
// Strip parenthetical qualifiers for any other bodies just in case
|
|
299
|
+
return lower.replace(/\s*\(.*?\)/g, "").replace(/\s+/g, "_");
|
|
300
|
+
}
|
|
283
301
|
function normalizePlanets(raw) {
|
|
284
302
|
if (Array.isArray(raw))
|
|
285
303
|
return raw;
|
|
286
304
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
287
305
|
return Object.entries(raw)
|
|
288
|
-
.filter(([k]) =>
|
|
306
|
+
.filter(([k]) => {
|
|
307
|
+
const canonical = canonicalizePlanetName(k);
|
|
308
|
+
return CLASSICAL_PLANETS.has(canonical);
|
|
309
|
+
})
|
|
289
310
|
.map(([name, p]) => ({
|
|
290
|
-
name: name
|
|
311
|
+
name: canonicalizePlanetName(name),
|
|
291
312
|
longitude: (p.longitude ?? p.lon ?? 0),
|
|
292
313
|
speed: (p.longitude_speed ?? p.speed),
|
|
293
314
|
retrograde: Boolean(p.is_retrograde ?? p.retrograde),
|
|
@@ -309,14 +330,30 @@ function normalizeHouses(raw) {
|
|
|
309
330
|
return [];
|
|
310
331
|
}
|
|
311
332
|
// ── Payload builder ────────────────────────────────────────────────────────────
|
|
333
|
+
function extractAngles(data) {
|
|
334
|
+
const rawAngles = data.angles;
|
|
335
|
+
const rawHouseObj = data.houses;
|
|
336
|
+
const ascendant = rawAngles?.ascendant ??
|
|
337
|
+
data.ascendant ??
|
|
338
|
+
rawHouseObj?.cusps?.[0] ??
|
|
339
|
+
0;
|
|
340
|
+
const midheaven = rawAngles?.midheaven ??
|
|
341
|
+
data.midheaven ??
|
|
342
|
+
rawHouseObj?.cusps?.[9] ??
|
|
343
|
+
0;
|
|
344
|
+
return { ascendant, midheaven };
|
|
345
|
+
}
|
|
312
346
|
function buildBiWheelPayload(innerData, outerData, mode, params1, params2) {
|
|
313
347
|
const innerPlanets = normalizePlanets(innerData.planets);
|
|
314
348
|
const outerPlanets = normalizePlanets(outerData.planets);
|
|
315
349
|
const innerHouses = normalizeHouses(innerData.houses);
|
|
316
350
|
const outerHouses = normalizeHouses(outerData.houses);
|
|
317
351
|
const crossAspects = computeCrossAspects(innerPlanets, outerPlanets);
|
|
352
|
+
const { ascendant, midheaven } = extractAngles(innerData);
|
|
318
353
|
return {
|
|
319
354
|
mode,
|
|
355
|
+
ascendant,
|
|
356
|
+
midheaven,
|
|
320
357
|
inner: innerPlanets,
|
|
321
358
|
outer: outerPlanets,
|
|
322
359
|
inner_houses: innerHouses,
|
|
@@ -413,6 +450,7 @@ registerTool({
|
|
|
413
450
|
required: ["person1_datetime", "person2_datetime",
|
|
414
451
|
"person1_latitude", "person1_longitude"],
|
|
415
452
|
},
|
|
453
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
416
454
|
annotations: {
|
|
417
455
|
title: "Interactive Bi-Wheel Explorer",
|
|
418
456
|
readOnlyHint: true,
|
|
@@ -487,14 +525,21 @@ registerTool({
|
|
|
487
525
|
person2_timezone: { type: "string" },
|
|
488
526
|
person2_name: { type: "string" },
|
|
489
527
|
location: { type: "string" },
|
|
528
|
+
house_system: {
|
|
529
|
+
type: "string",
|
|
530
|
+
enum: Object.keys(HOUSE_SYSTEM_MAP),
|
|
531
|
+
description: "House system for the inner (natal) wheel. Defaults to placidus.",
|
|
532
|
+
},
|
|
490
533
|
},
|
|
491
534
|
required: ["mode", "person1_datetime", "person1_latitude", "person1_longitude", "person2_datetime"],
|
|
492
535
|
},
|
|
536
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
493
537
|
annotations: { title: "Recalculate Bi-Wheel", readOnlyHint: true, openWorldHint: false },
|
|
494
538
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
495
539
|
handler: async (args) => {
|
|
496
540
|
const client = getActiveClient();
|
|
497
541
|
const mode = args.mode ?? "synastry";
|
|
542
|
+
const houseSystem = args.house_system ?? "placidus";
|
|
498
543
|
const dt1 = String(args.person1_datetime);
|
|
499
544
|
const lat1 = args.person1_latitude;
|
|
500
545
|
const lon1 = args.person1_longitude;
|
|
@@ -506,14 +551,14 @@ registerTool({
|
|
|
506
551
|
const loc1 = String(args.person1_name ?? args.location ?? `${lat1 ?? "?"},${lon1 ?? "?"}`);
|
|
507
552
|
const loc2 = MODE_META[mode].outerLabel(dt2, args.person2_name);
|
|
508
553
|
const [innerData, outerData] = await Promise.all([
|
|
509
|
-
client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
|
|
554
|
+
client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1, houseSystem)),
|
|
510
555
|
fetchOuterChart(client, mode, dt1, lat1, lon1, tz1, dt2, lat2, lon2, tz2),
|
|
511
556
|
]);
|
|
512
557
|
const params1 = { datetime: dt1, timezone: tz1 ?? null, latitude: lat1 ?? null, longitude: lon1 ?? null, location: loc1 };
|
|
513
558
|
const params2 = { datetime: dt2, timezone: tz2 ?? null, latitude: lat2 ?? null, longitude: lon2 ?? null, location: loc2 };
|
|
514
559
|
const payload = buildBiWheelPayload(innerData, outerData, mode, params1, params2);
|
|
515
560
|
return {
|
|
516
|
-
content: [{ type: "text", text: JSON.stringify({ ...payload, server_version: SERVER_VERSION }) }],
|
|
561
|
+
content: [{ type: "text", text: JSON.stringify({ ...payload, house_system: houseSystem, server_version: SERVER_VERSION }) }],
|
|
517
562
|
};
|
|
518
563
|
},
|
|
519
564
|
});
|
|
@@ -537,6 +582,7 @@ registerTool({
|
|
|
537
582
|
},
|
|
538
583
|
required: ["planet1", "planet2", "aspect_type"],
|
|
539
584
|
},
|
|
585
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
540
586
|
annotations: { title: "Cross-Aspect Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
541
587
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
542
588
|
handler: async (args) => {
|
|
@@ -575,6 +621,7 @@ registerTool({
|
|
|
575
621
|
},
|
|
576
622
|
required: ["planet", "wheel", "longitude"],
|
|
577
623
|
},
|
|
624
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
578
625
|
annotations: { title: "Bi-Wheel Planet Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
579
626
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
580
627
|
handler: async (args) => {
|
|
@@ -775,6 +822,7 @@ registerTool({
|
|
|
775
822
|
},
|
|
776
823
|
required: ["house_number"],
|
|
777
824
|
},
|
|
825
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
778
826
|
annotations: { title: "Bi-Wheel House Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
779
827
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
780
828
|
handler: async (args) => {
|
|
@@ -834,6 +882,7 @@ registerTool({
|
|
|
834
882
|
},
|
|
835
883
|
required: ["mode"],
|
|
836
884
|
},
|
|
885
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
837
886
|
annotations: { title: "Bi-Wheel Synopsis", readOnlyHint: true, openWorldHint: false },
|
|
838
887
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app", "model"] } },
|
|
839
888
|
handler: async (args) => {
|
|
@@ -19,6 +19,7 @@ import path from "node:path";
|
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
21
21
|
import { getActiveClient } from "../../backend/client.js";
|
|
22
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
22
23
|
// ── Constants ─────────────────────────────────────────────────────────────
|
|
23
24
|
export const BODYGRAPH_RESOURCE_URI = "ui://openephemeris/bodygraph";
|
|
24
25
|
export const BODYGRAPH_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -65,6 +66,40 @@ function ensureTimezone(dt) {
|
|
|
65
66
|
return dt;
|
|
66
67
|
return dt + "Z";
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Fetch the canonical bodygraph SVG from the Go renderer.
|
|
71
|
+
*
|
|
72
|
+
* The Go engine is the source of truth for bodygraph visuals (Phase 4 of the
|
|
73
|
+
* renderer unification). This SVG is inlined directly into the iframe and
|
|
74
|
+
* bound to interaction handlers via its data-* attributes (data-center,
|
|
75
|
+
* data-gate, data-channel, data-hanging, data-highlighted, etc.).
|
|
76
|
+
*
|
|
77
|
+
* Returns null on failure so the caller can fall back to the legacy client-side
|
|
78
|
+
* renderer in bodygraph-renderer.ts. Non-fatal: a missing SVG should never
|
|
79
|
+
* block the tool result.
|
|
80
|
+
*/
|
|
81
|
+
async function fetchGoBodygraphSVG(body) {
|
|
82
|
+
try {
|
|
83
|
+
const client = getActiveClient();
|
|
84
|
+
// /visualization/bodygraph is registered as binary in BINARY_ENDPOINT_PREFIXES.
|
|
85
|
+
// The client wraps it as { content_type, encoding: "base64", data_base64 } —
|
|
86
|
+
// we decode back to UTF-8 SVG text for inlining.
|
|
87
|
+
const resp = await client.post("/visualization/bodygraph?format=svg&size=800", body);
|
|
88
|
+
if (typeof resp === "string")
|
|
89
|
+
return resp;
|
|
90
|
+
if (resp && typeof resp === "object") {
|
|
91
|
+
const r = resp;
|
|
92
|
+
if (typeof r.data_base64 === "string") {
|
|
93
|
+
return Buffer.from(r.data_base64, "base64").toString("utf-8");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.warn("[bodygraph] Go SVG fetch failed, will fall back to client render:", err);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
68
103
|
/**
|
|
69
104
|
* Convert a local datetime string (no offset) to a UTC ISO 8601 string.
|
|
70
105
|
* Uses the IANA timezone to compute the correct UTC offset.
|
|
@@ -349,6 +384,7 @@ registerTool({
|
|
|
349
384
|
},
|
|
350
385
|
required: ["datetime"],
|
|
351
386
|
},
|
|
387
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
352
388
|
annotations: {
|
|
353
389
|
title: "Interactive Human Design Bodygraph Explorer",
|
|
354
390
|
readOnlyHint: true,
|
|
@@ -376,9 +412,18 @@ registerTool({
|
|
|
376
412
|
body.latitude = lat;
|
|
377
413
|
if (lon != null)
|
|
378
414
|
body.longitude = lon;
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
415
|
+
const bundleAvailable = Boolean(getBodygraphBundle());
|
|
416
|
+
// Fetch chart data and Go-rendered SVG in parallel. SVG only fetched when
|
|
417
|
+
// the iframe bundle is available (text-fallback hosts have no use for it,
|
|
418
|
+
// and the visualization endpoint costs a credit reservation).
|
|
419
|
+
const [chartData, svg] = await Promise.all([
|
|
420
|
+
client.request("POST", "/human-design/chart", {
|
|
421
|
+
data: body,
|
|
422
|
+
}),
|
|
423
|
+
bundleAvailable
|
|
424
|
+
? fetchGoBodygraphSVG(body)
|
|
425
|
+
: Promise.resolve(null),
|
|
426
|
+
]);
|
|
382
427
|
// API returns { chart: {...}, metadata: {...} } — unwrap the inner chart object
|
|
383
428
|
const chart = chartData.chart ?? chartData;
|
|
384
429
|
// Build payload once — summary and model data both derive from the same object
|
|
@@ -389,8 +434,9 @@ registerTool({
|
|
|
389
434
|
latitude: lat ?? null,
|
|
390
435
|
longitude: lon ?? null,
|
|
391
436
|
});
|
|
437
|
+
if (svg)
|
|
438
|
+
modelPayload._svg = svg;
|
|
392
439
|
const summary = buildHdSummary(modelPayload, location);
|
|
393
|
-
const bundleAvailable = Boolean(getBodygraphBundle());
|
|
394
440
|
if (bundleAvailable) {
|
|
395
441
|
return {
|
|
396
442
|
content: [
|
|
@@ -425,6 +471,7 @@ registerTool({
|
|
|
425
471
|
},
|
|
426
472
|
required: ["datetime"],
|
|
427
473
|
},
|
|
474
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
428
475
|
annotations: { title: "Recalculate Bodygraph", readOnlyHint: true, openWorldHint: false },
|
|
429
476
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
430
477
|
handler: async (args) => {
|
|
@@ -441,9 +488,13 @@ registerTool({
|
|
|
441
488
|
body.latitude = lat;
|
|
442
489
|
if (lon != null)
|
|
443
490
|
body.longitude = lon;
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
491
|
+
// Recalculate is iframe-only (visibility: ["app"]) — always co-fetch the SVG.
|
|
492
|
+
const [chartData, svg] = await Promise.all([
|
|
493
|
+
client.request("POST", "/human-design/chart", {
|
|
494
|
+
data: body,
|
|
495
|
+
}),
|
|
496
|
+
fetchGoBodygraphSVG(body),
|
|
497
|
+
]);
|
|
447
498
|
const chart = chartData.chart ?? chartData;
|
|
448
499
|
const modelPayload = buildHdModelPayload(chart, {
|
|
449
500
|
datetime,
|
|
@@ -452,6 +503,8 @@ registerTool({
|
|
|
452
503
|
latitude: lat ?? null,
|
|
453
504
|
longitude: lon ?? null,
|
|
454
505
|
});
|
|
506
|
+
if (svg)
|
|
507
|
+
modelPayload._svg = svg;
|
|
455
508
|
return {
|
|
456
509
|
content: [{ type: "text", text: JSON.stringify({ ...modelPayload, server_version: SERVER_VERSION }) }],
|
|
457
510
|
};
|
|
@@ -478,6 +531,7 @@ registerTool({
|
|
|
478
531
|
},
|
|
479
532
|
required: ["center", "defined"],
|
|
480
533
|
},
|
|
534
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
481
535
|
annotations: { title: "Center Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
482
536
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
483
537
|
handler: async (args) => {
|
|
@@ -535,6 +589,7 @@ registerTool({
|
|
|
535
589
|
},
|
|
536
590
|
required: ["gate"],
|
|
537
591
|
},
|
|
592
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
538
593
|
annotations: { title: "Gate Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
539
594
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
540
595
|
handler: async (args) => {
|
|
@@ -589,6 +644,7 @@ registerTool({
|
|
|
589
644
|
},
|
|
590
645
|
required: ["channel"],
|
|
591
646
|
},
|
|
647
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
592
648
|
annotations: { title: "Channel Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
593
649
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
594
650
|
handler: async (args) => {
|
|
@@ -734,7 +790,7 @@ const HD_GATE_DATA = {
|
|
|
734
790
|
38: { name: "The Fighter", hexagram: "Hex 38 — Opposition", keynote: "Struggle in the search for purpose", gift: "Perseverance — fighting for what is meaningful", shadow: "Struggle — fighting against everything without purpose", circuit: "Individual (Knowing)" },
|
|
735
791
|
39: { name: "Provocation", hexagram: "Hex 39 — Obstruction", keynote: "Freeing the spirit through provocation", gift: "Dynamism — provocateur that liberates", shadow: "Provocation — stirring trouble unconsciously", circuit: "Individual (Knowing)" },
|
|
736
792
|
40: { name: "Aloneness", hexagram: "Hex 40 — Deliverance", keynote: "The demand for aloneness as restoration", gift: "Resolve — willpower renewed in solitude", shadow: "Exhaustion — depleted by giving without replenishment", circuit: "Tribal (Ego)" },
|
|
737
|
-
41: { name: "Contraction", hexagram: "Hex 41 — Decrease", keynote: "The beginning of all experience — fantasy and desire", gift: "Fantasy — imagination that seeds new experience", shadow: "
|
|
793
|
+
41: { name: "Contraction", hexagram: "Hex 41 — Decrease", keynote: "The beginning of all experience — fantasy and desire", gift: "Fantasy — imagination that seeds new experience", shadow: "Escapism — escapism", circuit: "Collective (Abstract)" },
|
|
738
794
|
42: { name: "Growth", hexagram: "Hex 42 — Increase", keynote: "The ability to perpetuate and complete growth cycles", gift: "Detachment — completing cycles with grace", shadow: "Incompletion — abandoning cycles before they end", circuit: "Collective (Abstract)" },
|
|
739
795
|
43: { name: "Insight", hexagram: "Hex 43 — Breakthrough", keynote: "Breakthrough from within", gift: "Insight — inner knowing that arrives as genius", shadow: "Deafness — genius unrecognised or unheard", circuit: "Individual (Knowing)" },
|
|
740
796
|
44: { name: "Alertness", hexagram: "Hex 44 — Coming to Meet", keynote: "Instinctive memory and pattern recognition", gift: "Teamwork — bringing the right people together", shadow: "Interference — driven by karmic patterns from the past", circuit: "Tribal (Defence)" },
|
|
@@ -848,8 +904,8 @@ const HD_PLANET_HD_THEMES = {
|
|
|
848
904
|
"to act before the mind has a chance to deliberate. This energy can surprise even yourself.",
|
|
849
905
|
},
|
|
850
906
|
Jupiter: {
|
|
851
|
-
conscious: "**Jupiter** represents the law of your Personality — the karmic gifts and the
|
|
852
|
-
"higher wisdom
|
|
907
|
+
conscious: "**Jupiter** represents the law of your Personality — the karmic gifts and the principled framework through which you expand beyond " +
|
|
908
|
+
"higher wisdom and justice flow. It shows where you move beyond the self-limiting " +
|
|
853
909
|
"patterns of your conditioning.",
|
|
854
910
|
unconscious: "**Design Jupiter** shows the unconscious grace embedded in your design — the karmic correction " +
|
|
855
911
|
"your body naturally applies when you are living true to your nature.",
|
|
@@ -932,6 +988,7 @@ registerTool({
|
|
|
932
988
|
},
|
|
933
989
|
required: ["planet", "column"],
|
|
934
990
|
},
|
|
991
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
935
992
|
annotations: { title: "Planet Sidebar Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
936
993
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
937
994
|
handler: async (args) => {
|
|
@@ -1000,6 +1057,7 @@ registerTool({
|
|
|
1000
1057
|
},
|
|
1001
1058
|
required: ["variable", "direction"],
|
|
1002
1059
|
},
|
|
1060
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
1003
1061
|
annotations: { title: "Variable Arrow Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
1004
1062
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
1005
1063
|
handler: async (args) => {
|