@openephemeris/mcp-server 3.0.1 → 3.2.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.
Files changed (58) hide show
  1. package/README.md +32 -22
  2. package/config/dev-allowlist.json +1319 -1165
  3. package/dist/backend/client.d.ts +12 -0
  4. package/dist/backend/client.js +99 -35
  5. package/dist/index.js +5 -0
  6. package/dist/schema-packs/llm.d.ts +1 -1
  7. package/dist/schema-packs/llm.js +1 -1
  8. package/dist/scripts/dev-allowlist.d.ts +1 -0
  9. package/dist/scripts/dev-allowlist.js +287 -0
  10. package/dist/scripts/pack-audit.d.ts +1 -0
  11. package/dist/scripts/pack-audit.js +45 -0
  12. package/dist/scripts/schema-packs.d.ts +1 -0
  13. package/dist/scripts/schema-packs.js +150 -0
  14. package/dist/scripts/smoke-dev-profile.d.ts +1 -0
  15. package/dist/scripts/smoke-dev-profile.js +25 -0
  16. package/dist/scripts/sync-readme.d.ts +1 -0
  17. package/dist/scripts/sync-readme.js +141 -0
  18. package/dist/src/auth/credentials.d.ts +65 -0
  19. package/dist/src/auth/credentials.js +200 -0
  20. package/dist/src/auth/device-auth.d.ts +56 -0
  21. package/dist/src/auth/device-auth.js +144 -0
  22. package/dist/src/backend/client.d.ts +61 -0
  23. package/dist/src/backend/client.js +335 -0
  24. package/dist/src/index.d.ts +2 -0
  25. package/dist/src/index.js +92 -0
  26. package/dist/src/schema-packs/llm.d.ts +105 -0
  27. package/dist/src/schema-packs/llm.js +429 -0
  28. package/dist/src/tools/auth.d.ts +1 -0
  29. package/dist/src/tools/auth.js +202 -0
  30. package/dist/src/tools/dev.d.ts +1 -0
  31. package/dist/src/tools/dev.js +187 -0
  32. package/dist/src/tools/index.d.ts +25 -0
  33. package/dist/src/tools/index.js +33 -0
  34. package/dist/src/tools/specialized/eclipse.d.ts +1 -0
  35. package/dist/src/tools/specialized/eclipse.js +56 -0
  36. package/dist/src/tools/specialized/electional.d.ts +1 -0
  37. package/dist/src/tools/specialized/electional.js +79 -0
  38. package/dist/src/tools/specialized/human_design.d.ts +1 -0
  39. package/dist/src/tools/specialized/human_design.js +53 -0
  40. package/dist/src/tools/specialized/moon.d.ts +1 -0
  41. package/dist/src/tools/specialized/moon.js +50 -0
  42. package/dist/src/tools/specialized/natal.d.ts +1 -0
  43. package/dist/src/tools/specialized/natal.js +71 -0
  44. package/dist/src/tools/specialized/relocation.d.ts +1 -0
  45. package/dist/src/tools/specialized/relocation.js +71 -0
  46. package/dist/src/tools/specialized/synastry.d.ts +1 -0
  47. package/dist/src/tools/specialized/synastry.js +61 -0
  48. package/dist/src/tools/specialized/transits.d.ts +1 -0
  49. package/dist/src/tools/specialized/transits.js +80 -0
  50. package/dist/test/allowlist-and-tools.test.d.ts +1 -0
  51. package/dist/test/allowlist-and-tools.test.js +96 -0
  52. package/dist/test/backend-client.test.d.ts +1 -0
  53. package/dist/test/backend-client.test.js +286 -0
  54. package/dist/test/credentials.test.d.ts +1 -0
  55. package/dist/test/credentials.test.js +143 -0
  56. package/dist/tools/dev.js +7 -3
  57. package/dist/tools/index.d.ts +7 -0
  58. package/package.json +3 -3
