@openephemeris/mcp-server 4.8.0 → 4.10.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 +63 -0
- package/dist/tools/apps/bazi-app.js +14 -7
- package/dist/tools/apps/bi-wheel-app.js +5 -5
- package/dist/tools/apps/bodygraph-app.js +8 -8
- package/dist/tools/apps/chart-wheel-app.js +4 -4
- package/dist/tools/apps/location-tools.js +68 -6
- package/dist/tools/apps/moon-phase-app.js +1 -1
- package/dist/tools/apps/vedic-chart-app.js +1 -1
- package/dist/tools/auth.js +8 -0
- package/dist/tools/dev.js +15 -5
- package/dist/tools/specialized/account.js +5 -1
- package/dist/tools/specialized/acg.js +2 -2
- package/dist/tools/specialized/bazi.d.ts +38 -0
- package/dist/tools/specialized/bazi.js +126 -24
- package/dist/tools/specialized/bi_wheel.js +1 -1
- package/dist/tools/specialized/chart_wheel.js +1 -1
- package/dist/tools/specialized/comparative.js +4 -4
- package/dist/tools/specialized/eclipse.js +1 -1
- package/dist/tools/specialized/electional.js +5 -5
- package/dist/tools/specialized/ephemeris_core.js +2 -2
- package/dist/tools/specialized/ephemeris_extended.js +8 -8
- package/dist/tools/specialized/hd_bodygraph.js +1 -1
- package/dist/tools/specialized/hd_cycles.js +2 -2
- package/dist/tools/specialized/hd_group.js +2 -2
- package/dist/tools/specialized/human_design.js +1 -1
- package/dist/tools/specialized/moon.js +2 -2
- package/dist/tools/specialized/natal.js +1 -1
- package/dist/tools/specialized/progressed.js +1 -1
- package/dist/tools/specialized/relocation.js +1 -1
- package/dist/tools/specialized/returns.js +3 -3
- package/dist/tools/specialized/synastry.js +1 -1
- package/dist/tools/specialized/transits.js +1 -1
- package/dist/tools/specialized/vedic.js +1 -1
- package/dist/tools/specialized/venus_star_points.js +6 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,69 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [4.10.0] — 2026-08-05
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **BaZi tools never forwarded `timezone` to the API.** `parseBaziArgs` used it
|
|
14
|
+
only to read a zoned `datetime` back into local calendar components, then
|
|
15
|
+
dropped it — every BaZi request from MCP (`chinese_bazi`, `bazi_ten_gods`,
|
|
16
|
+
`bazi_element_balance`, `bazi_luck_pillars`, `bazi_chart`,
|
|
17
|
+
`explore_bazi_chart`, `bazi_compatibility`) landed on the API with no zone,
|
|
18
|
+
which resolves the year/month solar-term boundary (Li Chun, the Jié) against
|
|
19
|
+
the naive local clock instead of the true birth instant — exactly the defect
|
|
20
|
+
[#532](https://github.com/openephemeris/openephemeris/pull/532) fixes at the
|
|
21
|
+
API. A birth within roughly the birthplace's UTC offset of a boundary could
|
|
22
|
+
land on the wrong side and get the wrong year or month pillar. `timezone` is
|
|
23
|
+
now forwarded on every BaZi tool.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **BaZi convention parameters.** `minute`, `year_boundary`
|
|
27
|
+
(`lichun`/`cny`), `day_boundary` (`zi_hour`/`midnight`), `true_solar_time`,
|
|
28
|
+
and `latitude`/`longitude` are now request parameters on every BaZi tool
|
|
29
|
+
that accepts them at the API — `bazi_compatibility` takes them per chart
|
|
30
|
+
(`chart_a_*`/`chart_b_*`), since partners are often born under different
|
|
31
|
+
conventions or in different timezones.
|
|
32
|
+
|
|
33
|
+
## [4.9.0] — 2026-08-03
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- **`location_search` gave pre-1970 birthplaces the wrong UTC offset.** With a
|
|
37
|
+
`date` before 1970 it answered from local tzdata alone — and tzdata models
|
|
38
|
+
only a zone's *reference city* before 1970, which is exactly the error the
|
|
39
|
+
API's historical correction overlay exists to remove. Dallas on 1952-07-15
|
|
40
|
+
came back `-05:00` (Chicago observed daylight time that summer; Texas did
|
|
41
|
+
not) instead of the correct `-06:00`. Because this tool is the documented
|
|
42
|
+
one-call alternative to `timezone_resolve`, an agent that took the top match
|
|
43
|
+
and built a zone-suffixed datetime from it produced a chart an hour off with
|
|
44
|
+
nothing downstream able to detect it. The top match is now corrected through
|
|
45
|
+
the API (1 extra credit, pre-1970 only); remaining matches keep their tzdata
|
|
46
|
+
estimate and stay labelled `historical_estimate`. Post-1970 is unchanged and
|
|
47
|
+
still free.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- **Historical-correction provenance now reaches MCP consumers.**
|
|
51
|
+
`timezone_resolve` and `location_search` forwarded only `tzRuleSource` out of
|
|
52
|
+
the five fields the API publishes. They now also return `tzOverlayVersion`,
|
|
53
|
+
`tzOverlayHash`, `tzRuleCitation` and `serverVersion`. The first two are the
|
|
54
|
+
change signal — without them a consumer cannot tell that the correction
|
|
55
|
+
dataset moved underneath charts it already computed. `tzRuleCitation` carries
|
|
56
|
+
the actual primary source behind a correction (a statute reference, an agency
|
|
57
|
+
order, a dated almanac page), so a corrected time can show its work instead of
|
|
58
|
+
asking to be trusted. Keys are omitted rather than emitted as nulls when the
|
|
59
|
+
API sends none.
|
|
60
|
+
|
|
61
|
+
## [4.8.1] — 2026-08-02
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
- Every tool now declares all four MCP annotation hints explicitly
|
|
65
|
+
(`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`).
|
|
66
|
+
Notably `dev_write_api` is `destructiveHint: false` — every allowlisted
|
|
67
|
+
write is stateless chart computation — so strict clients no longer gate it
|
|
68
|
+
behind a confirmation, and `openWorldHint` is uniformly `false` (the server
|
|
69
|
+
reaches only the fixed OpenEphemeris API).
|
|
70
|
+
- `account_usage` reports a real authentication failure as a tool error
|
|
71
|
+
(`isError: true`) instead of a success-shaped message.
|
|
72
|
+
|
|
10
73
|
## [4.8.0] — 2026-08-02
|
|
11
74
|
|
|
12
75
|
### Added
|
|
@@ -25,7 +25,7 @@ import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
|
25
25
|
// One BaZi component parser, shared with the specialized tools. The local copy
|
|
26
26
|
// this replaces used `new Date(naive)`, which resolves in the host process's
|
|
27
27
|
// timezone and silently shifted the hour pillar on any non-UTC server.
|
|
28
|
-
import { parseBaziArgs } from "../specialized/bazi.js";
|
|
28
|
+
import { parseBaziArgs, buildBaziConventionFields, CONVENTION_PROPERTIES } from "../specialized/bazi.js";
|
|
29
29
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
30
30
|
export const BAZI_RESOURCE_URI = "ui://openephemeris/bazi";
|
|
31
31
|
export const BAZI_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -66,10 +66,11 @@ export function clearBaziBundleCache() {
|
|
|
66
66
|
* Go-rendered SVG (bazi.RenderBaziChartSVG), same shape the strict
|
|
67
67
|
* /chinese/bazi handler returns with a `visual` key attached.
|
|
68
68
|
*/
|
|
69
|
-
async function fetchBaziChart(components, theme = "dark") {
|
|
69
|
+
async function fetchBaziChart(components, theme = "dark", conventionFields = {}) {
|
|
70
70
|
const client = getActiveClient();
|
|
71
71
|
const body = {
|
|
72
72
|
...components,
|
|
73
|
+
...conventionFields,
|
|
73
74
|
include_visual: true,
|
|
74
75
|
visual_config: { format: "svg", theme, size: 800 },
|
|
75
76
|
};
|
|
@@ -129,6 +130,10 @@ registerTool({
|
|
|
129
130
|
description: "Birth hour (0–23). Optional, defaults to 12 (noon). " +
|
|
130
131
|
"Chinese shí hours are 2-hour blocks — precision within a 2-hour window is sufficient.",
|
|
131
132
|
},
|
|
133
|
+
minute: {
|
|
134
|
+
type: "integer",
|
|
135
|
+
description: "Birth minute (0–59). Optional, defaults to 0. Only matters under true_solar_time or near a boundary.",
|
|
136
|
+
},
|
|
132
137
|
datetime: {
|
|
133
138
|
type: "string",
|
|
134
139
|
description: "Alternative to year/month/day: ISO 8601 datetime read as LOCAL wall-clock time at the " +
|
|
@@ -137,9 +142,10 @@ registerTool({
|
|
|
137
142
|
},
|
|
138
143
|
timezone: {
|
|
139
144
|
type: "string",
|
|
140
|
-
description: "IANA timezone name for the birth place, e.g. 'Asia/Shanghai'.
|
|
141
|
-
"datetime
|
|
145
|
+
description: "IANA timezone name for the birth place, e.g. 'Asia/Shanghai'. Required to convert a zoned " +
|
|
146
|
+
"datetime back to local clock; also frames the year/month solar-term boundary.",
|
|
142
147
|
},
|
|
148
|
+
...CONVENTION_PROPERTIES,
|
|
143
149
|
},
|
|
144
150
|
additionalProperties: false,
|
|
145
151
|
},
|
|
@@ -159,17 +165,18 @@ registerTool({
|
|
|
159
165
|
},
|
|
160
166
|
handler: async (args) => {
|
|
161
167
|
const components = parseBaziArgs(args);
|
|
168
|
+
const conventionFields = buildBaziConventionFields(args);
|
|
162
169
|
const bundleAvailable = Boolean(getBaziBundle());
|
|
163
170
|
const theme = "dark";
|
|
164
171
|
if (!bundleAvailable) {
|
|
165
172
|
// Text-fallback hosts have no use for the SVG — skip the visual surcharge.
|
|
166
173
|
const data = await getActiveClient().request("POST", "/chinese/bazi", {
|
|
167
|
-
data: components,
|
|
174
|
+
data: { ...components, ...conventionFields },
|
|
168
175
|
});
|
|
169
176
|
const payload = buildModelPayload(data, components, theme);
|
|
170
177
|
return { content: [{ type: "text", text: buildSummary(payload) }] };
|
|
171
178
|
}
|
|
172
|
-
const data = await fetchBaziChart(components, theme);
|
|
179
|
+
const data = await fetchBaziChart(components, theme, conventionFields);
|
|
173
180
|
const payload = buildModelPayload(data, components, theme);
|
|
174
181
|
const summary = buildSummary(payload);
|
|
175
182
|
// MCP Apps wire format: the UI is declared via `_meta.ui.resourceUri` and
|
|
@@ -204,7 +211,7 @@ registerTool({
|
|
|
204
211
|
required: ["year", "month", "day"],
|
|
205
212
|
},
|
|
206
213
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
207
|
-
annotations: { title: "Recalculate BaZi Chart", readOnlyHint: true, openWorldHint: false },
|
|
214
|
+
annotations: { title: "Recalculate BaZi Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
208
215
|
_meta: { ui: { resourceUri: BAZI_RESOURCE_URI, visibility: ["app"] } },
|
|
209
216
|
handler: async (args) => {
|
|
210
217
|
const components = parseBaziArgs(args);
|
|
@@ -588,7 +588,7 @@ registerTool({
|
|
|
588
588
|
required: ["mode", "person1_datetime", "person1_latitude", "person1_longitude", "person2_datetime"],
|
|
589
589
|
},
|
|
590
590
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
591
|
-
annotations: { title: "Recalculate Bi-Wheel", readOnlyHint: true, openWorldHint: false },
|
|
591
|
+
annotations: { title: "Recalculate Bi-Wheel", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
592
592
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
593
593
|
handler: async (args) => {
|
|
594
594
|
const client = getActiveClient();
|
|
@@ -644,7 +644,7 @@ registerTool({
|
|
|
644
644
|
required: ["planet1", "planet2", "aspect_type"],
|
|
645
645
|
},
|
|
646
646
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
647
|
-
annotations: { title: "Cross-Aspect Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
647
|
+
annotations: { title: "Cross-Aspect Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
648
648
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
649
649
|
handler: async (args) => {
|
|
650
650
|
const p1 = capitalize(String(args.planet1));
|
|
@@ -683,7 +683,7 @@ registerTool({
|
|
|
683
683
|
required: ["planet", "wheel", "longitude"],
|
|
684
684
|
},
|
|
685
685
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
686
|
-
annotations: { title: "Bi-Wheel Planet Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
686
|
+
annotations: { title: "Bi-Wheel Planet Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
687
687
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
688
688
|
handler: async (args) => {
|
|
689
689
|
const planet = String(args.planet);
|
|
@@ -884,7 +884,7 @@ registerTool({
|
|
|
884
884
|
required: ["house_number"],
|
|
885
885
|
},
|
|
886
886
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
887
|
-
annotations: { title: "Bi-Wheel House Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
887
|
+
annotations: { title: "Bi-Wheel House Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
888
888
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
889
889
|
handler: async (args) => {
|
|
890
890
|
const num = Number(args.house_number);
|
|
@@ -944,7 +944,7 @@ registerTool({
|
|
|
944
944
|
required: ["mode"],
|
|
945
945
|
},
|
|
946
946
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
947
|
-
annotations: { title: "Bi-Wheel Synopsis", readOnlyHint: true, openWorldHint: false },
|
|
947
|
+
annotations: { title: "Bi-Wheel Synopsis", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
948
948
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app", "model"] } },
|
|
949
949
|
handler: async (args) => {
|
|
950
950
|
const mode = args.mode ?? "synastry";
|
|
@@ -471,7 +471,7 @@ registerTool({
|
|
|
471
471
|
required: ["datetime"],
|
|
472
472
|
},
|
|
473
473
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
474
|
-
annotations: { title: "Recalculate Bodygraph", readOnlyHint: true, openWorldHint: false },
|
|
474
|
+
annotations: { title: "Recalculate Bodygraph", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
475
475
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
476
476
|
handler: async (args) => {
|
|
477
477
|
const client = getActiveClient();
|
|
@@ -536,7 +536,7 @@ registerTool({
|
|
|
536
536
|
required: ["center", "defined"],
|
|
537
537
|
},
|
|
538
538
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
539
|
-
annotations: { title: "Center Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
539
|
+
annotations: { title: "Center Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
540
540
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
541
541
|
handler: async (args) => {
|
|
542
542
|
const center = String(args.center);
|
|
@@ -594,7 +594,7 @@ registerTool({
|
|
|
594
594
|
required: ["gate"],
|
|
595
595
|
},
|
|
596
596
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
597
|
-
annotations: { title: "Gate Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
597
|
+
annotations: { title: "Gate Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
598
598
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
599
599
|
handler: async (args) => {
|
|
600
600
|
const gate = Number(args.gate);
|
|
@@ -649,7 +649,7 @@ registerTool({
|
|
|
649
649
|
required: ["channel"],
|
|
650
650
|
},
|
|
651
651
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
652
|
-
annotations: { title: "Channel Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
652
|
+
annotations: { title: "Channel Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
653
653
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
654
654
|
handler: async (args) => {
|
|
655
655
|
const channelStr = String(args.channel);
|
|
@@ -751,7 +751,7 @@ registerTool({
|
|
|
751
751
|
required: ["channel"],
|
|
752
752
|
},
|
|
753
753
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
754
|
-
annotations: { title: "Connection Channel Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
754
|
+
annotations: { title: "Connection Channel Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
755
755
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
756
756
|
handler: async (args) => {
|
|
757
757
|
const channelStr = String(args.channel ?? "");
|
|
@@ -807,7 +807,7 @@ registerTool({
|
|
|
807
807
|
required: ["channel"],
|
|
808
808
|
},
|
|
809
809
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
810
|
-
annotations: { title: "Transit Channel Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
810
|
+
annotations: { title: "Transit Channel Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
811
811
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
812
812
|
handler: async (args) => {
|
|
813
813
|
const channelStr = String(args.channel ?? "");
|
|
@@ -1135,7 +1135,7 @@ registerTool({
|
|
|
1135
1135
|
required: ["planet", "column"],
|
|
1136
1136
|
},
|
|
1137
1137
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
1138
|
-
annotations: { title: "Planet Sidebar Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
1138
|
+
annotations: { title: "Planet Sidebar Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1139
1139
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
1140
1140
|
handler: async (args) => {
|
|
1141
1141
|
const planet = String(args.planet);
|
|
@@ -1204,7 +1204,7 @@ registerTool({
|
|
|
1204
1204
|
required: ["variable", "direction"],
|
|
1205
1205
|
},
|
|
1206
1206
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
1207
|
-
annotations: { title: "Variable Arrow Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
1207
|
+
annotations: { title: "Variable Arrow Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1208
1208
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
1209
1209
|
handler: async (args) => {
|
|
1210
1210
|
const variable = String(args.variable).toLowerCase();
|
|
@@ -259,7 +259,7 @@ registerTool({
|
|
|
259
259
|
required: ["planet", "longitude"],
|
|
260
260
|
},
|
|
261
261
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
262
|
-
annotations: { title: "Planet Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
262
|
+
annotations: { title: "Planet Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
263
263
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
264
264
|
handler: async (args) => {
|
|
265
265
|
const planet = String(args.planet);
|
|
@@ -294,7 +294,7 @@ registerTool({
|
|
|
294
294
|
required: ["house_number"],
|
|
295
295
|
},
|
|
296
296
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
297
|
-
annotations: { title: "House Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
297
|
+
annotations: { title: "House Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
298
298
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
299
299
|
handler: async (args) => {
|
|
300
300
|
const num = Number(args.house_number);
|
|
@@ -330,7 +330,7 @@ registerTool({
|
|
|
330
330
|
required: ["planet1", "planet2"],
|
|
331
331
|
},
|
|
332
332
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
333
|
-
annotations: { title: "Aspect Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
333
|
+
annotations: { title: "Aspect Interpretation", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
334
334
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
335
335
|
handler: async (args) => {
|
|
336
336
|
const p1 = capitalize(String(args.planet1));
|
|
@@ -379,7 +379,7 @@ registerTool({
|
|
|
379
379
|
required: ["datetime", "latitude", "longitude", "house_system"],
|
|
380
380
|
},
|
|
381
381
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
382
|
-
annotations: { title: "Recalculate Chart", readOnlyHint: true, openWorldHint: false },
|
|
382
|
+
annotations: { title: "Recalculate Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
383
383
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
384
384
|
handler: async (args) => {
|
|
385
385
|
const client = getActiveClient();
|
|
@@ -2,6 +2,34 @@ import { registerTool, validateRequired } from "../index.js";
|
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
4
|
import { TZDATA_AUTHORITATIVE_FROM_YEAR } from "../datetime-historical.js";
|
|
5
|
+
/**
|
|
6
|
+
* Lift the API's historical-correction provenance onto an MCP result.
|
|
7
|
+
*
|
|
8
|
+
* The server publishes five fields that say *why* a pre-1970 offset is what it
|
|
9
|
+
* is, and until now this server forwarded exactly one of them. That left an
|
|
10
|
+
* agent unable to tell a cited statutory correction from a tzdata guess, and
|
|
11
|
+
* left every MCP consumer blind to the overlay dataset changing underneath
|
|
12
|
+
* previously-computed charts — which is the whole reason the version signal
|
|
13
|
+
* exists.
|
|
14
|
+
*
|
|
15
|
+
* Placement mirrors the REST contract: `tz_overlay_version`/`tz_overlay_hash`
|
|
16
|
+
* sit at the top level of the /timezone/offset response alongside
|
|
17
|
+
* `tz_rule_source`; `tz_rule_citation` and `server_version` live in
|
|
18
|
+
* `calculation_metadata`.
|
|
19
|
+
*/
|
|
20
|
+
function historicalProvenance(off) {
|
|
21
|
+
const meta = (off?.calculation_metadata ?? {});
|
|
22
|
+
const citation = meta.tz_rule_citation;
|
|
23
|
+
const serverVersion = meta.server_version;
|
|
24
|
+
return {
|
|
25
|
+
...(typeof off.tz_rule_source === "string" ? { tzRuleSource: off.tz_rule_source } : {}),
|
|
26
|
+
...(typeof off.datetime_status === "string" ? { datetimeStatus: off.datetime_status } : {}),
|
|
27
|
+
...(typeof off.tz_overlay_version === "number" ? { tzOverlayVersion: off.tz_overlay_version } : {}),
|
|
28
|
+
...(typeof off.tz_overlay_hash === "string" ? { tzOverlayHash: off.tz_overlay_hash } : {}),
|
|
29
|
+
...(typeof citation === "string" && citation !== "" ? { tzRuleCitation: citation } : {}),
|
|
30
|
+
...(typeof serverVersion === "string" && serverVersion !== "" ? { serverVersion } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
5
33
|
/** Render minutes east of UTC as `±HH:MM`. */
|
|
6
34
|
function formatOffsetMinutes(offsetMinutes) {
|
|
7
35
|
const sign = offsetMinutes < 0 ? "-" : "+";
|
|
@@ -98,7 +126,7 @@ function offsetAtLocalNoon(timezone, date) {
|
|
|
98
126
|
}
|
|
99
127
|
registerTool({
|
|
100
128
|
name: "location_search",
|
|
101
|
-
description: "Resolve a place name to coordinates and IANA timezone — use this first whenever a user gives a birth city rather than latitude/longitude. NEVER recall coordinates from memory; always resolve them here. CREDIT COST: 1 credit per call. Returns display name, region (state/province), latitude, longitude, and IANA timezone. Pass the birth date as `date` to also get `utcOffsetAtDate`, the historically-correct UTC offset for that place on that date (1987 DST rules differ from today's)
|
|
129
|
+
description: "Resolve a place name to coordinates and IANA timezone — use this first whenever a user gives a birth city rather than latitude/longitude. NEVER recall coordinates from memory; always resolve them here. CREDIT COST: 1 credit per call. Returns display name, region (state/province), latitude, longitude, and IANA timezone. Pass the birth date as `date` to also get `utcOffsetAtDate`, the historically-correct UTC offset for that place on that date (1987 DST rules differ from today's). Post-1970 dates resolve locally and cost no extra credits; a pre-1970 date consults the API's historical correction overlay for the top match (1 extra credit) and returns its provenance — tzRuleSource, tzRuleCitation, tzOverlayVersion. When the result is `ambiguous` (several places share the name, e.g. \"portland\"), ASK the user which one they mean rather than assuming the first. Optional bias params (country/region/near) improve ranking; a trailing \"City, ST\" qualifier in the query is also honored.",
|
|
102
130
|
inputSchema: {
|
|
103
131
|
type: "object",
|
|
104
132
|
properties: {
|
|
@@ -124,14 +152,14 @@ registerTool({
|
|
|
124
152
|
},
|
|
125
153
|
date: {
|
|
126
154
|
type: "string",
|
|
127
|
-
description: "Optional birth/event date as 'YYYY-MM-DD'. When given, each suggestion also carries utcOffsetAtDate / utcOffsetMinutes / isDst — the UTC offset that actually applied at that place on that date, using historical DST rules.
|
|
155
|
+
description: "Optional birth/event date as 'YYYY-MM-DD'. When given, each suggestion also carries utcOffsetAtDate / utcOffsetMinutes / isDst — the UTC offset that actually applied at that place on that date, using historical DST rules. Post-1970 is free (local resolution, no API call). Pre-1970, the top match is additionally corrected through the API's historical overlay (1 extra credit) because tzdata models only the zone's reference city before 1970; the remaining suggestions keep their tzdata estimate and are labelled historical_estimate.",
|
|
128
156
|
},
|
|
129
157
|
},
|
|
130
158
|
required: ["query"],
|
|
131
159
|
additionalProperties: false,
|
|
132
160
|
},
|
|
133
161
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
134
|
-
annotations: { title: "Location Search", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
162
|
+
annotations: { title: "Location Search", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
135
163
|
// Model-visible as well as app-visible: without this the model had no
|
|
136
164
|
// geocoding tool and every prompt had to route through
|
|
137
165
|
// `dev_read_api /location/autocomplete` instead.
|
|
@@ -178,6 +206,41 @@ registerTool({
|
|
|
178
206
|
}
|
|
179
207
|
return mapped;
|
|
180
208
|
});
|
|
209
|
+
// Pre-1970, the tzdata offsets just attached are the very answers the
|
|
210
|
+
// historical-correction overlay exists to replace: a 1961 Robbinsdale
|
|
211
|
+
// date gets -05:00 from tzdata where state law says -06:00. This tool
|
|
212
|
+
// is documented as the one-call alternative to timezone_resolve, so an
|
|
213
|
+
// agent that takes the top hit and builds a zone-suffixed datetime from
|
|
214
|
+
// it would produce a chart an hour off — and nothing downstream could
|
|
215
|
+
// detect it.
|
|
216
|
+
//
|
|
217
|
+
// Correct the TOP suggestion through the server (one extra credit, the
|
|
218
|
+
// same cost timezone_resolve documents). Correcting all eight would
|
|
219
|
+
// multiply that for rows the caller almost never uses; the remaining
|
|
220
|
+
// rows keep their tzdata offsets and their historical_estimate label.
|
|
221
|
+
const topHit = suggestions[0];
|
|
222
|
+
if (preTzdataEra && date && topHit && typeof topHit.latitude === "number" && typeof topHit.longitude === "number") {
|
|
223
|
+
try {
|
|
224
|
+
const off = (await getActiveClient().request("POST", "/timezone/offset", {
|
|
225
|
+
data: { lat: topHit.latitude, lon: topHit.longitude, datetime_local: `${date.slice(0, 10)}T12:00:00` },
|
|
226
|
+
}));
|
|
227
|
+
if (typeof off?.offset_seconds === "number") {
|
|
228
|
+
const minutes = off.offset_seconds / 60;
|
|
229
|
+
Object.assign(topHit, {
|
|
230
|
+
utcOffsetAtDate: formatOffsetMinutes(minutes),
|
|
231
|
+
utcOffsetMinutes: minutes,
|
|
232
|
+
isDst: Boolean(off.is_dst),
|
|
233
|
+
tzConfidence: typeof off.tz_confidence === "string" ? off.tz_confidence : "historical_estimate",
|
|
234
|
+
...historicalProvenance(off),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Server path unavailable — the tzdata estimate stands, still
|
|
240
|
+
// labelled historical_estimate. Degrading to a labelled
|
|
241
|
+
// estimate is acceptable; failing the whole search is not.
|
|
242
|
+
}
|
|
243
|
+
}
|
|
181
244
|
// Ambiguity signal. "portland" really does match Oregon, Maine, Texas,
|
|
182
245
|
// England, ... — an agent that silently takes suggestions[0] produces a
|
|
183
246
|
// chart nothing downstream can detect as wrong.
|
|
@@ -235,7 +298,7 @@ registerTool({
|
|
|
235
298
|
additionalProperties: false,
|
|
236
299
|
},
|
|
237
300
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
238
|
-
annotations: { title: "Timezone Resolve", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
301
|
+
annotations: { title: "Timezone Resolve", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
239
302
|
_meta: {
|
|
240
303
|
ui: {
|
|
241
304
|
visibility: ["model", "app"],
|
|
@@ -272,8 +335,7 @@ registerTool({
|
|
|
272
335
|
isDst: Boolean(off.is_dst),
|
|
273
336
|
date,
|
|
274
337
|
tzConfidence: typeof off.tz_confidence === "string" ? off.tz_confidence : "historical_estimate",
|
|
275
|
-
...(
|
|
276
|
-
...(typeof off.datetime_status === "string" ? { datetimeStatus: off.datetime_status } : {}),
|
|
338
|
+
...historicalProvenance(off),
|
|
277
339
|
};
|
|
278
340
|
}
|
|
279
341
|
}
|
|
@@ -295,7 +295,7 @@ registerTool({
|
|
|
295
295
|
additionalProperties: false,
|
|
296
296
|
},
|
|
297
297
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
298
|
-
annotations: { title: "Recalculate Moon Phase", readOnlyHint: true, openWorldHint: false },
|
|
298
|
+
annotations: { title: "Recalculate Moon Phase", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
299
299
|
_meta: { ui: { resourceUri: MOON_PHASE_RESOURCE_URI, visibility: ["app"] } },
|
|
300
300
|
handler: async (args) => {
|
|
301
301
|
const { summary, uiPayload } = await computeMoonData(args);
|
|
@@ -240,7 +240,7 @@ registerTool({
|
|
|
240
240
|
required: ["datetime"],
|
|
241
241
|
},
|
|
242
242
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
243
|
-
annotations: { title: "Recalculate Vedic Chart", readOnlyHint: true, openWorldHint: false },
|
|
243
|
+
annotations: { title: "Recalculate Vedic Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
244
244
|
_meta: { ui: { resourceUri: VEDIC_CHART_RESOURCE_URI, visibility: ["app"] } },
|
|
245
245
|
handler: async (args) => {
|
|
246
246
|
const client = getActiveClient();
|
package/dist/tools/auth.js
CHANGED
|
@@ -24,6 +24,10 @@ registerTool({
|
|
|
24
24
|
title: "Connect Account",
|
|
25
25
|
readOnlyHint: false,
|
|
26
26
|
destructiveHint: false,
|
|
27
|
+
// Each call starts a fresh device-auth flow with a new code — repeating
|
|
28
|
+
// it is harmless but not a no-op.
|
|
29
|
+
idempotentHint: false,
|
|
30
|
+
openWorldHint: false,
|
|
27
31
|
},
|
|
28
32
|
stdioOnly: true,
|
|
29
33
|
handler: async () => {
|
|
@@ -105,6 +109,8 @@ registerTool({
|
|
|
105
109
|
title: "Auth Status",
|
|
106
110
|
readOnlyHint: true,
|
|
107
111
|
destructiveHint: false,
|
|
112
|
+
idempotentHint: true,
|
|
113
|
+
openWorldHint: false,
|
|
108
114
|
},
|
|
109
115
|
stdioOnly: true,
|
|
110
116
|
handler: async () => {
|
|
@@ -191,6 +197,8 @@ registerTool({
|
|
|
191
197
|
title: "Disconnect Account",
|
|
192
198
|
readOnlyHint: false,
|
|
193
199
|
destructiveHint: true,
|
|
200
|
+
idempotentHint: true,
|
|
201
|
+
openWorldHint: false,
|
|
194
202
|
},
|
|
195
203
|
stdioOnly: true,
|
|
196
204
|
handler: async () => {
|
package/dist/tools/dev.js
CHANGED
|
@@ -196,8 +196,10 @@ registerTool({
|
|
|
196
196
|
DEV_API_REFERENCE,
|
|
197
197
|
inputSchema: makeProxyInputSchema(READ_METHODS),
|
|
198
198
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
199
|
-
// GET-only: never mutates server state.
|
|
200
|
-
|
|
199
|
+
// GET-only: never mutates server state. openWorldHint false — the proxy
|
|
200
|
+
// reaches only the fixed, allowlisted OpenEphemeris API (a closed world),
|
|
201
|
+
// same as every typed tool.
|
|
202
|
+
annotations: { title: "API Read", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
201
203
|
handler: makeProxyHandler(READ_METHODS, "GET"),
|
|
202
204
|
});
|
|
203
205
|
registerTool({
|
|
@@ -209,8 +211,16 @@ registerTool({
|
|
|
209
211
|
DEV_API_REFERENCE,
|
|
210
212
|
inputSchema: makeProxyInputSchema(WRITE_METHODS),
|
|
211
213
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
212
|
-
// Issues POST/PUT/PATCH/DELETE — must not be advertised as read-only.
|
|
213
|
-
|
|
214
|
+
// Issues POST/PUT/PATCH/DELETE — must not be advertised as read-only. But
|
|
215
|
+
// destructiveHint is explicitly FALSE: every allowlisted write operation is
|
|
216
|
+
// stateless chart computation (natal, synastry, ACG, returns…) that debits
|
|
217
|
+
// credits and returns a result — nothing on the allowlist mutates or deletes
|
|
218
|
+
// user data, and the deny rules block /auth, /billing, /account, /api-keys.
|
|
219
|
+
// Left unset, strict clients default destructiveHint to true and gate every
|
|
220
|
+
// call behind a confirmation. idempotentHint true for the same reason: the
|
|
221
|
+
// same body yields the same chart. openWorldHint false — fixed allowlisted
|
|
222
|
+
// API only, a closed world.
|
|
223
|
+
annotations: { title: "API Write", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
214
224
|
handler: makeProxyHandler(WRITE_METHODS, "POST"),
|
|
215
225
|
});
|
|
216
226
|
registerTool({
|
|
@@ -226,7 +236,7 @@ registerTool({
|
|
|
226
236
|
additionalProperties: false,
|
|
227
237
|
},
|
|
228
238
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
229
|
-
annotations: { title: "List Allowed API Operations", readOnlyHint: true, openWorldHint: false },
|
|
239
|
+
annotations: { title: "List Allowed API Operations", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
230
240
|
handler: async () => {
|
|
231
241
|
const allowlist = loadAllowlist();
|
|
232
242
|
return {
|
|
@@ -27,7 +27,7 @@ registerTool({
|
|
|
27
27
|
additionalProperties: false,
|
|
28
28
|
},
|
|
29
29
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
30
|
-
annotations: { title: "Account Usage", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
30
|
+
annotations: { title: "Account Usage", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
31
31
|
handler: async (args) => {
|
|
32
32
|
const params = {};
|
|
33
33
|
if (args?.month)
|
|
@@ -39,7 +39,11 @@ registerTool({
|
|
|
39
39
|
}
|
|
40
40
|
catch (error) {
|
|
41
41
|
if (error instanceof BackendError && error.status === 401) {
|
|
42
|
+
// A real auth failure must carry isError — a success-shaped response
|
|
43
|
+
// teaches the model the call worked and it stops trying to fix auth.
|
|
44
|
+
// The guidance text is kept so hosts still show the user a way forward.
|
|
42
45
|
return {
|
|
46
|
+
isError: true,
|
|
43
47
|
content: [{
|
|
44
48
|
type: "text",
|
|
45
49
|
text: `**Not signed in.** To check usage, connect an OpenEphemeris account first.\n\n` +
|
|
@@ -60,7 +60,7 @@ registerTool({
|
|
|
60
60
|
additionalProperties: false,
|
|
61
61
|
},
|
|
62
62
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
63
|
-
annotations: { title: "Astrocartography Lines", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
63
|
+
annotations: { title: "Astrocartography Lines", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
64
64
|
handler: async (args) => {
|
|
65
65
|
validateRequired(args, ["birth_datetime", "birth_latitude", "birth_longitude"]);
|
|
66
66
|
const body = {
|
|
@@ -157,7 +157,7 @@ registerTool({
|
|
|
157
157
|
additionalProperties: false,
|
|
158
158
|
},
|
|
159
159
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
160
|
-
annotations: { title: "Astrocartography Location Hits", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
160
|
+
annotations: { title: "Astrocartography Location Hits", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
161
161
|
handler: async (args) => {
|
|
162
162
|
validateRequired(args, ["birth_datetime", "birth_latitude", "birth_longitude", "query_latitude", "query_longitude"]);
|
|
163
163
|
const body = {
|
|
@@ -3,5 +3,43 @@ export interface BaziComponents {
|
|
|
3
3
|
month: number;
|
|
4
4
|
day: number;
|
|
5
5
|
hour?: number;
|
|
6
|
+
minute?: number;
|
|
6
7
|
}
|
|
7
8
|
export declare function parseBaziArgs(args: any, datetimeField?: string): BaziComponents;
|
|
9
|
+
/**
|
|
10
|
+
* Build the optional BaZi convention fields (year_boundary, day_boundary,
|
|
11
|
+
* true_solar_time, latitude, longitude, timezone) to spread into a request
|
|
12
|
+
* body. `timezone` is wrapped to the API's `TimezoneInput` object shape.
|
|
13
|
+
*
|
|
14
|
+
* Kept separate from `parseBaziArgs`/`BaziComponents`: these fields frame
|
|
15
|
+
* which convention resolves the pillars rather than naming the birth moment
|
|
16
|
+
* itself, and bazi_compatibility needs the same set built twice under a
|
|
17
|
+
* `chart_a_`/`chart_b_` prefix.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildBaziConventionFields(args: any): Record<string, unknown>;
|
|
20
|
+
/** `buildBaziConventionFields`, reading `<prefix>_<field>` args — used by bazi_compatibility. */
|
|
21
|
+
export declare function buildBaziConventionFieldsPrefixed(args: any, prefix: string): Record<string, unknown>;
|
|
22
|
+
export declare const CONVENTION_PROPERTIES: {
|
|
23
|
+
readonly year_boundary: {
|
|
24
|
+
readonly type: "string";
|
|
25
|
+
readonly enum: readonly ["lichun", "cny"];
|
|
26
|
+
readonly description: "Year-pillar cutover convention. Default lichun.";
|
|
27
|
+
};
|
|
28
|
+
readonly day_boundary: {
|
|
29
|
+
readonly type: "string";
|
|
30
|
+
readonly enum: readonly ["zi_hour", "midnight"];
|
|
31
|
+
readonly description: "Day-pillar cutover convention. Default zi_hour.";
|
|
32
|
+
};
|
|
33
|
+
readonly true_solar_time: {
|
|
34
|
+
readonly type: "boolean";
|
|
35
|
+
readonly description: "Apply true solar time correction (needs latitude+longitude).";
|
|
36
|
+
};
|
|
37
|
+
readonly latitude: {
|
|
38
|
+
readonly type: "number";
|
|
39
|
+
readonly description: "Birth latitude, decimal degrees (north+).";
|
|
40
|
+
};
|
|
41
|
+
readonly longitude: {
|
|
42
|
+
readonly type: "number";
|
|
43
|
+
readonly description: "Birth longitude, decimal degrees (east+).";
|
|
44
|
+
};
|
|
45
|
+
};
|