ether-to-astro 1.0.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/.env.example +13 -0
- package/.github/pull_request_template.md +16 -0
- package/.github/workflows/release.yml +35 -0
- package/.github/workflows/test.yml +32 -0
- package/AGENTS.md +99 -0
- package/LICENSE +18 -0
- package/NOTICE.md +45 -0
- package/README.md +301 -0
- package/SETUP.md +70 -0
- package/TESTING_SUMMARY.md +238 -0
- package/TEST_SUITE_STATUS.md +218 -0
- package/biome.json +48 -0
- package/dist/astro-service.d.ts +98 -0
- package/dist/astro-service.js +496 -0
- package/dist/chart-types.d.ts +52 -0
- package/dist/chart-types.js +51 -0
- package/dist/charts.d.ts +125 -0
- package/dist/charts.js +324 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +472 -0
- package/dist/constants.d.ts +81 -0
- package/dist/constants.js +76 -0
- package/dist/eclipses.d.ts +85 -0
- package/dist/eclipses.js +184 -0
- package/dist/ephemeris.d.ts +120 -0
- package/dist/ephemeris.js +379 -0
- package/dist/formatter.d.ts +2 -0
- package/dist/formatter.js +22 -0
- package/dist/houses.d.ts +82 -0
- package/dist/houses.js +169 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +150 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +31 -0
- package/dist/logger.d.ts +25 -0
- package/dist/logger.js +73 -0
- package/dist/profile-store.d.ts +48 -0
- package/dist/profile-store.js +156 -0
- package/dist/riseset.d.ts +82 -0
- package/dist/riseset.js +185 -0
- package/dist/storage.d.ts +10 -0
- package/dist/storage.js +40 -0
- package/dist/time-utils.d.ts +68 -0
- package/dist/time-utils.js +136 -0
- package/dist/tool-registry.d.ts +35 -0
- package/dist/tool-registry.js +307 -0
- package/dist/tool-result.d.ts +175 -0
- package/dist/tool-result.js +188 -0
- package/dist/transits.d.ts +108 -0
- package/dist/transits.js +263 -0
- package/dist/types.d.ts +450 -0
- package/dist/types.js +161 -0
- package/example-usage.md +131 -0
- package/natal-chart.json +187 -0
- package/package.json +61 -0
- package/scripts/download-ephemeris.js +115 -0
- package/setup.sh +21 -0
- package/src/astro-service.ts +710 -0
- package/src/chart-types.ts +125 -0
- package/src/charts.ts +399 -0
- package/src/cli.ts +694 -0
- package/src/constants.ts +89 -0
- package/src/eclipses.ts +226 -0
- package/src/ephemeris.ts +437 -0
- package/src/formatter.ts +25 -0
- package/src/houses.ts +202 -0
- package/src/index.ts +170 -0
- package/src/loader.ts +36 -0
- package/src/logger.ts +104 -0
- package/src/profile-store.ts +285 -0
- package/src/riseset.ts +229 -0
- package/src/time-utils.ts +167 -0
- package/src/tool-registry.ts +357 -0
- package/src/tool-result.ts +283 -0
- package/src/transits.ts +352 -0
- package/src/types.ts +547 -0
- package/tests/README.md +173 -0
- package/tests/TESTING_STRATEGY.md +178 -0
- package/tests/fixtures/bowen-yang-chart.ts +69 -0
- package/tests/fixtures/calculate-expected.ts +81 -0
- package/tests/fixtures/expected-results.ts +117 -0
- package/tests/fixtures/generate-expected-simple.ts +94 -0
- package/tests/helpers/date-fixtures.ts +15 -0
- package/tests/helpers/ephem.ts +11 -0
- package/tests/helpers/temp.ts +9 -0
- package/tests/setup.ts +11 -0
- package/tests/unit/astro-service.test.ts +323 -0
- package/tests/unit/chart-types.test.ts +18 -0
- package/tests/unit/charts-errors.test.ts +42 -0
- package/tests/unit/charts.test.ts +157 -0
- package/tests/unit/cli-commands.test.ts +82 -0
- package/tests/unit/cli-profiles.test.ts +128 -0
- package/tests/unit/cli.test.ts +191 -0
- package/tests/unit/constants.test.ts +26 -0
- package/tests/unit/correctness-critical.test.ts +408 -0
- package/tests/unit/eclipses.test.ts +108 -0
- package/tests/unit/ephemeris.test.ts +213 -0
- package/tests/unit/error-handling.test.ts +116 -0
- package/tests/unit/formatter.test.ts +29 -0
- package/tests/unit/houses-errors.test.ts +27 -0
- package/tests/unit/houses-validation.test.ts +164 -0
- package/tests/unit/houses.test.ts +205 -0
- package/tests/unit/profile-store.test.ts +163 -0
- package/tests/unit/real-user-charts.test.ts +148 -0
- package/tests/unit/riseset.test.ts +106 -0
- package/tests/unit/solver-edges.test.ts +197 -0
- package/tests/unit/time-utils-temporal.test.ts +303 -0
- package/tests/unit/time-utils.test.ts +173 -0
- package/tests/unit/tool-registry.test.ts +222 -0
- package/tests/unit/tool-result.test.ts +45 -0
- package/tests/unit/transit-correctness.test.ts +78 -0
- package/tests/unit/transits.test.ts +238 -0
- package/tests/validation/README.md +32 -0
- package/tests/validation/adapters/astrolog.ts +306 -0
- package/tests/validation/adapters/internal.ts +184 -0
- package/tests/validation/compare/eclipses.ts +47 -0
- package/tests/validation/compare/houses.ts +76 -0
- package/tests/validation/compare/positions.ts +104 -0
- package/tests/validation/compare/riseSet.ts +48 -0
- package/tests/validation/compare/roots.ts +90 -0
- package/tests/validation/compare/transits.ts +69 -0
- package/tests/validation/fixtures/astrolog-parity/core.ts +194 -0
- package/tests/validation/fixtures/eclipses/core.ts +14 -0
- package/tests/validation/fixtures/houses/core.ts +47 -0
- package/tests/validation/fixtures/positions/core.ts +159 -0
- package/tests/validation/fixtures/rise-set/core.ts +20 -0
- package/tests/validation/fixtures/roots/core.ts +47 -0
- package/tests/validation/fixtures/transits/core.ts +61 -0
- package/tests/validation/fixtures/transits/dst.ts +21 -0
- package/tests/validation/oracle.spec.ts +129 -0
- package/tests/validation/utils/denseRootOracle.ts +269 -0
- package/tests/validation/utils/fixtureTypes.ts +146 -0
- package/tests/validation/utils/report.ts +60 -0
- package/tests/validation/utils/tolerances.ts +23 -0
- package/tests/validation/validation.spec.ts +836 -0
- package/tools/color-picker.html +388 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +31 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import type { AstroService } from './astro-service.js';
|
|
2
|
+
import type { Disambiguation } from './time-utils.js';
|
|
3
|
+
import type { HouseSystem, NatalChart } from './types.js';
|
|
4
|
+
|
|
5
|
+
type ToolContent =
|
|
6
|
+
| { type: 'text'; text: string }
|
|
7
|
+
| { type: 'image'; data: string; mimeType: string };
|
|
8
|
+
|
|
9
|
+
export type ToolExecutionResult =
|
|
10
|
+
| { kind: 'state'; data: Record<string, unknown>; text: string; natalChart?: NatalChart }
|
|
11
|
+
| { kind: 'content'; content: ToolContent[] };
|
|
12
|
+
|
|
13
|
+
export interface ToolExecutionContext {
|
|
14
|
+
service: AstroService;
|
|
15
|
+
natalChart: NatalChart | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ToolArgs = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
export interface ToolSpec {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
inputSchema: Record<string, unknown>;
|
|
24
|
+
requiresNatalChart: boolean;
|
|
25
|
+
execute: (
|
|
26
|
+
ctx: ToolExecutionContext,
|
|
27
|
+
args: ToolArgs
|
|
28
|
+
) => Promise<ToolExecutionResult> | ToolExecutionResult;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const MCP_TOOL_SPECS: ToolSpec[] = [
|
|
32
|
+
{
|
|
33
|
+
name: 'set_natal_chart',
|
|
34
|
+
description:
|
|
35
|
+
'Store natal chart data for transit calculations. Birth time should be LOCAL time at the birth location (not UTC). The server converts to UTC using the timezone parameter. Optional birth_time_disambiguation handles DST overlap/gap edge cases (default: reject).',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
name: { type: 'string', description: 'Name for this chart' },
|
|
40
|
+
year: { type: 'number', description: 'Birth year' },
|
|
41
|
+
month: { type: 'number', description: 'Birth month (1-12)' },
|
|
42
|
+
day: { type: 'number', description: 'Birth day' },
|
|
43
|
+
hour: { type: 'number', description: 'Birth hour (0-23, LOCAL TIME at birth location)' },
|
|
44
|
+
minute: { type: 'number', description: 'Birth minute' },
|
|
45
|
+
latitude: { type: 'number', description: 'Birth location latitude' },
|
|
46
|
+
longitude: { type: 'number', description: 'Birth location longitude' },
|
|
47
|
+
timezone: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Timezone (e.g., America/New_York, Europe/London)',
|
|
50
|
+
},
|
|
51
|
+
birth_time_disambiguation: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
enum: ['compatible', 'earlier', 'later', 'reject'],
|
|
54
|
+
description:
|
|
55
|
+
'How to handle DST-ambiguous or nonexistent local birth times. Default: reject.',
|
|
56
|
+
},
|
|
57
|
+
house_system: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description:
|
|
60
|
+
'House system preference: P=Placidus (default), W=Whole Sign, K=Koch, E=Equal',
|
|
61
|
+
enum: ['P', 'W', 'K', 'E'],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: [
|
|
65
|
+
'name',
|
|
66
|
+
'year',
|
|
67
|
+
'month',
|
|
68
|
+
'day',
|
|
69
|
+
'hour',
|
|
70
|
+
'minute',
|
|
71
|
+
'latitude',
|
|
72
|
+
'longitude',
|
|
73
|
+
'timezone',
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
requiresNatalChart: false,
|
|
77
|
+
execute: (ctx, args) => {
|
|
78
|
+
const result = ctx.service.setNatalChart({
|
|
79
|
+
name: args.name as string,
|
|
80
|
+
year: args.year as number,
|
|
81
|
+
month: args.month as number,
|
|
82
|
+
day: args.day as number,
|
|
83
|
+
hour: args.hour as number,
|
|
84
|
+
minute: args.minute as number,
|
|
85
|
+
latitude: args.latitude as number,
|
|
86
|
+
longitude: args.longitude as number,
|
|
87
|
+
timezone: args.timezone as string,
|
|
88
|
+
house_system: args.house_system as HouseSystem | undefined,
|
|
89
|
+
birth_time_disambiguation: args.birth_time_disambiguation as Disambiguation | undefined,
|
|
90
|
+
});
|
|
91
|
+
return { kind: 'state', data: result.data, text: result.text, natalChart: result.chart };
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'get_transits',
|
|
96
|
+
description:
|
|
97
|
+
'Get transits (aspects between current/future planets and natal chart). Returns aspects within orb, with exact timing when close. Date defaults to today at local noon in the natal chart timezone.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
date: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'Date for transits (ISO format YYYY-MM-DD). Defaults to today.',
|
|
104
|
+
},
|
|
105
|
+
categories: {
|
|
106
|
+
type: 'array',
|
|
107
|
+
items: { type: 'string', enum: ['moon', 'personal', 'outer', 'all'] },
|
|
108
|
+
description:
|
|
109
|
+
'Planet categories to include: moon, personal (Sun/Mercury/Venus/Mars), outer (Jupiter/Saturn/Uranus/Neptune/Pluto), or all. Defaults to ["all"].',
|
|
110
|
+
},
|
|
111
|
+
include_mundane: {
|
|
112
|
+
type: 'boolean',
|
|
113
|
+
description:
|
|
114
|
+
'Include current planetary positions (not transits to natal chart). Defaults to false.',
|
|
115
|
+
},
|
|
116
|
+
days_ahead: {
|
|
117
|
+
type: 'number',
|
|
118
|
+
description:
|
|
119
|
+
'Number of days to look ahead for upcoming transits. 0 = today only. Defaults to 0.',
|
|
120
|
+
default: 0,
|
|
121
|
+
},
|
|
122
|
+
max_orb: {
|
|
123
|
+
type: 'number',
|
|
124
|
+
description: 'Maximum orb in degrees to include. Defaults to 8.',
|
|
125
|
+
default: 8,
|
|
126
|
+
},
|
|
127
|
+
exact_only: {
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
description:
|
|
130
|
+
'Only return transits with exact times calculated (within 2° orb). Defaults to false.',
|
|
131
|
+
},
|
|
132
|
+
applying_only: {
|
|
133
|
+
type: 'boolean',
|
|
134
|
+
description: 'Only return applying (tightening) transits. Defaults to false.',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
requiresNatalChart: true,
|
|
139
|
+
execute: (ctx, args) => {
|
|
140
|
+
const result = ctx.service.getTransits(ctx.natalChart as NatalChart, {
|
|
141
|
+
date: args.date as string | undefined,
|
|
142
|
+
categories: args.categories as string[] | undefined,
|
|
143
|
+
include_mundane: args.include_mundane as boolean | undefined,
|
|
144
|
+
days_ahead: args.days_ahead as number | undefined,
|
|
145
|
+
max_orb: args.max_orb as number | undefined,
|
|
146
|
+
exact_only: args.exact_only as boolean | undefined,
|
|
147
|
+
applying_only: args.applying_only as boolean | undefined,
|
|
148
|
+
});
|
|
149
|
+
return { kind: 'state', data: result.data, text: result.text };
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'get_houses',
|
|
154
|
+
description:
|
|
155
|
+
'Calculate house cusps, Ascendant, and Midheaven for the natal chart using the specified house system',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
system: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
enum: ['P', 'K', 'W', 'E'],
|
|
162
|
+
description: 'House system: P=Placidus (default), K=Koch, W=Whole Sign, E=Equal',
|
|
163
|
+
default: 'P',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
requiresNatalChart: true,
|
|
168
|
+
execute: (ctx, args) => {
|
|
169
|
+
const result = ctx.service.getHouses(ctx.natalChart as NatalChart, {
|
|
170
|
+
system: args.system as string | undefined,
|
|
171
|
+
});
|
|
172
|
+
return { kind: 'state', data: result.data, text: result.text };
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'get_retrograde_planets',
|
|
177
|
+
description: 'Show which planets are currently retrograde',
|
|
178
|
+
inputSchema: { type: 'object', properties: {} },
|
|
179
|
+
requiresNatalChart: false,
|
|
180
|
+
execute: (ctx, args) => {
|
|
181
|
+
const timezone =
|
|
182
|
+
(args.timezone as string | undefined) ?? ctx.natalChart?.location.timezone ?? 'UTC';
|
|
183
|
+
const result = ctx.service.getRetrogradePlanets(timezone);
|
|
184
|
+
return { kind: 'state', data: result.data, text: result.text };
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'get_rise_set_times',
|
|
189
|
+
description: 'Get sunrise, sunset, moonrise, moonset times for today',
|
|
190
|
+
inputSchema: { type: 'object', properties: {} },
|
|
191
|
+
requiresNatalChart: true,
|
|
192
|
+
execute: async (ctx) => {
|
|
193
|
+
const result = await ctx.service.getRiseSetTimes(ctx.natalChart as NatalChart);
|
|
194
|
+
return { kind: 'state', data: result.data, text: result.text };
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'get_asteroid_positions',
|
|
199
|
+
description: 'Get positions of major asteroids (Chiron, Ceres, Pallas, Juno, Vesta) and Nodes',
|
|
200
|
+
inputSchema: { type: 'object', properties: {} },
|
|
201
|
+
requiresNatalChart: false,
|
|
202
|
+
execute: (ctx, args) => {
|
|
203
|
+
const timezone =
|
|
204
|
+
(args.timezone as string | undefined) ?? ctx.natalChart?.location.timezone ?? 'UTC';
|
|
205
|
+
const result = ctx.service.getAsteroidPositions(timezone);
|
|
206
|
+
return { kind: 'state', data: result.data, text: result.text };
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'get_next_eclipses',
|
|
211
|
+
description: 'Find the next solar and lunar eclipses',
|
|
212
|
+
inputSchema: { type: 'object', properties: {} },
|
|
213
|
+
requiresNatalChart: false,
|
|
214
|
+
execute: (ctx, args) => {
|
|
215
|
+
const timezone =
|
|
216
|
+
(args.timezone as string | undefined) ?? ctx.natalChart?.location.timezone ?? 'UTC';
|
|
217
|
+
const result = ctx.service.getNextEclipses(timezone);
|
|
218
|
+
return { kind: 'state', data: result.data, text: result.text };
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'get_server_status',
|
|
223
|
+
description:
|
|
224
|
+
'Inspect the current server state: whether a natal chart is loaded, its name and timezone, and the server version. Call this before making assumptions about loaded context.',
|
|
225
|
+
inputSchema: { type: 'object', properties: {} },
|
|
226
|
+
requiresNatalChart: false,
|
|
227
|
+
execute: (ctx) => {
|
|
228
|
+
const result = ctx.service.getServerStatus(ctx.natalChart);
|
|
229
|
+
return { kind: 'state', data: result.data, text: result.text };
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'generate_natal_chart',
|
|
234
|
+
description:
|
|
235
|
+
'Generate a visual natal chart wheel with planets, houses, and aspects. Supports SVG, PNG, and WebP formats. Theme defaults to dark (6pm-6am) or light (6am-6pm) based on current time, but can be overridden with the theme parameter.',
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
theme: {
|
|
240
|
+
type: 'string',
|
|
241
|
+
enum: ['light', 'dark'],
|
|
242
|
+
description:
|
|
243
|
+
'Color theme override. Defaults to time-based: dark (6pm-6am) or light (6am-6pm)',
|
|
244
|
+
},
|
|
245
|
+
format: {
|
|
246
|
+
type: 'string',
|
|
247
|
+
enum: ['svg', 'png', 'webp'],
|
|
248
|
+
description: 'Output format (svg, png, or webp), defaults to svg',
|
|
249
|
+
},
|
|
250
|
+
output_path: {
|
|
251
|
+
type: 'string',
|
|
252
|
+
description: 'Optional absolute file path to save the chart (e.g., /path/to/chart.webp)',
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
requiresNatalChart: true,
|
|
257
|
+
execute: async (ctx, args) => {
|
|
258
|
+
const result = await ctx.service.generateNatalChart(ctx.natalChart as NatalChart, {
|
|
259
|
+
theme: args.theme as 'light' | 'dark' | undefined,
|
|
260
|
+
format: args.format as 'svg' | 'png' | 'webp' | undefined,
|
|
261
|
+
output_path: args.output_path as string | undefined,
|
|
262
|
+
});
|
|
263
|
+
if (result.outputPath) {
|
|
264
|
+
return { kind: 'content', content: [{ type: 'text', text: result.text }] };
|
|
265
|
+
}
|
|
266
|
+
if (result.format === 'svg' && result.svg) {
|
|
267
|
+
return {
|
|
268
|
+
kind: 'content',
|
|
269
|
+
content: [
|
|
270
|
+
{ type: 'text', text: result.text },
|
|
271
|
+
{ type: 'text', text: result.svg },
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (result.image) {
|
|
276
|
+
return {
|
|
277
|
+
kind: 'content',
|
|
278
|
+
content: [
|
|
279
|
+
{ type: 'text', text: result.text },
|
|
280
|
+
{ type: 'image', data: result.image.data, mimeType: result.image.mimeType },
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
throw new Error('Chart generation returned no payload.');
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: 'generate_transit_chart',
|
|
289
|
+
description:
|
|
290
|
+
'Generate a visual transit chart showing current transits overlaid on the natal chart. Date defaults to today at local noon in the natal chart timezone. Supports SVG, PNG, and WebP formats with light or dark themes.',
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: {
|
|
294
|
+
date: {
|
|
295
|
+
type: 'string',
|
|
296
|
+
description: 'Optional date for transits (ISO format), defaults to today at local noon',
|
|
297
|
+
},
|
|
298
|
+
theme: {
|
|
299
|
+
type: 'string',
|
|
300
|
+
enum: ['light', 'dark'],
|
|
301
|
+
description:
|
|
302
|
+
'Color theme override. Defaults to time-based: dark (6pm-6am) or light (6am-6pm)',
|
|
303
|
+
},
|
|
304
|
+
format: {
|
|
305
|
+
type: 'string',
|
|
306
|
+
enum: ['svg', 'png', 'webp'],
|
|
307
|
+
description: 'Output format (svg, png, or webp), defaults to svg',
|
|
308
|
+
},
|
|
309
|
+
output_path: {
|
|
310
|
+
type: 'string',
|
|
311
|
+
description: 'Optional absolute file path to save the chart (e.g., /path/to/chart.webp)',
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
requiresNatalChart: true,
|
|
316
|
+
execute: async (ctx, args) => {
|
|
317
|
+
const result = await ctx.service.generateTransitChart(ctx.natalChart as NatalChart, {
|
|
318
|
+
date: args.date as string | undefined,
|
|
319
|
+
theme: args.theme as 'light' | 'dark' | undefined,
|
|
320
|
+
format: args.format as 'svg' | 'png' | 'webp' | undefined,
|
|
321
|
+
output_path: args.output_path as string | undefined,
|
|
322
|
+
});
|
|
323
|
+
if (result.outputPath) {
|
|
324
|
+
return { kind: 'content', content: [{ type: 'text', text: result.text }] };
|
|
325
|
+
}
|
|
326
|
+
if (result.format === 'svg' && result.svg) {
|
|
327
|
+
return {
|
|
328
|
+
kind: 'content',
|
|
329
|
+
content: [
|
|
330
|
+
{ type: 'text', text: result.text },
|
|
331
|
+
{ type: 'text', text: result.svg },
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (result.image) {
|
|
336
|
+
return {
|
|
337
|
+
kind: 'content',
|
|
338
|
+
content: [
|
|
339
|
+
{ type: 'text', text: result.text },
|
|
340
|
+
{ type: 'image', data: result.image.data, mimeType: result.image.mimeType },
|
|
341
|
+
],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
throw new Error('Transit chart generation returned no payload.');
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
export function createToolSpecIndex(specs: ToolSpec[] = MCP_TOOL_SPECS): Map<string, ToolSpec> {
|
|
350
|
+
return new Map(specs.map((spec) => [spec.name, spec]));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const TOOL_INDEX = createToolSpecIndex();
|
|
354
|
+
|
|
355
|
+
export function getToolSpec(name: string): ToolSpec | undefined {
|
|
356
|
+
return TOOL_INDEX.get(name);
|
|
357
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured error handling for MCP tool results
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Provides agent-recoverable error codes and suggestions for self-correction.
|
|
6
|
+
* Distinguishes between recoverable domain errors and hard infrastructure failures.
|
|
7
|
+
*
|
|
8
|
+
* Each error code includes whether it's retryable and suggested fixes
|
|
9
|
+
* to help the agent recover from errors automatically.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Error codes for MCP tool operations
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* Each code represents a specific category of error that can occur
|
|
17
|
+
* during astrological calculations. The codes are designed to be
|
|
18
|
+
* machine-readable while still being descriptive.
|
|
19
|
+
*/
|
|
20
|
+
export type ToolIssueCode =
|
|
21
|
+
| 'INVALID_INPUT'
|
|
22
|
+
| 'UNKNOWN_PLANET'
|
|
23
|
+
| 'NO_RISE_SET_EVENT'
|
|
24
|
+
| 'CIRCUMPOLAR_OBJECT'
|
|
25
|
+
| 'POLAR_LATITUDE_LIMIT'
|
|
26
|
+
| 'EPHEMERIS_NOT_INITIALIZED'
|
|
27
|
+
| 'EPHEMERIS_COMPUTE_FAILED'
|
|
28
|
+
| 'TIMEZONE_ERROR'
|
|
29
|
+
| 'INVALID_TIMEZONE'
|
|
30
|
+
| 'MISSING_NATAL_CHART'
|
|
31
|
+
| 'INVALID_DATE'
|
|
32
|
+
| 'INVALID_HOUSE_SYSTEM'
|
|
33
|
+
| 'FILE_WRITE_FAILED'
|
|
34
|
+
| 'CHART_RENDER_FAILED'
|
|
35
|
+
| 'INTERNAL_ERROR';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Structured error information for tool operations
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* Contains the error code, human-readable message, and metadata
|
|
42
|
+
* to help agents understand and recover from errors.
|
|
43
|
+
*/
|
|
44
|
+
export interface ToolIssue {
|
|
45
|
+
/** Machine-readable error code */
|
|
46
|
+
code: ToolIssueCode;
|
|
47
|
+
/** Human-readable error description */
|
|
48
|
+
message: string;
|
|
49
|
+
/** Whether the operation can be retried */
|
|
50
|
+
retryable: boolean;
|
|
51
|
+
/** Suggested fix for the agent (optional) */
|
|
52
|
+
suggestedFix?: string;
|
|
53
|
+
/** Additional error context (optional) */
|
|
54
|
+
details?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Result type for MCP tool operations
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* Discriminated union that distinguishes between successful
|
|
62
|
+
* and failed operations. Success includes data and optional warnings,
|
|
63
|
+
* while failure includes structured error information.
|
|
64
|
+
*/
|
|
65
|
+
export type ToolResult<T> =
|
|
66
|
+
| {
|
|
67
|
+
/** Success flag */
|
|
68
|
+
ok: true;
|
|
69
|
+
/** Result data */
|
|
70
|
+
data: T;
|
|
71
|
+
/** Optional warnings about the operation */
|
|
72
|
+
warnings?: ToolIssue[];
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
/** Failure flag */
|
|
76
|
+
ok: false;
|
|
77
|
+
/** Error information */
|
|
78
|
+
error: ToolIssue;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create a successful tool result
|
|
83
|
+
*
|
|
84
|
+
* @param data - The successful result data
|
|
85
|
+
* @param warnings - Optional warnings about the operation
|
|
86
|
+
* @returns Success result with data
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* Use this to wrap successful operation results. Warnings are
|
|
90
|
+
* optional and can indicate non-fatal issues.
|
|
91
|
+
*/
|
|
92
|
+
export function success<T>(data: T, warnings?: ToolIssue[]): ToolResult<T> {
|
|
93
|
+
return warnings ? { ok: true, data, warnings } : { ok: true, data };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a failed tool result
|
|
98
|
+
*
|
|
99
|
+
* @param error - Structured error information
|
|
100
|
+
* @returns Failure result with error
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* Use this to wrap failed operations. The error should include
|
|
104
|
+
* a code, message, and optionally retry/suggestion information.
|
|
105
|
+
*/
|
|
106
|
+
export function failure(error: ToolIssue): ToolResult<never> {
|
|
107
|
+
return { ok: false, error };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Schema version for structured responses.
|
|
112
|
+
* Increment on breaking changes to response shapes.
|
|
113
|
+
*/
|
|
114
|
+
export const SCHEMA_VERSION = '1.0';
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build a successful MCP tool response with structured data + human text.
|
|
118
|
+
*
|
|
119
|
+
* @param data - Structured payload
|
|
120
|
+
* @param humanText - Human-readable summary
|
|
121
|
+
* @param warnings - Optional warnings
|
|
122
|
+
* @returns MCP content array (JSON first, text second)
|
|
123
|
+
*/
|
|
124
|
+
export function mcpResult<T>(data: T, humanText: string, warnings?: ToolIssue[]) {
|
|
125
|
+
const envelope = warnings
|
|
126
|
+
? { ok: true as const, schemaVersion: SCHEMA_VERSION, data, warnings }
|
|
127
|
+
: { ok: true as const, schemaVersion: SCHEMA_VERSION, data };
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{ type: 'text' as const, text: JSON.stringify(envelope, null, 2) },
|
|
131
|
+
{ type: 'text' as const, text: humanText },
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Build an error MCP tool response with structured error.
|
|
138
|
+
*
|
|
139
|
+
* @param error - Structured error information
|
|
140
|
+
* @returns MCP content array with isError flag
|
|
141
|
+
*/
|
|
142
|
+
export function mcpError(error: ToolIssue) {
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: 'text' as const,
|
|
147
|
+
text: JSON.stringify({ ok: false, schemaVersion: SCHEMA_VERSION, error }, null, 2),
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Map Swiss Ephemeris errors to structured tool issues
|
|
156
|
+
*
|
|
157
|
+
* @param context - Operation context where error occurred
|
|
158
|
+
* @param err - Raw error from Swiss Ephemeris
|
|
159
|
+
* @param details - Additional error context
|
|
160
|
+
* @returns Structured tool issue with retry information
|
|
161
|
+
*
|
|
162
|
+
* @remarks
|
|
163
|
+
* Converts low-level Swiss Ephemeris errors into structured,
|
|
164
|
+
* agent-recoverable error codes with suggested fixes.
|
|
165
|
+
*/
|
|
166
|
+
export function mapSweError(
|
|
167
|
+
context: string,
|
|
168
|
+
err: unknown,
|
|
169
|
+
details?: Record<string, unknown>
|
|
170
|
+
): ToolIssue {
|
|
171
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
172
|
+
|
|
173
|
+
if (/not initialized/i.test(message)) {
|
|
174
|
+
return {
|
|
175
|
+
code: 'EPHEMERIS_NOT_INITIALIZED',
|
|
176
|
+
message: 'Swiss Ephemeris is not initialized.',
|
|
177
|
+
retryable: false,
|
|
178
|
+
suggestedFix: 'Initialize the ephemeris engine before requesting calculations.',
|
|
179
|
+
details,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
code: 'EPHEMERIS_COMPUTE_FAILED',
|
|
185
|
+
message: `Swiss Ephemeris failed during ${context}.`,
|
|
186
|
+
retryable: false,
|
|
187
|
+
suggestedFix: 'Check inputs and ephemeris configuration.',
|
|
188
|
+
details: { ...details, rawMessage: message },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create a structured error for missing rise/set events
|
|
194
|
+
*
|
|
195
|
+
* @param eventType - Type of event that was missing
|
|
196
|
+
* @param planet - Planet name for context
|
|
197
|
+
* @param details - Additional error context
|
|
198
|
+
* @returns Structured tool issue indicating no event
|
|
199
|
+
*
|
|
200
|
+
* @remarks
|
|
201
|
+
* Used when a planet doesn't rise/set at a location (circumpolar)
|
|
202
|
+
* or when meridian transits don't occur.
|
|
203
|
+
*/
|
|
204
|
+
export function noRiseSetEvent(
|
|
205
|
+
eventType: 'rise' | 'set' | 'upper_meridian' | 'lower_meridian',
|
|
206
|
+
planet: string,
|
|
207
|
+
details: Record<string, unknown>
|
|
208
|
+
): ToolIssue {
|
|
209
|
+
return {
|
|
210
|
+
code: 'NO_RISE_SET_EVENT',
|
|
211
|
+
message: `No ${eventType} event for ${planet} at the specified date and location.`,
|
|
212
|
+
retryable: true,
|
|
213
|
+
suggestedFix:
|
|
214
|
+
'Try another date, location, or request a different event type. Object may be circumpolar.',
|
|
215
|
+
details,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create a structured error for circumpolar objects
|
|
221
|
+
*
|
|
222
|
+
* @param planet - Planet name for context
|
|
223
|
+
* @param latitude - Observer latitude where object is circumpolar
|
|
224
|
+
* @param details - Additional error context
|
|
225
|
+
* @returns Structured tool issue for circumpolar object
|
|
226
|
+
*
|
|
227
|
+
* @remarks
|
|
228
|
+
* Used when a planet never rises or sets at extreme latitudes.
|
|
229
|
+
* The object is either always above or always below the horizon.
|
|
230
|
+
*/
|
|
231
|
+
export function circumpolarObject(
|
|
232
|
+
planet: string,
|
|
233
|
+
latitude: number,
|
|
234
|
+
details?: Record<string, unknown>
|
|
235
|
+
): ToolIssue {
|
|
236
|
+
return {
|
|
237
|
+
code: 'CIRCUMPOLAR_OBJECT',
|
|
238
|
+
message: `${planet} is circumpolar at latitude ${latitude.toFixed(1)}° - it does not rise or set.`,
|
|
239
|
+
retryable: true,
|
|
240
|
+
suggestedFix: 'Request meridian transit times instead, or try a different location.',
|
|
241
|
+
details: { planet, latitude, ...details },
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create a structured error for missing natal chart
|
|
247
|
+
*
|
|
248
|
+
* @returns Structured tool issue for missing natal chart
|
|
249
|
+
*
|
|
250
|
+
* @remarks
|
|
251
|
+
* Used when transit or chart operations are requested
|
|
252
|
+
* before a natal chart has been set.
|
|
253
|
+
*/
|
|
254
|
+
export function missingNatalChart(): ToolIssue {
|
|
255
|
+
return {
|
|
256
|
+
code: 'MISSING_NATAL_CHART',
|
|
257
|
+
message: 'No natal chart found. Please set natal chart first.',
|
|
258
|
+
retryable: true,
|
|
259
|
+
suggestedFix:
|
|
260
|
+
'Call set_natal_chart with birth details (date, time, location, timezone) before requesting transits or chart calculations.',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Create a warning for polar latitude limitations
|
|
266
|
+
*
|
|
267
|
+
* @param latitude - Observer latitude in degrees
|
|
268
|
+
* @param houseSystem - House system being used
|
|
269
|
+
* @returns Structured tool issue warning about polar limitations
|
|
270
|
+
*
|
|
271
|
+
* @remarks
|
|
272
|
+
* Some house systems (Placidus, Koch) fail at extreme latitudes.
|
|
273
|
+
* This warns the user and suggests Whole Sign as a fallback.
|
|
274
|
+
*/
|
|
275
|
+
export function polarLatitudeWarning(latitude: number, houseSystem: string): ToolIssue {
|
|
276
|
+
return {
|
|
277
|
+
code: 'POLAR_LATITUDE_LIMIT',
|
|
278
|
+
message: `${houseSystem} house system may be inaccurate at polar latitudes (${latitude.toFixed(1)}°).`,
|
|
279
|
+
retryable: true,
|
|
280
|
+
suggestedFix: 'Consider using Whole Sign house system for latitudes >66°.',
|
|
281
|
+
details: { latitude, houseSystem },
|
|
282
|
+
};
|
|
283
|
+
}
|