@openephemeris/mcp-server 3.13.10 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +3 -3
  3. package/dist/index.js +5 -43
  4. package/dist/oauth/discovery.d.ts +1 -0
  5. package/dist/oauth/discovery.js +8 -3
  6. package/dist/oauth/store.d.ts +20 -0
  7. package/dist/oauth/store.js +117 -3
  8. package/dist/oauth/token.js +4 -2
  9. package/dist/prompts.js +9 -0
  10. package/dist/server-sse.js +71 -64
  11. package/dist/tools/apps/bazi-app.js +2 -0
  12. package/dist/tools/apps/bi-wheel-app.js +55 -6
  13. package/dist/tools/apps/bodygraph-app.js +68 -10
  14. package/dist/tools/apps/chart-wheel-app.js +53 -4
  15. package/dist/tools/apps/location-tools.js +3 -0
  16. package/dist/tools/apps/moon-phase-app.js +75 -21
  17. package/dist/tools/apps/transit-timeline-app.js +75 -23
  18. package/dist/tools/apps/vedic-chart-app.js +2 -0
  19. package/dist/tools/auth.js +4 -0
  20. package/dist/tools/dev.js +3 -0
  21. package/dist/tools/index.d.ts +6 -0
  22. package/dist/tools/index.js +8 -7
  23. package/dist/tools/output-schemas.d.ts +37 -0
  24. package/dist/tools/output-schemas.js +130 -0
  25. package/dist/tools/specialized/acg.js +3 -0
  26. package/dist/tools/specialized/bazi.js +8 -0
  27. package/dist/tools/specialized/bi_wheel.js +2 -0
  28. package/dist/tools/specialized/chart_wheel.js +2 -0
  29. package/dist/tools/specialized/comparative.js +5 -0
  30. package/dist/tools/specialized/eclipse.js +2 -0
  31. package/dist/tools/specialized/electional.js +5 -0
  32. package/dist/tools/specialized/ephemeris_core.js +3 -0
  33. package/dist/tools/specialized/ephemeris_extended.js +9 -0
  34. package/dist/tools/specialized/hd_bodygraph.js +2 -0
  35. package/dist/tools/specialized/hd_cycles.js +3 -0
  36. package/dist/tools/specialized/hd_group.js +3 -0
  37. package/dist/tools/specialized/human_design.js +2 -0
  38. package/dist/tools/specialized/moon.js +3 -0
  39. package/dist/tools/specialized/natal.js +2 -0
  40. package/dist/tools/specialized/progressed.js +2 -0
  41. package/dist/tools/specialized/relocation.js +2 -0
  42. package/dist/tools/specialized/returns.js +4 -0
  43. package/dist/tools/specialized/synastry.js +2 -0
  44. package/dist/tools/specialized/transits.js +52 -43
  45. package/dist/tools/specialized/vedic.js +2 -0
  46. package/dist/tools/specialized/venus_star_points.js +7 -0
  47. package/dist/ui/bazi.html +1 -1
  48. package/dist/ui/bi-wheel.html +3514 -3121
  49. package/dist/ui/bodygraph.html +1308 -1261
  50. package/dist/ui/chart-wheel.html +3404 -3116
  51. package/dist/ui/moon-phase.html +2622 -2616
  52. package/dist/ui/transit-timeline.html +2695 -2656
  53. package/dist/ui/vedic-chart.html +1 -1
  54. package/package.json +14 -11
  55. package/smithery.yaml +16 -1
@@ -20,6 +20,7 @@ import path from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { registerTool, SERVER_VERSION } from "../index.js";
22
22
  import { getActiveClient } from "../../backend/client.js";
23
+ import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
23
24
  // ── Constants ─────────────────────────────────────────────────────────────
24
25
  export const CHART_WHEEL_RESOURCE_URI = "ui://openephemeris/chart-wheel";
25
26
  export const CHART_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
@@ -139,9 +140,18 @@ registerTool({
139
140
  enum: ["placidus", "whole_sign", "equal", "koch", "regiomontanus", "campanus"],
140
141
  description: "House system to use (default: placidus).",
141
142
  },
143
+ bodies: {
144
+ type: "array",
145
+ items: { type: "string" },
146
+ description: "Optional list of body names to include. " +
147
+ "Defaults to 13 classical bodies (Sun through Pluto + Chiron + Nodes). " +
148
+ "Use 'all' as a single item to include every available body (Lilith, Ceres, Juno, Vesta, Pallas, Vertex, etc.). " +
149
+ "Example: ['sun','moon','lilith','vertex'].",
150
+ },
142
151
  },
143
152
  required: ["datetime", "latitude", "longitude"],
144
153
  },
