@openephemeris/mcp-server 4.11.2 → 4.13.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 +117 -0
- package/README.md +2 -0
- package/dist/analytics.d.ts +11 -1
- package/dist/analytics.js +58 -5
- package/dist/index.js +46 -37
- package/dist/instructions.d.ts +16 -0
- package/dist/instructions.js +51 -0
- package/dist/prompts.js +3 -1
- package/dist/schema-packs/llm.d.ts +2 -2
- package/dist/schema-packs/llm.js +11 -2
- package/dist/server-sse.js +57 -17
- package/dist/tools/apps/_location-resolver.d.ts +11 -0
- package/dist/tools/apps/_location-resolver.js +24 -10
- package/dist/tools/apps/chart-wheel-app.js +36 -18
- package/dist/tools/apps/location-tools.js +2 -5
- package/dist/tools/dev.js +32 -24
- package/dist/tools/specialized/bi_wheel.js +3 -2
- package/dist/tools/specialized/chart_wheel.js +3 -2
- package/dist/tools/specialized/electional.js +31 -3
- package/dist/tools/specialized/ephemeris_extended.js +3 -1
- package/dist/tools/specialized/natal.js +79 -7
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,102 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [4.13.0] — 2026-08-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Sidereal charts on `ephemeris_natal_chart`.** New `zodiac_type`
|
|
14
|
+
(`tropical` | `sidereal`) and `ayanamsa` (Lahiri, Fagan-Bradley,
|
|
15
|
+
Krishnamurti, Raman, Yukteshwar). Sidereal subtracts the ayanamsa from every
|
|
16
|
+
longitude, so signs, dignities and element balance all move by roughly a
|
|
17
|
+
whole sign; aspects and house numbers do not change, because a uniform
|
|
18
|
+
offset preserves angular separation and which side of a cusp a body falls
|
|
19
|
+
on. The response names the zodiac and the ayanamsa that produced it. Naming
|
|
20
|
+
an ayanamsa without asking for sidereal is now an error rather than a
|
|
21
|
+
quietly tropical chart.
|
|
22
|
+
|
|
23
|
+
The API had accepted `zodiac_type` for as long as the natal contract has
|
|
24
|
+
existed and read it nowhere, so a request for a sidereal chart came back
|
|
25
|
+
tropical — not imprecise, a different chart — with nothing in the payload
|
|
26
|
+
saying which zodiac you had been given. That is fixed on the server, which
|
|
27
|
+
is the only reason the parameter is exposed here.
|
|
28
|
+
|
|
29
|
+
- **Whole-chart analytics on `ephemeris_natal_chart`.** `analytics` accepts any
|
|
30
|
+
of `element_balance`, `hemisphere_emphasis`, `aspect_patterns` and
|
|
31
|
+
`harmonics`. No extra credits. The endpoint has always computed these on
|
|
32
|
+
request and this server never asked for them, so they were unreachable from
|
|
33
|
+
MCP no matter what a caller wanted.
|
|
34
|
+
|
|
35
|
+
- **`include_minor_aspects` on `ephemeris_natal_chart`.** Adds quincunx,
|
|
36
|
+
semisextile, semisquare, sesquiquadrate, quintile and biquintile to the
|
|
37
|
+
aspect grid. Minors use their own orbs, so this widens the grid without
|
|
38
|
+
changing any major aspect.
|
|
39
|
+
|
|
40
|
+
- **`latitude` / `longitude` on `electional_moment_analysis`.** Both optional —
|
|
41
|
+
the tool is documented as taking no arguments at all and still does — but
|
|
42
|
+
without them sect falls back to diurnal, so a night moment is scored with
|
|
43
|
+
the day triplicity rulers. Pass both whenever the place is known.
|
|
44
|
+
|
|
45
|
+
### Removed
|
|
46
|
+
- **`include_fixed_stars` on `ephemeris_natal_chart`.** It never did anything.
|
|
47
|
+
The natal endpoint reads no fixed-star configuration on any of the keys this
|
|
48
|
+
flag was routed to, so the response was identical either way. Tool
|
|
49
|
+
definitions are re-sent on every model pass, which means a parameter that
|
|
50
|
+
does nothing is still paid for by every user on every request. Fixed stars
|
|
51
|
+
come from `ephemeris_fixed_stars`, which also reports which chart points sit
|
|
52
|
+
conjunct them. Unknown arguments are not rejected, so a caller still sending
|
|
53
|
+
the old flag is ignored exactly as it was before.
|
|
54
|
+
|
|
55
|
+
- **The `alcabitius` and `morinus` house systems.** Advertised on three tools
|
|
56
|
+
and broken on all of them: the codes sent for them are accepted by no code
|
|
57
|
+
path, so every call naming one came back a validation error. The seven
|
|
58
|
+
systems that remain are the whole set the engine implements.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
- **`dev_read_api` advertised fifteen POST endpoints it rejects.** It is
|
|
62
|
+
GET-only, and it shared one reference block with the write proxy, so most of
|
|
63
|
+
its description documented calls it cannot make. It is on the default tool
|
|
64
|
+
surface, so every client paid for that text on every model pass. Each proxy
|
|
65
|
+
now lists only the calls it accepts.
|
|
66
|
+
|
|
67
|
+
- **The compact `llm` output could not express a minor aspect.** Its aspect
|
|
68
|
+
dictionary declared five entries while the engine can now emit eleven, so a
|
|
69
|
+
minor aspect had no valid code to travel under. All eleven are declared.
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
- **Endpoint count corrected to 120** across the bundled skills, and the
|
|
73
|
+
plugin skill's fixed-star guidance now points at `ephemeris_fixed_stars`
|
|
74
|
+
rather than a flag that never worked.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## [4.12.0] — 2026-08-13
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
- **Server `instructions` told the model a tool needed no arguments when it
|
|
82
|
+
did.** The zero-data-entry-point line named `ephemeris_retrograde_status` as
|
|
83
|
+
needing no arguments; its schema has `required: ["datetime"]`. An agent
|
|
84
|
+
that took the line literally made a zero-argument call and got a validation
|
|
85
|
+
error as its first experience of the server. Now names
|
|
86
|
+
`explore_moon_phase` and `electional_moment_analysis` — both genuinely
|
|
87
|
+
zero-argument — and both are asserted against the real tool schemas in CI
|
|
88
|
+
so this cannot silently drift again.
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
- **Connector first-run activation telemetry.** `mcp_tools_listed` (both
|
|
92
|
+
transports, once per session) and `mcp_session_silent` (HTTP, on teardown
|
|
93
|
+
when tools were listed but never called) close the gap where "connected but
|
|
94
|
+
never used it" was only an absence of events — indistinguishable from a
|
|
95
|
+
session that died mid-handshake. Funnel: `mcp_session_init` →
|
|
96
|
+
`mcp_tools_listed` → `first_tool_call`.
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
- **Server `instructions` deduplicated** into `src/instructions.ts`, shared by
|
|
100
|
+
both transports. They were previously copy-pasted verbatim into each
|
|
101
|
+
transport file, which is how the argument-count claim above drifted from
|
|
102
|
+
the schemas undetected.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
10
106
|
## [4.11.2] — 2026-08-13
|
|
11
107
|
|
|
12
108
|
### Fixed
|
|
@@ -347,6 +443,27 @@ the code.
|
|
|
347
443
|
`options.include_hermetic_lots: true`); only the descriptions were
|
|
348
444
|
stale.
|
|
349
445
|
|
|
446
|
+
> **Correction (2026-08-23).** Half of the entry above is wrong, and it is
|
|
447
|
+
> left standing rather than deleted so the record shows what was claimed.
|
|
448
|
+
>
|
|
449
|
+
> `include_arabic_parts` is accurate: the natal endpoint reads
|
|
450
|
+
> `include_hermetic_lots` at the top level, under `options`, and under
|
|
451
|
+
> `enhanced_options`, and has done so all along.
|
|
452
|
+
>
|
|
453
|
+
> `include_fixed_stars` was **not** wired and still is not. The natal
|
|
454
|
+
> endpoint reads no fixed-star configuration at any of the three keys —
|
|
455
|
+
> not `configuration.fixed_star_options`, not
|
|
456
|
+
> `enhanced_options.include_fixed_stars`, not
|
|
457
|
+
> `options.include_fixed_stars` — so setting the flag changes nothing in
|
|
458
|
+
> the response and never has. What made it look wired is that the
|
|
459
|
+
> identical `configuration.fixed_star_options` block genuinely does work
|
|
460
|
+
> on the chart-wheel and bi-wheel renderers, which share the request
|
|
461
|
+
> shape but not the code path.
|
|
462
|
+
>
|
|
463
|
+
> Until the natal endpoint honours it, `POST /ephemeris/fixed-stars` is
|
|
464
|
+
> the way to get fixed-star positions; it accepts the same star names,
|
|
465
|
+
> groups, and magnitude limits and returns conjunctions to the angles.
|
|
466
|
+
|
|
350
467
|
---
|
|
351
468
|
|
|
352
469
|
## [4.3.1] — 2026-07-29
|
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
Model Context Protocol server for OpenEphemeris — typed astrology tools powered by the NASA JPL DE440 ephemeris. Zero hallucination on planetary positions, dates, and degrees. Covers 1,100 years of astronomical data.
|
|
12
12
|
|
|
13
|
+

