@openephemeris/mcp-server 3.14.0 → 3.16.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 +25 -0
- package/README.md +8 -6
- package/dist/prompts.js +16 -16
- package/dist/server-sse.js +65 -14
- package/dist/tools/apps/bi-wheel-app.js +12 -3
- package/dist/tools/apps/bodygraph-app.d.ts +9 -5
- package/dist/tools/apps/bodygraph-app.js +207 -9
- package/dist/tools/apps/chart-wheel-app.js +29 -5
- package/dist/tools/apps/location-tools.js +38 -5
- package/dist/tools/apps/moon-phase-app.js +7 -1
- package/dist/tools/dev.js +102 -68
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +15 -1
- package/dist/tools/specialized/bazi.js +6 -6
- package/dist/tools/specialized/bi_wheel.js +3 -2
- package/dist/tools/specialized/chart_wheel.js +3 -2
- package/dist/tools/specialized/comparative.js +2 -2
- package/dist/tools/specialized/hd_bodygraph.js +4 -3
- package/dist/ui/bazi.html +21 -21
- package/dist/ui/bi-wheel.html +335 -293
- package/dist/ui/bodygraph.html +1645 -1615
- package/dist/ui/chart-wheel.html +1966 -1912
- package/dist/ui/moon-phase.html +72 -49
- package/dist/ui/transit-timeline.html +1 -1
- package/dist/ui/vedic-chart.html +21 -21
- package/package.json +5 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* bodygraph-app.ts — MCP App tool registration for the Human Design Bodygraph Explorer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* • explore_human_design
|
|
6
|
-
* •
|
|
7
|
-
* •
|
|
8
|
-
*
|
|
4
|
+
* Entry tools [model + app]:
|
|
5
|
+
* • explore_human_design — natal bodygraph, data + UI resource
|
|
6
|
+
* • explore_human_design_transit — natal + transit overlay (premium)
|
|
7
|
+
* • explore_human_design_connection — two-person connection/synastry overlay (premium)
|
|
8
|
+
* Click handlers [app-only]: hd_on_center_click, hd_on_gate_click,
|
|
9
|
+
* hd_on_channel_click, hd_on_planet_click, hd_on_variable_click.
|
|
10
|
+
* The transit + connection tools reuse this bodygraph iframe resource; their
|
|
11
|
+
* overlay SVG is returned inline by /human-design/transit-chart and
|
|
12
|
+
* /human-design/composite (include_visual) rather than a separate render call.
|
|
9
13
|
*
|
|
10
14
|
* The bodygraph is rendered client-side from structured JSON — no SVG endpoint
|
|
11
15
|
* is called, keeping latency at zero and the architecture consistent with
|
|
@@ -78,13 +82,18 @@ function ensureTimezone(dt) {
|
|
|
78
82
|
* renderer in bodygraph-renderer.ts. Non-fatal: a missing SVG should never
|
|
79
83
|
* block the tool result.
|
|
80
84
|
*/
|
|
81
|
-
async function fetchGoBodygraphSVG(body) {
|
|
85
|
+
async function fetchGoBodygraphSVG(body, theme = "dark") {
|
|
82
86
|
try {
|
|
83
87
|
const client = getActiveClient();
|
|
84
88
|
// /visualization/bodygraph is registered as binary in BINARY_ENDPOINT_PREFIXES.
|
|
85
89
|
// The client wraps it as { content_type, encoding: "base64", data_base64 } —
|
|
86
90
|
// we decode back to UTF-8 SVG text for inlining.
|
|
87
|
-
|
|
91
|
+
//
|
|
92
|
+
// style=light|dark drives the Go renderer's palette. The endpoint defaults
|
|
93
|
+
// to "light" when style is omitted, which mismatches dark MCP hosts — so we
|
|
94
|
+
// always pass an explicit theme (default "dark" to match the app shell; the
|
|
95
|
+
// iframe re-fetches with the host-detected theme on load / theme change).
|
|
96
|
+
const resp = await client.post(`/visualization/bodygraph?format=svg&size=800&style=${theme}`, body);
|
|
88
97
|
if (typeof resp === "string")
|
|
89
98
|
return resp;
|
|
90
99
|
if (resp && typeof resp === "object") {
|
|
@@ -438,11 +447,16 @@ registerTool({
|
|
|
438
447
|
modelPayload._svg = svg;
|
|
439
448
|
const summary = buildHdSummary(modelPayload, location);
|
|
440
449
|
if (bundleAvailable) {
|
|
450
|
+
// MCP Apps wire format: the UI is declared via `_meta.ui.resourceUri` and
|
|
451
|
+
// delivered through resources/read — NOT as a content block. A URI-only
|
|
452
|
+
// `type: "resource"` block lacks the inline text/blob the base MCP schema
|
|
453
|
+
// requires and fails strict client validation. structuredContent carries
|
|
454
|
+
// the data for non-rendering hosts.
|
|
441
455
|
return {
|
|
442
456
|
content: [
|
|
443
457
|
{ type: "text", text: summary },
|
|
444
|
-
{ type: "text", text: JSON.stringify({ ...modelPayload, server_version: SERVER_VERSION }) },
|
|
445
458
|
],
|
|
459
|
+
structuredContent: { ...modelPayload, server_version: SERVER_VERSION },
|
|
446
460
|
_meta: {
|
|
447
461
|
"ui/resourceUri": BODYGRAPH_RESOURCE_URI,
|
|
448
462
|
ui: { resourceUri: BODYGRAPH_RESOURCE_URI },
|
|
@@ -468,6 +482,11 @@ registerTool({
|
|
|
468
482
|
type: "string",
|
|
469
483
|
description: "IANA timezone name (e.g. 'America/New_York') for accurate UTC conversion.",
|
|
470
484
|
},
|
|
485
|
+
theme: {
|
|
486
|
+
type: "string",
|
|
487
|
+
enum: ["light", "dark"],
|
|
488
|
+
description: "Render palette for the bodygraph SVG. Mirrors the MCP host's light/dark color scheme.",
|
|
489
|
+
},
|
|
471
490
|
},
|
|
472
491
|
required: ["datetime"],
|
|
473
492
|
},
|
|
@@ -481,6 +500,7 @@ registerTool({
|
|
|
481
500
|
const datetime = localToUtcIso(String(args.datetime), timezone);
|
|
482
501
|
const lat = args.latitude;
|
|
483
502
|
const lon = args.longitude;
|
|
503
|
+
const theme = args.theme === "light" ? "light" : "dark";
|
|
484
504
|
const body = {
|
|
485
505
|
birth_datetime_utc: datetime,
|
|
486
506
|
};
|
|
@@ -493,7 +513,7 @@ registerTool({
|
|
|
493
513
|
client.request("POST", "/human-design/chart", {
|
|
494
514
|
data: body,
|
|
495
515
|
}),
|
|
496
|
-
fetchGoBodygraphSVG(body),
|
|
516
|
+
fetchGoBodygraphSVG(body, theme),
|
|
497
517
|
]);
|
|
498
518
|
const chart = chartData.chart ?? chartData;
|
|
499
519
|
const modelPayload = buildHdModelPayload(chart, {
|
|
@@ -1157,3 +1177,181 @@ registerTool({
|
|
|
1157
1177
|
};
|
|
1158
1178
|
},
|
|
1159
1179
|
});
|
|
1180
|
+
// ── Tool: explore_human_design_transit ───────────────────────────────────────
|
|
1181
|
+
// Personalized Human Design transit overlay (natal chart + a transiting moment).
|
|
1182
|
+
// Premium (Developer tier). Calls POST /human-design/transit-chart.
|
|
1183
|
+
registerTool({
|
|
1184
|
+
name: "explore_human_design_transit",
|
|
1185
|
+
description: "Overlay the current (or a chosen) planetary transit on a person's natal Human Design bodygraph. " +
|
|
1186
|
+
"Highlights the channels a transit temporarily COMPLETES with the natal chart and any centers it " +
|
|
1187
|
+
"newly defines — the core of a Human Design transit reading. " +
|
|
1188
|
+
"Returns an interactive overlay bodygraph in MCP Apps-capable hosts (Claude Desktop), with a text " +
|
|
1189
|
+
"summary fallback elsewhere. Premium (Developer tier). Calculated with NASA JPL DE440 ephemerides.",
|
|
1190
|
+
inputSchema: {
|
|
1191
|
+
type: "object",
|
|
1192
|
+
properties: {
|
|
1193
|
+
datetime: {
|
|
1194
|
+
type: "string",
|
|
1195
|
+
description: "Natal birth datetime, ISO 8601 (e.g. '1990-04-15T19:30:00Z'). Include 'Z' or an offset, " +
|
|
1196
|
+
"or supply timezone for local time. HD is time-sensitive to the minute.",
|
|
1197
|
+
},
|
|
1198
|
+
latitude: { type: "number", description: "Natal birth latitude (decimal degrees, +N). Optional." },
|
|
1199
|
+
longitude: { type: "number", description: "Natal birth longitude (decimal degrees, +E). Optional." },
|
|
1200
|
+
location: { type: "string", description: "Natal location name, display only." },
|
|
1201
|
+
timezone: { type: "string", description: "IANA timezone for the birth location (e.g. 'America/New_York')." },
|
|
1202
|
+
transit_datetime: {
|
|
1203
|
+
type: "string",
|
|
1204
|
+
description: "Transit moment, ISO 8601. Defaults to now (UTC) when omitted.",
|
|
1205
|
+
},
|
|
1206
|
+
},
|
|
1207
|
+
required: ["datetime"],
|
|
1208
|
+
},
|
|
1209
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
1210
|
+
annotations: {
|
|
1211
|
+
title: "Human Design Transit Overlay",
|
|
1212
|
+
readOnlyHint: true,
|
|
1213
|
+
destructiveHint: false,
|
|
1214
|
+
idempotentHint: false,
|
|
1215
|
+
openWorldHint: false,
|
|
1216
|
+
},
|
|
1217
|
+
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["model", "app"] } },
|
|
1218
|
+
handler: async (args) => {
|
|
1219
|
+
const client = getActiveClient();
|
|
1220
|
+
const timezone = args.timezone;
|
|
1221
|
+
const natalIso = localToUtcIso(String(args.datetime), timezone);
|
|
1222
|
+
const lat = args.latitude ?? 0;
|
|
1223
|
+
const lon = args.longitude ?? 0;
|
|
1224
|
+
// SubjectRequest wire shape: name (required), birth_datetime.iso, and
|
|
1225
|
+
// birth_location with struct-typed coordinate/timezone inputs (matches the
|
|
1226
|
+
// live bi-wheel/transit-timeline tools — bare scalars are rejected).
|
|
1227
|
+
const body = {
|
|
1228
|
+
subject: {
|
|
1229
|
+
name: args.location ?? "Subject",
|
|
1230
|
+
birth_datetime: { iso: natalIso },
|
|
1231
|
+
birth_location: {
|
|
1232
|
+
latitude: { decimal: lat },
|
|
1233
|
+
longitude: { decimal: lon },
|
|
1234
|
+
...(timezone ? { timezone: { iana_name: timezone } } : {}),
|
|
1235
|
+
},
|
|
1236
|
+
},
|
|
1237
|
+
};
|
|
1238
|
+
if (args.transit_datetime) {
|
|
1239
|
+
body.transit_datetime = { iso: ensureTimezone(String(args.transit_datetime)) };
|
|
1240
|
+
}
|
|
1241
|
+
const bundleAvailable = Boolean(getBodygraphBundle());
|
|
1242
|
+
if (bundleAvailable)
|
|
1243
|
+
body.include_visual = true;
|
|
1244
|
+
const resp = await client.request("POST", "/human-design/transit-chart", { data: body });
|
|
1245
|
+
const transit = (resp?.transit ?? {});
|
|
1246
|
+
const completed = Array.isArray(transit.completed_channels) ? transit.completed_channels : [];
|
|
1247
|
+
const newCenters = Array.isArray(transit.newly_defined_centers) ? transit.newly_defined_centers : [];
|
|
1248
|
+
const natal = (resp?.natal ?? {});
|
|
1249
|
+
const svg = resp?.visual?.data;
|
|
1250
|
+
const summary = `**Human Design Transit Overlay**\n\n` +
|
|
1251
|
+
`Natal: ${natal.type ?? "—"} ${natal.profile ?? ""} · ${natal.authority ?? ""} authority.\n\n` +
|
|
1252
|
+
(completed.length
|
|
1253
|
+
? `The transit **completes ${completed.length} channel${completed.length === 1 ? "" : "s"}**: ${completed.join(", ")}.`
|
|
1254
|
+
: `The transit completes no channels right now.`) +
|
|
1255
|
+
(newCenters.length
|
|
1256
|
+
? `\n\nNewly-defined center${newCenters.length === 1 ? "" : "s"}: **${newCenters.join(", ")}** — a temporary shift while the transit holds.`
|
|
1257
|
+
: "");
|
|
1258
|
+
const payload = { ...resp, _svg: svg, server_version: SERVER_VERSION };
|
|
1259
|
+
if (bundleAvailable && svg) {
|
|
1260
|
+
return {
|
|
1261
|
+
content: [{ type: "text", text: summary }],
|
|
1262
|
+
structuredContent: payload,
|
|
1263
|
+
_meta: { "ui/resourceUri": BODYGRAPH_RESOURCE_URI, ui: { resourceUri: BODYGRAPH_RESOURCE_URI } },
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
return { content: [{ type: "text", text: summary }], structuredContent: payload };
|
|
1267
|
+
},
|
|
1268
|
+
});
|
|
1269
|
+
// ── Tool: explore_human_design_connection ────────────────────────────────────
|
|
1270
|
+
// Two-person Human Design connection (synastry) overlay. Premium (Developer tier).
|
|
1271
|
+
// Calls POST /human-design/composite.
|
|
1272
|
+
registerTool({
|
|
1273
|
+
name: "explore_human_design_connection",
|
|
1274
|
+
description: "Compare two people's Human Design charts and classify every connected channel by HD connection " +
|
|
1275
|
+
"theory — electromagnetic (attraction), companionship (sameness), dominance (one defines), and " +
|
|
1276
|
+
"compromise (friction). Returns an interactive two-person overlay bodygraph in MCP Apps-capable " +
|
|
1277
|
+
"hosts, with a text summary fallback elsewhere. Premium (Developer tier). NASA JPL DE440 ephemerides.",
|
|
1278
|
+
inputSchema: {
|
|
1279
|
+
type: "object",
|
|
1280
|
+
properties: {
|
|
1281
|
+
person_a: {
|
|
1282
|
+
type: "object",
|
|
1283
|
+
description: "First person's birth data.",
|
|
1284
|
+
properties: {
|
|
1285
|
+
datetime: { type: "string", description: "Birth datetime, ISO 8601 (Z/offset, or local with timezone)." },
|
|
1286
|
+
latitude: { type: "number", description: "Birth latitude (decimal degrees, +N). Optional." },
|
|
1287
|
+
longitude: { type: "number", description: "Birth longitude (decimal degrees, +E). Optional." },
|
|
1288
|
+
timezone: { type: "string", description: "IANA timezone (e.g. 'America/New_York')." },
|
|
1289
|
+
},
|
|
1290
|
+
required: ["datetime"],
|
|
1291
|
+
},
|
|
1292
|
+
person_b: {
|
|
1293
|
+
type: "object",
|
|
1294
|
+
description: "Second person's birth data.",
|
|
1295
|
+
properties: {
|
|
1296
|
+
datetime: { type: "string", description: "Birth datetime, ISO 8601 (Z/offset, or local with timezone)." },
|
|
1297
|
+
latitude: { type: "number", description: "Birth latitude (decimal degrees, +N). Optional." },
|
|
1298
|
+
longitude: { type: "number", description: "Birth longitude (decimal degrees, +E). Optional." },
|
|
1299
|
+
timezone: { type: "string", description: "IANA timezone (e.g. 'America/New_York')." },
|
|
1300
|
+
},
|
|
1301
|
+
required: ["datetime"],
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
required: ["person_a", "person_b"],
|
|
1305
|
+
},
|
|
1306
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
1307
|
+
annotations: {
|
|
1308
|
+
title: "Human Design Connection (Synastry)",
|
|
1309
|
+
readOnlyHint: true,
|
|
1310
|
+
destructiveHint: false,
|
|
1311
|
+
idempotentHint: true,
|
|
1312
|
+
openWorldHint: false,
|
|
1313
|
+
},
|
|
1314
|
+
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["model", "app"] } },
|
|
1315
|
+
handler: async (args) => {
|
|
1316
|
+
const client = getActiveClient();
|
|
1317
|
+
const toSubject = (p) => {
|
|
1318
|
+
const tz = p?.timezone;
|
|
1319
|
+
return {
|
|
1320
|
+
birth_datetime_utc: localToUtcIso(String(p?.datetime), tz),
|
|
1321
|
+
latitude: p?.latitude ?? 0,
|
|
1322
|
+
longitude: p?.longitude ?? 0,
|
|
1323
|
+
};
|
|
1324
|
+
};
|
|
1325
|
+
const body = {
|
|
1326
|
+
subject_1: toSubject(args.person_a),
|
|
1327
|
+
subject_2: toSubject(args.person_b),
|
|
1328
|
+
};
|
|
1329
|
+
const bundleAvailable = Boolean(getBodygraphBundle());
|
|
1330
|
+
if (bundleAvailable)
|
|
1331
|
+
body.include_visual = true;
|
|
1332
|
+
const resp = await client.request("POST", "/human-design/composite", { data: body });
|
|
1333
|
+
const connections = Array.isArray(resp?.connections) ? resp.connections : [];
|
|
1334
|
+
const svg = resp?.visual?.data;
|
|
1335
|
+
const byType = (t) => connections.filter((c) => c?.type === t).map((c) => c.id);
|
|
1336
|
+
const line = (label, t) => {
|
|
1337
|
+
const ids = byType(t);
|
|
1338
|
+
return ids.length ? `\n- **${label}** (${ids.length}): ${ids.join(", ")}` : "";
|
|
1339
|
+
};
|
|
1340
|
+
const summary = `**Human Design Connection**\n\n` +
|
|
1341
|
+
`${connections.length} connected channel${connections.length === 1 ? "" : "s"}:` +
|
|
1342
|
+
line("Electromagnetic — attraction", "electromagnetic") +
|
|
1343
|
+
line("Companionship — sameness", "companionship") +
|
|
1344
|
+
line("Dominance — one defines", "dominance") +
|
|
1345
|
+
line("Compromise — friction", "compromise") +
|
|
1346
|
+
(connections.length ? "" : " none.");
|
|
1347
|
+
const payload = { ...resp, _svg: svg, server_version: SERVER_VERSION };
|
|
1348
|
+
if (bundleAvailable && svg) {
|
|
1349
|
+
return {
|
|
1350
|
+
content: [{ type: "text", text: summary }],
|
|
1351
|
+
structuredContent: payload,
|
|
1352
|
+
_meta: { "ui/resourceUri": BODYGRAPH_RESOURCE_URI, ui: { resourceUri: BODYGRAPH_RESOURCE_URI } },
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
return { content: [{ type: "text", text: summary }], structuredContent: payload };
|
|
1356
|
+
},
|
|
1357
|
+
});
|
|
@@ -199,11 +199,17 @@ registerTool({
|
|
|
199
199
|
}, houseSystem, undefined, requestedBodies);
|
|
200
200
|
const bundleAvailable = Boolean(getChartWheelBundle());
|
|
201
201
|
if (bundleAvailable) {
|
|
202
|
+
// MCP Apps wire format: the UI is declared via `_meta.ui.resourceUri` and
|
|
203
|
+
// delivered through resources/read — NOT as a content block. A URI-only
|
|
204
|
+
// `type: "resource"` block lacks the inline text/blob the base MCP schema
|
|
205
|
+
// requires and fails strict client validation. Keep `structuredContent`
|
|
206
|
+
// so non-rendering hosts still get the data, and a text summary so chat
|
|
207
|
+
// transcripts read coherently.
|
|
202
208
|
return {
|
|
203
209
|
content: [
|
|
204
210
|
{ type: "text", text: summary },
|
|
205
|
-
{ type: "text", text: JSON.stringify({ ...modelPayload, server_version: SERVER_VERSION }) },
|
|
206
211
|
],
|
|
212
|
+
structuredContent: { ...modelPayload, server_version: SERVER_VERSION },
|
|
207
213
|
_meta: {
|
|
208
214
|
"ui/resourceUri": CHART_WHEEL_RESOURCE_URI,
|
|
209
215
|
ui: { resourceUri: CHART_WHEEL_RESOURCE_URI },
|
|
@@ -414,8 +420,26 @@ function zodSignFromLon(lon) {
|
|
|
414
420
|
const CLASSICAL_PLANETS = new Set([
|
|
415
421
|
"sun", "moon", "mercury", "venus", "mars",
|
|
416
422
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
417
|
-
|
|
423
|
+
// Nodes use space form to match the renderer's glyph table (chart-renderer.ts).
|
|
424
|
+
"chiron", "north node", "south node", "true node", "asc", "mc",
|
|
418
425
|
]);
|
|
426
|
+
/**
|
|
427
|
+
* Canonicalise the Go backend's verbose node keys ("North Node (Mean)" /
|
|
428
|
+
* "North Node (True)") to the space-form names the renderer expects, keeping
|
|
429
|
+
* mean vs true DISTINCT. Without this the natal wheel dropped the nodes entirely
|
|
430
|
+
* (the raw key never matched the classical-planet set). All other body names are
|
|
431
|
+
* returned lowercased and otherwise unchanged.
|
|
432
|
+
*/
|
|
433
|
+
function canonicalizeBodyName(raw) {
|
|
434
|
+
const lower = raw.toLowerCase().trim();
|
|
435
|
+
if (lower === "north node (true)" || lower === "true node")
|
|
436
|
+
return "true node";
|
|
437
|
+
if (lower === "north node (mean)" || lower === "mean node" || lower === "north node")
|
|
438
|
+
return "north node";
|
|
439
|
+
if (lower === "south node (mean)" || lower === "south node (true)" || lower === "south node")
|
|
440
|
+
return "south node";
|
|
441
|
+
return lower;
|
|
442
|
+
}
|
|
419
443
|
const EXTENDED_BODIES = new Set([
|
|
420
444
|
"mean_lilith", "lilith", "true_lilith",
|
|
421
445
|
"ceres", "juno", "vesta", "pallas", "pholus",
|
|
@@ -487,19 +511,19 @@ function buildModelPayload(chartData, birthParams, houseSystem, svgBase, bodyFil
|
|
|
487
511
|
if (rawPlanets && typeof rawPlanets === "object" && !Array.isArray(rawPlanets)) {
|
|
488
512
|
planetsArray = Object.entries(rawPlanets)
|
|
489
513
|
.filter(([k]) => {
|
|
490
|
-
const key = k
|
|
514
|
+
const key = canonicalizeBodyName(k);
|
|
491
515
|
// If specific bodies were requested, use that filter
|
|
492
516
|
if (bodyFilter) {
|
|
493
517
|
const wantsAll = bodyFilter.some(b => b.toLowerCase() === "all");
|
|
494
518
|
if (wantsAll)
|
|
495
519
|
return ALL_KNOWN_BODIES.has(key);
|
|
496
|
-
return bodyFilter.some(b => b
|
|
520
|
+
return bodyFilter.some(b => canonicalizeBodyName(b) === key);
|
|
497
521
|
}
|
|
498
522
|
// Default: classical planets only
|
|
499
523
|
return CLASSICAL_PLANETS.has(key);
|
|
500
524
|
})
|
|
501
525
|
.map(([name, p]) => ({
|
|
502
|
-
name: name
|
|
526
|
+
name: canonicalizeBodyName(name),
|
|
503
527
|
longitude: (p.longitude ?? p.lon ?? 0),
|
|
504
528
|
latitude: (p.latitude ?? p.lat),
|
|
505
529
|
speed: (p.longitude_speed ?? p.speed),
|
|
@@ -3,13 +3,29 @@ import { getActiveClient } from "../../backend/client.js";
|
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
4
|
registerTool({
|
|
5
5
|
name: "location_search",
|
|
6
|
-
description: "App-only tool for getting location autocomplete suggestions. Returns latitude, longitude, and IANA timezone.",
|
|
6
|
+
description: "App-only tool for getting location autocomplete suggestions. Returns display name, region (state/province), latitude, longitude, and IANA timezone. Optional bias params (country/region/near) improve ranking; a trailing \"City, ST\" qualifier in the query is also honored.",
|
|
7
7
|
inputSchema: {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
10
10
|
query: {
|
|
11
11
|
type: "string",
|
|
12
|
-
description: "Search query for location, e.g. 'Los Angeles'",
|
|
12
|
+
description: "Search query for location, e.g. 'Los Angeles' or 'Wakefield, MI'",
|
|
13
|
+
},
|
|
14
|
+
country: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Optional ISO 3166-1 alpha-2 country code (e.g. 'US') to bias ranking. Boost, not filter.",
|
|
17
|
+
},
|
|
18
|
+
region: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Optional admin1 (state/province) qualifier to bias ranking — full name ('Michigan'), ASCII name, or code ('MI'). Boost, not filter.",
|
|
21
|
+
},
|
|
22
|
+
admin1: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Alias for region (region wins if both supplied).",
|
|
25
|
+
},
|
|
26
|
+
near: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Optional 'lat,lon' proximity hint (e.g. '37.77,-122.42') to bias ranking toward nearby places.",
|
|
13
29
|
},
|
|
14
30
|
},
|
|
15
31
|
required: ["query"],
|
|
@@ -23,9 +39,26 @@ registerTool({
|
|
|
23
39
|
},
|
|
24
40
|
handler: async (args) => {
|
|
25
41
|
validateRequired(args, ["query"]);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
// NB: the endpoint param is `query` (only /geocode/search reads `q`).
|
|
43
|
+
const params = { query: args.query };
|
|
44
|
+
for (const k of ["country", "region", "admin1", "near"]) {
|
|
45
|
+
if (args[k] != null && String(args[k]).trim() !== "")
|
|
46
|
+
params[k] = String(args[k]);
|
|
47
|
+
}
|
|
48
|
+
const raw = (await getActiveClient().request("GET", "/location/autocomplete", { params }));
|
|
49
|
+
// Map the API's snake_case response to the camelCase shape the app UI consumes.
|
|
50
|
+
const list = Array.isArray(raw?.suggestions) ? raw.suggestions : [];
|
|
51
|
+
const suggestions = list.map((s) => ({
|
|
52
|
+
displayName: s.display_name,
|
|
53
|
+
shortName: s.short_name,
|
|
54
|
+
region: s.region,
|
|
55
|
+
countryCode: s.country_code,
|
|
56
|
+
placeId: s.place_id,
|
|
57
|
+
latitude: s.latitude,
|
|
58
|
+
longitude: s.longitude,
|
|
59
|
+
timezone: s.timezone,
|
|
60
|
+
}));
|
|
61
|
+
return { suggestions };
|
|
29
62
|
},
|
|
30
63
|
});
|
|
31
64
|
registerTool({
|
|
@@ -227,11 +227,17 @@ registerTool({
|
|
|
227
227
|
const summary = buildMoonSummary(mergedData);
|
|
228
228
|
const bundleAvailable = Boolean(getMoonPhaseBundle());
|
|
229
229
|
if (bundleAvailable) {
|
|
230
|
+
// MCP Apps wire format: the UI is declared via `_meta.ui.resourceUri` and
|
|
231
|
+
// delivered through resources/read — NOT as a content block. An embedded
|
|
232
|
+
// `type: "resource"` block must carry inline text/blob per the base MCP
|
|
233
|
+
// schema, so a URI-only reference fails strict client validation; the
|
|
234
|
+
// _meta keys below are what the host uses to render the iframe.
|
|
235
|
+
// structuredContent carries the data payload for non-rendering hosts.
|
|
230
236
|
return {
|
|
231
237
|
content: [
|
|
232
238
|
{ type: "text", text: summary },
|
|
233
|
-
{ type: "text", text: JSON.stringify(uiPayload) },
|
|
234
239
|
],
|
|
240
|
+
structuredContent: uiPayload,
|
|
235
241
|
_meta: {
|
|
236
242
|
"ui/resourceUri": MOON_PHASE_RESOURCE_URI,
|
|
237
243
|
ui: { resourceUri: MOON_PHASE_RESOURCE_URI },
|
package/dist/tools/dev.js
CHANGED
|
@@ -32,66 +32,72 @@ function isDeniedByPrefix(pathname, prefixes) {
|
|
|
32
32
|
function isAllowedOperation(method, pathname, allow) {
|
|
33
33
|
return allow.some((e) => e.method === method && e.path === pathname);
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
35
|
+
// Shared reference block appended to both read/write proxy tools. Names the
|
|
36
|
+
// target API (Open Ephemeris) explicitly — required by the directory for
|
|
37
|
+
// freeform-path tools — plus credit costs and common calls.
|
|
38
|
+
const DEV_API_REFERENCE = "Target API: Open Ephemeris REST API (https://api.openephemeris.com). " +
|
|
39
|
+
"Call dev_list_allowed to see all currently available endpoint paths.\n\n" +
|
|
40
|
+
"AUTH: Set OPENEPHEMERIS_API_KEY in your environment. See openephemeris.com/dashboard for active plan limits.\n\n" +
|
|
41
|
+
"CREDIT COSTS:\n" +
|
|
42
|
+
" • Standard chart math (natal, progressed, bazi, vedic, iching): 1 credit\n" +
|
|
43
|
+
" • Human Design: 2 credits\n" +
|
|
44
|
+
" • Visualization rendering (chart-wheel, bi-wheel, charts/*): 2 credits\n" +
|
|
45
|
+
" • Comparative math (synastry, composite, overlay): 3 credits\n" +
|
|
46
|
+
" • Predictive ops (transits/search, returns): 5 credits\n" +
|
|
47
|
+
" • Predictive transit-chart: 1 credit\n" +
|
|
48
|
+
" • ACG / astrocartography: 10 credits (acg/hits: 15 credits)\n" +
|
|
49
|
+
" • Calendar endpoints: 10 credits\n" +
|
|
50
|
+
" • Catalog / metadata / health endpoints: 0 credits\n" +
|
|
51
|
+
" • Compute surcharge: requests > 30s add 1 credit per 30s (predictive, acg, calendar, electional)\n" +
|
|
52
|
+
" • format=llm (token-optimized output): available on all tiers\n\n" +
|
|
53
|
+
"COMMON CALLS:\n" +
|
|
54
|
+
" POST /ephemeris/natal-chart — Full natal chart (body: {subject: {name: 'Name', birth_datetime: {iso: '1990-04-15T14:30:00-05:00'}, birth_location: {latitude: {decimal: 40.0}, longitude: {decimal: -70.0}, timezone: {}}}})\n" +
|
|
55
|
+
" POST /ephemeris/natal/batch — Up to 50 natal charts in one request\n" +
|
|
56
|
+
" POST /ephemeris/relocation — Relocated chart (same natal, new location)\n" +
|
|
57
|
+
" POST /predictive/transits/search — Transit event search over a date range\n" +
|
|
58
|
+
" POST /predictive/returns/solar — Solar return chart\n" +
|
|
59
|
+
" POST /predictive/returns/lunar — Lunar return chart\n" +
|
|
60
|
+
" POST /comparative/synastry — Two-person synastry chart\n" +
|
|
61
|
+
" POST /comparative/composite — Composite (midpoint) chart\n" +
|
|
62
|
+
" POST /human-design/chart — Full HD chart (body: {birth_datetime_utc: '1990-04-15T19:30:00Z'}) — lat/lon optional\n" +
|
|
63
|
+
" POST /time/julian-day — Convert date to JD (body: {year: 1987, month: 7, day: 15, hour: 14, minute: 1})\n" +
|
|
64
|
+
" GET /ephemeris/moon/phase — Current/queried moon phase\n" +
|
|
65
|
+
" GET /ephemeris/moon/void-of-course — Next void-of-course period\n" +
|
|
66
|
+
" GET /ephemeris/agro/daily — Biodynamic farming day quality\n" +
|
|
67
|
+
" GET /ephemeris/agro/calendar — Multi-day biodynamic calendar\n" +
|
|
68
|
+
" GET /ephemeris/agro/void-of-course — Biodynamic VoC periods\n" +
|
|
69
|
+
" GET /eclipse/next-visible — Next eclipse visible from a location (query: lat, lon, type=solar|lunar)\n" +
|
|
70
|
+
" GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
|
|
71
|
+
" GET /eclipse/solar/local — Local solar eclipse (query: lat, lon)\n" +
|
|
72
|
+
" GET /tidal/forcing — Gravitational tidal forcing index\n" +
|
|
73
|
+
" POST /acg/power-lines — Astrocartography power lines (lat/lon GeoJSON)\n" +
|
|
74
|
+
" POST /acg/hits — ACG power at a specific location\n" +
|
|
75
|
+
" GET /calendar/astrology/moon-phases — Moon phase calendar for a date range\n" +
|
|
76
|
+
" GET /location/autocomplete — Geocode a place name (query: q=City Name)\n" +
|
|
77
|
+
" POST /timezone/lookup — Resolve timezone + UTC offset for a location\n" +
|
|
78
|
+
" POST /chinese/bazi — Chinese Ba Zi (Four Pillars) chart (body: {year, month, day, hour})\n" +
|
|
79
|
+
" GET /chinese/zodiac — Chinese zodiac year element/animal\n" +
|
|
80
|
+
" POST /vedic/chart — Vedic (Jyotish) natal chart (body: {datetime_utc, latitude, longitude})\n" +
|
|
81
|
+
" GET /catalogs/bodies — List all supported celestial bodies\n\n" +
|
|
82
|
+
"BINARY RESPONSES:\n" +
|
|
83
|
+
" • Binary/image endpoints return {content_type, content_length, encoding, data_base64}\n" +
|
|
84
|
+
" so callers can decode bytes deterministically.\n\n" +
|
|
85
|
+
"ECLIPSE NOTE: Eclipse endpoints accept format=llm via the query param like other endpoints.\n\n" +
|
|
86
|
+
"format=llm NOTE: Add query: {format: 'llm'} to natal/synastry/composite/HD endpoints for " +
|
|
87
|
+
"compact columnar output optimized for LLM token budgets (availability depends on your current plan).";
|
|
88
|
+
// The read and write proxies are kept as separate tools (not one method-switching
|
|
89
|
+
// tool) so that safe GET reads never share a surface with state-changing writes —
|
|
90
|
+
// a hard requirement of the Anthropic connector directory.
|
|
91
|
+
const READ_METHODS = ["GET"];
|
|
92
|
+
const WRITE_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
|
|
93
|
+
function makeProxyInputSchema(methods) {
|
|
94
|
+
return {
|
|
89
95
|
type: "object",
|
|
90
96
|
properties: {
|
|
91
97
|
method: {
|
|
92
98
|
type: "string",
|
|
93
|
-
enum:
|
|
94
|
-
description: "HTTP method",
|
|
99
|
+
enum: methods,
|
|
100
|
+
description: methods.length === 1 ? `HTTP method (always ${methods[0]})` : "HTTP method",
|
|
95
101
|
},
|
|
96
102
|
path: {
|
|
97
103
|
type: "string",
|
|
@@ -100,10 +106,10 @@ registerTool({
|
|
|
100
106
|
query: {
|
|
101
107
|
type: "object",
|
|
102
108
|
additionalProperties: true,
|
|
103
|
-
description: "Query params
|
|
109
|
+
description: "Query params (optional)",
|
|
104
110
|
},
|
|
105
111
|
body: {
|
|
106
|
-
description: "JSON body
|
|
112
|
+
description: "JSON request body (optional)",
|
|
107
113
|
},
|
|
108
114
|
preset: {
|
|
109
115
|
type: "string",
|
|
@@ -121,15 +127,16 @@ registerTool({
|
|
|
121
127
|
description: "Legacy convenience (deprecated): if provided, set query.output_mode and also map to query.preset/query.format when possible.",
|
|
122
128
|
},
|
|
123
129
|
},
|
|
124
|
-
required: ["
|
|
130
|
+
required: ["path"],
|
|
125
131
|
additionalProperties: false,
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function makeProxyHandler(allowedMethods, defaultMethod) {
|
|
135
|
+
return async (args) => {
|
|
129
136
|
validateRequired(args, ["path"]);
|
|
130
|
-
const methodStr = String(args.method ||
|
|
131
|
-
if (!
|
|
132
|
-
throw new Error(`
|
|
137
|
+
const methodStr = String(args.method || defaultMethod).toUpperCase();
|
|
138
|
+
if (!allowedMethods.includes(methodStr)) {
|
|
139
|
+
throw new Error(`Method ${methodStr} not permitted by this tool. Allowed: ${allowedMethods.join(", ")}.`);
|
|
133
140
|
}
|
|
134
141
|
const method = methodStr;
|
|
135
142
|
const pathname = String(args.path);
|
|
@@ -177,21 +184,48 @@ registerTool({
|
|
|
177
184
|
params: query,
|
|
178
185
|
data: body,
|
|
179
186
|
});
|
|
180
|
-
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
registerTool({
|
|
190
|
+
name: "dev_read_api",
|
|
191
|
+
description: "Read from any allowlisted Open Ephemeris API endpoint via HTTP GET. This is the read-only " +
|
|
192
|
+
"power-user escape hatch — use the typed tools (ephemeris_natal_chart, ephemeris_transits, etc.) " +
|
|
193
|
+
"first for common operations. For endpoints that compute via POST (natal-chart, synastry, etc.), " +
|
|
194
|
+
"use dev_write_api.\n\n" +
|
|
195
|
+
DEV_API_REFERENCE,
|
|
196
|
+
inputSchema: makeProxyInputSchema(READ_METHODS),
|
|
197
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
198
|
+
// GET-only: never mutates server state.
|
|
199
|
+
annotations: { title: "API Read", readOnlyHint: true, openWorldHint: true },
|
|
200
|
+
handler: makeProxyHandler(READ_METHODS, "GET"),
|
|
201
|
+
});
|
|
202
|
+
registerTool({
|
|
203
|
+
name: "dev_write_api",
|
|
204
|
+
description: "Send a request to any allowlisted Open Ephemeris API endpoint via POST/PUT/PATCH/DELETE. " +
|
|
205
|
+
"Most chart computations (natal-chart, synastry, composite, transits/search, returns, ACG) are " +
|
|
206
|
+
"POST endpoints and use this tool. Use the typed tools first for common operations; use dev_read_api " +
|
|
207
|
+
"for GET endpoints.\n\n" +
|
|
208
|
+
DEV_API_REFERENCE,
|
|
209
|
+
inputSchema: makeProxyInputSchema(WRITE_METHODS),
|
|
210
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
211
|
+
// Issues POST/PUT/PATCH/DELETE — must not be advertised as read-only.
|
|
212
|
+
annotations: { title: "API Write", readOnlyHint: false, openWorldHint: true },
|
|
213
|
+
handler: makeProxyHandler(WRITE_METHODS, "POST"),
|
|
181
214
|
});
|
|
182
215
|
registerTool({
|
|
183
216
|
name: "dev_list_allowed",
|
|
184
217
|
description: "List all API operations (method + path) that this MCP instance is authorized to call. " +
|
|
185
218
|
"Returns endpoint entries grouped by method, plus the active deny rules. " +
|
|
186
|
-
"Use this to discover what's available before calling
|
|
219
|
+
"Use this to discover what's available before calling dev_read_api / dev_write_api, or to verify an endpoint path. " +
|
|
187
220
|
"Typed shortcut tools (ephemeris_natal_chart, ephemeris_transits, etc.) cover the most common operations — " +
|
|
188
|
-
"check those first before reaching for
|
|
221
|
+
"check those first before reaching for the generic proxies.",
|
|
189
222
|
inputSchema: {
|
|
190
223
|
type: "object",
|
|
191
224
|
properties: {},
|
|
192
225
|
additionalProperties: false,
|
|
193
226
|
},
|
|
194
227
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
228
|
+
annotations: { title: "List Allowed API Operations", readOnlyHint: true, openWorldHint: false },
|
|
195
229
|
handler: async () => {
|
|
196
230
|
const allowlist = loadAllowlist();
|
|
197
231
|
return {
|