@openephemeris/mcp-server 3.3.0 → 3.3.2

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/index.js CHANGED
@@ -5,7 +5,7 @@ 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
7
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
8
- import { initTools, toolRegistry } from "./tools/index.js";
8
+ import { initTools, toolRegistry, formatToolResponse } from "./tools/index.js";
9
9
  function resolveServerVersion() {
10
10
  try {
11
11
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -104,53 +104,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
104
104
  try {
105
105
  const result = await tool.handler(request.params.arguments ?? {});
106
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
- };
107
+ return formatToolResponse(toolName, result, durationMs);
154
108
  }
155
109
  catch (error) {
156
110
  const durationMs = Date.now() - startTime;
@@ -20,7 +20,7 @@ import axios from "axios";
20
20
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
21
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
22
22
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
- import { initTools, toolRegistry } from "./tools/index.js";
23
+ import { initTools, toolRegistry, formatToolResponse } from "./tools/index.js";
24
24
  import { backendClient } from "./backend/client.js";
25
25
  // ---------------------------------------------------------------------------
26
26
  // Helpers
@@ -108,13 +108,16 @@ function createMcpServer() {
108
108
  throw new Error(`Unknown tool: ${toolName}`);
109
109
  }
110
110
  try {
111
+ const startTime = Date.now();
112
+ console.error(`[MCP] Executing tool: ${toolName}`);
111
113
  const result = await tool.handler(request.params.arguments ?? {});
112
- return {
113
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
114
- };
114
+ const durationMs = Date.now() - startTime;
115
+ return formatToolResponse(toolName, result, durationMs);
115
116
  }
116
117
  catch (error) {
118
+ const startTime = Date.now(); // Not strictly accurate for errors but acceptable for logging structure if needed
117
119
  const errorMessage = error instanceof Error ? error.message : String(error);
120
+ console.error(`[MCP] \u274c Failed: ${toolName} - ${errorMessage}`);
118
121
  return {
119
122
  content: [
120
123
  {
@@ -31,3 +31,28 @@ export declare function validateRequired(args: any, requiredKeys: string[]): voi
31
31
  * Throws an error if only one of the coordinate pair is provided.
32
32
  */
33
33
  export declare function validateCoordinates(args: any, latKey: string, lonKey: string): void;
34
+ /**
35
+ * Formats a raw tool response into native MCP content blocks.
36
+ * Intercepts binary image responses (like Chart Wheels) and returns them as native MCP Images.
37
+ * Applies a safety limit to JSON payloads to prevent crashing LLM contexts.
38
+ */
39
+ export declare function formatToolResponse(toolName: string, result: any, durationMs: number): {
40
+ content: {
41
+ type: string;
42
+ data: string;
43
+ mimeType: string;
44
+ }[];
45
+ isError?: undefined;
46
+ } | {
47
+ content: {
48
+ type: string;
49
+ text: string;
50
+ }[];
51
+ isError: boolean;
52
+ } | {
53
+ content: {
54
+ type: string;
55
+ text: string;
56
+ }[];
57
+ isError?: undefined;
58
+ };
@@ -66,3 +66,57 @@ export function validateCoordinates(args, latKey, lonKey) {
66
66
  throw new Error(`Both ${latKey} and ${lonKey} must be provided together, or both omitted.`);
67
67
  }
68
68
  }
69
+ /**
70
+ * Formats a raw tool response into native MCP content blocks.
71
+ * Intercepts binary image responses (like Chart Wheels) and returns them as native MCP Images.
72
+ * Applies a safety limit to JSON payloads to prevent crashing LLM contexts.
73
+ */
74
+ export function formatToolResponse(toolName, result, durationMs) {
75
+ // Intercept binary backend responses (like Chart Wheels) to return native MCP Images
76
+ if (result &&
77
+ typeof result === "object" &&
78
+ "encoding" in result &&
79
+ result.encoding === "base64" &&
80
+ "data_base64" in result &&
81
+ "content_type" in result) {
82
+ const binResp = result;
83
+ console.error(`[MCP] 🎨 Returning image (${binResp.content_length} bytes) for ${toolName} (${durationMs}ms)`);
84
+ return {
85
+ content: [
86
+ {
87
+ type: "image",
88
+ data: binResp.data_base64,
89
+ mimeType: binResp.content_type,
90
+ },
91
+ ],
92
+ };
93
+ }
94
+ const jsonStr = JSON.stringify(result, null, 2);
95
+ // Safety limit: ~500kb JSON is roughly 100k+ tokens.
96
+ // Usually happens if LLMs ask for 50+ years of transits or raw ephemeris arrays.
97
+ if (jsonStr.length > 500_000) {
98
+ console.error(`[MCP] ⚠️ Warning: Payload too large (${(jsonStr.length / 1024).toFixed(1)}kb) for ${toolName}`);
99
+ return {
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: JSON.stringify({
104
+ success: false,
105
+ error: "PAYLOAD_TOO_LARGE",
106
+ 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.`
107
+ }, null, 2),
108
+ },
109
+ ],
110
+ isError: true,
111
+ };
112
+ }
113
+ console.error(`[MCP] ✅ Success: ${toolName} (${durationMs}ms)`);
114
+ return {
115
+ content: [
116
+ {
117
+ type: "text",
118
+ text: jsonStr,
119
+ },
120
+ ],
121
+ };
122
+ }
@@ -17,9 +17,11 @@ registerTool({
17
17
  type: "object",
18
18
  properties: {
19
19
  datetime_a: { type: "string", description: DATETIME_DESC },
20
+ timezone_a: { type: "string", description: "IANA timezone name for subject A, e.g. 'America/Denver'." },
20
21
  latitude_a: { type: "number", description: "Latitude for subject A" },
21
22
  longitude_a: { type: "number", description: "Longitude for subject A" },
22
23
  datetime_b: { type: "string", description: DATETIME_DESC },
24
+ timezone_b: { type: "string", description: "IANA timezone name for subject B, e.g. 'Europe/London'." },
23
25
  latitude_b: { type: "number", description: "Latitude for subject B" },
24
26
  longitude_b: { type: "number", description: "Longitude for subject B" },
25
27
  house_system: {
@@ -56,6 +58,12 @@ registerTool({
56
58
  },
57
59
  }
58
60
  };
61
+ if (args.timezone_a) {
62
+ body.subject_a.birth_location.timezone = { iana_name: args.timezone_a };
63
+ }
64
+ if (args.timezone_b) {
65
+ body.subject_b.birth_location.timezone = { iana_name: args.timezone_b };
66
+ }
59
67
  if (args.house_system) {
60
68
  const mappedCode = HOUSE_SYSTEM_MAP[args.house_system] || "P";
61
69
  body.configuration = { house_system: mappedCode };
@@ -20,6 +20,10 @@ registerTool({
20
20
  type: "string",
21
21
  description: DATETIME_DESC,
22
22
  },
23
+ timezone: {
24
+ type: "string",
25
+ description: "IANA timezone name, e.g. 'America/Denver'. Use this if passing local time without a UTC offset in the datetime string.",
26
+ },
23
27
  latitude: {
24
28
  type: "number",
25
29
  description: "Geographic latitude of birth location in decimal degrees (positive = North).",
@@ -54,6 +58,9 @@ registerTool({
54
58
  },
55
59
  }
56
60
  };
61
+ if (args.timezone) {
62
+ body.subject.birth_location.timezone = { iana_name: args.timezone };
63
+ }
57
64
  if (args.house_system) {
58
65
  const mappedCode = HOUSE_SYSTEM_MAP[args.house_system] || "P";
59
66
  body.configuration = { house_system: mappedCode };
@@ -1,7 +1,7 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
2
  import { backendClient } from "../../backend/client.js";
3
- function buildSubject(name, datetime, lat, lon) {
4
- return {
3
+ function buildSubject(name, datetime, lat, lon, timezone) {
4
+ const subj = {
5
5
  name,
6
6
  birth_datetime: { iso: datetime },
7
7
  birth_location: {
@@ -9,6 +9,10 @@ function buildSubject(name, datetime, lat, lon) {
9
9
  longitude: { decimal: lon },
10
10
  },
11
11
  };
12
+ if (timezone) {
13
+ subj.birth_location.timezone = { iana_name: timezone };
14
+ }
15
+ return subj;
12
16
  }
13
17
  // POST /comparative/composite
14
18
  registerTool({
@@ -23,9 +27,11 @@ registerTool({
23
27
  type: "object",
24
28
  properties: {
25
29
  person_a_datetime: { type: "string", description: "Person A birth datetime (ISO 8601)." },
30
+ person_a_timezone: { type: "string", description: "IANA timezone name for Person A, e.g. 'America/Denver'." },
26
31
  person_a_latitude: { type: "number", description: "Person A birth latitude." },
27
32
  person_a_longitude: { type: "number", description: "Person A birth longitude." },
28
33
  person_b_datetime: { type: "string", description: "Person B birth datetime (ISO 8601)." },
34
+ person_b_timezone: { type: "string", description: "IANA timezone name for Person B, e.g. 'Europe/London'." },
29
35
  person_b_latitude: { type: "number", description: "Person B birth latitude." },
30
36
  person_b_longitude: { type: "number", description: "Person B birth longitude." },
31
37
  },
@@ -42,8 +48,8 @@ registerTool({
42
48
  ]);
43
49
  return await backendClient.post("/comparative/composite", {
44
50
  subjects: [
45
- buildSubject("Person A", args.person_a_datetime, args.person_a_latitude, args.person_a_longitude),
46
- buildSubject("Person B", args.person_b_datetime, args.person_b_latitude, args.person_b_longitude),
51
+ buildSubject("Person A", args.person_a_datetime, args.person_a_latitude, args.person_a_longitude, args.person_a_timezone),
52
+ buildSubject("Person B", args.person_b_datetime, args.person_b_latitude, args.person_b_longitude, args.person_b_timezone),
47
53
  ],
48
54
  });
49
55
  },
@@ -58,9 +64,11 @@ registerTool({
58
64
  type: "object",
59
65
  properties: {
60
66
  person_a_datetime: { type: "string", description: "Person A birth datetime (ISO 8601)." },
67
+ person_a_timezone: { type: "string", description: "IANA timezone name for Person A, e.g. 'America/Denver'." },
61
68
  person_a_latitude: { type: "number", description: "Person A birth latitude." },
62
69
  person_a_longitude: { type: "number", description: "Person A birth longitude." },
63
70
  person_b_datetime: { type: "string", description: "Person B birth datetime (ISO 8601)." },
71
+ person_b_timezone: { type: "string", description: "IANA timezone name for Person B, e.g. 'Europe/London'." },
64
72
  person_b_latitude: { type: "number", description: "Person B birth latitude." },
65
73
  person_b_longitude: { type: "number", description: "Person B birth longitude." },
66
74
  },
@@ -77,8 +85,8 @@ registerTool({
77
85
  ]);
78
86
  return await backendClient.post("/comparative/composite/midpoint", {
79
87
  subjects: [
80
- buildSubject("Person A", args.person_a_datetime, args.person_a_latitude, args.person_a_longitude),
81
- buildSubject("Person B", args.person_b_datetime, args.person_b_latitude, args.person_b_longitude),
88
+ buildSubject("Person A", args.person_a_datetime, args.person_a_latitude, args.person_a_longitude, args.person_a_timezone),
89
+ buildSubject("Person B", args.person_b_datetime, args.person_b_latitude, args.person_b_longitude, args.person_b_timezone),
82
90
  ],
83
91
  });
84
92
  },
@@ -93,9 +101,11 @@ registerTool({
93
101
  type: "object",
94
102
  properties: {
95
103
  person_a_datetime: { type: "string", description: "Person A birth datetime (ISO 8601)." },
104
+ person_a_timezone: { type: "string", description: "IANA timezone name for Person A, e.g. 'America/Denver'." },
96
105
  person_a_latitude: { type: "number", description: "Person A birth latitude." },
97
106
  person_a_longitude: { type: "number", description: "Person A birth longitude." },
98
107
  person_b_datetime: { type: "string", description: "Person B birth datetime (ISO 8601)." },
108
+ person_b_timezone: { type: "string", description: "IANA timezone name for Person B, e.g. 'Europe/London'." },
99
109
  person_b_latitude: { type: "number", description: "Person B birth latitude." },
100
110
  person_b_longitude: { type: "number", description: "Person B birth longitude." },
101
111
  },
@@ -112,8 +122,8 @@ registerTool({
112
122
  ]);
113
123
  return await backendClient.post("/comparative/overlay", {
114
124
  subjects: [
115
- buildSubject("Person A", args.person_a_datetime, args.person_a_latitude, args.person_a_longitude),
116
- buildSubject("Person B", args.person_b_datetime, args.person_b_latitude, args.person_b_longitude),
125
+ buildSubject("Person A", args.person_a_datetime, args.person_a_latitude, args.person_a_longitude, args.person_a_timezone),
126
+ buildSubject("Person B", args.person_b_datetime, args.person_b_latitude, args.person_b_longitude, args.person_b_timezone),
117
127
  ],
118
128
  });
119
129
  },
@@ -130,9 +140,11 @@ registerTool({
130
140
  type: "object",
131
141
  properties: {
132
142
  natal_datetime: { type: "string", description: "Natal birth datetime (ISO 8601)." },
143
+ natal_timezone: { type: "string", description: "IANA timezone name for Natal, e.g. 'America/Denver'." },
133
144
  natal_latitude: { type: "number", description: "Natal birth latitude." },
134
145
  natal_longitude: { type: "number", description: "Natal birth longitude." },
135
146
  transit_datetime: { type: "string", description: "Transit moment (ISO 8601). Defaults to now." },
147
+ transit_timezone: { type: "string", description: "IANA timezone name for Transit, e.g. 'America/Denver'. Only applicable if transit_datetime is provided without offset." },
136
148
  },
137
149
  required: ["natal_datetime", "natal_latitude", "natal_longitude"],
138
150
  additionalProperties: false,
@@ -140,11 +152,11 @@ registerTool({
140
152
  handler: async (args) => {
141
153
  validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude"]);
142
154
  const subjects = [
143
- buildSubject("Natal", args.natal_datetime, args.natal_latitude, args.natal_longitude),
155
+ buildSubject("Natal", args.natal_datetime, args.natal_latitude, args.natal_longitude, args.natal_timezone),
144
156
  ];
145
157
  // The transit subject uses the transit datetime or current time
146
158
  if (args.transit_datetime) {
147
- subjects.push(buildSubject("Transit", args.transit_datetime, args.natal_latitude, args.natal_longitude));
159
+ subjects.push(buildSubject("Transit", args.transit_datetime, args.natal_latitude, args.natal_longitude, args.transit_timezone));
148
160
  }
149
161
  return await backendClient.post("/comparative/natal-transits", {
150
162
  subjects,
@@ -6,7 +6,8 @@ registerTool({
6
6
  description: "Get the precise ecliptic longitude, latitude, distance, speed, and retrograde status " +
7
7
  "for a single planet/body at a given date and time. " +
8
8
  "Planet IDs: 0=Sun, 1=Moon, 2=Mercury, 3=Venus, 4=Mars, 5=Jupiter, 6=Saturn, " +
9
- "7=Uranus, 8=Neptune, 9=Pluto, 10=North Node, 11=South Node, 12=Lilith.\n\n" +
9
+ "7=Uranus, 8=Neptune, 9=Pluto, 10=North Node, 11=South Node, 12=Lilith, " +
10
+ "15=Chiron, 17=Ceres, 18=Pallas, 19=Juno, 20=Vesta.\n\n" +
10
11
  "CREDIT COST: 1 credit per call.\n\n" +
11
12
  "EXAMPLE: Where is Mars on 2026-03-20 at noon UTC?\n" +
12
13
  " planet_id=4, datetime='2026-03-20T12:00:00Z'",
@@ -15,11 +16,11 @@ registerTool({
15
16
  properties: {
16
17
  planet_id: {
17
18
  type: "integer",
18
- description: "Planet/body ID (0=Sun, 1=Moon, 2=Mercury, 3=Venus, 4=Mars, 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune, 9=Pluto, 10=MeanNode, 11=TrueNode, 12=Lilith).",
19
+ description: "Planet/body ID (0=Sun, 1=Moon, 2=Mercury, 3=Venus, 4=Mars, 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune, 9=Pluto, 10=MeanNode, 11=TrueNode, 12=Lilith, 15=Chiron, 17=Ceres, 18=Pallas, 19=Juno, 20=Vesta).",
19
20
  },
20
21
  datetime: {
21
22
  type: "string",
22
- description: "ISO 8601 date/time (e.g. '2026-03-20T12:00:00Z').",
23
+ description: "ISO 8601 date/time in UTC or with offset (e.g. '2026-03-20T12:00:00Z' or '2026-03-20T12:00:00-05:00').",
23
24
  },
24
25
  latitude: {
25
26
  type: "number",
@@ -60,7 +61,7 @@ registerTool({
60
61
  properties: {
61
62
  datetime: {
62
63
  type: "string",
63
- description: "ISO 8601 date/time.",
64
+ description: "ISO 8601 date/time in UTC or with offset (e.g. '2026-03-20T12:00:00Z').",
64
65
  },
65
66
  latitude: {
66
67
  type: "number",
@@ -12,8 +12,8 @@ registerTool({
12
12
  name: "ephemeris_natal_chart",
13
13
  description: "Calculate a full natal (birth) chart for a person. Returns planetary positions, house cusps, " +
14
14
  "aspects, and chart patterns. Use format='llm' for a compact, token-efficient output ideal for " +
15
- "interpretation (available on all tiers). The result includes all major planets, angles (ASC/MC/DSC/IC), " +
16
- "essential dignities, retrograde status, house system data, and major aspect grid.\n\n" +
15
+ "interpretation (available on all tiers). The result includes all major planets, Chiron and major asteroids like Ceres, angles (ASC/MC/DSC/IC), " +
16
+ "essential dignities, retrograde status, house system data, and major aspect grid. Asteroids are automatically included.\n\n" +
17
17
  "CREDIT COST: 1 credit per call.\n\n" +
18
18
  "EXAMPLE: Calculate the natal chart for someone born April 15, 1990 at 2:30 PM in Chicago:\n" +
19
19
  " datetime='1990-04-15T14:30:00', latitude=41.8781, longitude=-87.6298",
@@ -24,6 +24,10 @@ registerTool({
24
24
  type: "string",
25
25
  description: DATETIME_DESC,
26
26
  },
27
+ timezone: {
28
+ type: "string",
29
+ description: "IANA timezone name, e.g. 'America/Denver'. Use this if passing local time without a UTC offset in the datetime string.",
30
+ },
27
31
  latitude: {
28
32
  type: "number",
29
33
  description: "Geographic latitude of birth location in decimal degrees (positive = North).",
@@ -66,6 +70,9 @@ registerTool({
66
70
  },
67
71
  }
68
72
  };
73
+ if (args.timezone) {
74
+ body.subject.birth_location.timezone = { iana_name: args.timezone };
75
+ }
69
76
  if (args.house_system) {
70
77
  const code = HOUSE_SYSTEM_MAP[args.house_system] ?? args.house_system;
71
78
  body.configuration = { house_system: code };
@@ -13,11 +13,11 @@ registerTool({
13
13
  properties: {
14
14
  birth_datetime: {
15
15
  type: "string",
16
- description: "ISO 8601 birth date/time (e.g. '1985-06-21T14:00:00').",
16
+ description: "ISO 8601 birth date/time. MUST include offset or Z (e.g. '1985-06-21T14:00:00-05:00').",
17
17
  },
18
18
  target_datetime: {
19
19
  type: "string",
20
- description: "Date/time near which to find the Solar Return (ISO 8601). Required.",
20
+ description: "Date/time near which to find the Solar Return (ISO 8601). MUST include offset or Z. Required.",
21
21
  },
22
22
  birth_latitude: {
23
23
  type: "number",
@@ -73,11 +73,11 @@ registerTool({
73
73
  properties: {
74
74
  birth_datetime: {
75
75
  type: "string",
76
- description: "ISO 8601 birth date/time.",
76
+ description: "ISO 8601 birth date/time. MUST include offset or Z.",
77
77
  },
78
78
  target_datetime: {
79
79
  type: "string",
80
- description: "Date/time near which to find the Lunar Return (ISO 8601). Required.",
80
+ description: "Date/time near which to find the Lunar Return (ISO 8601). MUST include offset or Z. Required.",
81
81
  },
82
82
  birth_latitude: {
83
83
  type: "number",
@@ -123,11 +123,11 @@ registerTool({
123
123
  },
124
124
  birth_datetime: {
125
125
  type: "string",
126
- description: "ISO 8601 birth date/time.",
126
+ description: "ISO 8601 birth date/time. MUST include offset or Z.",
127
127
  },
128
128
  target_datetime: {
129
129
  type: "string",
130
- description: "Date/time near which to find the return (ISO 8601). Required.",
130
+ description: "Date/time near which to find the return (ISO 8601). MUST include offset or Z. Required.",
131
131
  },
132
132
  },
133
133
  required: ["body", "birth_datetime", "target_datetime"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openephemeris/mcp-server",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "Model Context Protocol server for the Open Ephemeris astronomical computation API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",