@openephemeris/mcp-server 3.2.12 → 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-all-tools.d.ts +0 -1
- package/dist/scripts/test-all-tools.js +0 -104
- 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
package/dist/backend/client.d.ts
CHANGED
|
@@ -21,20 +21,42 @@ export interface BinaryBackendResponse {
|
|
|
21
21
|
encoding: "base64";
|
|
22
22
|
data_base64: string;
|
|
23
23
|
}
|
|
24
|
+
export declare class BackendError extends Error {
|
|
25
|
+
readonly status: number;
|
|
26
|
+
readonly code: string;
|
|
27
|
+
readonly retryable: boolean;
|
|
28
|
+
readonly upgradeUrl?: string | undefined;
|
|
29
|
+
constructor(message: string, status: number, code: string, retryable: boolean, upgradeUrl?: string | undefined);
|
|
30
|
+
}
|
|
24
31
|
export declare class BackendClient {
|
|
25
32
|
private client;
|
|
26
33
|
private userId;
|
|
27
34
|
private jwt?;
|
|
28
35
|
private serviceKey?;
|
|
29
36
|
private apiKey?;
|
|
37
|
+
private credentialManager;
|
|
38
|
+
private _pendingAuthFlow;
|
|
39
|
+
private _authFlowResult;
|
|
30
40
|
constructor(config: BackendConfig);
|
|
41
|
+
/**
|
|
42
|
+
* Override the API key at runtime. Used by the SSE server to inject the
|
|
43
|
+
* per-session user's key so that all tool calls are authenticated and
|
|
44
|
+
* metered against the correct user account.
|
|
45
|
+
*/
|
|
46
|
+
setApiKey(key: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* Start the device auth flow in the background if not already running.
|
|
49
|
+
* Called automatically when no credentials are found in the interceptor.
|
|
50
|
+
* Idempotent — only starts once per client lifetime.
|
|
51
|
+
*/
|
|
52
|
+
private autoStartDeviceAuth;
|
|
31
53
|
private expectsBinaryResponse;
|
|
32
54
|
private toBuffer;
|
|
33
55
|
private decodePayload;
|
|
34
56
|
private extractMessage;
|
|
35
57
|
private normalizeContentType;
|
|
36
58
|
private isBinaryContentType;
|
|
37
|
-
/** Maps an AxiosError to a
|
|
59
|
+
/** Maps an AxiosError to a structured BackendError. */
|
|
38
60
|
private mapError;
|
|
39
61
|
get<T>(path: string, params?: Record<string, any>, timeoutMs?: number): Promise<T>;
|
|
40
62
|
post<T>(path: string, data?: any, timeoutMs?: number): Promise<T>;
|
package/dist/backend/client.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import axios, { AxiosError } from "axios";
|
|
2
|
+
import { CredentialManager } from "../auth/credentials.js";
|
|
3
|
+
import { DeviceAuthFlow } from "../auth/device-auth.js";
|
|
2
4
|
const DEFAULT_TIMEOUT_MS = 45_000;
|
|
3
|
-
const
|
|
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"]);
|
|
4
8
|
const BINARY_ENDPOINT_PREFIXES = [
|
|
5
9
|
"/visualization/bi-wheel",
|
|
6
10
|
"/visualization/chart-wheel",
|
|
@@ -13,29 +17,51 @@ const UPGRADE_URL = "https://openephemeris.com/pay";
|
|
|
13
17
|
function sleep(ms) {
|
|
14
18
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
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
|
+
}
|
|
16
34
|
export class BackendClient {
|
|
17
35
|
client;
|
|
18
36
|
userId;
|
|
19
37
|
jwt;
|
|
20
38
|
serviceKey;
|
|
21
39
|
apiKey;
|
|
40
|
+
credentialManager;
|
|
41
|
+
_pendingAuthFlow = null;
|
|
42
|
+
_authFlowResult = null;
|
|
22
43
|
constructor(config) {
|
|
23
44
|
this.userId = config.userId || "anonymous";
|
|
24
45
|
this.jwt =
|
|
25
46
|
config.jwt ||
|
|
26
47
|
config.authToken ||
|
|
48
|
+
process.env.OPENEPHEMERIS_JWT ||
|
|
27
49
|
process.env.ASTROMCP_JWT ||
|
|
28
50
|
process.env.MERIDIAN_AUTH_TOKEN;
|
|
29
51
|
this.serviceKey =
|
|
30
52
|
config.serviceKey ||
|
|
53
|
+
process.env.OPENEPHEMERIS_SERVICE_KEY ||
|
|
31
54
|
process.env.ASTROMCP_SERVICE_KEY ||
|
|
32
55
|
process.env.MERIDIAN_SERVICE_KEY;
|
|
33
56
|
this.apiKey =
|
|
34
57
|
config.apiKey ||
|
|
58
|
+
process.env.OPENEPHEMERIS_API_KEY ||
|
|
35
59
|
process.env.ASTROMCP_API_KEY ||
|
|
36
60
|
process.env.MERIDIAN_API_KEY;
|
|
61
|
+
this.credentialManager = new CredentialManager();
|
|
37
62
|
this.client = axios.create({
|
|
38
63
|
baseURL: config.baseURL ||
|
|
64
|
+
process.env.OPENEPHEMERIS_BACKEND_URL ||
|
|
39
65
|
process.env.ASTROMCP_BACKEND_URL ||
|
|
40
66
|
process.env.MERIDIAN_BACKEND_URL ||
|
|
41
67
|
"https://api.openephemeris.com",
|
|
@@ -44,8 +70,8 @@ export class BackendClient {
|
|
|
44
70
|
"Content-Type": "application/json",
|
|
45
71
|
},
|
|
46
72
|
});
|
|
47
|
-
// Add auth interceptor (priority: service key > API key > JWT)
|
|
48
|
-
this.client.interceptors.request.use((req) => {
|
|
73
|
+
// Add auth interceptor (priority: service key > API key > JWT env > cached JWT)
|
|
74
|
+
this.client.interceptors.request.use(async (req) => {
|
|
49
75
|
req.headers = req.headers ?? {};
|
|
50
76
|
// Explicit per-request headers win.
|
|
51
77
|
const hasAuthHeader = typeof req.headers.Authorization === "string" &&
|
|
@@ -64,10 +90,75 @@ export class BackendClient {
|
|
|
64
90
|
else if (this.jwt) {
|
|
65
91
|
req.headers.Authorization = `Bearer ${this.jwt}`;
|
|
66
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
|
+
}
|
|
67
104
|
}
|
|
68
105
|
return req;
|
|
69
106
|
});
|
|
70
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
|
+
}
|
|
71
162
|
expectsBinaryResponse(path, options) {
|
|
72
163
|
const normalizedPath = path.split("?")[0].trim().toLowerCase();
|
|
73
164
|
if (BINARY_ENDPOINT_PREFIXES.some((prefix) => normalizedPath.startsWith(prefix))) {
|
|
@@ -139,36 +230,61 @@ export class BackendClient {
|
|
|
139
230
|
isBinaryContentType(contentType) {
|
|
140
231
|
return contentType.startsWith("image/") || contentType === "application/octet-stream";
|
|
141
232
|
}
|
|
142
|
-
/** Maps an AxiosError to a
|
|
233
|
+
/** Maps an AxiosError to a structured BackendError. */
|
|
143
234
|
mapError(error) {
|
|
144
235
|
if (error.response) {
|
|
145
236
|
const status = error.response.status;
|
|
146
237
|
const data = this.decodePayload(error.response.data);
|
|
147
238
|
const msg = this.extractMessage(data, error.message);
|
|
148
239
|
if (status === 401) {
|
|
149
|
-
|
|
150
|
-
|
|
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);
|
|
151
250
|
}
|
|
152
251
|
if (status === 402) {
|
|
153
|
-
return new
|
|
154
|
-
`
|
|
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);
|
|
155
259
|
}
|
|
156
260
|
if (status === 403) {
|
|
157
|
-
return new
|
|
158
|
-
`
|
|
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);
|
|
159
267
|
}
|
|
160
268
|
if (status === 429) {
|
|
161
|
-
return new
|
|
162
|
-
|
|
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);
|
|
163
275
|
}
|
|
164
276
|
if (status === 404)
|
|
165
|
-
return new
|
|
277
|
+
return new BackendError(`Resource not found: ${msg}`, 404, "not_found", false);
|
|
166
278
|
if (status === 400)
|
|
167
|
-
return new
|
|
279
|
+
return new BackendError(`Invalid request: ${msg}`, 400, "bad_request", false);
|
|
168
280
|
if (status >= 500)
|
|
169
|
-
return new
|
|
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);
|
|
170
286
|
}
|
|
171
|
-
return new
|
|
287
|
+
return new BackendError(`Network error: ${error.message}`, 0, "network_error", true);
|
|
172
288
|
}
|
|
173
289
|
async get(path, params, timeoutMs) {
|
|
174
290
|
return this.request("GET", path, { params, timeoutMs });
|
|
@@ -183,8 +299,8 @@ export class BackendClient {
|
|
|
183
299
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
184
300
|
const expectsBinary = this.expectsBinaryResponse(path, options);
|
|
185
301
|
let lastError = new Error("Unknown error");
|
|
186
|
-
// Attempt + retries on 429
|
|
187
|
-
for (let attempt = 0; attempt <=
|
|
302
|
+
// Attempt + retries on transient failures (429, 502, 503, 504, network errors)
|
|
303
|
+
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
188
304
|
try {
|
|
189
305
|
const response = await this.client.request({
|
|
190
306
|
method,
|
|
@@ -213,9 +329,11 @@ export class BackendClient {
|
|
|
213
329
|
}
|
|
214
330
|
catch (err) {
|
|
215
331
|
if (err instanceof AxiosError) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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);
|
|
219
337
|
continue;
|
|
220
338
|
}
|
|
221
339
|
lastError = this.mapError(err);
|
|
@@ -234,11 +352,12 @@ export class BackendClient {
|
|
|
234
352
|
}
|
|
235
353
|
// Singleton instance
|
|
236
354
|
export const backendClient = new BackendClient({
|
|
237
|
-
baseURL: process.env.
|
|
355
|
+
baseURL: process.env.OPENEPHEMERIS_BACKEND_URL ||
|
|
356
|
+
process.env.ASTROMCP_BACKEND_URL ||
|
|
238
357
|
process.env.MERIDIAN_BACKEND_URL ||
|
|
239
358
|
"https://api.openephemeris.com",
|
|
240
359
|
userId: process.env.MCP_USER_ID || "anonymous",
|
|
241
|
-
jwt: process.env.ASTROMCP_JWT || process.env.MERIDIAN_AUTH_TOKEN,
|
|
242
|
-
serviceKey: process.env.ASTROMCP_SERVICE_KEY || process.env.MERIDIAN_SERVICE_KEY,
|
|
243
|
-
apiKey: process.env.ASTROMCP_API_KEY || process.env.MERIDIAN_API_KEY,
|
|
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,
|
|
244
363
|
});
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
import { initTools, toolRegistry } from "./tools/index.js";
|
|
9
9
|
function resolveServerVersion() {
|
|
10
10
|
try {
|
|
@@ -24,9 +24,16 @@ function resolveServerVersion() {
|
|
|
24
24
|
const server = new Server({
|
|
25
25
|
name: "openephemeris-mcp",
|
|
26
26
|
version: resolveServerVersion(),
|
|
27
|
+
icons: [
|
|
28
|
+
{
|
|
29
|
+
src: "https://mcp.openephemeris.com/icon.png",
|
|
30
|
+
mimeType: "image/png",
|
|
31
|
+
}
|
|
32
|
+
]
|
|
27
33
|
}, {
|
|
28
34
|
capabilities: {
|
|
29
35
|
tools: {},
|
|
36
|
+
prompts: {},
|
|
30
37
|
},
|
|
31
38
|
});
|
|
32
39
|
// List available tools
|
|
@@ -44,6 +51,47 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
44
51
|
})),
|
|
45
52
|
};
|
|
46
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
|
+
});
|
|
47
95
|
// Handle tool calls
|
|
48
96
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
49
97
|
const toolName = request.params.name;
|
|
@@ -51,19 +99,63 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
51
99
|
if (!tool) {
|
|
52
100
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
53
101
|
}
|
|
102
|
+
const startTime = Date.now();
|
|
103
|
+
console.error(`[MCP] Executing tool: ${toolName}`);
|
|
54
104
|
try {
|
|
55
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
|
+
}
|
|
56
146
|
return {
|
|
57
147
|
content: [
|
|
58
148
|
{
|
|
59
149
|
type: "text",
|
|
60
|
-
text:
|
|
150
|
+
text: jsonStr,
|
|
61
151
|
},
|
|
62
152
|
],
|
|
63
153
|
};
|
|
64
154
|
}
|
|
65
155
|
catch (error) {
|
|
156
|
+
const durationMs = Date.now() - startTime;
|
|
66
157
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
158
|
+
console.error(`[MCP] ❌ Failed: ${toolName} (${durationMs}ms) - ${errorMessage}`);
|
|
67
159
|
return {
|
|
68
160
|
content: [
|
|
69
161
|
{
|
package/dist/tools/dev.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { registerTool } from "./index.js";
|
|
1
|
+
import { registerTool, validateRequired } from "./index.js";
|
|
2
2
|
import { backendClient } from "../backend/client.js";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
function getAllowlistPath() {
|
|
7
|
-
const envPath = process.env.ASTROMCP_DEV_ALLOWLIST_PATH;
|
|
7
|
+
const envPath = process.env.OPENEPHEMERIS_DEV_ALLOWLIST_PATH || process.env.ASTROMCP_DEV_ALLOWLIST_PATH;
|
|
8
8
|
if (envPath && envPath.trim())
|
|
9
9
|
return envPath;
|
|
10
10
|
// dist lives at mcp-server/dist; src lives at mcp-server/src.
|
|
11
11
|
// We resolve relative to current file location for both dev+prod.
|
|
12
12
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
let mcpServerRoot = path.resolve(here, "..", "..");
|
|
14
|
+
if (path.basename(mcpServerRoot) === "dist") {
|
|
15
|
+
mcpServerRoot = path.resolve(mcpServerRoot, "..");
|
|
16
|
+
}
|
|
15
17
|
return path.join(mcpServerRoot, "config", "dev-allowlist.json");
|
|
16
18
|
}
|
|
17
19
|
function loadAllowlist() {
|
|
@@ -30,11 +32,11 @@ function isAllowedOperation(method, pathname, allow) {
|
|
|
30
32
|
return allow.some((e) => e.method === method && e.path === pathname);
|
|
31
33
|
}
|
|
32
34
|
registerTool({
|
|
33
|
-
name: "
|
|
35
|
+
name: "dev_call",
|
|
34
36
|
description: "Call any allowlisted Open Ephemeris API endpoint directly. This is the power-user escape hatch " +
|
|
35
|
-
"— use the typed tools (
|
|
36
|
-
"Call
|
|
37
|
-
"AUTH: Set
|
|
37
|
+
"— use the typed tools (ephemeris_natal_chart, ephemeris_transits, etc.) first for common operations. " +
|
|
38
|
+
"Call dev_list_allowed to see all currently available endpoint paths.\n\n" +
|
|
39
|
+
"AUTH: Set OPENEPHEMERIS_API_KEY in your environment. See openephemeris.com/dashboard for active plan limits.\n\n" +
|
|
38
40
|
"CREDIT COSTS:\n" +
|
|
39
41
|
" • Standard chart math (natal, progressed, bazi, HD): 1 credit\n" +
|
|
40
42
|
" • Visualization rendering (chart-wheel, bi-wheel, charts/*): 2 credits\n" +
|
|
@@ -42,9 +44,9 @@ registerTool({
|
|
|
42
44
|
" • Predictive ops (transits, returns, transit-chart): 5 credits\n" +
|
|
43
45
|
" • ACG / astrocartography: 5 credits\n" +
|
|
44
46
|
" • Catalog / metadata / health endpoints: 0 credits\n" +
|
|
45
|
-
" • format=llm (token-optimized output):
|
|
47
|
+
" • format=llm (token-optimized output): available on all tiers\n\n" +
|
|
46
48
|
"COMMON CALLS:\n" +
|
|
47
|
-
" POST /ephemeris/natal-chart — Full natal chart (body: {
|
|
49
|
+
" 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" +
|
|
48
50
|
" POST /ephemeris/natal/batch — Up to 50 natal charts in one request\n" +
|
|
49
51
|
" POST /ephemeris/relocation — Relocated chart (same natal, new location)\n" +
|
|
50
52
|
" POST /predictive/transits/search — Transit event search over a date range\n" +
|
|
@@ -52,15 +54,16 @@ registerTool({
|
|
|
52
54
|
" POST /predictive/returns/lunar — Lunar return chart\n" +
|
|
53
55
|
" POST /comparative/synastry — Two-person synastry chart\n" +
|
|
54
56
|
" POST /comparative/composite — Composite (midpoint) chart\n" +
|
|
55
|
-
" POST /human-design/chart — Full HD
|
|
57
|
+
" POST /human-design/chart — Full HD chart (body: {birth_datetime_utc: '1990-04-15T19:30:00Z'}) — lat/lon optional\n" +
|
|
58
|
+
" POST /time/julian-day — Convert date to JD (body: {year: 1987, month: 7, day: 15, hour: 14, minute: 1})\n" +
|
|
56
59
|
" GET /ephemeris/moon/phase — Current/queried moon phase\n" +
|
|
57
60
|
" GET /ephemeris/moon/void-of-course — Next void-of-course period\n" +
|
|
58
61
|
" GET /ephemeris/agro/daily — Biodynamic farming day quality\n" +
|
|
59
62
|
" GET /ephemeris/agro/calendar — Multi-day biodynamic calendar\n" +
|
|
60
63
|
" GET /ephemeris/agro/void-of-course — Biodynamic VoC periods\n" +
|
|
64
|
+
" GET /eclipse/next-visible — Next eclipse visible from a location (query: lat, lon, type=solar|lunar)\n" +
|
|
61
65
|
" GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
|
|
62
|
-
" GET /eclipse/solar/local — Local solar eclipse (query:
|
|
63
|
-
" GET /eclipse/next-visible — Next eclipse visible from a location\n" +
|
|
66
|
+
" GET /eclipse/solar/local — Local solar eclipse (query: lat, lon)\n" +
|
|
64
67
|
" GET /tidal/forcing — Gravitational tidal forcing index\n" +
|
|
65
68
|
" GET /tidal/forcing/deep-time — Extended tidal deep-time analysis\n" +
|
|
66
69
|
" POST /acg/power-lines — Astrocartography power lines (lat/lon GeoJSON)\n" +
|
|
@@ -68,9 +71,9 @@ registerTool({
|
|
|
68
71
|
" GET /calendar/astrology/moon-phases — Moon phase calendar for a date range\n" +
|
|
69
72
|
" GET /location/autocomplete — Geocode a place name (query: q=City Name)\n" +
|
|
70
73
|
" POST /timezone/lookup — Resolve timezone + UTC offset for a location\n" +
|
|
71
|
-
" POST /chinese/bazi — Chinese Ba Zi (Four Pillars) chart\n" +
|
|
74
|
+
" POST /chinese/bazi — Chinese Ba Zi (Four Pillars) chart (body: {year, month, day, hour})\n" +
|
|
72
75
|
" GET /chinese/zodiac — Chinese zodiac year element/animal\n" +
|
|
73
|
-
" POST /vedic/chart — Vedic (Jyotish) natal chart\n" +
|
|
76
|
+
" POST /vedic/chart — Vedic (Jyotish) natal chart (body: {datetime_utc, latitude, longitude})\n" +
|
|
74
77
|
" GET /catalogs/bodies — List all supported celestial bodies\n\n" +
|
|
75
78
|
"BINARY RESPONSES:\n" +
|
|
76
79
|
" • Binary/image endpoints return {content_type, content_length, encoding, data_base64}\n" +
|
|
@@ -118,8 +121,13 @@ registerTool({
|
|
|
118
121
|
additionalProperties: false,
|
|
119
122
|
},
|
|
120
123
|
handler: async (args) => {
|
|
121
|
-
|
|
122
|
-
const
|
|
124
|
+
validateRequired(args, ["path"]);
|
|
125
|
+
const methodStr = String(args.method || "GET").toUpperCase();
|
|
126
|
+
if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(methodStr)) {
|
|
127
|
+
throw new Error(`Invalid HTTP method: ${methodStr}`);
|
|
128
|
+
}
|
|
129
|
+
const method = methodStr;
|
|
130
|
+
const pathname = String(args.path);
|
|
123
131
|
if (!pathname.startsWith("/")) {
|
|
124
132
|
throw new Error("path must start with '/'");
|
|
125
133
|
}
|
|
@@ -161,12 +169,12 @@ registerTool({
|
|
|
161
169
|
},
|
|
162
170
|
});
|
|
163
171
|
registerTool({
|
|
164
|
-
name: "
|
|
172
|
+
name: "dev_list_allowed",
|
|
165
173
|
description: "List all API operations (method + path) that this MCP instance is authorized to call. " +
|
|
166
174
|
"Returns endpoint entries grouped by method, plus the active deny rules. " +
|
|
167
|
-
"Use this to discover what's available before calling
|
|
168
|
-
"Typed shortcut tools (
|
|
169
|
-
"check those first before reaching for
|
|
175
|
+
"Use this to discover what's available before calling dev_call, or to verify an endpoint path. " +
|
|
176
|
+
"Typed shortcut tools (ephemeris_natal_chart, ephemeris_transits, etc.) cover the most common operations — " +
|
|
177
|
+
"check those first before reaching for dev_call.",
|
|
170
178
|
inputSchema: {
|
|
171
179
|
type: "object",
|
|
172
180
|
properties: {},
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -23,3 +23,11 @@ export type ToolProfile = "dev" | "legacy";
|
|
|
23
23
|
* - `legacy`: registers only the generic tools (back-compat).
|
|
24
24
|
*/
|
|
25
25
|
export declare function initTools(profile?: ToolProfile): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Throws an error if any of the required keys are missing or empty.
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateRequired(args: any, requiredKeys: string[]): void;
|
|
30
|
+
/**
|
|
31
|
+
* Throws an error if only one of the coordinate pair is provided.
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateCoordinates(args: any, latKey: string, lonKey: string): void;
|
package/dist/tools/index.js
CHANGED
|
@@ -14,7 +14,9 @@ export async function initTools(profile) {
|
|
|
14
14
|
if (toolsInitialized)
|
|
15
15
|
return;
|
|
16
16
|
toolsInitialized = true;
|
|
17
|
-
const resolvedProfile = (profile || process.env.ASTROMCP_PROFILE || "dev").toLowerCase();
|
|
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");
|
|
18
20
|
// Always register the generic proxy tools (dev.call + dev.list_allowed).
|
|
19
21
|
await import("./dev.js");
|
|
20
22
|
if (resolvedProfile === "dev") {
|
|
@@ -27,5 +29,32 @@ export async function initTools(profile) {
|
|
|
27
29
|
await import("./specialized/synastry.js");
|
|
28
30
|
await import("./specialized/relocation.js");
|
|
29
31
|
await import("./specialized/electional.js");
|
|
32
|
+
await import("./specialized/vedic.js");
|
|
33
|
+
await import("./specialized/bazi.js");
|
|
34
|
+
await import("./specialized/chart_wheel.js");
|
|
35
|
+
await import("./specialized/bi_wheel.js");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Throws an error if any of the required keys are missing or empty.
|
|
40
|
+
*/
|
|
41
|
+
export function validateRequired(args, requiredKeys) {
|
|
42
|
+
if (!args)
|
|
43
|
+
throw new Error("Missing arguments object.");
|
|
44
|
+
const missing = requiredKeys.filter((k) => args[k] == null || args[k] === "");
|
|
45
|
+
if (missing.length > 0) {
|
|
46
|
+
throw new Error(`Missing required arguments: ${missing.join(", ")}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Throws an error if only one of the coordinate pair is provided.
|
|
51
|
+
*/
|
|
52
|
+
export function validateCoordinates(args, latKey, lonKey) {
|
|
53
|
+
if (!args)
|
|
54
|
+
return;
|
|
55
|
+
const hasLat = args[latKey] != null && args[latKey] !== "";
|
|
56
|
+
const hasLon = args[lonKey] != null && args[lonKey] !== "";
|
|
57
|
+
if (hasLat !== hasLon) {
|
|
58
|
+
throw new Error(`Both ${latKey} and ${lonKey} must be provided together, or both omitted.`);
|
|
30
59
|
}
|
|
31
60
|
}
|