@openephemeris/mcp-server 3.0.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +32 -22
  2. package/config/dev-allowlist.json +1319 -1165
  3. package/dist/backend/client.d.ts +12 -0
  4. package/dist/backend/client.js +99 -35
  5. package/dist/index.js +5 -0
  6. package/dist/schema-packs/llm.d.ts +1 -1
  7. package/dist/schema-packs/llm.js +1 -1
  8. package/dist/scripts/dev-allowlist.d.ts +1 -0
  9. package/dist/scripts/dev-allowlist.js +287 -0
  10. package/dist/scripts/pack-audit.d.ts +1 -0
  11. package/dist/scripts/pack-audit.js +45 -0
  12. package/dist/scripts/schema-packs.d.ts +1 -0
  13. package/dist/scripts/schema-packs.js +150 -0
  14. package/dist/scripts/smoke-dev-profile.d.ts +1 -0
  15. package/dist/scripts/smoke-dev-profile.js +25 -0
  16. package/dist/scripts/sync-readme.d.ts +1 -0
  17. package/dist/scripts/sync-readme.js +141 -0
  18. package/dist/src/auth/credentials.d.ts +65 -0
  19. package/dist/src/auth/credentials.js +200 -0
  20. package/dist/src/auth/device-auth.d.ts +56 -0
  21. package/dist/src/auth/device-auth.js +144 -0
  22. package/dist/src/backend/client.d.ts +61 -0
  23. package/dist/src/backend/client.js +335 -0
  24. package/dist/src/index.d.ts +2 -0
  25. package/dist/src/index.js +92 -0
  26. package/dist/src/schema-packs/llm.d.ts +105 -0
  27. package/dist/src/schema-packs/llm.js +429 -0
  28. package/dist/src/tools/auth.d.ts +1 -0
  29. package/dist/src/tools/auth.js +202 -0
  30. package/dist/src/tools/dev.d.ts +1 -0
  31. package/dist/src/tools/dev.js +187 -0
  32. package/dist/src/tools/index.d.ts +25 -0
  33. package/dist/src/tools/index.js +33 -0
  34. package/dist/src/tools/specialized/eclipse.d.ts +1 -0
  35. package/dist/src/tools/specialized/eclipse.js +56 -0
  36. package/dist/src/tools/specialized/electional.d.ts +1 -0
  37. package/dist/src/tools/specialized/electional.js +79 -0
  38. package/dist/src/tools/specialized/human_design.d.ts +1 -0
  39. package/dist/src/tools/specialized/human_design.js +53 -0
  40. package/dist/src/tools/specialized/moon.d.ts +1 -0
  41. package/dist/src/tools/specialized/moon.js +50 -0
  42. package/dist/src/tools/specialized/natal.d.ts +1 -0
  43. package/dist/src/tools/specialized/natal.js +71 -0
  44. package/dist/src/tools/specialized/relocation.d.ts +1 -0
  45. package/dist/src/tools/specialized/relocation.js +71 -0
  46. package/dist/src/tools/specialized/synastry.d.ts +1 -0
  47. package/dist/src/tools/specialized/synastry.js +61 -0
  48. package/dist/src/tools/specialized/transits.d.ts +1 -0
  49. package/dist/src/tools/specialized/transits.js +80 -0
  50. package/dist/test/allowlist-and-tools.test.d.ts +1 -0
  51. package/dist/test/allowlist-and-tools.test.js +96 -0
  52. package/dist/test/backend-client.test.d.ts +1 -0
  53. package/dist/test/backend-client.test.js +286 -0
  54. package/dist/test/credentials.test.d.ts +1 -0
  55. package/dist/test/credentials.test.js +143 -0
  56. package/dist/tools/dev.js +7 -3
  57. package/dist/tools/index.d.ts +7 -0
  58. package/package.json +3 -3
