@openephemeris/mcp-server 4.7.0 → 4.8.1

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +35 -3
  3. package/dist/index.js +5 -2
  4. package/dist/oauth/token.js +30 -3
  5. package/dist/server-sse.js +7 -2
  6. package/dist/tools/apps/bazi-app.js +4 -4
  7. package/dist/tools/apps/bi-wheel-app.js +57 -23
  8. package/dist/tools/apps/bodygraph-app.js +12 -9
  9. package/dist/tools/apps/chart-wheel-app.js +4 -4
  10. package/dist/tools/apps/location-tools.js +2 -2
  11. package/dist/tools/apps/moon-phase-app.js +1 -1
  12. package/dist/tools/apps/vedic-chart-app.js +1 -1
  13. package/dist/tools/auth.js +8 -0
  14. package/dist/tools/dev.js +15 -5
  15. package/dist/tools/index.d.ts +41 -2
  16. package/dist/tools/index.js +201 -4
  17. package/dist/tools/specialized/account.js +5 -1
  18. package/dist/tools/specialized/acg.js +2 -2
  19. package/dist/tools/specialized/bazi.js +7 -7
  20. package/dist/tools/specialized/bi_wheel.js +1 -1
  21. package/dist/tools/specialized/chart_wheel.js +1 -1
  22. package/dist/tools/specialized/comparative.js +4 -4
  23. package/dist/tools/specialized/eclipse.js +1 -1
  24. package/dist/tools/specialized/electional.js +5 -5
  25. package/dist/tools/specialized/ephemeris_core.js +2 -2
  26. package/dist/tools/specialized/ephemeris_extended.js +8 -8
  27. package/dist/tools/specialized/hd_bodygraph.js +1 -1
  28. package/dist/tools/specialized/hd_cycles.js +2 -2
  29. package/dist/tools/specialized/hd_group.js +2 -2
  30. package/dist/tools/specialized/human_design.js +1 -1
  31. package/dist/tools/specialized/moon.js +10 -13
  32. package/dist/tools/specialized/natal.js +1 -1
  33. package/dist/tools/specialized/progressed.js +1 -1
  34. package/dist/tools/specialized/relocation.js +1 -1
  35. package/dist/tools/specialized/returns.js +3 -3
  36. package/dist/tools/specialized/synastry.js +1 -1
  37. package/dist/tools/specialized/transits.js +1 -1
  38. package/dist/tools/specialized/vedic.js +1 -1
  39. package/dist/tools/specialized/venus_star_points.js +6 -6
  40. package/dist/ui/chart-wheel.html +56 -53
  41. package/package.json +4 -2
  42. package/smithery.yaml +14 -3
