@openephemeris/mcp-server 3.14.0 → 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.
- package/CHANGELOG.md +14 -0
- package/README.md +5 -5
- package/dist/prompts.js +16 -16
- package/dist/server-sse.js +65 -14
- package/dist/tools/apps/bi-wheel-app.js +12 -3
- package/dist/tools/apps/bodygraph-app.js +20 -4
- package/dist/tools/apps/chart-wheel-app.js +29 -5
- package/dist/tools/apps/location-tools.js +38 -5
- package/dist/tools/apps/moon-phase-app.js +7 -1
- package/dist/tools/dev.js +102 -68
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +15 -1
- package/dist/tools/specialized/bazi.js +6 -6
- package/dist/tools/specialized/bi_wheel.js +3 -2
- package/dist/tools/specialized/chart_wheel.js +3 -2
- package/dist/tools/specialized/comparative.js +2 -2
- package/dist/tools/specialized/hd_bodygraph.js +4 -3
- package/dist/ui/bazi.html +21 -21
- package/dist/ui/bodygraph.html +1645 -1615
- package/dist/ui/chart-wheel.html +1966 -1912
- package/dist/ui/vedic-chart.html +21 -21
- package/package.json +5 -1
- package/dist/ui/bi-wheel.html +0 -7878
- package/dist/ui/moon-phase.html +0 -6764
- package/dist/ui/transit-timeline.html +0 -6874
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [3.15.0] — 2026-06-15
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **`dev_call` split into `dev_read_api` (GET) and `dev_write_api` (POST/PUT/PATCH/DELETE).** Read and write now live in separate tools so a safe read surface never shares a tool with state-changing calls — required for MCP connector-directory compliance. `dev_read_api` carries `readOnlyHint: true`; `dev_write_api` carries `readOnlyHint: false`. Both name their target API explicitly.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Brand-gold WCAG AA contrast pass** across the chart-wheel, bi-wheel, and bodygraph iframes — gold accents darkened to clear AA against their backgrounds; all three apps are axe-clean.
|
|
17
|
+
- **Moon-phase color cue** now encodes speed/proximity, and aspect-label contrast corrected.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **Collapsible glyph legend** in the interactive chart iframes — keeps the wheel uncluttered while remaining one tap from a full symbol key.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
10
24
|
## [3.14.0] — 2026-05-24
|
|
11
25
|
|
|
12
26
|
### Added
|
package/README.md
CHANGED
|
@@ -251,15 +251,15 @@ The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable
|
|
|
251
251
|
## Tooling Model
|
|
252
252
|
|
|
253
253
|
- Typed tools are preferred for common workflows (natal, transits, moon phase, eclipse, synastry, relocation, electional, Human Design).
|
|
254
|
-
- Generic tools: `
|
|
254
|
+
- Generic tools: `dev_list_allowed` returns all currently allowlisted operations; `dev_read_api` invokes allowlisted **GET** (read) operations and `dev_write_api` invokes allowlisted **POST/PUT/PATCH/DELETE** (write/compute) operations, each by `method + path`. Read and write are kept as separate tools so a safe read surface never shares a tool with state-changing writes.
|
|
255
255
|
- Security model: default-deny with explicit allowlist in `config/dev-allowlist.json`.
|
|
256
256
|
- Deny prefixes block sensitive route families (`/auth`, `/billing`, `/admin`, etc.).
|
|
257
257
|
|
|
258
|
-
### `
|
|
258
|
+
### `dev_read_api` / `dev_write_api` input
|
|
259
259
|
|
|
260
260
|
| Parameter | Type | Required | Description |
|
|
261
261
|
|---|---|---|---|
|
|
262
|
-
| `method` | `GET
|
|
262
|
+
| `method` | `dev_read_api`: `GET` · `dev_write_api`: `POST\|PUT\|PATCH\|DELETE` | No | HTTP method (defaults to the tool's natural method) |
|
|
263
263
|
| `path` | `string` | Yes | Absolute API path, e.g. `/ephemeris/natal-chart` |
|
|
264
264
|
| `query` | `object` | No | Query parameters |
|
|
265
265
|
| `body` | `object` | No | JSON body for non-GET requests |
|
|
@@ -355,8 +355,8 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
355
355
|
|
|
356
356
|
- Allowlisted operations: **28**
|
|
357
357
|
- Methods: `GET=4`, `POST=24`, `PUT=0`, `PATCH=0`, `DELETE=0`
|
|
358
|
-
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **
|
|
359
|
-
- Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bazi_annual_pillar`, `bazi_chart`, `bazi_compatibility`, `bazi_element_balance`, `bazi_luck_pillars`, `bazi_ten_gods`, `bi_wheel_on_cross_aspect_click`, `bi_wheel_on_house_click`, `bi_wheel_on_planet_click`, `bi_wheel_recalculate`, `bi_wheel_synopsis`, `bodygraph_recalculate`, `chart_wheel_on_aspect_click`, `chart_wheel_on_house_click`, `chart_wheel_on_planet_click`, `chart_wheel_recalculate`, `chinese_bazi`, `
|
|
358
|
+
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **80**
|
|
359
|
+
- Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bazi_annual_pillar`, `bazi_chart`, `bazi_compatibility`, `bazi_element_balance`, `bazi_luck_pillars`, `bazi_ten_gods`, `bi_wheel_on_cross_aspect_click`, `bi_wheel_on_house_click`, `bi_wheel_on_planet_click`, `bi_wheel_recalculate`, `bi_wheel_synopsis`, `bodygraph_recalculate`, `chart_wheel_on_aspect_click`, `chart_wheel_on_house_click`, `chart_wheel_on_planet_click`, `chart_wheel_recalculate`, `chinese_bazi`, `dev_list_allowed`, `dev_read_api`, `dev_write_api`, `electional_aspect_search`, `electional_moment_analysis`, `electional_station_tracker`, `ephemeris_angles_points`, `ephemeris_aspect_check`, `ephemeris_bi_wheel`, `ephemeris_chart_wheel`, `ephemeris_composite`, `ephemeris_composite_midpoint`, `ephemeris_dignities`, `ephemeris_electional`, `ephemeris_fixed_stars`, `ephemeris_hermetic_lots`, `ephemeris_house_cusps`, `ephemeris_lunar_return`, `ephemeris_midpoints`, `ephemeris_moon_phase`, `ephemeris_natal_batch`, `ephemeris_natal_chart`, `ephemeris_natal_transits`, `ephemeris_next_eclipse`, `ephemeris_next_lunar_phase`, `ephemeris_overlay`, `ephemeris_planet_position`, `ephemeris_planetary_return`, `ephemeris_progressed_chart`, `ephemeris_relocation`, `ephemeris_retrograde_status`, `ephemeris_solar_return`, `ephemeris_synastry`, `ephemeris_transits`, `explore_bi_wheel`, `explore_human_design`, `explore_moon_phase`, `explore_natal_chart`, `hd_on_center_click`, `hd_on_channel_click`, `hd_on_gate_click`, `hd_on_planet_click`, `hd_on_variable_click`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `location_search`, `timezone_resolve`, `vedic_chart`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
|
|
360
360
|
- Generic tools:
|
|
361
361
|
|
|
362
362
|
### Allowlist Families
|
package/dist/prompts.js
CHANGED
|
@@ -68,9 +68,9 @@ export const PROMPTS = [
|
|
|
68
68
|
" Default to Placidus if they're unsure.\n\n" +
|
|
69
69
|
"## Step 2 — Resolve Coordinates and Timezone (MANDATORY)\n" +
|
|
70
70
|
"**Do NOT guess coordinates or timezones from memory.** Use the API tools to resolve them:\n\n" +
|
|
71
|
-
"**A) Geocode** — call `
|
|
71
|
+
"**A) Geocode** — call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
72
72
|
"`query: { q: 'City Name, Country' }`. This returns verified decimal lat/lon.\n\n" +
|
|
73
|
-
"**B) Timezone** — call `
|
|
73
|
+
"**B) Timezone** — call `dev_write_api` with `path: '/timezone/lookup'`, " +
|
|
74
74
|
"`body: { latitude: ..., longitude: ..., datetime: 'YYYY-MM-DDTHH:MM:SS' }`. " +
|
|
75
75
|
"This returns the correct IANA timezone and UTC offset for the birth year, including historical DST.\n\n" +
|
|
76
76
|
"- Format: pass `datetime` as local ISO 8601 (e.g. `1990-04-15T14:30:00`) " +
|
|
@@ -148,9 +148,9 @@ export const PROMPTS = [
|
|
|
148
148
|
"4. **Any major events coming up** they already know about — reframes the transits around real context\n\n" +
|
|
149
149
|
"## Step 2 — Resolve Coordinates and Timezone (MANDATORY)\n" +
|
|
150
150
|
"**Do NOT guess coordinates or timezones from memory.** Use the API tools:\n\n" +
|
|
151
|
-
"**A) Geocode** — call `
|
|
151
|
+
"**A) Geocode** — call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
152
152
|
"`query: { q: 'Birth City, Country' }`. Use the returned decimal lat/lon.\n\n" +
|
|
153
|
-
"**B) Timezone** — call `
|
|
153
|
+
"**B) Timezone** — call `dev_write_api` with `path: '/timezone/lookup'`, " +
|
|
154
154
|
"`body: { latitude: ..., longitude: ..., datetime: 'YYYY-MM-DDTHH:MM:SS' }`.\n\n" +
|
|
155
155
|
"- Set `start_date` to today and `end_date` to the end of the forecast window\n\n" +
|
|
156
156
|
"## Step 3 — Tool Call Sequence\n" +
|
|
@@ -211,9 +211,9 @@ export const PROMPTS = [
|
|
|
211
211
|
"7. **House system**: Placidus (default), Whole Sign, Koch, Equal?\n\n" +
|
|
212
212
|
"## Step 2 — Resolve Both Sets of Coordinates (MANDATORY)\n" +
|
|
213
213
|
"**Do NOT guess coordinates or timezones.** For EACH person:\n\n" +
|
|
214
|
-
"**A) Geocode** — call `
|
|
214
|
+
"**A) Geocode** — call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
215
215
|
"`query: { q: 'Birth City, Country' }`.\n\n" +
|
|
216
|
-
"**B) Timezone** — call `
|
|
216
|
+
"**B) Timezone** — call `dev_write_api` with `path: '/timezone/lookup'`, " +
|
|
217
217
|
"`body: { latitude: ..., longitude: ..., datetime: 'YYYY-MM-DDTHH:MM:SS' }`.\n\n" +
|
|
218
218
|
"- Format: local ISO 8601 datetime (no Z) + IANA timezone name separately for each person\n" +
|
|
219
219
|
"- Note if either birth time is unknown — flag this upfront before running tools\n\n" +
|
|
@@ -283,9 +283,9 @@ export const PROMPTS = [
|
|
|
283
283
|
"sometimes it explains why a place felt 'off'\n\n" +
|
|
284
284
|
"## Step 2 — Resolve Coordinates (MANDATORY)\n" +
|
|
285
285
|
"**Do NOT guess coordinates or timezones.** Use the API tools:\n\n" +
|
|
286
|
-
"**A) Geocode birth city** — call `
|
|
286
|
+
"**A) Geocode birth city** — call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
287
287
|
"`query: { q: 'Birth City, Country' }`.\n\n" +
|
|
288
|
-
"**B) Timezone** — call `
|
|
288
|
+
"**B) Timezone** — call `dev_write_api` with `path: '/timezone/lookup'`, " +
|
|
289
289
|
"`body: { latitude: ..., longitude: ..., datetime: 'YYYY-MM-DDTHH:MM:SS' }`.\n\n" +
|
|
290
290
|
"**C) Geocode each target city** — repeat the autocomplete call for each destination city.\n\n" +
|
|
291
291
|
"- Format birth datetime as local ISO 8601 (no Z), pass IANA timezone separately\n\n" +
|
|
@@ -372,9 +372,9 @@ export const PROMPTS = [
|
|
|
372
372
|
"4. **Birth city and country** — needed for timezone conversion\n\n" +
|
|
373
373
|
"## Step 2 — CRITICAL: Convert to UTC\n" +
|
|
374
374
|
"Human Design requires **UTC datetime**. The tool rejects local times without a UTC offset.\n\n" +
|
|
375
|
-
"**A) Geocode** — call `
|
|
375
|
+
"**A) Geocode** — call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
376
376
|
"`query: { q: 'Birth City, Country' }`.\n\n" +
|
|
377
|
-
"**B) Timezone** — call `
|
|
377
|
+
"**B) Timezone** — call `dev_write_api` with `path: '/timezone/lookup'`, " +
|
|
378
378
|
"`body: { latitude: ..., longitude: ..., datetime: 'YYYY-MM-DDTHH:MM:SS' }`.\n" +
|
|
379
379
|
"This returns the UTC offset for the birth year, including historical DST.\n\n" +
|
|
380
380
|
"- Use the returned offset to convert local birth time → UTC\n" +
|
|
@@ -458,7 +458,7 @@ export const PROMPTS = [
|
|
|
458
458
|
"5. **Any personal chart data?** If yes, run natal chart first and look for dates when the " +
|
|
459
459
|
"moon triggers benefic natal planets.\n\n" +
|
|
460
460
|
"## Step 1.5 — Resolve Event Location (MANDATORY)\n" +
|
|
461
|
-
"**Do NOT guess coordinates.** Call `
|
|
461
|
+
"**Do NOT guess coordinates.** Call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
462
462
|
"`query: { q: 'Event City, Country' }` to get verified decimal lat/lon for the event location.\n\n" +
|
|
463
463
|
"## Step 2 — Check the Planetary Weather First\n" +
|
|
464
464
|
"Before scanning for windows:\n\n" +
|
|
@@ -545,9 +545,9 @@ export const PROMPTS = [
|
|
|
545
545
|
" Default to Lahiri if unsure.\n\n" +
|
|
546
546
|
"## Step 2 — Convert to UTC (MANDATORY)\n" +
|
|
547
547
|
"The `vedic_chart` tool requires a UTC datetime (with Z suffix):\n\n" +
|
|
548
|
-
"**A) Geocode** — call `
|
|
548
|
+
"**A) Geocode** — call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
549
549
|
"`query: { q: 'Birth City, Country' }`.\n\n" +
|
|
550
|
-
"**B) Timezone** — call `
|
|
550
|
+
"**B) Timezone** — call `dev_write_api` with `path: '/timezone/lookup'`, " +
|
|
551
551
|
"`body: { latitude: ..., longitude: ..., datetime: 'YYYY-MM-DDTHH:MM:SS' }`.\n" +
|
|
552
552
|
"This returns the correct IANA timezone and UTC offset for the birth year, including historical DST.\n\n" +
|
|
553
553
|
"- Use the returned offset to convert local birth time → UTC\n" +
|
|
@@ -629,7 +629,7 @@ export const PROMPTS = [
|
|
|
629
629
|
"this difference is small, but it can shift the hour animal by one block for cities far " +
|
|
630
630
|
"from their timezone meridian (e.g. western Xinjiang in China UTC+8, western Spain in CET).\n" +
|
|
631
631
|
"Pass individual date/time components — do NOT convert to UTC.\n\n" +
|
|
632
|
-
"**Geocode the birth city** — call `
|
|
632
|
+
"**Geocode the birth city** — call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
633
633
|
"`query: { q: 'Birth City, Country' }` to get verified coordinates for longitude offset calculation.\n\n" +
|
|
634
634
|
"## Step 3 — Call the Tool\n" +
|
|
635
635
|
"Call `chinese_bazi` with:\n" +
|
|
@@ -770,9 +770,9 @@ export const PROMPTS = [
|
|
|
770
770
|
"4. **House system**: Placidus (default), Whole Sign, Koch, Equal?\n\n" +
|
|
771
771
|
"## Step 2 — Resolve Data (MANDATORY)\n" +
|
|
772
772
|
"**Do NOT guess coordinates or timezones.** Use the API tools:\n\n" +
|
|
773
|
-
"**A) Geocode birth city** — call `
|
|
773
|
+
"**A) Geocode birth city** — call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
774
774
|
"`query: { q: 'Birth City, Country' }`.\n\n" +
|
|
775
|
-
"**B) Timezone** — call `
|
|
775
|
+
"**B) Timezone** — call `dev_write_api` with `path: '/timezone/lookup'`, " +
|
|
776
776
|
"`body: { latitude: ..., longitude: ..., datetime: 'YYYY-MM-DDTHH:MM:SS' }`.\n\n" +
|
|
777
777
|
"- For `birth_datetime`, include the UTC offset from the timezone lookup: `1985-06-21T14:00:00-05:00`\n" +
|
|
778
778
|
" (or convert to UTC with Z: `1985-06-21T19:00:00Z`)\n\n" +
|
package/dist/server-sse.js
CHANGED
|
@@ -66,8 +66,10 @@ const BACKEND_URL = process.env.OPENEPHEMERIS_BACKEND_URL ||
|
|
|
66
66
|
"https://api.openephemeris.com";
|
|
67
67
|
const version = resolveServerVersion();
|
|
68
68
|
/**
|
|
69
|
-
* Validate a user-supplied API key by hitting an auth-gated endpoint on
|
|
70
|
-
*
|
|
69
|
+
* Validate a user-supplied API key by hitting an auth-gated endpoint on the Go
|
|
70
|
+
* sidecar. Tri-state so callers can fail closed when validation is unavailable
|
|
71
|
+
* (APP-6): "valid" (2xx), "invalid" (401/403), or "unknown" (network error or
|
|
72
|
+
* non-auth status — could not confirm).
|
|
71
73
|
*/
|
|
72
74
|
async function validateApiKey(apiKey) {
|
|
73
75
|
try {
|
|
@@ -77,17 +79,26 @@ async function validateApiKey(apiKey) {
|
|
|
77
79
|
timeout: 5_000,
|
|
78
80
|
validateStatus: () => true, // don't throw on any status
|
|
79
81
|
});
|
|
80
|
-
// 2xx = valid key, 401/403 = invalid key, anything else = let through
|
|
81
82
|
if (resp.status === 401 || resp.status === 403)
|
|
82
|
-
return
|
|
83
|
-
|
|
83
|
+
return "invalid";
|
|
84
|
+
if (resp.status >= 200 && resp.status < 300)
|
|
85
|
+
return "valid";
|
|
86
|
+
return "unknown"; // 5xx etc. — can't confirm
|
|
84
87
|
}
|
|
85
88
|
catch {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return true;
|
|
89
|
+
console.warn("Could not validate API key against backend.");
|
|
90
|
+
return "unknown";
|
|
89
91
|
}
|
|
90
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Browser-originated requests carry an Origin header (the MCP apps run in a
|
|
95
|
+
* cross-origin iframe). Server-to-server MCP clients generally do not. We use
|
|
96
|
+
* this to fail closed for browsers when key validation is unavailable, while
|
|
97
|
+
* still letting trusted server clients through (APP-6).
|
|
98
|
+
*/
|
|
99
|
+
function isBrowserOrigin(req) {
|
|
100
|
+
return typeof req.headers.origin === "string" && req.headers.origin.length > 0;
|
|
101
|
+
}
|
|
91
102
|
/**
|
|
92
103
|
* Extract authentication from a request, distinguishing JWTs from API keys.
|
|
93
104
|
*
|
|
@@ -212,8 +223,12 @@ function createMcpServer() {
|
|
|
212
223
|
prompts: {},
|
|
213
224
|
resources: {},
|
|
214
225
|
experimental: {
|
|
215
|
-
// Declare MCP Apps extension (spec: io.modelcontextprotocol/ui, 2026-01-26)
|
|
216
|
-
|
|
226
|
+
// Declare MCP Apps extension (spec: io.modelcontextprotocol/ui, 2026-01-26).
|
|
227
|
+
// Advertise the supported UI MIME type so Directory-compliant hosts know
|
|
228
|
+
// this server returns renderable HTML resources.
|
|
229
|
+
"io.modelcontextprotocol/ui": {
|
|
230
|
+
mimeTypes: ["text/html;profile=mcp-app"],
|
|
231
|
+
},
|
|
217
232
|
},
|
|
218
233
|
},
|
|
219
234
|
instructions: "Open Ephemeris is a precision astronomical computation engine. " +
|
|
@@ -421,6 +436,24 @@ async function main() {
|
|
|
421
436
|
app.use(express.json({ limit: "1mb" }), oauthDcrRouter);
|
|
422
437
|
app.use("/oauth/token", tokenRateLimiter);
|
|
423
438
|
app.use(express.urlencoded({ extended: false }), oauthTokenRouter(oauthStore));
|
|
439
|
+
// Per-IP throttle on MCP session creation to bound unauthenticated session
|
|
440
|
+
// churn / volumetric abuse (APP-7). Only NEW sessions are limited — requests
|
|
441
|
+
// carrying an mcp-session-id are existing sessions (ongoing tool calls) and
|
|
442
|
+
// must never be throttled.
|
|
443
|
+
const sessionRateLimiter = createRateLimiter({
|
|
444
|
+
windowMs: 60_000,
|
|
445
|
+
maxRequests: 30,
|
|
446
|
+
trustProxy: true,
|
|
447
|
+
});
|
|
448
|
+
const throttleSessionCreation = (req, res, next) => {
|
|
449
|
+
if (req.headers["mcp-session-id"]) {
|
|
450
|
+
next();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
sessionRateLimiter(req, res, next);
|
|
454
|
+
};
|
|
455
|
+
app.use("/mcp", throttleSessionCreation);
|
|
456
|
+
app.use("/sse", sessionRateLimiter);
|
|
424
457
|
// Health check
|
|
425
458
|
app.get("/health", (_req, res) => {
|
|
426
459
|
res.json({
|
|
@@ -508,8 +541,8 @@ async function main() {
|
|
|
508
541
|
// Validate API keys against the Go backend; JWTs are validated downstream
|
|
509
542
|
// by the Go sidecar's ValidateSupabaseJWT when tool calls are proxied.
|
|
510
543
|
if (apiKey) {
|
|
511
|
-
const
|
|
512
|
-
if (
|
|
544
|
+
const keyStatus = await validateApiKey(apiKey);
|
|
545
|
+
if (keyStatus === "invalid") {
|
|
513
546
|
res.set("WWW-Authenticate", `Bearer realm="OpenEphemeris MCP", error="invalid_token", resource_metadata="${PROTECTED_RESOURCE_METADATA_URL}"`);
|
|
514
547
|
res.status(401).json({
|
|
515
548
|
error: "invalid_api_key",
|
|
@@ -517,6 +550,15 @@ async function main() {
|
|
|
517
550
|
});
|
|
518
551
|
return;
|
|
519
552
|
}
|
|
553
|
+
// Fail closed for browser clients when validation is unavailable (APP-6).
|
|
554
|
+
if (keyStatus === "unknown" && isBrowserOrigin(req)) {
|
|
555
|
+
res.set("Retry-After", "5");
|
|
556
|
+
res.status(503).json({
|
|
557
|
+
error: "validation_unavailable",
|
|
558
|
+
message: "Could not verify your API key right now. Please retry shortly.",
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
520
562
|
}
|
|
521
563
|
// New session — must be an initialize request
|
|
522
564
|
if (!isInitializeRequest(req.body)) {
|
|
@@ -624,14 +666,23 @@ async function main() {
|
|
|
624
666
|
}
|
|
625
667
|
// Validate API keys against the Go backend; JWTs validated downstream.
|
|
626
668
|
if (apiKey) {
|
|
627
|
-
const
|
|
628
|
-
if (
|
|
669
|
+
const keyStatus = await validateApiKey(apiKey);
|
|
670
|
+
if (keyStatus === "invalid") {
|
|
629
671
|
res.status(403).json({
|
|
630
672
|
error: "invalid_api_key",
|
|
631
673
|
message: "The provided API key is invalid or inactive. Check your key at https://openephemeris.com/dashboard?tab=apikeys",
|
|
632
674
|
});
|
|
633
675
|
return;
|
|
634
676
|
}
|
|
677
|
+
// Fail closed for browser clients when validation is unavailable (APP-6).
|
|
678
|
+
if (keyStatus === "unknown" && isBrowserOrigin(req)) {
|
|
679
|
+
res.set("Retry-After", "5");
|
|
680
|
+
res.status(503).json({
|
|
681
|
+
error: "validation_unavailable",
|
|
682
|
+
message: "Could not verify your API key right now. Please retry shortly.",
|
|
683
|
+
});
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
635
686
|
}
|
|
636
687
|
const transport = new SSEServerTransport("/message", res);
|
|
637
688
|
const sessionId = transport.sessionId;
|
|
@@ -288,8 +288,12 @@ const CLASSICAL_PLANETS = new Set([
|
|
|
288
288
|
*/
|
|
289
289
|
function canonicalizePlanetName(raw) {
|
|
290
290
|
const lower = raw.toLowerCase();
|
|
291
|
-
// Various node/chiron aliases the Go PlanetName() function emits
|
|
292
|
-
|
|
291
|
+
// Various node/chiron aliases the Go PlanetName() function emits.
|
|
292
|
+
// Keep mean vs true DISTINCT so the UI can offer a node toggle; collapsing
|
|
293
|
+
// both to "north_node" (the old behaviour) produced two overlapping ☊ glyphs.
|
|
294
|
+
if (lower === "north node (true)" || lower === "true node")
|
|
295
|
+
return "true_node";
|
|
296
|
+
if (lower === "north node (mean)" || lower === "mean node" || lower === "north node")
|
|
293
297
|
return "north_node";
|
|
294
298
|
if (lower === "south node (mean)" || lower === "south node (true)")
|
|
295
299
|
return "south_node";
|
|
@@ -491,11 +495,16 @@ registerTool({
|
|
|
491
495
|
const payload = buildBiWheelPayload(innerData, outerData, mode, params1, params2);
|
|
492
496
|
const bundleAvailable = Boolean(getBiWheelBundle());
|
|
493
497
|
if (bundleAvailable) {
|
|
498
|
+
// MCP Apps wire format: the UI is declared via `_meta.ui.resourceUri` and
|
|
499
|
+
// delivered through resources/read — NOT as a content block. A URI-only
|
|
500
|
+
// `type: "resource"` block lacks the inline text/blob the base MCP schema
|
|
501
|
+
// requires and fails strict client validation. structuredContent
|
|
502
|
+
// preserves the full payload for hosts that don't render the iframe.
|
|
494
503
|
return {
|
|
495
504
|
content: [
|
|
496
505
|
{ type: "text", text: summary },
|
|
497
|
-
{ type: "text", text: JSON.stringify({ ...payload, server_version: SERVER_VERSION }) },
|
|
498
506
|
],
|
|
507
|
+
structuredContent: { ...payload, server_version: SERVER_VERSION },
|
|
499
508
|
_meta: {
|
|
500
509
|
"ui/resourceUri": BI_WHEEL_RESOURCE_URI,
|
|
501
510
|
ui: { resourceUri: BI_WHEEL_RESOURCE_URI },
|
|
@@ -78,13 +78,18 @@ function ensureTimezone(dt) {
|
|
|
78
78
|
* renderer in bodygraph-renderer.ts. Non-fatal: a missing SVG should never
|
|
79
79
|
* block the tool result.
|
|
80
80
|
*/
|
|
81
|
-
async function fetchGoBodygraphSVG(body) {
|
|
81
|
+
async function fetchGoBodygraphSVG(body, theme = "dark") {
|
|
82
82
|
try {
|
|
83
83
|
const client = getActiveClient();
|
|
84
84
|
// /visualization/bodygraph is registered as binary in BINARY_ENDPOINT_PREFIXES.
|
|
85
85
|
// The client wraps it as { content_type, encoding: "base64", data_base64 } —
|
|
86
86
|
// we decode back to UTF-8 SVG text for inlining.
|
|
87
|
-
|
|
87
|
+
//
|
|
88
|
+
// style=light|dark drives the Go renderer's palette. The endpoint defaults
|
|
89
|
+
// to "light" when style is omitted, which mismatches dark MCP hosts — so we
|
|
90
|
+
// always pass an explicit theme (default "dark" to match the app shell; the
|
|
91
|
+
// iframe re-fetches with the host-detected theme on load / theme change).
|
|
92
|
+
const resp = await client.post(`/visualization/bodygraph?format=svg&size=800&style=${theme}`, body);
|
|
88
93
|
if (typeof resp === "string")
|
|
89
94
|
return resp;
|
|
90
95
|
if (resp && typeof resp === "object") {
|
|
@@ -438,11 +443,16 @@ registerTool({
|
|
|
438
443
|
modelPayload._svg = svg;
|
|
439
444
|
const summary = buildHdSummary(modelPayload, location);
|
|
440
445
|
if (bundleAvailable) {
|
|
446
|
+
// MCP Apps wire format: the UI is declared via `_meta.ui.resourceUri` and
|
|
447
|
+
// delivered through resources/read — NOT as a content block. A URI-only
|
|
448
|
+
// `type: "resource"` block lacks the inline text/blob the base MCP schema
|
|
449
|
+
// requires and fails strict client validation. structuredContent carries
|
|
450
|
+
// the data for non-rendering hosts.
|
|
441
451
|
return {
|
|
442
452
|
content: [
|
|
443
453
|
{ type: "text", text: summary },
|
|
444
|
-
{ type: "text", text: JSON.stringify({ ...modelPayload, server_version: SERVER_VERSION }) },
|
|
445
454
|
],
|
|
455
|
+
structuredContent: { ...modelPayload, server_version: SERVER_VERSION },
|
|
446
456
|
_meta: {
|
|
447
457
|
"ui/resourceUri": BODYGRAPH_RESOURCE_URI,
|
|
448
458
|
ui: { resourceUri: BODYGRAPH_RESOURCE_URI },
|
|
@@ -468,6 +478,11 @@ registerTool({
|
|
|
468
478
|
type: "string",
|
|
469
479
|
description: "IANA timezone name (e.g. 'America/New_York') for accurate UTC conversion.",
|
|
470
480
|
},
|
|
481
|
+
theme: {
|
|
482
|
+
type: "string",
|
|
483
|
+
enum: ["light", "dark"],
|
|
484
|
+
description: "Render palette for the bodygraph SVG. Mirrors the MCP host's light/dark color scheme.",
|
|
485
|
+
},
|
|
471
486
|
},
|
|
472
487
|
required: ["datetime"],
|
|
473
488
|
},
|
|
@@ -481,6 +496,7 @@ registerTool({
|
|
|
481
496
|
const datetime = localToUtcIso(String(args.datetime), timezone);
|
|
482
497
|
const lat = args.latitude;
|
|
483
498
|
const lon = args.longitude;
|
|
499
|
+
const theme = args.theme === "light" ? "light" : "dark";
|
|
484
500
|
const body = {
|
|
485
501
|
birth_datetime_utc: datetime,
|
|
486
502
|
};
|
|
@@ -493,7 +509,7 @@ registerTool({
|
|
|
493
509
|
client.request("POST", "/human-design/chart", {
|
|
494
510
|
data: body,
|
|
495
511
|
}),
|
|
496
|
-
fetchGoBodygraphSVG(body),
|
|
512
|
+
fetchGoBodygraphSVG(body, theme),
|
|
497
513
|
]);
|
|
498
514
|
const chart = chartData.chart ?? chartData;
|
|
499
515
|
const modelPayload = buildHdModelPayload(chart, {
|
|
@@ -199,11 +199,17 @@ registerTool({
|
|
|
199
199
|
}, houseSystem, undefined, requestedBodies);
|
|
200
200
|
const bundleAvailable = Boolean(getChartWheelBundle());
|
|
201
201
|
if (bundleAvailable) {
|
|
202
|
+
// MCP Apps wire format: the UI is declared via `_meta.ui.resourceUri` and
|
|
203
|
+
// delivered through resources/read — NOT as a content block. A URI-only
|
|
204
|
+
// `type: "resource"` block lacks the inline text/blob the base MCP schema
|
|
205
|
+
// requires and fails strict client validation. Keep `structuredContent`
|
|
206
|
+
// so non-rendering hosts still get the data, and a text summary so chat
|
|
207
|
+
// transcripts read coherently.
|
|
202
208
|
return {
|
|
203
209
|
content: [
|
|
204
210
|
{ type: "text", text: summary },
|
|
205
|
-
{ type: "text", text: JSON.stringify({ ...modelPayload, server_version: SERVER_VERSION }) },
|
|
206
211
|
],
|
|
212
|
+
structuredContent: { ...modelPayload, server_version: SERVER_VERSION },
|
|
207
213
|
_meta: {
|
|
208
214
|
"ui/resourceUri": CHART_WHEEL_RESOURCE_URI,
|
|
209
215
|
ui: { resourceUri: CHART_WHEEL_RESOURCE_URI },
|
|
@@ -414,8 +420,26 @@ function zodSignFromLon(lon) {
|
|
|
414
420
|
const CLASSICAL_PLANETS = new Set([
|
|
415
421
|
"sun", "moon", "mercury", "venus", "mars",
|
|
416
422
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
417
|
-
|
|
423
|
+
// Nodes use space form to match the renderer's glyph table (chart-renderer.ts).
|
|
424
|
+
"chiron", "north node", "south node", "true node", "asc", "mc",
|
|
418
425
|
]);
|
|
426
|
+
/**
|
|
427
|
+
* Canonicalise the Go backend's verbose node keys ("North Node (Mean)" /
|
|
428
|
+
* "North Node (True)") to the space-form names the renderer expects, keeping
|
|
429
|
+
* mean vs true DISTINCT. Without this the natal wheel dropped the nodes entirely
|
|
430
|
+
* (the raw key never matched the classical-planet set). All other body names are
|
|
431
|
+
* returned lowercased and otherwise unchanged.
|
|
432
|
+
*/
|
|
433
|
+
function canonicalizeBodyName(raw) {
|
|
434
|
+
const lower = raw.toLowerCase().trim();
|
|
435
|
+
if (lower === "north node (true)" || lower === "true node")
|
|
436
|
+
return "true node";
|
|
437
|
+
if (lower === "north node (mean)" || lower === "mean node" || lower === "north node")
|
|
438
|
+
return "north node";
|
|
439
|
+
if (lower === "south node (mean)" || lower === "south node (true)" || lower === "south node")
|
|
440
|
+
return "south node";
|
|
441
|
+
return lower;
|
|
442
|
+
}
|
|
419
443
|
const EXTENDED_BODIES = new Set([
|
|
420
444
|
"mean_lilith", "lilith", "true_lilith",
|
|
421
445
|
"ceres", "juno", "vesta", "pallas", "pholus",
|
|
@@ -487,19 +511,19 @@ function buildModelPayload(chartData, birthParams, houseSystem, svgBase, bodyFil
|
|
|
487
511
|
if (rawPlanets && typeof rawPlanets === "object" && !Array.isArray(rawPlanets)) {
|
|
488
512
|
planetsArray = Object.entries(rawPlanets)
|
|
489
513
|
.filter(([k]) => {
|
|
490
|
-
const key = k
|
|
514
|
+
const key = canonicalizeBodyName(k);
|
|
491
515
|
// If specific bodies were requested, use that filter
|
|
492
516
|
if (bodyFilter) {
|
|
493
517
|
const wantsAll = bodyFilter.some(b => b.toLowerCase() === "all");
|
|
494
518
|
if (wantsAll)
|
|
495
519
|
return ALL_KNOWN_BODIES.has(key);
|
|
496
|
-
return bodyFilter.some(b => b
|
|
520
|
+
return bodyFilter.some(b => canonicalizeBodyName(b) === key);
|
|
497
521
|
}
|
|
498
522
|
// Default: classical planets only
|
|
499
523
|
return CLASSICAL_PLANETS.has(key);
|
|
500
524
|
})
|
|
501
525
|
.map(([name, p]) => ({
|
|
502
|
-
name: name
|
|
526
|
+
name: canonicalizeBodyName(name),
|
|
503
527
|
longitude: (p.longitude ?? p.lon ?? 0),
|
|
504
528
|
latitude: (p.latitude ?? p.lat),
|
|
505
529
|
speed: (p.longitude_speed ?? p.speed),
|
|
@@ -3,13 +3,29 @@ import { getActiveClient } from "../../backend/client.js";
|
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
4
|
registerTool({
|
|
5
5
|
name: "location_search",
|
|
6
|
-
description: "App-only tool for getting location autocomplete suggestions. Returns latitude, longitude, and IANA timezone.",
|
|
6
|
+
description: "App-only tool for getting location autocomplete suggestions. Returns display name, region (state/province), latitude, longitude, and IANA timezone. Optional bias params (country/region/near) improve ranking; a trailing \"City, ST\" qualifier in the query is also honored.",
|
|
7
7
|
inputSchema: {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
10
10
|
query: {
|
|
11
11
|
type: "string",
|
|
12
|
-
description: "Search query for location, e.g. 'Los Angeles'",
|
|
12
|
+
description: "Search query for location, e.g. 'Los Angeles' or 'Wakefield, MI'",
|
|
13
|
+
},
|
|
14
|
+
country: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Optional ISO 3166-1 alpha-2 country code (e.g. 'US') to bias ranking. Boost, not filter.",
|
|
17
|
+
},
|
|
18
|
+
region: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Optional admin1 (state/province) qualifier to bias ranking — full name ('Michigan'), ASCII name, or code ('MI'). Boost, not filter.",
|
|
21
|
+
},
|
|
22
|
+
admin1: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Alias for region (region wins if both supplied).",
|
|
25
|
+
},
|
|
26
|
+
near: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Optional 'lat,lon' proximity hint (e.g. '37.77,-122.42') to bias ranking toward nearby places.",
|
|
13
29
|
},
|
|
14
30
|
},
|
|
15
31
|
required: ["query"],
|
|
@@ -23,9 +39,26 @@ registerTool({
|
|
|
23
39
|
},
|
|
24
40
|
handler: async (args) => {
|
|
25
41
|
validateRequired(args, ["query"]);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
// NB: the endpoint param is `query` (only /geocode/search reads `q`).
|
|
43
|
+
const params = { query: args.query };
|
|
44
|
+
for (const k of ["country", "region", "admin1", "near"]) {
|
|
45
|
+
if (args[k] != null && String(args[k]).trim() !== "")
|
|
46
|
+
params[k] = String(args[k]);
|
|
47
|
+
}
|
|
48
|
+
const raw = (await getActiveClient().request("GET", "/location/autocomplete", { params }));
|
|
49
|
+
// Map the API's snake_case response to the camelCase shape the app UI consumes.
|
|
50
|
+
const list = Array.isArray(raw?.suggestions) ? raw.suggestions : [];
|
|
51
|
+
const suggestions = list.map((s) => ({
|
|
52
|
+
displayName: s.display_name,
|
|
53
|
+
shortName: s.short_name,
|
|
54
|
+
region: s.region,
|
|
55
|
+
countryCode: s.country_code,
|
|
56
|
+
placeId: s.place_id,
|
|
57
|
+
latitude: s.latitude,
|
|
58
|
+
longitude: s.longitude,
|
|
59
|
+
timezone: s.timezone,
|
|
60
|
+
}));
|
|
61
|
+
return { suggestions };
|
|
29
62
|
},
|
|
30
63
|
});
|
|
31
64
|
registerTool({
|
|
@@ -227,11 +227,17 @@ registerTool({
|
|
|
227
227
|
const summary = buildMoonSummary(mergedData);
|
|
228
228
|
const bundleAvailable = Boolean(getMoonPhaseBundle());
|
|
229
229
|
if (bundleAvailable) {
|
|
230
|
+
// MCP Apps wire format: the UI is declared via `_meta.ui.resourceUri` and
|
|
231
|
+
// delivered through resources/read — NOT as a content block. An embedded
|
|
232
|
+
// `type: "resource"` block must carry inline text/blob per the base MCP
|
|
233
|
+
// schema, so a URI-only reference fails strict client validation; the
|
|
234
|
+
// _meta keys below are what the host uses to render the iframe.
|
|
235
|
+
// structuredContent carries the data payload for non-rendering hosts.
|
|
230
236
|
return {
|
|
231
237
|
content: [
|
|
232
238
|
{ type: "text", text: summary },
|
|
233
|
-
{ type: "text", text: JSON.stringify(uiPayload) },
|
|
234
239
|
],
|
|
240
|
+
structuredContent: uiPayload,
|
|
235
241
|
_meta: {
|
|
236
242
|
"ui/resourceUri": MOON_PHASE_RESOURCE_URI,
|
|
237
243
|
ui: { resourceUri: MOON_PHASE_RESOURCE_URI },
|