@openephemeris/mcp-server 3.2.11 → 3.2.13
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/dist/backend/client.d.ts +23 -1
- package/dist/backend/client.js +144 -25
- package/dist/index.js +94 -2
- package/dist/tools/dev.js +29 -21
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +30 -1
- package/dist/tools/specialized/eclipse.js +20 -26
- package/dist/tools/specialized/electional.js +4 -3
- package/dist/tools/specialized/human_design.js +27 -11
- package/dist/tools/specialized/moon.js +3 -2
- package/dist/tools/specialized/natal.js +36 -15
- package/dist/tools/specialized/relocation.js +14 -10
- package/dist/tools/specialized/returns.js +126 -0
- package/dist/tools/specialized/synastry.js +22 -12
- package/dist/tools/specialized/transits.js +78 -28
- package/package.json +3 -2
- package/dist/scripts/dev-allowlist.js +0 -287
- package/dist/scripts/pack-audit.d.ts +0 -1
- package/dist/scripts/pack-audit.js +0 -45
- package/dist/scripts/schema-packs.d.ts +0 -1
- package/dist/scripts/schema-packs.js +0 -150
- package/dist/scripts/smoke-dev-profile.d.ts +0 -1
- package/dist/scripts/smoke-dev-profile.js +0 -25
- package/dist/scripts/sync-readme.d.ts +0 -1
- package/dist/scripts/sync-readme.js +0 -141
- package/dist/scripts/test-client.d.ts +0 -1
- package/dist/scripts/test-client.js +0 -69
- package/dist/scripts/test-sse-client.d.ts +0 -1
- package/dist/scripts/test-sse-client.js +0 -221
- package/dist/src/backend/client.d.ts +0 -67
- package/dist/src/backend/client.js +0 -363
- package/dist/src/index.d.ts +0 -2
- package/dist/src/index.js +0 -184
- package/dist/src/schema-packs/llm.d.ts +0 -105
- package/dist/src/schema-packs/llm.js +0 -429
- package/dist/src/tools/dev.d.ts +0 -1
- package/dist/src/tools/dev.js +0 -195
- package/dist/src/tools/index.d.ts +0 -33
- package/dist/src/tools/index.js +0 -60
- package/dist/src/tools/specialized/eclipse.d.ts +0 -1
- package/dist/src/tools/specialized/eclipse.js +0 -50
- package/dist/src/tools/specialized/electional.d.ts +0 -1
- package/dist/src/tools/specialized/electional.js +0 -80
- package/dist/src/tools/specialized/human_design.d.ts +0 -1
- package/dist/src/tools/specialized/human_design.js +0 -69
- package/dist/src/tools/specialized/moon.d.ts +0 -1
- package/dist/src/tools/specialized/moon.js +0 -51
- package/dist/src/tools/specialized/natal.d.ts +0 -1
- package/dist/src/tools/specialized/natal.js +0 -92
- package/dist/src/tools/specialized/relocation.d.ts +0 -1
- package/dist/src/tools/specialized/relocation.js +0 -76
- package/dist/src/tools/specialized/synastry.d.ts +0 -1
- package/dist/src/tools/specialized/synastry.js +0 -73
- package/dist/src/tools/specialized/transits.d.ts +0 -1
- package/dist/src/tools/specialized/transits.js +0 -130
- package/dist/test/allowlist-and-tools.test.d.ts +0 -1
- package/dist/test/allowlist-and-tools.test.js +0 -96
- package/dist/test/backend-client.test.d.ts +0 -1
- package/dist/test/backend-client.test.js +0 -284
- package/dist/test/credentials.test.d.ts +0 -1
- package/dist/test/credentials.test.js +0 -143
- /package/dist/{src/auth → auth}/credentials.d.ts +0 -0
- /package/dist/{src/auth → auth}/credentials.js +0 -0
- /package/dist/{src/auth → auth}/device-auth.d.ts +0 -0
- /package/dist/{src/auth → auth}/device-auth.js +0 -0
- /package/dist/{src/server-sse.d.ts → server-sse.d.ts} +0 -0
- /package/dist/{src/server-sse.js → server-sse.js} +0 -0
- /package/dist/{src/tools → tools}/auth.d.ts +0 -0
- /package/dist/{src/tools → tools}/auth.js +0 -0
- /package/dist/{src/tools → tools}/specialized/bazi.d.ts +0 -0
- /package/dist/{src/tools → tools}/specialized/bazi.js +0 -0
- /package/dist/{src/tools → tools}/specialized/bi_wheel.d.ts +0 -0
- /package/dist/{src/tools → tools}/specialized/bi_wheel.js +0 -0
- /package/dist/{src/tools → tools}/specialized/chart_wheel.d.ts +0 -0
- /package/dist/{src/tools → tools}/specialized/chart_wheel.js +0 -0
- /package/dist/{scripts/dev-allowlist.d.ts → tools/specialized/returns.d.ts} +0 -0
- /package/dist/{src/tools → tools}/specialized/vedic.d.ts +0 -0
- /package/dist/{src/tools → tools}/specialized/vedic.js +0 -0
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import axios, { AxiosError } from "axios";
|
|
2
|
-
import { CredentialManager } from "../auth/credentials.js";
|
|
3
|
-
import { DeviceAuthFlow } from "../auth/device-auth.js";
|
|
4
|
-
const DEFAULT_TIMEOUT_MS = 45_000;
|
|
5
|
-
const RETRY_DELAYS_MS = [1_000, 2_000, 4_000]; // Three retries with backoff
|
|
6
|
-
const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
|
|
7
|
-
const RETRYABLE_CODES = new Set(["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN"]);
|
|
8
|
-
const BINARY_ENDPOINT_PREFIXES = [
|
|
9
|
-
"/visualization/bi-wheel",
|
|
10
|
-
"/visualization/chart-wheel",
|
|
11
|
-
"/comparative/visualization/bi-wheel",
|
|
12
|
-
"/comparative/visualization/chart-wheel",
|
|
13
|
-
];
|
|
14
|
-
const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboard?tab=account";
|
|
15
|
-
const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
|
|
16
|
-
const UPGRADE_URL = "https://openephemeris.com/pay";
|
|
17
|
-
function sleep(ms) {
|
|
18
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
-
}
|
|
20
|
-
export class BackendError extends Error {
|
|
21
|
-
status;
|
|
22
|
-
code;
|
|
23
|
-
retryable;
|
|
24
|
-
upgradeUrl;
|
|
25
|
-
constructor(message, status, code, retryable, upgradeUrl) {
|
|
26
|
-
super(message);
|
|
27
|
-
this.status = status;
|
|
28
|
-
this.code = code;
|
|
29
|
-
this.retryable = retryable;
|
|
30
|
-
this.upgradeUrl = upgradeUrl;
|
|
31
|
-
this.name = "BackendError";
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
export class BackendClient {
|
|
35
|
-
client;
|
|
36
|
-
userId;
|
|
37
|
-
jwt;
|
|
38
|
-
serviceKey;
|
|
39
|
-
apiKey;
|
|
40
|
-
credentialManager;
|
|
41
|
-
_pendingAuthFlow = null;
|
|
42
|
-
_authFlowResult = null;
|
|
43
|
-
constructor(config) {
|
|
44
|
-
this.userId = config.userId || "anonymous";
|
|
45
|
-
this.jwt =
|
|
46
|
-
config.jwt ||
|
|
47
|
-
config.authToken ||
|
|
48
|
-
process.env.OPENEPHEMERIS_JWT ||
|
|
49
|
-
process.env.ASTROMCP_JWT ||
|
|
50
|
-
process.env.MERIDIAN_AUTH_TOKEN;
|
|
51
|
-
this.serviceKey =
|
|
52
|
-
config.serviceKey ||
|
|
53
|
-
process.env.OPENEPHEMERIS_SERVICE_KEY ||
|
|
54
|
-
process.env.ASTROMCP_SERVICE_KEY ||
|
|
55
|
-
process.env.MERIDIAN_SERVICE_KEY;
|
|
56
|
-
this.apiKey =
|
|
57
|
-
config.apiKey ||
|
|
58
|
-
process.env.OPENEPHEMERIS_API_KEY ||
|
|
59
|
-
process.env.ASTROMCP_API_KEY ||
|
|
60
|
-
process.env.MERIDIAN_API_KEY;
|
|
61
|
-
this.credentialManager = new CredentialManager();
|
|
62
|
-
this.client = axios.create({
|
|
63
|
-
baseURL: config.baseURL ||
|
|
64
|
-
process.env.OPENEPHEMERIS_BACKEND_URL ||
|
|
65
|
-
process.env.ASTROMCP_BACKEND_URL ||
|
|
66
|
-
process.env.MERIDIAN_BACKEND_URL ||
|
|
67
|
-
"https://api.openephemeris.com",
|
|
68
|
-
timeout: DEFAULT_TIMEOUT_MS,
|
|
69
|
-
headers: {
|
|
70
|
-
"Content-Type": "application/json",
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
// Add auth interceptor (priority: service key > API key > JWT env > cached JWT)
|
|
74
|
-
this.client.interceptors.request.use(async (req) => {
|
|
75
|
-
req.headers = req.headers ?? {};
|
|
76
|
-
// Explicit per-request headers win.
|
|
77
|
-
const hasAuthHeader = typeof req.headers.Authorization === "string" &&
|
|
78
|
-
req.headers.Authorization.length > 0;
|
|
79
|
-
const hasServiceKeyHeader = typeof req.headers["X-Service-Key"] === "string" &&
|
|
80
|
-
req.headers["X-Service-Key"].length > 0;
|
|
81
|
-
const hasApiKeyHeader = typeof req.headers["X-API-Key"] === "string" &&
|
|
82
|
-
req.headers["X-API-Key"].length > 0;
|
|
83
|
-
if (!hasAuthHeader && !hasServiceKeyHeader && !hasApiKeyHeader) {
|
|
84
|
-
if (this.serviceKey) {
|
|
85
|
-
req.headers["X-Service-Key"] = this.serviceKey;
|
|
86
|
-
}
|
|
87
|
-
else if (this.apiKey) {
|
|
88
|
-
req.headers["X-API-Key"] = this.apiKey;
|
|
89
|
-
}
|
|
90
|
-
else if (this.jwt) {
|
|
91
|
-
req.headers.Authorization = `Bearer ${this.jwt}`;
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
// Fall through to cached credentials from device auth flow
|
|
95
|
-
const cachedToken = await this.credentialManager.getValidToken();
|
|
96
|
-
if (cachedToken) {
|
|
97
|
-
req.headers.Authorization = `Bearer ${cachedToken}`;
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
// No credentials anywhere — auto-start device auth
|
|
101
|
-
await this.autoStartDeviceAuth();
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return req;
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Override the API key at runtime. Used by the SSE server to inject the
|
|
110
|
-
* per-session user's key so that all tool calls are authenticated and
|
|
111
|
-
* metered against the correct user account.
|
|
112
|
-
*/
|
|
113
|
-
setApiKey(key) {
|
|
114
|
-
this.apiKey = key;
|
|
115
|
-
// Clear any cached JWT/service key so the interceptor uses the new API key
|
|
116
|
-
this.serviceKey = undefined;
|
|
117
|
-
this.jwt = undefined;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Start the device auth flow in the background if not already running.
|
|
121
|
-
* Called automatically when no credentials are found in the interceptor.
|
|
122
|
-
* Idempotent — only starts once per client lifetime.
|
|
123
|
-
*/
|
|
124
|
-
async autoStartDeviceAuth() {
|
|
125
|
-
if (this._pendingAuthFlow)
|
|
126
|
-
return; // Already started
|
|
127
|
-
try {
|
|
128
|
-
const flow = new DeviceAuthFlow();
|
|
129
|
-
const startResult = await flow.start();
|
|
130
|
-
this._authFlowResult = {
|
|
131
|
-
verification_uri: startResult.verification_uri,
|
|
132
|
-
user_code: startResult.user_code,
|
|
133
|
-
verification_uri_complete: startResult.verification_uri_complete,
|
|
134
|
-
};
|
|
135
|
-
// Print to stderr so it's visible in MCP host logs
|
|
136
|
-
console.error(`\n🔗 OpenEphemeris: Account connection required.\n` +
|
|
137
|
-
` Visit: ${startResult.verification_uri}\n` +
|
|
138
|
-
` Code: ${startResult.user_code}\n` +
|
|
139
|
-
` Or: ${startResult.verification_uri_complete}\n`);
|
|
140
|
-
// Poll in background (don't await)
|
|
141
|
-
this._pendingAuthFlow = flow
|
|
142
|
-
.poll(startResult.device_code, (attempt) => {
|
|
143
|
-
if (attempt % 12 === 0) {
|
|
144
|
-
console.error(` Still waiting... Enter code ${startResult.user_code} at ${startResult.verification_uri}`);
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
.then(() => {
|
|
148
|
-
console.error(`✅ Authenticated! Subsequent requests will use your account.`);
|
|
149
|
-
this._authFlowResult = null;
|
|
150
|
-
})
|
|
151
|
-
.catch((err) => {
|
|
152
|
-
console.error(`❌ Device auth failed: ${err.message}`);
|
|
153
|
-
this._pendingAuthFlow = null; // Allow retry
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
catch (err) {
|
|
157
|
-
console.error(`Could not start device auth: ${err.message}`);
|
|
158
|
-
// Don't block — the request will go out unauthenticated and
|
|
159
|
-
// the 401 response will surface the error message to the user.
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
expectsBinaryResponse(path, options) {
|
|
163
|
-
const normalizedPath = path.split("?")[0].trim().toLowerCase();
|
|
164
|
-
if (BINARY_ENDPOINT_PREFIXES.some((prefix) => normalizedPath.startsWith(prefix))) {
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
const format = options?.params?.format;
|
|
168
|
-
if (typeof format === "string") {
|
|
169
|
-
const normalizedFormat = format.trim().toLowerCase();
|
|
170
|
-
if (normalizedFormat === "png" || normalizedFormat === "svg") {
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const acceptHeader = Object.entries(options?.headers ?? {}).find(([key]) => key.toLowerCase() === "accept")?.[1];
|
|
175
|
-
if (typeof acceptHeader === "string" && acceptHeader.toLowerCase().includes("image/")) {
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
toBuffer(data) {
|
|
181
|
-
if (data == null)
|
|
182
|
-
return null;
|
|
183
|
-
if (Buffer.isBuffer(data))
|
|
184
|
-
return data;
|
|
185
|
-
if (data instanceof ArrayBuffer)
|
|
186
|
-
return Buffer.from(data);
|
|
187
|
-
if (ArrayBuffer.isView(data)) {
|
|
188
|
-
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
189
|
-
}
|
|
190
|
-
if (typeof data === "string")
|
|
191
|
-
return Buffer.from(data);
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
decodePayload(data) {
|
|
195
|
-
const asBuffer = this.toBuffer(data);
|
|
196
|
-
if (!asBuffer) {
|
|
197
|
-
return data;
|
|
198
|
-
}
|
|
199
|
-
const text = asBuffer.toString("utf8").trim();
|
|
200
|
-
if (!text)
|
|
201
|
-
return "";
|
|
202
|
-
try {
|
|
203
|
-
return JSON.parse(text);
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
return text;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
extractMessage(data, fallback) {
|
|
210
|
-
if (typeof data === "string") {
|
|
211
|
-
const trimmed = data.trim();
|
|
212
|
-
return trimmed || fallback;
|
|
213
|
-
}
|
|
214
|
-
if (data && typeof data === "object") {
|
|
215
|
-
const obj = data;
|
|
216
|
-
for (const key of ["message", "detail", "title", "error"]) {
|
|
217
|
-
const value = obj[key];
|
|
218
|
-
if (typeof value === "string" && value.trim()) {
|
|
219
|
-
return value.trim();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return fallback;
|
|
224
|
-
}
|
|
225
|
-
normalizeContentType(value) {
|
|
226
|
-
if (typeof value !== "string")
|
|
227
|
-
return "";
|
|
228
|
-
return value.split(";")[0]?.trim().toLowerCase() || "";
|
|
229
|
-
}
|
|
230
|
-
isBinaryContentType(contentType) {
|
|
231
|
-
return contentType.startsWith("image/") || contentType === "application/octet-stream";
|
|
232
|
-
}
|
|
233
|
-
/** Maps an AxiosError to a structured BackendError. */
|
|
234
|
-
mapError(error) {
|
|
235
|
-
if (error.response) {
|
|
236
|
-
const status = error.response.status;
|
|
237
|
-
const data = this.decodePayload(error.response.data);
|
|
238
|
-
const msg = this.extractMessage(data, error.message);
|
|
239
|
-
if (status === 401) {
|
|
240
|
-
const authInfo = this._authFlowResult;
|
|
241
|
-
if (authInfo) {
|
|
242
|
-
return new BackendError(`🔗 Account connection required. Please visit ${authInfo.verification_uri} ` +
|
|
243
|
-
`and enter code: ${authInfo.user_code}\n\n` +
|
|
244
|
-
`Or open this direct link: ${authInfo.verification_uri_complete}\n\n` +
|
|
245
|
-
`Once you authorize, retry your request — the server is polling in the background. ` +
|
|
246
|
-
`If you don't have an account, you can create one at the link above (free tier, no credit card needed).`, 401, "auth_required", true, authInfo.verification_uri_complete);
|
|
247
|
-
}
|
|
248
|
-
return new BackendError(`Authentication required: ${msg}. Run auth.login to connect your account, ` +
|
|
249
|
-
`or set OPENEPHEMERIS_API_KEY in your MCP server config.`, 401, "auth_required", false, LOGIN_SIGNUP_URL);
|
|
250
|
-
}
|
|
251
|
-
if (status === 402) {
|
|
252
|
-
return new BackendError(`⚠️ USAGE QUOTA EXCEEDED — DO NOT FABRICATE DATA.\n\n` +
|
|
253
|
-
`${msg}\n\n` +
|
|
254
|
-
`Tell the user: their API credit quota has been reached for this billing period. ` +
|
|
255
|
-
`They can review usage and enable overages at ${DASHBOARD_ACCOUNT_URL}, ` +
|
|
256
|
-
`or upgrade their plan at ${UPGRADE_URL}.\n\n` +
|
|
257
|
-
`DO NOT attempt to generate astrological data from your own knowledge. ` +
|
|
258
|
-
`All chart positions, aspects, and timing data MUST come from the API.`, 402, "quota_exceeded", false, UPGRADE_URL);
|
|
259
|
-
}
|
|
260
|
-
if (status === 403) {
|
|
261
|
-
return new BackendError(`⚠️ ENDPOINT ACCESS RESTRICTED — DO NOT FABRICATE DATA.\n\n` +
|
|
262
|
-
`${msg}\n\n` +
|
|
263
|
-
`Tell the user: this feature requires a higher-tier plan. ` +
|
|
264
|
-
`They can upgrade at ${UPGRADE_URL} or manage their account at ${DASHBOARD_ACCOUNT_URL}.\n\n` +
|
|
265
|
-
`DO NOT attempt to generate astrological data from your own knowledge. ` +
|
|
266
|
-
`All chart positions, aspects, and timing data MUST come from the API.`, 403, "tier_required", false, UPGRADE_URL);
|
|
267
|
-
}
|
|
268
|
-
if (status === 429) {
|
|
269
|
-
return new BackendError(`⚠️ RATE LIMIT EXCEEDED — DO NOT FABRICATE DATA.\n\n` +
|
|
270
|
-
`${msg}\n\n` +
|
|
271
|
-
`Tell the user: requests are being sent too frequently or their plan's rate limit has been hit. ` +
|
|
272
|
-
`They should wait a moment and retry, or check usage at ${DASHBOARD_ACCOUNT_URL}.\n\n` +
|
|
273
|
-
`DO NOT attempt to generate astrological data from your own knowledge while waiting. ` +
|
|
274
|
-
`Simply inform the user and wait for them to retry.`, 429, "rate_limited", true);
|
|
275
|
-
}
|
|
276
|
-
if (status === 404)
|
|
277
|
-
return new BackendError(`Resource not found: ${msg}`, 404, "not_found", false);
|
|
278
|
-
if (status === 400)
|
|
279
|
-
return new BackendError(`Invalid request: ${msg}`, 400, "bad_request", false);
|
|
280
|
-
if (status >= 500)
|
|
281
|
-
return new BackendError(`Backend error (${status}): ${msg}`, status, "server_error", true);
|
|
282
|
-
}
|
|
283
|
-
// Explicit timeout or abort interception
|
|
284
|
-
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
|
|
285
|
-
return new BackendError(`The computation took too long and timed out. This usually happens when requesting a massive date range (like 20+ years of transits) or an overly complex query. Please narrow your request and try again.`, 408, "timeout", false);
|
|
286
|
-
}
|
|
287
|
-
return new BackendError(`Network error: ${error.message}`, 0, "network_error", true);
|
|
288
|
-
}
|
|
289
|
-
async get(path, params, timeoutMs) {
|
|
290
|
-
return this.request("GET", path, { params, timeoutMs });
|
|
291
|
-
}
|
|
292
|
-
async post(path, data, timeoutMs) {
|
|
293
|
-
return this.request("POST", path, { data, timeoutMs });
|
|
294
|
-
}
|
|
295
|
-
async delete(path) {
|
|
296
|
-
return this.request("DELETE", path);
|
|
297
|
-
}
|
|
298
|
-
async request(method, path, options) {
|
|
299
|
-
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
300
|
-
const expectsBinary = this.expectsBinaryResponse(path, options);
|
|
301
|
-
let lastError = new Error("Unknown error");
|
|
302
|
-
// Attempt + retries on transient failures (429, 502, 503, 504, network errors)
|
|
303
|
-
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
304
|
-
try {
|
|
305
|
-
const response = await this.client.request({
|
|
306
|
-
method,
|
|
307
|
-
url: path,
|
|
308
|
-
params: options?.params,
|
|
309
|
-
data: options?.data,
|
|
310
|
-
headers: options?.headers,
|
|
311
|
-
timeout: timeoutMs,
|
|
312
|
-
responseType: expectsBinary ? "arraybuffer" : "json",
|
|
313
|
-
});
|
|
314
|
-
if (!expectsBinary) {
|
|
315
|
-
return response.data;
|
|
316
|
-
}
|
|
317
|
-
const contentType = this.normalizeContentType(response.headers?.["content-type"]);
|
|
318
|
-
if (!this.isBinaryContentType(contentType)) {
|
|
319
|
-
return this.decodePayload(response.data);
|
|
320
|
-
}
|
|
321
|
-
const bytes = this.toBuffer(response.data) ?? Buffer.alloc(0);
|
|
322
|
-
const payload = {
|
|
323
|
-
content_type: contentType || "application/octet-stream",
|
|
324
|
-
content_length: bytes.length,
|
|
325
|
-
encoding: "base64",
|
|
326
|
-
data_base64: bytes.toString("base64"),
|
|
327
|
-
};
|
|
328
|
-
return payload;
|
|
329
|
-
}
|
|
330
|
-
catch (err) {
|
|
331
|
-
if (err instanceof AxiosError) {
|
|
332
|
-
const isRetryable = (err.response && RETRYABLE_STATUSES.has(err.response.status)) ||
|
|
333
|
-
(!err.response && err.code && RETRYABLE_CODES.has(err.code));
|
|
334
|
-
if (isRetryable && attempt < RETRY_DELAYS_MS.length) {
|
|
335
|
-
const jitter = Math.random() * 500;
|
|
336
|
-
await sleep(RETRY_DELAYS_MS[attempt] + jitter);
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
lastError = this.mapError(err);
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
343
|
-
}
|
|
344
|
-
throw lastError;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
throw lastError;
|
|
348
|
-
}
|
|
349
|
-
getUserId() {
|
|
350
|
-
return this.userId;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
// Singleton instance
|
|
354
|
-
export const backendClient = new BackendClient({
|
|
355
|
-
baseURL: process.env.OPENEPHEMERIS_BACKEND_URL ||
|
|
356
|
-
process.env.ASTROMCP_BACKEND_URL ||
|
|
357
|
-
process.env.MERIDIAN_BACKEND_URL ||
|
|
358
|
-
"https://api.openephemeris.com",
|
|
359
|
-
userId: process.env.MCP_USER_ID || "anonymous",
|
|
360
|
-
jwt: process.env.OPENEPHEMERIS_JWT || process.env.ASTROMCP_JWT || process.env.MERIDIAN_AUTH_TOKEN,
|
|
361
|
-
serviceKey: process.env.OPENEPHEMERIS_SERVICE_KEY || process.env.ASTROMCP_SERVICE_KEY || process.env.MERIDIAN_SERVICE_KEY,
|
|
362
|
-
apiKey: process.env.OPENEPHEMERIS_API_KEY || process.env.ASTROMCP_API_KEY || process.env.MERIDIAN_API_KEY,
|
|
363
|
-
});
|
package/dist/src/index.d.ts
DELETED
package/dist/src/index.js
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
-
import { initTools, toolRegistry } from "./tools/index.js";
|
|
9
|
-
function resolveServerVersion() {
|
|
10
|
-
try {
|
|
11
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const pkgPath = path.resolve(here, "..", "package.json");
|
|
13
|
-
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
14
|
-
const parsed = JSON.parse(raw);
|
|
15
|
-
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
16
|
-
return parsed.version.trim();
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
// Ignore and fall back to an explicit unknown version.
|
|
21
|
-
}
|
|
22
|
-
return "0.0.0-unknown";
|
|
23
|
-
}
|
|
24
|
-
const server = new Server({
|
|
25
|
-
name: "openephemeris-mcp",
|
|
26
|
-
version: resolveServerVersion(),
|
|
27
|
-
icons: [
|
|
28
|
-
{
|
|
29
|
-
src: "https://mcp.openephemeris.com/icon.png",
|
|
30
|
-
mimeType: "image/png",
|
|
31
|
-
}
|
|
32
|
-
]
|
|
33
|
-
}, {
|
|
34
|
-
capabilities: {
|
|
35
|
-
tools: {},
|
|
36
|
-
prompts: {},
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
// List available tools
|
|
40
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
41
|
-
return {
|
|
42
|
-
tools: Object.values(toolRegistry).map((tool) => ({
|
|
43
|
-
name: tool.name,
|
|
44
|
-
description: tool.description,
|
|
45
|
-
inputSchema: tool.inputSchema,
|
|
46
|
-
annotations: tool.annotations ?? {
|
|
47
|
-
title: tool.name,
|
|
48
|
-
readOnlyHint: true,
|
|
49
|
-
destructiveHint: false,
|
|
50
|
-
},
|
|
51
|
-
})),
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
// List available prompts
|
|
55
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
56
|
-
return {
|
|
57
|
-
prompts: [
|
|
58
|
-
{
|
|
59
|
-
name: "welcome_to_open_ephemeris",
|
|
60
|
-
description: "Getting started guide for the Open Ephemeris MCP. Use this to orient yourself to available tools, astrology terminology, and usage.",
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
};
|
|
64
|
-
});
|
|
65
|
-
// Get prompt content
|
|
66
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
67
|
-
if (request.params.name !== "welcome_to_open_ephemeris") {
|
|
68
|
-
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
description: "Open Ephemeris Agent & Developer Orientation",
|
|
72
|
-
messages: [
|
|
73
|
-
{
|
|
74
|
-
role: "user",
|
|
75
|
-
content: {
|
|
76
|
-
type: "text",
|
|
77
|
-
text: "Welcome to the **Open Ephemeris MCP Server**!\n\n" +
|
|
78
|
-
"You are now connected to an enterprise-grade astrological and astronomical computation engine powered by NASA JPL DE440 and DE441 ephemeris data. As a generative guide, here is how you can best utilize these tools to help the user:\n\n" +
|
|
79
|
-
"### Core Capabilities\n" +
|
|
80
|
-
"- **Birth Charts:** Use `ephemeris_natal_chart` for full planetary positions, houses, dignities, and aspects.\n" +
|
|
81
|
-
"- **Predictive Timing:** `ephemeris_transits` searches for exact dates when transiting planets hit natal points.\n" +
|
|
82
|
-
"- **Location Astrology:** `ephemeris_relocation` projects a natal chart to a new geographic location (Astrocartography).\n" +
|
|
83
|
-
"- **Specialized Modalities:** Dedicated tools for `vedic_chart`, `chinese_bazi`, and `human_design_chart`.\n" +
|
|
84
|
-
"- **Eclipse Hunter:** `ephemeris_next_eclipse` dynamically scans 20 years for the next total or partial hit at the user's location.\n\n" +
|
|
85
|
-
"### Best Practices for AI Agents\n" +
|
|
86
|
-
"1. **Coordinate Formatting:** Always convert addresses/city names into `latitude` and `longitude` decimals *before* calling the tools (e.g., Chicago is `41.8781, -87.6298`).\n" +
|
|
87
|
-
"2. **Datetime Handling:** Most endpoints accept ISO 8601 strings. If you have a local birth time but the endpoint asks for UTC, explicitly append a `Z` or timezone offset (e.g., `1990-04-15T19:30:00Z`).\n" +
|
|
88
|
-
"3. **Token Efficiency:** Our responses are hyper-condensed to save you context space. You don't need to struggle with raw JSON arrays; interpret the condensed strings directly (e.g., `Su 15°Pi30 H10`).\n\n" +
|
|
89
|
-
"You're equipped with professional-grade math. Start by asking the user if they'd like to calculate a natal chart, check recent transits, or explore another modality!"
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
]
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
// Handle tool calls
|
|
96
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
97
|
-
const toolName = request.params.name;
|
|
98
|
-
const tool = toolRegistry[toolName];
|
|
99
|
-
if (!tool) {
|
|
100
|
-
throw new Error(`Unknown tool: ${toolName}`);
|
|
101
|
-
}
|
|
102
|
-
const startTime = Date.now();
|
|
103
|
-
console.error(`[MCP] Executing tool: ${toolName}`);
|
|
104
|
-
try {
|
|
105
|
-
const result = await tool.handler(request.params.arguments ?? {});
|
|
106
|
-
const durationMs = Date.now() - startTime;
|
|
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
|
-
}
|
|
127
|
-
const jsonStr = JSON.stringify(result, null, 2);
|
|
128
|
-
// Safety limit: ~100kb JSON is roughly 20-30k tokens.
|
|
129
|
-
// Usually happens if LLMs ask for 50+ years of transits or raw ephemeris arrays.
|
|
130
|
-
if (jsonStr.length > 100_000) {
|
|
131
|
-
console.error(`[MCP] ⚠️ Warning: Payload too large (${(jsonStr.length / 1024).toFixed(1)}kb) for ${toolName}`);
|
|
132
|
-
return {
|
|
133
|
-
content: [
|
|
134
|
-
{
|
|
135
|
-
type: "text",
|
|
136
|
-
text: JSON.stringify({
|
|
137
|
-
success: false,
|
|
138
|
-
error: "PAYLOAD_TOO_LARGE",
|
|
139
|
-
message: `The requested query produced too much data (${(jsonStr.length / 1024).toFixed(1)}kb). Please significantly narrow your request parameters (e.g., fewer years, smaller coordinate bounding box, etc.) to fit within context limits.`
|
|
140
|
-
}, null, 2),
|
|
141
|
-
},
|
|
142
|
-
],
|
|
143
|
-
isError: true,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
content: [
|
|
148
|
-
{
|
|
149
|
-
type: "text",
|
|
150
|
-
text: jsonStr,
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
const durationMs = Date.now() - startTime;
|
|
157
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
158
|
-
console.error(`[MCP] ❌ Failed: ${toolName} (${durationMs}ms) - ${errorMessage}`);
|
|
159
|
-
return {
|
|
160
|
-
content: [
|
|
161
|
-
{
|
|
162
|
-
type: "text",
|
|
163
|
-
text: JSON.stringify({
|
|
164
|
-
success: false,
|
|
165
|
-
error: "TOOL_EXECUTION_ERROR",
|
|
166
|
-
message: errorMessage,
|
|
167
|
-
}, null, 2),
|
|
168
|
-
},
|
|
169
|
-
],
|
|
170
|
-
isError: true,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
// Start server
|
|
175
|
-
async function main() {
|
|
176
|
-
await initTools();
|
|
177
|
-
const transport = new StdioServerTransport();
|
|
178
|
-
await server.connect(transport);
|
|
179
|
-
console.error("Open Ephemeris MCP server running on stdio");
|
|
180
|
-
}
|
|
181
|
-
main().catch((error) => {
|
|
182
|
-
console.error("Fatal error:", error);
|
|
183
|
-
process.exit(1);
|
|
184
|
-
});
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
export declare const LLM_V2_POINTS_SCHEMA: readonly ["id", "kind", "src", "lon", "lat", "spd", "rx", "sg", "deg", "hs", "dec", "oob", "ed", "ed_sc", "ad_sc", "on_cusp"];
|
|
3
|
-
export declare const LLM_V2_ASPECTS_SCHEMA: readonly ["a_i", "b_i", "t", "orb", "orb_pct", "app", "str"];
|
|
4
|
-
export declare const LLM_V2_DICT: {
|
|
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];
|
|
8
|
-
readonly kind_id: readonly ["planet", "angle", "node", "lilith", "asteroid", "other"];
|
|
9
|
-
};
|
|
10
|
-
export declare const LlmV2PayloadSchema: z.ZodObject<{
|
|
11
|
-
output_mode: z.ZodOptional<z.ZodLiteral<"llm">>;
|
|
12
|
-
schema_version: z.ZodOptional<z.ZodLiteral<"llm">>;
|
|
13
|
-
chart: z.ZodObject<{
|
|
14
|
-
chart_type: z.ZodEnum<{
|
|
15
|
-
natal: "natal";
|
|
16
|
-
synastry: "synastry";
|
|
17
|
-
}>;
|
|
18
|
-
subject: z.ZodOptional<z.ZodObject<{
|
|
19
|
-
datetime: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
20
|
-
jd: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
21
|
-
lat: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
22
|
-
lon: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
23
|
-
}, z.core.$strict>>;
|
|
24
|
-
}, z.core.$strict>;
|
|
25
|
-
rules: z.ZodOptional<z.ZodObject<{
|
|
26
|
-
zodiac_type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
27
|
-
house_system: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
28
|
-
coordinate_system: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
29
|
-
node_source: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
30
|
-
lon_range: z.ZodLiteral<"0_360">;
|
|
31
|
-
aspect_set: z.ZodLiteral<"major_default">;
|
|
32
|
-
orb_profile: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
33
|
-
strength_basis: z.ZodLiteral<"engine">;
|
|
34
|
-
point_order_id: z.ZodLiteral<"astro_point_order_v1">;
|
|
35
|
-
core_set_id: z.ZodLiteral<"core_interp_set_v1">;
|
|
36
|
-
}, z.core.$strict>>;
|
|
37
|
-
reliability: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
38
|
-
provenance: z.ZodOptional<z.ZodObject<{
|
|
39
|
-
engine: z.ZodString;
|
|
40
|
-
ephemeris_source: z.ZodString;
|
|
41
|
-
ephemeris_version: z.ZodString;
|
|
42
|
-
validation_profile: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
43
|
-
precision_level: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
44
|
-
checksum: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
45
|
-
}, z.core.$strict>>;
|
|
46
|
-
dict: z.ZodObject<{
|
|
47
|
-
sign_id: z.ZodTuple<any, null>;
|
|
48
|
-
aspect_id: z.ZodTuple<any, null>;
|
|
49
|
-
aspect_angle: z.ZodTuple<any, null>;
|
|
50
|
-
kind_id: z.ZodTuple<any, null>;
|
|
51
|
-
}, z.core.$strict>;
|
|
52
|
-
present: z.ZodObject<{
|
|
53
|
-
point_count: z.ZodNumber;
|
|
54
|
-
kinds: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
55
|
-
}, z.core.$strict>;
|
|
56
|
-
points_schema: z.ZodTuple<any, null>;
|
|
57
|
-
points: z.ZodArray<z.ZodTuple<[z.ZodString, z.ZodEnum<{
|
|
58
|
-
[x: string]: any;
|
|
59
|
-
}>, z.ZodNumber, z.ZodNumber, z.ZodNullable<z.ZodNumber>, z.ZodNullable<z.ZodNumber>, z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNullable<z.ZodNumber>, z.ZodNullable<z.ZodNumber>, z.ZodNumber, z.ZodEnum<{
|
|
60
|
-
none: "none";
|
|
61
|
-
dom: "dom";
|
|
62
|
-
ex: "ex";
|
|
63
|
-
det: "det";
|
|
64
|
-
fall: "fall";
|
|
65
|
-
}>, z.ZodNullable<z.ZodNumber>, z.ZodNullable<z.ZodNumber>, z.ZodNumber], null>>;
|
|
66
|
-
angles: z.ZodOptional<z.ZodObject<{
|
|
67
|
-
asc: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
68
|
-
mc: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
69
|
-
dsc: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
70
|
-
ic: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
71
|
-
}, z.core.$strict>>;
|
|
72
|
-
houses: z.ZodOptional<z.ZodObject<{
|
|
73
|
-
system: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
74
|
-
cusps: z.ZodArray<z.ZodNumber>;
|
|
75
|
-
}, z.core.$strict>>;
|
|
76
|
-
aspects_schema: z.ZodTuple<any, null>;
|
|
77
|
-
aspects: z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
|
|
78
|
-
patterns: z.ZodOptional<z.ZodObject<{
|
|
79
|
-
stellium: z.ZodArray<z.ZodUnknown>;
|
|
80
|
-
t_square: z.ZodArray<z.ZodUnknown>;
|
|
81
|
-
grand_trine: z.ZodArray<z.ZodUnknown>;
|
|
82
|
-
kite: z.ZodArray<z.ZodUnknown>;
|
|
83
|
-
yod: z.ZodArray<z.ZodUnknown>;
|
|
84
|
-
grand_cross: z.ZodArray<z.ZodUnknown>;
|
|
85
|
-
}, z.core.$strict>>;
|
|
86
|
-
extras: z.ZodOptional<z.ZodObject<{
|
|
87
|
-
fixed_star_conjunctions: z.ZodArray<z.ZodUnknown>;
|
|
88
|
-
lots: z.ZodArray<z.ZodUnknown>;
|
|
89
|
-
}, z.core.$strict>>;
|
|
90
|
-
counts: z.ZodObject<{
|
|
91
|
-
point_count: z.ZodNumber;
|
|
92
|
-
aspect_count: z.ZodNumber;
|
|
93
|
-
aspect_count_unfiltered: z.ZodOptional<z.ZodNumber>;
|
|
94
|
-
midpoint_count: z.ZodOptional<z.ZodNumber>;
|
|
95
|
-
declination_aspect_count: z.ZodOptional<z.ZodNumber>;
|
|
96
|
-
stellium_count: z.ZodOptional<z.ZodNumber>;
|
|
97
|
-
}, z.core.$strict>;
|
|
98
|
-
analysis: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
99
|
-
human_design: z.ZodOptional<z.ZodObject<{
|
|
100
|
-
gates_schema: z.ZodArray<z.ZodString>;
|
|
101
|
-
gates: z.ZodArray<z.ZodArray<z.ZodAny>>;
|
|
102
|
-
}, z.core.$strict>>;
|
|
103
|
-
}, z.core.$strict>;
|
|
104
|
-
export type LlmV2Payload = z.infer<typeof LlmV2PayloadSchema>;
|
|
105
|
-
export declare const llmV2JsonSchema: Record<string, unknown>;
|