@@ -57,7 +57,11 @@ export interface ToolDefinition {
57
57
  * multi-tenant HTTP server cannot vary it per session, and an unregistered
58
58
  * tool becomes uncallable rather than merely hidden.
59
59
  */
60
- export type ToolSurface = "core" | "full";
60
+ export type ToolSurface = "core" | "full" | ToolsetSurface;
61
+ /** A caller-chosen set of traditions, e.g. `?profile=hd,bazi`. */
62
+ export interface ToolsetSurface {
63
+ readonly toolsets: readonly string[];
64
+ }
61
65
  /**
62
66
  * The default advertised surface: everyday astrology work, one tool per job.
63
67
  *
@@ -80,6 +84,28 @@ export type ToolSurface = "core" | "full";
80
84
  * explore_natal_chart are present. Do not remove those four.
81
85
  */
82
86
  export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
87
+ /**
88
+ * Tool surfaces grouped by tradition, so a caller pays context only for the
89
+ * work they actually do: `?profile=hd,bazi` on HTTP, `OPENEPHEMERIS_TOOLS=hd`
90
+ * on stdio. BASE_TOOLSET is always included.
91
+ *
92
+ * This is the same *view* filter as `core`/`full` — nothing is unregistered,
93
+ * every tool stays callable by name — so a toolset is a context-budget choice,
94
+ * never a capability one.
95
+ *
96
+ * Why this exists: the core surface is re-sent on every model pass and sat at
97
+ * its ceiling with ~0 headroom, and the only levers left were deleting the
98
+ * disambiguation prose that makes tool selection work. Grouping by tradition
99
+ * is the structural fix — a Human Design user should not pay for Venus Star
100
+ * Points on every message.
101
+ *
102
+ * Names here are asserted against the registry by a test; a typo fails the
103
+ * build rather than silently narrowing someone's surface.
104
+ */
105
+ export declare const TOOLSETS: Readonly<Record<string, readonly string[]>>;
106
+ export declare const TOOLSET_NAMES: readonly string[];
107
+ /** Resolve a toolset selection to the tool names it advertises. */
108
+ export declare function toolsetSelectionNames(names: readonly string[]): Set<string>;
83
109
  /**
84
110
  * Returns tools that should be exposed to Claude in the ListTools response.
85
111
  * Filters out tools that have visibility restricted to the UI (app-only),
@@ -87,8 +113,21 @@ export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
87
113
  * "core" — everything outside CORE_TOOL_NAMES.
88
114
  */
89
115
  export declare function modelVisibleTools(transport?: "stdio" | "http", surface?: ToolSurface): ToolDefinition[];
90
- /** Parses a caller-supplied surface hint. Anything unrecognised means "core". */
116
+ /**
117
+ * Parses a caller-supplied surface hint.
118
+ *
119
+ * "full" -> every model-visible tool
120
+ * "hd" | "hd,bazi" -> those traditions plus BASE_TOOLSET
121
+ * anything else -> "core" (the curated default)
122
+ *
123
+ * Unknown names in a comma list are ignored rather than rejected: a typo
124
+ * degrades to a smaller surface, never to an error at `initialize` that would
125
+ * leave the connector dead. If NO name in the list is recognised, fall back to
126
+ * "core" so a mistyped profile can't strand a caller on BASE alone.
127
+ */
91
128
  export declare function parseToolSurface(raw: unknown): ToolSurface;
129
+ /** Stable label for logs/analytics — an object surface has no useful toString. */
130
+ export declare function describeSurface(surface: ToolSurface): string;
92
131
  export declare const toolRegistry: Record<string, ToolDefinition>;
93
132
  export declare function registerTool(tool: ToolDefinition): void;
94
133
  export type ToolProfile = "dev" | "legacy";
@@ -12,6 +12,9 @@ export const SERVER_VERSION = (() => {
12
12
  return "0.0.0-unknown";
13
13
  }
14
14
  })();
