@openephemeris/mcp-server 4.6.0 → 4.8.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 +69 -0
- package/README.md +35 -3
- package/dist/index.js +5 -2
- package/dist/oauth/token.js +30 -3
- package/dist/prompts.js +3 -2
- package/dist/server-sse.js +7 -2
- package/dist/tools/apps/bazi-app.js +3 -3
- package/dist/tools/apps/bi-wheel-app.js +52 -18
- package/dist/tools/apps/bodygraph-app.js +12 -8
- package/dist/tools/apps/location-tools.js +67 -5
- package/dist/tools/apps/moon-phase-app.js +3 -2
- package/dist/tools/apps/vedic-chart-app.js +3 -3
- package/dist/tools/datetime-historical.d.ts +19 -0
- package/dist/tools/datetime-historical.js +77 -0
- package/dist/tools/index.d.ts +41 -2
- package/dist/tools/index.js +201 -4
- package/dist/tools/specialized/acg.js +4 -3
- package/dist/tools/specialized/hd_bodygraph.js +12 -3
- package/dist/tools/specialized/hd_cycles.js +19 -3
- package/dist/tools/specialized/hd_group.js +31 -6
- package/dist/tools/specialized/human_design.js +3 -2
- package/dist/tools/specialized/moon.js +11 -13
- package/dist/tools/specialized/vedic.js +3 -2
- package/dist/ui/bazi.html +30 -30
- package/dist/ui/bi-wheel.html +35 -35
- package/dist/ui/bodygraph.html +39 -39
- package/dist/ui/chart-wheel.html +93 -90
- package/dist/ui/moon-phase.html +32 -32
- package/dist/ui/transit-timeline.html +30 -30
- package/dist/ui/vedic-chart.html +35 -35
- package/package.json +4 -2
- package/smithery.yaml +14 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Where IANA tzdata stops being reference-city best effort. */
|
|
2
|
+
export declare const TZDATA_AUTHORITATIVE_FROM_YEAR = 1970;
|
|
3
|
+
export interface BirthCoords {
|
|
4
|
+
latitude?: number | null;
|
|
5
|
+
longitude?: number | null;
|
|
6
|
+
}
|
|
7
|
+
/** True when a naive local datetime needs the server-side historical path. */
|
|
8
|
+
export declare function needsHistoricalResolution(dt: string, coords?: BirthCoords): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* `localToUtcIso`, but historically correct when it matters.
|
|
11
|
+
*
|
|
12
|
+
* Same contract as `localToUtcIso` (naive value with no `tz` throws the
|
|
13
|
+
* ambiguous-datetime error; zone-suffixed and date-only values pass through).
|
|
14
|
+
* For a pre-1970 naive value with known coordinates, the conversion is done
|
|
15
|
+
* by the API's `/timezone/offset` `datetime_local` mode so the historical
|
|
16
|
+
* correction overlay applies. Everything else — including every post-1970
|
|
17
|
+
* conversion — stays on the local, zero-latency Intl path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function localToUtcIsoHistorical(field: string, dt: string, tz?: string, coords?: BirthCoords, timezoneField?: string): Promise<string>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Historically-correct local → UTC conversion.
|
|
3
|
+
*
|
|
4
|
+
* IANA tzdata — Node's Intl and the Go engine's tzdata alike — is only
|
|
5
|
+
* authoritative from 1970. Before the US Uniform Time Act (effective 1967),
|
|
6
|
+
* DST was a state and municipal matter, and tzdata models each zone's
|
|
7
|
+
* reference city only. `localToUtcIso`'s Intl math is therefore silently an
|
|
8
|
+
* hour off for births like Robbinsdale, MN 1961-10-09 17:56 (Minnesota was on
|
|
9
|
+
* CST while America/Chicago says CDT), which flips the Ascendant sign and the
|
|
10
|
+
* HD design-Moon gate.
|
|
11
|
+
*
|
|
12
|
+
* The Go API owns the correction: `POST /timezone/offset` with
|
|
13
|
+
* `datetime_local` + coordinates resolves through its historical-correction
|
|
14
|
+
* overlay (apps/api/go-sidecar/internal/historicaltz) and returns the true
|
|
15
|
+
* instant plus a `tz_confidence` grade. This module routes the conversions
|
|
16
|
+
* that need it — pre-1970, naive, with known coordinates — through that
|
|
17
|
+
* endpoint, and leaves every other case on the local Intl path, which is
|
|
18
|
+
* exactly as authoritative as the server for 1970+.
|
|
19
|
+
*
|
|
20
|
+
* Fallback discipline: if the server call fails or resolves a different zone
|
|
21
|
+
* than the caller explicitly named, we fall back to `localToUtcIso` — the
|
|
22
|
+
* pre-existing behavior — rather than failing the chart call. Best effort
|
|
23
|
+
* beats an outage; the server remains the single authority whenever it is
|
|
24
|
+
* reachable.
|
|
25
|
+
*/
|
|
26
|
+
import { getActiveClient } from "../backend/client.js";
|
|
27
|
+
import { hasZoneSuffix, isNaiveClockTime, localToUtcIso } from "./datetime.js";
|
|
28
|
+
/** Where IANA tzdata stops being reference-city best effort. */
|
|
29
|
+
export const TZDATA_AUTHORITATIVE_FROM_YEAR = 1970;
|
|
30
|
+
/** True when a naive local datetime needs the server-side historical path. */
|
|
31
|
+
export function needsHistoricalResolution(dt, coords) {
|
|
32
|
+
const value = (dt ?? "").trim();
|
|
33
|
+
if (!isNaiveClockTime(value) || hasZoneSuffix(value))
|
|
34
|
+
return false;
|
|
35
|
+
const year = Number(value.slice(0, 4));
|
|
36
|
+
return (Number.isFinite(year) &&
|
|
37
|
+
year < TZDATA_AUTHORITATIVE_FROM_YEAR &&
|
|
38
|
+
typeof coords?.latitude === "number" &&
|
|
39
|
+
typeof coords?.longitude === "number");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `localToUtcIso`, but historically correct when it matters.
|
|
43
|
+
*
|
|
44
|
+
* Same contract as `localToUtcIso` (naive value with no `tz` throws the
|
|
45
|
+
* ambiguous-datetime error; zone-suffixed and date-only values pass through).
|
|
46
|
+
* For a pre-1970 naive value with known coordinates, the conversion is done
|
|
47
|
+
* by the API's `/timezone/offset` `datetime_local` mode so the historical
|
|
48
|
+
* correction overlay applies. Everything else — including every post-1970
|
|
49
|
+
* conversion — stays on the local, zero-latency Intl path.
|
|
50
|
+
*/
|
|
51
|
+
export async function localToUtcIsoHistorical(field, dt, tz, coords, timezoneField = "timezone") {
|
|
52
|
+
const value = (dt ?? "").trim();
|
|
53
|
+
if (!needsHistoricalResolution(value, coords) || !tz || tz.trim() === "") {
|
|
54
|
+
// Includes the naive-without-tz case, which must throw the same
|
|
55
|
+
// ambiguous-datetime error localToUtcIso throws.
|
|
56
|
+
return localToUtcIso(field, dt, tz, timezoneField);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const res = (await getActiveClient().request("POST", "/timezone/offset", {
|
|
60
|
+
data: {
|
|
61
|
+
lat: coords.latitude,
|
|
62
|
+
lon: coords.longitude,
|
|
63
|
+
datetime_local: value,
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
// Respect an explicit caller zone: the server resolves the zone from
|
|
67
|
+
// the coordinates, so only trust its instant when the zones agree.
|
|
68
|
+
if (res?.resolved_utc && (!res.timezone || res.timezone === tz.trim())) {
|
|
69
|
+
return res.resolved_utc;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Server unreachable or rejected the request — fall through to the
|
|
74
|
+
// local conversion rather than failing the chart call.
|
|
75
|
+
}
|
|
76
|
+
return localToUtcIso(field, dt, tz, timezoneField);
|
|
77
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -57,7 +57,11 @@ export interface ToolDefinition {
|
|
|
57
57
|
* multi-tenant HTTP server cannot vary it per session, and an unregistered
|
|
58
58
|
* tool becomes uncallable rather than merely hidden.
|
|
59
59
|
*/
|
|
60
|
-
export type ToolSurface = "core" | "full";
|
|
60
|
+
export type ToolSurface = "core" | "full" | ToolsetSurface;
|
|
61
|
+
/** A caller-chosen set of traditions, e.g. `?profile=hd,bazi`. */
|
|
62
|
+
export interface ToolsetSurface {
|
|
63
|
+
readonly toolsets: readonly string[];
|
|
64
|
+
}
|
|
61
65
|
/**
|
|
62
66
|
* The default advertised surface: everyday astrology work, one tool per job.
|
|
63
67
|
*
|
|
@@ -80,6 +84,28 @@ export type ToolSurface = "core" | "full";
|
|
|
80
84
|
* explore_natal_chart are present. Do not remove those four.
|
|
81
85
|
*/
|
|
82
86
|
export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
|
|
87
|
+
/**
|
|
88
|
+
* Tool surfaces grouped by tradition, so a caller pays context only for the
|
|
89
|
+
* work they actually do: `?profile=hd,bazi` on HTTP, `OPENEPHEMERIS_TOOLS=hd`
|
|
90
|
+
* on stdio. BASE_TOOLSET is always included.
|
|
91
|
+
*
|
|
92
|
+
* This is the same *view* filter as `core`/`full` — nothing is unregistered,
|
|
93
|
+
* every tool stays callable by name — so a toolset is a context-budget choice,
|
|
94
|
+
* never a capability one.
|
|
95
|
+
*
|
|
96
|
+
* Why this exists: the core surface is re-sent on every model pass and sat at
|
|
97
|
+
* its ceiling with ~0 headroom, and the only levers left were deleting the
|
|
98
|
+
* disambiguation prose that makes tool selection work. Grouping by tradition
|
|
99
|
+
* is the structural fix — a Human Design user should not pay for Venus Star
|
|
100
|
+
* Points on every message.
|
|
101
|
+
*
|
|
102
|
+
* Names here are asserted against the registry by a test; a typo fails the
|
|
103
|
+
* build rather than silently narrowing someone's surface.
|
|
104
|
+
*/
|
|
105
|
+
export declare const TOOLSETS: Readonly<Record<string, readonly string[]>>;
|
|
106
|
+
export declare const TOOLSET_NAMES: readonly string[];
|
|
107
|
+
/** Resolve a toolset selection to the tool names it advertises. */
|
|
108
|
+
export declare function toolsetSelectionNames(names: readonly string[]): Set<string>;
|
|
83
109
|
/**
|
|
84
110
|
* Returns tools that should be exposed to Claude in the ListTools response.
|
|
85
111
|
* Filters out tools that have visibility restricted to the UI (app-only),
|
|
@@ -87,8 +113,21 @@ export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
|
|
|
87
113
|
* "core" — everything outside CORE_TOOL_NAMES.
|
|
88
114
|
*/
|
|
89
115
|
export declare function modelVisibleTools(transport?: "stdio" | "http", surface?: ToolSurface): ToolDefinition[];
|
|
90
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Parses a caller-supplied surface hint.
|
|
118
|
+
*
|
|
119
|
+
* "full" -> every model-visible tool
|
|
120
|
+
* "hd" | "hd,bazi" -> those traditions plus BASE_TOOLSET
|
|
121
|
+
* anything else -> "core" (the curated default)
|
|
122
|
+
*
|
|
123
|
+
* Unknown names in a comma list are ignored rather than rejected: a typo
|
|
124
|
+
* degrades to a smaller surface, never to an error at `initialize` that would
|
|
125
|
+
* leave the connector dead. If NO name in the list is recognised, fall back to
|
|
126
|
+
* "core" so a mistyped profile can't strand a caller on BASE alone.
|
|
127
|
+
*/
|
|
91
128
|
export declare function parseToolSurface(raw: unknown): ToolSurface;
|
|
129
|
+
/** Stable label for logs/analytics — an object surface has no useful toString. */
|
|
130
|
+
export declare function describeSurface(surface: ToolSurface): string;
|
|
92
131
|
export declare const toolRegistry: Record<string, ToolDefinition>;
|
|
93
132
|
export declare function registerTool(tool: ToolDefinition): void;
|
|
94
133
|
export type ToolProfile = "dev" | "legacy";
|
package/dist/tools/index.js
CHANGED
|
@@ -12,6 +12,9 @@ export const SERVER_VERSION = (() => {
|
|
|
12
12
|
return "0.0.0-unknown";
|
|
13
13
|
}
|
|
14
14
|
})();
|
|
15
|
+
function isToolsetSurface(s) {
|
|
16
|
+
return typeof s === "object" && Array.isArray(s.toolsets);
|
|
17
|
+
}
|
|
15
18
|
/**
|
|
16
19
|
* The default advertised surface: everyday astrology work, one tool per job.
|
|
17
20
|
*
|
|
@@ -53,6 +56,13 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
53
56
|
"ephemeris_transits",
|
|
54
57
|
"ephemeris_synastry",
|
|
55
58
|
"ephemeris_retrograde_status",
|
|
59
|
+
// Promoted 2026-08-02 on usage: both were reachable only via profile=full,
|
|
60
|
+
// yet ephemeris_aspect_check drew 19 calls from 3 users over 90 days (more
|
|
61
|
+
// than 20 of the tools already in this set) and ephemeris_angles_points drew
|
|
62
|
+
// 9. People found them while they were hidden; advertising them is cheap
|
|
63
|
+
// (202 and 227 tokens) and earns its place.
|
|
64
|
+
"ephemeris_aspect_check",
|
|
65
|
+
"ephemeris_angles_points",
|
|
56
66
|
"ephemeris_next_eclipse",
|
|
57
67
|
"ephemeris_relocation",
|
|
58
68
|
"ephemeris_progressed_chart",
|
|
@@ -60,13 +70,31 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
60
70
|
// Traditions
|
|
61
71
|
"human_design_chart",
|
|
62
72
|
"vedic_chart",
|
|
63
|
-
"chinese_bazi",
|
|
64
73
|
"bazi_annual_pillar",
|
|
74
|
+
// chinese_bazi demoted 2026-08-02: explore_bazi_chart POSTs the same
|
|
75
|
+
// /chinese/bazi at the same 1-credit base and keeps every substantive field
|
|
76
|
+
// (year/month/day/hour pillars + day_master), dropping only the `metadata`
|
|
77
|
+
// and `success` envelope keys — verified against a live response, not
|
|
78
|
+
// inferred from the names. It also degrades to a text summary on hosts that
|
|
79
|
+
// cannot render the iframe, so nothing is lost without an MCP Apps host.
|
|
80
|
+
// chinese_bazi drew 0 calls in 90 days while advertised; explore_bazi_chart
|
|
81
|
+
// drew 3. Still registered and callable by name, and still in profile=full.
|
|
82
|
+
//
|
|
83
|
+
// NOTE: this reasoning does NOT extend to the sibling pairs. vedic_chart (1
|
|
84
|
+
// credit) vs explore_vedic_chart (3), and human_design_chart (2 credits, full
|
|
85
|
+
// response) vs explore_human_design (4 credits, and buildHdModelPayload drops
|
|
86
|
+
// activations/design/personality/strategy/variables) are not folds — they
|
|
87
|
+
// would cost users money and data. Verify in the handler before adding to
|
|
88
|
+
// this list.
|
|
65
89
|
// Timing
|
|
66
90
|
"ephemeris_electional",
|
|
67
91
|
"electional_moment_analysis",
|
|
68
92
|
"electional_station_tracker",
|
|
69
|
-
|
|
93
|
+
// electional_angle_crossings demoted 2026-08-02: 715 tokens (the fattest
|
|
94
|
+
// non-proxy tool in this set) for 0 calls in 90 days, and at 5 credits it is
|
|
95
|
+
// advanced-practitioner work rather than an everyday ask. Three electional
|
|
96
|
+
// entry points remain advertised; this one is a profile=full / skill-named
|
|
97
|
+
// tool now.
|
|
70
98
|
// Astrocartography
|
|
71
99
|
"acg_power_lines",
|
|
72
100
|
"acg_hits",
|
|
@@ -83,6 +111,141 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
83
111
|
"auth_status",
|
|
84
112
|
"auth_logout",
|
|
85
113
|
]);
|
|
114
|
+
/**
|
|
115
|
+
* Tools every session needs regardless of tradition: turning a place name into
|
|
116
|
+
* coordinates, checking credits, and the allowlist-gated escape hatch. Always
|
|
117
|
+
* unioned into a toolset selection — a caller who asks for `hd` still has to be
|
|
118
|
+
* able to resolve a birthplace, or they are back to recalling coordinates from
|
|
119
|
+
* memory, which is the bug BASE exists to prevent.
|
|
120
|
+
*/
|
|
121
|
+
const BASE_TOOLSET = [
|
|
122
|
+
"location_search",
|
|
123
|
+
"timezone_resolve",
|
|
124
|
+
"account_usage",
|
|
125
|
+
"dev_read_api",
|
|
126
|
+
// The write half of the escape hatch. 26 of the 31 allowlisted operations
|
|
127
|
+
// are POST — BASE without dev_write_api would strand a toolset caller with
|
|
128
|
+
// only the 5 GET proxies. (Deliberately NOT advertised in `core`, where the
|
|
129
|
+
// curated typed tools cover the write surface; a toolset selection has no
|
|
130
|
+
// such coverage, so it needs the proxy whole.)
|
|
131
|
+
"dev_write_api",
|
|
132
|
+
"dev_list_allowed",
|
|
133
|
+
"auth_login",
|
|
134
|
+
"auth_status",
|
|
135
|
+
"auth_logout",
|
|
136
|
+
];
|
|
137
|
+
/**
|
|
138
|
+
* Tool surfaces grouped by tradition, so a caller pays context only for the
|
|
139
|
+
* work they actually do: `?profile=hd,bazi` on HTTP, `OPENEPHEMERIS_TOOLS=hd`
|
|
140
|
+
* on stdio. BASE_TOOLSET is always included.
|
|
141
|
+
*
|
|
142
|
+
* This is the same *view* filter as `core`/`full` — nothing is unregistered,
|
|
143
|
+
* every tool stays callable by name — so a toolset is a context-budget choice,
|
|
144
|
+
* never a capability one.
|
|
145
|
+
*
|
|
146
|
+
* Why this exists: the core surface is re-sent on every model pass and sat at
|
|
147
|
+
* its ceiling with ~0 headroom, and the only levers left were deleting the
|
|
148
|
+
* disambiguation prose that makes tool selection work. Grouping by tradition
|
|
149
|
+
* is the structural fix — a Human Design user should not pay for Venus Star
|
|
150
|
+
* Points on every message.
|
|
151
|
+
*
|
|
152
|
+
* Names here are asserted against the registry by a test; a typo fails the
|
|
153
|
+
* build rather than silently narrowing someone's surface.
|
|
154
|
+
*/
|
|
155
|
+
export const TOOLSETS = {
|
|
156
|
+
/** Western natal/predictive work — the default tradition. */
|
|
157
|
+
astrology: [
|
|
158
|
+
"ephemeris_natal_chart",
|
|
159
|
+
"explore_natal_chart",
|
|
160
|
+
"ephemeris_planet_position",
|
|
161
|
+
"ephemeris_house_cusps",
|
|
162
|
+
"ephemeris_angles_points",
|
|
163
|
+
"ephemeris_aspect_check",
|
|
164
|
+
"ephemeris_transits",
|
|
165
|
+
"ephemeris_natal_transits",
|
|
166
|
+
"explore_transit_timeline",
|
|
167
|
+
"ephemeris_synastry",
|
|
168
|
+
"explore_bi_wheel",
|
|
169
|
+
"bi_wheel_synopsis",
|
|
170
|
+
"ephemeris_retrograde_status",
|
|
171
|
+
"ephemeris_progressed_chart",
|
|
172
|
+
"ephemeris_solar_return",
|
|
173
|
+
"ephemeris_lunar_return",
|
|
174
|
+
"ephemeris_planetary_return",
|
|
175
|
+
"ephemeris_relocation",
|
|
176
|
+
"ephemeris_dignities",
|
|
177
|
+
"ephemeris_midpoints",
|
|
178
|
+
"ephemeris_hermetic_lots",
|
|
179
|
+
"ephemeris_fixed_stars",
|
|
180
|
+
"ephemeris_composite",
|
|
181
|
+
"ephemeris_composite_midpoint",
|
|
182
|
+
"ephemeris_overlay",
|
|
183
|
+
"ephemeris_chart_wheel",
|
|
184
|
+
"ephemeris_bi_wheel",
|
|
185
|
+
"ephemeris_natal_batch",
|
|
186
|
+
],
|
|
187
|
+
/** Lunar phase, void-of-course and eclipse work. */
|
|
188
|
+
moon: [
|
|
189
|
+
"ephemeris_moon_phase",
|
|
190
|
+
"explore_moon_phase",
|
|
191
|
+
"ephemeris_next_lunar_phase",
|
|
192
|
+
"ephemeris_next_eclipse",
|
|
193
|
+
],
|
|
194
|
+
/** Human Design. */
|
|
195
|
+
hd: [
|
|
196
|
+
"human_design_chart",
|
|
197
|
+
"explore_human_design",
|
|
198
|
+
"explore_human_design_transit",
|
|
199
|
+
"explore_human_design_connection",
|
|
200
|
+
"human_design_bodygraph",
|
|
201
|
+
"human_design_composite",
|
|
202
|
+
"human_design_penta",
|
|
203
|
+
"hd_opposition",
|
|
204
|
+
"hd_planetary_return",
|
|
205
|
+
],
|
|
206
|
+
/** Chinese BaZi / Four Pillars. */
|
|
207
|
+
bazi: [
|
|
208
|
+
"explore_bazi_chart",
|
|
209
|
+
"chinese_bazi",
|
|
210
|
+
"bazi_chart",
|
|
211
|
+
"bazi_ten_gods",
|
|
212
|
+
"bazi_element_balance",
|
|
213
|
+
"bazi_luck_pillars",
|
|
214
|
+
"bazi_annual_pillar",
|
|
215
|
+
"bazi_compatibility",
|
|
216
|
+
],
|
|
217
|
+
/** Vedic / Jyotish. */
|
|
218
|
+
vedic: ["vedic_chart", "explore_vedic_chart"],
|
|
219
|
+
/** Astrocartography. */
|
|
220
|
+
acg: ["acg_hits", "acg_power_lines"],
|
|
221
|
+
/** Electional timing. */
|
|
222
|
+
electional: [
|
|
223
|
+
"ephemeris_electional",
|
|
224
|
+
"electional_moment_analysis",
|
|
225
|
+
"electional_station_tracker",
|
|
226
|
+
"electional_angle_crossings",
|
|
227
|
+
"electional_aspect_search",
|
|
228
|
+
],
|
|
229
|
+
/** Venus Star Point / synodic-cycle work. */
|
|
230
|
+
venus: [
|
|
231
|
+
"venus_star_points",
|
|
232
|
+
"venus_star_points_conjunctions",
|
|
233
|
+
"venus_phase",
|
|
234
|
+
"venus_elongations",
|
|
235
|
+
"venus_stations",
|
|
236
|
+
"venus_eight_year_star",
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
export const TOOLSET_NAMES = Object.keys(TOOLSETS);
|
|
240
|
+
/** Resolve a toolset selection to the tool names it advertises. */
|
|
241
|
+
export function toolsetSelectionNames(names) {
|
|
242
|
+
const out = new Set(BASE_TOOLSET);
|
|
243
|
+
for (const n of names) {
|
|
244
|
+
for (const t of TOOLSETS[n] ?? [])
|
|
245
|
+
out.add(t);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
86
249
|
/**
|
|
87
250
|
* Returns tools that should be exposed to Claude in the ListTools response.
|
|
88
251
|
* Filters out tools that have visibility restricted to the UI (app-only),
|
|
@@ -90,6 +253,9 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
90
253
|
* "core" — everything outside CORE_TOOL_NAMES.
|
|
91
254
|
*/
|
|
92
255
|
export function modelVisibleTools(transport = "stdio", surface = "full") {
|
|
256
|
+
const allowed = isToolsetSurface(surface)
|
|
257
|
+
? toolsetSelectionNames(surface.toolsets)
|
|
258
|
+
: null;
|
|
93
259
|
return Object.values(toolRegistry).filter((t) => {
|
|
94
260
|
if (transport === "http" && t.stdioOnly)
|
|
95
261
|
return false;
|
|
@@ -97,14 +263,45 @@ export function modelVisibleTools(transport = "stdio", surface = "full") {
|
|
|
97
263
|
// If visibility is explicitly ["app"] (no "model"), hide from model
|
|
98
264
|
if (vis && !vis.includes("model") && vis.includes("app"))
|
|
99
265
|
return false;
|
|
266
|
+
if (allowed)
|
|
267
|
+
return allowed.has(t.name);
|
|
100
268
|
if (surface === "core" && !CORE_TOOL_NAMES.has(t.name))
|
|
101
269
|
return false;
|
|
102
270
|
return true;
|
|
103
271
|
});
|
|
104
272
|
}
|
|
105
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* Parses a caller-supplied surface hint.
|
|
275
|
+
*
|
|
276
|
+
* "full" -> every model-visible tool
|
|
277
|
+
* "hd" | "hd,bazi" -> those traditions plus BASE_TOOLSET
|
|
278
|
+
* anything else -> "core" (the curated default)
|
|
279
|
+
*
|
|
280
|
+
* Unknown names in a comma list are ignored rather than rejected: a typo
|
|
281
|
+
* degrades to a smaller surface, never to an error at `initialize` that would
|
|
282
|
+
* leave the connector dead. If NO name in the list is recognised, fall back to
|
|
283
|
+
* "core" so a mistyped profile can't strand a caller on BASE alone.
|
|
284
|
+
*/
|
|
106
285
|
export function parseToolSurface(raw) {
|
|
107
|
-
|
|
286
|
+
if (typeof raw !== "string")
|
|
287
|
+
return "core";
|
|
288
|
+
const value = raw.trim().toLowerCase();
|
|
289
|
+
if (value === "full")
|
|
290
|
+
return "full";
|
|
291
|
+
if (value === "" || value === "core")
|
|
292
|
+
return "core";
|
|
293
|
+
const requested = value
|
|
294
|
+
.split(",")
|
|
295
|
+
.map((s) => s.trim())
|
|
296
|
+
.filter(Boolean);
|
|
297
|
+
const known = requested.filter((n) => Object.hasOwn(TOOLSETS, n));
|
|
298
|
+
if (known.length === 0)
|
|
299
|
+
return "core";
|
|
300
|
+
return { toolsets: known };
|
|
301
|
+
}
|
|
302
|
+
/** Stable label for logs/analytics — an object surface has no useful toString. */
|
|
303
|
+
export function describeSurface(surface) {
|
|
304
|
+
return isToolsetSurface(surface) ? surface.toolsets.join("+") : surface;
|
|
108
305
|
}
|
|
109
306
|
export const toolRegistry = {};
|
|
110
307
|
export function registerTool(tool) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
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
|
-
import { DATETIME_DESC, TIMEZONE_PROPERTY
|
|
4
|
+
import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
|
|
5
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
5
6
|
const ACG_BODY_DESCRIPTION = "List of celestial bodies for line calculation. " +
|
|
6
7
|
"E.g. ['Sun', 'Moon', 'Venus', 'Mars', 'Jupiter', 'Saturn']. " +
|
|
7
8
|
"Aliases: 'NorthNode'/'Node'/'Rahu' → MeanNode, 'SouthNode'/'Ketu' → SouthNode. " +
|
|
@@ -67,7 +68,7 @@ registerTool({
|
|
|
67
68
|
birthplace_lat: args.birth_latitude,
|
|
68
69
|
birthplace_lon: args.birth_longitude,
|
|
69
70
|
},
|
|
70
|
-
epoch:
|
|
71
|
+
epoch: await localToUtcIsoHistorical("birth_datetime", args.birth_datetime, args.timezone, { latitude: args.birth_latitude, longitude: args.birth_longitude }),
|
|
71
72
|
};
|
|
72
73
|
if (args.bodies)
|
|
73
74
|
body.bodies = args.bodies;
|
|
@@ -164,7 +165,7 @@ registerTool({
|
|
|
164
165
|
birthplace_lat: args.birth_latitude,
|
|
165
166
|
birthplace_lon: args.birth_longitude,
|
|
166
167
|
},
|
|
167
|
-
epoch:
|
|
168
|
+
epoch: await localToUtcIsoHistorical("birth_datetime", args.birth_datetime, args.timezone, { latitude: args.birth_latitude, longitude: args.birth_longitude }),
|
|
168
169
|
query_lat: args.query_latitude,
|
|
169
170
|
query_lon: args.query_longitude,
|
|
170
171
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { registerTool, validateRequired, pickEnum } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
|
-
import { DATETIME_DESC, TIMEZONE_PROPERTY
|
|
4
|
+
import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
|
|
5
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
5
6
|
registerTool({
|
|
6
7
|
name: "human_design_bodygraph",
|
|
7
8
|
description: "Generate a Human Design Bodygraph image (SVG) from a birth datetime (UTC). " +
|
|
@@ -12,7 +13,7 @@ registerTool({
|
|
|
12
13
|
"For a user-facing interactive bodygraph explorer, use explore_human_design instead.\n\n" +
|
|
13
14
|
"CREDIT COST: 2 credits per call.\n\n" +
|
|
14
15
|
"EXAMPLE (local birth time + zone): born 15 April 1990 at 14:30 in Chicago:\n" +
|
|
15
|
-
" datetime='1990-04-15T14:30:00', timezone='America/Chicago'\n" +
|
|
16
|
+
" datetime='1990-04-15T14:30:00', timezone='America/Chicago', latitude=41.8781, longitude=-87.6298\n" +
|
|
16
17
|
"EXAMPLE (already in UTC):\n" +
|
|
17
18
|
" datetime='1990-04-15T19:30:00Z'",
|
|
18
19
|
inputSchema: {
|
|
@@ -24,6 +25,14 @@ registerTool({
|
|
|
24
25
|
" Human Design is time-sensitive — accuracy to the minute matters.",
|
|
25
26
|
},
|
|
26
27
|
timezone: TIMEZONE_PROPERTY,
|
|
28
|
+
latitude: {
|
|
29
|
+
type: "number",
|
|
30
|
+
description: "Optional birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times (e.g. state-level DST deviations); has no effect on the bodygraph itself.",
|
|
31
|
+
},
|
|
32
|
+
longitude: {
|
|
33
|
+
type: "number",
|
|
34
|
+
description: "Optional birth longitude in decimal degrees. See latitude.",
|
|
35
|
+
},
|
|
27
36
|
style: {
|
|
28
37
|
type: "string",
|
|
29
38
|
enum: ["light", "dark", "mono"],
|
|
@@ -44,7 +53,7 @@ registerTool({
|
|
|
44
53
|
handler: async (args) => {
|
|
45
54
|
validateRequired(args, ["datetime"]);
|
|
46
55
|
const body = {
|
|
47
|
-
birth_datetime_utc:
|
|
56
|
+
birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
|
|
48
57
|
};
|
|
49
58
|
const format = pickEnum(args.format, ["svg", "png"]) || "svg";
|
|
50
59
|
const style = pickEnum(args.style, ["light", "dark", "mono"]);
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
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
|
-
import { DATETIME_DESC, TIMEZONE_PROPERTY
|
|
4
|
+
import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
|
|
5
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
6
|
+
/** Optional birth coordinates, present only to feed the historical timezone
|
|
7
|
+
* correction for pre-1970 naive local birth times — not sent to the Go
|
|
8
|
+
* endpoint, which is non-relocational. */
|
|
9
|
+
const OPTIONAL_BIRTH_COORDS = {
|
|
10
|
+
latitude: {
|
|
11
|
+
type: "number",
|
|
12
|
+
description: "Optional birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times; has no other effect.",
|
|
13
|
+
},
|
|
14
|
+
longitude: {
|
|
15
|
+
type: "number",
|
|
16
|
+
description: "Optional birth longitude in decimal degrees. See latitude.",
|
|
17
|
+
},
|
|
18
|
+
};
|
|
5
19
|
registerTool({
|
|
6
20
|
name: "hd_planetary_return",
|
|
7
21
|
description: "Calculate a Human Design planetary return chart — full HD chart " +
|
|
@@ -29,6 +43,7 @@ registerTool({
|
|
|
29
43
|
description: DATETIME_DESC,
|
|
30
44
|
},
|
|
31
45
|
timezone: TIMEZONE_PROPERTY,
|
|
46
|
+
...OPTIONAL_BIRTH_COORDS,
|
|
32
47
|
return_year: {
|
|
33
48
|
type: "integer",
|
|
34
49
|
description: "Year to find the return near (e.g. 2020 for a Saturn return at ~age 29).",
|
|
@@ -49,7 +64,7 @@ registerTool({
|
|
|
49
64
|
return await getActiveClient().request("POST", "/human-design/cycles/return", {
|
|
50
65
|
data: {
|
|
51
66
|
planet: args.planet,
|
|
52
|
-
birth_datetime_utc:
|
|
67
|
+
birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
|
|
53
68
|
return_year: args.return_year,
|
|
54
69
|
format: args.format,
|
|
55
70
|
include_chiron: args.include_chiron,
|
|
@@ -82,6 +97,7 @@ registerTool({
|
|
|
82
97
|
description: DATETIME_DESC,
|
|
83
98
|
},
|
|
84
99
|
timezone: TIMEZONE_PROPERTY,
|
|
100
|
+
...OPTIONAL_BIRTH_COORDS,
|
|
85
101
|
target_year: {
|
|
86
102
|
type: "integer",
|
|
87
103
|
description: "Year to search for the opposition near.",
|
|
@@ -102,7 +118,7 @@ registerTool({
|
|
|
102
118
|
return await getActiveClient().request("POST", "/human-design/cycles/opposition", {
|
|
103
119
|
data: {
|
|
104
120
|
planet: args.planet,
|
|
105
|
-
birth_datetime_utc:
|
|
121
|
+
birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
|
|
106
122
|
target_year: args.target_year,
|
|
107
123
|
format: args.format,
|
|
108
124
|
include_chiron: args.include_chiron,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
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
|
-
import { DATETIME_DESC,
|
|
4
|
+
import { DATETIME_DESC, timezoneProperty } from "../datetime.js";
|
|
5
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
5
6
|
// POST /human-design/composite — OE-027
|
|
6
7
|
registerTool({
|
|
7
8
|
name: "human_design_composite",
|
|
@@ -21,11 +22,27 @@ registerTool({
|
|
|
21
22
|
description: DATETIME_DESC,
|
|
22
23
|
},
|
|
23
24
|
person_a_timezone: timezoneProperty("Person A's birth location"),
|
|
25
|
+
person_a_latitude: {
|
|
26
|
+
type: "number",
|
|
27
|
+
description: "Optional Person A birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times; has no other effect.",
|
|
28
|
+
},
|
|
29
|
+
person_a_longitude: {
|
|
30
|
+
type: "number",
|
|
31
|
+
description: "Optional Person A birth longitude in decimal degrees. See person_a_latitude.",
|
|
32
|
+
},
|
|
24
33
|
person_b_datetime: {
|
|
25
34
|
type: "string",
|
|
26
35
|
description: DATETIME_DESC,
|
|
27
36
|
},
|
|
28
37
|
person_b_timezone: timezoneProperty("Person B's birth location", "America/Los_Angeles"),
|
|
38
|
+
person_b_latitude: {
|
|
39
|
+
type: "number",
|
|
40
|
+
description: "Optional Person B birth latitude in decimal degrees. See person_a_latitude.",
|
|
41
|
+
},
|
|
42
|
+
person_b_longitude: {
|
|
43
|
+
type: "number",
|
|
44
|
+
description: "Optional Person B birth longitude in decimal degrees.",
|
|
45
|
+
},
|
|
29
46
|
format: {
|
|
30
47
|
type: "string",
|
|
31
48
|
enum: ["json", "llm"],
|
|
@@ -41,10 +58,10 @@ registerTool({
|
|
|
41
58
|
validateRequired(args, ["person_a_datetime", "person_b_datetime"]);
|
|
42
59
|
const body = {
|
|
43
60
|
subject_1: {
|
|
44
|
-
birth_datetime_utc:
|
|
61
|
+
birth_datetime_utc: await localToUtcIsoHistorical("person_a_datetime", args.person_a_datetime, args.person_a_timezone, { latitude: args.person_a_latitude, longitude: args.person_a_longitude }, "person_a_timezone"),
|
|
45
62
|
},
|
|
46
63
|
subject_2: {
|
|
47
|
-
birth_datetime_utc:
|
|
64
|
+
birth_datetime_utc: await localToUtcIsoHistorical("person_b_datetime", args.person_b_datetime, args.person_b_timezone, { latitude: args.person_b_latitude, longitude: args.person_b_longitude }, "person_b_timezone"),
|
|
48
65
|
},
|
|
49
66
|
};
|
|
50
67
|
if (args.format)
|
|
@@ -81,6 +98,14 @@ registerTool({
|
|
|
81
98
|
name: { type: "string", description: "Member's name." },
|
|
82
99
|
datetime: { type: "string", description: DATETIME_DESC },
|
|
83
100
|
timezone: timezoneProperty("this member's birth location"),
|
|
101
|
+
latitude: {
|
|
102
|
+
type: "number",
|
|
103
|
+
description: "Optional birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times; has no other effect.",
|
|
104
|
+
},
|
|
105
|
+
longitude: {
|
|
106
|
+
type: "number",
|
|
107
|
+
description: "Optional birth longitude in decimal degrees. See latitude.",
|
|
108
|
+
},
|
|
84
109
|
},
|
|
85
110
|
required: ["id", "name", "datetime"],
|
|
86
111
|
},
|
|
@@ -101,13 +126,13 @@ registerTool({
|
|
|
101
126
|
annotations: { title: "HD Penta Group Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
102
127
|
handler: async (args) => {
|
|
103
128
|
validateRequired(args, ["group_name", "members"]);
|
|
104
|
-
const members = args.members.map((m, i) => ({
|
|
129
|
+
const members = await Promise.all(args.members.map(async (m, i) => ({
|
|
105
130
|
id: m.id,
|
|
106
131
|
name: m.name,
|
|
107
132
|
birth_data: {
|
|
108
|
-
birth_datetime_utc:
|
|
133
|
+
birth_datetime_utc: await localToUtcIsoHistorical(`members[${i}].datetime`, m.datetime, m.timezone, { latitude: m.latitude, longitude: m.longitude }, `members[${i}].timezone`),
|
|
109
134
|
},
|
|
110
|
-
}));
|
|
135
|
+
})));
|
|
111
136
|
const body = {
|
|
112
137
|
group_name: args.group_name,
|
|
113
138
|
members,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { registerTool, validateRequired } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_IMAGE_AND_JSON } from "../output-schemas.js";
|
|
4
|
-
import { DATETIME_DESC, TIMEZONE_PROPERTY
|
|
4
|
+
import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
|
|
5
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
5
6
|
registerTool({
|
|
6
7
|
name: "human_design_chart",
|
|
7
8
|
description: "Calculate a full Human Design I Ching hexagram chart from birth data. Returns the person's Type " +
|
|
@@ -65,7 +66,7 @@ registerTool({
|
|
|
65
66
|
handler: async (args) => {
|
|
66
67
|
validateRequired(args, ["datetime"]);
|
|
67
68
|
const body = {
|
|
68
|
-
birth_datetime_utc:
|
|
69
|
+
birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
|
|
69
70
|
};
|
|
70
71
|
if (args.latitude != null)
|
|
71
72
|
body.latitude = args.latitude;
|