@openephemeris/mcp-server 3.13.10 → 3.15.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 +53 -0
  2. package/README.md +6 -6
  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 +25 -16
  10. package/dist/server-sse.js +136 -78
  11. package/dist/tools/apps/bazi-app.js +2 -0
  12. package/dist/tools/apps/bi-wheel-app.js +65 -7
  13. package/dist/tools/apps/bodygraph-app.js +85 -11
  14. package/dist/tools/apps/chart-wheel-app.js +80 -7
  15. package/dist/tools/apps/location-tools.js +41 -5
  16. package/dist/tools/apps/moon-phase-app.js +82 -22
  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 +104 -67
  21. package/dist/tools/index.d.ts +16 -0
  22. package/dist/tools/index.js +23 -8
  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 +14 -6
  27. package/dist/tools/specialized/bi_wheel.js +5 -2
  28. package/dist/tools/specialized/chart_wheel.js +5 -2
  29. package/dist/tools/specialized/comparative.js +7 -2
  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 +6 -3
  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 +22 -22
  48. package/dist/ui/bodygraph.html +1981 -1904
  49. package/dist/ui/chart-wheel.html +3148 -2806
  50. package/dist/ui/vedic-chart.html +22 -22
  51. package/package.json +18 -11
  52. package/smithery.yaml +16 -1
  53. package/dist/ui/bi-wheel.html +0 -7485
  54. package/dist/ui/moon-phase.html +0 -6758
  55. package/dist/ui/transit-timeline.html +0 -6835
