@openephemeris/mcp-server 4.19.0 → 4.20.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [4.20.0] — 2026-09-27
11
+
12
+ ### Added
13
+ - **Six more tools can show their chart as an image.** Pass `include_visual: true` (+2 credits) to
14
+ `ephemeris_progressed_chart` (natal inside, progressed outside), `ephemeris_relocation` (the
15
+ relocated wheel), `ephemeris_composite` (the Davison relationship wheel), `ephemeris_overlay` (both
16
+ charts as a bi-wheel), `hd_planetary_return` and `hd_opposition` (the bodygraph for that moment).
17
+ The picture appears in the conversation next to the data.
18
+
19
+ ### Fixed
20
+ - `ephemeris_composite` described itself as a planet-by-planet midpoint composite; it returns a
21
+ Davison relationship chart. For the midpoint method use `ephemeris_composite_midpoint`.
22
+
10
23
  ## [4.19.0] — 2026-09-27
11
24
 
12
25
  ### Changed
@@ -1,6 +1,7 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
- import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
3
+ import { OUTPUT_SCHEMA_IMAGE_AND_JSON, OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
4
+ import { applyVisual, visualProperties } from "../visual.js";
4
5
  import { DATETIME_DESC, assertZonedDatetime, toDateTimeInputBody, timezoneProperty } from "../datetime.js";
5
6
  /** Reject a zone-less person datetime here, before a credit is spent on it. */
6
7
  function assertPersonZoned(prefix, args) {
@@ -23,8 +24,9 @@ function buildSubject(name, datetime, lat, lon, timezone) {
23
24
  // POST /comparative/composite
24
25
  registerTool({
25
26
  name: "ephemeris_composite",
26
- description: "Calculate a composite chart from two or more natal charts. The composite " +
27
- "uses midpoints of each planet pair to derive a single relationship chart.\n\n" +
27
+ description: "Calculate a Davison relationship chart for two people — a real chart cast for the " +
28
+ "midpoint in time and place between the two births, with its own houses and angles. " +
29
+ "For the planet-by-planet midpoint composite use ephemeris_composite_midpoint.\n\n" +
28
30
  "CREDIT COST: 3 credits per call.\n\n" +
29
31
  "EXAMPLE: Composite for two people:\n" +
30
32
  " person_a_datetime='1990-04-15T14:30:00', person_a_timezone='America/Chicago',\n" +
@@ -43,6 +45,7 @@ registerTool({
43
45
  person_b_latitude: { type: "number", description: "Person B birth latitude." },
44
46
  person_b_longitude: { type: "number", description: "Person B birth longitude." },
45
47
  format: { type: "string", enum: ["json", "llm"], description: "Use 'llm' for token-efficient LLM projection." },
48
+ ...visualProperties("the relationship chart wheel"),
46
49
  },
47
50
  required: [
48
51
  "person_a_datetime", "person_a_latitude", "person_a_longitude",
@@ -50,7 +53,7 @@ registerTool({
50
53
  ],
51
54
  additionalProperties: false,
52
55
  },
53
- outputSchema: OUTPUT_SCHEMA_JSON,
56
+ outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
54
57
  annotations: { title: "Davison Relationship Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
55
58
  handler: async (args) => {
56
59
  validateRequired(args, [
@@ -62,15 +65,14 @@ registerTool({
62
65
  const query = {};
63
66
  if (args.format)
64
67
  query.format = args.format;
65
- return await getActiveClient().request("POST", "/comparative/composite", {
66
- params: query,
67
- data: {
68
- subjects: [
69
- buildSubject("Person A", args.person_a_datetime, args.person_a_latitude, args.person_a_longitude, args.person_a_timezone),
70
- buildSubject("Person B", args.person_b_datetime, args.person_b_latitude, args.person_b_longitude, args.person_b_timezone),
71
- ],
72
- }
73
- });
68
+ const body = {
69
+ subjects: [
70
+ buildSubject("Person A", args.person_a_datetime, args.person_a_latitude, args.person_a_longitude, args.person_a_timezone),
71
+ buildSubject("Person B", args.person_b_datetime, args.person_b_latitude, args.person_b_longitude, args.person_b_timezone),
72
+ ],
73
+ };
74
+ applyVisual(body, args);
75
+ return await getActiveClient().request("POST", "/comparative/composite", { params: query, data: body });
74
76
  },
75
77
  });
76
78
  // POST /comparative/composite/midpoint
@@ -139,6 +141,7 @@ registerTool({
139
141
  person_b_latitude: { type: "number", description: "Person B birth latitude." },
140
142
  person_b_longitude: { type: "number", description: "Person B birth longitude." },
141
143
  format: { type: "string", enum: ["json", "llm"], description: "Use 'llm' for token-efficient LLM projection." },
144
+ ...visualProperties("a bi-wheel of the two charts"),
142
145
  },
143
146
  required: [
144
147
  "person_a_datetime", "person_a_latitude", "person_a_longitude",
@@ -146,7 +149,7 @@ registerTool({
146
149
  ],
147
150
  additionalProperties: false,
148
151
  },
149
- outputSchema: OUTPUT_SCHEMA_JSON,
152
+ outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
150
153
  annotations: { title: "Synastry House Overlay", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
151
154
  handler: async (args) => {
152
155
  validateRequired(args, [
@@ -158,15 +161,14 @@ registerTool({
158
161
  const query = {};
159
162
  if (args.format)
160
163
  query.format = args.format;
161
- return await getActiveClient().request("POST", "/comparative/overlay", {
162
- params: query,
163
- data: {
164
- subjects: [
165
- buildSubject("Person A", args.person_a_datetime, args.person_a_latitude, args.person_a_longitude, args.person_a_timezone),
166
- buildSubject("Person B", args.person_b_datetime, args.person_b_latitude, args.person_b_longitude, args.person_b_timezone),
167
- ],
168
- }
169
- });
164
+ const body = {
165
+ subjects: [
166
+ buildSubject("Person A", args.person_a_datetime, args.person_a_latitude, args.person_a_longitude, args.person_a_timezone),
167
+ buildSubject("Person B", args.person_b_datetime, args.person_b_latitude, args.person_b_longitude, args.person_b_timezone),
168
+ ],
169
+ };
170
+ applyVisual(body, args);
171
+ return await getActiveClient().request("POST", "/comparative/overlay", { params: query, data: body });
170
172
  },
171
173
  });
172
174
  // POST /comparative/natal-transits
@@ -1,6 +1,7 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
- import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
3
+ import { OUTPUT_SCHEMA_IMAGE_AND_JSON } from "../output-schemas.js";
4
+ import { applyVisual, visualProperties } from "../visual.js";
4
5
  import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
5
6
  import { localToUtcIsoHistorical } from "../datetime-historical.js";
6
7
  /** Optional birth coordinates, present only to feed the historical timezone
@@ -53,24 +54,23 @@ registerTool({
53
54
  enum: ["json", "llm"],
54
55
  description: "Output format. 'llm' returns compact projection for token efficiency.",
55
56
  },
57
+ ...visualProperties("the bodygraph for that moment"),
56
58
  },
57
59
  required: ["planet", "datetime", "return_year"],
58
60
  additionalProperties: false,
59
61
  },
60
- outputSchema: OUTPUT_SCHEMA_JSON,
62
+ outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
61
63
  annotations: { title: "HD Planetary Return", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
62
64
  handler: async (args) => {
63
65
  validateRequired(args, ["planet", "datetime", "return_year"]);
64
- return await getActiveClient().request("POST", "/human-design/cycles/return", {
65
- data: {
66
- planet: args.planet,
67
- birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
68
- return_year: args.return_year,
69
- format: args.format,
70
- include_chiron: args.include_chiron,
71
- include_lilith: args.include_lilith,
72
- },
73
- });
66
+ const body = {
67
+ planet: args.planet,
68
+ birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
69
+ return_year: args.return_year,
70
+ format: args.format,
71
+ };
72
+ applyVisual(body, args);
73
+ return await getActiveClient().request("POST", "/human-design/cycles/return", { data: body });
74
74
  },
75
75
  });
76
76
  registerTool({
@@ -107,23 +107,22 @@ registerTool({
107
107
  enum: ["json", "llm"],
108
108
  description: "Output format. 'llm' returns compact projection for token efficiency.",
109
109
  },
110
+ ...visualProperties("the bodygraph for that moment"),
110
111
  },
111
112
  required: ["planet", "datetime", "target_year"],
112
113
  additionalProperties: false,
113
114
  },
114
- outputSchema: OUTPUT_SCHEMA_JSON,
115
+ outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
115
116
  annotations: { title: "HD Opposition Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
116
117
  handler: async (args) => {
117
118
  validateRequired(args, ["planet", "datetime", "target_year"]);
118
- return await getActiveClient().request("POST", "/human-design/cycles/opposition", {
119
- data: {
120
- planet: args.planet,
121
- birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
122
- target_year: args.target_year,
123
- format: args.format,
124
- include_chiron: args.include_chiron,
125
- include_lilith: args.include_lilith,
126
- },
127
- });
119
+ const body = {
120
+ planet: args.planet,
121
+ birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
122
+ target_year: args.target_year,
123
+ format: args.format,
124
+ };
125
+ applyVisual(body, args);
126
+ return await getActiveClient().request("POST", "/human-design/cycles/opposition", { data: body });
128
127
  },
129
128
  });
@@ -1,6 +1,7 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
- import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
3
+ import { OUTPUT_SCHEMA_IMAGE_AND_JSON } from "../output-schemas.js";
4
+ import { applyVisual, visualProperties } from "../visual.js";
4
5
  import { DATETIME_DESC, TIMEZONE_PROPERTY, WINDOW_DATE_DESC, assertZonedDatetime } from "../datetime.js";
5
6
  registerTool({
6
7
  name: "ephemeris_progressed_chart",
@@ -44,11 +45,12 @@ registerTool({
44
45
  type: "boolean",
45
46
  description: "Whether to include aspect grid in the response. Default false.",
46
47
  },
48
+ ...visualProperties("a natal-inside, progressed-outside bi-wheel"),
47
49
  },
48
50
  required: ["birth_datetime", "birth_latitude", "birth_longitude", "target_datetime"],
49
51
  additionalProperties: false,
50
52
  },
51
- outputSchema: OUTPUT_SCHEMA_JSON,
53
+ outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
52
54
  annotations: { title: "Progressed Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
53
55
  handler: async (args) => {
54
56
  validateRequired(args, ["birth_datetime", "birth_latitude", "birth_longitude", "target_datetime"]);
@@ -76,6 +78,7 @@ registerTool({
76
78
  if (args.include_aspects != null) {
77
79
  body.options = { include_aspects: args.include_aspects };
78
80
  }
81
+ applyVisual(body, args);
79
82
  return await getActiveClient().post("/ephemeris/progressed", body);
80
83
  },
81
84
  });
@@ -1,6 +1,7 @@
1
1
  import { registerTool, validateRequired } from "../index.js";
2
2
  import { getActiveClient } from "../../backend/client.js";
3
- import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
3
+ import { OUTPUT_SCHEMA_IMAGE_AND_JSON } from "../output-schemas.js";
4
+ import { applyVisual, visualProperties } from "../visual.js";
4
5
  import { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime } from "../datetime.js";
5
6
  registerTool({
6
7
  name: "ephemeris_relocation",
@@ -45,11 +46,12 @@ registerTool({
45
46
  enum: ["json", "llm"],
46
47
  description: "Output format. 'llm' = compact token-efficient output (available on all tiers).",
47
48
  },
49
+ ...visualProperties("the relocated chart wheel"),
48
50
  },
49
51
  required: ["natal_datetime", "natal_latitude", "natal_longitude", "relocation_latitude", "relocation_longitude"],
50
52
  additionalProperties: false,
51
53
  },
52
- outputSchema: OUTPUT_SCHEMA_JSON,
54
+ outputSchema: OUTPUT_SCHEMA_IMAGE_AND_JSON,
53
55
  annotations: { title: "Relocation Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
54
56
  handler: async (args) => {
55
57
  validateRequired(args, ["natal_datetime", "natal_latitude", "natal_longitude", "relocation_latitude", "relocation_longitude"]);
@@ -72,6 +74,7 @@ registerTool({
72
74
  };
73
75
  if (args.house_system)
74
76
  body.house_system = args.house_system;
77
+ applyVisual(body, args);
75
78
  const query = {};
76
79
  if (args.format)
77
80
  query.format = args.format;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * include_visual for data tools whose endpoint can embed a rendered picture
3
+ * (+2 credits). formatToolResponse turns the returned `visual` into an image
4
+ * block next to the JSON. Kept to one terse flag: tool text is re-sent to the
5
+ * model on every pass (see tool-surface-budget.test.ts).
6
+ */
7
+ /** Input-schema property; `what` names the picture, e.g. "the relocated wheel". */
8
+ export declare function visualProperties(what: string): {
9
+ readonly include_visual: {
10
+ readonly type: "boolean";
11
+ readonly description: `Also return ${string} as an image (+2 credits).`;
12
+ };
13
+ };
14
+ /** Copies the visual request onto the API body. Always SVG: the MCP host rasterizes nothing. */
15
+ export declare function applyVisual(body: Record<string, unknown>, args: any): void;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * include_visual for data tools whose endpoint can embed a rendered picture
3
+ * (+2 credits). formatToolResponse turns the returned `visual` into an image
4
+ * block next to the JSON. Kept to one terse flag: tool text is re-sent to the
5
+ * model on every pass (see tool-surface-budget.test.ts).
6
+ */
7
+ /** Input-schema property; `what` names the picture, e.g. "the relocated wheel". */
8
+ export function visualProperties(what) {
9
+ return {
10
+ include_visual: {
11
+ type: "boolean",
12
+ description: `Also return ${what} as an image (+2 credits).`,
13
+ },
14
+ };
15
+ }
16
+ /** Copies the visual request onto the API body. Always SVG: the MCP host rasterizes nothing. */
17
+ export function applyVisual(body, args) {
18
+ if (!args.include_visual)
19
+ return;
20
+ body.include_visual = true;
21
+ body.visual_config = { format: "svg", theme: "light", size: 800 };
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openephemeris/mcp-server",
3
- "version": "4.19.0",
3
+ "version": "4.20.0",
4
4
  "description": "MCP server for Open Ephemeris — NASA JPL DE440 astrology and astronomy, with interactive charts that render inline in Claude and ChatGPT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",