15
+ function isToolsetSurface(s) {
16
+ return typeof s === "object" && Array.isArray(s.toolsets);
17
+ }
15
18
  /**
16
19
  * The default advertised surface: everyday astrology work, one tool per job.
17
20
  *
@@ -53,6 +56,13 @@ export const CORE_TOOL_NAMES = new Set([
53
56
  "ephemeris_transits",
54
57
  "ephemeris_synastry",
55
58
  "ephemeris_retrograde_status",
59
+ // Promoted 2026-08-02 on usage: both were reachable only via profile=full,
60
+ // yet ephemeris_aspect_check drew 19 calls from 3 users over 90 days (more
61
+ // than 20 of the tools already in this set) and ephemeris_angles_points drew
62
+ // 9. People found them while they were hidden; advertising them is cheap
63
+ // (202 and 227 tokens) and earns its place.
64
+ "ephemeris_aspect_check",
65
+ "ephemeris_angles_points",
56
66
  "ephemeris_next_eclipse",
57
67
  "ephemeris_relocation",
58
68
  "ephemeris_progressed_chart",
@@ -60,13 +70,31 @@ export const CORE_TOOL_NAMES = new Set([
60
70
  // Traditions
61
71
  "human_design_chart",
62
72
  "vedic_chart",
63
- "chinese_bazi",
64
73
  "bazi_annual_pillar",
74
+ // chinese_bazi demoted 2026-08-02: explore_bazi_chart POSTs the same
75
+ // /chinese/bazi at the same 1-credit base and keeps every substantive field
76
+ // (year/month/day/hour pillars + day_master), dropping only the `metadata`
77
+ // and `success` envelope keys — verified against a live response, not
78
+ // inferred from the names. It also degrades to a text summary on hosts that
79
+ // cannot render the iframe, so nothing is lost without an MCP Apps host.
80
+ // chinese_bazi drew 0 calls in 90 days while advertised; explore_bazi_chart
81
+ // drew 3. Still registered and callable by name, and still in profile=full.
82
+ //
83
+ // NOTE: this reasoning does NOT extend to the sibling pairs. vedic_chart (1
84
+ // credit) vs explore_vedic_chart (3), and human_design_chart (2 credits, full
85
+ // response) vs explore_human_design (4 credits, and buildHdModelPayload drops
86
+ // activations/design/personality/strategy/variables) are not folds — they
87
+ // would cost users money and data. Verify in the handler before adding to
88
+ // this list.
65
89
  // Timing
66
90
  "ephemeris_electional",
67
91
  "electional_moment_analysis",
68
92
  "electional_station_tracker",
69
- "electional_angle_crossings",
93
+ // electional_angle_crossings demoted 2026-08-02: 715 tokens (the fattest
94
+ // non-proxy tool in this set) for 0 calls in 90 days, and at 5 credits it is
95
+ // advanced-practitioner work rather than an everyday ask. Three electional
96
+ // entry points remain advertised; this one is a profile=full / skill-named
97
+ // tool now.
70
98
  // Astrocartography
71
99
  "acg_power_lines",
72
100
  "acg_hits",
@@ -83,6 +111,141 @@ export const CORE_TOOL_NAMES = new Set([
83
111
  "auth_status",
84
112
  "auth_logout",
85
113
  ]);
114
+ /**
115
+ * Tools every session needs regardless of tradition: turning a place name into
116
+ * coordinates, checking credits, and the allowlist-gated escape hatch. Always
117
+ * unioned into a toolset selection — a caller who asks for `hd` still has to be
118
+ * able to resolve a birthplace, or they are back to recalling coordinates from
119
+ * memory, which is the bug BASE exists to prevent.
120
+ */
121
+ const BASE_TOOLSET = [
122
+ "location_search",
123
+ "timezone_resolve",
124
+ "account_usage",
125
+ "dev_read_api",
126
+ // The write half of the escape hatch. 26 of the 31 allowlisted operations
127
+ // are POST — BASE without dev_write_api would strand a toolset caller with
128
+ // only the 5 GET proxies. (Deliberately NOT advertised in `core`, where the
129
+ // curated typed tools cover the write surface; a toolset selection has no
130
+ // such coverage, so it needs the proxy whole.)
131
+ "dev_write_api",
132
+ "dev_list_allowed",
133
+ "auth_login",
134
+ "auth_status",
135
+ "auth_logout",
136
+ ];
137
+ /**
138
+ * Tool surfaces grouped by tradition, so a caller pays context only for the
139
+ * work they actually do: `?profile=hd,bazi` on HTTP, `OPENEPHEMERIS_TOOLS=hd`
140
+ * on stdio. BASE_TOOLSET is always included.
141
+ *
142
+ * This is the same *view* filter as `core`/`full` — nothing is unregistered,
143
+ * every tool stays callable by name — so a toolset is a context-budget choice,
144
+ * never a capability one.
145
+ *
146
+ * Why this exists: the core surface is re-sent on every model pass and sat at
147
+ * its ceiling with ~0 headroom, and the only levers left were deleting the
148
+ * disambiguation prose that makes tool selection work. Grouping by tradition
149
+ * is the structural fix — a Human Design user should not pay for Venus Star
150
+ * Points on every message.
151
+ *
152
+ * Names here are asserted against the registry by a test; a typo fails the
153
+ * build rather than silently narrowing someone's surface.
154
+ */
155
+ export const TOOLSETS = {
156
+ /** Western natal/predictive work — the default tradition. */
157
+ astrology: [
158
+ "ephemeris_natal_chart",
159
+ "explore_natal_chart",
160
+ "ephemeris_planet_position",
161
+ "ephemeris_house_cusps",
162
+ "ephemeris_angles_points",
163
+ "ephemeris_aspect_check",
164
+ "ephemeris_transits",
165
+ "ephemeris_natal_transits",
166
+ "explore_transit_timeline",
167
+ "ephemeris_synastry",
168
+ "explore_bi_wheel",
169
+ "bi_wheel_synopsis",
170
+ "ephemeris_retrograde_status",
171
+ "ephemeris_progressed_chart",
172
+ "ephemeris_solar_return",
173
+ "ephemeris_lunar_return",
174
+ "ephemeris_planetary_return",
175
+ "ephemeris_relocation",
176
+ "ephemeris_dignities",
177
+ "ephemeris_midpoints",
178
+ "ephemeris_hermetic_lots",
179
+ "ephemeris_fixed_stars",
180
+ "ephemeris_composite",
181
+ "ephemeris_composite_midpoint",
182
+ "ephemeris_overlay",
183
+ "ephemeris_chart_wheel",
184
+ "ephemeris_bi_wheel",
185
+ "ephemeris_natal_batch",
186
+ ],
187
+ /** Lunar phase, void-of-course and eclipse work. */
188
+ moon: [
189
+ "ephemeris_moon_phase",
190
+ "explore_moon_phase",
191
+ "ephemeris_next_lunar_phase",
192
+ "ephemeris_next_eclipse",
193
+ ],
194
+ /** Human Design. */
195
+ hd: [
196
+ "human_design_chart",
197
+ "explore_human_design",
198
+ "explore_human_design_transit",
199
+ "explore_human_design_connection",
200
+ "human_design_bodygraph",
201
+ "human_design_composite",
202
+ "human_design_penta",
203
+ "hd_opposition",
204
+ "hd_planetary_return",
205
+ ],
206
+ /** Chinese BaZi / Four Pillars. */
207
+ bazi: [
208
+ "explore_bazi_chart",
209
+ "chinese_bazi",
210
+ "bazi_chart",
211
+ "bazi_ten_gods",
212
+ "bazi_element_balance",
213
+ "bazi_luck_pillars",
214
+ "bazi_annual_pillar",
215
+ "bazi_compatibility",
216
+ ],
217
+ /** Vedic / Jyotish. */
218
+ vedic: ["vedic_chart", "explore_vedic_chart"],
219
+ /** Astrocartography. */
220
+ acg: ["acg_hits", "acg_power_lines"],
221
+ /** Electional timing. */
222
+ electional: [
223
+ "ephemeris_electional",
224
+ "electional_moment_analysis",
225
+ "electional_station_tracker",
226
+ "electional_angle_crossings",
227
+ "electional_aspect_search",
228
+ ],
229
+ /** Venus Star Point / synodic-cycle work. */
230
+ venus: [
231
+ "venus_star_points",
232
+ "venus_star_points_conjunctions",
233
+ "venus_phase",
234
+ "venus_elongations",
235
+ "venus_stations",
236
+ "venus_eight_year_star",
237
+ ],
238
+ };
239
+ export const TOOLSET_NAMES = Object.keys(TOOLSETS);
240
+ /** Resolve a toolset selection to the tool names it advertises. */
241
+ export function toolsetSelectionNames(names) {
242
+ const out = new Set(BASE_TOOLSET);
243
+ for (const n of names) {
244
+ for (const t of TOOLSETS[n] ?? [])
245
+ out.add(t);
246
+ }
247
+ return out;
248
+ }
86
249
  /**
87
250
  * Returns tools that should be exposed to Claude in the ListTools response.
88
251
  * Filters out tools that have visibility restricted to the UI (app-only),
@@ -90,6 +253,9 @@ export const CORE_TOOL_NAMES = new Set([
90
253
  * "core" — everything outside CORE_TOOL_NAMES.
91
254
  */