|
|
14
|
+
|
|
13
15
|
**Hosted endpoint:** `https://mcp.openephemeris.com/mcp` (Streamable HTTP, MCP 2025-11-25 spec)
|
|
14
16
|
|
|
15
17
|
## Quick Start
|
package/dist/analytics.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
/** Stable
|
|
1
|
+
/** Stable identity from a credential — the Supabase user id for OAuth, else a hash of the API key. */
|
|
2
2
|
export declare function distinctIdFor(credential: string | undefined): string;
|
|
3
|
+
/**
|
|
4
|
+
* Fire `first_tool_call` the first time a given identity completes a tool
|
|
5
|
+
* call, so activation (site visit → connector install → first real use) is a
|
|
6
|
+
* queryable event instead of "earliest mcp_tool_call per person" math. Never
|
|
7
|
+
* fires for "anonymous" (no credential at all — nothing to attribute
|
|
8
|
+
* activation to). Best-effort dedup only: the set is per-process, so a
|
|
9
|
+
* session that lands on a different Fly machine can refire once — acceptable
|
|
10
|
+
* for an activation metric read in aggregate.
|
|
11
|
+
*/
|
|
12
|
+
export declare function captureFirstToolCallOnce(distinctId: string, properties?: Record<string, unknown>): void;
|
|
3
13
|
export declare function captureEvent(event: string, distinctId: string, properties?: Record<string, unknown>): void;
|
package/dist/analytics.js
CHANGED
|
@@ -6,6 +6,21 @@
|
|
|
6
6
|
* the 402 paywall. These events close that gap. Everything is best-effort:
|
|
7
7
|
* analytics must never slow down or fail a tool call.
|
|
8
8
|
*
|
|
9
|
+
* Events, in funnel order:
|
|
10
|
+
* mcp_session_init — handshake completed
|
|
11
|
+
* mcp_tools_listed — host fetched tools/list (once per session)
|
|
12
|
+
* first_tool_call — this identity's first-ever completed tool call
|
|
13
|
+
* mcp_tool_call — every completed call
|
|
14
|
+
* mcp_tool_error — every failed call
|
|
15
|
+
* mcp_session_silent — HTTP only: listed tools, never attempted one. See
|
|
16
|
+
* reportSilentSession() in server-sse.ts for its two
|
|
17
|
+
* reading caveats (late arrival, undercount).
|
|
18
|
+
*
|
|
19
|
+
* mcp_session_init → mcp_tools_listed → first_tool_call is the activation
|
|
20
|
+
* funnel. Before mcp_tools_listed existed, "connected but never used it" was
|
|
21
|
+
* only an absence of events and could not be told apart from a host that never
|
|
22
|
+
* listed, or a session that died mid-handshake.
|
|
23
|
+
*
|
|
9
24
|
* Configuration:
|
|
10
25
|
* POSTHOG_API_KEY — project API key (phc_...). On the Fly-hosted remote
|
|
11
26
|
* server this comes from a secret. On stdio (the npm
|
|
@@ -18,12 +33,18 @@
|
|
|
18
33
|
* DO_NOT_TRACK=1 (de-facto cross-tool standard)
|
|
19
34
|
* Disclosed in README.md under "Telemetry".
|
|
20
35
|
*
|
|
21
|
-
* Identity: we never send raw API keys
|
|
22
|
-
* prefix of the
|
|
23
|
-
* authentication.
|
|
24
|
-
*
|
|
36
|
+
* Identity: we never send raw API keys. API-key sessions get a SHA-256
|
|
37
|
+
* prefix of the key, stable per user across sessions but useless for
|
|
38
|
+
* authentication. OAuth (Supabase JWT) sessions instead use the JWT's `sub`
|
|
39
|
+
* claim directly as the distinct_id — that's the same Supabase user id the
|
|
40
|
+
* web app passes to posthog.identify() at apps/web/contexts/AuthContext.tsx,
|
|
41
|
+
* so an MCP session and a browser session for the same account land on one
|
|
42
|
+
* PostHog person instead of two unmergeable ones. No birth data, coordinates,
|
|
43
|
+
* names, or tool arguments are ever sent — only the tool NAME, duration,
|
|
44
|
+
* error status, and client name.
|
|
25
45
|
*/
|
|
26
46
|
import { createHash } from "node:crypto";
|
|
47
|
+
import { decodeJwtSub } from "./oauth/session-utils.js";
|
|
27
48
|
const POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
|
|
28
49
|
/**
|
|
29
50
|
* Public, write-only PostHog project ingestion key.
|
|
@@ -51,13 +72,45 @@ function apiKey() {
|
|
|
51
72
|
return configured;
|
|
52
73
|
return DEFAULT_INGEST_KEY.startsWith("phc_") ? DEFAULT_INGEST_KEY : undefined;
|
|
53
74
|
}
|
|
54
|
-
/** Stable
|
|
75
|
+
/** Stable identity from a credential — the Supabase user id for OAuth, else a hash of the API key. */
|
|
55
76
|
export function distinctIdFor(credential) {
|
|
56
77
|
if (!credential)
|
|
57
78
|
return "anonymous";
|
|
79
|
+
if (credential.startsWith("eyJ")) {
|
|
80
|
+
const sub = decodeJwtSub(credential);
|
|
81
|
+
if (sub)
|
|
82
|
+
return sub;
|
|
83
|
+
}
|
|
58
84
|
return "mcp_" + createHash("sha256").update(credential).digest("hex").slice(0, 24);
|
|
59
85
|
}
|
|
86
|
+
const identitiesWithFirstToolCall = new Set();
|
|
87
|
+
/**
|
|
88
|
+
* Fire `first_tool_call` the first time a given identity completes a tool
|
|
89
|
+
* call, so activation (site visit → connector install → first real use) is a
|
|
90
|
+
* queryable event instead of "earliest mcp_tool_call per person" math. Never
|
|
91
|
+
* fires for "anonymous" (no credential at all — nothing to attribute
|
|
92
|
+
* activation to). Best-effort dedup only: the set is per-process, so a
|
|
93
|
+
* session that lands on a different Fly machine can refire once — acceptable
|
|
94
|
+
* for an activation metric read in aggregate.
|
|
95
|
+
*/
|
|
96
|
+
export function captureFirstToolCallOnce(distinctId, properties = {}) {
|
|
97
|
+
if (distinctId === "anonymous" || identitiesWithFirstToolCall.has(distinctId))
|
|
98
|
+
return;
|
|
99
|
+
identitiesWithFirstToolCall.add(distinctId);
|
|
100
|
+
captureEvent("first_tool_call", distinctId, properties);
|
|
101
|
+
}
|
|
102
|
+
// Our own e2e/harness/canary scripts declare an MCP clientInfo.name that ends
|
|
103
|
+
// up in every event's `client_name` property (see transportProps() in
|
|
104
|
+
// server-sse.ts / index.ts) — e.g. "e2e-http-client", "harness-fixture-capture".
|
|
105
|
+
// Their runs were previously indistinguishable from real users in PostHog.
|
|
106
|
+
const INTERNAL_CLIENT_PREFIXES = ["e2e-", "harness-", "test-", "canary-", "deploy-state-check", "local-agent-mode", "verify"];
|
|
107
|
+
/** True if a captured `client_name` identifies one of our own test/harness/canary scripts. */
|
|
108
|
+
function isInternalClientName(name) {
|
|
109
|
+
return typeof name === "string" && INTERNAL_CLIENT_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
110
|
+
}
|
|
60
111
|
export function captureEvent(event, distinctId, properties = {}) {
|
|
112
|
+
if (isInternalClientName(properties.client_name))
|
|
113
|
+
return;
|
|
61
114
|
const key = apiKey();
|
|
62
115
|
if (!key)
|
|
63
116
|
return;
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
import { initTools, toolRegistry, formatToolResponse, formatToolError, modelVisibleTools, parseToolSurface, SERVER_VERSION } from "./tools/index.js";
|
|
9
|
-
import {
|
|
10
|
-
import { captureEvent, distinctIdFor } from "./analytics.js";
|
|
9
|
+
import { SERVER_INSTRUCTIONS } from "./instructions.js";
|
|
10
|
+
import { captureEvent, captureFirstToolCallOnce, distinctIdFor } from "./analytics.js";
|
|
11
11
|
import { listPrompts, getPromptContent } from "./prompts.js";
|
|
12
12
|
// ── MCP App resource imports ────────────────────────────────────────────────
|
|
13
13
|
import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
|
|
@@ -63,15 +63,7 @@ const server = new Server({
|
|
|
63
63
|
},
|
|
64
64
|
},
|
|
65
65
|
},
|
|
66
|
-
instructions:
|
|
67
|
-
"or approximate positions yourself; always call a tool. " +
|
|
68
|
-
"For anything the user should SEE (natal chart, bi-wheel, bodygraph, moon phase), " +
|
|
69
|
-
"prefer the explore_* tools — they render interactive visuals inline. " +
|
|
70
|
-
"ephemeris_* tools return data; use format='llm' on them for compact output. " +
|
|
71
|
-
"If the user has no birth data handy, start with the sky right now — " +
|
|
72
|
-
"explore_moon_phase and ephemeris_retrograde_status need none. " +
|
|
73
|
-
"See the 'welcome_to_open_ephemeris' prompt for orientation.\n\n" +
|
|
74
|
-
DATETIME_CONTRACT_INSTRUCTIONS,
|
|
66
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
75
67
|
});
|
|
76
68
|
// Which slice of the registry this process advertises. Tools outside the
|
|
77
69
|
// surface stay callable by name — this only controls what tools/list returns.
|
|
@@ -110,34 +102,49 @@ function transportProps() {
|
|
|
110
102
|
server_version: SERVER_VERSION,
|
|
111
103
|
};
|
|
112
104
|
}
|
|
105
|
+
// One-shot latch for mcp_tools_listed. A stdio process serves exactly one
|
|
106
|
+
// session for its whole lifetime, so process scope IS session scope here.
|
|
107
|
+
//
|
|
108
|
+
// There is deliberately no stdio equivalent of the HTTP transport's
|
|
109
|
+
// `mcp_session_silent`: the only teardown signal is process exit, and
|
|
110
|
+
// captureEvent is a fire-and-forget fetch that would not flush before the
|
|
111
|
+
// process is gone. On stdio, "listed but never called" is a PostHog query
|
|
112
|
+
// (mcp_tools_listed with no following first_tool_call), not an event.
|
|
113
|
+
let toolsListedReported = false;
|
|
113
114
|
// List available tools
|
|
114
115
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
visibility: tool._meta.ui.visibility ?? ["model", "app"],
|
|
135
|
-
},
|
|
116
|
+
const tools = modelVisibleTools("stdio", STDIO_SURFACE).map((tool) => ({
|
|
117
|
+
name: tool.name,
|
|
118
|
+
description: tool.description,
|
|
119
|
+
inputSchema: tool.inputSchema,
|
|
120
|
+
annotations: {
|
|
121
|
+
title: tool.annotations?.title ?? tool.name,
|
|
122
|
+
readOnlyHint: tool.annotations?.readOnlyHint ?? true,
|
|
123
|
+
destructiveHint: tool.annotations?.destructiveHint ?? false,
|
|
124
|
+
idempotentHint: tool.annotations?.idempotentHint ?? true,
|
|
125
|
+
openWorldHint: tool.annotations?.openWorldHint ?? false,
|
|
126
|
+
},
|
|
127
|
+
// Expose MCP Apps UI linkage so Claude Desktop can prefetch the resource
|
|
128
|
+
...(tool._meta?.ui?.resourceUri
|
|
129
|
+
? {
|
|
130
|
+
_meta: {
|
|
131
|
+
"ui/resourceUri": tool._meta.ui.resourceUri,
|
|
132
|
+
ui: {
|
|
133
|
+
resourceUri: tool._meta.ui.resourceUri,
|
|
134
|
+
visibility: tool._meta.ui.visibility ?? ["model", "app"],
|
|
136
135
|
},
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
};
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
: {}),
|
|
139
|
+
}));
|
|
140
|
+
if (!toolsListedReported) {
|
|
141
|
+
toolsListedReported = true;
|
|
142
|
+
captureEvent("mcp_tools_listed", analyticsId(), {
|
|
143
|
+
tool_count: tools.length,
|
|
144
|
+
...transportProps(),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return { tools };
|
|
141
148
|
});
|
|
142
149
|
// List available prompts
|
|
143
150
|
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
@@ -279,11 +286,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
279
286
|
try {
|
|
280
287
|
const result = await tool.handler(request.params.arguments ?? {});
|
|
281
288
|
const durationMs = Date.now() - startTime;
|
|
282
|
-
|
|
289
|
+
const id = analyticsId();
|
|
290
|
+
captureEvent("mcp_tool_call", id, {
|
|
283
291
|
tool: toolName,
|
|
284
292
|
duration_ms: durationMs,
|
|
285
293
|
...transportProps(),
|
|
286
294
|
});
|
|
295
|
+
captureFirstToolCallOnce(id, { tool: toolName, ...transportProps() });
|
|
287
296
|
return formatToolResponse(toolName, result, durationMs);
|
|
288
297
|
}
|
|
289
298
|
catch (error) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools an agent can call with literally no arguments — the entry points for a
|
|
3
|
+
* user who has not offered birth data.
|
|
4
|
+
*
|
|
5
|
+
* Asserted against the real schemas in test/description-cross-references.test.ts:
|
|
6
|
+
* each name here must be registered, must be on the CORE surface (most hosts
|
|
7
|
+
* refuse to call a tool absent from tools/list), and must genuinely have no
|
|
8
|
+
* required fields. Naming a tool here that needs an argument puts the model's
|
|
9
|
+
* very first call on a path to a validation error, which is the exact failure
|
|
10
|
+
* this list exists to prevent.
|
|
11
|
+
*
|
|
12
|
+
* Order is deliberate: the visual one leads, because a rendered dial is a
|
|
13
|
+
* better first impression than a score.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ZERO_ARG_ENTRY_TOOLS: readonly ["explore_moon_phase", "electional_moment_analysis"];
|
|
16
|
+
export declare const SERVER_INSTRUCTIONS: string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* instructions.ts — the server `instructions` string, shared by both transports.
|
|
3
|
+
*
|
|
4
|
+
* This is the highest-leverage text in the product. It is the only guidance a
|
|
5
|
+
* host puts in front of the model before the user's first message, and the
|
|
6
|
+
* connector funnel is where we lose people: of the MCP connector identities
|
|
7
|
+
* seen in a 60-day window, ~1 in 8 ever invoked a tool at all.
|
|
8
|
+
*
|
|
9
|
+
* It previously lived inline and duplicated verbatim in src/index.ts and
|
|
10
|
+
* src/server-sse.ts. That duplication is how it came to advertise
|
|
11
|
+
* `ephemeris_retrograde_status` as needing no arguments when its schema has
|
|
12
|
+
* `required: ["datetime"]` — an agent that took the line literally made a
|
|
13
|
+
* zero-argument call and got a validation error as its first experience of the
|
|
14
|
+
* server. Defining the text once, next to the list it interpolates, lets a test
|
|
15
|
+
* check the claim against the real schemas (see description-cross-references).
|
|
16
|
+
*
|
|
17
|
+
* Kept OUT of the tool surface on purpose: `instructions` is sent once at
|
|
18
|
+
* initialize and is not counted by the tool-surface token budget (GATE 4, see
|
|
19
|
+
* test/tool-surface-budget.test.ts), which is at ~19.6k of its 20k ceiling.
|
|
20
|
+
* First-run routing guidance therefore belongs here rather than in a tool
|
|
21
|
+
* description that every model pass re-pays for.
|
|
22
|
+
*/
|
|
23
|
+
import { DATETIME_CONTRACT_INSTRUCTIONS } from "./tools/datetime.js";
|
|
24
|
+
/**
|
|
25
|
+
* Tools an agent can call with literally no arguments — the entry points for a
|
|
26
|
+
* user who has not offered birth data.
|
|
27
|
+
*
|
|
28
|
+
* Asserted against the real schemas in test/description-cross-references.test.ts:
|
|
29
|
+
* each name here must be registered, must be on the CORE surface (most hosts
|
|
30
|
+
* refuse to call a tool absent from tools/list), and must genuinely have no
|
|
31
|
+
* required fields. Naming a tool here that needs an argument puts the model's
|
|
32
|
+
* very first call on a path to a validation error, which is the exact failure
|
|
33
|
+
* this list exists to prevent.
|
|
34
|
+
*
|
|
35
|
+
* Order is deliberate: the visual one leads, because a rendered dial is a
|
|
36
|
+
* better first impression than a score.
|
|
37
|
+
*/
|
|
38
|
+
export const ZERO_ARG_ENTRY_TOOLS = [
|
|
39
|
+
"explore_moon_phase",
|
|
40
|
+
"electional_moment_analysis",
|
|
41
|
+
];
|
|
42
|
+
export const SERVER_INSTRUCTIONS = "Open Ephemeris computes real astronomy (JPL DE440, sub-arcsecond) — never guess " +
|
|
43
|
+
"or approximate positions yourself; always call a tool. " +
|
|
44
|
+
"For anything the user should SEE (natal chart, bi-wheel, bodygraph, moon phase), " +
|
|
45
|
+
"prefer the explore_* tools — they render interactive visuals inline. " +
|
|
46
|
+
"ephemeris_* tools return data; use format='llm' on them for compact output. " +
|
|
47
|
+
"If the user has no birth data handy, open with the sky right now: " +
|
|
48
|
+
`${ZERO_ARG_ENTRY_TOOLS.join(" and ")} take no arguments at all — ` +
|
|
49
|
+
"call one rather than asking for a birth date first. " +
|
|
50
|
+
"See the 'welcome_to_open_ephemeris' prompt for orientation.\n\n" +
|
|
51
|
+
DATETIME_CONTRACT_INSTRUCTIONS;
|
package/dist/prompts.js
CHANGED
|
@@ -16,7 +16,9 @@ export const PROMPTS = [
|
|
|
16
16
|
description: "Orientation guide for the Open Ephemeris MCP. Understand available tools, key concepts, " +
|
|
17
17
|
"and how to pick the right prompt for the task.",
|
|
18
18
|
text: "Welcome to the **Open Ephemeris MCP Server** — an enterprise-grade astrological and astronomical " +
|
|
19
|
-
"computation engine powered by NASA JPL DE440 ephemeris data (1550–2650 CE
|
|
19
|
+
"computation engine powered by NASA JPL DE440 ephemeris data (1550–2650 CE for the Sun, " +
|
|
20
|
+
"Moon, and planets; 1600–2200 CE for the extended asteroid, centaur, and trans-Neptunian " +
|
|
21
|
+
"catalog).\n\n" +
|
|
20
22
|
"## Available Workflows\n" +
|
|
21
23
|
"There are specialized prompts for each of the following. Ask the user which they'd like, " +
|
|
22
24
|
"or jump in directly if they've already told you:\n\n" +
|
|
@@ -3,8 +3,8 @@ export declare const LLM_V2_POINTS_SCHEMA: readonly ["id", "kind", "src", "lon",
|
|
|
3
3
|
export declare const LLM_V2_ASPECTS_SCHEMA: readonly ["a_i", "b_i", "t", "orb", "orb_pct", "app", "str"];
|
|
4
4
|
export declare const LLM_V2_DICT: {
|
|
5
5
|
readonly sign_id: readonly ["ari", "tau", "gem", "can", "leo", "vir", "lib", "sco", "sag", "cap", "aqu", "pis"];
|
|
6
|
-
readonly aspect_id: readonly ["con", "opp", "tri", "sqr", "sex"];
|
|
7
|
-
readonly aspect_angle: readonly [0, 180, 120, 90, 60];
|
|
6
|
+
readonly aspect_id: readonly ["con", "opp", "tri", "sqr", "sex", "ssx", "ssq", "qui", "sqq", "bqu", "qcx"];
|
|
7
|
+
readonly aspect_angle: readonly [0, 180, 120, 90, 60, 30, 45, 72, 135, 144, 150];
|
|
8
8
|
readonly kind_id: readonly ["planet", "angle", "node", "lilith", "asteroid", "other"];
|
|
9
9
|
};
|
|
10
10
|
export declare const LlmV2PayloadSchema: z.ZodObject<{
|
package/dist/schema-packs/llm.js
CHANGED
|
@@ -18,10 +18,19 @@ export const LLM_V2_POINTS_SCHEMA = [
|
|
|
18
18
|
"on_cusp",
|
|
19
19
|
];
|
|
20
20
|
export const LLM_V2_ASPECTS_SCHEMA = ["a_i", "b_i", "t", "orb", "orb_pct", "app", "str"];
|
|
21
|
+
// aspect_id / aspect_angle mirror the Go projection's dictionary
|
|
22
|
+
// (llmAspectIDs / llmAspectAngles in internal/api/handlers/llm_projection.go).
|
|
23
|
+
// The six minor aspects became detectable behind options.include_minor_aspects,
|
|
24
|
+
// so they are enumerated here too — a `t` value the pack does not declare fails
|
|
25
|
+
// DictZ, which is a strict tuple of literals, and check:schema-packs gates the
|
|
26
|
+
// release on it.
|
|
27
|
+
//
|
|
28
|
+
// APPEND ONLY, and in the Go dictionary's order: the index is the wire value of
|
|
29
|
+
// the aspects table's `t` column, so reordering re-points every stored row.
|
|
21
30
|
export const LLM_V2_DICT = {
|
|
22
31
|
sign_id: ["ari", "tau", "gem", "can", "leo", "vir", "lib", "sco", "sag", "cap", "aqu", "pis"],
|
|
23
|
-
aspect_id: ["con", "opp", "tri", "sqr", "sex"],
|
|
24
|
-
aspect_angle: [0, 180, 120, 90, 60],
|
|
32
|
+
aspect_id: ["con", "opp", "tri", "sqr", "sex", "ssx", "ssq", "qui", "sqq", "bqu", "qcx"],
|
|
33
|
+
aspect_angle: [0, 180, 120, 90, 60, 30, 45, 72, 135, 144, 150],
|
|
25
34
|
kind_id: ["planet", "angle", "node", "lilith", "asteroid", "other"],
|
|
26
35
|
};
|
|
27
36
|
const PointsSchemaZ = z.tuple(LLM_V2_POINTS_SCHEMA.map((v) => z.literal(v)));
|
package/dist/server-sse.js
CHANGED
|
@@ -21,10 +21,10 @@ import axios from "axios";
|
|
|
21
21
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
22
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23
23
|
import { InMemoryEventStore } from "./event-store.js";
|
|
24
|
-
import { captureEvent, distinctIdFor } from "./analytics.js";
|
|
24
|
+
import { captureEvent, captureFirstToolCallOnce, distinctIdFor } from "./analytics.js";
|
|
25
25
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, isInitializeRequest, } from "@modelcontextprotocol/sdk/types.js";
|
|
26
26
|
import { initTools, toolRegistry, formatToolResponse, formatToolError, modelVisibleTools, parseToolSurface, describeSurface } from "./tools/index.js";
|
|
27
|
-
import {
|
|
27
|
+
import { SERVER_INSTRUCTIONS } from "./instructions.js";
|
|
28
28
|
import { BackendClient, runWithClient } from "./backend/client.js";
|
|
29
29
|
import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
|
|
30
30
|
import { BODYGRAPH_RESOURCE_URI, BODYGRAPH_MIME_TYPE, getBodygraphBundle, } from "./tools/apps/bodygraph-app.js";
|
|
@@ -224,7 +224,36 @@ function transportProps(server, surface) {
|
|
|
224
224
|
server_version: version,
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
|
-
function
|
|
227
|
+
function newSessionActivity() {
|
|
228
|
+
return { startedAt: Date.now(), listedTools: false, attemptedTool: false, silenceReported: false };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Emit `mcp_session_silent` for a session that saw the tool list and never tried
|
|
232
|
+
* to use it — the measurable form of the connector drop-off.
|
|
233
|
+
*
|
|
234
|
+
* Two caveats for whoever reads this in PostHog, because they change the query:
|
|
235
|
+
*
|
|
236
|
+
* 1. The event lands when the session TEARS DOWN, not when it went quiet. A
|
|
237
|
+
* host that vanishes without a DELETE is only cleaned up by the idle reaper,
|
|
238
|
+
* so its event can arrive SESSION_IDLE_TTL_MS + up to one 10-minute sweep
|
|
239
|
+
* after the fact. Read it in daily aggregate, not as a live signal, and use
|
|
240
|
+
* `session_duration_ms` to separate "closed straight away" from "sat open
|
|
241
|
+
* and unused".
|
|
242
|
+
* 2. It UNDERCOUNTS. A process restart (i.e. every deploy) drops the sessions
|
|
243
|
+
* still pending teardown. `mcp_tools_listed` with no following
|
|
244
|
+
* `first_tool_call` is the reliable denominator; this event is the richer
|
|
245
|
+
* but lossier view.
|
|
246
|
+
*/
|
|
247
|
+
function reportSilentSession(server, analyticsId, surface, activity) {
|
|
248
|
+
if (!activity.listedTools || activity.attemptedTool || activity.silenceReported)
|
|
249
|
+
return;
|
|
250
|
+
activity.silenceReported = true;
|
|
251
|
+
captureEvent("mcp_session_silent", analyticsId, {
|
|
252
|
+
session_duration_ms: Date.now() - activity.startedAt,
|
|
253
|
+
...transportProps(server, surface),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
function createMcpServer(analyticsId = "anonymous", surface = "core", activity = newSessionActivity()) {
|
|
228
257
|
const server = new Server({
|
|
229
258
|
name: "openephemeris-mcp",
|
|
230
259
|
title: "Open Ephemeris",
|
|
@@ -266,19 +295,11 @@ function createMcpServer(analyticsId = "anonymous", surface = "core") {
|
|
|
266
295
|
},
|
|
267
296
|
},
|
|
268
297
|
},
|
|
269
|
-
instructions:
|
|
270
|
-
"or approximate positions yourself; always call a tool. " +
|
|
271
|
-
"For anything the user should SEE (natal chart, bi-wheel, bodygraph, moon phase), " +
|
|
272
|
-
"prefer the explore_* tools — they render interactive visuals inline. " +
|
|
273
|
-
"ephemeris_* tools return data; use format='llm' on them for compact output. " +
|
|
274
|
-
"If the user has no birth data handy, start with the sky right now — " +
|
|
275
|
-
"explore_moon_phase and ephemeris_retrograde_status need none. " +
|
|
276
|
-
"See the 'welcome_to_open_ephemeris' prompt for orientation.\n\n" +
|
|
277
|
-
DATETIME_CONTRACT_INSTRUCTIONS,
|
|
298
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
278
299
|
});
|
|
279
300
|
// --- Tool handlers ---
|
|
280
|
-
server.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
281
|
-
tools
|
|
301
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
302
|
+
const tools = modelVisibleTools("http", surface).map((tool) => ({
|
|
282
303
|
name: tool.name,
|
|
283
304
|
description: tool.description,
|
|
284
305
|
inputSchema: tool.inputSchema,
|
|
@@ -295,8 +316,18 @@ function createMcpServer(analyticsId = "anonymous", surface = "core") {
|
|
|
295
316
|
},
|
|
296
317
|
}
|
|
297
318
|
: {}),
|
|
298
|
-
}))
|
|
299
|
-
|
|
319
|
+
}));
|
|
320
|
+
// Once per session: the host has fetched the surface. Everything after this
|
|
321
|
+
// point is the model's choice, so this is the denominator for activation.
|
|
322
|
+
if (!activity.listedTools) {
|
|
323
|
+
activity.listedTools = true;
|
|
324
|
+
captureEvent("mcp_tools_listed", analyticsId, {
|
|
325
|
+
tool_count: tools.length,
|
|
326
|
+
...transportProps(server, surface),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return { tools };
|
|
330
|
+
});
|
|
300
331
|
// --- Prompt handlers ---
|
|
301
332
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
302
333
|
prompts: [WELCOME_PROMPT],
|
|
@@ -313,6 +344,9 @@ function createMcpServer(analyticsId = "anonymous", surface = "core") {
|
|
|
313
344
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
314
345
|
const toolName = request.params.name;
|
|
315
346
|
const tool = toolRegistry[toolName];
|
|
347
|
+
// Marked before the registry check: reaching for a tool that does not exist
|
|
348
|
+
// is still a session that tried, and must not be counted as silence.
|
|
349
|
+
activity.attemptedTool = true;
|
|
316
350
|
if (!tool || tool.stdioOnly) {
|
|
317
351
|
// stdioOnly tools (device auth) mutate process-global state and must not
|
|
318
352
|
// be callable on the multi-tenant HTTP transport, even by name.
|
|
@@ -331,6 +365,7 @@ function createMcpServer(analyticsId = "anonymous", surface = "core") {
|
|
|
331
365
|
duration_ms: durationMs,
|
|
332
366
|
...transportProps(server, surface),
|
|
333
367
|
});
|
|
368
|
+
captureFirstToolCallOnce(analyticsId, { tool: toolName, ...transportProps(server, surface) });
|
|
334
369
|
return formatToolResponse(toolName, result, durationMs);
|
|
335
370
|
}
|
|
336
371
|
catch (error) {
|
|
@@ -854,7 +889,8 @@ export async function createSseApp() {
|
|
|
854
889
|
// A comma list of traditions works the same way — `?profile=hd,bazi` —
|
|
855
890
|
// and advertises just those plus geocoding, account and the escape hatch.
|
|
856
891
|
const surface = parseToolSurface(req.query.profile ?? req.headers["x-oe-tool-surface"]);
|
|
857
|
-
const
|
|
892
|
+
const activity = newSessionActivity();
|
|
893
|
+
const server = createMcpServer(analyticsId, surface, activity);
|
|
858
894
|
// Fire session_init after the handshake so getClientVersion() is populated
|
|
859
895
|
// — without this the connecting host is unknown and we cannot tell which
|
|
860
896
|
// clients the remote server is actually serving.
|
|
@@ -869,6 +905,10 @@ export async function createSseApp() {
|
|
|
869
905
|
httpSessions.delete(transport.sessionId);
|
|
870
906
|
console.error(`[HTTP] Session closed: ${transport.sessionId}`);
|
|
871
907
|
}
|
|
908
|
+
// All three teardown paths (transport close, DELETE /mcp, idle reaper)
|
|
909
|
+
// converge here — server.close() closes the transport — so this is the one
|
|
910
|
+
// place the silence verdict has to be made.
|
|
911
|
+
reportSilentSession(server, analyticsId, surface, activity);
|
|
872
912
|
};
|
|
873
913
|
await server.connect(transport);
|
|
874
914
|
await runWithClient(client, () => transport.handleRequest(req, res, req.body));
|
|
@@ -5,6 +5,17 @@ export interface ResolvedLocation {
|
|
|
5
5
|
displayName: string;
|
|
6
6
|
placeId: string | null;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Whether the query itself already pins the top hit — "dallas texas",
|
|
10
|
+
* "san francisco, ca", "paris, france", "london uk" — so rival cities of
|
|
11
|
+
* the same short name are not actually ambiguous.
|
|
12
|
+
*
|
|
13
|
+
* Single source of truth for the ambiguity qualifier: both
|
|
14
|
+
* `resolveLocationOrThrow` and the `location_search` tool call this. The
|
|
15
|
+
* two call sites read differently-named fields (raw `region`/`country_code`
|
|
16
|
+
* vs mapped `region`/`countryCode`), so this takes plain strings.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isQualifiedForTopHit(query: string, region: unknown, countryCode: unknown): boolean;
|
|
8
19
|
/**
|
|
9
20
|
* Resolve a place name to lat/lon/tz. Throws if ambiguous or unresolvable.
|
|
10
21
|
*
|
|
@@ -52,6 +52,29 @@ function countryNameFor(code) {
|
|
|
52
52
|
// Colloquial forms Intl does not produce: it renders GB as "United Kingdom",
|
|
53
53
|
// so a bare "uk" token would otherwise miss.
|
|
54
54
|
const COUNTRY_ALIASES = { uk: "gb", usa: "us", uae: "ae" };
|
|
55
|
+
/**
|
|
56
|
+
* Whether the query itself already pins the top hit — "dallas texas",
|
|
57
|
+
* "san francisco, ca", "paris, france", "london uk" — so rival cities of
|
|
58
|
+
* the same short name are not actually ambiguous.
|
|
59
|
+
*
|
|
60
|
+
* Single source of truth for the ambiguity qualifier: both
|
|
61
|
+
* `resolveLocationOrThrow` and the `location_search` tool call this. The
|
|
62
|
+
* two call sites read differently-named fields (raw `region`/`country_code`
|
|
63
|
+
* vs mapped `region`/`countryCode`), so this takes plain strings.
|
|
64
|
+
*/
|
|
65
|
+
export function isQualifiedForTopHit(query, region, countryCode) {
|
|
66
|
+
const norm = (v) => String(v ?? "").trim().toLowerCase();
|
|
67
|
+
const q = norm(query);
|
|
68
|
+
const qTokens = new Set(q.split(/[^a-z0-9]+/).filter(Boolean));
|
|
69
|
+
const topRegion = norm(region);
|
|
70
|
+
const topCountry = norm(countryCode);
|
|
71
|
+
const topCountryName = topCountry === "" ? "" : countryNameFor(topCountry);
|
|
72
|
+
return ((topRegion !== "" && q.includes(topRegion)) ||
|
|
73
|
+
(topRegion !== "" && [...qTokens].some((t) => US_STATE_ABBREVIATIONS[t] === topRegion)) ||
|
|
74
|
+
(topCountry !== "" && qTokens.has(topCountry)) ||
|
|
75
|
+
(topCountryName !== "" && q.includes(topCountryName)) ||
|
|
76
|
+
(topCountry !== "" && [...qTokens].some((t) => COUNTRY_ALIASES[t] === topCountry)));
|
|
77
|
+
}
|
|
55
78
|
// `code` rides along to PostHog via the existing `code` property on
|
|
56
79
|
// mcp_tool_error. The event deliberately records no message or stack, so
|
|
57
80
|
// without a code every one of these lands as code:"none" / error_kind:"local"
|
|
@@ -82,16 +105,7 @@ export async function resolveLocationOrThrow(location) {
|
|
|
82
105
|
const norm = (v) => String(v ?? "").trim().toLowerCase();
|
|
83
106
|
const top = list[0];
|
|
84
107
|
const rivals = list.filter((s) => norm(s.short_name) === norm(top.short_name));
|
|
85
|
-
const
|
|
86
|
-
const qTokens = new Set(q.split(/[^a-z0-9]+/).filter(Boolean));
|
|
87
|
-
const topRegion = norm(top.region);
|
|
88
|
-
const topCountry = norm(top.country_code);
|
|
89
|
-
const topCountryName = topCountry === "" ? "" : countryNameFor(topCountry);
|
|
90
|
-
const qualified = (topRegion !== "" && q.includes(topRegion)) ||
|
|
91
|
-
(topRegion !== "" && [...qTokens].some((t) => US_STATE_ABBREVIATIONS[t] === topRegion)) ||
|
|
92
|
-
(topCountry !== "" && qTokens.has(topCountry)) ||
|
|
93
|
-
(topCountryName !== "" && q.includes(topCountryName)) ||
|
|
94
|
-
(topCountry !== "" && [...qTokens].some((t) => COUNTRY_ALIASES[t] === topCountry));
|
|
108
|
+
const qualified = isQualifiedForTopHit(query, top.region, top.country_code);
|
|
95
109
|
const ambiguous = rivals.length > 1 && !qualified;
|
|
96
110
|
if (ambiguous) {
|
|
97
111
|
const options = rivals
|
|
@@ -88,7 +88,16 @@ function prettyHouseSystem(hs) {
|
|
|
88
88
|
return hs.split("_").map(capitalize).join(" ");
|
|
89
89
|
}
|
|
90
90
|
/** Build the nested request body expected by /ephemeris/natal-chart (POST). */
|
|
91
|
-
function buildNatalBody(datetime, lat, lon, houseSystem, timezone) {
|
|
91
|
+
function buildNatalBody(datetime, lat, lon, houseSystem, timezone, additionalObjects) {
|
|
92
|
+
const configuration = {
|
|
93
|
+
house_system: HOUSE_SYSTEM_MAP[houseSystem] ?? "P",
|
|
94
|
+
};
|
|
95
|
+
if (additionalObjects && additionalObjects.length > 0) {
|
|
96
|
+
// additional_objects is additive on top of the API's fixed base roster
|
|
97
|
+
// (classical planets, nodes, Chiron, major asteroids) — it does not
|
|
98
|
+
// replace it. See handler_ephemeris2.go's body-ID resolution.
|
|
99
|
+
configuration.additional_objects = additionalObjects;
|
|
100
|
+
}
|
|
92
101
|
const body = {
|
|
93
102
|
subject: {
|
|
94
103
|
name: "MCP Request",
|
|
@@ -98,9 +107,7 @@ function buildNatalBody(datetime, lat, lon, houseSystem, timezone) {
|
|
|
98
107
|
longitude: { decimal: lon ?? 0 },
|
|
99
108
|
},
|
|
100
109
|
},
|
|
101
|
-
configuration
|
|
102
|
-
house_system: HOUSE_SYSTEM_MAP[houseSystem] ?? "P",
|
|
103
|
-
},
|
|
110
|
+
configuration,
|
|
104
111
|
options: {
|
|
105
112
|
include_aspects: true,
|
|
106
113
|
},
|
|
@@ -149,8 +156,10 @@ registerTool({
|
|
|
149
156
|
items: { type: "string" },
|
|
150
157
|
description: "Optional list of body names to include. " +
|
|
151
158
|
"Defaults to 13 classical bodies (Sun through Pluto + Chiron + Nodes). " +
|
|
152
|
-
"Use 'all' as a single item to include every available body (Lilith, Ceres, Juno, Vesta, Pallas, Vertex,
|
|
153
|
-
"
|
|
159
|
+
"Use 'all' as a single item to include every available body (Lilith, Ceres, Juno, Vesta, Pallas, Vertex, " +
|
|
160
|
+
"the 8 trans-Neptunian objects — eris, sedna, makemake, haumea, quaoar, orcus, ixion, varuna — and the " +
|
|
161
|
+
"8 Uranian/Hamburg-School points — cupido, hades, zeus, kronos, apollon, admetos, vulkanus, poseidon). " +
|
|
162
|
+
"Example: ['sun','moon','lilith','sedna'].",
|
|
154
163
|
},
|
|
155
164
|
},
|
|
156
165
|
required: ["datetime"],
|
|
@@ -189,18 +198,22 @@ registerTool({
|
|
|
189
198
|
// caller's explicit timezone still wins when supplied.
|
|
190
199
|
const effectiveTimezone = args.timezone ?? resolved.timezone;
|
|
191
200
|
const effectiveLocation = resolved.location ?? args.location;
|
|
192
|
-
|
|
193
|
-
//
|
|
201
|
+
// Determine which bodies are requested. Recognized extended slugs (TNOs,
|
|
202
|
+
// Uranian points, Lilith variants, etc.) are sent to the API as
|
|
203
|
+
// additional_objects so the API actually computes them — previously this
|
|
204
|
+
// only flipped legacy include_* booleans the natal-chart endpoint's
|
|
205
|
+
// schema doesn't have, so extended bodies (beyond the ones already in the
|
|
206
|
+
// API's fixed default roster) never reached the response. See #612-area
|
|
207
|
+
// catalog-expansion follow-up: TNO/Uranian bodies were addressable via
|
|
208
|
+
// /catalogs/bodies but unreachable from any chart-computation endpoint.
|
|
194
209
|
const requestedBodies = args.bodies;
|
|
195
210
|
const wantsAll = requestedBodies?.some(b => b.toLowerCase() === "all");
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
config.include_chiron = true;
|
|
203
|
-
}
|
|
211
|
+
const additionalObjects = wantsAll
|
|
212
|
+
? Array.from(EXTENDED_BODIES)
|
|
213
|
+
: (requestedBodies ?? [])
|
|
214
|
+
.map(b => b.toLowerCase())
|
|
215
|
+
.filter(b => EXTENDED_BODIES.has(b));
|
|
216
|
+
const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, effectiveTimezone, additionalObjects);
|
|
204
217
|
// Fetch natal chart JSON. The chart is rendered client-side in the UI iframe,
|
|
205
218
|
// so we do NOT call the /visualization/chart-wheel endpoint — that was the
|
|
206
219
|
// source of the ~90 second blocking delay.
|
|
@@ -461,10 +474,15 @@ function canonicalizeBodyName(raw) {
|
|
|
461
474
|
return lower;
|
|
462
475
|
}
|
|
463
476
|
const EXTENDED_BODIES = new Set([
|
|
464
|
-
|
|
477
|
+
// Slugs match the Go API's ephemerisIDForSlug (handler_wheel_resolver.go) so
|
|
478
|
+
// additional_objects entries actually resolve server-side.
|
|
479
|
+
"mean_lilith", "lilith", "lilith_true",
|
|
465
480
|
"ceres", "juno", "vesta", "pallas", "pholus",
|
|
466
481
|
"vertex", "part_of_fortune",
|
|
467
|
-
|
|
482
|
+
// Trans-Neptunian objects (all 8 — #603 catalog expansion)
|
|
483
|
+
"eris", "sedna", "makemake", "haumea", "quaoar", "orcus", "ixion", "varuna",
|
|
484
|
+
// Uranian / Hamburg School hypothetical points (all 8 — #603 catalog expansion)
|
|
485
|
+
"cupido", "hades", "zeus", "kronos", "apollon", "admetos", "vulkanus", "poseidon",
|
|
468
486
|
]);
|
|
469
487
|
const ALL_KNOWN_BODIES = new Set([...CLASSICAL_PLANETS, ...EXTENDED_BODIES]);
|
|
470
488
|
/**
|
|
@@ -2,6 +2,7 @@ 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
|
+
import { isQualifiedForTopHit } from "./_location-resolver.js";
|
|
5
6
|
/**
|
|
6
7
|
* Lift the API's historical-correction provenance onto an MCP result.
|
|
7
8
|
*
|
|
@@ -254,11 +255,7 @@ registerTool({
|
|
|
254
255
|
const rivals = top ? suggestions.filter((s) => norm(s.shortName) === norm(top.shortName)) : [];
|
|
255
256
|
// If the caller already pinned the place — "dallas texas", "portland uk" —
|
|
256
257
|
// the top hit is what they asked for and there is nothing to ask about.
|
|
257
|
-
const
|
|
258
|
-
const qTokens = new Set(q.split(/[^a-z0-9]+/).filter(Boolean));
|
|
259
|
-
const qualified = !!top &&
|
|
260
|
-
((norm(top.region) !== "" && q.includes(norm(top.region))) ||
|
|
261
|
-
(norm(top.countryCode) !== "" && qTokens.has(norm(top.countryCode))));
|
|
258
|
+
const qualified = !!top && isQualifiedForTopHit(args.query, top.region, top.countryCode);
|
|
262
259
|
const ambiguous = rivals.length > 1 && !qualified;
|
|
263
260
|
return {
|
|
264
261
|
suggestions,
|
package/dist/tools/dev.js
CHANGED
|
@@ -34,7 +34,14 @@ function isAllowedOperation(method, pathname, allow) {
|
|
|
34
34
|
}
|
|
35
35
|
// Shared reference block appended to both read/write proxy tools. Names the
|
|
36
36
|
// target API (Open Ephemeris) explicitly — required by the directory for
|
|
37
|
-
// freeform-path tools — plus credit costs
|
|
37
|
+
// freeform-path tools — plus credit costs.
|
|
38
|
+
//
|
|
39
|
+
// The COMMON CALLS list is NOT shared, because it is method-specific and the
|
|
40
|
+
// two tools do not accept the same methods. It used to be: dev_read_api is
|
|
41
|
+
// GET-only (READ_METHODS), and it was spending most of its description listing
|
|
42
|
+
// fifteen POST endpoints it will reject — on the core surface, which every
|
|
43
|
+
// client pays for on every model pass. Each tool now lists the calls it can
|
|
44
|
+
// actually make.
|
|
38
45
|
const DEV_API_REFERENCE = "Target API: Open Ephemeris REST API (https://api.openephemeris.com). " +
|
|
39
46
|
"Call dev_list_allowed to see all currently available endpoint paths.\n\n" +
|
|
40
47
|
"AUTH: Set OPENEPHEMERIS_API_KEY in your environment. See openephemeris.com/dashboard for active plan limits.\n\n" +
|
|
@@ -50,7 +57,27 @@ const DEV_API_REFERENCE = "Target API: Open Ephemeris REST API (https://api.open
|
|
|
50
57
|
" • Catalog / metadata / health endpoints: 0 credits\n" +
|
|
51
58
|
" • Compute surcharge: requests > 30s add 1 credit per 30s (predictive, acg, calendar, electional)\n" +
|
|
52
59
|
" • format=llm (token-optimized output): available on all tiers\n\n" +
|
|
53
|
-
"
|
|
60
|
+
"BINARY RESPONSES:\n" +
|
|
61
|
+
" • Binary/image endpoints return {content_type, content_length, encoding, data_base64}\n" +
|
|
62
|
+
" so callers can decode bytes deterministically.\n\n" +
|
|
63
|
+
"ECLIPSE NOTE: Eclipse endpoints accept format=llm via the query param like other endpoints.\n\n" +
|
|
64
|
+
"format=llm NOTE: Add query: {format: 'llm'} to natal/synastry/composite/HD endpoints for " +
|
|
65
|
+
"compact columnar output optimized for LLM token budgets (availability depends on your current plan).";
|
|
66
|
+
const READ_COMMON_CALLS = "COMMON CALLS:\n" +
|
|
67
|
+
" GET /ephemeris/moon/phase — Current/queried moon phase\n" +
|
|
68
|
+
" GET /ephemeris/moon/void-of-course — Next void-of-course period\n" +
|
|
69
|
+
" GET /ephemeris/agro/daily — Biodynamic farming day quality\n" +
|
|
70
|
+
" GET /ephemeris/agro/calendar — Multi-day biodynamic calendar\n" +
|
|
71
|
+
" GET /ephemeris/agro/void-of-course — Biodynamic VoC periods\n" +
|
|
72
|
+
" GET /eclipse/next-visible — Next eclipse visible from a location (query: lat, lon, type=solar|lunar)\n" +
|
|
73
|
+
" GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
|
|
74
|
+
" GET /eclipse/solar/local — Local solar eclipse (query: lat, lon)\n" +
|
|
75
|
+
" GET /tidal/forcing — Gravitational tidal forcing index\n" +
|
|
76
|
+
" GET /calendar/astrology/moon-phases — Moon phase calendar for a date range\n" +
|
|
77
|
+
" GET /location/autocomplete — Geocode a place name (query: query=City Name)\n" +
|
|
78
|
+
" GET /chinese/zodiac — Chinese zodiac year element/animal\n" +
|
|
79
|
+
" GET /catalogs/bodies — List all supported celestial bodies\n";
|
|
80
|
+
const WRITE_COMMON_CALLS = "COMMON CALLS:\n" +
|
|
54
81
|
" POST /ephemeris/natal-chart — Full natal chart (body: {subject: {name: 'Name', birth_datetime: {iso: '1990-04-15T14:30:00-05:00'}, birth_location: {latitude: {decimal: 40.0}, longitude: {decimal: -70.0}, timezone: {}}}})\n" +
|
|
55
82
|
" POST /ephemeris/natal/batch — Up to 50 natal charts in one request\n" +
|
|
56
83
|
" POST /ephemeris/relocation — Relocated chart (same natal, new location)\n" +
|
|
@@ -61,30 +88,11 @@ const DEV_API_REFERENCE = "Target API: Open Ephemeris REST API (https://api.open
|
|
|
61
88
|
" POST /comparative/composite — Composite (midpoint) chart\n" +
|
|
62
89
|
" POST /human-design/chart — Full HD chart (body: {birth_datetime_utc: '1990-04-15T19:30:00Z'}) — lat/lon optional\n" +
|
|
63
90
|
" POST /time/julian-day — Convert date to JD (body: {year: 1987, month: 7, day: 15, hour: 14, minute: 1})\n" +
|
|
64
|
-
" GET /ephemeris/moon/phase — Current/queried moon phase\n" +
|
|
65
|
-
" GET /ephemeris/moon/void-of-course — Next void-of-course period\n" +
|
|
66
|
-
" GET /ephemeris/agro/daily — Biodynamic farming day quality\n" +
|
|
67
|
-
" GET /ephemeris/agro/calendar — Multi-day biodynamic calendar\n" +
|
|
68
|
-
" GET /ephemeris/agro/void-of-course — Biodynamic VoC periods\n" +
|
|
69
|
-
" GET /eclipse/next-visible — Next eclipse visible from a location (query: lat, lon, type=solar|lunar)\n" +
|
|
70
|
-
" GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
|
|
71
|
-
" GET /eclipse/solar/local — Local solar eclipse (query: lat, lon)\n" +
|
|
72
|
-
" GET /tidal/forcing — Gravitational tidal forcing index\n" +
|
|
73
91
|
" POST /acg/power-lines — Astrocartography power lines (lat/lon GeoJSON)\n" +
|
|
74
92
|
" POST /acg/hits — ACG power at a specific location\n" +
|
|
75
|
-
" GET /calendar/astrology/moon-phases — Moon phase calendar for a date range\n" +
|
|
76
|
-
" GET /location/autocomplete — Geocode a place name (query: query=City Name)\n" +
|
|
77
93
|
" POST /timezone/lookup — Resolve timezone + UTC offset for a location\n" +
|
|
78
94
|
" POST /chinese/bazi — Chinese Ba Zi (Four Pillars) chart (body: {year, month, day, hour})\n" +
|
|
79
|
-
"
|
|
80
|
-
" POST /vedic/chart — Vedic (Jyotish) natal chart (body: {datetime_utc, latitude, longitude})\n" +
|
|
81
|
-
" GET /catalogs/bodies — List all supported celestial bodies\n\n" +
|
|
82
|
-
"BINARY RESPONSES:\n" +
|
|
83
|
-
" • Binary/image endpoints return {content_type, content_length, encoding, data_base64}\n" +
|
|
84
|
-
" so callers can decode bytes deterministically.\n\n" +
|
|
85
|
-
"ECLIPSE NOTE: Eclipse endpoints accept format=llm via the query param like other endpoints.\n\n" +
|
|
86
|
-
"format=llm NOTE: Add query: {format: 'llm'} to natal/synastry/composite/HD endpoints for " +
|
|
87
|
-
"compact columnar output optimized for LLM token budgets (availability depends on your current plan).";
|
|
95
|
+
" POST /vedic/chart — Vedic (Jyotish) natal chart (body: {datetime_utc, latitude, longitude})";
|
|
88
96
|
// The read and write proxies are kept as separate tools (not one method-switching
|
|
89
97
|
// tool) so that safe GET reads never share a surface with state-changing writes —
|
|
90
98
|
// a hard requirement of the Anthropic connector directory.
|
|
@@ -193,7 +201,7 @@ registerTool({
|
|
|
193
201
|
"first for common operations. Endpoints that compute via POST (natal-chart, synastry, etc.) " +
|
|
194
202
|
"are covered by the typed tools; the generic POST proxy is only exposed on the full tool " +
|
|
195
203
|
"surface (`?profile=full`).\n\n" +
|
|
196
|
-
DEV_API_REFERENCE,
|
|
204
|
+
DEV_API_REFERENCE + "\n\n" + READ_COMMON_CALLS,
|
|
197
205
|
inputSchema: makeProxyInputSchema(READ_METHODS),
|
|
198
206
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
199
207
|
// GET-only: never mutates server state. openWorldHint false — the proxy
|
|
@@ -208,7 +216,7 @@ registerTool({
|
|
|
208
216
|
"Most chart computations (natal-chart, synastry, composite, transits/search, returns, ACG) are " +
|
|
209
217
|
"POST endpoints and use this tool. Use the typed tools first for common operations; use dev_read_api " +
|
|
210
218
|
"for GET endpoints.\n\n" +
|
|
211
|
-
DEV_API_REFERENCE,
|
|
219
|
+
DEV_API_REFERENCE + "\n\n" + WRITE_COMMON_CALLS,
|
|
212
220
|
inputSchema: makeProxyInputSchema(WRITE_METHODS),
|
|
213
221
|
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
214
222
|
// Issues POST/PUT/PATCH/DELETE — must not be advertised as read-only. But
|
|
@@ -2,10 +2,11 @@ import { registerTool, validateRequired, pickEnum } from "../index.js";
|
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_IMAGE } from "../output-schemas.js";
|
|
4
4
|
import { DATETIME_DESC, assertZonedDatetime, timezoneProperty } from "../datetime.js";
|
|
5
|
+
// The seven systems the engine accepts (internal/math/houses.go).
|
|
6
|
+
// Do not re-add alcabitius/morinus — see the note in natal.ts.
|
|
5
7
|
const HOUSE_SYSTEM_MAP = {
|
|
6
8
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
7
9
|
campanus: "C", regiomontanus: "R", porphyry: "O",
|
|
8
|
-
alcabitius: "B", morinus: "M",
|
|
9
10
|
};
|
|
10
11
|
registerTool({
|
|
11
12
|
name: "ephemeris_bi_wheel",
|
|
@@ -27,7 +28,7 @@ registerTool({
|
|
|
27
28
|
longitude_b: { type: "number", description: "Longitude for subject B" },
|
|
28
29
|
house_system: {
|
|
29
30
|
type: "string",
|
|
30
|
-
enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry"
|
|
31
|
+
enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry"],
|
|
31
32
|
description: "House system to use. Defaults to 'placidus' if omitted.",
|
|
32
33
|
},
|
|
33
34
|
style: {
|
|
@@ -2,10 +2,11 @@ import { registerTool, validateRequired, pickEnum } from "../index.js";
|
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_IMAGE } from "../output-schemas.js";
|
|
4
4
|
import { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime } from "../datetime.js";
|
|
5
|
+
// The seven systems the engine accepts (internal/math/houses.go).
|
|
6
|
+
// Do not re-add alcabitius/morinus — see the note in natal.ts.
|
|
5
7
|
const HOUSE_SYSTEM_MAP = {
|
|
6
8
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
7
9
|
campanus: "C", regiomontanus: "R", porphyry: "O",
|
|
8
|
-
alcabitius: "B", morinus: "M",
|
|
9
10
|
};
|
|
10
11
|
registerTool({
|
|
11
12
|
name: "ephemeris_chart_wheel",
|
|
@@ -32,7 +33,7 @@ registerTool({
|
|
|
32
33
|
},
|
|
33
34
|
house_system: {
|
|
34
35
|
type: "string",
|
|
35
|
-
enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry"
|
|
36
|
+
enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry"],
|
|
36
37
|
description: "House system to use. Defaults to 'placidus' if omitted.",
|
|
37
38
|
},
|
|
38
39
|
style: {
|
|
@@ -85,14 +85,27 @@ registerTool({
|
|
|
85
85
|
},
|
|
86
86
|
});
|
|
87
87
|
// GET /electional/moment-analysis
|
|
88
|
+
//
|
|
89
|
+
// latitude/longitude are OPTIONAL here and required on ephemeris_electional
|
|
90
|
+
// above, deliberately. This tool is one of two ZERO_ARG_ENTRY_TOOLS
|
|
91
|
+
// (src/instructions.ts): the server instructions promise a host it "takes no
|
|
92
|
+
// arguments at all", and test/description-cross-references.test.ts asserts the
|
|
93
|
+
// schema keeps that promise. Requiring coordinates would turn the cold-open
|
|
94
|
+
// call — the first thing a model does for a user who has offered no data — into
|
|
95
|
+
// a validation error, which is the exact failure that list exists to prevent.
|
|
96
|
+
//
|
|
97
|
+
// The scoring risk is handled in the parameter descriptions instead: the Go
|
|
98
|
+
// handler falls back to a day chart without coordinates and says so in
|
|
99
|
+
// score_detail.sect_basis ("default_diurnal_no_coordinates"), so a caller that
|
|
100
|
+
// omits them gets a wrong-half-of-the-day score that is at least labelled.
|
|
88
101
|
registerTool({
|
|
89
102
|
name: "electional_moment_analysis",
|
|
90
103
|
description: "Analyze the astrological quality of a specific moment: planet positions, aspects, " +
|
|
91
|
-
"void of course status, lunar phase, day ruler, and an overall electional score (0-100). " +
|
|
104
|
+
"void of course status, lunar phase, day ruler, sect, and an overall electional score (0-100). " +
|
|
92
105
|
"Perfect for evaluating whether 'right now' or a specific date/time is good for action.\n\n" +
|
|
93
106
|
"CREDIT COST: 5 credits per call.\n\n" +
|
|
94
|
-
"EXAMPLE: Analyze March 21, 2026 at noon:\n" +
|
|
95
|
-
" date='2026-03-21T12:00:00Z'",
|
|
107
|
+
"EXAMPLE: Analyze March 21, 2026 at noon in New York:\n" +
|
|
108
|
+
" date='2026-03-21T12:00:00Z', latitude=40.7128, longitude=-74.0060",
|
|
96
109
|
inputSchema: {
|
|
97
110
|
type: "object",
|
|
98
111
|
properties: {
|
|
@@ -100,6 +113,14 @@ registerTool({
|
|
|
100
113
|
type: "string",
|
|
101
114
|
description: "ISO 8601 datetime to analyze, with a zone (e.g., '2026-03-21T12:00:00Z' or '2026-03-21T08:00:00-04:00'). Defaults to now.",
|
|
102
115
|
},
|
|
116
|
+
latitude: {
|
|
117
|
+
type: "number",
|
|
118
|
+
description: "Latitude of location in decimal degrees (positive = North). Resolve from a place name with location_search; never recall coordinates from memory. Optional, but pass both whenever the place is known — without them sect falls back to diurnal and a night moment is scored with the DAY triplicity rulers.",
|
|
119
|
+
},
|
|
120
|
+
longitude: {
|
|
121
|
+
type: "number",
|
|
122
|
+
description: "Longitude of location in decimal degrees (positive = East). Needed alongside latitude — day/night is a local fact that neither coordinate settles alone.",
|
|
123
|
+
},
|
|
103
124
|
format: {
|
|
104
125
|
type: "string",
|
|
105
126
|
enum: ["json", "llm"],
|
|
@@ -115,6 +136,13 @@ registerTool({
|
|
|
115
136
|
const query = {};
|
|
116
137
|
if (args.date)
|
|
117
138
|
query.date = args.date;
|
|
139
|
+
// `!== undefined`, not truthiness: latitude 0 (equator) and longitude 0
|
|
140
|
+
// (Greenwich) are real places, and a falsy check would drop them and
|
|
141
|
+
// silently score those elections as day charts.
|
|
142
|
+
if (args.latitude !== undefined)
|
|
143
|
+
query.latitude = args.latitude;
|
|
144
|
+
if (args.longitude !== undefined)
|
|
145
|
+
query.longitude = args.longitude;
|
|
118
146
|
if (args.format)
|
|
119
147
|
query.format = args.format;
|
|
120
148
|
return await getActiveClient().request("GET", "/electional/moment-analysis", {
|
|
@@ -276,7 +276,9 @@ registerTool({
|
|
|
276
276
|
registerTool({
|
|
277
277
|
name: "ephemeris_angles_points",
|
|
278
278
|
description: "Calculate chart angles and sensitive points (ASC, MC, DSC, IC, Vertex, " +
|
|
279
|
-
"
|
|
279
|
+
"Equatorial Ascendant/Descendant) for a given date/time and location. " +
|
|
280
|
+
"The equatorial ascendant is also returned under the key `east_point` — " +
|
|
281
|
+
"a synonym for the same point, not a separate one.\n\n" +
|
|
280
282
|
"CREDIT COST: 1 credit per call.",
|
|
281
283
|
inputSchema: {
|
|
282
284
|
type: "object",
|
|
@@ -2,11 +2,25 @@ 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
4
|
import { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime } from "../datetime.js";
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Map human-readable house system names to standard single-letter codes.
|
|
7
|
+
*
|
|
8
|
+
* These seven are the whole set. They are exactly what
|
|
9
|
+
* `CanonicalHouseSystemCode` accepts (`internal/math/houses.go`) and exactly
|
|
10
|
+
* what `GET /ephemeris/house-systems` returns.
|
|
11
|
+
*
|
|
12
|
+
* Do not add `alcabitius: "B"` or `morinus: "M"` back. They were advertised
|
|
13
|
+
* here through 4.4.0 and never worked: the codes "B" and "M" are accepted by
|
|
14
|
+
* no code path, so every call naming them came back
|
|
15
|
+
* `422 Validation error: house_system is invalid`. The engine does take the
|
|
16
|
+
* *slugs* "alcabitius"/"morinus", but only by silently aliasing them to Koch
|
|
17
|
+
* and Regiomontanus — so wiring the slug through would answer a request for
|
|
18
|
+
* one system with a different system's cusps. Neither is implemented; until
|
|
19
|
+
* one is, neither belongs in a schema.
|
|
20
|
+
*/
|
|
6
21
|
const HOUSE_SYSTEM_MAP = {
|
|
7
22
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
8
23
|
campanus: "C", regiomontanus: "R", porphyry: "O",
|
|
9
|
-
alcabitius: "B", morinus: "M",
|
|
10
24
|
};
|
|
11
25
|
registerTool({
|
|
12
26
|
name: "ephemeris_natal_chart",
|
|
@@ -37,7 +51,7 @@ registerTool({
|
|
|
37
51
|
},
|
|
38
52
|
house_system: {
|
|
39
53
|
type: "string",
|
|
40
|
-
enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry"
|
|
54
|
+
enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry"],
|
|
41
55
|
description: "House system to use. Defaults to 'placidus' if omitted.",
|
|
42
56
|
},
|
|
43
57
|
format: {
|
|
@@ -49,9 +63,34 @@ registerTool({
|
|
|
49
63
|
type: "boolean",
|
|
50
64
|
description: "Include Hermetic Lots / Arabic Parts in the natal response (routed as `options.include_hermetic_lots: true`). For a standalone lots-only payload without a full natal chart, use `/ephemeris/hermetic-lots` instead.",
|
|
51
65
|
},
|
|
52
|
-
|
|
66
|
+
zodiac_type: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: ["tropical", "sidereal"],
|
|
69
|
+
description: "Zodiac frame. Default tropical. `sidereal` subtracts the ayanamsa (~24°) from every " +
|
|
70
|
+
"longitude, so signs, dignities and element balance all move; aspects and house numbers " +
|
|
71
|
+
"do not. The response names the frame it used.",
|
|
72
|
+
},
|
|
73
|
+
ayanamsa: {
|
|
74
|
+
type: "string",
|
|
75
|
+
enum: ["lahiri", "fagan_bradley", "krishnamurti", "raman", "yukteshwar"],
|
|
76
|
+
description: "Sidereal system. Default lahiri. Only read when zodiac_type is `sidereal`.",
|
|
77
|
+
},
|
|
78
|
+
analytics: {
|
|
79
|
+
type: "array",
|
|
80
|
+
items: {
|
|
81
|
+
type: "string",
|
|
82
|
+
enum: ["element_balance", "hemisphere_emphasis", "aspect_patterns", "harmonics"],
|
|
83
|
+
},
|
|
84
|
+
description: "Optional whole-chart analytics, off by default: element_balance (fire/earth/air/water plus " +
|
|
85
|
+
"modality counts over the classical ten, unweighted), hemisphere_emphasis (above/below and " +
|
|
86
|
+
"east/west from the angles), aspect_patterns (aspect matrix + detected patterns), harmonics. " +
|
|
87
|
+
"No extra credits. Absent from format='llm'.",
|
|
88
|
+
},
|
|
89
|
+
include_minor_aspects: {
|
|
53
90
|
type: "boolean",
|
|
54
|
-
description: "
|
|
91
|
+
description: "Add quincunx, semisextile, semisquare, sesquiquadrate, quintile and biquintile to the aspect " +
|
|
92
|
+
"grid. Off by default; minors use their own orbs, so this widens the grid without changing " +
|
|
93
|
+
"any major aspect.",
|
|
55
94
|
},
|
|
56
95
|
include_visual: {
|
|
57
96
|
type: "boolean",
|
|
@@ -98,10 +137,43 @@ registerTool({
|
|
|
98
137
|
const code = HOUSE_SYSTEM_MAP[args.house_system] ?? args.house_system;
|
|
99
138
|
body.configuration = { house_system: code };
|
|
100
139
|
}
|
|
101
|
-
|
|
140
|
+
// Sidereal. The endpoint accepted zodiac_type on paper for as long as
|
|
141
|
+
// NatalConfig has existed and read it nowhere, so asking for a sidereal
|
|
142
|
+
// chart returned a tropical one with every body most of a whole sign
|
|
143
|
+
// out and nothing in the payload saying so. It is honoured now, which
|
|
144
|
+
// is the only reason this is exposed here.
|
|
145
|
+
if (args.zodiac_type === "sidereal") {
|
|
102
146
|
body.configuration = {
|
|
103
147
|
...(body.configuration || {}),
|
|
104
|
-
|
|
148
|
+
zodiac_type: "sidereal",
|
|
149
|
+
...(args.ayanamsa ? { ayanamsa: args.ayanamsa } : {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
else if (args.ayanamsa) {
|
|
153
|
+
// Naming a system and getting the tropical chart anyway is the same
|
|
154
|
+
// silence this parameter spent its whole life as. Say so instead.
|
|
155
|
+
throw new Error("ayanamsa only applies when zodiac_type is 'sidereal' — the tropical zodiac has no ayanamsa.");
|
|
156
|
+
}
|
|
157
|
+
// enhanced_options is where the Go engine keeps whole-chart analytics.
|
|
158
|
+
// The endpoint has always accepted these; nothing here ever sent them,
|
|
159
|
+
// so every one of these capabilities was unreachable from MCP.
|
|
160
|
+
if (Array.isArray(args.analytics) && args.analytics.length > 0) {
|
|
161
|
+
const want = new Set(args.analytics);
|
|
162
|
+
const enhanced = {};
|
|
163
|
+
if (want.has("element_balance"))
|
|
164
|
+
enhanced.element_balance = true;
|
|
165
|
+
if (want.has("hemisphere_emphasis"))
|
|
166
|
+
enhanced.hemisphere_emphasis = true;
|
|
167
|
+
if (want.has("aspect_patterns"))
|
|
168
|
+
enhanced.aspect_patterns = true;
|
|
169
|
+
if (want.has("harmonics"))
|
|
170
|
+
enhanced.include_harmonics = true;
|
|
171
|
+
body.enhanced_options = enhanced;
|
|
172
|
+
}
|
|
173
|
+
if (args.include_minor_aspects) {
|
|
174
|
+
body.options = {
|
|
175
|
+
...body.options,
|
|
176
|
+
include_minor_aspects: true,
|
|
105
177
|
};
|
|
106
178
|
}
|
|
107
179
|
if (args.include_arabic_parts) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openephemeris/mcp-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.0",
|
|
4
4
|
"description": "Model Context Protocol server for the Open Ephemeris astronomical computation API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"@types/adm-zip": "^0.5.8",
|
|
93
93
|
"@types/express": "^5.0.3",
|
|
94
94
|
"@types/node": "^25.0.1",
|
|
95
|
-
"adm-zip": "^0.
|
|
95
|
+
"adm-zip": "^0.6.0",
|
|
96
96
|
"cross-env": "^10.1.0",
|
|
97
97
|
"tsx": "^4.21.0",
|
|
98
98
|
"typescript": "^5.9.3",
|