@openephemeris/mcp-server 3.24.0 → 4.0.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 +97 -0
- package/LICENSE +21 -21
- package/README.md +40 -2
- package/dist/analytics.js +37 -5
- package/dist/backend/client.d.ts +7 -0
- package/dist/backend/client.js +39 -38
- package/dist/index.js +64 -2
- package/dist/oauth/session-utils.d.ts +42 -18
- package/dist/oauth/session-utils.js +79 -0
- package/dist/server-sse.js +114 -14
- package/dist/tools/apps/bazi-app.js +12 -26
- package/dist/tools/apps/bi-wheel-app.js +2 -2
- package/dist/tools/apps/bodygraph-app.d.ts +5 -5
- package/dist/tools/apps/bodygraph-app.js +157 -210
- package/dist/tools/apps/chart-wheel-app.js +5 -3
- package/dist/tools/apps/location-tools.js +167 -18
- package/dist/tools/apps/moon-phase-app.js +10 -3
- package/dist/tools/apps/transit-timeline-app.js +6 -4
- package/dist/tools/apps/vedic-chart-app.js +15 -49
- package/dist/tools/datetime.d.ts +65 -0
- package/dist/tools/datetime.js +153 -0
- package/dist/tools/index.d.ts +45 -2
- package/dist/tools/index.js +78 -2
- package/dist/tools/specialized/account.d.ts +1 -0
- package/dist/tools/specialized/account.js +100 -0
- package/dist/tools/specialized/acg.js +16 -14
- package/dist/tools/specialized/bazi.d.ts +7 -1
- package/dist/tools/specialized/bazi.js +86 -19
- package/dist/tools/specialized/bi_wheel.js +5 -4
- package/dist/tools/specialized/chart_wheel.js +5 -8
- package/dist/tools/specialized/comparative.js +13 -5
- package/dist/tools/specialized/electional.js +7 -7
- package/dist/tools/specialized/ephemeris_core.js +13 -8
- package/dist/tools/specialized/ephemeris_extended.js +27 -17
- package/dist/tools/specialized/hd_bodygraph.js +8 -10
- package/dist/tools/specialized/hd_cycles.js +7 -14
- package/dist/tools/specialized/hd_group.js +20 -13
- package/dist/tools/specialized/human_design.js +11 -17
- package/dist/tools/specialized/moon.js +8 -2
- package/dist/tools/specialized/natal.js +7 -9
- package/dist/tools/specialized/progressed.js +12 -8
- package/dist/tools/specialized/relocation.js +9 -3
- package/dist/tools/specialized/returns.js +23 -11
- package/dist/tools/specialized/synastry.js +17 -6
- package/dist/tools/specialized/transits.js +9 -5
- package/dist/tools/specialized/vedic.js +5 -3
- package/dist/tools/specialized/venus_star_points.js +14 -9
- package/dist/ui/bazi.html +1063 -1049
- package/dist/ui/bi-wheel.html +4188 -4128
- package/dist/ui/bodygraph.html +3673 -3616
- package/dist/ui/chart-wheel.html +3769 -3713
- package/dist/ui/moon-phase.html +3219 -3153
- package/dist/ui/transit-timeline.html +199 -170
- package/dist/ui/vedic-chart.html +1116 -1098
- package/package.json +3 -2
- package/smithery.yaml +1 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The datetime contract, stated once for the whole tool surface.
|
|
3
|
+
*
|
|
4
|
+
* A datetime that names a clock time but not a zone does not name an instant.
|
|
5
|
+
* The API used to read such a value as UTC, so a birth time of 09:01 in Chicago
|
|
6
|
+
* was computed as 09:01 UTC — five hours early. Every planet keeps its sign at
|
|
7
|
+
* that error scale, so the answer looked right while the Ascendant was two signs
|
|
8
|
+
* off and every house placement was wrong.
|
|
9
|
+
*
|
|
10
|
+
* The rules, identical here and in the Go engine
|
|
11
|
+
* (apps/api/go-sidecar/internal/api/handlers/datetime_contract.go):
|
|
12
|
+
*
|
|
13
|
+
* 1. A datetime WITH a clock time must carry its zone — either a `Z`/`±HH:MM`
|
|
14
|
+
* suffix on the string, or a companion `timezone` argument.
|
|
15
|
+
* 2. A date-only value ("1987-07-15") has no clock time to be ambiguous about
|
|
16
|
+
* and resolves to 12:00 UTC.
|
|
17
|
+
* 3. Nothing in this layer ever appends a `Z` to a zone-less value. Doing so
|
|
18
|
+
* asserts a fact the caller never stated.
|
|
19
|
+
*
|
|
20
|
+
* Violations are rejected here rather than at the API so the model gets the
|
|
21
|
+
* correction immediately and no credit is spent on a chart that is wrong.
|
|
22
|
+
*/
|
|
23
|
+
/** Matches a `Z`, `±HH:MM`, or `±HHMM` zone suffix. */
|
|
24
|
+
const ZONE_SUFFIX = /([Zz]|[+-]\d{2}:?\d{2})$/;
|
|
25
|
+
/** Matches an ISO value that carries a clock time (as opposed to a bare date). */
|
|
26
|
+
const HAS_CLOCK_TIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
|
|
27
|
+
/** True when `value` states a clock time but not the zone that clock is in. */
|
|
28
|
+
export function isNaiveClockTime(value) {
|
|
29
|
+
const s = (value ?? "").trim();
|
|
30
|
+
return HAS_CLOCK_TIME.test(s) && !ZONE_SUFFIX.test(s);
|
|
31
|
+
}
|
|
32
|
+
/** True when `value` already carries its own zone. */
|
|
33
|
+
export function hasZoneSuffix(value) {
|
|
34
|
+
return ZONE_SUFFIX.test((value ?? "").trim());
|
|
35
|
+
}
|
|
36
|
+
// ─── Canonical parameter documentation ───────────────────────────────────────
|
|
37
|
+
// Every tool that accepts a datetime uses these strings verbatim, so the contract
|
|
38
|
+
// reads identically everywhere. `test/datetime-contract.test.ts` fails if a tool
|
|
39
|
+
// description grows an ISO example that violates it.
|
|
40
|
+
/** The canonical description for a birth / chart-moment datetime parameter. */
|
|
41
|
+
// Kept deliberately terse: this string is repeated on every datetime-accepting
|
|
42
|
+
// tool, so each character costs ~40x across the surface and is re-sent on every
|
|
43
|
+
// model pass. Every RULE stays; the rationale for the rule lives in the module
|
|
44
|
+
// docstring above, which the model never sees.
|
|
45
|
+
export const DATETIME_DESC = "ISO 8601 datetime that states its zone. Either put the zone on the value " +
|
|
46
|
+
"('1987-07-15T09:01:00-05:00', or '...T14:01:00Z' for UTC), or pass local " +
|
|
47
|
+
"wall-clock time plus the `timezone` argument. A zone-less time is REJECTED. " +
|
|
48
|
+
"A date with no time ('1987-07-15') resolves to 12:00 UTC.";
|
|
49
|
+
/** The canonical description for the companion `timezone` parameter. */
|
|
50
|
+
export const TIMEZONE_DESC = "IANA timezone name for the birth/observation location, e.g. 'America/Chicago'. " +
|
|
51
|
+
"Required when the datetime has no 'Z' or ±HH:MM offset; ignored when it does. " +
|
|
52
|
+
"Historical DST is resolved correctly.";
|
|
53
|
+
/** The canonical description for a search-window date parameter (date or datetime). */
|
|
54
|
+
export const WINDOW_DATE_DESC = "ISO 8601 date ('2026-01-01') or a zoned datetime ('2026-01-01T00:00:00Z'). " +
|
|
55
|
+
"A date with no time resolves to 12:00 UTC. A time without a 'Z' or ±HH:MM offset " +
|
|
56
|
+
"is rejected as ambiguous.";
|
|
57
|
+
/** The `timezone` property to spread into a tool's inputSchema. */
|
|
58
|
+
export const TIMEZONE_PROPERTY = {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: TIMEZONE_DESC,
|
|
61
|
+
};
|
|
62
|
+
/** Build a `<prefix>_timezone` property with a tailored example. */
|
|
63
|
+
export function timezoneProperty(label, example = "America/Chicago") {
|
|
64
|
+
return {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: `IANA timezone name for ${label}, e.g. '${example}'. ` +
|
|
67
|
+
"Required when that datetime has no 'Z' or ±HH:MM offset; ignored when it does.",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// ─── Enforcement ─────────────────────────────────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* The rejection message. Names both remedies, rewriting the caller's own value
|
|
73
|
+
* into each — the commonest failure is not realising the value was ambiguous.
|
|
74
|
+
*/
|
|
75
|
+
export function ambiguousDatetimeMessage(field, value, timezoneField = "timezone") {
|
|
76
|
+
const v = (value ?? "").trim();
|
|
77
|
+
return (`\`${field}\` is "${v}", which states a clock time but not the zone that clock is in, ` +
|
|
78
|
+
`so it does not name a moment. OpenEphemeris will not guess. Fix it either way:\n` +
|
|
79
|
+
` 1. Put the zone on the datetime: ${field}="${v}-05:00" for a local time, ` +
|
|
80
|
+
`or ${field}="${v}Z" if it really is UTC.\n` +
|
|
81
|
+
` 2. Keep the local wall-clock time and name the zone: ` +
|
|
82
|
+
`${field}="${v}", ${timezoneField}="America/Chicago".\n` +
|
|
83
|
+
`An unstated zone moves the Ascendant by ~15° per hour and shifts every house placement.`);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Throws unless `value` names an unambiguous instant.
|
|
87
|
+
*
|
|
88
|
+
* Call this in every tool handler before building a request body. Passing a
|
|
89
|
+
* `timezone` satisfies the contract for a zone-less value; the zone itself is
|
|
90
|
+
* resolved server-side (or by `localToUtcIso` for the `*_utc` endpoints).
|
|
91
|
+
*/
|
|
92
|
+
export function assertZonedDatetime(field, value, timezone, timezoneField = "timezone") {
|
|
93
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
94
|
+
return; // required-ness is checked elsewhere
|
|
95
|
+
if (!isNaiveClockTime(value))
|
|
96
|
+
return;
|
|
97
|
+
if (typeof timezone === "string" && timezone.trim() !== "")
|
|
98
|
+
return;
|
|
99
|
+
throw new Error(ambiguousDatetimeMessage(field, value, timezoneField));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Convert a datetime to a UTC ISO 8601 string for the endpoints whose field is
|
|
103
|
+
* named `*_utc` and whose Go type is a strict RFC 3339 `time.Time`.
|
|
104
|
+
*
|
|
105
|
+
* Unlike the four ad-hoc `ensureTimezone` helpers this replaces, it never
|
|
106
|
+
* appends a bare "Z" to a zone-less value — it throws instead. Appending Z
|
|
107
|
+
* asserts the input was UTC, which is exactly the silent assumption that made
|
|
108
|
+
* the original defect invisible.
|
|
109
|
+
*/
|
|
110
|
+
export function localToUtcIso(field, dt, tz, timezoneField = "timezone") {
|
|
111
|
+
const value = (dt ?? "").trim();
|
|
112
|
+
if (hasZoneSuffix(value)) {
|
|
113
|
+
// Normalise `±HHMM` to `±HH:MM` for Go's RFC 3339 parser.
|
|
114
|
+
return value.replace(/([+-]\d{2})(\d{2})$/, "$1:$2");
|
|
115
|
+
}
|
|
116
|
+
if (!isNaiveClockTime(value)) {
|
|
117
|
+
// Date-only or unrecognised: leave it to the server to interpret or reject.
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
if (!tz || tz.trim() === "") {
|
|
121
|
+
throw new Error(ambiguousDatetimeMessage(field, value, timezoneField));
|
|
122
|
+
}
|
|
123
|
+
const [datePart, timePart = "00:00:00"] = value.split(/[T ]/);
|
|
124
|
+
const [year, month, day] = datePart.split("-").map(Number);
|
|
125
|
+
const [hour, min, sec = 0] = timePart.split(":").map(Number);
|
|
126
|
+
// Treat the components as local wall-clock time in `tz`, then shift by the
|
|
127
|
+
// offset Intl reports for that instant. Iterate twice: the offset at the UTC
|
|
128
|
+
// candidate can differ from the offset at the true instant across a DST edge.
|
|
129
|
+
const offsetMsAt = (utcMs) => {
|
|
130
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
131
|
+
timeZone: tz,
|
|
132
|
+
hour12: false,
|
|
133
|
+
year: "numeric",
|
|
134
|
+
month: "2-digit",
|
|
135
|
+
day: "2-digit",
|
|
136
|
+
hour: "2-digit",
|
|
137
|
+
minute: "2-digit",
|
|
138
|
+
second: "2-digit",
|
|
139
|
+
}).formatToParts(new Date(utcMs));
|
|
140
|
+
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
141
|
+
const asUtc = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
|
|
142
|
+
return asUtc - utcMs;
|
|
143
|
+
};
|
|
144
|
+
let utcMs = Date.UTC(year, month - 1, day, hour, min, sec);
|
|
145
|
+
utcMs -= offsetMsAt(utcMs);
|
|
146
|
+
utcMs = Date.UTC(year, month - 1, day, hour, min, sec) - offsetMsAt(utcMs);
|
|
147
|
+
const iso = new Date(utcMs).toISOString();
|
|
148
|
+
if (Number.isNaN(utcMs)) {
|
|
149
|
+
throw new Error(`\`${field}\` could not be resolved: "${value}" in timezone "${tz}". ` +
|
|
150
|
+
"Check the datetime format and that the IANA zone name is spelled correctly.");
|
|
151
|
+
}
|
|
152
|
+
return iso.replace(/\.000Z$/, "Z");
|
|
153
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -40,12 +40,55 @@ export interface ToolDefinition {
|
|
|
40
40
|
stdioOnly?: boolean;
|
|
41
41
|
handler: (args: any) => Promise<any>;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Which slice of the registry a ListTools response advertises.
|
|
45
|
+
*
|
|
46
|
+
* - `core`: the curated everyday set (see CORE_TOOL_NAMES).
|
|
47
|
+
* - `full`: every model-visible tool.
|
|
48
|
+
*
|
|
49
|
+
* This is a *view* filter, not a registration filter. Tools outside the core
|
|
50
|
+
* set stay registered and stay callable by name — CallTool resolves straight
|
|
51
|
+
* out of toolRegistry (src/index.ts, src/server-sse.ts) and never consults the
|
|
52
|
+
* surface. Narrowing the surface therefore costs zero capability; it only
|
|
53
|
+
* shrinks the tool-definition payload the model has to reason over.
|
|
54
|
+
*
|
|
55
|
+
* Registration-time filtering (a third ToolProfile) would NOT work here: the
|
|
56
|
+
* registry is process-global with a one-shot `toolsInitialized` latch, so the
|
|
57
|
+
* multi-tenant HTTP server cannot vary it per session, and an unregistered
|
|
58
|
+
* tool becomes uncallable rather than merely hidden.
|
|
59
|
+
*/
|
|
60
|
+
export type ToolSurface = "core" | "full";
|
|
61
|
+
/**
|
|
62
|
+
* The default advertised surface: everyday astrology work, one tool per job.
|
|
63
|
+
*
|
|
64
|
+
* Selection rules:
|
|
65
|
+
* - every explore_* app (the visual entry point to each tradition),
|
|
66
|
+
* - the primary data tool per domain,
|
|
67
|
+
* - geocoding (location_search / timezone_resolve) so charts can be built
|
|
68
|
+
* from a place name without falling back to dev_read_api,
|
|
69
|
+
* - the dev escape hatch, which reaches 28 further endpoints via the
|
|
70
|
+
* allowlist and is how anything outside this set gets discovered.
|
|
71
|
+
*
|
|
72
|
+
* Everything omitted here — the Venus family, the typed BaZi derivations, the
|
|
73
|
+
* comparative and returns long tail, the standalone SVG renderers — remains
|
|
74
|
+
* fully callable by name, and is listed by `dev_list_allowed` where the
|
|
75
|
+
* allowlist covers it. Clients that want the whole surface advertised can ask
|
|
76
|
+
* for it: `?profile=full` on HTTP, `OPENEPHEMERIS_TOOLS=full` on stdio.
|
|
77
|
+
*
|
|
78
|
+
* NOTE: scripts/test-mcp-http.ts (the 6-hourly production canary) asserts
|
|
79
|
+
* dev_read_api, ephemeris_moon_phase, ephemeris_natal_chart and
|
|
80
|
+
* explore_natal_chart are present. Do not remove those four.
|
|
81
|
+
*/
|
|
82
|
+
export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
|
|
43
83
|
/**
|
|
44
84
|
* Returns tools that should be exposed to Claude in the ListTools response.
|
|
45
85
|
* Filters out tools that have visibility restricted to the UI (app-only),
|
|
46
|
-
*
|
|
86
|
+
* for the HTTP transport tools marked stdioOnly, and — when `surface` is
|
|
87
|
+
* "core" — everything outside CORE_TOOL_NAMES.
|
|
47
88
|
*/
|
|
48
|
-
export declare function modelVisibleTools(transport?: "stdio" | "http"): ToolDefinition[];
|
|
89
|
+
export declare function modelVisibleTools(transport?: "stdio" | "http", surface?: ToolSurface): ToolDefinition[];
|
|
90
|
+
/** Parses a caller-supplied surface hint. Anything unrecognised means "core". */
|
|
91
|
+
export declare function parseToolSurface(raw: unknown): ToolSurface;
|
|
49
92
|
export declare const toolRegistry: Record<string, ToolDefinition>;
|
|
50
93
|
export declare function registerTool(tool: ToolDefinition): void;
|
|
51
94
|
export type ToolProfile = "dev" | "legacy";
|
package/dist/tools/index.js
CHANGED
|
@@ -12,12 +12,80 @@ export const SERVER_VERSION = (() => {
|
|
|
12
12
|
return "0.0.0-unknown";
|
|
13
13
|
}
|
|
14
14
|
})();
|
|
15
|
+
/**
|
|
16
|
+
* The default advertised surface: everyday astrology work, one tool per job.
|
|
17
|
+
*
|
|
18
|
+
* Selection rules:
|
|
19
|
+
* - every explore_* app (the visual entry point to each tradition),
|
|
20
|
+
* - the primary data tool per domain,
|
|
21
|
+
* - geocoding (location_search / timezone_resolve) so charts can be built
|
|
22
|
+
* from a place name without falling back to dev_read_api,
|
|
23
|
+
* - the dev escape hatch, which reaches 28 further endpoints via the
|
|
24
|
+
* allowlist and is how anything outside this set gets discovered.
|
|
25
|
+
*
|
|
26
|
+
* Everything omitted here — the Venus family, the typed BaZi derivations, the
|
|
27
|
+
* comparative and returns long tail, the standalone SVG renderers — remains
|
|
28
|
+
* fully callable by name, and is listed by `dev_list_allowed` where the
|
|
29
|
+
* allowlist covers it. Clients that want the whole surface advertised can ask
|
|
30
|
+
* for it: `?profile=full` on HTTP, `OPENEPHEMERIS_TOOLS=full` on stdio.
|
|
31
|
+
*
|
|
32
|
+
* NOTE: scripts/test-mcp-http.ts (the 6-hourly production canary) asserts
|
|
33
|
+
* dev_read_api, ephemeris_moon_phase, ephemeris_natal_chart and
|
|
34
|
+
* explore_natal_chart are present. Do not remove those four.
|
|
35
|
+
*/
|
|
36
|
+
export const CORE_TOOL_NAMES = new Set([
|
|
37
|
+
// Interactive apps — the visual entry point to each tradition
|
|
38
|
+
"explore_natal_chart",
|
|
39
|
+
"explore_bi_wheel",
|
|
40
|
+
"explore_human_design",
|
|
41
|
+
"explore_human_design_transit",
|
|
42
|
+
"explore_human_design_connection",
|
|
43
|
+
"explore_moon_phase",
|
|
44
|
+
"explore_transit_timeline",
|
|
45
|
+
"explore_vedic_chart",
|
|
46
|
+
"explore_bazi_chart",
|
|
47
|
+
// Core ephemeris data
|
|
48
|
+
"ephemeris_natal_chart",
|
|
49
|
+
"ephemeris_planet_position",
|
|
50
|
+
"ephemeris_house_cusps",
|
|
51
|
+
"ephemeris_moon_phase",
|
|
52
|
+
"ephemeris_transits",
|
|
53
|
+
"ephemeris_synastry",
|
|
54
|
+
"ephemeris_retrograde_status",
|
|
55
|
+
"ephemeris_next_eclipse",
|
|
56
|
+
"ephemeris_relocation",
|
|
57
|
+
"ephemeris_progressed_chart",
|
|
58
|
+
"ephemeris_solar_return",
|
|
59
|
+
// Traditions
|
|
60
|
+
"human_design_chart",
|
|
61
|
+
"vedic_chart",
|
|
62
|
+
"chinese_bazi",
|
|
63
|
+
// Timing
|
|
64
|
+
"ephemeris_electional",
|
|
65
|
+
"electional_moment_analysis",
|
|
66
|
+
// Astrocartography
|
|
67
|
+
"acg_power_lines",
|
|
68
|
+
"acg_hits",
|
|
69
|
+
// Geocoding — promoted to model visibility so chart building does not have
|
|
70
|
+
// to route through dev_read_api /location/autocomplete
|
|
71
|
+
"location_search",
|
|
72
|
+
"timezone_resolve",
|
|
73
|
+
// Account + escape hatch
|
|
74
|
+
"account_usage",
|
|
75
|
+
"dev_read_api",
|
|
76
|
+
"dev_list_allowed",
|
|
77
|
+
// Auth (stdio transport only — already fenced by stdioOnly)
|
|
78
|
+
"auth_login",
|
|
79
|
+
"auth_status",
|
|
80
|
+
"auth_logout",
|
|
81
|
+
]);
|
|
15
82
|
/**
|
|
16
83
|
* Returns tools that should be exposed to Claude in the ListTools response.
|
|
17
84
|
* Filters out tools that have visibility restricted to the UI (app-only),
|
|
18
|
-
*
|
|
85
|
+
* for the HTTP transport tools marked stdioOnly, and — when `surface` is
|
|
86
|
+
* "core" — everything outside CORE_TOOL_NAMES.
|
|
19
87
|
*/
|
|
20
|
-
export function modelVisibleTools(transport = "stdio") {
|
|
88
|
+
export function modelVisibleTools(transport = "stdio", surface = "full") {
|
|
21
89
|
return Object.values(toolRegistry).filter((t) => {
|
|
22
90
|
if (transport === "http" && t.stdioOnly)
|
|
23
91
|
return false;
|
|
@@ -25,9 +93,15 @@ export function modelVisibleTools(transport = "stdio") {
|
|
|
25
93
|
// If visibility is explicitly ["app"] (no "model"), hide from model
|
|
26
94
|
if (vis && !vis.includes("model") && vis.includes("app"))
|
|
27
95
|
return false;
|
|
96
|
+
if (surface === "core" && !CORE_TOOL_NAMES.has(t.name))
|
|
97
|
+
return false;
|
|
28
98
|
return true;
|
|
29
99
|
});
|
|
30
100
|
}
|
|
101
|
+
/** Parses a caller-supplied surface hint. Anything unrecognised means "core". */
|
|
102
|
+
export function parseToolSurface(raw) {
|
|
103
|
+
return typeof raw === "string" && raw.toLowerCase() === "full" ? "full" : "core";
|
|
104
|
+
}
|
|
31
105
|
export const toolRegistry = {};
|
|
32
106
|
export function registerTool(tool) {
|
|
33
107
|
toolRegistry[tool.name] = tool;
|
|
@@ -47,6 +121,8 @@ export async function initTools(profile) {
|
|
|
47
121
|
const resolvedProfile = (profile || process.env.OPENEPHEMERIS_PROFILE || process.env.ASTROMCP_PROFILE || "dev").toLowerCase();
|
|
48
122
|
// Always register auth tools (available in all profiles).
|
|
49
123
|
await import("./auth.js");
|
|
124
|
+
// Always register account/usage tools (0-credit, useful in all profiles).
|
|
125
|
+
await import("./specialized/account.js");
|
|
50
126
|
// Always register the generic proxy tools (dev_read_api + dev_write_api + dev_list_allowed).
|
|
51
127
|
await import("./dev.js");
|
|
52
128
|
if (resolvedProfile === "dev") {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { registerTool } from "../index.js";
|
|
2
|
+
import { getActiveClient, BackendError, DASHBOARD_ACCOUNT_URL, LOGIN_SIGNUP_URL, UPGRADE_URL, WALLET_TOPUP_URL, } from "../../backend/client.js";
|
|
3
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
|
+
// Tiers where the primary next step is a wallet top-up rather than a plan change.
|
|
5
|
+
const WALLET_TIERS = new Set(["explorer", "free", "payg", "wallet"]);
|
|
6
|
+
registerTool({
|
|
7
|
+
name: "account_usage",
|
|
8
|
+
description: "Check the user's OpenEphemeris account usage and remaining credits. " +
|
|
9
|
+
"Returns their plan tier, billing period, credits used / included / remaining, " +
|
|
10
|
+
"percent of quota used, total API calls, and subscription status with renewal date.\n\n" +
|
|
11
|
+
"✅ USE THIS TOOL FOR: 'How many credits do I have left?', 'What's my usage this month?', " +
|
|
12
|
+
"'Am I close to my limit?', 'What plan am I on?', 'How do I upgrade or top up?'\n\n" +
|
|
13
|
+
"CREDIT COST: Free (0 credits).\n\n" +
|
|
14
|
+
"EXAMPLE: Check current usage:\n" +
|
|
15
|
+
" (call with no arguments)\n\n" +
|
|
16
|
+
"EXAMPLE: Check a past month:\n" +
|
|
17
|
+
" month='2026-06'",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
month: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Billing month to query in YYYY-MM format (e.g. '2026-06'). Defaults to the current month.",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: [],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
30
|
+
annotations: { title: "Account Usage", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
31
|
+
handler: async (args) => {
|
|
32
|
+
const params = {};
|
|
33
|
+
if (args?.month)
|
|
34
|
+
params.month = args.month;
|
|
35
|
+
let usageRows;
|
|
36
|
+
try {
|
|
37
|
+
const raw = await getActiveClient().request("GET", "/api-keys/overage-status", { params });
|
|
38
|
+
usageRows = Array.isArray(raw) ? raw : [];
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof BackendError && error.status === 401) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: `**Not signed in.** To check usage, connect an OpenEphemeris account first.\n\n` +
|
|
46
|
+
`${error.message}\n\n` +
|
|
47
|
+
`New here? Create a free account at ${LOGIN_SIGNUP_URL}`,
|
|
48
|
+
}],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
// Subscription info is best-effort — usage is still useful without it.
|
|
54
|
+
let subscription = null;
|
|
55
|
+
try {
|
|
56
|
+
subscription = await getActiveClient().request("GET", "/billing/stripe/me");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Tolerate failure (e.g. no Stripe customer on free tier).
|
|
60
|
+
}
|
|
61
|
+
const row = usageRows[0] ?? {};
|
|
62
|
+
const tier = (row.tier || subscription?.tier || "explorer").toLowerCase();
|
|
63
|
+
const period = row.month || args?.month || new Date().toISOString().substring(0, 7);
|
|
64
|
+
const used = row.total_units ?? 0;
|
|
65
|
+
const included = row.included_units ?? 0;
|
|
66
|
+
const remaining = Math.max(included - used, 0);
|
|
67
|
+
const percent = row.percent_units_used ?? (included > 0 ? Math.round((used / included) * 1000) / 10 : 0);
|
|
68
|
+
const totalCalls = row.total_calls ?? 0;
|
|
69
|
+
const lines = [
|
|
70
|
+
`**OpenEphemeris Account Usage**`,
|
|
71
|
+
``,
|
|
72
|
+
`- **Plan:** ${tier}`,
|
|
73
|
+
`- **Period:** ${period}`,
|
|
74
|
+
`- **Credits:** ${used.toLocaleString()} used of ${included.toLocaleString()} included — **${remaining.toLocaleString()} remaining** (${percent}% used)`,
|
|
75
|
+
`- **Total API calls:** ${totalCalls.toLocaleString()}`,
|
|
76
|
+
];
|
|
77
|
+
if ((row.overage_units ?? 0) > 0) {
|
|
78
|
+
lines.push(`- **Overage:** ${row.overage_units.toLocaleString()} credits over the included amount`);
|
|
79
|
+
}
|
|
80
|
+
if (subscription?.status) {
|
|
81
|
+
lines.push(`- **Subscription:** ${subscription.status}`);
|
|
82
|
+
}
|
|
83
|
+
if (subscription?.current_period_end) {
|
|
84
|
+
const renewal = new Date(subscription.current_period_end);
|
|
85
|
+
if (!Number.isNaN(renewal.getTime())) {
|
|
86
|
+
lines.push(`- **Renews:** ${renewal.toISOString().substring(0, 10)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
lines.push(``);
|
|
90
|
+
if (WALLET_TIERS.has(tier)) {
|
|
91
|
+
lines.push(`**Need more credits?** Top up your wallet at ${WALLET_TOPUP_URL} (from $5), ` +
|
|
92
|
+
`or upgrade to a monthly plan at ${UPGRADE_URL}.`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
lines.push(`**Need more credits?** Upgrade your plan at ${UPGRADE_URL}, ` +
|
|
96
|
+
`or manage your subscription at ${DASHBOARD_ACCOUNT_URL}.`);
|
|
97
|
+
}
|
|
98
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
99
|
+
},
|
|
100
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
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, localToUtcIso } from "../datetime.js";
|
|
4
5
|
const ACG_BODY_DESCRIPTION = "List of celestial bodies for line calculation. " +
|
|
5
6
|
"E.g. ['Sun', 'Moon', 'Venus', 'Mars', 'Jupiter', 'Saturn']. " +
|
|
6
7
|
"Aliases: 'NorthNode'/'Node'/'Rahu' → MeanNode, 'SouthNode'/'Ketu' → SouthNode. " +
|
|
@@ -16,21 +17,21 @@ registerTool({
|
|
|
16
17
|
"→ For a specific city/location analysis, use acg_hits instead (faster and more relevant).\n" +
|
|
17
18
|
"✅ USE FOR: Getting the full global GeoJSON line geometry for map rendering or bulk geographic analysis.\n\n" +
|
|
18
19
|
"CREDIT COST: 10 credits per call.\n\n" +
|
|
19
|
-
"EXAMPLE: Saturn and Jupiter power lines for a chart born 1990-04-15 in Chicago:\n" +
|
|
20
|
-
" birth_datetime='1990-04-15T14:30:00',
|
|
21
|
-
" bodies=['Saturn', 'Jupiter']",
|
|
20
|
+
"EXAMPLE: Saturn and Jupiter power lines for a chart born 1990-04-15 at 2:30 PM in Chicago:\n" +
|
|
21
|
+
" birth_datetime='1990-04-15T14:30:00', timezone='America/Chicago',\n" +
|
|
22
|
+
" birth_latitude=41.8781, birth_longitude=-87.6298, bodies=['Saturn', 'Jupiter']",
|
|
22
23
|
inputSchema: {
|
|
23
24
|
type: "object",
|
|
24
25
|
properties: {
|
|
25
26
|
birth_datetime: {
|
|
26
27
|
type: "string",
|
|
27
|
-
description:
|
|
28
|
-
"
|
|
29
|
-
"interpreted as UTC and will place the ACG lines in the wrong location.",
|
|
28
|
+
description: DATETIME_DESC +
|
|
29
|
+
" An unstated zone moves every ACG line by ~15° of longitude per hour.",
|
|
30
30
|
},
|
|
31
|
+
timezone: TIMEZONE_PROPERTY,
|
|
31
32
|
birth_latitude: {
|
|
32
33
|
type: "number",
|
|
33
|
-
description: "Birth latitude in decimal degrees.",
|
|
34
|
+
description: "Birth latitude in decimal degrees. Resolve from a place name with location_search; never recall coordinates from memory.",
|
|
34
35
|
},
|
|
35
36
|
birth_longitude: {
|
|
36
37
|
type: "number",
|
|
@@ -66,7 +67,7 @@ registerTool({
|
|
|
66
67
|
birthplace_lat: args.birth_latitude,
|
|
67
68
|
birthplace_lon: args.birth_longitude,
|
|
68
69
|
},
|
|
69
|
-
epoch: args.birth_datetime,
|
|
70
|
+
epoch: localToUtcIso("birth_datetime", args.birth_datetime, args.timezone),
|
|
70
71
|
};
|
|
71
72
|
if (args.bodies)
|
|
72
73
|
body.bodies = args.bodies;
|
|
@@ -109,20 +110,21 @@ registerTool({
|
|
|
109
110
|
"❌ NOT FOR: Full global map geometry → use acg_power_lines for that instead.\n\n" +
|
|
110
111
|
"CREDIT COST: 15 credits per call.\n\n" +
|
|
111
112
|
"EXAMPLE: All ACG lines within 3° of Paris for a chart born 1990-04-15 in Chicago:\n" +
|
|
112
|
-
" birth_datetime='1990-04-15T14:30:00',
|
|
113
|
+
" birth_datetime='1990-04-15T14:30:00', timezone='America/Chicago',\n" +
|
|
114
|
+
" birth_latitude=41.8781, birth_longitude=-87.6298,\n" +
|
|
113
115
|
" query_latitude=48.8566, query_longitude=2.3522, radius_deg=3",
|
|
114
116
|
inputSchema: {
|
|
115
117
|
type: "object",
|
|
116
118
|
properties: {
|
|
117
119
|
birth_datetime: {
|
|
118
120
|
type: "string",
|
|
119
|
-
description:
|
|
120
|
-
"
|
|
121
|
-
"interpreted as UTC and will place the ACG lines in the wrong location.",
|
|
121
|
+
description: DATETIME_DESC +
|
|
122
|
+
" An unstated zone moves every ACG line by ~15° of longitude per hour.",
|
|
122
123
|
},
|
|
124
|
+
timezone: TIMEZONE_PROPERTY,
|
|
123
125
|
birth_latitude: {
|
|
124
126
|
type: "number",
|
|
125
|
-
description: "Birth latitude in decimal degrees.",
|
|
127
|
+
description: "Birth latitude in decimal degrees. Resolve from a place name with location_search; never recall coordinates from memory.",
|
|
126
128
|
},
|
|
127
129
|
birth_longitude: {
|
|
128
130
|
type: "number",
|
|
@@ -162,7 +164,7 @@ registerTool({
|
|
|
162
164
|
birthplace_lat: args.birth_latitude,
|
|
163
165
|
birthplace_lon: args.birth_longitude,
|
|
164
166
|
},
|
|
165
|
-
epoch: args.birth_datetime,
|
|
167
|
+
epoch: localToUtcIso("birth_datetime", args.birth_datetime, args.timezone),
|
|
166
168
|
query_lat: args.query_latitude,
|
|
167
169
|
query_lon: args.query_longitude,
|
|
168
170
|
};
|