92
255
  export function modelVisibleTools(transport = "stdio", surface = "full") {
256
+ const allowed = isToolsetSurface(surface)
257
+ ? toolsetSelectionNames(surface.toolsets)
258
+ : null;
93
259
  return Object.values(toolRegistry).filter((t) => {
94
260
  if (transport === "http" && t.stdioOnly)
95
261
  return false;
@@ -97,14 +263,45 @@ export function modelVisibleTools(transport = "stdio", surface = "full") {
97
263
  // If visibility is explicitly ["app"] (no "model"), hide from model
98
264
  if (vis && !vis.includes("model") && vis.includes("app"))
99
265
  return false;
266
+ if (allowed)
267
+ return allowed.has(t.name);
100
268
  if (surface === "core" && !CORE_TOOL_NAMES.has(t.name))
101
269
  return false;
102
270
  return true;
103
271
  });
104
272
  }
105
- /** Parses a caller-supplied surface hint. Anything unrecognised means "core". */
273
+ /**
274
+ * Parses a caller-supplied surface hint.
275
+ *
276
+ * "full" -> every model-visible tool
277
+ * "hd" | "hd,bazi" -> those traditions plus BASE_TOOLSET
278
+ * anything else -> "core" (the curated default)
279
+ *
280
+ * Unknown names in a comma list are ignored rather than rejected: a typo
281
+ * degrades to a smaller surface, never to an error at `initialize` that would
282
+ * leave the connector dead. If NO name in the list is recognised, fall back to
283
+ * "core" so a mistyped profile can't strand a caller on BASE alone.
284
+ */
106
285
  export function parseToolSurface(raw) {
107
- return typeof raw === "string" && raw.toLowerCase() === "full" ? "full" : "core";
286
+ if (typeof raw !== "string")
287
+ return "core";
288
+ const value = raw.trim().toLowerCase();
289
+ if (value === "full")
290
+ return "full";
291
+ if (value === "" || value === "core")
292
+ return "core";
293
+ const requested = value
294
+ .split(",")
295
+ .map((s) => s.trim())
296
+ .filter(Boolean);
297
+ const known = requested.filter((n) => Object.hasOwn(TOOLSETS, n));
298
+ if (known.length === 0)
299
+ return "core";
300
+ return { toolsets: known };
301
+ }
302
+ /** Stable label for logs/analytics — an object surface has no useful toString. */
303
+ export function describeSurface(surface) {
304
+ return isToolsetSurface(surface) ? surface.toolsets.join("+") : surface;
108
305
  }
109
306
  export const toolRegistry = {};
110
307
  export function registerTool(tool) {
@@ -27,7 +27,7 @@ registerTool({
27
27
  additionalProperties: false,
28
28
  },
29
29
  outputSchema: OUTPUT_SCHEMA_JSON,
30
- annotations: { title: "Account Usage", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
30
+ annotations: { title: "Account Usage", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
31
31
  handler: async (args) => {
32
32
  const params = {};
33
33
  if (args?.month)
@@ -39,7 +39,11 @@ registerTool({
39
39
  }
40
40
  catch (error) {
41
41
  if (error instanceof BackendError && error.status === 401) {
42
+ // A real auth failure must carry isError — a success-shaped response
43
+ // teaches the model the call worked and it stops trying to fix auth.
44
+ // The guidance text is kept so hosts still show the user a way forward.
42
45
  return {
46
+ isError: true,
43
47
  content: [{
44
48
  type: "text",
45
49
  text: `**Not signed in.** To check usage, connect an OpenEphemeris account first.\n\n` +
@@ -60,7 +60,7 @@ registerTool({
60
60
  additionalProperties: false,
61
61
  },
62
62
  outputSchema: OUTPUT_SCHEMA_JSON,
63
- annotations: { title: "Astrocartography Lines", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
63
+ annotations: { title: "Astrocartography Lines", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
64
64
  handler: async (args) => {
65
65
  validateRequired(args, ["birth_datetime", "birth_latitude", "birth_longitude"]);
66
66
  const body = {
@@ -157,7 +157,7 @@ registerTool({
157
157
  additionalProperties: false,
158
158
  },
159
159
  outputSchema: OUTPUT_SCHEMA_JSON,
160
- annotations: { title: "Astrocartography Location Hits", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
160
+ annotations: { title: "Astrocartography Location Hits", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
161
161
  handler: async (args) => {
162
162
  validateRequired(args, ["birth_datetime", "birth_latitude", "birth_longitude", "query_latitude", "query_longitude"]);
163
163
  const body = {
@@ -158,7 +158,7 @@ registerTool({
158
158
  additionalProperties: false,
159
159
  },
160
160
  outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
161
- annotations: { title: "BaZi Four Pillars Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
161
+ annotations: { title: "BaZi Four Pillars Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
162
162
  handler: async (args) => {
163
163
  const body = { ...parseBaziArgs(args) };
164
164
  applyVisualConfig(body, args);
@@ -195,7 +195,7 @@ registerTool({
195
195
  additionalProperties: false,
196
196
  },
197
197
  outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
198
- annotations: { title: "BaZi Ten Gods", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
198
+ annotations: { title: "BaZi Ten Gods", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
199
199
  handler: async (args) => {
200
200
  const body = { ...parseBaziArgs(args) };
201
201
  applyVisualConfig(body, args);
@@ -230,7 +230,7 @@ registerTool({
230
230
  additionalProperties: false,
231
231
  },
232
232
  outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
233
- annotations: { title: "BaZi Element Balance", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
233
+ annotations: { title: "BaZi Element Balance", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
234
234
  handler: async (args) => {
235
235
  const body = { ...parseBaziArgs(args) };
236
236
  applyVisualConfig(body, args);
@@ -273,7 +273,7 @@ registerTool({
273
273
  additionalProperties: false,
274
274
  },
275
275
  outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
276
- annotations: { title: "BaZi Luck Pillars", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
276
+ annotations: { title: "BaZi Luck Pillars", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
277
277
  handler: async (args) => {
278
278
  if (!args.gender) {
279
279
  throw new Error("gender is required for luck pillar calculation. " +
@@ -316,7 +316,7 @@ registerTool({
316
316
  additionalProperties: false,
317
317
  },
318
318
  outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
319
- annotations: { title: "BaZi Annual Pillar", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
319
+ annotations: { title: "BaZi Annual Pillar", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
320
320
  handler: async (args) => {
321
321
  if (!args.year) {
322
322
  throw new Error("year is required. Provide a Gregorian year (1–9999), e.g. year=2025.");
@@ -385,7 +385,7 @@ registerTool({
385
385
  additionalProperties: false,
386
386
  },
387
387
  outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
388
- annotations: { title: "BaZi Compatibility", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
388
+ annotations: { title: "BaZi Compatibility", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
389
389
  handler: async (args) => {
390
390
  const chartAComponents = parseBaziArgs({
391
391
  year: args.chart_a_year,
@@ -447,7 +447,7 @@ registerTool({
447
447
  additionalProperties: false,
448
448
  },
449
449
  outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
450
- annotations: { title: "BaZi Chart (Visual)", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
450
+ annotations: { title: "BaZi Chart (Visual)", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
451
451
  handler: async (args) => {
452
452
  const { year, month, day, hour } = parseBaziArgs(args);
453
453
  const body = {
@@ -40,7 +40,7 @@ registerTool({
40
40
  additionalProperties: false,
41
41
  },
42
42
  outputSchema: OUTPUT_SCHEMA_IMAGE,
43
- annotations: { title: "Bi-Wheel Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
43
+ annotations: { title: "Bi-Wheel Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
44
44
  handler: async (args) => {
45
45
  validateRequired(args, ["datetime_a", "latitude_a", "longitude_a", "datetime_b", "latitude_b", "longitude_b"]);
46
46
  assertZonedDatetime("datetime_a", args.datetime_a, args.timezone_a, "timezone_a");
@@ -45,7 +45,7 @@ registerTool({
45
45
  additionalProperties: false,
46
46
  },
47
47
  outputSchema: OUTPUT_SCHEMA_IMAGE,
48
- annotations: { title: "Chart Wheel SVG", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
48
+ annotations: { title: "Chart Wheel SVG", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
49
49
  handler: async (args) => {
50
50
  validateRequired(args, ["datetime", "latitude", "longitude"]);
51
51
  assertZonedDatetime("datetime", args.datetime, args.timezone);
@@ -51,7 +51,7 @@ registerTool({
51
51
  additionalProperties: false,
52
52
  },
53
53
  outputSchema: OUTPUT_SCHEMA_JSON,
54
- annotations: { title: "Davison Relationship Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
54
+ annotations: { title: "Davison Relationship Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
55
55
  handler: async (args) => {
56
56
  validateRequired(args, [
57
57
  "person_a_datetime", "person_a_latitude", "person_a_longitude",
@@ -99,7 +99,7 @@ registerTool({
99
99
  additionalProperties: false,
100
100
  },
101
101
  outputSchema: OUTPUT_SCHEMA_JSON,
102
- annotations: { title: "Midpoint Composite Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
102
+ annotations: { title: "Midpoint Composite Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
103
103
  handler: async (args) => {
104
104
  validateRequired(args, [
105
105
  "person_a_datetime", "person_a_latitude", "person_a_longitude",
@@ -147,7 +147,7 @@ registerTool({
147
147
  additionalProperties: false,
148
148
  },
149
149
  outputSchema: OUTPUT_SCHEMA_JSON,
150
- annotations: { title: "Synastry House Overlay", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
150
+ annotations: { title: "Synastry House Overlay", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
151
151
  handler: async (args) => {
152
152
  validateRequired(args, [
153
153
  "person_a_datetime", "person_a_latitude", "person_a_longitude",
@@ -193,7 +193,7 @@ registerTool({
193
193
  additionalProperties: false,
194
194
  },
195
195
  outputSchema: OUTPUT_SCHEMA_JSON,
196
- annotations: { title: "Transits to Natal Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
196
+ annotations: { title: "Transits to Natal Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
197
197
  handler: async (args) => {
198
198
  validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude"]);
199
199
  assertPersonZoned("natal", args);
@@ -42,7 +42,7 @@ registerTool({
42
42
  additionalProperties: false,
43
43
  },
44
44
  outputSchema: OUTPUT_SCHEMA_JSON,
45
- annotations: { title: "Eclipse Finder", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
45
+ annotations: { title: "Eclipse Finder", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
46
46
  handler: async (args) => {
47
47
  validateRequired(args, ["eclipse_type"]);
48
48
  validateCoordinates(args, "latitude", "longitude");
@@ -59,7 +59,7 @@ registerTool({
59
59
  additionalProperties: false,
60
60
  },
61
61
  outputSchema: OUTPUT_SCHEMA_JSON,
62
- annotations: { title: "Electional Timing Window", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
62
+ annotations: { title: "Electional Timing Window", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
63
63
  handler: async (args) => {
64
64
  validateRequired(args, ["start_date", "end_date", "latitude", "longitude"]);
65
65
  const query = {
@@ -110,7 +110,7 @@ registerTool({
110
110
  additionalProperties: false,
111
111
  },
112
112
  outputSchema: OUTPUT_SCHEMA_JSON,
113
- annotations: { title: "Moment Quality Analysis", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
113
+ annotations: { title: "Moment Quality Analysis", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
114
114
  handler: async (args) => {
115
115
  const query = {};
116
116
  if (args.date)
@@ -164,7 +164,7 @@ registerTool({
164
164
  additionalProperties: false,
165
165
  },
166
166
  outputSchema: OUTPUT_SCHEMA_JSON,
167
- annotations: { title: "Retrograde Station Tracker", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
167
+ annotations: { title: "Retrograde Station Tracker", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
168
168
  handler: async (args) => {
169
169
  const query = {};
170
170
  // Map human-readable planet names to IDs if needed
@@ -224,7 +224,7 @@ registerTool({
224
224
  additionalProperties: false,
225
225
  },
226
226
  outputSchema: OUTPUT_SCHEMA_JSON,
227
- annotations: { title: "Active Aspects Search", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
227
+ annotations: { title: "Active Aspects Search", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
228
228
  handler: async (args) => {
229
229
  const query = {};
230
230
  if (args.date)
@@ -302,7 +302,7 @@ registerTool({
302
302
  additionalProperties: false,
303
303
  },
304
304
  outputSchema: OUTPUT_SCHEMA_JSON,
305
- annotations: { title: "Angle Crossing Search", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
305
+ annotations: { title: "Angle Crossing Search", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
306
306
  handler: async (args) => {
307
307
  validateRequired(args, ["latitude", "longitude"]);
308
308
  const query = {
@@ -40,7 +40,7 @@ registerTool({
40
40
  additionalProperties: false,
41
41
  },
42
42
  outputSchema: OUTPUT_SCHEMA_JSON,
43
- annotations: { title: "Planet Position", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
43
+ annotations: { title: "Planet Position", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
44
44
  handler: async (args) => {
45
45
  validateRequired(args, ["planet_id", "datetime"]);
46
46
  const body = {
@@ -89,7 +89,7 @@ registerTool({
89
89
  additionalProperties: false,
90
90
  },
91
91
  outputSchema: OUTPUT_SCHEMA_JSON,
92
- annotations: { title: "House Cusps", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
92
+ annotations: { title: "House Cusps", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
93
93
  handler: async (args) => {
94
94
  validateRequired(args, ["datetime", "latitude", "longitude"]);
95
95
  const body = {
@@ -56,7 +56,7 @@ registerTool({
56
56
  additionalProperties: false,
57
57
  },
58
58
  outputSchema: OUTPUT_SCHEMA_JSON,
59
- annotations: { title: "Natal Batch Charts", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
59
+ annotations: { title: "Natal Batch Charts", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
60
60
  handler: async (args) => {
61
61
  validateRequired(args, ["subjects"]);
62
62
  args.subjects.forEach((s, i) => assertZonedDatetime(`subjects[${i}].datetime`, s?.datetime, s?.timezone, `subjects[${i}].timezone`));
@@ -82,7 +82,7 @@ registerTool({
82
82
  additionalProperties: false,
83
83
  },
84
84
  outputSchema: OUTPUT_SCHEMA_JSON,
85
- annotations: { title: "Essential Dignities", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
85
+ annotations: { title: "Essential Dignities", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
86
86
  handler: async (args) => {
87
87
  validateRequired(args, ["datetime"]);
88
88
  return await getActiveClient().post("/ephemeris/dignities", {
@@ -122,7 +122,7 @@ registerTool({
122
122
  additionalProperties: false,
123
123
  },
124
124
  outputSchema: OUTPUT_SCHEMA_JSON,
125
- annotations: { title: "Retrograde Status", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
125
+ annotations: { title: "Retrograde Status", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
126
126
  handler: async (args) => {
127
127
  validateRequired(args, ["datetime"]);
128
128
  const client = getActiveClient();
@@ -173,7 +173,7 @@ registerTool({
173
173
  additionalProperties: false,
174
174
  },
175
175
  outputSchema: OUTPUT_SCHEMA_JSON,
176
- annotations: { title: "Midpoints", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
176
+ annotations: { title: "Midpoints", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
177
177
  handler: async (args) => {
178
178
  validateRequired(args, ["datetime", "latitude", "longitude"]);
179
179
  return await getActiveClient().post("/ephemeris/midpoints", {
@@ -219,7 +219,7 @@ registerTool({
219
219
  additionalProperties: false,
220
220
  },
221
221
  outputSchema: OUTPUT_SCHEMA_JSON,
222
- annotations: { title: "Fixed Stars", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
222
+ annotations: { title: "Fixed Stars", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
223
223
  handler: async (args) => {
224
224
  validateRequired(args, ["datetime"]);
225
225
  // A lone latitude would silently drop the angles from the scan, which is
@@ -262,7 +262,7 @@ registerTool({
262
262
  additionalProperties: false,
263
263
  },
264
264
  outputSchema: OUTPUT_SCHEMA_JSON,
265
- annotations: { title: "Hermetic Lots", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
265
+ annotations: { title: "Hermetic Lots", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
266
266
  handler: async (args) => {
267
267
  validateRequired(args, ["datetime", "latitude", "longitude"]);
268
268
  return await getActiveClient().post("/ephemeris/hermetic-lots", {
@@ -290,7 +290,7 @@ registerTool({
290
290
  additionalProperties: false,
291
291
  },
292
292
  outputSchema: OUTPUT_SCHEMA_JSON,
293
- annotations: { title: "Angles & Points", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
293
+ annotations: { title: "Angles & Points", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
294
294
  handler: async (args) => {
295
295
  validateRequired(args, ["datetime", "latitude", "longitude"]);
296
296
  return await getActiveClient().post("/ephemeris/angles-points", {
@@ -319,7 +319,7 @@ registerTool({
319
319
  additionalProperties: false,
320
320
  },
321
321
  outputSchema: OUTPUT_SCHEMA_JSON,
322
- annotations: { title: "Aspect Check", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
322
+ annotations: { title: "Aspect Check", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
323
323
  handler: async (args) => {
324
324
  validateRequired(args, ["longitude_1", "longitude_2"]);
325
325
  // Backend field names are longitude1 / longitude2 (no underscore)
@@ -49,7 +49,7 @@ registerTool({
49
49
  additionalProperties: false,
50
50
  },
51
51
  outputSchema: OUTPUT_SCHEMA_JSON,
52
- annotations: { title: "Human Design Bodygraph", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
52
+ annotations: { title: "Human Design Bodygraph", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
53
53
  handler: async (args) => {
54
54
  validateRequired(args, ["datetime"]);
55
55
  const body = {
@@ -58,7 +58,7 @@ registerTool({
58
58
  additionalProperties: false,
59
59
  },
60
60
  outputSchema: OUTPUT_SCHEMA_JSON,
61
- annotations: { title: "HD Planetary Return", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
61
+ annotations: { title: "HD Planetary Return", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
62
62
  handler: async (args) => {
63
63
  validateRequired(args, ["planet", "datetime", "return_year"]);
64
64
  return await getActiveClient().request("POST", "/human-design/cycles/return", {
@@ -112,7 +112,7 @@ registerTool({
112
112
  additionalProperties: false,
113
113
  },
114
114
  outputSchema: OUTPUT_SCHEMA_JSON,
115
- annotations: { title: "HD Opposition Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
115
+ annotations: { title: "HD Opposition Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
116
116
  handler: async (args) => {
117
117
  validateRequired(args, ["planet", "datetime", "target_year"]);
118
118
  return await getActiveClient().request("POST", "/human-design/cycles/opposition", {