@openephemeris/mcp-server 3.1.0 → 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 (51) hide show
  1. package/README.md +30 -21
  2. package/dist/index.js +0 -0
  3. package/dist/scripts/dev-allowlist.d.ts +1 -0
  4. package/dist/scripts/dev-allowlist.js +287 -0
  5. package/dist/scripts/pack-audit.d.ts +1 -0
  6. package/dist/scripts/pack-audit.js +45 -0
  7. package/dist/scripts/schema-packs.d.ts +1 -0
  8. package/dist/scripts/schema-packs.js +150 -0
  9. package/dist/scripts/smoke-dev-profile.d.ts +1 -0
  10. package/dist/scripts/smoke-dev-profile.js +25 -0
  11. package/dist/scripts/sync-readme.d.ts +1 -0
  12. package/dist/scripts/sync-readme.js +141 -0
  13. package/dist/src/auth/credentials.d.ts +65 -0
  14. package/dist/src/auth/credentials.js +200 -0
  15. package/dist/src/auth/device-auth.d.ts +56 -0
  16. package/dist/src/auth/device-auth.js +144 -0
  17. package/dist/src/backend/client.d.ts +61 -0
  18. package/dist/src/backend/client.js +335 -0
  19. package/dist/src/index.d.ts +2 -0
  20. package/dist/src/index.js +92 -0
  21. package/dist/src/schema-packs/llm.d.ts +105 -0
  22. package/dist/src/schema-packs/llm.js +429 -0
  23. package/dist/src/tools/auth.d.ts +1 -0
  24. package/dist/src/tools/auth.js +202 -0
  25. package/dist/src/tools/dev.d.ts +1 -0
  26. package/dist/src/tools/dev.js +187 -0
  27. package/dist/src/tools/index.d.ts +25 -0
  28. package/dist/src/tools/index.js +33 -0
  29. package/dist/src/tools/specialized/eclipse.d.ts +1 -0
  30. package/dist/src/tools/specialized/eclipse.js +56 -0
  31. package/dist/src/tools/specialized/electional.d.ts +1 -0
  32. package/dist/src/tools/specialized/electional.js +79 -0
  33. package/dist/src/tools/specialized/human_design.d.ts +1 -0
  34. package/dist/src/tools/specialized/human_design.js +53 -0
  35. package/dist/src/tools/specialized/moon.d.ts +1 -0
  36. package/dist/src/tools/specialized/moon.js +50 -0
  37. package/dist/src/tools/specialized/natal.d.ts +1 -0
  38. package/dist/src/tools/specialized/natal.js +71 -0
  39. package/dist/src/tools/specialized/relocation.d.ts +1 -0
  40. package/dist/src/tools/specialized/relocation.js +71 -0
  41. package/dist/src/tools/specialized/synastry.d.ts +1 -0
  42. package/dist/src/tools/specialized/synastry.js +61 -0
  43. package/dist/src/tools/specialized/transits.d.ts +1 -0
  44. package/dist/src/tools/specialized/transits.js +80 -0
  45. package/dist/test/allowlist-and-tools.test.d.ts +1 -0
  46. package/dist/test/allowlist-and-tools.test.js +96 -0
  47. package/dist/test/backend-client.test.d.ts +1 -0
  48. package/dist/test/backend-client.test.js +286 -0
  49. package/dist/test/credentials.test.d.ts +1 -0
  50. package/dist/test/credentials.test.js +143 -0
  51. package/package.json +21 -18
