@openephemeris/mcp-server 3.10.4 → 3.12.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/README.md +3 -2
- package/config/dev-allowlist.json +83 -2
- package/dist/index.js +2 -2
- package/dist/server-sse.js +3 -3
- package/dist/tools/apps/chart-wheel-app.d.ts +10 -6
- package/dist/tools/apps/chart-wheel-app.js +202 -56
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.js +13 -0
- package/dist/ui/chart-wheel.html +68 -51
- package/glama.json +6 -0
- package/package.json +4 -2
- package/smithery.yaml +50 -0
package/README.md
CHANGED
|
@@ -353,8 +353,8 @@ When you update the MCP server logic (handlers, bug fixes, hardening), you shoul
|
|
|
353
353
|
|
|
354
354
|
Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live tool registry.
|
|
355
355
|
|
|
356
|
-
- Allowlisted operations: **
|
|
357
|
-
- Methods: `GET=4`, `POST=
|
|
356
|
+
- Allowlisted operations: **28**
|
|
357
|
+
- Methods: `GET=4`, `POST=24`, `PUT=0`, `PATCH=0`, `DELETE=0`
|
|
358
358
|
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **57**
|
|
359
359
|
- Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `chart_wheel_on_aspect_click`, `chart_wheel_on_house_click`, `chart_wheel_on_planet_click`, `chart_wheel_recalculate`, `chinese_bazi`, `dev_call`, `dev_list_allowed`, `electional_aspect_search`, `electional_moment_analysis`, `electional_station_tracker`, `ephemeris_angles_points`, `ephemeris_aspect_check`, `ephemeris_bi_wheel`, `ephemeris_chart_wheel`, `ephemeris_composite`, `ephemeris_composite_midpoint`, `ephemeris_dignities`, `ephemeris_electional`, `ephemeris_fixed_stars`, `ephemeris_hermetic_lots`, `ephemeris_house_cusps`, `ephemeris_lunar_return`, `ephemeris_midpoints`, `ephemeris_moon_phase`, `ephemeris_natal_batch`, `ephemeris_natal_chart`, `ephemeris_natal_transits`, `ephemeris_next_eclipse`, `ephemeris_next_lunar_phase`, `ephemeris_overlay`, `ephemeris_planet_position`, `ephemeris_planetary_return`, `ephemeris_progressed_chart`, `ephemeris_relocation`, `ephemeris_retrograde_status`, `ephemeris_solar_return`, `ephemeris_synastry`, `ephemeris_transits`, `explore_natal_chart`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `vedic_chart`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
|
|
360
360
|
- Generic tools:
|
|
@@ -364,6 +364,7 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
364
364
|
| Family | Operations | Example |
|
|
365
365
|
|---|---:|---|
|
|
366
366
|
| `acg` | 4 | `POST /acg/ccg`, `POST /acg/hits` |
|
|
367
|
+
| `chinese` | 5 | `POST /chinese/bazi/compatibility`, `POST /chinese/bazi/element-balance` |
|
|
367
368
|
| `comparative` | 5 | `POST /comparative/composite`, `POST /comparative/composite/midpoint` |
|
|
368
369
|
| `electional` | 4 | `GET /electional/aspect-search`, `GET /electional/find-window` |
|
|
369
370
|
| `ephemeris` | 1 | `POST /ephemeris/relocation` |
|
|
@@ -31,6 +31,26 @@
|
|
|
31
31
|
"method": "POST",
|
|
32
32
|
"path": "/acg/power-lines"
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
"method": "POST",
|
|
36
|
+
"path": "/chinese/bazi/compatibility"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"method": "POST",
|
|
40
|
+
"path": "/chinese/bazi/element-balance"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"method": "POST",
|
|
44
|
+
"path": "/chinese/bazi/luck-pillars"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"method": "POST",
|
|
48
|
+
"path": "/chinese/bazi/solar-terms"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"method": "POST",
|
|
52
|
+
"path": "/chinese/bazi/ten-gods"
|
|
53
|
+
},
|
|
34
54
|
{
|
|
35
55
|
"method": "POST",
|
|
36
56
|
"path": "/comparative/composite/midpoint"
|
|
@@ -108,8 +128,8 @@
|
|
|
108
128
|
"path": "/visualization/chart-wheel"
|
|
109
129
|
}
|
|
110
130
|
],
|
|
111
|
-
"last_generated_at": "2026-04-
|
|
112
|
-
"openapi_sha256": "
|
|
131
|
+
"last_generated_at": "2026-04-25T17:47:00.681Z",
|
|
132
|
+
"openapi_sha256": "9b2a79811fef658557233fc649b598178488985708b3ad10f252be598567080a",
|
|
113
133
|
"candidates_get": [
|
|
114
134
|
{
|
|
115
135
|
"method": "GET",
|
|
@@ -391,6 +411,14 @@
|
|
|
391
411
|
],
|
|
392
412
|
"operationId": "reverse_geocode_location_reverse_get"
|
|
393
413
|
},
|
|
414
|
+
{
|
|
415
|
+
"method": "GET",
|
|
416
|
+
"path": "/tidal/forcing/deep-time",
|
|
417
|
+
"tags": [
|
|
418
|
+
"tidal"
|
|
419
|
+
],
|
|
420
|
+
"operationId": "get_tidal_forcing_deep_time_endpoint_tidal_forcing_deep_time_get"
|
|
421
|
+
},
|
|
394
422
|
{
|
|
395
423
|
"method": "GET",
|
|
396
424
|
"path": "/tidal/forcing",
|
|
@@ -521,6 +549,54 @@
|
|
|
521
549
|
],
|
|
522
550
|
"operationId": "power_lines_acg_power_lines_post"
|
|
523
551
|
},
|
|
552
|
+
{
|
|
553
|
+
"method": "POST",
|
|
554
|
+
"path": "/chinese/bazi/annual-pillar",
|
|
555
|
+
"tags": [
|
|
556
|
+
"chinese"
|
|
557
|
+
],
|
|
558
|
+
"operationId": "bazi_annual_pillar_chinese_bazi_annual_pillar_post"
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
"method": "POST",
|
|
562
|
+
"path": "/chinese/bazi/compatibility",
|
|
563
|
+
"tags": [
|
|
564
|
+
"chinese"
|
|
565
|
+
],
|
|
566
|
+
"operationId": "bazi_compatibility_chinese_bazi_compatibility_post"
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
"method": "POST",
|
|
570
|
+
"path": "/chinese/bazi/element-balance",
|
|
571
|
+
"tags": [
|
|
572
|
+
"chinese"
|
|
573
|
+
],
|
|
574
|
+
"operationId": "bazi_element_balance_chinese_bazi_element_balance_post"
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
"method": "POST",
|
|
578
|
+
"path": "/chinese/bazi/luck-pillars",
|
|
579
|
+
"tags": [
|
|
580
|
+
"chinese"
|
|
581
|
+
],
|
|
582
|
+
"operationId": "bazi_luck_pillars_chinese_bazi_luck_pillars_post"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
"method": "POST",
|
|
586
|
+
"path": "/chinese/bazi/solar-terms",
|
|
587
|
+
"tags": [
|
|
588
|
+
"chinese"
|
|
589
|
+
],
|
|
590
|
+
"operationId": "bazi_solar_terms_chinese_bazi_solar_terms_post"
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
"method": "POST",
|
|
594
|
+
"path": "/chinese/bazi/ten-gods",
|
|
595
|
+
"tags": [
|
|
596
|
+
"chinese"
|
|
597
|
+
],
|
|
598
|
+
"operationId": "bazi_ten_gods_chinese_bazi_ten_gods_post"
|
|
599
|
+
},
|
|
524
600
|
{
|
|
525
601
|
"method": "POST",
|
|
526
602
|
"path": "/chinese/bazi",
|
|
@@ -994,6 +1070,11 @@
|
|
|
994
1070
|
"Non-GET allowlisted: POST /acg/hits",
|
|
995
1071
|
"Non-GET allowlisted: POST /acg/local-space",
|
|
996
1072
|
"Non-GET allowlisted: POST /acg/power-lines",
|
|
1073
|
+
"Non-GET allowlisted: POST /chinese/bazi/compatibility",
|
|
1074
|
+
"Non-GET allowlisted: POST /chinese/bazi/element-balance",
|
|
1075
|
+
"Non-GET allowlisted: POST /chinese/bazi/luck-pillars",
|
|
1076
|
+
"Non-GET allowlisted: POST /chinese/bazi/solar-terms",
|
|
1077
|
+
"Non-GET allowlisted: POST /chinese/bazi/ten-gods",
|
|
997
1078
|
"Non-GET allowlisted: POST /comparative/composite/midpoint",
|
|
998
1079
|
"Non-GET allowlisted: POST /comparative/composite",
|
|
999
1080
|
"Non-GET allowlisted: POST /comparative/natal-transits",
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
-
import { initTools, toolRegistry, formatToolResponse } from "./tools/index.js";
|
|
8
|
+
import { initTools, toolRegistry, formatToolResponse, modelVisibleTools } from "./tools/index.js";
|
|
9
9
|
import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
|
|
10
10
|
function resolveServerVersion() {
|
|
11
11
|
try {
|
|
@@ -41,7 +41,7 @@ const server = new Server({
|
|
|
41
41
|
// List available tools
|
|
42
42
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
43
43
|
return {
|
|
44
|
-
tools:
|
|
44
|
+
tools: modelVisibleTools().map((tool) => ({
|
|
45
45
|
name: tool.name,
|
|
46
46
|
description: tool.description,
|
|
47
47
|
inputSchema: tool.inputSchema,
|
package/dist/server-sse.js
CHANGED
|
@@ -22,7 +22,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
22
22
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
23
23
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
24
24
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, isInitializeRequest, } from "@modelcontextprotocol/sdk/types.js";
|
|
25
|
-
import { initTools, toolRegistry, formatToolResponse } from "./tools/index.js";
|
|
25
|
+
import { initTools, toolRegistry, formatToolResponse, modelVisibleTools } from "./tools/index.js";
|
|
26
26
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -193,7 +193,7 @@ function createMcpServer() {
|
|
|
193
193
|
});
|
|
194
194
|
// --- Tool handlers ---
|
|
195
195
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
196
|
-
tools:
|
|
196
|
+
tools: modelVisibleTools().map((tool) => ({
|
|
197
197
|
name: tool.name,
|
|
198
198
|
description: tool.description,
|
|
199
199
|
inputSchema: tool.inputSchema,
|
|
@@ -315,7 +315,7 @@ async function main() {
|
|
|
315
315
|
// Spec: https://smithery.ai/docs/build/publish#server-scanning
|
|
316
316
|
// ---------------------------------------------------------------------------
|
|
317
317
|
app.get("/.well-known/mcp/server-card.json", (_req, res) => {
|
|
318
|
-
const tools =
|
|
318
|
+
const tools = modelVisibleTools().map((tool) => ({
|
|
319
319
|
name: tool.name,
|
|
320
320
|
description: tool.description,
|
|
321
321
|
inputSchema: tool.inputSchema,
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* chart-wheel-app.ts — MCP App tool registration for the Chart Wheel Explorer.
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
5
|
-
* • explore_natal_chart
|
|
6
|
-
* •
|
|
7
|
-
* •
|
|
8
|
-
* •
|
|
9
|
-
* •
|
|
4
|
+
* Registers 7 tools:
|
|
5
|
+
* • explore_natal_chart — primary entry point, returns data + UI resource [model]
|
|
6
|
+
* • chart_wheel_fetch_svg — lazy SVG fetcher called by UI on first render [app-only]
|
|
7
|
+
* • chart_wheel_on_planet_click — planet click handler [app-only]
|
|
8
|
+
* • chart_wheel_on_house_click — house click handler [app-only]
|
|
9
|
+
* • chart_wheel_on_aspect_click — aspect click handler [app-only]
|
|
10
|
+
* • chart_wheel_recalculate — house-system switcher [app-only]
|
|
11
|
+
*
|
|
12
|
+
* SVG payloads are NEVER embedded in model-facing text blocks. The UI calls
|
|
13
|
+
* chart_wheel_fetch_svg lazily to keep Claude's context lean.
|
|
10
14
|
*
|
|
11
15
|
* Also exports resource helpers (getChartWheelBundle, etc.) for use in
|
|
12
16
|
* index.ts and server-sse.ts.
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* chart-wheel-app.ts — MCP App tool registration for the Chart Wheel Explorer.
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
5
|
-
* • explore_natal_chart
|
|
6
|
-
* •
|
|
7
|
-
* •
|
|
8
|
-
* •
|
|
9
|
-
* •
|
|
4
|
+
* Registers 7 tools:
|
|
5
|
+
* • explore_natal_chart — primary entry point, returns data + UI resource [model]
|
|
6
|
+
* • chart_wheel_fetch_svg — lazy SVG fetcher called by UI on first render [app-only]
|
|
7
|
+
* • chart_wheel_on_planet_click — planet click handler [app-only]
|
|
8
|
+
* • chart_wheel_on_house_click — house click handler [app-only]
|
|
9
|
+
* • chart_wheel_on_aspect_click — aspect click handler [app-only]
|
|
10
|
+
* • chart_wheel_recalculate — house-system switcher [app-only]
|
|
11
|
+
*
|
|
12
|
+
* SVG payloads are NEVER embedded in model-facing text blocks. The UI calls
|
|
13
|
+
* chart_wheel_fetch_svg lazily to keep Claude's context lean.
|
|
10
14
|
*
|
|
11
15
|
* Also exports resource helpers (getChartWheelBundle, etc.) for use in
|
|
12
16
|
* index.ts and server-sse.ts.
|
|
@@ -55,6 +59,22 @@ export function getChartWheelBundle() {
|
|
|
55
59
|
export function clearBundleCache() {
|
|
56
60
|
cachedBundle = null;
|
|
57
61
|
}
|
|
62
|
+
// ── Per-request SVG cache (serves chart_wheel_fetch_svg lazily) ────────────
|
|
63
|
+
// Keyed by "datetime|lat|lon|houseSystem". Bounded to last 50 renders.
|
|
64
|
+
const SVG_CACHE_MAX = 50;
|
|
65
|
+
const cachedSvgMap = new Map();
|
|
66
|
+
function buildSvgCacheKey(datetime, lat, lon, houseSystem) {
|
|
67
|
+
return `${datetime}|${lat ?? 0}|${lon ?? 0}|${houseSystem}`;
|
|
68
|
+
}
|
|
69
|
+
function cacheSetSvg(key, svg) {
|
|
70
|
+
if (cachedSvgMap.size >= SVG_CACHE_MAX) {
|
|
71
|
+
// Evict oldest entry
|
|
72
|
+
const first = cachedSvgMap.keys().next().value;
|
|
73
|
+
if (first !== undefined)
|
|
74
|
+
cachedSvgMap.delete(first);
|
|
75
|
+
}
|
|
76
|
+
cachedSvgMap.set(key, svg);
|
|
77
|
+
}
|
|
58
78
|
// ── House system helpers ───────────────────────────────────────────────────
|
|
59
79
|
const HOUSE_SYSTEM_MAP = {
|
|
60
80
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
@@ -136,39 +156,26 @@ registerTool({
|
|
|
136
156
|
const lat = args.latitude;
|
|
137
157
|
const lon = args.longitude;
|
|
138
158
|
const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// Decode SVG from binary response (base64 → UTF-8 string)
|
|
145
|
-
let svgString = null;
|
|
146
|
-
if (svgBinary && typeof svgBinary === "object" && svgBinary.encoding === "base64" && svgBinary.data_base64) {
|
|
147
|
-
svgString = Buffer.from(svgBinary.data_base64, "base64").toString("utf-8");
|
|
148
|
-
}
|
|
149
|
-
// Build the enriched chart payload for the UI
|
|
150
|
-
const uiPayload = {
|
|
151
|
-
...chartData,
|
|
152
|
-
_svg_base: svgString,
|
|
153
|
-
_birth_params: {
|
|
154
|
-
datetime,
|
|
155
|
-
timezone: args.timezone ?? null,
|
|
156
|
-
latitude: lat ?? null,
|
|
157
|
-
longitude: lon ?? null,
|
|
158
|
-
location: args.location ?? null,
|
|
159
|
-
},
|
|
160
|
-
house_system: houseSystem,
|
|
161
|
-
};
|
|
162
|
-
// Build human-readable summary for the LLM context / fallback text
|
|
159
|
+
// Fetch natal chart JSON. The chart is rendered client-side in the UI iframe,
|
|
160
|
+
// so we do NOT call the /visualization/chart-wheel endpoint — that was the
|
|
161
|
+
// source of the ~90 second blocking delay.
|
|
162
|
+
const chartData = await client.post("/ephemeris/natal-chart", natalBody);
|
|
163
|
+
// Build human-readable summary for the LLM context
|
|
163
164
|
const summary = buildChartSummary(chartData, String(args.location ?? `${lat}, ${lon}`));
|
|
164
|
-
//
|
|
165
|
+
// Build the UI payload (planets normalised to array, aspects computed)
|
|
166
|
+
const modelPayload = buildModelPayload(chartData, {
|
|
167
|
+
datetime,
|
|
168
|
+
timezone: args.timezone ?? null,
|
|
169
|
+
latitude: lat ?? null,
|
|
170
|
+
longitude: lon ?? null,
|
|
171
|
+
location: args.location ?? null,
|
|
172
|
+
}, houseSystem);
|
|
165
173
|
const bundleAvailable = Boolean(getChartWheelBundle());
|
|
166
174
|
if (bundleAvailable) {
|
|
167
|
-
// Return: text fallback + JSON payload + resource reference
|
|
168
175
|
return {
|
|
169
176
|
content: [
|
|
170
177
|
{ type: "text", text: summary },
|
|
171
|
-
{ type: "text", text: JSON.stringify(
|
|
178
|
+
{ type: "text", text: JSON.stringify(modelPayload) },
|
|
172
179
|
{
|
|
173
180
|
type: "resource",
|
|
174
181
|
resource: {
|
|
@@ -183,17 +190,14 @@ registerTool({
|
|
|
183
190
|
},
|
|
184
191
|
};
|
|
185
192
|
}
|
|
186
|
-
// Fallback: text summary
|
|
187
|
-
|
|
188
|
-
{ type: "text", text: summary },
|
|
189
|
-
];
|
|
190
|
-
if (svgString) {
|
|
191
|
-
const b64 = Buffer.from(svgString).toString("base64");
|
|
192
|
-
content.push({ type: "image", data: b64, mimeType: "image/svg+xml" });
|
|
193
|
-
}
|
|
194
|
-
return { content };
|
|
193
|
+
// Fallback: text summary only (non-App hosts; no SVG available here)
|
|
194
|
+
return { content: [{ type: "text", text: summary }] };
|
|
195
195
|
},
|
|
196
196
|
});
|
|
197
|
+
// ── chart_wheel_fetch_svg removed ────────────────────────────────────────────
|
|
198
|
+
// SVG is now embedded inline in the explore_natal_chart payload (_svg_base).
|
|
199
|
+
// The UI fast-path in main.ts checks _svg_base first and skips the tool call.
|
|
200
|
+
// The SVG cache is retained for chart_wheel_recalculate house system switches.
|
|
197
201
|
// ── Tool: chart_wheel_on_planet_click ──────────────────────────────────────
|
|
198
202
|
registerTool({
|
|
199
203
|
name: "chart_wheel_on_planet_click",
|
|
@@ -214,6 +218,7 @@ registerTool({
|
|
|
214
218
|
required: ["planet", "longitude"],
|
|
215
219
|
},
|
|
216
220
|
annotations: { title: "Planet Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
221
|
+
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
217
222
|
handler: async (args) => {
|
|
218
223
|
const planet = String(args.planet);
|
|
219
224
|
const sign = args.sign ? String(args.sign) : zodSignFromLon(Number(args.longitude));
|
|
@@ -247,6 +252,7 @@ registerTool({
|
|
|
247
252
|
required: ["house_number"],
|
|
248
253
|
},
|
|
249
254
|
annotations: { title: "House Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
255
|
+
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
250
256
|
handler: async (args) => {
|
|
251
257
|
const num = Number(args.house_number);
|
|
252
258
|
const sign = args.sign ? String(args.sign) : "";
|
|
@@ -281,6 +287,7 @@ registerTool({
|
|
|
281
287
|
required: ["planet1", "planet2"],
|
|
282
288
|
},
|
|
283
289
|
annotations: { title: "Aspect Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
290
|
+
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
284
291
|
handler: async (args) => {
|
|
285
292
|
const p1 = capitalize(String(args.planet1));
|
|
286
293
|
const p2 = capitalize(String(args.planet2));
|
|
@@ -319,6 +326,7 @@ registerTool({
|
|
|
319
326
|
required: ["datetime", "latitude", "longitude", "house_system"],
|
|
320
327
|
},
|
|
321
328
|
annotations: { title: "Recalculate Chart", readOnlyHint: true, openWorldHint: false },
|
|
329
|
+
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
322
330
|
handler: async (args) => {
|
|
323
331
|
const client = getActiveClient();
|
|
324
332
|
const houseSystem = String(args.house_system);
|
|
@@ -326,17 +334,11 @@ registerTool({
|
|
|
326
334
|
const lat = args.latitude;
|
|
327
335
|
const lon = args.longitude;
|
|
328
336
|
const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
let svgString = null;
|
|
334
|
-
if (svgBinary && typeof svgBinary === "object" && svgBinary.encoding === "base64" && svgBinary.data_base64) {
|
|
335
|
-
svgString = Buffer.from(svgBinary.data_base64, "base64").toString("utf-8");
|
|
336
|
-
}
|
|
337
|
-
const uiPayload = {
|
|
337
|
+
// Fetch updated chart JSON only — chart renders client-side in the iframe
|
|
338
|
+
const chartData = await client.post("/ephemeris/natal-chart", natalBody);
|
|
339
|
+
// Return only chart data (no SVG) — the UI calls chart_wheel_fetch_svg
|
|
340
|
+
const payload = {
|
|
338
341
|
...chartData,
|
|
339
|
-
_svg_base: svgString,
|
|
340
342
|
_birth_params: {
|
|
341
343
|
datetime,
|
|
342
344
|
timezone: args.timezone ?? null,
|
|
@@ -347,7 +349,7 @@ registerTool({
|
|
|
347
349
|
house_system: houseSystem,
|
|
348
350
|
};
|
|
349
351
|
return {
|
|
350
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
352
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
351
353
|
};
|
|
352
354
|
},
|
|
353
355
|
});
|
|
@@ -365,6 +367,97 @@ function zodSignFromLon(lon) {
|
|
|
365
367
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"];
|
|
366
368
|
return signs[Math.floor(((lon % 360) + 360) % 360 / 30)];
|
|
367
369
|
}
|
|
370
|
+
const CLASSICAL_PLANETS = new Set([
|
|
371
|
+
"sun", "moon", "mercury", "venus", "mars",
|
|
372
|
+
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
373
|
+
"chiron", "north_node", "south_node", "true_node", "asc", "mc",
|
|
374
|
+
]);
|
|
375
|
+
/**
|
|
376
|
+
* Build a lean, SVG-free payload for Claude's text context.
|
|
377
|
+
* Strips SVG, filters to classical planets only, and packages birth params.
|
|
378
|
+
* Tokens saved vs. full uiPayload: ~85–95% reduction (no 100KB SVG, no asteroid flood).
|
|
379
|
+
*/
|
|
380
|
+
/**
|
|
381
|
+
* Compute major aspects between a set of planets.
|
|
382
|
+
* Returns a structured AspectData[] array — the natal chart API does not
|
|
383
|
+
* include aspects in its JSON response; we derive them from longitude data.
|
|
384
|
+
*/
|
|
385
|
+
function computeAspects(planets) {
|
|
386
|
+
const ASPECTS = [
|
|
387
|
+
{ type: "conjunction", angle: 0, orb: 8 },
|
|
388
|
+
{ type: "opposition", angle: 180, orb: 8 },
|
|
389
|
+
{ type: "trine", angle: 120, orb: 8 },
|
|
390
|
+
{ type: "square", angle: 90, orb: 7 },
|
|
391
|
+
{ type: "sextile", angle: 60, orb: 6 },
|
|
392
|
+
{ type: "quincunx", angle: 150, orb: 3 },
|
|
393
|
+
{ type: "semisquare", angle: 45, orb: 2 },
|
|
394
|
+
];
|
|
395
|
+
const results = [];
|
|
396
|
+
for (let i = 0; i < planets.length; i++) {
|
|
397
|
+
for (let j = i + 1; j < planets.length; j++) {
|
|
398
|
+
const p1 = planets[i];
|
|
399
|
+
const p2 = planets[j];
|
|
400
|
+
// Angular separation: shortest arc (0–180)
|
|
401
|
+
let diff = Math.abs(p1.longitude - p2.longitude) % 360;
|
|
402
|
+
if (diff > 180)
|
|
403
|
+
diff = 360 - diff;
|
|
404
|
+
for (const asp of ASPECTS) {
|
|
405
|
+
const orb = Math.abs(diff - asp.angle);
|
|
406
|
+
if (orb <= asp.orb) {
|
|
407
|
+
// Applying: combined longitudinal speed closing the orb
|
|
408
|
+
const applying = ((p1.speed ?? 1) - (p2.speed ?? 0)) < 0 ? diff < asp.angle : diff > asp.angle;
|
|
409
|
+
results.push({
|
|
410
|
+
planet1: p1.name,
|
|
411
|
+
planet2: p2.name,
|
|
412
|
+
type: asp.type,
|
|
413
|
+
angle: asp.angle,
|
|
414
|
+
orb: Math.round(orb * 100) / 100,
|
|
415
|
+
applying,
|
|
416
|
+
});
|
|
417
|
+
break; // Each pair only matches one aspect type
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return results;
|
|
423
|
+
}
|
|
424
|
+
function buildModelPayload(chartData, birthParams, houseSystem, svgBase) {
|
|
425
|
+
// ── 1. Normalize planets: keyed object → typed PlanetData array ──────────
|
|
426
|
+
// The natal API returns { sun: { longitude, sign_name, is_retrograde, ... }, moon: {...} }.
|
|
427
|
+
// The chart wheel UI expects PlanetData[] with canonical field names.
|
|
428
|
+
const rawPlanets = chartData.planets;
|
|
429
|
+
let planetsArray = [];
|
|
430
|
+
if (rawPlanets && typeof rawPlanets === "object" && !Array.isArray(rawPlanets)) {
|
|
431
|
+
planetsArray = Object.entries(rawPlanets)
|
|
432
|
+
.filter(([k]) => CLASSICAL_PLANETS.has(k.toLowerCase()))
|
|
433
|
+
.map(([name, p]) => ({
|
|
434
|
+
name: name.toLowerCase(),
|
|
435
|
+
longitude: (p.longitude ?? p.lon ?? 0),
|
|
436
|
+
latitude: (p.latitude ?? p.lat),
|
|
437
|
+
speed: (p.longitude_speed ?? p.speed),
|
|
438
|
+
retrograde: Boolean(p.is_retrograde ?? p.retrograde),
|
|
439
|
+
sign: (p.sign_name ?? p.sign),
|
|
440
|
+
house: (p.house),
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
else if (Array.isArray(chartData.planets)) {
|
|
444
|
+
// Already normalised (e.g. house system recalculate path)
|
|
445
|
+
planetsArray = chartData.planets;
|
|
446
|
+
}
|
|
447
|
+
// ── 2. Compute aspects from longitude data (API doesn't return them) ──────
|
|
448
|
+
const aspects = computeAspects(planetsArray);
|
|
449
|
+
return {
|
|
450
|
+
...chartData,
|
|
451
|
+
// Typed array replaces the keyed object — the renderer can now iterate it
|
|
452
|
+
planets: planetsArray,
|
|
453
|
+
// Structured aspect array — powers highlighting and info panel
|
|
454
|
+
aspects,
|
|
455
|
+
// Embed SVG for fast first-render in the iframe.
|
|
456
|
+
_svg_base: svgBase ?? undefined,
|
|
457
|
+
_birth_params: birthParams,
|
|
458
|
+
house_system: houseSystem,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
368
461
|
function buildChartSummary(data, location) {
|
|
369
462
|
// The natal API returns planets as a keyed object { sun: {...}, moon: {...} }.
|
|
370
463
|
// Normalise to an array before processing.
|
|
@@ -373,12 +466,65 @@ function buildChartSummary(data, location) {
|
|
|
373
466
|
? raw
|
|
374
467
|
: Object.entries(raw).map(([name, p]) => ({
|
|
375
468
|
name,
|
|
376
|
-
sign: p.sign,
|
|
469
|
+
sign: (p.sign_name ?? p.sign),
|
|
377
470
|
house: p.house,
|
|
378
|
-
retrograde: Boolean(p.retrograde),
|
|
471
|
+
retrograde: Boolean(p.is_retrograde ?? p.retrograde),
|
|
472
|
+
longitude: p.longitude,
|
|
473
|
+
longitude_speed: p.longitude_speed,
|
|
379
474
|
}));
|
|
380
|
-
const
|
|
475
|
+
const CLASSICAL_ORDER = [
|
|
476
|
+
"sun", "moon", "mercury", "venus", "mars",
|
|
477
|
+
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
478
|
+
"chiron", "north_node", "south_node", "true_node", "asc", "mc",
|
|
479
|
+
];
|
|
480
|
+
planetsArray.sort((a, b) => {
|
|
481
|
+
const ai = CLASSICAL_ORDER.indexOf(a.name.toLowerCase());
|
|
482
|
+
const bi = CLASSICAL_ORDER.indexOf(b.name.toLowerCase());
|
|
483
|
+
if (ai !== -1 && bi !== -1)
|
|
484
|
+
return ai - bi;
|
|
485
|
+
if (ai !== -1)
|
|
486
|
+
return -1;
|
|
487
|
+
if (bi !== -1)
|
|
488
|
+
return 1;
|
|
489
|
+
return a.name.localeCompare(b.name);
|
|
490
|
+
});
|
|
491
|
+
const lines = planetsArray.slice(0, 12).map((p) => `• **${capitalize(p.name)}** in ${p.sign ?? "?"} (H${p.house ?? "?"})${p.retrograde ? " ℞" : ""}`);
|
|
492
|
+
// ── Enrichment: sect, element tally, strongest aspect ──────────────────
|
|
493
|
+
const sun = planetsArray.find((p) => p.name.toLowerCase() === "sun");
|
|
494
|
+
const ascLon = data.ascendant;
|
|
495
|
+
const sunLon = sun?.longitude;
|
|
496
|
+
let sectNote = "";
|
|
497
|
+
if (sunLon != null && ascLon != null) {
|
|
498
|
+
// Day chart: Sun above horizon (houses 7–12, i.e. longitude within 180° of ASC going upper half)
|
|
499
|
+
const sunRelative = ((sunLon - ascLon) + 360) % 360;
|
|
500
|
+
sectNote = sunRelative < 180 ? "Day chart ☀️" : "Night chart 🌙";
|
|
501
|
+
}
|
|
502
|
+
const ELEMENT_MAP = {
|
|
503
|
+
aries: "fire", leo: "fire", sagittarius: "fire",
|
|
504
|
+
taurus: "earth", virgo: "earth", capricorn: "earth",
|
|
505
|
+
gemini: "air", libra: "air", aquarius: "air",
|
|
506
|
+
cancer: "water", scorpio: "water", pisces: "water",
|
|
507
|
+
};
|
|
508
|
+
const elementCount = { fire: 0, earth: 0, air: 0, water: 0 };
|
|
509
|
+
for (const p of planetsArray.slice(0, 10)) {
|
|
510
|
+
const sign = (p.sign ?? "").toLowerCase();
|
|
511
|
+
const el = ELEMENT_MAP[sign];
|
|
512
|
+
if (el)
|
|
513
|
+
elementCount[el]++;
|
|
514
|
+
}
|
|
515
|
+
const domElement = Object.entries(elementCount).sort((a, b) => b[1] - a[1])[0];
|
|
516
|
+
// Strongest aspect by tightest orb (use computed aspects from planetsArray)
|
|
517
|
+
const aspects = computeAspects(planetsArray
|
|
518
|
+
.filter((p) => p.longitude != null)
|
|
519
|
+
.map((p) => ({ name: p.name, longitude: p.longitude, speed: p.longitude_speed })));
|
|
520
|
+
const strongest = aspects.sort((a, b) => a.orb - b.orb)[0];
|
|
521
|
+
const strongestNote = strongest
|
|
522
|
+
? `Closest aspect: ${capitalize(strongest.planet1)} ${strongest.type} ${capitalize(strongest.planet2)} (${strongest.orb.toFixed(1)}° orb)`
|
|
523
|
+
: "";
|
|
524
|
+
const enrichment = [sectNote, domElement ? `Dominant element: ${domElement[0]} (${domElement[1]})` : "", strongestNote]
|
|
525
|
+
.filter(Boolean).join(" · ");
|
|
381
526
|
return (`**Natal Chart — ${location}**\n\n` +
|
|
527
|
+
(enrichment ? `${enrichment}\n\n` : "") +
|
|
382
528
|
`Key Placements:\n${lines.join("\n")}\n\n` +
|
|
383
529
|
"Click any planet, house, or aspect line for interpretation.");
|
|
384
530
|
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -10,8 +10,25 @@ export interface ToolDefinition {
|
|
|
10
10
|
idempotentHint?: boolean;
|
|
11
11
|
openWorldHint?: boolean;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* UI metadata — mirrors the ext-apps SDK _meta.ui shape.
|
|
15
|
+
* When visibility includes "app" only (not "model"), the tool is hidden from
|
|
16
|
+
* Claude's tool list (ListTools) but remains callable by the UI via callServerTool().
|
|
17
|
+
*/
|
|
18
|
+
_meta?: {
|
|
19
|
+
ui?: {
|
|
20
|
+
resourceUri?: string;
|
|
21
|
+
/** SDK visibility array — use ["app"] to hide from model, ["model"] for model-only. */
|
|
22
|
+
visibility?: Array<"app" | "model">;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
13
25
|
handler: (args: any) => Promise<any>;
|
|
14
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns tools that should be exposed to Claude in the ListTools response.
|
|
29
|
+
* Filters out tools that have visibility restricted to the UI (app-only).
|
|
30
|
+
*/
|
|
31
|
+
export declare function modelVisibleTools(): ToolDefinition[];
|
|
15
32
|
export declare const toolRegistry: Record<string, ToolDefinition>;
|
|
16
33
|
export declare function registerTool(tool: ToolDefinition): void;
|
|
17
34
|
export type ToolProfile = "dev" | "legacy";
|
package/dist/tools/index.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns tools that should be exposed to Claude in the ListTools response.
|
|
3
|
+
* Filters out tools that have visibility restricted to the UI (app-only).
|
|
4
|
+
*/
|
|
5
|
+
export function modelVisibleTools() {
|
|
6
|
+
return Object.values(toolRegistry).filter((t) => {
|
|
7
|
+
const vis = t._meta?.ui?.visibility;
|
|
8
|
+
// If visibility is explicitly ["app"] (no "model"), hide from model
|
|
9
|
+
if (vis && !vis.includes("model") && vis.includes("app"))
|
|
10
|
+
return false;
|
|
11
|
+
return true;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
1
14
|
export const toolRegistry = {};
|
|
2
15
|
export function registerTool(tool) {
|
|
3
16
|
toolRegistry[tool.name] = tool;
|