@@ -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";
@@ -31,66 +32,72 @@ function isDeniedByPrefix(pathname, prefixes) {
31
32
  function isAllowedOperation(method, pathname, allow) {
32
33
  return allow.some((e) => e.method === method && e.path === pathname);
33
34
  }
34
- registerTool({
35
- name: "dev_call",
36
- description: "Call any allowlisted Open Ephemeris API endpoint directly. This is the power-user escape hatch " +
37
- "— use the typed tools (ephemeris_natal_chart, ephemeris_transits, etc.) first for common operations. " +
38
- "Call dev_list_allowed to see all currently available endpoint paths.\n\n" +
39
- "AUTH: Set OPENEPHEMERIS_API_KEY in your environment. See openephemeris.com/dashboard for active plan limits.\n\n" +
40
- "CREDIT COSTS:\n" +
41
- " • Standard chart math (natal, progressed, bazi, vedic, iching): 1 credit\n" +
42
- " • Human Design: 2 credits\n" +
43
- " • Visualization rendering (chart-wheel, bi-wheel, charts/*): 2 credits\n" +
44
- " • Comparative math (synastry, composite, overlay): 3 credits\n" +
45
- " • Predictive ops (transits/search, returns): 5 credits\n" +
46
- " • Predictive transit-chart: 1 credit\n" +
47
- " • ACG / astrocartography: 10 credits (acg/hits: 15 credits)\n" +
48
- " • Calendar endpoints: 10 credits\n" +
49
- " • Catalog / metadata / health endpoints: 0 credits\n" +
50
- " • Compute surcharge: requests > 30s add 1 credit per 30s (predictive, acg, calendar, electional)\n" +
51
- " • format=llm (token-optimized output): available on all tiers\n\n" +
52
- "COMMON CALLS:\n" +
53
- " POST /ephemeris/natal-chart — Full natal chart (body: {subject: {name: 'Name', birth_datetime: {iso: '1990-04-15T14:30:00-05:00'}, birth_location: {latitude: {decimal: 40.0}, longitude: {decimal: -70.0}, timezone: {}}}})\n" +
54
- " POST /ephemeris/natal/batch — Up to 50 natal charts in one request\n" +
55
- " POST /ephemeris/relocation — Relocated chart (same natal, new location)\n" +
56
- " POST /predictive/transits/search — Transit event search over a date range\n" +
57
- " POST /predictive/returns/solar — Solar return chart\n" +
58
- " POST /predictive/returns/lunar — Lunar return chart\n" +
59
- " POST /comparative/synastry — Two-person synastry chart\n" +
60
- " POST /comparative/composite — Composite (midpoint) chart\n" +
61
- " POST /human-design/chart — Full HD chart (body: {birth_datetime_utc: '1990-04-15T19:30:00Z'}) — lat/lon optional\n" +
62
- " POST /time/julian-day — Convert date to JD (body: {year: 1987, month: 7, day: 15, hour: 14, minute: 1})\n" +
63
- " GET /ephemeris/moon/phase — Current/queried moon phase\n" +
64
- " GET /ephemeris/moon/void-of-course — Next void-of-course period\n" +
65
- " GET /ephemeris/agro/daily — Biodynamic farming day quality\n" +
66
- " GET /ephemeris/agro/calendar — Multi-day biodynamic calendar\n" +
67
- " GET /ephemeris/agro/void-of-course — Biodynamic VoC periods\n" +
68
- " GET /eclipse/next-visible — Next eclipse visible from a location (query: lat, lon, type=solar|lunar)\n" +
69
- " GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
70
- " GET /eclipse/solar/local — Local solar eclipse (query: lat, lon)\n" +
71
- " GET /tidal/forcing — Gravitational tidal forcing index\n" +
72
- " POST /acg/power-lines — Astrocartography power lines (lat/lon GeoJSON)\n" +
73
- " POST /acg/hits — ACG power at a specific location\n" +
74
- " GET /calendar/astrology/moon-phases — Moon phase calendar for a date range\n" +
75
- " GET /location/autocomplete — Geocode a place name (query: q=City Name)\n" +
76
- " POST /timezone/lookup — Resolve timezone + UTC offset for a location\n" +
77
- " POST /chinese/bazi — Chinese Ba Zi (Four Pillars) chart (body: {year, month, day, hour})\n" +
78
- " GET /chinese/zodiac — Chinese zodiac year element/animal\n" +
79
- " POST /vedic/chart — Vedic (Jyotish) natal chart (body: {datetime_utc, latitude, longitude})\n" +
80
- " GET /catalogs/bodies — List all supported celestial bodies\n\n" +
81
- "BINARY RESPONSES:\n" +
82
- " • Binary/image endpoints return {content_type, content_length, encoding, data_base64}\n" +
83
- " so callers can decode bytes deterministically.\n\n" +
84
- "ECLIPSE NOTE: Eclipse endpoints accept format=llm via the query param like other endpoints.\n\n" +
85
- "format=llm NOTE: Add query: {format: 'llm'} to natal/synastry/composite/HD endpoints for " +
86
- "compact columnar output optimized for LLM token budgets (availability depends on your current plan).",
87
- inputSchema: {
35
+ // Shared reference block appended to both read/write proxy tools. Names the
36
+ // target API (Open Ephemeris) explicitly — required by the directory for
37
+ // freeform-path tools plus credit costs and common calls.
38
+ const DEV_API_REFERENCE = "Target API: Open Ephemeris REST API (https://api.openephemeris.com). " +
39
+ "Call dev_list_allowed to see all currently available endpoint paths.\n\n" +
40
+ "AUTH: Set OPENEPHEMERIS_API_KEY in your environment. See openephemeris.com/dashboard for active plan limits.\n\n" +
41
+ "CREDIT COSTS:\n" +
42
+ " • Standard chart math (natal, progressed, bazi, vedic, iching): 1 credit\n" +
43
+ " • Human Design: 2 credits\n" +
44
+ " • Visualization rendering (chart-wheel, bi-wheel, charts/*): 2 credits\n" +
45
+ " • Comparative math (synastry, composite, overlay): 3 credits\n" +
46
+ " • Predictive ops (transits/search, returns): 5 credits\n" +
47
+ " • Predictive transit-chart: 1 credit\n" +
48
+ " • ACG / astrocartography: 10 credits (acg/hits: 15 credits)\n" +
49
+ " • Calendar endpoints: 10 credits\n" +
50
+ " • Catalog / metadata / health endpoints: 0 credits\n" +
51
+ " • Compute surcharge: requests > 30s add 1 credit per 30s (predictive, acg, calendar, electional)\n" +
52
+ " • format=llm (token-optimized output): available on all tiers\n\n" +
53
+ "COMMON CALLS:\n" +
54
+ " POST /ephemeris/natal-chart — Full natal chart (body: {subject: {name: 'Name', birth_datetime: {iso: '1990-04-15T14:30:00-05:00'}, birth_location: {latitude: {decimal: 40.0}, longitude: {decimal: -70.0}, timezone: {}}}})\n" +
55
+ " POST /ephemeris/natal/batch — Up to 50 natal charts in one request\n" +
56
+ " POST /ephemeris/relocation — Relocated chart (same natal, new location)\n" +
57
+ " POST /predictive/transits/search — Transit event search over a date range\n" +
58
+ " POST /predictive/returns/solar — Solar return chart\n" +
59
+ " POST /predictive/returns/lunar — Lunar return chart\n" +
60
+ " POST /comparative/synastry — Two-person synastry chart\n" +
61
+ " POST /comparative/composite — Composite (midpoint) chart\n" +
62
+ " POST /human-design/chart — Full HD chart (body: {birth_datetime_utc: '1990-04-15T19:30:00Z'}) — lat/lon optional\n" +
63
+ " POST /time/julian-day — Convert date to JD (body: {year: 1987, month: 7, day: 15, hour: 14, minute: 1})\n" +
64
+ " GET /ephemeris/moon/phase — Current/queried moon phase\n" +
65
+ " GET /ephemeris/moon/void-of-course — Next void-of-course period\n" +
66
+ " GET /ephemeris/agro/daily — Biodynamic farming day quality\n" +
67
+ " GET /ephemeris/agro/calendar — Multi-day biodynamic calendar\n" +
68
+ " GET /ephemeris/agro/void-of-course — Biodynamic VoC periods\n" +
69
+ " GET /eclipse/next-visible — Next eclipse visible from a location (query: lat, lon, type=solar|lunar)\n" +
70
+ " GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
71
+ " GET /eclipse/solar/local — Local solar eclipse (query: lat, lon)\n" +
72
+ " GET /tidal/forcing — Gravitational tidal forcing index\n" +
73
+ " POST /acg/power-lines — Astrocartography power lines (lat/lon GeoJSON)\n" +
74
+ " POST /acg/hits — ACG power at a specific location\n" +
75
+ " GET /calendar/astrology/moon-phases — Moon phase calendar for a date range\n" +
76
+ " GET /location/autocomplete — Geocode a place name (query: q=City Name)\n" +
77
+ " POST /timezone/lookup — Resolve timezone + UTC offset for a location\n" +
78
+ " POST /chinese/bazi — Chinese Ba Zi (Four Pillars) chart (body: {year, month, day, hour})\n" +
79
+ " GET /chinese/zodiac — Chinese zodiac year element/animal\n" +
80
+ " POST /vedic/chart — Vedic (Jyotish) natal chart (body: {datetime_utc, latitude, longitude})\n" +
81
+ " GET /catalogs/bodies — List all supported celestial bodies\n\n" +
82
+ "BINARY RESPONSES:\n" +
83
+ " • Binary/image endpoints return {content_type, content_length, encoding, data_base64}\n" +
84
+ " so callers can decode bytes deterministically.\n\n" +
85
+ "ECLIPSE NOTE: Eclipse endpoints accept format=llm via the query param like other endpoints.\n\n" +
86
+ "format=llm NOTE: Add query: {format: 'llm'} to natal/synastry/composite/HD endpoints for " +
87
+ "compact columnar output optimized for LLM token budgets (availability depends on your current plan).";
88
+ // The read and write proxies are kept as separate tools (not one method-switching
89
+ // tool) so that safe GET reads never share a surface with state-changing writes —
90
+ // a hard requirement of the Anthropic connector directory.
91
+ const READ_METHODS = ["GET"];
92
+ const WRITE_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
93
+ function makeProxyInputSchema(methods) {
94
+ return {
88
95
  type: "object",
89
96
  properties: {
90
97
  method: {
91
98
  type: "string",
92
- enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
93
- description: "HTTP method",
99
+ enum: methods,
100
+ description: methods.length === 1 ? `HTTP method (always ${methods[0]})` : "HTTP method",
94
101
  },
95
102
  path: {
96
103
  type: "string",
@@ -99,10 +106,10 @@ registerTool({
99
106
  query: {
100
107
  type: "object",
101
108
  additionalProperties: true,
102
- description: "Query params for GET requests (optional)",
109
+ description: "Query params (optional)",
103
110
  },
104
111
  body: {
105
- description: "JSON body for POST/PUT/PATCH (optional)",
112
+ description: "JSON request body (optional)",
106
113
  },
107
114
  preset: {
108
115
  type: "string",
@@ -120,14 +127,16 @@ registerTool({
120
127
  description: "Legacy convenience (deprecated): if provided, set query.output_mode and also map to query.preset/query.format when possible.",
121
128
  },
122
129
  },
123
- required: ["method", "path"],
130
+ required: ["path"],
124
131
  additionalProperties: false,
125
- },
126
- handler: async (args) => {
132
+ };
133
+ }
134
+ function makeProxyHandler(allowedMethods, defaultMethod) {
135
+ return async (args) => {
127
136
  validateRequired(args, ["path"]);
128
- const methodStr = String(args.method || "GET").toUpperCase();
129
- if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(methodStr)) {
130
- throw new Error(`Invalid HTTP method: ${methodStr}`);
137
+ const methodStr = String(args.method || defaultMethod).toUpperCase();
138
+ if (!allowedMethods.includes(methodStr)) {
139
+ throw new Error(`Method ${methodStr} not permitted by this tool. Allowed: ${allowedMethods.join(", ")}.`);
131
140
  }
132
141
  const method = methodStr;
133
142
  const pathname = String(args.path);
@@ -175,20 +184,48 @@ registerTool({
175
184
  params: query,
176
185
  data: body,
177
186
  });
178
- },
187
+ };
188
+ }
189
+ registerTool({
190
+ name: "dev_read_api",
191
+ description: "Read from any allowlisted Open Ephemeris API endpoint via HTTP GET. This is the read-only " +
192
+ "power-user escape hatch — use the typed tools (ephemeris_natal_chart, ephemeris_transits, etc.) " +
193
+ "first for common operations. For endpoints that compute via POST (natal-chart, synastry, etc.), " +
194
+ "use dev_write_api.\n\n" +
195
+ DEV_API_REFERENCE,
196
+ inputSchema: makeProxyInputSchema(READ_METHODS),
197
+ outputSchema: OUTPUT_SCHEMA_JSON,
198
+ // GET-only: never mutates server state.
199
+ annotations: { title: "API Read", readOnlyHint: true, openWorldHint: true },
200
+ handler: makeProxyHandler(READ_METHODS, "GET"),
201
+ });
202
+ registerTool({
203
+ name: "dev_write_api",
204
+ description: "Send a request to any allowlisted Open Ephemeris API endpoint via POST/PUT/PATCH/DELETE. " +
205
+ "Most chart computations (natal-chart, synastry, composite, transits/search, returns, ACG) are " +
206
+ "POST endpoints and use this tool. Use the typed tools first for common operations; use dev_read_api " +
207
+ "for GET endpoints.\n\n" +
208
+ DEV_API_REFERENCE,
209
+ inputSchema: makeProxyInputSchema(WRITE_METHODS),
210
+ outputSchema: OUTPUT_SCHEMA_JSON,
211
+ // Issues POST/PUT/PATCH/DELETE — must not be advertised as read-only.
212
+ annotations: { title: "API Write", readOnlyHint: false, openWorldHint: true },
213
+ handler: makeProxyHandler(WRITE_METHODS, "POST"),
179
214
  });
180
215
  registerTool({
181
216
  name: "dev_list_allowed",
182
217
  description: "List all API operations (method + path) that this MCP instance is authorized to call. " +
183
218
  "Returns endpoint entries grouped by method, plus the active deny rules. " +
184
- "Use this to discover what's available before calling dev_call, or to verify an endpoint path. " +
219
+ "Use this to discover what's available before calling dev_read_api / dev_write_api, or to verify an endpoint path. " +
185
220
  "Typed shortcut tools (ephemeris_natal_chart, ephemeris_transits, etc.) cover the most common operations — " +
186
- "check those first before reaching for dev_call.",
221
+ "check those first before reaching for the generic proxies.",
187
222
  inputSchema: {
188
223
  type: "object",
189
224
  properties: {},
190
225
  additionalProperties: false,
191
226
  },
227
+ outputSchema: OUTPUT_SCHEMA_JSON,
228
+ annotations: { title: "List Allowed API Operations", readOnlyHint: true, openWorldHint: false },
192
229
  handler: async () => {
193
230
  const allowlist = loadAllowlist();
194
231
  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;
@@ -46,6 +52,16 @@ export declare function initTools(profile?: ToolProfile): Promise<void>;
46
52
  * Throws an error if any of the required keys are missing or empty.
47
53
  */
48
54
  export declare function validateRequired(args: any, requiredKeys: string[]): void;
55
+ /**
56
+ * Returns `value` only if it is one of `allowed`, otherwise `undefined`.
57
+ *
58
+ * Use for any tool argument that gets interpolated into a backend URL/query
59
+ * string. Even though the inputSchema declares an `enum`, the MCP host does
60
+ * not guarantee enum enforcement, so a hostile client could smuggle extra
61
+ * query parameters (e.g. style="dark&admin=1"). Whitelisting here closes that
62
+ * injection vector at the boundary (DATA-6).
63
+ */
64
+ export declare function pickEnum<T extends string>(value: unknown, allowed: readonly T[]): T | undefined;
49
65
  /**
50
66
  * Throws an error if only one of the coordinate pair is provided.
51
67
  */
@@ -44,10 +44,14 @@ export async function initTools(profile) {
44
44
  const resolvedProfile = (profile || process.env.OPENEPHEMERIS_PROFILE || process.env.ASTROMCP_PROFILE || "dev").toLowerCase();
45
45
  // Always register auth tools (available in all profiles).
46
46
  await import("./auth.js");
47
- // Always register the generic proxy tools (dev.call + dev.list_allowed).
47
+ // Always register the generic proxy tools (dev_read_api + dev_write_api + 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
  /**
@@ -92,6 +93,20 @@ export function validateRequired(args, requiredKeys) {
92
93
  throw new Error(`Missing required arguments: ${missing.join(", ")}`);
93
94
  }
94
95
  }
96
+ /**
97
+ * Returns `value` only if it is one of `allowed`, otherwise `undefined`.
98
+ *
99
+ * Use for any tool argument that gets interpolated into a backend URL/query
100
+ * string. Even though the inputSchema declares an `enum`, the MCP host does
101
+ * not guarantee enum enforcement, so a hostile client could smuggle extra
102
+ * query parameters (e.g. style="dark&admin=1"). Whitelisting here closes that
103
+ * injection vector at the boundary (DATA-6).
104
+ */
105
+ export function pickEnum(value, allowed) {
106
+ return typeof value === "string" && allowed.includes(value)
107
+ ? value
108
+ : undefined;
109
+ }
95
110
  /**
96
111
  * Throws an error if only one of the coordinate pair is provided.
97
112
  */
@@ -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>;