@@ -0,0 +1,429 @@
1
+ import { z } from "zod";
2
+ export const LLM_V2_POINTS_SCHEMA = [
3
+ "id",
4
+ "kind",
5
+ "src",
6
+ "lon",
7
+ "lat",
8
+ "spd",
9
+ "rx",
10
+ "sg",
11
+ "deg",
12
+ "hs",
13
+ "dec",
14
+ "oob",
15
+ "ed",
16
+ "ed_sc",
17
+ "ad_sc",
18
+ "on_cusp",
19
+ ];
20
+ export const LLM_V2_ASPECTS_SCHEMA = ["a_i", "b_i", "t", "orb", "orb_pct", "app", "str"];
21
+ export const LLM_V2_DICT = {
22
+ sign_id: ["ari", "tau", "gem", "can", "leo", "vir", "lib", "sco", "sag", "cap", "aqu", "pis"],
23
+ aspect_id: ["con", "opp", "tri", "sqr", "sex"],
24
+ aspect_angle: [0, 180, 120, 90, 60],
25
+ kind_id: ["planet", "angle", "node", "lilith", "asteroid", "other"],
26
+ };
27
+ const PointsSchemaZ = z.tuple(LLM_V2_POINTS_SCHEMA.map((v) => z.literal(v)));
28
+ const AspectsSchemaZ = z.tuple(LLM_V2_ASPECTS_SCHEMA.map((v) => z.literal(v)));
29
+ const PointKindZ = z.enum(LLM_V2_DICT.kind_id);
30
+ const EdZ = z.enum(["none", "dom", "ex", "det", "fall"]);
31
+ const PointRowZ = z.tuple([
32
+ z.string(), // id
33
+ PointKindZ, // kind
34
+ z.number().int(), // src (single-frame currently 0)
35
+ z.number(), // lon
36
+ z.number().nullable(), // lat
37
+ z.number().nullable(), // spd
38
+ z.number().int(), // rx
39
+ z.number().int(), // sg
40
+ z.number(), // deg
41
+ z.number().nullable(), // hs
42
+ z.number().nullable(), // dec
43
+ z.number().int(), // oob
44
+ EdZ, // ed
45
+ z.number().nullable(), // ed_sc
46
+ z.number().nullable(), // ad_sc
47
+ z.number().int(), // on_cusp
48
+ ]);
49
+ const AspectRowZ = z.tuple([
50
+ z.number().int(), // a_i
51
+ z.number().int(), // b_i
52
+ z.number().int(), // t
53
+ z.number(), // orb
54
+ z.number(), // orb_pct
55
+ z.number().int(), // app
56
+ z.number(), // str
57
+ ]);
58
+ const ChartSubjectZ = z
59
+ .object({
60
+ datetime: z.string().nullable().optional(),
61
+ jd: z.number().nullable().optional(),
62
+ lat: z.number().nullable().optional(),
63
+ lon: z.number().nullable().optional(),
64
+ })
65
+ .strict();
66
+ const ChartZ = z
67
+ .object({
68
+ chart_type: z.enum(["natal", "synastry"]),
69
+ // Natal-style payloads include subject; synastry-style table payloads omit it.
70
+ subject: ChartSubjectZ.optional(),
71
+ })
72
+ .strict();
73
+ const RulesZ = z
74
+ .object({
75
+ zodiac_type: z.string().nullable().optional(),
76
+ house_system: z.string().nullable().optional(),
77
+ coordinate_system: z.string().nullable().optional(),
78
+ node_source: z.string().nullable().optional(),
79
+ lon_range: z.literal("0_360"),
80
+ aspect_set: z.literal("major_default"),
81
+ orb_profile: z.string().nullable().optional(),
82
+ strength_basis: z.literal("engine"),
83
+ point_order_id: z.literal("astro_point_order_v1"),
84
+ core_set_id: z.literal("core_interp_set_v1"),
85
+ })
86
+ .strict();
87
+ const ProvenanceZ = z
88
+ .object({
89
+ engine: z.string(),
90
+ ephemeris_source: z.string(),
91
+ ephemeris_version: z.string(),
92
+ validation_profile: z.string().nullable().optional(),
93
+ precision_level: z.string().nullable().optional(),
94
+ checksum: z.string().nullable().optional(),
95
+ })
96
+ .strict();
97
+ const DictZ = z
98
+ .object({
99
+ sign_id: z.tuple(LLM_V2_DICT.sign_id.map((v) => z.literal(v))),
100
+ aspect_id: z.tuple(LLM_V2_DICT.aspect_id.map((v) => z.literal(v))),
101
+ aspect_angle: z.tuple(LLM_V2_DICT.aspect_angle.map((v) => z.literal(v))),
102
+ kind_id: z.tuple(LLM_V2_DICT.kind_id.map((v) => z.literal(v))),
103
+ })
104
+ .strict();
105
+ const PresentZ = z
106
+ .object({
107
+ point_count: z.number().int(),
108
+ kinds: z.record(z.string(), z.number().int()),
109
+ })
110
+ .strict();
111
+ const AnglesZ = z
112
+ .object({
113
+ asc: z.number().nullable().optional(),
114
+ mc: z.number().nullable().optional(),
115
+ dsc: z.number().nullable().optional(),
116
+ ic: z.number().nullable().optional(),
117
+ })
118
+ .strict();
119
+ const HousesZ = z
120
+ .object({
121
+ system: z.string().nullable().optional(),
122
+ cusps: z.array(z.number()),
123
+ })
124
+ .strict();
125
+ const PatternsZ = z
126
+ .object({
127
+ stellium: z.array(z.unknown()),
128
+ t_square: z.array(z.unknown()),
129
+ grand_trine: z.array(z.unknown()),
130
+ kite: z.array(z.unknown()),
131
+ yod: z.array(z.unknown()),
132
+ grand_cross: z.array(z.unknown()),
133
+ })
134
+ .strict();
135
+ const ExtrasZ = z
136
+ .object({
137
+ fixed_star_conjunctions: z.array(z.unknown()),
138
+ lots: z.array(z.unknown()),
139
+ })
140
+ .strict();
141
+ const CountsZ = z
142
+ .object({
143
+ point_count: z.number().int(),
144
+ aspect_count: z.number().int(),
145
+ // Natal-style payloads include these; synastry-style may omit.
146
+ aspect_count_unfiltered: z.number().int().optional(),
147
+ midpoint_count: z.number().int().optional(),
148
+ declination_aspect_count: z.number().int().optional(),
149
+ stellium_count: z.number().int().optional(),
150
+ })
151
+ .strict();
152
+ const HumanDesignZ = z
153
+ .object({
154
+ gates_schema: z.array(z.string()),
155
+ gates: z.array(z.array(z.any())),
156
+ })
157
+ .strict();
158
+ export const LlmV2PayloadSchema = z
159
+ .object({
160
+ // Some fixtures include this (envelope-like), but backend projector may omit it.
161
+ output_mode: z.literal("llm").optional(),
162
+ // Backend keeps schema versioning internal; allow older fixtures to include it.
163
+ schema_version: z.literal("llm").optional(),
164
+ chart: ChartZ,
165
+ // Natal-style payloads include these blocks; synastry-style may omit.
166
+ rules: RulesZ.optional(),
167
+ reliability: z.record(z.string(), z.unknown()).optional(),
168
+ provenance: ProvenanceZ.optional(),
169
+ dict: DictZ,
170
+ present: PresentZ,
171
+ points_schema: PointsSchemaZ,
172
+ points: z.array(PointRowZ),
173
+ angles: AnglesZ.optional(),
174
+ houses: HousesZ.optional(),
175
+ aspects_schema: AspectsSchemaZ,
176
+ aspects: z.array(AspectRowZ),
177
+ patterns: PatternsZ.optional(),
178
+ extras: ExtrasZ.optional(),
179
+ counts: CountsZ,
180
+ // Optional enrichment blocks added by some endpoints.
181
+ analysis: z.record(z.string(), z.unknown()).optional(),
182
+ human_design: HumanDesignZ.optional(),
183
+ })
184
+ .strict()
185
+ .superRefine((value, ctx) => {
186
+ const expectedPointsLen = LLM_V2_POINTS_SCHEMA.length;
187
+ for (let i = 0; i < Math.min(value.points.length, 200); i++) {
188
+ const row = value.points[i];
189
+ if (row.length !== expectedPointsLen) {
190
+ ctx.addIssue({
191
+ code: z.ZodIssueCode.custom,
192
+ path: ["points", i],
193
+ message: `points[${i}] row length ${row.length} != ${expectedPointsLen}`,
194
+ });
195
+ }
196
+ }
197
+ const expectedAspectsLen = LLM_V2_ASPECTS_SCHEMA.length;
198
+ for (let i = 0; i < Math.min(value.aspects.length, 2000); i++) {
199
+ const row = value.aspects[i];
200
+ if (row.length !== expectedAspectsLen) {
201
+ ctx.addIssue({
202
+ code: z.ZodIssueCode.custom,
203
+ path: ["aspects", i],
204
+ message: `aspects[${i}] row length ${row.length} != ${expectedAspectsLen}`,
205
+ });
206
+ }
207
+ }
208
+ });
209
+ // A JSON Schema companion for downstream consumers.
210
+ // (We intentionally keep this small and explicit rather than depending on a zod->jsonschema lib.)
211
+ export const llmV2JsonSchema = {
212
+ $schema: "https://json-schema.org/draft/2020-12/schema",
213
+ title: "Open Ephemeris LLM Projection (format=llm)",
214
+ type: "object",
215
+ additionalProperties: false,
216
+ required: ["chart", "dict", "present", "points_schema", "points", "aspects_schema", "aspects", "counts"],
217
+ properties: {
218
+ output_mode: { type: "string", enum: ["llm"] },
219
+ schema_version: { type: "string", enum: ["llm"] },
220
+ chart: {
221
+ type: "object",
222
+ additionalProperties: false,
223
+ required: ["chart_type"],
224
+ properties: {
225
+ chart_type: { type: "string", enum: ["natal", "synastry"] },
226
+ subject: {
227
+ type: "object",
228
+ additionalProperties: false,
229
+ properties: {
230
+ datetime: { anyOf: [{ type: "string" }, { type: "null" }] },
231
+ jd: { anyOf: [{ type: "number" }, { type: "null" }] },
232
+ lat: { anyOf: [{ type: "number" }, { type: "null" }] },
233
+ lon: { anyOf: [{ type: "number" }, { type: "null" }] },
234
+ },
235
+ },
236
+ },
237
+ },
238
+ rules: {
239
+ type: "object",
240
+ additionalProperties: false,
241
+ required: [
242
+ "lon_range",
243
+ "aspect_set",
244
+ "strength_basis",
245
+ "point_order_id",
246
+ "core_set_id",
247
+ ],
248
+ properties: {
249
+ zodiac_type: { anyOf: [{ type: "string" }, { type: "null" }] },
250
+ house_system: { anyOf: [{ type: "string" }, { type: "null" }] },
251
+ coordinate_system: { anyOf: [{ type: "string" }, { type: "null" }] },
252
+ node_source: { anyOf: [{ type: "string" }, { type: "null" }] },
253
+ lon_range: { type: "string", enum: ["0_360"] },
254
+ aspect_set: { type: "string", enum: ["major_default"] },
255
+ orb_profile: { anyOf: [{ type: "string" }, { type: "null" }] },
256
+ strength_basis: { type: "string", enum: ["engine"] },
257
+ point_order_id: { type: "string", enum: ["astro_point_order_v1"] },
258
+ core_set_id: { type: "string", enum: ["core_interp_set_v1"] },
259
+ },
260
+ },
261
+ reliability: { type: "object" },
262
+ provenance: {
263
+ type: "object",
264
+ additionalProperties: false,
265
+ required: ["engine", "ephemeris_source", "ephemeris_version"],
266
+ properties: {
267
+ engine: { type: "string" },
268
+ ephemeris_source: { type: "string" },
269
+ ephemeris_version: { type: "string" },
270
+ validation_profile: { anyOf: [{ type: "string" }, { type: "null" }] },
271
+ precision_level: { anyOf: [{ type: "string" }, { type: "null" }] },
272
+ checksum: { anyOf: [{ type: "string" }, { type: "null" }] },
273
+ },
274
+ },
275
+ dict: {
276
+ type: "object",
277
+ additionalProperties: false,
278
+ required: ["sign_id", "aspect_id", "aspect_angle", "kind_id"],
279
+ properties: {
280
+ sign_id: {
281
+ type: "array",
282
+ items: { type: "string" },
283
+ const: LLM_V2_DICT.sign_id,
284
+ },
285
+ aspect_id: {
286
+ type: "array",
287
+ items: { type: "string" },
288
+ const: LLM_V2_DICT.aspect_id,
289
+ },
290
+ aspect_angle: {
291
+ type: "array",
292
+ items: { type: "number" },
293
+ const: LLM_V2_DICT.aspect_angle,
294
+ },
295
+ kind_id: {
296
+ type: "array",
297
+ items: { type: "string" },
298
+ const: LLM_V2_DICT.kind_id,
299
+ },
300
+ },
301
+ },
302
+ present: {
303
+ type: "object",
304
+ additionalProperties: false,
305
+ required: ["point_count", "kinds"],
306
+ properties: {
307
+ point_count: { type: "integer" },
308
+ kinds: { type: "object", additionalProperties: { type: "integer" } },
309
+ },
310
+ },
311
+ points_schema: {
312
+ type: "array",
313
+ items: { type: "string" },
314
+ const: LLM_V2_POINTS_SCHEMA,
315
+ },
316
+ points: {
317
+ type: "array",
318
+ items: {
319
+ type: "array",
320
+ minItems: LLM_V2_POINTS_SCHEMA.length,
321
+ maxItems: LLM_V2_POINTS_SCHEMA.length,
322
+ prefixItems: [
323
+ { type: "string" },
324
+ { type: "string" },
325
+ { type: "integer" },
326
+ { type: "number" },
327
+ { anyOf: [{ type: "number" }, { type: "null" }] },
328
+ { anyOf: [{ type: "number" }, { type: "null" }] },
329
+ { type: "integer" },
330
+ { type: "integer" },
331
+ { type: "number" },
332
+ { anyOf: [{ type: "number" }, { type: "null" }] },
333
+ { anyOf: [{ type: "number" }, { type: "null" }] },
334
+ { type: "integer" },
335
+ { type: "string" },
336
+ { anyOf: [{ type: "number" }, { type: "null" }] },
337
+ { anyOf: [{ type: "number" }, { type: "null" }] },
338
+ { type: "integer" },
339
+ ],
340
+ },
341
+ },
342
+ angles: {
343
+ type: "object",
344
+ additionalProperties: false,
345
+ properties: {
346
+ asc: { anyOf: [{ type: "number" }, { type: "null" }] },
347
+ mc: { anyOf: [{ type: "number" }, { type: "null" }] },
348
+ dsc: { anyOf: [{ type: "number" }, { type: "null" }] },
349
+ ic: { anyOf: [{ type: "number" }, { type: "null" }] },
350
+ },
351
+ },
352
+ houses: {
353
+ type: "object",
354
+ additionalProperties: false,
355
+ required: ["cusps"],
356
+ properties: {
357
+ system: { anyOf: [{ type: "string" }, { type: "null" }] },
358
+ cusps: { type: "array", items: { type: "number" } },
359
+ },
360
+ },
361
+ aspects_schema: {
362
+ type: "array",
363
+ items: { type: "string" },
364
+ const: LLM_V2_ASPECTS_SCHEMA,
365
+ },
366
+ aspects: {
367
+ type: "array",
368
+ items: {
369
+ type: "array",
370
+ minItems: LLM_V2_ASPECTS_SCHEMA.length,
371
+ maxItems: LLM_V2_ASPECTS_SCHEMA.length,
372
+ prefixItems: [
373
+ { type: "integer" },
374
+ { type: "integer" },
375
+ { type: "integer" },
376
+ { type: "number" },
377
+ { type: "number" },
378
+ { type: "integer" },
379
+ { type: "number" },
380
+ ],
381
+ },
382
+ },
383
+ patterns: {
384
+ type: "object",
385
+ additionalProperties: false,
386
+ required: ["stellium", "t_square", "grand_trine", "kite", "yod", "grand_cross"],
387
+ properties: {
388
+ stellium: { type: "array" },
389
+ t_square: { type: "array" },
390
+ grand_trine: { type: "array" },
391
+ kite: { type: "array" },
392
+ yod: { type: "array" },
393
+ grand_cross: { type: "array" },
394
+ },
395
+ },
396
+ extras: {
397
+ type: "object",
398
+ additionalProperties: false,
399
+ required: ["fixed_star_conjunctions", "lots"],
400
+ properties: {
401
+ fixed_star_conjunctions: { type: "array" },
402
+ lots: { type: "array" },
403
+ },
404
+ },
405
+ counts: {
406
+ type: "object",
407
+ additionalProperties: false,
408
+ required: ["point_count", "aspect_count"],
409
+ properties: {
410
+ point_count: { type: "integer" },
411
+ aspect_count: { type: "integer" },
412
+ aspect_count_unfiltered: { type: "integer" },
413
+ midpoint_count: { type: "integer" },
414
+ declination_aspect_count: { type: "integer" },
415
+ stellium_count: { type: "integer" },
416
+ },
417
+ },
418
+ analysis: { type: "object" },
419
+ human_design: {
420
+ type: "object",
421
+ additionalProperties: false,
422
+ required: ["gates_schema", "gates"],
423
+ properties: {
424
+ gates_schema: { type: "array", items: { type: "string" } },
425
+ gates: { type: "array", items: { type: "array" } },
426
+ },
427
+ },
428
+ },
429
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -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 {};