@openephemeris/mcp-server 4.16.0 → 4.18.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 +46 -0
- package/README.md +15 -15
- package/config/dev-allowlist.json +31 -19
- package/dist/backend/client.d.ts +57 -1
- package/dist/backend/client.js +125 -15
- package/dist/prompts.js +26 -24
- package/dist/server-sse.d.ts +18 -0
- package/dist/server-sse.js +41 -6
- package/dist/tools/apps/_render-token.d.ts +34 -0
- package/dist/tools/apps/_render-token.js +50 -0
- package/dist/tools/apps/bazi-app.js +36 -12
- package/dist/tools/apps/bi-wheel-app.d.ts +9 -2
- package/dist/tools/apps/bi-wheel-app.js +76 -117
- package/dist/tools/apps/bodygraph-app.js +88 -41
- package/dist/tools/apps/chart-wheel-app.js +56 -16
- package/dist/tools/apps/location-tools.js +10 -3
- package/dist/tools/apps/moon-phase-app.js +10 -2
- package/dist/tools/apps/transit-timeline-app.d.ts +3 -0
- package/dist/tools/apps/transit-timeline-app.js +77 -9
- package/dist/tools/apps/vedic-chart-app.js +10 -1
- package/dist/tools/datetime-historical.js +7 -2
- package/dist/tools/datetime.js +2 -1
- package/dist/tools/dev.js +7 -6
- package/dist/tools/specialized/electional.js +4 -3
- package/dist/tools/specialized/ephemeris_extended.js +19 -4
- package/dist/tools/specialized/hd_group.js +2 -2
- package/dist/tools/specialized/moon.d.ts +1 -1
- package/dist/tools/specialized/moon.js +51 -43
- package/dist/tools/specialized/progressed.js +2 -25
- package/dist/tools/specialized/transits.js +5 -5
- package/dist/ui/bazi.html +1523 -1522
- package/dist/ui/bi-wheel.html +406 -374
- package/dist/ui/bodygraph.html +83 -79
- package/dist/ui/chart-wheel.html +394 -361
- package/dist/ui/transit-timeline.html +3 -3
- package/dist/ui/vedic-chart.html +818 -818
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { registerTool, validateCoordinates } from "../index.js";
|
|
2
|
-
import { getActiveClient } from "../../backend/client.js";
|
|
2
|
+
import { getActiveClient, rethrowBillingOrAuth } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
4
|
import { TIMEZONE_PROPERTY } from "../datetime.js";
|
|
5
5
|
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
@@ -53,8 +53,15 @@ registerTool({
|
|
|
53
53
|
getActiveClient().request("GET", "/ephemeris/moon/phase", { params }),
|
|
54
54
|
getActiveClient().request("GET", "/ephemeris/moon/void-of-course", { params }),
|
|
55
55
|
]);
|
|
56
|
+
// Out of credits / signed out / tier-gated must reach the assistant as
|
|
57
|
+
// the thrown credit-wall (or auth) error with its top-up link — never
|
|
58
|
+
// as an `error` string tucked inside a success-shaped result.
|
|
59
|
+
rethrowBillingOrAuth([phase, voc]);
|
|
60
|
+
// The phase IS the answer; with it gone there is nothing to degrade to.
|
|
61
|
+
if (phase.status === "rejected")
|
|
62
|
+
throw phase.reason;
|
|
56
63
|
return {
|
|
57
|
-
phase: phase.
|
|
64
|
+
phase: phase.value,
|
|
58
65
|
void_of_course: voc.status === "fulfilled" ? voc.value : { error: voc.reason?.message },
|
|
59
66
|
};
|
|
60
67
|
},
|
|
@@ -65,7 +72,7 @@ registerTool({
|
|
|
65
72
|
"date is the next quarter moon' or 'list the next 3 full moons'. Returns the exact UTC datetime " +
|
|
66
73
|
"of each upcoming occurrence of the requested phase (phase = new_moon | full_moon | first_quarter " +
|
|
67
74
|
"| last_quarter), optionally after a given date and for count occurrences.\n\n" +
|
|
68
|
-
"CREDIT COST:
|
|
75
|
+
"CREDIT COST: 2 credits per occurrence; count max 12.\n\n" +
|
|
69
76
|
"Do not use for the phase or sign the Moon is in right now — use ephemeris_moon_phase; for " +
|
|
70
77
|
"eclipses use ephemeris_next_eclipse.",
|
|
71
78
|
inputSchema: {
|
|
@@ -86,7 +93,7 @@ registerTool({
|
|
|
86
93
|
},
|
|
87
94
|
count: {
|
|
88
95
|
type: "integer",
|
|
89
|
-
description: "Number of upcoming occurrences to return (1-12). Default 1.",
|
|
96
|
+
description: "Number of upcoming occurrences to return (1-12, 2 credits each). Default 1.",
|
|
90
97
|
},
|
|
91
98
|
},
|
|
92
99
|
required: ["phase"],
|
|
@@ -113,55 +120,24 @@ registerTool({
|
|
|
113
120
|
if (!wanted) {
|
|
114
121
|
throw new Error(`Unknown phase '${args.phase}'. Use one of: ${Object.keys(PHASE_IDS).join(", ")}.`);
|
|
115
122
|
}
|
|
116
|
-
const
|
|
123
|
+
const requested = Number.isFinite(Number(args.count)) ? Math.trunc(Number(args.count)) : 1;
|
|
124
|
+
const count = Math.max(1, Math.min(NEXT_LUNAR_PHASE_MAX_COUNT, requested));
|
|
117
125
|
const startDate = (args.after_date ?? new Date().toISOString()).substring(0, 10);
|
|
118
126
|
const startMs = Date.parse(startDate + "T00:00:00Z");
|
|
119
127
|
if (Number.isNaN(startMs)) {
|
|
120
128
|
throw new Error(`after_date must be an ISO date like '2026-06-01', got '${args.after_date}'.`);
|
|
121
129
|
}
|
|
122
|
-
const
|
|
123
|
-
// The calendar endpoint takes a single `date` (it searches forward from noon UTC
|
|
124
|
-
// that day) and returns the first occurrence of each principal phase within the
|
|
125
|
-
// following ~30 days. So one request yields at most one hit for the phase we want.
|
|
126
|
-
async function phaseTimesFrom(probeDay) {
|
|
127
|
-
const result = await getActiveClient().request("GET", "/calendar/astrology/moon-phases", { params: { date: probeDay } });
|
|
128
|
-
const events = Array.isArray(result?.data?.events) ? result.data.events : [];
|
|
129
|
-
// The endpoint prepends the Moon's *current* 8-phase bucket, stamped with the
|
|
130
|
-
// query instant (noon UTC of `date`) rather than an event time. Left in, it
|
|
131
|
-
// reads as a real phase occurring at exactly 12:00:00. Computed moments always
|
|
132
|
-
// carry sub-second precision, so an exact hit on noon is that placeholder.
|
|
133
|
-
const probeNoonMs = Date.parse(probeDay + "T12:00:00Z");
|
|
134
|
-
return events
|
|
135
|
-
.filter((e) => wanted.includes(String(e?.phase ?? e?.phase_name ?? "").toLowerCase()))
|
|
136
|
-
.map((e) => Date.parse(e?.time ?? e?.iso_time ?? ""))
|
|
137
|
-
.filter((ms) => !Number.isNaN(ms) && ms !== probeNoonMs)
|
|
138
|
-
.sort((a, b) => a - b);
|
|
139
|
-
}
|
|
140
|
-
// Probe from the day containing `afterMs - 12h`, so its noon-UTC search origin is
|
|
141
|
-
// always at or before `afterMs` and an occurrence earlier the same day is still
|
|
142
|
-
// visible. That leaves under a day of slack behind the cursor, which can hold at
|
|
143
|
-
// most one occurrence (they are ~29.5 days apart) — hence a single re-probe.
|
|
144
|
-
async function findNext(afterMs) {
|
|
145
|
-
let probeMs = afterMs - HALF_DAY;
|
|
146
|
-
for (let probe = 0; probe < 2; probe++) {
|
|
147
|
-
const hits = await phaseTimesFrom(new Date(probeMs).toISOString().substring(0, 10));
|
|
148
|
-
const next = hits.find((ms) => ms > afterMs);
|
|
149
|
-
if (next != null)
|
|
150
|
-
return next;
|
|
151
|
-
if (hits.length === 0)
|
|
152
|
-
return null;
|
|
153
|
-
probeMs = hits[hits.length - 1] + HALF_DAY;
|
|
154
|
-
}
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
130
|
+
const client = getActiveClient();
|
|
157
131
|
const results = [];
|
|
158
|
-
|
|
132
|
+
// Include an occurrence at exactly 00:00 on after_date: the engine
|
|
133
|
+
// reports phases strictly after the probe instant.
|
|
134
|
+
let probeMs = startMs - 1000;
|
|
159
135
|
for (let i = 0; i < count; i++) {
|
|
160
|
-
const hitMs = await
|
|
136
|
+
const hitMs = await nextPhaseAfter(client, probeMs, wanted);
|
|
161
137
|
if (hitMs == null)
|
|
162
138
|
break;
|
|
163
139
|
results.push({ phase: args.phase, datetime: new Date(hitMs).toISOString() });
|
|
164
|
-
|
|
140
|
+
probeMs = hitMs + PROBE_STEP_PAST_HIT_MS;
|
|
165
141
|
}
|
|
166
142
|
// Every principal phase recurs every ~29.5 days, so an empty result is impossible.
|
|
167
143
|
// Fail loudly rather than handing back a plausible "there isn't one" non-answer.
|
|
@@ -179,3 +155,35 @@ registerTool({
|
|
|
179
155
|
};
|
|
180
156
|
},
|
|
181
157
|
});
|
|
158
|
+
// ── Next-lunar-phase lookup ─────────────────────────────────────────────────
|
|
159
|
+
//
|
|
160
|
+
// /calendar/astrology/moon-phases is the engine's exact principal-phase
|
|
161
|
+
// finder. It takes a single instant (`jd`, or a `date` = noon UTC), not a
|
|
162
|
+
// range, and returns the first New / First Quarter / Full / Third Quarter
|
|
163
|
+
// strictly after it within 30 days — so one call yields exactly one
|
|
164
|
+
// occurrence of each phase. The Go meter prices it at a flat 2 credits (one
|
|
165
|
+
// fixed 30-day span; usage_meter.go). There is no range form to widen, so
|
|
166
|
+
// `count` occurrences of one phase take `count` calls: 2 credits each.
|
|
167
|
+
//
|
|
168
|
+
// Probing with `jd` (the exact instant) rather than `date` (noon UTC) is what
|
|
169
|
+
// makes it one call per occurrence: the search origin sits exactly on the
|
|
170
|
+
// cursor, so no occurrence between midnight and noon can be skipped and no
|
|
171
|
+
// re-probe is needed.
|
|
172
|
+
export const NEXT_LUNAR_PHASE_MAX_COUNT = 12;
|
|
173
|
+
const UNIX_EPOCH_JD = 2440587.5;
|
|
174
|
+
const DAY_MS = 86_400_000;
|
|
175
|
+
/** Step past a found root before the next probe, so the solver's tolerance cannot return it again. */
|
|
176
|
+
const PROBE_STEP_PAST_HIT_MS = 60_000;
|
|
177
|
+
async function nextPhaseAfter(client, afterMs, wanted) {
|
|
178
|
+
const jd = afterMs / DAY_MS + UNIX_EPOCH_JD;
|
|
179
|
+
const result = await client.request("GET", "/calendar/astrology/moon-phases", {
|
|
180
|
+
params: { jd: jd.toFixed(6) },
|
|
181
|
+
});
|
|
182
|
+
const events = Array.isArray(result?.data?.events) ? result.data.events : [];
|
|
183
|
+
const hits = events
|
|
184
|
+
.filter((e) => wanted.includes(String(e?.phase ?? e?.phase_name ?? "").toLowerCase()))
|
|
185
|
+
.map((e) => Date.parse(e?.time ?? e?.iso_time ?? ""))
|
|
186
|
+
.filter((ms) => !Number.isNaN(ms) && ms > afterMs)
|
|
187
|
+
.sort((a, b) => a - b);
|
|
188
|
+
return hits.length ? hits[0] : null;
|
|
189
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { registerTool, validateRequired } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
|
-
import {
|
|
3
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
4
|
import { DATETIME_DESC, TIMEZONE_PROPERTY, WINDOW_DATE_DESC, assertZonedDatetime } from "../datetime.js";
|
|
5
5
|
registerTool({
|
|
6
6
|
name: "ephemeris_progressed_chart",
|
|
@@ -44,26 +44,11 @@ registerTool({
|
|
|
44
44
|
type: "boolean",
|
|
45
45
|
description: "Whether to include aspect grid in the response. Default false.",
|
|
46
46
|
},
|
|
47
|
-
include_visual: {
|
|
48
|
-
type: "boolean",
|
|
49
|
-
description: "When true, renders the progressed chart wheel as an SVG and embeds it in the response. " +
|
|
50
|
-
"The chart image will display in the conversation alongside the data. " +
|
|
51
|
-
"Costs 2 additional credits on top of the base call cost.",
|
|
52
|
-
},
|
|
53
|
-
visual_config: {
|
|
54
|
-
type: "object",
|
|
55
|
-
description: "Optional rendering preferences (only used when include_visual=true).",
|
|
56
|
-
properties: {
|
|
57
|
-
theme: { type: "string", enum: ["light", "dark"], description: "Color theme. Default: light." },
|
|
58
|
-
size: { type: "number", description: "Canvas size in pixels, 200–4000. Default: 800." },
|
|
59
|
-
},
|
|
60
|
-
additionalProperties: false,
|
|
61
|
-
},
|
|
62
47
|
},
|
|
63
48
|
required: ["birth_datetime", "birth_latitude", "birth_longitude", "target_datetime"],
|
|
64
49
|
additionalProperties: false,
|
|
65
50
|
},
|
|
66
|
-
outputSchema:
|
|
51
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
67
52
|
annotations: { title: "Progressed Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
68
53
|
handler: async (args) => {
|
|
69
54
|
validateRequired(args, ["birth_datetime", "birth_latitude", "birth_longitude", "target_datetime"]);
|
|
@@ -91,14 +76,6 @@ registerTool({
|
|
|
91
76
|
if (args.include_aspects != null) {
|
|
92
77
|
body.options = { include_aspects: args.include_aspects };
|
|
93
78
|
}
|
|
94
|
-
if (args.include_visual) {
|
|
95
|
-
body.include_visual = true;
|
|
96
|
-
body.visual_config = {
|
|
97
|
-
format: "svg",
|
|
98
|
-
theme: args.visual_config?.theme || "light",
|
|
99
|
-
size: args.visual_config?.size || 800,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
79
|
return await getActiveClient().post("/ephemeris/progressed", body);
|
|
103
80
|
},
|
|
104
81
|
});
|
|
@@ -11,11 +11,11 @@ registerTool({
|
|
|
11
11
|
"ASPECT ANGLES (aspect_angle): 0 = conjunction/return (default), 180 = opposition, 90 = square, " +
|
|
12
12
|
"120 = trine. Saturn return → transiting_planets=['saturn'], natal_points=['saturn'], " +
|
|
13
13
|
"aspect_angle=0.\n" +
|
|
14
|
-
"EFFICIENCY: always pass transiting_planets and natal_points
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
14
|
+
"EFFICIENCY: always pass transiting_planets and natal_points (omitting both is slow). DEFAULT " +
|
|
15
|
+
"natal_points: sun, moon, mercury, venus, mars, jupiter, saturn.\n" +
|
|
16
|
+
"MAX SPAN: Explorer/PayG 1y, Pro 5y, Startup 10y, Scale 40y (longer = 400).\n" +
|
|
17
|
+
"CREDIT COST: 1 + span price: ≤1y 5, ≤3y 10, ≤5y 15, ≤10y 25, ≤20y 40, ≤40y 70. truncated:true = " +
|
|
18
|
+
"partial; re-call from truncation.resume_from.\n\n" +
|
|
19
19
|
"Do not use when the user should SEE a forecast — use explore_transit_timeline; for the sky today " +
|
|
20
20
|
"with no birth data use electional_moment_analysis; for retrograde dates use " +
|
|
21
21
|
"electional_station_tracker.",
|