@@ -0,0 +1,50 @@
1
+ import { registerTool } from "../index.js";
2
+ import { backendClient } from "../../backend/client.js";
3
+ registerTool({
4
+ name: "ephemeris.moon_phase",
5
+ description: "Get the current Moon phase and void-of-course status. Returns the Moon's sign, phase name " +
6
+ "(New, Waxing Crescent, First Quarter, Waxing Gibbous, Full, Waning Gibbous, Last Quarter, " +
7
+ "Waning Crescent), exact phase angle, illumination percentage, and the next void-of-course period.\n\n" +
8
+ "CREDIT COST: 1 credit per call.\n\n" +
9
+ "If no datetime is provided, returns the current (live) moon phase.\n\n" +
10
+ "EXAMPLE: Get moon phase for a specific date/time:\n" +
11
+ " datetime='2026-03-20T12:00:00Z'\n\n" +
12
+ "EXAMPLE: Get the current moon phase right now:\n" +
13
+ " (call with no arguments)",
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {
17
+ datetime: {
18
+ type: "string",
19
+ description: "ISO 8601 datetime to query. If omitted, returns the current live moon phase (UTC now).",
20
+ },
21
+ latitude: {
22
+ type: "number",
23
+ description: "Observer latitude (optional, used for local void-of-course calculations).",
24
+ },
25
+ longitude: {
26
+ type: "number",
27
+ description: "Observer longitude (optional, used for local void-of-course calculations).",
28
+ },
29
+ },
30
+ required: [],
31
+ additionalProperties: false,
32
+ },
33
+ handler: async (args) => {
34
+ const params = {};
35
+ if (args.datetime)
36
+ params.datetime = args.datetime;
37
+ if (args.latitude != null)
38
+ params.latitude = args.latitude;
39
+ if (args.longitude != null)
40
+ params.longitude = args.longitude;
41
+ const [phase, voc] = await Promise.allSettled([
42
+ backendClient.request("GET", "/ephemeris/moon/phase", { params }),
43
+ backendClient.request("GET", "/ephemeris/moon/void-of-course", { params }),
44
+ ]);
45
+ return {
46
+ phase: phase.status === "fulfilled" ? phase.value : { error: phase.reason?.message },
47
+ void_of_course: voc.status === "fulfilled" ? voc.value : { error: voc.reason?.message },
48
+ };
49
+ },
50
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import { registerTool } from "../index.js";
2
+ import { backendClient } from "../../backend/client.js";
3
+ const DATETIME_DESC = "ISO 8601 datetime string, e.g. '1990-04-15T14:30:00' (local time at birth location). " +
4
+ "Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.";
5
+ registerTool({
6
+ name: "ephemeris.natal_chart",
7
+ description: "Calculate a full natal (birth) chart for a person. Returns planetary positions, house cusps, " +
8
+ "aspects, and chart patterns. Use format='llm' for a compact, token-efficient output ideal for " +
9
+ "interpretation (available on all tiers). The result includes all major planets, angles (ASC/MC/DSC/IC), " +
10
+ "essential dignities, retrograde status, house system data, and major aspect grid.\n\n" +
11
+ "CREDIT COST: 1 credit per call.\n\n" +
12
+ "EXAMPLE: Calculate the natal chart for someone born April 15, 1990 at 2:30 PM in Chicago:\n" +
13
+ " datetime='1990-04-15T14:30:00', latitude=41.8781, longitude=-87.6298",
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {
17
+ datetime: {
18
+ type: "string",
19
+ description: DATETIME_DESC,
20
+ },
21
+ latitude: {
22
+ type: "number",
23
+ description: "Geographic latitude of birth location in decimal degrees (positive = North).",
24
+ },
25
+ longitude: {
26
+ type: "number",
27
+ description: "Geographic longitude of birth location in decimal degrees (positive = East).",
28
+ },
29
+ house_system: {
30
+ type: "string",
31
+ enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry", "alcabitius", "morinus"],
32
+ description: "House system to use. Defaults to 'placidus' if omitted.",
33
+ },
34
+ format: {
35
+ type: "string",
36
+ enum: ["json", "llm"],
37
+ description: "Output format. 'llm' returns a compact array-based projection optimized for LLM token efficiency (available on all tiers). 'json' returns full verbose JSON.",
38
+ },
39
+ include_arabic_parts: {
40
+ type: "boolean",
41
+ description: "If true, include Hermetic Lots (Arabic Parts) in the response.",
42
+ },
43
+ include_fixed_stars: {
44
+ type: "boolean",
45
+ description: "If true, include fixed star conjunctions in the response.",
46
+ },
47
+ },
48
+ required: ["datetime", "latitude", "longitude"],
49
+ additionalProperties: false,
50
+ },
51
+ handler: async (args) => {
52
+ const body = {
53
+ datetime: args.datetime,
54
+ latitude: args.latitude,
55
+ longitude: args.longitude,
56
+ };
57
+ if (args.house_system)
58
+ body.house_system = args.house_system;
59
+ if (args.include_arabic_parts)
60
+ body.include_arabic_parts = args.include_arabic_parts;
61
+ if (args.include_fixed_stars)
62
+ body.include_fixed_stars = args.include_fixed_stars;
63
+ const query = {};
64
+ if (args.format)
65
+ query.format = args.format;
66
+ return await backendClient.request("POST", "/ephemeris/natal-chart", {
67
+ data: body,
68
+ params: query,
69
+ });
70
+ },
71
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import { registerTool } from "../index.js";
2
+ import { backendClient } from "../../backend/client.js";
3
+ registerTool({
4
+ name: "ephemeris.relocation",
5
+ description: "Calculate a relocation chart — the same natal planetary positions re-cast for a different " +
6
+ "geographic location. Used to understand how living in a different city shifts house placements " +
7
+ "and angles, without changing the planetary longitudes in the chart.\n\n" +
8
+ "CREDIT COST: 1 credit per call.\n\n" +
9
+ "EXAMPLE: How does moving from Chicago to London change someone's chart?\n" +
10
+ " natal_datetime='1990-04-15T14:30:00', natal_latitude=41.8781, natal_longitude=-87.6298,\n" +
11
+ " relocation_latitude=51.5074, relocation_longitude=-0.1278",
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ natal_datetime: {
16
+ type: "string",
17
+ description: "ISO 8601 birth datetime (local time at birth location).",
18
+ },
19
+ natal_latitude: {
20
+ type: "number",
21
+ description: "Latitude of birth location in decimal degrees.",
22
+ },
23
+ natal_longitude: {
24
+ type: "number",
25
+ description: "Longitude of birth location in decimal degrees.",
26
+ },
27
+ relocation_latitude: {
28
+ type: "number",
29
+ description: "Latitude of the relocation city in decimal degrees (positive = North).",
30
+ },
31
+ relocation_longitude: {
32
+ type: "number",
33
+ description: "Longitude of the relocation city in decimal degrees (positive = East).",
34
+ },
35
+ house_system: {
36
+ type: "string",
37
+ enum: ["placidus", "whole_sign", "equal", "koch", "campanus", "regiomontanus", "porphyry"],
38
+ description: "House system to use. Defaults to 'placidus'.",
39
+ },
40
+ format: {
41
+ type: "string",
42
+ enum: ["json", "llm"],
43
+ description: "Output format. 'llm' = compact token-efficient output (available on all tiers).",
44
+ },
45
+ },
46
+ required: ["natal_datetime", "natal_latitude", "natal_longitude", "relocation_latitude", "relocation_longitude"],
47
+ additionalProperties: false,
48
+ },
49
+ handler: async (args) => {
50
+ const body = {
51
+ natal: {
52
+ datetime: args.natal_datetime,
53
+ latitude: args.natal_latitude,
54
+ longitude: args.natal_longitude,
55
+ },
56
+ relocation: {
57
+ latitude: args.relocation_latitude,
58
+ longitude: args.relocation_longitude,
59
+ },
60
+ };
61
+ if (args.house_system)
62
+ body.house_system = args.house_system;
63
+ const query = {};
64
+ if (args.format)
65
+ query.format = args.format;
66
+ return await backendClient.request("POST", "/ephemeris/relocation", {
67
+ data: body,
68
+ params: query,
69
+ });
70
+ },
71
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import { registerTool } from "../index.js";
2
+ import { backendClient } from "../../backend/client.js";
3
+ registerTool({
4
+ name: "ephemeris.synastry",
5
+ description: "Calculate a synastry chart comparing two people's natal charts. Returns inter-aspects " +
6
+ "(planetary connections between the two charts), composite points, and relationship indicators. " +
7
+ "Use this for compatibility analysis, relationship timing, or partnership insights.\n\n" +
8
+ "CREDIT COST: 5 credits per call.\n\n" +
9
+ "EXAMPLE: Compare two people's charts:\n" +
10
+ " person_a_datetime='1990-04-15T14:30:00', person_a_latitude=41.8781, person_a_longitude=-87.6298,\n" +
11
+ " person_b_datetime='1988-09-22T08:15:00', person_b_latitude=34.0522, person_b_longitude=-118.2437",
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ person_a_datetime: { type: "string", description: "Person A birth datetime (ISO 8601)." },
16
+ person_a_latitude: { type: "number", description: "Person A birth latitude." },
17
+ person_a_longitude: { type: "number", description: "Person A birth longitude." },
18
+ person_b_datetime: { type: "string", description: "Person B birth datetime (ISO 8601)." },
19
+ person_b_latitude: { type: "number", description: "Person B birth latitude." },
20
+ person_b_longitude: { type: "number", description: "Person B birth longitude." },
21
+ house_system: {
22
+ type: "string",
23
+ enum: ["placidus", "whole_sign", "equal", "koch"],
24
+ description: "House system for both charts. Defaults to 'placidus'.",
25
+ },
26
+ format: {
27
+ type: "string",
28
+ enum: ["json", "llm"],
29
+ description: "Output format. 'llm' is compact and token-efficient (available on all tiers).",
30
+ },
31
+ },
32
+ required: [
33
+ "person_a_datetime", "person_a_latitude", "person_a_longitude",
34
+ "person_b_datetime", "person_b_latitude", "person_b_longitude",
35
+ ],
36
+ additionalProperties: false,
37
+ },
38
+ handler: async (args) => {
39
+ const body = {
40
+ natal_a: {
41
+ datetime: args.person_a_datetime,
42
+ latitude: args.person_a_latitude,
43
+ longitude: args.person_a_longitude,
44
+ },
45
+ natal_b: {
46
+ datetime: args.person_b_datetime,
47
+ latitude: args.person_b_latitude,
48
+ longitude: args.person_b_longitude,
49
+ },
50
+ };
51
+ if (args.house_system)
52
+ body.house_system = args.house_system;
53
+ const query = {};
54
+ if (args.format)
55
+ query.format = args.format;
56
+ return await backendClient.request("POST", "/comparative/synastry", {
57
+ data: body,
58
+ params: query,
59
+ });
60
+ },
61
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ import { registerTool } from "../index.js";
2
+ import { backendClient } from "../../backend/client.js";
3
+ registerTool({
4
+ name: "ephemeris.transits",
5
+ description: "Search for astrological transit events affecting a natal chart over a date range. " +
6
+ "Returns a list of exact transit moments — when transiting planets form specified aspects " +
7
+ "to natal planet positions. Ideal for generating horoscope timelines, event forecasting, or " +
8
+ "finding optimal timing windows.\n\n" +
9
+ "CREDIT COST: 5 credits per call.\n\n" +
10
+ "EXAMPLE: Find all Saturn transits to the natal Sun/Moon for the next 6 months:\n" +
11
+ " natal_datetime='1990-04-15T14:30:00', natal_latitude=41.8781, natal_longitude=-87.6298,\n" +
12
+ " start_date='2026-01-01', end_date='2026-06-30', transiting_planets=['saturn']",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {
16
+ natal_datetime: {
17
+ type: "string",
18
+ description: "ISO 8601 birth datetime for the natal chart.",
19
+ },
20
+ natal_latitude: {
21
+ type: "number",
22
+ description: "Latitude of birth location in decimal degrees.",
23
+ },
24
+ natal_longitude: {
25
+ type: "number",
26
+ description: "Longitude of birth location in decimal degrees.",
27
+ },
28
+ start_date: {
29
+ type: "string",
30
+ description: "Start of the transit search window, ISO 8601 date or datetime (e.g. '2026-01-01').",
31
+ },
32
+ end_date: {
33
+ type: "string",
34
+ description: "End of the transit search window, ISO 8601 date or datetime (e.g. '2026-06-30').",
35
+ },
36
+ transiting_planets: {
37
+ type: "array",
38
+ items: { type: "string" },
39
+ description: "List of transiting planet IDs to search. E.g. ['saturn', 'jupiter', 'uranus', 'pluto']. " +
40
+ "Omit to search all outer planets.",
41
+ },
42
+ natal_points: {
43
+ type: "array",
44
+ items: { type: "string" },
45
+ description: "Natal point IDs to receive transits. E.g. ['sun', 'moon', 'asc', 'mc']. Omit for all core points.",
46
+ },
47
+ aspects: {
48
+ type: "array",
49
+ items: { type: "string", enum: ["conjunction", "opposition", "trine", "square", "sextile"] },
50
+ description: "Aspect types to include in the search. Defaults to all major aspects.",
51
+ },
52
+ orb: {
53
+ type: "number",
54
+ description: "Maximum orb in degrees (default: 1.0).",
55
+ },
56
+ },
57
+ required: ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"],
58
+ additionalProperties: false,
59
+ },
60
+ handler: async (args) => {
61
+ const body = {
62
+ natal: {
63
+ datetime: args.natal_datetime,
64
+ latitude: args.natal_latitude,
65
+ longitude: args.natal_longitude,
66
+ },
67
+ start_date: args.start_date,
68
+ end_date: args.end_date,
69
+ };
70
+ if (args.transiting_planets)
71
+ body.transiting_planets = args.transiting_planets;
72
+ if (args.natal_points)
73
+ body.natal_points = args.natal_points;
74
+ if (args.aspects)
75
+ body.aspects = args.aspects;
76
+ if (args.orb != null)
77
+ body.orb = args.orb;
78
+ return await backendClient.request("POST", "/predictive/transits/search", { data: body });
79
+ },
80
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, beforeAll } from "vitest";
2
+ import { toolRegistry, initTools } from "../src/tools/index.js";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const here = path.dirname(fileURLToPath(import.meta.url));
7
+ const allowlistPath = path.resolve(here, "../config/dev-allowlist.json");
8
+ function isDeniedByPrefix(pathname, prefixes) {
9
+ return prefixes.some((p) => pathname.startsWith(p));
10
+ }
11
+ function isAllowedOperation(method, pathname, allow) {
12
+ return allow.some((e) => e.method.toUpperCase() === method.toUpperCase() && e.path === pathname);
13
+ }
14
+ describe("dev-allowlist.json", () => {
15
+ const raw = fs.readFileSync(allowlistPath, "utf-8");
16
+ const allowlist = JSON.parse(raw);
17
+ it("has valid schema identifier", () => {
18
+ expect(allowlist.schema).toBe("astromcp-dev-allowlist-v1");
19
+ });
20
+ it("has at least 60 allowlisted operations", () => {
21
+ // 68 entries from Go openapi.json; this guard catches accidental mass-deletion
22
+ expect(allowlist.allow.length).toBeGreaterThanOrEqual(60);
23
+ });
24
+ it("all allow entries have valid method and path starting with /", () => {
25
+ const validMethods = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
26
+ for (const entry of allowlist.allow) {
27
+ expect(validMethods.has(entry.method), `Invalid method: ${entry.method}`).toBe(true);
28
+ expect(entry.path.startsWith("/"), `Path must start with /: ${entry.path}`).toBe(true);
29
+ }
30
+ });
31
+ it("deny prefixes block auth/billing/admin paths", () => {
32
+ const denyPrefixes = allowlist.deny?.path_prefixes ?? [];
33
+ const blockedPaths = ["/auth/login", "/billing/me", "/admin/users", "/api-keys"];
34
+ for (const blocked of blockedPaths) {
35
+ expect(isDeniedByPrefix(blocked, denyPrefixes), `Expected ${blocked} to be blocked by deny prefixes`).toBe(true);
36
+ }
37
+ });
38
+ it("no allow entry violates deny prefixes", () => {
39
+ const denyPrefixes = allowlist.deny?.path_prefixes ?? [];
40
+ for (const entry of allowlist.allow) {
41
+ expect(isDeniedByPrefix(entry.path, denyPrefixes), `Allow entry ${entry.method} ${entry.path} violates a deny prefix`).toBe(false);
42
+ }
43
+ });
44
+ it("isAllowedOperation works for known Go endpoints", () => {
45
+ expect(isAllowedOperation("GET", "/catalogs/bodies", allowlist.allow)).toBe(true);
46
+ expect(isAllowedOperation("GET", "/electional/find-window", allowlist.allow)).toBe(true);
47
+ });
48
+ it("phantom paths are NOT in the allowlist (Go API only)", () => {
49
+ // These paths don't exist in the Go openapi.json — they were legacy Python routes
50
+ expect(isAllowedOperation("GET", "/agro/daily", allowlist.allow)).toBe(false);
51
+ expect(isAllowedOperation("GET", "/agro/calendar", allowlist.allow)).toBe(false);
52
+ expect(isAllowedOperation("GET", "/batch/position", allowlist.allow)).toBe(false);
53
+ expect(isAllowedOperation("GET", "/houses", allowlist.allow)).toBe(false);
54
+ expect(isAllowedOperation("GET", "/lunar/solunar", allowlist.allow)).toBe(false);
55
+ expect(isAllowedOperation("GET", "/lunar/void-of-course", allowlist.allow)).toBe(false);
56
+ });
57
+ it("isAllowedOperation blocks denied paths", () => {
58
+ expect(isAllowedOperation("POST", "/auth/login", allowlist.allow)).toBe(false);
59
+ expect(isAllowedOperation("GET", "/admin/users", allowlist.allow)).toBe(false);
60
+ expect(isAllowedOperation("DELETE", "/api-keys/abc", allowlist.allow)).toBe(false);
61
+ });
62
+ });
63
+ describe("toolRegistry", () => {
64
+ beforeAll(async () => {
65
+ await initTools("dev");
66
+ });
67
+ it("registers all expected tools", () => {
68
+ const expectedTools = [
69
+ "auth.login",
70
+ "auth.status",
71
+ "auth.logout",
72
+ "dev.call",
73
+ "dev.list_allowed",
74
+ "ephemeris.natal_chart",
75
+ "ephemeris.transits",
76
+ "ephemeris.moon_phase",
77
+ "ephemeris.next_eclipse",
78
+ "ephemeris.synastry",
79
+ "ephemeris.relocation",
80
+ "ephemeris.electional",
81
+ "human_design.chart",
82
+ ];
83
+ for (const name of expectedTools) {
84
+ expect(toolRegistry[name], `Missing tool: ${name}`).toBeDefined();
85
+ }
86
+ });
87
+ it("every registered tool has required fields", () => {
88
+ for (const [name, tool] of Object.entries(toolRegistry)) {
89
+ expect(typeof tool.name, `${name}: name must be string`).toBe("string");
90
+ expect(typeof tool.description, `${name}: description must be string`).toBe("string");
91
+ expect(tool.description.length, `${name}: description must not be empty`).toBeGreaterThan(10);
92
+ expect(typeof tool.handler, `${name}: handler must be function`).toBe("function");
93
+ expect(tool.inputSchema, `${name}: inputSchema must be defined`).toBeDefined();
94
+ }
95
+ });
96
+ });
@@ -0,0 +1 @@
1
+ export {};