@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.
- package/CHANGELOG.md +53 -0
- package/README.md +6 -6
- package/dist/index.js +5 -43
- package/dist/oauth/discovery.d.ts +1 -0
- package/dist/oauth/discovery.js +8 -3
- package/dist/oauth/store.d.ts +20 -0
- package/dist/oauth/store.js +117 -3
- package/dist/oauth/token.js +4 -2
- package/dist/prompts.js +25 -16
- package/dist/server-sse.js +136 -78
- package/dist/tools/apps/bazi-app.js +2 -0
- package/dist/tools/apps/bi-wheel-app.js +65 -7
- package/dist/tools/apps/bodygraph-app.js +85 -11
- package/dist/tools/apps/chart-wheel-app.js +80 -7
- package/dist/tools/apps/location-tools.js +41 -5
- package/dist/tools/apps/moon-phase-app.js +82 -22
- package/dist/tools/apps/transit-timeline-app.js +75 -23
- package/dist/tools/apps/vedic-chart-app.js +2 -0
- package/dist/tools/auth.js +4 -0
- package/dist/tools/dev.js +104 -67
- package/dist/tools/index.d.ts +16 -0
- package/dist/tools/index.js +23 -8
- package/dist/tools/output-schemas.d.ts +37 -0
- package/dist/tools/output-schemas.js +130 -0
- package/dist/tools/specialized/acg.js +3 -0
- package/dist/tools/specialized/bazi.js +14 -6
- package/dist/tools/specialized/bi_wheel.js +5 -2
- package/dist/tools/specialized/chart_wheel.js +5 -2
- package/dist/tools/specialized/comparative.js +7 -2
- package/dist/tools/specialized/eclipse.js +2 -0
- package/dist/tools/specialized/electional.js +5 -0
- package/dist/tools/specialized/ephemeris_core.js +3 -0
- package/dist/tools/specialized/ephemeris_extended.js +9 -0
- package/dist/tools/specialized/hd_bodygraph.js +6 -3
- package/dist/tools/specialized/hd_cycles.js +3 -0
- package/dist/tools/specialized/hd_group.js +3 -0
- package/dist/tools/specialized/human_design.js +2 -0
- package/dist/tools/specialized/moon.js +3 -0
- package/dist/tools/specialized/natal.js +2 -0
- package/dist/tools/specialized/progressed.js +2 -0
- package/dist/tools/specialized/relocation.js +2 -0
- package/dist/tools/specialized/returns.js +4 -0
- package/dist/tools/specialized/synastry.js +2 -0
- package/dist/tools/specialized/transits.js +52 -43
- package/dist/tools/specialized/vedic.js +2 -0
- package/dist/tools/specialized/venus_star_points.js +7 -0
- package/dist/ui/bazi.html +22 -22
- package/dist/ui/bodygraph.html +1981 -1904
- package/dist/ui/chart-wheel.html +3148 -2806
- package/dist/ui/vedic-chart.html +22 -22
- package/package.json +18 -11
- package/smithery.yaml +16 -1
- package/dist/ui/bi-wheel.html +0 -7485
- package/dist/ui/moon-phase.html +0 -6758
- package/dist/ui/transit-timeline.html +0 -6835
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,59 @@ 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
|
+
|
|
24
|
+
## [3.14.0] — 2026-05-24
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **Fullscreen bottom-bar layout** for bodygraph, bi-wheel, and chart-wheel iframes — chart fills the viewport, controls dock to the bottom for a cleaner read on small windows.
|
|
28
|
+
- **Debug highlight click handler** in the bodygraph (`?debugHighlight`) — click any gate to surface its center/channel data via iframe postMessage.
|
|
29
|
+
- **Highlight visualization** — highlighted gates render with CSS glow while non-highlighted gates dim, making activation patterns visually obvious.
|
|
30
|
+
- **Click payload metadata** — bodygraph gate groups now emit `data-center` for richer iframe click events.
|
|
31
|
+
- **OAuth refresh-token persistence** to Supabase, with in-memory expiry enforcement and a 90-day TTL (up from 30 days).
|
|
32
|
+
- **OAuth discovery test suite** with 401-compliance coverage.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- Auth check now runs before `isInitializeRequest` — resolves the Claude Web `ofid_` initialization error.
|
|
36
|
+
- Light-mode ring tokens wired correctly in bi-wheel (were missing token references).
|
|
37
|
+
- Fullscreen bottom-bar layout polish — edge cases and styling fixes after initial rollout.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- **Server-side SVG is now the only bodygraph render path** — the legacy client-side renderer has been retired. Reduces bundle size and removes a source of cross-environment rendering drift.
|
|
41
|
+
- Claude Web connector docs updated to reflect OAuth (not API key) as the connection method.
|
|
42
|
+
|
|
43
|
+
### Underlying API improvements (delivered transparently)
|
|
44
|
+
- **LLM-format projection now covers all 7 benchmarked endpoints** (was 4): `/ephemeris/natal-chart`, `/comparative/synastry`, `/comparative/natal-transits`, `/human-design/chart`, `/vedic/chart`, `/chinese/bazi`, `/acg/hits`. Weighted average reduction: 61% (GPT-4o), 60% (Claude). Range: 13%–98% per endpoint.
|
|
45
|
+
- `/acg/hits` newly supports `format=llm` — proximity hits as compact rows. Other `/acg/*` endpoints continue to return precision GeoJSON for map rendering.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## [3.13.11] — 2026-05-15
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- **Bi-Wheel UI Parity** — Unified the Bi-Wheel and Natal Chart Wheel architectures with matching feature sets.
|
|
53
|
+
- **Tally & Dignity Panels** — Integrated element/modality tallies and planetary dignity badges into the Bi-Wheel UI.
|
|
54
|
+
- **House System Controls** — Added dynamic house system selection to the Bi-Wheel aspect panel, triggering live recalculations.
|
|
55
|
+
- **Enhanced Rendering** — Exported dignity/tally logic from core renderer for consistent client-side UI generation.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
- Widened `BiWheelMode` type definitions to prevent TypeScript compilation errors during mode-specific panel rendering.
|
|
59
|
+
- Standardized CSS design tokens and layout containers between natal and comparative chart wheels.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
10
63
|
## [3.10.4] — 2026-04-24
|
|
11
64
|
|
|
12
65
|
### Changed
|
package/README.md
CHANGED
|
@@ -183,7 +183,7 @@ Cursor deeplink payload:
|
|
|
183
183
|
|
|
184
184
|
The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable HTTP support (MCP 2025-11-25 spec). Remote-only clients can connect directly — no bridge/proxy required:
|
|
185
185
|
|
|
186
|
-
- **Claude Web**: Add `https://mcp.openephemeris.com/mcp` as a
|
|
186
|
+
- **Claude Web**: Add `https://mcp.openephemeris.com/mcp` as a custom connector URL — leave OAuth Client ID and Secret **blank**. The server uses OAuth 2.1 + PKCE (Dynamic Client Registration), so Claude handles authentication via a browser popup automatically.
|
|
187
187
|
- **Via Smithery**: Use the [Smithery listing](https://smithery.ai/servers/open-ephemeris/openephemeris) for managed connections with any client
|
|
188
188
|
- **Legacy SSE**: `https://mcp.openephemeris.com/sse` remains available for SSE-only clients
|
|
189
189
|
|
|
@@ -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/index.js
CHANGED
|
@@ -7,12 +7,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
7
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
import { initTools, toolRegistry, formatToolResponse, modelVisibleTools } from "./tools/index.js";
|
|
9
9
|
import { listPrompts, getPromptContent } from "./prompts.js";
|
|
10
|
+
// ── MCP App resource imports ────────────────────────────────────────────────
|
|
10
11
|
import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
|
|
11
12
|
import { BODYGRAPH_RESOURCE_URI, BODYGRAPH_MIME_TYPE, getBodygraphBundle, } from "./tools/apps/bodygraph-app.js";
|
|
12
13
|
import { BI_WHEEL_RESOURCE_URI, BI_WHEEL_MIME_TYPE, getBiWheelBundle, } from "./tools/apps/bi-wheel-app.js";
|
|
13
|
-
import { TRANSIT_TIMELINE_RESOURCE_URI, TRANSIT_TIMELINE_MIME_TYPE, getTransitTimelineBundle, } from "./tools/apps/transit-timeline-app.js";
|
|
14
14
|
import { MOON_PHASE_RESOURCE_URI, MOON_PHASE_MIME_TYPE, getMoonPhaseBundle, } from "./tools/apps/moon-phase-app.js";
|
|
15
|
-
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
16
|
function resolveServerVersion() {
|
|
17
17
|
try {
|
|
18
18
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -42,9 +42,6 @@ const server = new Server({
|
|
|
42
42
|
tools: {},
|
|
43
43
|
prompts: {},
|
|
44
44
|
resources: {},
|
|
45
|
-
experimental: {
|
|
46
|
-
"io.modelcontextprotocol/ui": {},
|
|
47
|
-
},
|
|
48
45
|
},
|
|
49
46
|
});
|
|
50
47
|
// List available tools
|
|
@@ -62,10 +59,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
62
59
|
openWorldHint: tool.annotations?.openWorldHint ?? false,
|
|
63
60
|
},
|
|
64
61
|
// Expose MCP Apps UI linkage so Claude Desktop can prefetch the resource
|
|
65
|
-
// and render the iframe when a tool result arrives.
|
|
66
|
-
// Include BOTH the modern nested key (_meta.ui.resourceUri) and the
|
|
67
|
-
// legacy flat key (_meta["ui/resourceUri"]) — the official ext-apps SDK
|
|
68
|
-
// normalizes both via registerAppTool() for host compatibility.
|
|
69
62
|
...(tool._meta?.ui?.resourceUri
|
|
70
63
|
? {
|
|
71
64
|
_meta: {
|
|
@@ -127,14 +120,6 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
|
127
120
|
mimeType: BI_WHEEL_MIME_TYPE,
|
|
128
121
|
});
|
|
129
122
|
}
|
|
130
|
-
if (getTransitTimelineBundle()) {
|
|
131
|
-
resources.push({
|
|
132
|
-
uri: TRANSIT_TIMELINE_RESOURCE_URI,
|
|
133
|
-
name: "Transit Timeline Explorer",
|
|
134
|
-
description: "Interactive Gantt-style transit timeline showing planetary aspects over time.",
|
|
135
|
-
mimeType: TRANSIT_TIMELINE_MIME_TYPE,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
123
|
if (getMoonPhaseBundle()) {
|
|
139
124
|
resources.push({
|
|
140
125
|
uri: MOON_PHASE_RESOURCE_URI,
|
|
@@ -143,14 +128,6 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
|
143
128
|
mimeType: MOON_PHASE_MIME_TYPE,
|
|
144
129
|
});
|
|
145
130
|
}
|
|
146
|
-
if (getBaziBundle()) {
|
|
147
|
-
resources.push({
|
|
148
|
-
uri: BAZI_RESOURCE_URI,
|
|
149
|
-
name: "BaZi Four Pillars Explorer",
|
|
150
|
-
description: "Interactive BaZi chart with pillars, Ten Gods, hidden stems, and Wu Xing balance.",
|
|
151
|
-
mimeType: BAZI_MIME_TYPE,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
131
|
return { resources };
|
|
155
132
|
});
|
|
156
133
|
// Read resource
|
|
@@ -174,24 +151,12 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
174
151
|
throw new Error("Bi-Wheel UI bundle not found. Run `npm run build:ui` to build it.");
|
|
175
152
|
return { contents: [{ uri: BI_WHEEL_RESOURCE_URI, mimeType: BI_WHEEL_MIME_TYPE, text: bundle }] };
|
|
176
153
|
}
|
|
177
|
-
if (uri === TRANSIT_TIMELINE_RESOURCE_URI) {
|
|
178
|
-
const bundle = getTransitTimelineBundle();
|
|
179
|
-
if (!bundle)
|
|
180
|
-
throw new Error("Transit Timeline UI bundle not found. Run `npm run build:ui` to build it.");
|
|
181
|
-
return { contents: [{ uri: TRANSIT_TIMELINE_RESOURCE_URI, mimeType: TRANSIT_TIMELINE_MIME_TYPE, text: bundle }] };
|
|
182
|
-
}
|
|
183
154
|
if (uri === MOON_PHASE_RESOURCE_URI) {
|
|
184
155
|
const bundle = getMoonPhaseBundle();
|
|
185
156
|
if (!bundle)
|
|
186
157
|
throw new Error("Moon Phase UI bundle not found. Run `npm run build:ui` to build it.");
|
|
187
158
|
return { contents: [{ uri: MOON_PHASE_RESOURCE_URI, mimeType: MOON_PHASE_MIME_TYPE, text: bundle }] };
|
|
188
159
|
}
|
|
189
|
-
if (uri === BAZI_RESOURCE_URI) {
|
|
190
|
-
const bundle = getBaziBundle();
|
|
191
|
-
if (!bundle)
|
|
192
|
-
throw new Error("BaZi UI bundle not found. Run `npm run build:ui` to build it.");
|
|
193
|
-
return { contents: [{ uri: BAZI_RESOURCE_URI, mimeType: BAZI_MIME_TYPE, text: bundle }] };
|
|
194
|
-
}
|
|
195
160
|
throw new Error(`Unknown resource: ${uri}`);
|
|
196
161
|
});
|
|
197
162
|
// Handle tool calls
|
|
@@ -211,19 +176,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
211
176
|
catch (error) {
|
|
212
177
|
const durationMs = Date.now() - startTime;
|
|
213
178
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
179
|
+
const isRetryable = error.retryable === true || error.status === 401 || error.code === "auth_required";
|
|
214
180
|
console.error(`[MCP] ❌ Failed: ${toolName} (${durationMs}ms) - ${errorMessage}`);
|
|
215
181
|
return {
|
|
216
182
|
content: [
|
|
217
183
|
{
|
|
218
184
|
type: "text",
|
|
219
|
-
text:
|
|
220
|
-
success: false,
|
|
221
|
-
error: "TOOL_EXECUTION_ERROR",
|
|
222
|
-
message: errorMessage,
|
|
223
|
-
}, null, 2),
|
|
185
|
+
text: `Tool execution failed:\n\n${errorMessage}\n\nPlease read the error above carefully and assist the user (e.g. by providing them the auth link or explaining the issue).`,
|
|
224
186
|
},
|
|
225
187
|
],
|
|
226
|
-
isError:
|
|
188
|
+
isError: !isRetryable,
|
|
227
189
|
};
|
|
228
190
|
}
|
|
229
191
|
});
|
|
@@ -8,4 +8,5 @@
|
|
|
8
8
|
* All URLs use https:// per the OAuth 2.1 specification.
|
|
9
9
|
*/
|
|
10
10
|
import { type Router } from "express";
|
|
11
|
+
export declare const PROTECTED_RESOURCE_METADATA_URL = "https://mcp.openephemeris.com/.well-known/oauth-protected-resource";
|
|
11
12
|
export declare const oauthDiscoveryRouter: Router;
|
package/dist/oauth/discovery.js
CHANGED
|
@@ -10,18 +10,23 @@
|
|
|
10
10
|
import express from "express";
|
|
11
11
|
const MCP_SERVER_ORIGIN = "https://mcp.openephemeris.com";
|
|
12
12
|
const WEB_APP_ORIGIN = "https://openephemeris.com";
|
|
13
|
+
export const PROTECTED_RESOURCE_METADATA_URL = `${MCP_SERVER_ORIGIN}/.well-known/oauth-protected-resource`;
|
|
13
14
|
export const oauthDiscoveryRouter = express.Router();
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// GET /.well-known/oauth-protected-resource
|
|
17
|
+
// Also served at the path-scoped /.well-known/oauth-protected-resource/mcp
|
|
18
|
+
// per the latest MCP Authorization spec draft (some clients probe per-resource).
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
20
|
+
const protectedResourceHandler = (_req, res) => {
|
|
18
21
|
res.json({
|
|
19
|
-
resource: MCP_SERVER_ORIGIN
|
|
22
|
+
resource: `${MCP_SERVER_ORIGIN}/mcp`,
|
|
20
23
|
authorization_servers: [MCP_SERVER_ORIGIN],
|
|
21
24
|
bearer_methods_supported: ["header"],
|
|
22
25
|
scopes_supported: ["openephemeris:read"],
|
|
23
26
|
});
|
|
24
|
-
}
|
|
27
|
+
};
|
|
28
|
+
oauthDiscoveryRouter.get("/.well-known/oauth-protected-resource", protectedResourceHandler);
|
|
29
|
+
oauthDiscoveryRouter.get("/.well-known/oauth-protected-resource/mcp", protectedResourceHandler);
|
|
25
30
|
// ---------------------------------------------------------------------------
|
|
26
31
|
// GET /.well-known/oauth-authorization-server
|
|
27
32
|
// ---------------------------------------------------------------------------
|
package/dist/oauth/store.d.ts
CHANGED
|
@@ -59,8 +59,28 @@ export declare class OAuthStore {
|
|
|
59
59
|
used: boolean;
|
|
60
60
|
expires_at: Date;
|
|
61
61
|
}) | null>;
|
|
62
|
+
/**
|
|
63
|
+
* Refresh-token TTL — 90 days, sliding (reset on each rotation).
|
|
64
|
+
* Matches GitHub's 6-month convention more closely than Supabase's 30d
|
|
65
|
+
* default. Tradeoff: longer-lived refresh tokens = bigger blast radius
|
|
66
|
+
* on leak; rotation + Supabase RLS mitigates this. An active user who
|
|
67
|
+
* touches OE at least once per 90d gets an effectively permanent
|
|
68
|
+
* connection.
|
|
69
|
+
*/
|
|
70
|
+
private static REFRESH_TTL_MS;
|
|
62
71
|
storeRefreshToken(token: string, meta: object): void;
|
|
72
|
+
/**
|
|
73
|
+
* Synchronous in-memory consume. Kept for backward compat with any callers
|
|
74
|
+
* that don't need cross-instance resilience. Token endpoint must use
|
|
75
|
+
* consumeRefreshTokenAsync.
|
|
76
|
+
*/
|
|
63
77
|
consumeRefreshToken(token: string): object | null;
|
|
78
|
+
/**
|
|
79
|
+
* Async consume — checks in-memory first, then Supabase. This is what the
|
|
80
|
+
* token endpoint uses so refresh requests survive machine restarts and
|
|
81
|
+
* load-balancing across multiple Fly instances.
|
|
82
|
+
*/
|
|
83
|
+
consumeRefreshTokenAsync(token: string): Promise<object | null>;
|
|
64
84
|
generateRefreshToken(): string;
|
|
65
85
|
reset(): void;
|
|
66
86
|
private purgeExpired;
|
package/dist/oauth/store.js
CHANGED
|
@@ -85,7 +85,7 @@ function persistCodeToSupabase(code, options, expiresAt) {
|
|
|
85
85
|
redirect_uri: options.redirect_uri,
|
|
86
86
|
code_challenge: options.code_challenge,
|
|
87
87
|
code_challenge_method: options.code_challenge_method,
|
|
88
|
-
scopes: ["
|
|
88
|
+
scopes: ["openephemeris:read"],
|
|
89
89
|
consumed: false,
|
|
90
90
|
expires_at: expiresAt.toISOString(),
|
|
91
91
|
})
|
|
@@ -104,6 +104,36 @@ function persistCodeToSupabase(code, options, expiresAt) {
|
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
// ---------------------------------------------------------------------------
|
|
107
|
+
// Refresh-token persistence (mirrors persistCodeToSupabase pattern)
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
function persistRefreshTokenToSupabase(token, meta, expiresAt) {
|
|
110
|
+
const cfg = getSupabaseConfig();
|
|
111
|
+
if (!cfg)
|
|
112
|
+
return; // No-op in tests
|
|
113
|
+
const m = meta;
|
|
114
|
+
postgrestRequest("POST", "/rest/v1/oauth_refresh_tokens", {
|
|
115
|
+
token,
|
|
116
|
+
client_id: m.client_id ?? "unknown",
|
|
117
|
+
user_id: m.user_id ?? null,
|
|
118
|
+
supabase_refresh_token: m.supabase_refresh_token ?? null,
|
|
119
|
+
consumed: false,
|
|
120
|
+
expires_at: expiresAt.toISOString(),
|
|
121
|
+
})
|
|
122
|
+
.then(({ status, data }) => {
|
|
123
|
+
if (status < 200 || status >= 300) {
|
|
124
|
+
console.warn(`[store] Supabase refresh-token persist failed: ${status}`, data);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
128
|
+
const id = rows[0]?.id ?? "(unknown)";
|
|
129
|
+
console.info(`[store] refresh token persisted to Supabase id=${id}`);
|
|
130
|
+
})
|
|
131
|
+
.catch((err) => {
|
|
132
|
+
// Non-fatal — in-memory store still serves same-instance refreshes
|
|
133
|
+
console.warn("[store] Supabase refresh-token persist error (non-fatal):", err.message);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
107
137
|
// OAuthStore
|
|
108
138
|
// ---------------------------------------------------------------------------
|
|
109
139
|
export class OAuthStore {
|
|
@@ -206,18 +236,102 @@ export class OAuthStore {
|
|
|
206
236
|
}
|
|
207
237
|
}
|
|
208
238
|
// ------------------------------------------------------------------
|
|
209
|
-
// Refresh tokens
|
|
239
|
+
// Refresh tokens
|
|
240
|
+
//
|
|
241
|
+
// In-memory map is the hot path. When Supabase is configured, we ALSO
|
|
242
|
+
// persist to oauth_refresh_tokens so a refresh request that lands on a
|
|
243
|
+
// different Fly machine (oe-mcp-live has min_machines_running=2) or
|
|
244
|
+
// arrives after a machine restart can still be honored. Without this,
|
|
245
|
+
// ~50% of refresh attempts fail with invalid_grant just from
|
|
246
|
+
// multi-instance load balancing alone.
|
|
210
247
|
// ------------------------------------------------------------------
|
|
248
|
+
/**
|
|
249
|
+
* Refresh-token TTL — 90 days, sliding (reset on each rotation).
|
|
250
|
+
* Matches GitHub's 6-month convention more closely than Supabase's 30d
|
|
251
|
+
* default. Tradeoff: longer-lived refresh tokens = bigger blast radius
|
|
252
|
+
* on leak; rotation + Supabase RLS mitigates this. An active user who
|
|
253
|
+
* touches OE at least once per 90d gets an effectively permanent
|
|
254
|
+
* connection.
|
|
255
|
+
*/
|
|
256
|
+
static REFRESH_TTL_MS = 90 * 24 * 60 * 60 * 1000;
|
|
211
257
|
storeRefreshToken(token, meta) {
|
|
212
|
-
|
|
258
|
+
const expires_at = new Date(Date.now() + OAuthStore.REFRESH_TTL_MS);
|
|
259
|
+
this.refreshTokens.set(token, { ...meta, consumed: false, expires_at });
|
|
260
|
+
// Async durability write — non-blocking
|
|
261
|
+
persistRefreshTokenToSupabase(token, meta, expires_at);
|
|
213
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Synchronous in-memory consume. Kept for backward compat with any callers
|
|
265
|
+
* that don't need cross-instance resilience. Token endpoint must use
|
|
266
|
+
* consumeRefreshTokenAsync.
|
|
267
|
+
*/
|
|
214
268
|
consumeRefreshToken(token) {
|
|
215
269
|
const stored = this.refreshTokens.get(token);
|
|
216
270
|
if (!stored || stored.consumed)
|
|
217
271
|
return null;
|
|
272
|
+
if (stored.expires_at < new Date()) {
|
|
273
|
+
// In-memory entry outlived its TTL. Drop it; the Supabase row (if any)
|
|
274
|
+
// is also past its expires_at and will be rejected by the WHERE filter
|
|
275
|
+
// in consumeRefreshTokenAsync's slow path.
|
|
276
|
+
this.refreshTokens.delete(token);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
218
279
|
stored.consumed = true;
|
|
219
280
|
return { ...stored };
|
|
220
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Async consume — checks in-memory first, then Supabase. This is what the
|
|
284
|
+
* token endpoint uses so refresh requests survive machine restarts and
|
|
285
|
+
* load-balancing across multiple Fly instances.
|
|
286
|
+
*/
|
|
287
|
+
async consumeRefreshTokenAsync(token) {
|
|
288
|
+
// Fast path: in-memory hit
|
|
289
|
+
const inMem = this.consumeRefreshToken(token);
|
|
290
|
+
if (inMem)
|
|
291
|
+
return inMem;
|
|
292
|
+
// Slow path: Supabase lookup with atomic mark-consumed
|
|
293
|
+
const cfg = getSupabaseConfig();
|
|
294
|
+
if (!cfg)
|
|
295
|
+
return null;
|
|
296
|
+
try {
|
|
297
|
+
const selectPath = `/rest/v1/oauth_refresh_tokens` +
|
|
298
|
+
`?token=eq.${encodeURIComponent(token)}` +
|
|
299
|
+
`&consumed=eq.false` +
|
|
300
|
+
`&expires_at=gt.${encodeURIComponent(new Date().toISOString())}` +
|
|
301
|
+
`&limit=1`;
|
|
302
|
+
const { status: selStatus, data: selData } = await postgrestRequest("GET", selectPath);
|
|
303
|
+
if (selStatus < 200 || selStatus >= 300)
|
|
304
|
+
return null;
|
|
305
|
+
const rows = Array.isArray(selData) ? selData : [];
|
|
306
|
+
if (rows.length === 0)
|
|
307
|
+
return null;
|
|
308
|
+
const row = rows[0];
|
|
309
|
+
// Optimistic lock: mark consumed=true WHERE consumed=false
|
|
310
|
+
const updatePath = `/rest/v1/oauth_refresh_tokens?id=eq.${row.id}&consumed=eq.false`;
|
|
311
|
+
const { status: updStatus } = await postgrestRequest("PATCH", updatePath, {
|
|
312
|
+
consumed: true,
|
|
313
|
+
});
|
|
314
|
+
if (updStatus < 200 || updStatus >= 300) {
|
|
315
|
+
// Lost the race — another instance consumed it. Treat as invalid.
|
|
316
|
+
console.warn(`[store] Supabase refresh-token consume race lost id=${row.id} status=${updStatus}`);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
console.info(`[store] refresh token consumed from Supabase id=${row.id}`);
|
|
320
|
+
const result = {
|
|
321
|
+
consumed: true,
|
|
322
|
+
client_id: row.client_id,
|
|
323
|
+
};
|
|
324
|
+
if (row.user_id)
|
|
325
|
+
result.user_id = row.user_id;
|
|
326
|
+
if (row.supabase_refresh_token)
|
|
327
|
+
result.supabase_refresh_token = row.supabase_refresh_token;
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
console.warn("[store] Supabase refresh-token consume error:", err.message);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
221
335
|
generateRefreshToken() {
|
|
222
336
|
return randomBytes(32).toString("hex");
|
|
223
337
|
}
|
package/dist/oauth/token.js
CHANGED
|
@@ -150,8 +150,10 @@ async function handleRefreshToken(req, res, store) {
|
|
|
150
150
|
});
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
|
-
// Consume from
|
|
154
|
-
|
|
153
|
+
// Consume from store (in-memory first, then Supabase). Async so refresh
|
|
154
|
+
// requests survive Fly machine restarts and load-balancing across the
|
|
155
|
+
// two oe-mcp-live instances.
|
|
156
|
+
const storedMeta = (await store.consumeRefreshTokenAsync(refresh_token));
|
|
155
157
|
if (!storedMeta) {
|
|
156
158
|
res.status(400).json({
|
|
157
159
|
error: "invalid_grant",
|
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" +
|
|
@@ -511,6 +511,9 @@ export const PROMPTS = [
|
|
|
511
511
|
"- Cautions: [anything to be aware of]\n\n" +
|
|
512
512
|
"Close with a clear **★ Top Recommendation** with a one-paragraph explanation of why this " +
|
|
513
513
|
"window is the strongest pick.\n\n" +
|
|
514
|
+
"**Applying Moon aspects are critical**: The Moon's applying aspects (not yet exact) describe what " +
|
|
515
|
+
"unfolds after the event begins. A Moon applying to trine Jupiter is excellent; a Moon applying to " +
|
|
516
|
+
"square Saturn is a warning. Always prioritize applying aspects over separating ones.\n\n" +
|
|
514
517
|
"### If No Good Windows Exist\n" +
|
|
515
518
|
"Be direct: 'There are no high-quality windows in your date range because [specific reason]. " +
|
|
516
519
|
"I'd suggest expanding to [suggested range] where [better conditions] occur.' " +
|
|
@@ -542,9 +545,9 @@ export const PROMPTS = [
|
|
|
542
545
|
" Default to Lahiri if unsure.\n\n" +
|
|
543
546
|
"## Step 2 — Convert to UTC (MANDATORY)\n" +
|
|
544
547
|
"The `vedic_chart` tool requires a UTC datetime (with Z suffix):\n\n" +
|
|
545
|
-
"**A) Geocode** — call `
|
|
548
|
+
"**A) Geocode** — call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
546
549
|
"`query: { q: 'Birth City, Country' }`.\n\n" +
|
|
547
|
-
"**B) Timezone** — call `
|
|
550
|
+
"**B) Timezone** — call `dev_write_api` with `path: '/timezone/lookup'`, " +
|
|
548
551
|
"`body: { latitude: ..., longitude: ..., datetime: 'YYYY-MM-DDTHH:MM:SS' }`.\n" +
|
|
549
552
|
"This returns the correct IANA timezone and UTC offset for the birth year, including historical DST.\n\n" +
|
|
550
553
|
"- Use the returned offset to convert local birth time → UTC\n" +
|
|
@@ -563,6 +566,9 @@ export const PROMPTS = [
|
|
|
563
566
|
"- **Whole Sign Bhavas**: the Lagna's sign = the entire 1st Bhava. No intercepted signs.\n" +
|
|
564
567
|
"- **Nakshatras**: 27 lunar mansions of ~13°20' each — the heart of timing and soul-level analysis\n" +
|
|
565
568
|
"- **The Moon is king**: in Jyotish the Moon holds more weight than the Sun for disposition and timing\n" +
|
|
569
|
+
"- **Vedic aspects (drishti)** differ from Western aspects: they are whole-sign (not degree-orb), and " +
|
|
570
|
+
"Mars, Jupiter, and Saturn have special additional aspects (Mars: 4th/8th; Jupiter: 5th/9th; Saturn: 3rd/10th). " +
|
|
571
|
+
"Use the aspect data as returned by the API \u2014 do not apply Western orb-based aspect rules.\n" +
|
|
566
572
|
"- **Guna of chart**: Sattvic (clear, spiritual), Rajasic (active, worldly), or Tamasic (inert)?\n\n" +
|
|
567
573
|
"## Step 5 — Structure the Reading\n\n" +
|
|
568
574
|
"### The Lagna (Ascendant)\n" +
|
|
@@ -613,6 +619,9 @@ export const PROMPTS = [
|
|
|
613
619
|
" | 🐉 Dragon | 07:00–09:00 | 🐕 Dog | 19:00–21:00 |\n" +
|
|
614
620
|
" | 🐍 Snake | 09:00–11:00 | 🐖 Pig | 21:00–23:00 |\n" +
|
|
615
621
|
" If the hour is unknown, note it — the Hour Pillar will be missing.\n" +
|
|
622
|
+
" If the birth time is approximate (e.g. 'around noon'), note that the Hour Pillar may be wrong — " +
|
|
623
|
+
"each shí block is only 2 hours wide. Offer to calculate for the adjacent pillar as a comparison " +
|
|
624
|
+
"so the user can see which resonates more.\n" +
|
|
616
625
|
"3. **Birth city** — BaZi uses LOCAL solar time, not clock time. City is needed to detect " +
|
|
617
626
|
"any significant longitude offset from the timezone meridian.\n\n" +
|
|
618
627
|
"## Step 2 — Local Solar Time\n" +
|
|
@@ -620,7 +629,7 @@ export const PROMPTS = [
|
|
|
620
629
|
"this difference is small, but it can shift the hour animal by one block for cities far " +
|
|
621
630
|
"from their timezone meridian (e.g. western Xinjiang in China UTC+8, western Spain in CET).\n" +
|
|
622
631
|
"Pass individual date/time components — do NOT convert to UTC.\n\n" +
|
|
623
|
-
"**Geocode the birth city** — call `
|
|
632
|
+
"**Geocode the birth city** — call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
624
633
|
"`query: { q: 'Birth City, Country' }` to get verified coordinates for longitude offset calculation.\n\n" +
|
|
625
634
|
"## Step 3 — Call the Tool\n" +
|
|
626
635
|
"Call `chinese_bazi` with:\n" +
|
|
@@ -761,9 +770,9 @@ export const PROMPTS = [
|
|
|
761
770
|
"4. **House system**: Placidus (default), Whole Sign, Koch, Equal?\n\n" +
|
|
762
771
|
"## Step 2 — Resolve Data (MANDATORY)\n" +
|
|
763
772
|
"**Do NOT guess coordinates or timezones.** Use the API tools:\n\n" +
|
|
764
|
-
"**A) Geocode birth city** — call `
|
|
773
|
+
"**A) Geocode birth city** — call `dev_read_api` with `path: '/location/autocomplete'`, " +
|
|
765
774
|
"`query: { q: 'Birth City, Country' }`.\n\n" +
|
|
766
|
-
"**B) Timezone** — call `
|
|
775
|
+
"**B) Timezone** — call `dev_write_api` with `path: '/timezone/lookup'`, " +
|
|
767
776
|
"`body: { latitude: ..., longitude: ..., datetime: 'YYYY-MM-DDTHH:MM:SS' }`.\n\n" +
|
|
768
777
|
"- For `birth_datetime`, include the UTC offset from the timezone lookup: `1985-06-21T14:00:00-05:00`\n" +
|
|
769
778
|
" (or convert to UTC with Z: `1985-06-21T19:00:00Z`)\n\n" +
|