@openephemeris/mcp-server 3.2.7 → 3.2.9
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/README.md +8 -6
- package/dist/src/index.js +19 -0
- package/dist/src/tools/index.js +2 -0
- package/dist/src/tools/specialized/bi_wheel.d.ts +1 -0
- package/dist/src/tools/specialized/bi_wheel.js +66 -0
- package/dist/src/tools/specialized/chart_wheel.d.ts +1 -0
- package/dist/src/tools/specialized/chart_wheel.js +64 -0
- package/dist/src/tools/specialized/transits.js +66 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -179,8 +179,8 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
179
179
|
|
|
180
180
|
- Allowlisted operations: **97**
|
|
181
181
|
- Methods: `GET=41`, `POST=56`, `PUT=0`, `PATCH=0`, `DELETE=0`
|
|
182
|
-
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **
|
|
183
|
-
- Typed tools: `auth_login`, `auth_logout`, `auth_status`, `chinese_bazi`, `dev_call`, `dev_list_allowed`, `ephemeris_electional`, `ephemeris_moon_phase`, `ephemeris_natal_chart`, `ephemeris_next_eclipse`, `ephemeris_relocation`, `ephemeris_synastry`, `ephemeris_transits`, `human_design_chart`, `vedic_chart`
|
|
182
|
+
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **17**
|
|
183
|
+
- Typed tools: `auth_login`, `auth_logout`, `auth_status`, `chinese_bazi`, `dev_call`, `dev_list_allowed`, `ephemeris_bi_wheel`, `ephemeris_chart_wheel`, `ephemeris_electional`, `ephemeris_moon_phase`, `ephemeris_natal_chart`, `ephemeris_next_eclipse`, `ephemeris_relocation`, `ephemeris_synastry`, `ephemeris_transits`, `human_design_chart`, `vedic_chart`
|
|
184
184
|
- Generic tools:
|
|
185
185
|
|
|
186
186
|
### Allowlist Families
|
|
@@ -206,9 +206,11 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
206
206
|
| `visualization` | 2 | `POST /visualization/bi-wheel`, `POST /visualization/chart-wheel` |
|
|
207
207
|
<!-- GENERATED:RUNTIME_SNAPSHOT:END -->
|
|
208
208
|
|
|
209
|
+
## Why OpenEphemeris for AI Agents?
|
|
209
210
|
|
|
210
|
-
|
|
211
|
+
Most LLMs (like Claude and ChatGPT) struggle heavily with astronomical calculations (trigonometry, Julian date conversions, and Swiss Ephemeris lookups). OpenEphemeris serves as a **secure, remote math engine**.
|
|
211
212
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
By pairing LLMs with the OpenEphemeris MCP server, your agents can instantly access:
|
|
214
|
+
- **Zero-hallucination coordinates**: Direct, sub-arcsecond NASA JPL DE440/DE441 calculations.
|
|
215
|
+
- **LLM-optimized tokens (`format=llm`)**: We compress standard 25,000 token JSON chart responses into minimal text blocks, cutting your inference costs by 50%.
|
|
216
|
+
- **Ready-to-use astrology layers**: Built-in support for Astrocartography geoJSON lines, Hermetic Lots, Fixed Stars, and complex Human Design matrix generation.
|
package/dist/src/index.js
CHANGED
|
@@ -105,6 +105,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
105
105
|
const result = await tool.handler(request.params.arguments ?? {});
|
|
106
106
|
const durationMs = Date.now() - startTime;
|
|
107
107
|
console.error(`[MCP] ✅ Success: ${toolName} (${durationMs}ms)`);
|
|
108
|
+
// Intercept binary backend responses (like Chart Wheels) to return native MCP Images
|
|
109
|
+
if (result &&
|
|
110
|
+
typeof result === "object" &&
|
|
111
|
+
"encoding" in result &&
|
|
112
|
+
result.encoding === "base64" &&
|
|
113
|
+
"data_base64" in result &&
|
|
114
|
+
"content_type" in result) {
|
|
115
|
+
const binResp = result;
|
|
116
|
+
console.error(`[MCP] 🎨 Returning image (${binResp.content_length} bytes) for ${toolName}`);
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "image",
|
|
121
|
+
data: binResp.data_base64,
|
|
122
|
+
mimeType: binResp.content_type,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
108
127
|
const jsonStr = JSON.stringify(result, null, 2);
|
|
109
128
|
// Safety limit: ~100kb JSON is roughly 20-30k tokens.
|
|
110
129
|
// Usually happens if LLMs ask for 50+ years of transits or raw ephemeris arrays.
|
package/dist/src/tools/index.js
CHANGED
|
@@ -31,6 +31,8 @@ export async function initTools(profile) {
|
|
|
31
31
|
await import("./specialized/electional.js");
|
|
32
32
|
await import("./specialized/vedic.js");
|
|
33
33
|
await import("./specialized/bazi.js");
|
|
34
|
+
await import("./specialized/chart_wheel.js");
|
|
35
|
+
await import("./specialized/bi_wheel.js");
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { registerTool, validateRequired } from "../index.js";
|
|
2
|
+
import { backendClient } from "../../backend/client.js";
|
|
3
|
+
const DATETIME_DESC = "ISO 8601 datetime string, e.g. '1990-04-15T14:30:00' (local time at birth location). " +
|
|
4
|
+
"Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.";
|
|
5
|
+
const HOUSE_SYSTEM_MAP = {
|
|
6
|
+
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
7
|
+
campanus: "C", regiomontanus: "R", porphyry: "O",
|
|
8
|
+
alcabitius: "B", morinus: "M",
|
|
9
|
+
};
|
|
10
|
+
registerTool({
|
|
11
|
+
name: "ephemeris_bi_wheel",
|
|
12
|
+
description: "Generate a Bi-Wheel (Synastry/Transit) image (PNG) comparing two charts. Draws Subject A's planets on the inside wheel and Subject B's on the outside wheel. The tool returns a raw image (base64) which most Vision-enabled AI agents can display.\n\n" +
|
|
13
|
+
"CREDIT COST: 2 credits per call.\n\n" +
|
|
14
|
+
"EXAMPLE: Compare someone born April 15, 1990 (A) to someone born June 10, 1992 (B):\n" +
|
|
15
|
+
" datetime_a='...', latitude_a=..., longitude_a=..., datetime_b='...', latitude_b=..., longitude_b=...",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
datetime_a: { type: "string", description: DATETIME_DESC },
|
|
20
|
+
latitude_a: { type: "number", description: "Latitude for subject A" },
|
|
21
|
+
longitude_a: { type: "number", description: "Longitude for subject A" },
|
|
22
|
+
datetime_b: { type: "string", description: DATETIME_DESC },
|
|
23
|
+
latitude_b: { type: "number", description: "Latitude for subject B" },
|
|
24
|
+
longitude_b: { type: "number", description: "Longitude for subject B" },
|
|
25
|
+
house_system: {
|
|
26
|
+
type: "string",
|
|
27
|
+
enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry", "alcabitius", "morinus"],
|
|
28
|
+
description: "House system to use. Defaults to 'placidus' if omitted.",
|
|
29
|
+
},
|
|
30
|
+
style: {
|
|
31
|
+
type: "string",
|
|
32
|
+
enum: ["modern", "classic", "dark"],
|
|
33
|
+
description: "Aesthetic style of the chart. Defaults to 'modern'.",
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
required: ["datetime_a", "latitude_a", "longitude_a", "datetime_b", "latitude_b", "longitude_b"],
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
handler: async (args) => {
|
|
40
|
+
validateRequired(args, ["datetime_a", "latitude_a", "longitude_a", "datetime_b", "latitude_b", "longitude_b"]);
|
|
41
|
+
const body = {
|
|
42
|
+
subject_a: {
|
|
43
|
+
name: "Subject A",
|
|
44
|
+
birth_datetime: { iso: args.datetime_a },
|
|
45
|
+
birth_location: {
|
|
46
|
+
latitude: { decimal: args.latitude_a },
|
|
47
|
+
longitude: { decimal: args.longitude_a }
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
subject_b: {
|
|
51
|
+
name: "Subject B",
|
|
52
|
+
birth_datetime: { iso: args.datetime_b },
|
|
53
|
+
birth_location: {
|
|
54
|
+
latitude: { decimal: args.latitude_b },
|
|
55
|
+
longitude: { decimal: args.longitude_b }
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
if (args.house_system) {
|
|
60
|
+
const mappedCode = HOUSE_SYSTEM_MAP[args.house_system] || "P";
|
|
61
|
+
body.configuration = { house_system: mappedCode };
|
|
62
|
+
}
|
|
63
|
+
const styleParam = args.style ? `&style=${args.style}` : "";
|
|
64
|
+
return await backendClient.post(`/visualization/bi-wheel?format=png&size=800${styleParam}`, body);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { registerTool, validateRequired } from "../index.js";
|
|
2
|
+
import { backendClient } from "../../backend/client.js";
|
|
3
|
+
const DATETIME_DESC = "ISO 8601 datetime string, e.g. '1990-04-15T14:30:00' (local time at birth location). " +
|
|
4
|
+
"Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.";
|
|
5
|
+
const HOUSE_SYSTEM_MAP = {
|
|
6
|
+
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
7
|
+
campanus: "C", regiomontanus: "R", porphyry: "O",
|
|
8
|
+
alcabitius: "B", morinus: "M",
|
|
9
|
+
};
|
|
10
|
+
registerTool({
|
|
11
|
+
name: "ephemeris_chart_wheel",
|
|
12
|
+
description: "Generate a classic astrological Chart Wheel image (PNG) for a person/event. This draws a standard circular chart wheel with planets, aspects, and house cusps. The tool returns a raw image (base64) which most Vision-enabled AI agents (GPT-4o, Claude 3.5 Sonnet) can display and visually read.\n\n" +
|
|
13
|
+
"CREDIT COST: 2 credits per call.\n\n" +
|
|
14
|
+
"EXAMPLE: Generate a natal chart wheel for someone born April 15, 1990 in Chicago:\n" +
|
|
15
|
+
" datetime='1990-04-15T14:30:00', latitude=41.8781, longitude=-87.6298",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
datetime: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: DATETIME_DESC,
|
|
22
|
+
},
|
|
23
|
+
latitude: {
|
|
24
|
+
type: "number",
|
|
25
|
+
description: "Geographic latitude of birth location in decimal degrees (positive = North).",
|
|
26
|
+
},
|
|
27
|
+
longitude: {
|
|
28
|
+
type: "number",
|
|
29
|
+
description: "Geographic longitude of birth location in decimal degrees (positive = East).",
|
|
30
|
+
},
|
|
31
|
+
house_system: {
|
|
32
|
+
type: "string",
|
|
33
|
+
enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry", "alcabitius", "morinus"],
|
|
34
|
+
description: "House system to use. Defaults to 'placidus' if omitted.",
|
|
35
|
+
},
|
|
36
|
+
style: {
|
|
37
|
+
type: "string",
|
|
38
|
+
enum: ["modern", "classic", "dark"],
|
|
39
|
+
description: "Aesthetic style of the chart. Defaults to 'modern'.",
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
required: ["datetime", "latitude", "longitude"],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
handler: async (args) => {
|
|
46
|
+
validateRequired(args, ["datetime", "latitude", "longitude"]);
|
|
47
|
+
const body = {
|
|
48
|
+
subject: {
|
|
49
|
+
name: "Chart Wheel Request",
|
|
50
|
+
birth_datetime: { iso: args.datetime },
|
|
51
|
+
birth_location: {
|
|
52
|
+
latitude: { decimal: args.latitude },
|
|
53
|
+
longitude: { decimal: args.longitude }
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
if (args.house_system) {
|
|
58
|
+
const mappedCode = HOUSE_SYSTEM_MAP[args.house_system] || "P";
|
|
59
|
+
body.configuration = { house_system: mappedCode };
|
|
60
|
+
}
|
|
61
|
+
const styleParam = args.style ? `&style=${args.style}` : "";
|
|
62
|
+
return await backendClient.post(`/visualization/chart-wheel?format=png&size=800${styleParam}`, body);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -6,7 +6,9 @@ registerTool({
|
|
|
6
6
|
"Returns a list of exact transit moments — when transiting planets form specified aspects " +
|
|
7
7
|
"to natal planet positions. Ideal for generating horoscope timelines, event forecasting, or " +
|
|
8
8
|
"finding optimal timing windows.\n\n" +
|
|
9
|
-
"
|
|
9
|
+
"HOW IT WORKS: This tool first calculates the natal chart to extract your actual planetary " +
|
|
10
|
+
"ecliptic longitudes, then searches for transiting planets crossing those exact degrees.\n\n" +
|
|
11
|
+
"CREDIT COST: 6 credits per call (1 for natal + 5 for transit search).\n\n" +
|
|
10
12
|
"EXAMPLE: Find all Saturn transits to the natal Sun/Moon for the next 6 months:\n" +
|
|
11
13
|
" natal_datetime='1990-04-15T14:30:00', natal_latitude=41.8781, natal_longitude=-87.6298,\n" +
|
|
12
14
|
" start_date='2026-01-01', end_date='2026-06-30', transiting_planets=['saturn']",
|
|
@@ -42,16 +44,7 @@ registerTool({
|
|
|
42
44
|
natal_points: {
|
|
43
45
|
type: "array",
|
|
44
46
|
items: { type: "string" },
|
|
45
|
-
description: "Natal point IDs
|
|
46
|
-
},
|
|
47
|
-
aspects: {
|
|
48
|
-
type: "array",
|
|
49
|
-
items: { type: "string", enum: ["conjunction", "opposition", "trine", "square", "sextile"] },
|
|
50
|
-
description: "Aspect types to include in the search. Defaults to all major aspects.",
|
|
51
|
-
},
|
|
52
|
-
orb: {
|
|
53
|
-
type: "number",
|
|
54
|
-
description: "Maximum orb in degrees (default: 1.0).",
|
|
47
|
+
description: "Natal point IDs whose exact longitudes should be targeted. E.g. ['sun', 'moon', 'asc', 'mc']. Omit for all core points.",
|
|
55
48
|
},
|
|
56
49
|
},
|
|
57
50
|
required: ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"],
|
|
@@ -59,6 +52,58 @@ registerTool({
|
|
|
59
52
|
},
|
|
60
53
|
handler: async (args) => {
|
|
61
54
|
validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"]);
|
|
55
|
+
// ── Step 1: Compute natal chart to extract real planetary longitudes ──
|
|
56
|
+
const natalBody = {
|
|
57
|
+
subject: {
|
|
58
|
+
name: "Transit Natal Subject",
|
|
59
|
+
birth_datetime: { iso: args.natal_datetime },
|
|
60
|
+
birth_location: {
|
|
61
|
+
latitude: { decimal: args.natal_latitude },
|
|
62
|
+
longitude: { decimal: args.natal_longitude }
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const natalResult = await backendClient.post("/ephemeris/natal", natalBody);
|
|
67
|
+
// Extract ecliptic longitudes from natal chart planets
|
|
68
|
+
const targetDegrees = [];
|
|
69
|
+
const natalPositionMap = {};
|
|
70
|
+
// The natal response typically has a `planets` array or object
|
|
71
|
+
const planets = natalResult?.planets || natalResult?.data?.planets || [];
|
|
72
|
+
const planetArray = Array.isArray(planets) ? planets : Object.values(planets);
|
|
73
|
+
// Filter to requested natal points if specified
|
|
74
|
+
const wantedPoints = args.natal_points
|
|
75
|
+
? new Set(args.natal_points.map((p) => p.toLowerCase()))
|
|
76
|
+
: null;
|
|
77
|
+
for (const planet of planetArray) {
|
|
78
|
+
const name = (planet.name || planet.id || "").toLowerCase();
|
|
79
|
+
const lon = planet.longitude ?? planet.ecliptic_longitude ?? planet.lon;
|
|
80
|
+
if (typeof lon !== "number" || !isFinite(lon))
|
|
81
|
+
continue;
|
|
82
|
+
if (wantedPoints && !wantedPoints.has(name))
|
|
83
|
+
continue;
|
|
84
|
+
targetDegrees.push(Math.round(lon * 100) / 100); // 2 decimal places
|
|
85
|
+
natalPositionMap[name] = lon;
|
|
86
|
+
}
|
|
87
|
+
// Also extract angles (ASC, MC, DSC, IC) if present
|
|
88
|
+
const angles = natalResult?.angles || natalResult?.data?.angles;
|
|
89
|
+
if (angles) {
|
|
90
|
+
const angleEntries = Array.isArray(angles) ? angles : Object.entries(angles).map(([k, v]) => ({ name: k, ...(typeof v === 'object' ? v : { longitude: v }) }));
|
|
91
|
+
for (const angle of angleEntries) {
|
|
92
|
+
const name = (angle.name || angle.id || "").toLowerCase();
|
|
93
|
+
const lon = angle.longitude ?? angle.ecliptic_longitude ?? angle.lon;
|
|
94
|
+
if (typeof lon !== "number" || !isFinite(lon))
|
|
95
|
+
continue;
|
|
96
|
+
if (wantedPoints && !wantedPoints.has(name))
|
|
97
|
+
continue;
|
|
98
|
+
targetDegrees.push(Math.round(lon * 100) / 100);
|
|
99
|
+
natalPositionMap[name] = lon;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (targetDegrees.length === 0) {
|
|
103
|
+
throw new Error("Could not extract natal planet longitudes from the natal chart calculation. " +
|
|
104
|
+
"Please verify your natal_datetime, natal_latitude, and natal_longitude are correct.");
|
|
105
|
+
}
|
|
106
|
+
// ── Step 2: Search transits against those real natal longitudes ──
|
|
62
107
|
let startStr = args.start_date;
|
|
63
108
|
if (/^\d{4}-\d{2}-\d{2}$/.test(startStr)) {
|
|
64
109
|
startStr += "T00:00:00Z";
|
|
@@ -67,26 +112,19 @@ registerTool({
|
|
|
67
112
|
if (/^\d{4}-\d{2}-\d{2}$/.test(endStr)) {
|
|
68
113
|
endStr += "T23:59:59Z";
|
|
69
114
|
}
|
|
70
|
-
const
|
|
115
|
+
const transitBody = {
|
|
71
116
|
start_date: startStr,
|
|
72
117
|
end_date: endStr,
|
|
73
|
-
|
|
74
|
-
datetime: args.natal_datetime,
|
|
75
|
-
latitude: args.natal_latitude,
|
|
76
|
-
longitude: args.natal_longitude,
|
|
77
|
-
},
|
|
118
|
+
target_degrees: targetDegrees,
|
|
78
119
|
};
|
|
79
120
|
if (args.transiting_planets)
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (Object.keys(searchCriteria).length > 0)
|
|
89
|
-
body.search_criteria = searchCriteria;
|
|
90
|
-
return await backendClient.request("POST", "/predictive/transits/search", { data: body });
|
|
121
|
+
transitBody.planet_names = args.transiting_planets;
|
|
122
|
+
const transitResult = await backendClient.request("POST", "/predictive/transits/search", { data: transitBody });
|
|
123
|
+
// Return combined context so the LLM knows what natal positions were targeted
|
|
124
|
+
return {
|
|
125
|
+
natal_positions: natalPositionMap,
|
|
126
|
+
target_degrees_used: targetDegrees,
|
|
127
|
+
transit_results: transitResult,
|
|
128
|
+
};
|
|
91
129
|
},
|
|
92
130
|
});
|