@@ -0,0 +1,202 @@
1
+ import { registerTool } from "./index.js";
2
+ import { CredentialManager } from "../auth/credentials.js";
3
+ import { DeviceAuthFlow } from "../auth/device-auth.js";
4
+ const credentialManager = new CredentialManager();
5
+ // ────────────────────────────────────────────────────────
6
+ // auth.login — Start the device authorization flow
7
+ // ────────────────────────────────────────────────────────
8
+ registerTool({
9
+ name: "auth.login",
10
+ description: "Start the device authorization flow to connect this MCP server to your OpenEphemeris account. " +
11
+ "Returns a verification URL and code for the user to enter in their browser. " +
12
+ "The MCP server will then automatically receive credentials and all API calls will be " +
13
+ "linked to the user's account (tier, credits, rate limits). " +
14
+ "Only needed if no OPENEPHEMERIS_API_KEY env var is set and no cached credentials exist.",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {},
18
+ additionalProperties: false,
19
+ },
20
+ annotations: {
21
+ title: "Connect Account",
22
+ readOnlyHint: false,
23
+ destructiveHint: false,
24
+ },
25
+ handler: async () => {
26
+ // Check if already authenticated
27
+ const status = credentialManager.getStatus();
28
+ if (status.authenticated) {
29
+ return {
30
+ already_authenticated: true,
31
+ email: status.email,
32
+ user_id: status.userId,
33
+ expires_at: status.expiresAt,
34
+ message: `Already authenticated as ${status.email}. Use auth.logout to disconnect.`,
35
+ };
36
+ }
37
+ // Check if env var credentials are set
38
+ if (process.env.OPENEPHEMERIS_API_KEY ||
39
+ process.env.OPENEPHEMERIS_JWT ||
40
+ process.env.OPENEPHEMERIS_SERVICE_KEY) {
41
+ return {
42
+ already_authenticated: true,
43
+ method: "environment_variable",
44
+ message: "Credentials are configured via environment variables. " +
45
+ "Device auth is not needed.",
46
+ };
47
+ }
48
+ // Start the device auth flow
49
+ const flow = new DeviceAuthFlow();
50
+ const startResult = await flow.start();
51
+ // Begin polling in the background — this runs until the user authorizes
52
+ // or the code expires. We don't await it here because the tool should
53
+ // return immediately with the code for the user.
54
+ flow
55
+ .poll(startResult.device_code, (attempt) => {
56
+ if (attempt % 6 === 0) {
57
+ console.error(`Still waiting... Visit ${startResult.verification_uri} and enter code: ${startResult.user_code}`);
58
+ }
59
+ })
60
+ .then((result) => {
61
+ console.error(`✅ Authenticated as ${result.user?.email || "user"}`);
62
+ console.error(` Credentials saved to ${CredentialManager.credentialsPath}`);
63
+ })
64
+ .catch((err) => {
65
+ console.error(`❌ Device auth failed: ${err.message}`);
66
+ });
67
+ return {
68
+ action_required: true,
69
+ verification_uri: startResult.verification_uri,
70
+ user_code: startResult.user_code,
71
+ verification_uri_complete: startResult.verification_uri_complete,
72
+ expires_in: startResult.expires_in,
73
+ message: `Please visit ${startResult.verification_uri} and enter the code: ${startResult.user_code}. ` +
74
+ `Alternatively, open this direct link: ${startResult.verification_uri_complete}. ` +
75
+ `The server is polling in the background — once you authorize, all subsequent ` +
76
+ `tool calls will be linked to your account automatically.`,
77
+ instructions_for_agent: "Present the verification URL and code to the user clearly. " +
78
+ "They need to open the URL in a browser, log in to their OpenEphemeris account, " +
79
+ "and enter the code. After that, all API calls will work automatically. " +
80
+ "Wait 10-15 seconds after the user confirms they've entered the code, " +
81
+ "then try the original request again.",
82
+ };
83
+ },
84
+ });
85
+ // ────────────────────────────────────────────────────────
86
+ // auth.status — Check current authentication state
87
+ // ────────────────────────────────────────────────────────
88
+ registerTool({
89
+ name: "auth.status",
90
+ description: "Check the current authentication status of this MCP server. " +
91
+ "Shows whether the server is authenticated, which account it's linked to, " +
92
+ "the authentication method (API key, JWT, device auth), and token expiry.",
93
+ inputSchema: {
94
+ type: "object",
95
+ properties: {},
96
+ additionalProperties: false,
97
+ },
98
+ annotations: {
99
+ title: "Auth Status",
100
+ readOnlyHint: true,
101
+ destructiveHint: false,
102
+ },
103
+ handler: async () => {
104
+ // Check env var methods first
105
+ const hasApiKey = !!(process.env.OPENEPHEMERIS_API_KEY ||
106
+ process.env.ASTROMCP_API_KEY ||
107
+ process.env.MERIDIAN_API_KEY);
108
+ const hasJwt = !!(process.env.OPENEPHEMERIS_JWT ||
109
+ process.env.ASTROMCP_JWT ||
110
+ process.env.MERIDIAN_AUTH_TOKEN);
111
+ const hasServiceKey = !!(process.env.OPENEPHEMERIS_SERVICE_KEY ||
112
+ process.env.ASTROMCP_SERVICE_KEY ||
113
+ process.env.MERIDIAN_SERVICE_KEY);
114
+ if (hasServiceKey) {
115
+ return {
116
+ authenticated: true,
117
+ method: "service_key",
118
+ message: "Authenticated via service key (admin/internal access).",
119
+ };
120
+ }
121
+ if (hasApiKey) {
122
+ return {
123
+ authenticated: true,
124
+ method: "api_key",
125
+ message: "Authenticated via API key environment variable.",
126
+ };
127
+ }
128
+ if (hasJwt) {
129
+ return {
130
+ authenticated: true,
131
+ method: "jwt_env_var",
132
+ message: "Authenticated via JWT environment variable.",
133
+ };
134
+ }
135
+ // Check cached credentials
136
+ const status = credentialManager.getStatus();
137
+ if (status.authenticated) {
138
+ return {
139
+ authenticated: true,
140
+ method: "device_auth",
141
+ email: status.email,
142
+ user_id: status.userId,
143
+ expires_at: status.expiresAt,
144
+ credentials_path: CredentialManager.credentialsPath,
145
+ message: `Authenticated as ${status.email} via device authorization.`,
146
+ };
147
+ }
148
+ // Check if credentials exist but are expired
149
+ const creds = credentialManager.load();
150
+ if (creds) {
151
+ return {
152
+ authenticated: false,
153
+ method: "device_auth_expired",
154
+ email: creds.user_email,
155
+ expired_at: creds.expires_at,
156
+ message: "Device auth credentials exist but are expired. " +
157
+ "The server will attempt to refresh automatically on the next API call, " +
158
+ "or you can run auth.login to re-authenticate.",
159
+ };
160
+ }
161
+ return {
162
+ authenticated: false,
163
+ method: "none",
164
+ message: "Not authenticated. Set OPENEPHEMERIS_API_KEY in your MCP server config, " +
165
+ "or run auth.login to connect your account interactively.",
166
+ };
167
+ },
168
+ });
169
+ // ────────────────────────────────────────────────────────
170
+ // auth.logout — Clear cached credentials
171
+ // ────────────────────────────────────────────────────────
172
+ registerTool({
173
+ name: "auth.logout",
174
+ description: "Disconnect this MCP server from your OpenEphemeris account by clearing " +
175
+ "cached credentials. Does NOT revoke the API key if one is set via environment " +
176
+ "variable — only clears device-auth cached credentials.",
177
+ inputSchema: {
178
+ type: "object",
179
+ properties: {},
180
+ additionalProperties: false,
181
+ },
182
+ annotations: {
183
+ title: "Disconnect Account",
184
+ readOnlyHint: false,
185
+ destructiveHint: true,
186
+ },
187
+ handler: async () => {
188
+ const status = credentialManager.getStatus();
189
+ if (!status.authenticated && !credentialManager.load()) {
190
+ return {
191
+ success: true,
192
+ message: "No cached credentials to clear.",
193
+ };
194
+ }
195
+ credentialManager.clear();
196
+ return {
197
+ success: true,
198
+ previous_email: status.email,
199
+ message: `Disconnected. Cached credentials for ${status.email || "unknown user"} have been removed.`,
200
+ };
201
+ },
202
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,187 @@
1
+ import { registerTool } from "./index.js";
2
+ import { backendClient } from "../backend/client.js";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ function getAllowlistPath() {
7
+ const envPath = process.env.OPENEPHEMERIS_DEV_ALLOWLIST_PATH || process.env.ASTROMCP_DEV_ALLOWLIST_PATH;
8
+ if (envPath && envPath.trim())
9
+ return envPath;
10
+ // dist lives at mcp-server/dist; src lives at mcp-server/src.
11
+ // We resolve relative to current file location for both dev+prod.
12
+ const here = path.dirname(fileURLToPath(import.meta.url));
13
+ // here: .../dist/tools OR .../src/tools
14
+ const mcpServerRoot = path.resolve(here, "..", "..");
15
+ return path.join(mcpServerRoot, "config", "dev-allowlist.json");
16
+ }
17
+ function loadAllowlist() {
18
+ const allowlistPath = getAllowlistPath();
19
+ const raw = fs.readFileSync(allowlistPath, "utf-8");
20
+ const parsed = JSON.parse(raw);
21
+ if (!parsed || parsed.schema !== "astromcp-dev-allowlist-v1" || !Array.isArray(parsed.allow)) {
22
+ throw new Error(`Invalid allowlist file at ${allowlistPath}. Expected schema astromcp-dev-allowlist-v1.`);
23
+ }
24
+ return parsed;
25
+ }
26
+ function isDeniedByPrefix(pathname, prefixes) {
27
+ return prefixes.some((p) => pathname.startsWith(p));
28
+ }
29
+ function isAllowedOperation(method, pathname, allow) {
30
+ return allow.some((e) => e.method === method && e.path === pathname);
31
+ }
32
+ registerTool({
33
+ name: "dev.call",
34
+ description: "Call any allowlisted Open Ephemeris API endpoint directly. This is the power-user escape hatch " +
35
+ "— use the typed tools (ephemeris.natal_chart, ephemeris.transits, etc.) first for common operations. " +
36
+ "Call dev.list_allowed to see all currently available endpoint paths.\n\n" +
37
+ "AUTH: Set OPENEPHEMERIS_API_KEY in your environment. See openephemeris.com/dashboard for active plan limits.\n\n" +
38
+ "CREDIT COSTS:\n" +
39
+ " • Standard chart math (natal, progressed, bazi, HD): 1 credit\n" +
40
+ " • Visualization rendering (chart-wheel, bi-wheel, charts/*): 2 credits\n" +
41
+ " • Comparative math (synastry, composite, overlay): 3 credits\n" +
42
+ " • Predictive ops (transits, returns, transit-chart): 5 credits\n" +
43
+ " • ACG / astrocartography: 5 credits\n" +
44
+ " • Catalog / metadata / health endpoints: 0 credits\n" +
45
+ " • format=llm (token-optimized output): available on all tiers\n\n" +
46
+ "COMMON CALLS:\n" +
47
+ " POST /ephemeris/natal-chart — Full natal chart (body: {datetime, latitude, longitude})\n" +
48
+ " POST /ephemeris/natal/batch — Up to 50 natal charts in one request\n" +
49
+ " POST /ephemeris/relocation — Relocated chart (same natal, new location)\n" +
50
+ " POST /predictive/transits/search — Transit event search over a date range\n" +
51
+ " POST /predictive/returns/solar — Solar return chart\n" +
52
+ " POST /predictive/returns/lunar — Lunar return chart\n" +
53
+ " POST /comparative/synastry — Two-person synastry chart\n" +
54
+ " POST /comparative/composite — Composite (midpoint) chart\n" +
55
+ " POST /human-design/chart — Full HD bodygraph\n" +
56
+ " GET /ephemeris/moon/phase — Current/queried moon phase\n" +
57
+ " GET /ephemeris/moon/void-of-course — Next void-of-course period\n" +
58
+ " GET /ephemeris/agro/daily — Biodynamic farming day quality\n" +
59
+ " GET /ephemeris/agro/calendar — Multi-day biodynamic calendar\n" +
60
+ " GET /ephemeris/agro/void-of-course — Biodynamic VoC periods\n" +
61
+ " GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
62
+ " GET /eclipse/solar/local — Local solar eclipse (query: latitude, longitude)\n" +
63
+ " GET /eclipse/next-visible — Next eclipse visible from a location\n" +
64
+ " GET /tidal/forcing — Gravitational tidal forcing index\n" +
65
+ " GET /tidal/forcing/deep-time — Extended tidal deep-time analysis\n" +
66
+ " POST /acg/power-lines — Astrocartography power lines (lat/lon GeoJSON)\n" +
67
+ " POST /acg/hits — ACG power at a specific location\n" +
68
+ " GET /calendar/astrology/moon-phases — Moon phase calendar for a date range\n" +
69
+ " GET /location/autocomplete — Geocode a place name (query: q=City Name)\n" +
70
+ " POST /timezone/lookup — Resolve timezone + UTC offset for a location\n" +
71
+ " POST /chinese/bazi — Chinese Ba Zi (Four Pillars) chart\n" +
72
+ " GET /chinese/zodiac — Chinese zodiac year element/animal\n" +
73
+ " POST /vedic/chart — Vedic (Jyotish) natal chart\n" +
74
+ " GET /catalogs/bodies — List all supported celestial bodies\n\n" +
75
+ "BINARY RESPONSES:\n" +
76
+ " • Binary/image endpoints return {content_type, content_length, encoding, data_base64}\n" +
77
+ " so callers can decode bytes deterministically.\n\n" +
78
+ "ECLIPSE NOTE: Eclipse endpoints accept format=llm via the query param like other endpoints.\n\n" +
79
+ "format=llm NOTE: Add query: {format: 'llm'} to natal/synastry/composite/HD endpoints for " +
80
+ "compact columnar output optimized for LLM token budgets (availability depends on your current plan).",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: {
84
+ method: {
85
+ type: "string",
86
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
87
+ description: "HTTP method",
88
+ },
89
+ path: {
90
+ type: "string",
91
+ description: "Absolute API path (e.g., /ephemeris/natal-chart)",
92
+ },
93
+ query: {
94
+ type: "object",
95
+ additionalProperties: true,
96
+ description: "Query params for GET requests (optional)",
97
+ },
98
+ body: {
99
+ description: "JSON body for POST/PUT/PATCH (optional)",
100
+ },
101
+ preset: {
102
+ type: "string",
103
+ enum: ["full", "simple"],
104
+ description: "Convenience: if provided, set query.preset (optional).",
105
+ },
106
+ format: {
107
+ type: "string",
108
+ enum: ["json", "llm", "llm_v2"],
109
+ description: "Convenience: if provided, set query.format (optional). 'llm' is canonical; 'llm_v2' is accepted as a legacy alias.",
110
+ },
111
+ output_mode: {
112
+ type: "string",
113
+ enum: ["full", "simple", "llm", "llm_v2"],
114
+ description: "Legacy convenience (deprecated): if provided, set query.output_mode and also map to query.preset/query.format when possible.",
115
+ },
116
+ },
117
+ required: ["method", "path"],
118
+ additionalProperties: false,
119
+ },
120
+ handler: async (args) => {
121
+ const method = String(args.method || "").toUpperCase();
122
+ const pathname = String(args.path || "");
123
+ if (!pathname.startsWith("/")) {
124
+ throw new Error("path must start with '/'");
125
+ }
126
+ const allowlist = loadAllowlist();
127
+ const denyPrefixes = allowlist.deny?.path_prefixes ?? [];
128
+ if (denyPrefixes.length > 0 && isDeniedByPrefix(pathname, denyPrefixes)) {
129
+ throw new Error(`Endpoint denied by policy: ${pathname}`);
130
+ }
131
+ if (!isAllowedOperation(method, pathname, allowlist.allow)) {
132
+ throw new Error(`Endpoint not allowlisted: ${method} ${pathname}`);
133
+ }
134
+ const queryBase = args.query && typeof args.query === "object" ? args.query : {};
135
+ const query = { ...queryBase };
136
+ const body = args.body;
137
+ if (args.preset) {
138
+ query.preset = args.preset;
139
+ }
140
+ if (args.format) {
141
+ const rawFormat = String(args.format).trim().toLowerCase();
142
+ // Canonical: "llm". Normalize legacy alias "llm_v2" → "llm".
143
+ query.format = rawFormat === "llm_v2" ? "llm" : rawFormat;
144
+ }
145
+ // Legacy: output_mode used to overload both compute preset and output projection.
146
+ // Keep forwarding it for older endpoints, but also map it to preset/format for new endpoints.
147
+ if (args.output_mode) {
148
+ const rawMode = String(args.output_mode).trim().toLowerCase();
149
+ query.output_mode = rawMode;
150
+ if ((rawMode === "full" || rawMode === "simple") && query.preset == null) {
151
+ query.preset = rawMode;
152
+ }
153
+ if ((rawMode === "llm" || rawMode === "llm_v2") && query.format == null) {
154
+ query.format = "llm";
155
+ }
156
+ }
157
+ return await backendClient.request(method, pathname, {
158
+ params: query,
159
+ data: body,
160
+ });
161
+ },
162
+ });
163
+ registerTool({
164
+ name: "dev.list_allowed",
165
+ description: "List all API operations (method + path) that this MCP instance is authorized to call. " +
166
+ "Returns endpoint entries grouped by method, plus the active deny rules. " +
167
+ "Use this to discover what's available before calling dev.call, or to verify an endpoint path. " +
168
+ "Typed shortcut tools (ephemeris.natal_chart, ephemeris.transits, etc.) cover the most common operations — " +
169
+ "check those first before reaching for dev.call.",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {},
173
+ additionalProperties: false,
174
+ },
175
+ handler: async () => {
176
+ const allowlist = loadAllowlist();
177
+ return {
178
+ schema: allowlist.schema,
179
+ generated_from: allowlist.generated_from,
180
+ deny: allowlist.deny ?? null,
181
+ allow: allowlist.allow,
182
+ counts: {
183
+ allow: allowlist.allow.length,
184
+ },
185
+ };
186
+ },
187
+ });
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ export interface ToolDefinition {
3
+ name: string;
4
+ description: string;
5
+ inputSchema: z.ZodType<any> | Record<string, unknown>;
6
+ annotations?: {
7
+ title?: string;
8
+ readOnlyHint?: boolean;
9
+ destructiveHint?: boolean;
10
+ idempotentHint?: boolean;
11
+ openWorldHint?: boolean;
12
+ };
13
+ handler: (args: any) => Promise<any>;
14
+ }
15
+ export declare const toolRegistry: Record<string, ToolDefinition>;
16
+ export declare function registerTool(tool: ToolDefinition): void;
17
+ export type ToolProfile = "dev" | "legacy";
18
+ /**
19
+ * Initializes tool modules.
20
+ *
21
+ * - `dev`: registers the allowlist-gated generic call tools AND all specialized
22
+ * domain tools (natal chart, transits, moon phase, eclipse, synastry, HD).
23
+ * - `legacy`: registers only the generic tools (back-compat).
24
+ */
25
+ export declare function initTools(profile?: ToolProfile): Promise<void>;
@@ -0,0 +1,33 @@
1
+ export const toolRegistry = {};
2
+ export function registerTool(tool) {
3
+ toolRegistry[tool.name] = tool;
4
+ }
5
+ let toolsInitialized = false;
6
+ /**
7
+ * Initializes tool modules.
8
+ *
9
+ * - `dev`: registers the allowlist-gated generic call tools AND all specialized
10
+ * domain tools (natal chart, transits, moon phase, eclipse, synastry, HD).
11
+ * - `legacy`: registers only the generic tools (back-compat).
12
+ */
13
+ export async function initTools(profile) {
14
+ if (toolsInitialized)
15
+ return;
16
+ toolsInitialized = true;
17
+ const resolvedProfile = (profile || process.env.OPENEPHEMERIS_PROFILE || process.env.ASTROMCP_PROFILE || "dev").toLowerCase();
18
+ // Always register auth tools (available in all profiles).
19
+ await import("./auth.js");
20
+ // Always register the generic proxy tools (dev.call + dev.list_allowed).
21
+ await import("./dev.js");
22
+ if (resolvedProfile === "dev") {
23
+ // Register all specialized domain tools.
24
+ await import("./specialized/natal.js");
25
+ await import("./specialized/transits.js");
26
+ await import("./specialized/moon.js");
27
+ await import("./specialized/eclipse.js");
28
+ await import("./specialized/human_design.js");
29
+ await import("./specialized/synastry.js");
30
+ await import("./specialized/relocation.js");
31
+ await import("./specialized/electional.js");
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,56 @@
1
+ import { registerTool } from "../index.js";
2
+ import { backendClient } from "../../backend/client.js";
3
+ registerTool({
4
+ name: "ephemeris.next_eclipse",
5
+ description: "Find the next solar or lunar eclipse visible from a given location (or globally). " +
6
+ "Returns the eclipse type, date/time of maximum, magnitude, duration of totality (if any), " +
7
+ "and local contact times if coordinates are provided.\n\n" +
8
+ "CREDIT COST: 1 credit per call.\n\n" +
9
+ "EXAMPLE: Find the next solar eclipse visible from New York:\n" +
10
+ " eclipse_type='solar', latitude=40.7128, longitude=-74.006\n\n" +
11
+ "EXAMPLE: Find the next lunar eclipse globally:\n" +
12
+ " eclipse_type='lunar'",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {
16
+ eclipse_type: {
17
+ type: "string",
18
+ enum: ["solar", "lunar"],
19
+ description: "Eclipse type to search for.",
20
+ },
21
+ latitude: {
22
+ type: "number",
23
+ description: "Observer latitude in decimal degrees. If provided, returns local visibility and contact times.",
24
+ },
25
+ longitude: {
26
+ type: "number",
27
+ description: "Observer longitude in decimal degrees.",
28
+ },
29
+ after_date: {
30
+ type: "string",
31
+ description: "ISO 8601 date to search after (e.g. '2026-01-01'). Defaults to today if omitted.",
32
+ },
33
+ },
34
+ required: ["eclipse_type"],
35
+ additionalProperties: false,
36
+ },
37
+ handler: async (args) => {
38
+ const params = {};
39
+ if (args.latitude != null)
40
+ params.latitude = args.latitude;
41
+ if (args.longitude != null)
42
+ params.longitude = args.longitude;
43
+ if (args.after_date)
44
+ params.date = args.after_date;
45
+ if (args.eclipse_type === "solar") {
46
+ // Use local endpoint if coordinates provided, otherwise global
47
+ if (args.latitude != null && args.longitude != null) {
48
+ return await backendClient.request("GET", "/eclipse/solar/local", { params, timeoutMs: 60_000 });
49
+ }
50
+ return await backendClient.request("GET", "/eclipse/solar/global", { params, timeoutMs: 60_000 });
51
+ }
52
+ else {
53
+ return await backendClient.request("GET", "/eclipse/lunar/global", { params, timeoutMs: 60_000 });
54
+ }
55
+ },
56
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,79 @@
1
+ import { registerTool } from "../index.js";
2
+ import { backendClient } from "../../backend/client.js";
3
+ registerTool({
4
+ name: "ephemeris.electional",
5
+ description: "Find optimal planetary timing windows (electional astrology). Scans a date range to find the " +
6
+ "best times for an event based on essential dignity, aspect quality, sect, and void-of-course " +
7
+ "moon penalties. Evaluates every hour and clusters the best continuous windows.\n\n" +
8
+ "CREDIT COST: 5 credits per call (heavy calculation).\n\n" +
9
+ "EXAMPLE: Find the best time to launch a business in early March 2026.\n" +
10
+ " start_date='2026-03-01', end_date='2026-03-10', latitude=40.7128, longitude=-74.0060,\n" +
11
+ " avoid_voc=true, lunar_phase='waxing'",
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ start_date: {
16
+ type: "string",
17
+ description: "ISO 8601 start date or datetime for the search window (e.g., 2026-03-01).",
18
+ },
19
+ end_date: {
20
+ type: "string",
21
+ description: "ISO 8601 end date or datetime for the search window.",
22
+ },
23
+ latitude: {
24
+ type: "number",
25
+ description: "Latitude of location in decimal degrees (positive = North).",
26
+ },
27
+ longitude: {
28
+ type: "number",
29
+ description: "Longitude of location in decimal degrees (positive = East).",
30
+ },
31
+ max_results: {
32
+ type: "number",
33
+ description: "Maximum number of top windows to return (default 5).",
34
+ },
35
+ avoid_retrograde: {
36
+ type: "string",
37
+ description: "Comma-separated list of planets to avoid when retrograde (e.g., 'mercury,venus').",
38
+ },
39
+ lunar_phase: {
40
+ type: "string",
41
+ enum: ["waxing", "waning", "new", "full", "any"],
42
+ description: "Filter windows by lunar phase. Defaults to 'any'.",
43
+ },
44
+ avoid_voc: {
45
+ type: "boolean",
46
+ description: "If true, strictly ignores any moments where the Moon is Void of Course.",
47
+ },
48
+ format: {
49
+ type: "string",
50
+ enum: ["json", "llm"],
51
+ description: "Output format. 'llm' = compact token-efficient output (available on all tiers).",
52
+ },
53
+ },
54
+ required: ["start_date", "end_date", "latitude", "longitude"],
55
+ additionalProperties: false,
56
+ },
57
+ handler: async (args) => {
58
+ const query = {
59
+ start_date: args.start_date,
60
+ end_date: args.end_date,
61
+ latitude: args.latitude,
62
+ longitude: args.longitude,
63
+ };
64
+ if (args.max_results !== undefined)
65
+ query.max_results = args.max_results;
66
+ if (args.avoid_retrograde)
67
+ query.avoid_retrograde = args.avoid_retrograde;
68
+ if (args.lunar_phase)
69
+ query.lunar_phase = args.lunar_phase;
70
+ if (args.avoid_voc !== undefined)
71
+ query.avoid_voc = args.avoid_voc;
72
+ if (args.format)
73
+ query.format = args.format;
74
+ return await backendClient.request("GET", "/electional/find-window", {
75
+ data: {},
76
+ params: query,
77
+ });
78
+ },
79
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { registerTool } from "../index.js";
2
+ import { backendClient } from "../../backend/client.js";
3
+ registerTool({
4
+ name: "human_design.chart",
5
+ description: "Calculate a full Human Design bodygraph chart from birth data. Returns the person's Type " +
6
+ "(Generator, Manifesting Generator, Projector, Manifestor, Reflector), Strategy, Authority, " +
7
+ "Profile (e.g. 1/3, 2/4), defined and undefined Centers, activated Gates and Channels, " +
8
+ "Incarnation Cross, and both Personality (conscious) and Design (unconscious) planetary positions.\n\n" +
9
+ "CREDIT COST: 1 credit per call.\n\n" +
10
+ "Human Design uses two calculation moments: the birth time (Personality) and ~88° of Sun motion " +
11
+ "before birth (~3 months prior, the Design calculation). The API handles this automatically.\n\n" +
12
+ "EXAMPLE: Get the Human Design chart for someone born April 15, 1990 at 2:30 PM in Chicago:\n" +
13
+ " datetime='1990-04-15T14:30:00', latitude=41.8781, longitude=-87.6298",
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {
17
+ datetime: {
18
+ type: "string",
19
+ description: "ISO 8601 birth datetime (local time at birth location), e.g. '1990-04-15T14:30:00'.",
20
+ },
21
+ latitude: {
22
+ type: "number",
23
+ description: "Latitude of birth location in decimal degrees (positive = North).",
24
+ },
25
+ longitude: {
26
+ type: "number",
27
+ description: "Longitude of birth location in decimal degrees (positive = East).",
28
+ },
29
+ format: {
30
+ type: "string",
31
+ enum: ["json", "llm"],
32
+ description: "Output format. 'llm' returns compact array projection for token efficiency (available on all tiers). " +
33
+ "'json' returns verbose full output.",
34
+ },
35
+ },
36
+ required: ["datetime", "latitude", "longitude"],
37
+ additionalProperties: false,
38
+ },
39
+ handler: async (args) => {
40
+ const body = {
41
+ datetime: args.datetime,
42
+ latitude: args.latitude,
43
+ longitude: args.longitude,
44
+ };
45
+ const query = {};
46
+ if (args.format)
47
+ query.format = args.format;
48
+ return await backendClient.request("POST", "/human-design/chart", {
49
+ data: body,
50
+ params: query,
51
+ });
52
+ },
53
+ });
@@ -0,0 +1 @@
1
+ export {};