154
+ outputSchema: OUTPUT_SCHEMA_JSON,
145
155
  annotations: {
146
156
  title: "Interactive Chart Wheel Explorer",
147
157
  readOnlyHint: true,
@@ -162,6 +172,17 @@ registerTool({
162
172
  const lat = args.latitude;
163
173
  const lon = args.longitude;
164
174
  const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
175
+ // Determine which bodies are requested
176
+ const requestedBodies = args.bodies;
177
+ const wantsAll = requestedBodies?.some(b => b.toLowerCase() === "all");
178
+ const wantsExtended = wantsAll || requestedBodies?.some(b => EXTENDED_BODIES.has(b.toLowerCase()));
179
+ // Enable extended body computation on the API side when needed
180
+ if (wantsExtended) {
181
+ const config = natalBody.configuration;
182
+ config.include_asteroids = true;
183
+ config.include_lilith = true;
184
+ config.include_chiron = true;
185
+ }
165
186
  // Fetch natal chart JSON. The chart is rendered client-side in the UI iframe,
166
187
  // so we do NOT call the /visualization/chart-wheel endpoint — that was the
167
188
  // source of the ~90 second blocking delay.
@@ -175,7 +196,7 @@ registerTool({
175
196
  latitude: lat ?? null,
176
197
  longitude: lon ?? null,
177
198
  location: args.location ?? null,
178
- }, houseSystem);
199
+ }, houseSystem, undefined, requestedBodies);
179
200
  const bundleAvailable = Boolean(getChartWheelBundle());
180
201
  if (bundleAvailable) {
181
202
  return {
@@ -216,6 +237,7 @@ registerTool({
216
237
  },
217
238
  required: ["planet", "longitude"],
218
239
  },
240
+ outputSchema: OUTPUT_SCHEMA_JSON,
219
241
  annotations: { title: "Planet Interpretation", readOnlyHint: true, openWorldHint: false },
220
242
  _meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
221
243
  handler: async (args) => {
@@ -250,6 +272,7 @@ registerTool({
250
272
  },
251
273
  required: ["house_number"],
252
274
  },
275
+ outputSchema: OUTPUT_SCHEMA_JSON,
253
276
  annotations: { title: "House Interpretation", readOnlyHint: true, openWorldHint: false },
254
277
  _meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
255
278
  handler: async (args) => {
@@ -285,6 +308,7 @@ registerTool({
285
308
  },
286
309
  required: ["planet1", "planet2"],
287
310
  },
311
+ outputSchema: OUTPUT_SCHEMA_JSON,
288
312
  annotations: { title: "Aspect Interpretation", readOnlyHint: true, openWorldHint: false },
289
313
  _meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
290
314
  handler: async (args) => {
@@ -333,6 +357,7 @@ registerTool({
333
357
  },
334
358
  required: ["datetime", "latitude", "longitude", "house_system"],
335
359
  },
360
+ outputSchema: OUTPUT_SCHEMA_JSON,
336
361
  annotations: { title: "Recalculate Chart", readOnlyHint: true, openWorldHint: false },
337
362
  _meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
338
363
  handler: async (args) => {
@@ -391,6 +416,13 @@ const CLASSICAL_PLANETS = new Set([
391
416
  "jupiter", "saturn", "uranus", "neptune", "pluto",
392
417
  "chiron", "north_node", "south_node", "true_node", "asc", "mc",
393
418
  ]);
419
+ const EXTENDED_BODIES = new Set([
420
+ "mean_lilith", "lilith", "true_lilith",
421
+ "ceres", "juno", "vesta", "pallas", "pholus",
422
+ "vertex", "part_of_fortune",
423
+ "eris", "sedna",
424
+ ]);
425
+ const ALL_KNOWN_BODIES = new Set([...CLASSICAL_PLANETS, ...EXTENDED_BODIES]);
394
426
  /**
395
427
  * Build a lean, SVG-free payload for Claude's text context.
396
428
  * Strips SVG, filters to classical planets only, and packages birth params.
@@ -422,7 +454,13 @@ function computeAspects(planets) {
422
454
  diff = 360 - diff;
423
455
  for (const asp of ASPECTS) {
424
456
  const orb = Math.abs(diff - asp.angle);
425
- if (orb <= asp.orb) {
457
+ // Tighten orbs when one or both bodies are extended (asteroids, etc.)
458
+ const p1Extended = EXTENDED_BODIES.has(p1.name.toLowerCase());
459
+ const p2Extended = EXTENDED_BODIES.has(p2.name.toLowerCase());
460
+ const effectiveOrb = (p1Extended || p2Extended)
461
+ ? Math.min(asp.orb * 0.5, 3) // half standard, capped at 3°
462
+ : asp.orb;
463
+ if (orb <= effectiveOrb) {
426
464
  // Applying: combined longitudinal speed closing the orb
427
465
  const applying = ((p1.speed ?? 1) - (p2.speed ?? 0)) < 0 ? diff < asp.angle : diff > asp.angle;
428
466
  results.push({
@@ -440,7 +478,7 @@ function computeAspects(planets) {
440
478
  }
441
479
  return results;
442
480
  }
443
- function buildModelPayload(chartData, birthParams, houseSystem, svgBase) {
481
+ function buildModelPayload(chartData, birthParams, houseSystem, svgBase, bodyFilter) {
444
482
  // ── 1. Normalize planets: keyed object → typed PlanetData array ──────────
445
483
  // The natal API returns { sun: { longitude, sign_name, is_retrograde, ... }, moon: {...} }.
446
484
  // The chart wheel UI expects PlanetData[] with canonical field names.
@@ -448,7 +486,18 @@ function buildModelPayload(chartData, birthParams, houseSystem, svgBase) {
448
486
  let planetsArray = [];
449
487
  if (rawPlanets && typeof rawPlanets === "object" && !Array.isArray(rawPlanets)) {
450
488
  planetsArray = Object.entries(rawPlanets)
451
- .filter(([k]) => CLASSICAL_PLANETS.has(k.toLowerCase()))
489
+ .filter(([k]) => {
490
+ const key = k.toLowerCase();
491
+ // If specific bodies were requested, use that filter
492
+ if (bodyFilter) {
493
+ const wantsAll = bodyFilter.some(b => b.toLowerCase() === "all");
494
+ if (wantsAll)
495
+ return ALL_KNOWN_BODIES.has(key);
496
+ return bodyFilter.some(b => b.toLowerCase() === key);
497
+ }
498
+ // Default: classical planets only
499
+ return CLASSICAL_PLANETS.has(key);
500
+ })
452
501
  .map(([name, p]) => ({
453
502
  name: name.toLowerCase(),
454
503
  longitude: (p.longitude ?? p.lon ?? 0),
@@ -1,5 +1,6 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
+ import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
3
4
  registerTool({
4
5
  name: "location_search",
5
6
  description: "App-only tool for getting location autocomplete suggestions. Returns latitude, longitude, and IANA timezone.",
@@ -14,6 +15,7 @@ registerTool({
14
15
  required: ["query"],
15
16
  additionalProperties: false,
16
17
  },
18
+ outputSchema: OUTPUT_SCHEMA_JSON,
17
19
  _meta: {
18
20
  ui: {
19
21
  visibility: ["app"],
@@ -38,6 +40,7 @@ registerTool({
38
40
  required: ["latitude", "longitude"],
39
41
  additionalProperties: false,
40
42
  },
43
+ outputSchema: OUTPUT_SCHEMA_JSON,
41
44
  _meta: {
42
45
  ui: {
43
46
  visibility: ["app"],
@@ -16,6 +16,7 @@ import path from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { registerTool, validateCoordinates, SERVER_VERSION } from "../index.js";
18
18
  import { getActiveClient } from "../../backend/client.js";
19
+ import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
19
20
  // ── Constants ─────────────────────────────────────────────────────────────────
20
21
  export const MOON_PHASE_RESOURCE_URI = "ui://openephemeris/moon-phase";
21
22
  export const MOON_PHASE_MIME_TYPE = "text/html;profile=mcp-app";
@@ -54,15 +55,26 @@ export function clearMoonPhaseBundleCache() {
54
55
  function buildMoonSummary(data) {
55
56
  const phase = data.phase;
56
57
  const voc = data.void_of_course;
57
- const phaseName = String(phase?.phase_name ?? phase?.name ?? "Unknown");
58
- const illumination = typeof phase?.illumination === "number" ? Math.round(phase.illumination * 100) : "?";
59
- const moonSign = String(phase?.moon_sign ?? phase?.sign ?? "");
58
+ const meta = (phase?.metadata ?? {});
59
+ const phaseName = String(phase?.phase_name ?? phase?.name ?? "Unknown").replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
60
+ const illumination = typeof phase?.illumination === "number" ? Math.round(phase.illumination) : "?";
61
+ const moonSign = String(meta?.moon_sign ?? phase?.moon_sign ?? phase?.sign ?? voc?.current_sign ?? "");
60
62
  const isVoc = Boolean(voc?.is_void ?? voc?.is_void_of_course ?? false);
63
+ const speed = typeof meta?.longitude_speed === "number" ? meta.longitude_speed : null;
64
+ const daysToFull = typeof meta?.days_to_full === "number" ? meta.days_to_full :
65
+ typeof phase?.days_to_full === "number" ? phase.days_to_full : null;
61
66
  let summary = "**Moon Phase** — " + phaseName + "\n\n" +
62
67
  "Illumination: **" + illumination + "%**\n";
63
68
  if (moonSign) {
64
69
  summary += "Moon Sign: **" + moonSign + "**\n";
65
70
  }
71
+ if (speed !== null) {
72
+ const tempo = speed > 14.5 ? "fast" : speed < 12.0 ? "slow" : "average";
73
+ summary += "Speed: **" + speed.toFixed(2) + "°/day** (" + tempo + ")\n";
74
+ }
75
+ if (daysToFull !== null) {
76
+ summary += "Full Moon in: **" + daysToFull.toFixed(1) + " days**\n";
77
+ }
66
78
  if (isVoc) {
67
79
  summary += "\n⚠️ **Void of Course** — The Moon is currently VOC.";
68
80
  if (voc?.ends)
@@ -107,6 +119,7 @@ registerTool({
107
119
  required: [],
108
120
  additionalProperties: false,
109
121
  },
122
+ outputSchema: OUTPUT_SCHEMA_JSON,
110
123
  annotations: {
111
124
  title: "Interactive Moon Phase Explorer",
112
125
  readOnlyHint: true,
@@ -129,38 +142,79 @@ registerTool({
129
142
  params.latitude = args.latitude;
130
143
  if (args.longitude != null)
131
144
  params.longitude = args.longitude;
132
- // Fetch phase and VOC in parallel
133
- const [phase, voc] = await Promise.allSettled([
145
+ // Fetch phase, VOC, and moon aspects in parallel
146
+ const [phase, voc, aspects] = await Promise.allSettled([
134
147
  client.request("GET", "/ephemeris/moon/phase", { params }),
135
148
  client.request("GET", "/ephemeris/moon/void-of-course", { params }),
149
+ client.request("GET", "/ephemeris/moon/aspects", { params }),
136
150
  ]);
137
151
  const mergedData = {
138
152
  phase: phase.status === "fulfilled" ? phase.value : { error: phase.reason?.message },
139
153
  void_of_course: voc.status === "fulfilled" ? voc.value : { error: voc.reason?.message },
154
+ aspects: aspects.status === "fulfilled" ? aspects.value : null,
140
155
  };
141
156
  // Flatten for the UI — merge phase fields to top level
142
157
  const phaseData = mergedData.phase;
143
158
  const vocData = mergedData.void_of_course;
159
+ const aspectsData = mergedData.aspects;
160
+ // Extract enriched metadata fields from the GET response
161
+ const meta = (phaseData?.metadata ?? {});
162
+ const moonSign = String(meta?.moon_sign ?? phaseData?.moon_sign ?? phaseData?.sign ?? vocData?.current_sign ?? "");
163
+ const moonSpeedRaw = meta?.longitude_speed;
164
+ const moonSpeed = typeof moonSpeedRaw === "number" ? moonSpeedRaw : null;
165
+ const angularDiameter = typeof meta?.angular_diameter === "number" ? meta.angular_diameter : null;
166
+ const distanceKm = typeof meta?.moon_distance_km === "number" ? meta.moon_distance_km : null;
167
+ const moonLat = typeof meta?.moon_lat === "number" ? meta.moon_lat : null;
168
+ const ageDays = typeof meta?.age_days === "number" ? meta.age_days :
169
+ (typeof phaseData?.age_days === "number" ? phaseData.age_days : null);
170
+ // days_to_new / days_to_full can come as the union value or from the top-level field
171
+ const extractDays = (field) => {
172
+ if (typeof field === "number")
173
+ return field;
174
+ if (field && typeof field === "object") {
175
+ const v = Object.values(field)[0];
176
+ if (typeof v === "number")
177
+ return v;
178
+ }
179
+ return null;
180
+ };
181
+ const daysToNew = extractDays(phaseData?.days_to_new);
182
+ const daysToFull = extractDays(phaseData?.days_to_full);
183
+ // Top 3 applying aspects by tightest orb
184
+ const rawAspects = (aspectsData?.aspects ?? aspectsData?.data ?? []);
185
+ const applyingAspects = Array.isArray(rawAspects)
186
+ ? rawAspects
187
+ .filter((a) => a.applying === true || a.type === "applying")
188
+ .sort((a, b) => Math.abs(a.orb ?? 99) - Math.abs(b.orb ?? 99))
189
+ .slice(0, 3)
190
+ .map((a) => ({
191
+ planet: a.planet ?? a.body ?? a.planet_name ?? "",
192
+ aspect: a.aspect ?? a.aspect_name ?? a.type_name ?? "",
193
+ orb: typeof a.orb === "number" ? Math.abs(a.orb) : null,
194
+ }))
195
+ : [];
144
196
  const uiPayload = {
145
- phase_name: phaseData?.phase_name ?? phaseData?.name ?? "Unknown",
146
- illumination: phaseData?.illumination ?? 0,
147
- age_days: phaseData?.age_days ?? phaseData?.age ?? null,
197
+ phase_name: String(phaseData?.phase_name ?? phaseData?.name ?? "Unknown").replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()),
198
+ illumination: typeof phaseData?.illumination === "number" ? phaseData.illumination / 100.0 : 0,
199
+ phase_angle: typeof phaseData?.phase_angle === "number" ? phaseData.phase_angle : null,
200
+ age_days: ageDays,
201
+ days_to_new: daysToNew,
202
+ days_to_full: daysToFull,
148
203
  waxing: phaseData?.waxing ??
149
204
  // Fallback chain: elongation < 180° = waxing, else age_days < 14.75 = waxing.
150
- // Never use illumination alone — it's identical for mirror-symmetric waxing/waning phases.
151
- (typeof phaseData?.elongation === "number"
152
- ? phaseData.elongation < 180
153
- : typeof phaseData?.age_days === "number"
154
- ? phaseData.age_days < 14.75
205
+ (typeof phaseData?.phase_angle === "number"
206
+ ? phaseData.phase_angle < 180
207
+ : typeof ageDays === "number"
208
+ ? ageDays < 14.75
155
209
  : true), // safe default: assume waxing
156
- moon_sign: phaseData?.moon_sign ?? phaseData?.sign ?? "",
157
- moon_longitude: phaseData?.moon_longitude ?? phaseData?.longitude ?? null,
158
- sun_longitude: phaseData?.sun_longitude ?? null,
159
- elongation: phaseData?.elongation ?? null,
160
- moon_latitude: phaseData?.moon_latitude ?? null,
161
- moon_distance_km: phaseData?.moon_distance_km ?? phaseData?.distance_km ?? null,
162
- next_new_moon: phaseData?.next_new_moon ?? null,
163
- next_full_moon: phaseData?.next_full_moon ?? null,
210
+ moon_sign: moonSign,
211
+ moon_longitude: meta?.moon_lon ?? phaseData?.moon_longitude ?? phaseData?.longitude ?? null,
212
+ moon_latitude: moonLat,
213
+ moon_distance_km: distanceKm,
214
+ angular_diameter: angularDiameter,
215
+ moon_speed: moonSpeed,
216
+ sun_longitude: meta?.sun_lon ?? phaseData?.sun_longitude ?? null,
217
+ applying_aspects: applyingAspects,
164
218
  voc_status: {
165
219
  is_void: Boolean(vocData?.is_void ?? vocData?.is_void_of_course ?? false),
166
220
  started: vocData?.started ?? vocData?.start ?? null,
@@ -17,6 +17,7 @@ import path from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { registerTool, validateRequired, SERVER_VERSION } from "../index.js";
19
19
  import { getActiveClient } from "../../backend/client.js";
20
+ import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
20
21
  // ── Constants ─────────────────────────────────────────────────────────────────
21
22
  export const TRANSIT_TIMELINE_RESOURCE_URI = "ui://openephemeris/transit-timeline";
22
23
  export const TRANSIT_TIMELINE_MIME_TYPE = "text/html;profile=mcp-app";
@@ -129,6 +130,7 @@ registerTool({
129
130
  required: ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"],
130
131
  additionalProperties: false,
131
132
  },
133
+ outputSchema: OUTPUT_SCHEMA_JSON,
132
134
  annotations: {
133
135
  title: "Interactive Transit Timeline Explorer",
134
136
  readOnlyHint: true,
@@ -145,6 +147,19 @@ registerTool({
145
147
  handler: async (args) => {
146
148
  validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"]);
147
149
  const client = getActiveClient();
150
+ // Guard: reject excessively wide date ranges before hitting the backend.
151
+ // The Go backend clamps Explorer tier to 1 year, but larger tiers can still
152
+ // trigger compute-bound 45-second timeouts on very wide windows.
153
+ const startMs = new Date(args.start_date).getTime();
154
+ const endMs = new Date(args.end_date).getTime();
155
+ if (!isNaN(startMs) && !isNaN(endMs)) {
156
+ const spanDays = (endMs - startMs) / (1000 * 60 * 60 * 24);
157
+ if (spanDays > 5 * 365) {
158
+ throw new Error(`Date range too wide (${Math.round(spanDays / 365)} years). ` +
159
+ `The transit timeline works best with windows under 2 years — try narrowing ` +
160
+ `start_date and end_date to a 1–2 year window and re-run.`);
161
+ }
162
+ }
148
163
  // Step 1: Compute natal chart to extract planetary longitudes
149
164
  const natalBody = {
150
165
  subject: {
@@ -161,32 +176,57 @@ registerTool({
161
176
  const natalPositionMap = {};
162
177
  const planets = natalResult?.planets || natalResult?.data?.planets || [];
163
178
  const planetArray = Array.isArray(planets) ? planets : Object.values(planets);
179
+ // ── Core natal points to target (conjunction search) ─────────────────────
180
+ // When no natal_points filter is given, we limit to 7 core bodies to keep
181
+ // target_degrees count manageable. 4 outer planets × 10+ targets creates a
182
+ // combinatoric explosion (40+ serial SearchTransit calls on the backend).
183
+ const CORE_BODIES_DEFAULT = new Set([
184
+ "sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn",
185
+ ]);
164
186
  const wantedPoints = args.natal_points
165
187
  ? new Set(args.natal_points.map((p) => p.toLowerCase()))
166
- : null;
188
+ : CORE_BODIES_DEFAULT;
167
189
  for (const planet of planetArray) {
168
190
  const name = (planet.name || planet.id || "").toLowerCase();
169
191
  const lon = planet.longitude ?? planet.ecliptic_longitude ?? planet.lon;
170
192
  if (typeof lon !== "number" || !isFinite(lon))
171
193
  continue;
172
- if (wantedPoints && !wantedPoints.has(name))
194
+ if (!wantedPoints.has(name))
173
195
  continue;
174
196
  targetDegrees.push(Math.round(lon * 100) / 100);
175
197
  natalPositionMap[name] = lon;
176
198
  }
177
- // Extract angles (ASC, MC)
178
- const angles = natalResult?.angles || natalResult?.data?.angles;
179
- if (angles) {
180
- const angleEntries = Array.isArray(angles) ? angles : Object.entries(angles).map(([k, v]) => ({ name: k, ...(typeof v === 'object' ? v : { longitude: v }) }));
181
- for (const angle of angleEntries) {
182
- const name = (angle.name || angle.id || "").toLowerCase();
183
- const lon = angle.longitude ?? angle.ecliptic_longitude ?? angle.lon;
184
- if (typeof lon !== "number" || !isFinite(lon))
185
- continue;
186
- if (wantedPoints && !wantedPoints.has(name))
187
- continue;
188
- targetDegrees.push(Math.round(lon * 100) / 100);
189
- natalPositionMap[name] = lon;
199
+ // Extract angles (ASC, MC) — only if explicitly requested via natal_points
200
+ if (args.natal_points) {
201
+ const angles = natalResult?.angles || natalResult?.data?.angles;
202
+ if (angles) {
203
+ const angleEntries = Array.isArray(angles)
204
+ ? angles
205
+ : Object.entries(angles).map(([k, v]) => ({ name: k, ...(typeof v === "object" ? v : { longitude: v }) }));
206
+ for (const angle of angleEntries) {
207
+ const name = (angle.name || angle.id || "").toLowerCase();
208
+ const lon = angle.longitude ?? angle.ecliptic_longitude ?? angle.lon;
209
+ if (typeof lon !== "number" || !isFinite(lon))
210
+ continue;
211
+ if (!wantedPoints.has(name))
212
+ continue;
213
+ targetDegrees.push(Math.round(lon * 100) / 100);
214
+ natalPositionMap[name] = lon;
215
+ }
216
+ }
217
+ }
218
+ // Safety cap: never send more than 12 target degrees to the backend
219
+ // (prevents combinatoric timeouts when many planets + many targets overlap).
220
+ const MAX_TARGETS = 12;
221
+ if (targetDegrees.length > MAX_TARGETS) {
222
+ targetDegrees.splice(MAX_TARGETS);
223
+ // Prune natalPositionMap to match
224
+ const keepNames = new Set(Object.entries(natalPositionMap)
225
+ .filter(([, v]) => targetDegrees.includes(Math.round(v * 100) / 100))
226
+ .map(([k]) => k));
227
+ for (const k of Object.keys(natalPositionMap)) {
228
+ if (!keepNames.has(k))
229
+ delete natalPositionMap[k];
190
230
  }
191
231
  }
192
232
  if (targetDegrees.length === 0) {
@@ -199,31 +239,43 @@ registerTool({
199
239
  let endStr = args.end_date;
200
240
  if (/^\d{4}-\d{2}-\d{2}$/.test(endStr))
201
241
  endStr += "T23:59:59Z";
242
+ const aspectAngle = typeof args.aspect_angle === "number" ? args.aspect_angle : 0;
243
+ let effectiveTargets = targetDegrees;
244
+ if (aspectAngle !== 0) {
245
+ effectiveTargets = targetDegrees.map((deg) => ((deg + aspectAngle) % 360 + 360) % 360);
246
+ }
202
247
  const transitBody = {
203
248
  start_date: startStr,
204
249
  end_date: endStr,
205
- target_degrees: targetDegrees,
250
+ target_degrees: effectiveTargets,
206
251
  };
207
252
  if (args.transiting_planets)
208
253
  transitBody.planet_names = args.transiting_planets;
209
- let effectiveTargets = targetDegrees;
210
- const aspectAngle = typeof args.aspect_angle === "number" ? args.aspect_angle : 0;
211
254
  if (aspectAngle !== 0) {
212
- effectiveTargets = targetDegrees.map((deg) => ((deg + aspectAngle) % 360 + 360) % 360);
213
- transitBody.target_degrees = effectiveTargets;
214
255
  transitBody.search_criteria = { aspect_angle: aspectAngle };
215
256
  }
216
- const transitResult = await client.request("POST", "/predictive/transits/search", { data: transitBody });
257
+ // Give the backend a 90s window explorer tier allows 30s Go compute
258
+ // timeout, but the node-to-Fly round-trip including queuing can extend this.
259
+ const transitResult = await client.request("POST", "/predictive/transits/search", { data: transitBody, timeoutMs: 90_000 });
260
+ // Normalize the API response into a flat array so the iframe UI always
261
+ // receives an iterable — some backend response shapes wrap events in a
262
+ // container object rather than returning a bare array.
263
+ const transitArray = Array.isArray(transitResult)
264
+ ? transitResult
265
+ : (transitResult?.events ??
266
+ transitResult?.transits ??
267
+ transitResult?.results ??
268
+ []);
217
269
  // Build payload
218
270
  const payload = {
219
271
  natal_positions: natalPositionMap,
220
272
  target_degrees_used: effectiveTargets,
221
273
  aspect_angle: aspectAngle,
222
- transit_results: transitResult,
274
+ transit_results: transitArray,
223
275
  search_window: { start_date: args.start_date, end_date: args.end_date },
224
276
  server_version: SERVER_VERSION,
225
277
  };
226
- const summary = buildTransitSummary(natalPositionMap, transitResult, aspectAngle, args.start_date, args.end_date);
278
+ const summary = buildTransitSummary(natalPositionMap, transitArray, aspectAngle, args.start_date, args.end_date);
227
279
  const bundleAvailable = Boolean(getTransitTimelineBundle());
228
280
  if (bundleAvailable) {
229
281
  return {
@@ -14,6 +14,7 @@ import path from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { registerTool, SERVER_VERSION } from "../index.js";
16
16
  import { getActiveClient } from "../../backend/client.js";
17
+ import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
17
18
  // ── Constants ─────────────────────────────────────────────────────────────────
18
19
  export const VEDIC_CHART_RESOURCE_URI = "ui://openephemeris/vedic-chart";
19
20
  export const VEDIC_CHART_MIME_TYPE = "text/html;profile=mcp-app";
@@ -149,6 +150,7 @@ registerTool({
149
150
  required: ["datetime", "latitude", "longitude"],
150
151
  additionalProperties: false,
151
152
  },
153
+ outputSchema: OUTPUT_SCHEMA_JSON,
152
154
  annotations: {
153
155
  title: "Interactive Vedic Jyotish Chart",
154
156
  readOnlyHint: true,
@@ -1,6 +1,7 @@
1
1
  import { registerTool } from "./index.js";
2
2
  import { CredentialManager } from "../auth/credentials.js";
3
3
  import { DeviceAuthFlow } from "../auth/device-auth.js";
4
+ import { OUTPUT_SCHEMA_AUTH } from "./output-schemas.js";
4
5
  const credentialManager = new CredentialManager();
5
6
  // ────────────────────────────────────────────────────────
6
7
  // auth.login — Start the device authorization flow
@@ -17,6 +18,7 @@ registerTool({
17
18
  properties: {},
18
19
  additionalProperties: false,
19
20
  },
21
+ outputSchema: OUTPUT_SCHEMA_AUTH,
20
22
  annotations: {
21
23
  title: "Connect Account",
22
24
  readOnlyHint: false,
@@ -95,6 +97,7 @@ registerTool({
95
97
  properties: {},
96
98
  additionalProperties: false,
97
99
  },
100
+ outputSchema: OUTPUT_SCHEMA_AUTH,
98
101
  annotations: {
99
102
  title: "Auth Status",
100
103
  readOnlyHint: true,
@@ -179,6 +182,7 @@ registerTool({
179
182
  properties: {},
180
183
  additionalProperties: false,
181
184
  },
185
+ outputSchema: OUTPUT_SCHEMA_AUTH,
182
186
  annotations: {
183
187
  title: "Disconnect Account",
184
188
  readOnlyHint: false,
package/dist/tools/dev.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { registerTool, validateRequired } from "./index.js";
2
2
  import { getActiveClient } from "../backend/client.js";
3
+ import { OUTPUT_SCHEMA_JSON } from "./output-schemas.js";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import { fileURLToPath } from "node:url";
@@ -123,6 +124,7 @@ registerTool({
123
124
  required: ["method", "path"],
124
125
  additionalProperties: false,
125
126
  },
127
+ outputSchema: OUTPUT_SCHEMA_JSON,
126
128
  handler: async (args) => {
127
129
  validateRequired(args, ["path"]);
128
130
  const methodStr = String(args.method || "GET").toUpperCase();
@@ -189,6 +191,7 @@ registerTool({
189
191
  properties: {},
190
192
  additionalProperties: false,
191
193
  },
194
+ outputSchema: OUTPUT_SCHEMA_JSON,
192
195
  handler: async () => {
193
196
  const allowlist = loadAllowlist();
194
197
  return {
@@ -5,6 +5,12 @@ export interface ToolDefinition {
5
5
  name: string;
6
6
  description: string;
7
7
  inputSchema: z.ZodType<any> | Record<string, unknown>;
8
+ /**
9
+ * MCP spec 2025-11-25 — JSON Schema describing the structured output of this tool.
10
+ * Smithery uses this to score "Output schemas" in the quality rubric.
11
+ * All tools return MCP content block arrays; specialized tools declare richer shapes.
12
+ */
13
+ outputSchema?: Record<string, unknown>;
8
14
  annotations?: {
9
15
  title?: string;
10
16
  readOnlyHint?: boolean;
@@ -47,7 +47,11 @@ export async function initTools(profile) {
47
47
  // Always register the generic proxy tools (dev.call + dev.list_allowed).
48
48
  await import("./dev.js");
49
49
  if (resolvedProfile === "dev") {
50
- // Register all specialized domain tools.
50
+ // ── FULL BUILD: all specialized tools ───────────────────────────
51
+ // Payload size theory was disproven. The "Failed to call tool"
52
+ // bug was caused by Claude Desktop's MCP App iframe handler
53
+ // corruption. Iframe tools are purged; all specialized tools are restored.
54
+ // ────────────────────────────────────────────────────────────────
51
55
  await import("./specialized/natal.js");
52
56
  await import("./specialized/transits.js");
53
57
  await import("./specialized/moon.js");
@@ -70,15 +74,12 @@ export async function initTools(profile) {
70
74
  await import("./specialized/ephemeris_extended.js");
71
75
  await import("./specialized/venus_star_points.js");
72
76
  await import("./specialized/acg.js");
73
- // MCP App tools (interactive UI)
77
+ await import("./apps/location-tools.js");
78
+ // Re-enable requested MCP apps (iframes)
74
79
  await import("./apps/chart-wheel-app.js");
75
- await import("./apps/bodygraph-app.js");
76
80
  await import("./apps/bi-wheel-app.js");
77
- await import("./apps/transit-timeline-app.js");
81
+ await import("./apps/bodygraph-app.js");
78
82
  await import("./apps/moon-phase-app.js");
79
- await import("./apps/bazi-app.js");
80
- await import("./apps/vedic-chart-app.js");
81
- await import("./apps/location-tools.js");
82
83
  }
83
84
  }
84
85
  /**
@@ -0,0 +1,37 @@
1
+ /**
2
+ * output-schemas.ts — Shared JSON Schema output schema definitions for MCP tools.
3
+ *
4
+ * MCP spec 2025-11-25 supports `outputSchema` on tool definitions. Smithery
5
+ * uses this field to score the "Output schemas" quality criterion (10.37pt / 57 tools).
6
+ *
7
+ * All tools return an MCP content block response. Most return a single JSON text
8
+ * block; visual tools return an image block or an image + text block.
9
+ *
10
+ * Usage:
11
+ * import { OUTPUT_SCHEMA_JSON, OUTPUT_SCHEMA_IMAGE } from "../output-schemas.js";
12
+ * registerTool({ ..., outputSchema: OUTPUT_SCHEMA_JSON });
13
+ */
14
+ /**
15
+ * Standard tool output schema: a single JSON text block.
16
+ * Used by all data-returning tools (natal chart, transits, synastry, etc.).
17
+ */
18
+ export declare const OUTPUT_SCHEMA_JSON: Record<string, unknown>;
19
+ /**
20
+ * Visual tool output schema: a single SVG or PNG image block.
21
+ * Used by chart wheel (direct image), BaZi chart, Vedic chart, etc.
22
+ */
23
+ export declare const OUTPUT_SCHEMA_IMAGE: Record<string, unknown>;
24
+ /**
25
+ * Dual output schema: an image block followed by a JSON text block.
26
+ * Used by tools with include_visual=true (natal chart SVG + data, bi-wheel, etc.).
27
+ */
28
+ export declare const OUTPUT_SCHEMA_IMAGE_AND_JSON: Record<string, unknown>;
29
+ /**
30
+ * Interactive app tool output schema: a text block containing MCP App metadata.
31
+ * Used by explore_* tools that return an embedded UI resource reference.
32
+ */
33
+ export declare const OUTPUT_SCHEMA_APP: Record<string, unknown>;
34
+ /**
35
+ * Auth tool output schema: a JSON text block confirming auth status or credentials.
36
+ */
37
+ export declare const OUTPUT_SCHEMA_AUTH: Record<string, unknown>;