ether-to-astro 1.0.2 → 1.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.
- package/.github/ISSUE_TEMPLATE/bug-report.yml +87 -0
- package/.github/ISSUE_TEMPLATE/capability-request.yml +117 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/paper-cut.yml +59 -0
- package/.github/pull_request_template.md +1 -0
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/test.yml +2 -2
- package/AGENTS.md +46 -1
- package/DEVELOPER.md +78 -0
- package/README.md +128 -75
- package/SETUP.md +100 -41
- package/dist/astro-service.d.ts +51 -2
- package/dist/astro-service.js +660 -56
- package/dist/cli.js +31 -0
- package/dist/entrypoint.d.ts +13 -0
- package/dist/entrypoint.js +78 -0
- package/dist/ephemeris.d.ts +15 -0
- package/dist/ephemeris.js +33 -0
- package/dist/formatter.d.ts +5 -1
- package/dist/formatter.js +4 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +63 -114
- package/dist/loader.d.ts +1 -1
- package/dist/loader.js +61 -23
- package/dist/mcp-alias.d.ts +2 -0
- package/dist/mcp-alias.js +8 -0
- package/dist/time-utils.d.ts +8 -0
- package/dist/time-utils.js +16 -0
- package/dist/tool-registry.js +111 -5
- package/dist/tool-result.d.ts +8 -0
- package/dist/tool-result.js +39 -0
- package/dist/types.d.ts +79 -1
- package/docs/product/adrs/0001-mcp-vs-skill-boundary.md +96 -0
- package/docs/product/architecture-boundaries.md +223 -0
- package/docs/product/product-tenets.md +174 -0
- package/docs/releases/1.2.0-draft.md +48 -0
- package/package.json +7 -7
- package/skills/.curated/daily-brief/SKILL.md +75 -0
- package/skills/.curated/electional-overlay/SKILL.md +67 -0
- package/skills/.curated/weekly-overview/SKILL.md +73 -0
- package/skills/.system/write-skill/SKILL.md +90 -0
- package/src/astro-service.ts +861 -60
- package/src/cli.ts +84 -0
- package/src/entrypoint.ts +118 -0
- package/src/ephemeris.ts +44 -0
- package/src/formatter.ts +13 -1
- package/src/index.ts +77 -121
- package/src/loader.ts +69 -25
- package/src/mcp-alias.ts +10 -0
- package/src/time-utils.ts +18 -0
- package/src/tool-registry.ts +129 -9
- package/src/tool-result.ts +44 -0
- package/src/types.ts +91 -1
- package/tests/unit/astro-service.test.ts +751 -5
- package/tests/unit/cli-commands.test.ts +13 -0
- package/tests/unit/entrypoint.test.ts +67 -0
- package/tests/unit/error-mapping.test.ts +20 -0
- package/tests/unit/formatter.test.ts +6 -0
- package/tests/unit/tool-registry.test.ts +114 -2
- package/setup.sh +0 -21
package/src/cli.ts
CHANGED
|
@@ -56,11 +56,25 @@ interface TransitOptions extends SharedOptions {
|
|
|
56
56
|
categories?: string;
|
|
57
57
|
includeMundane?: boolean;
|
|
58
58
|
daysAhead?: string;
|
|
59
|
+
mode?: 'snapshot' | 'best_hit' | 'forecast';
|
|
59
60
|
maxOrb?: string;
|
|
60
61
|
exactOnly?: boolean;
|
|
61
62
|
applyingOnly?: boolean;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
interface ElectionalContextOptions {
|
|
66
|
+
date?: string;
|
|
67
|
+
time?: string;
|
|
68
|
+
timezone?: string;
|
|
69
|
+
latitude?: string;
|
|
70
|
+
longitude?: string;
|
|
71
|
+
houseSystem?: 'P' | 'K' | 'W' | 'R';
|
|
72
|
+
includeRulerBasics?: boolean;
|
|
73
|
+
planetaryApplications?: boolean;
|
|
74
|
+
orbDegrees?: string;
|
|
75
|
+
pretty?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
64
78
|
interface HousesOptions extends SharedOptions {
|
|
65
79
|
system?: string;
|
|
66
80
|
}
|
|
@@ -441,6 +455,69 @@ export async function runCli(
|
|
|
441
455
|
emitExecution(io, result, options.pretty ?? false);
|
|
442
456
|
});
|
|
443
457
|
|
|
458
|
+
program
|
|
459
|
+
.command('get-electional-context')
|
|
460
|
+
.description(mustTool('get_electional_context').description)
|
|
461
|
+
.requiredOption(
|
|
462
|
+
'--date <yyyy-mm-dd>',
|
|
463
|
+
toolSchemaProperty('get_electional_context', 'date').description ?? 'Target local date'
|
|
464
|
+
)
|
|
465
|
+
.requiredOption(
|
|
466
|
+
'--time <hh:mm[:ss]>',
|
|
467
|
+
toolSchemaProperty('get_electional_context', 'time').description ?? 'Target local time'
|
|
468
|
+
)
|
|
469
|
+
.requiredOption(
|
|
470
|
+
'--timezone <tz>',
|
|
471
|
+
toolSchemaProperty('get_electional_context', 'timezone').description ?? 'Timezone'
|
|
472
|
+
)
|
|
473
|
+
.requiredOption(
|
|
474
|
+
'--latitude <number>',
|
|
475
|
+
toolSchemaProperty('get_electional_context', 'latitude').description ?? 'Latitude'
|
|
476
|
+
)
|
|
477
|
+
.requiredOption(
|
|
478
|
+
'--longitude <number>',
|
|
479
|
+
toolSchemaProperty('get_electional_context', 'longitude').description ?? 'Longitude'
|
|
480
|
+
)
|
|
481
|
+
.addOption(
|
|
482
|
+
new Option(
|
|
483
|
+
'--house-system <system>',
|
|
484
|
+
toolSchemaProperty('get_electional_context', 'house_system').description ?? 'House system'
|
|
485
|
+
).choices(['P', 'K', 'W', 'R'])
|
|
486
|
+
)
|
|
487
|
+
.option(
|
|
488
|
+
'--include-ruler-basics',
|
|
489
|
+
toolSchemaProperty('get_electional_context', 'include_ruler_basics').description ??
|
|
490
|
+
'Include ascendant-ruler basics'
|
|
491
|
+
)
|
|
492
|
+
.option(
|
|
493
|
+
'--no-planetary-applications',
|
|
494
|
+
'Exclude applying major aspects from the electional context response'
|
|
495
|
+
)
|
|
496
|
+
.option(
|
|
497
|
+
'--orb-degrees <number>',
|
|
498
|
+
toolSchemaProperty('get_electional_context', 'orb_degrees').description ?? 'Aspect orb'
|
|
499
|
+
)
|
|
500
|
+
.option('--pretty', 'Human-readable output instead of JSON')
|
|
501
|
+
.action(async (options: ElectionalContextOptions) => {
|
|
502
|
+
const spec = mustTool('get_electional_context');
|
|
503
|
+
const result = await spec.execute(
|
|
504
|
+
{ service, natalChart: null },
|
|
505
|
+
{
|
|
506
|
+
date: options.date,
|
|
507
|
+
time: options.time,
|
|
508
|
+
timezone: options.timezone,
|
|
509
|
+
latitude: toNumber(options.latitude, 'latitude'),
|
|
510
|
+
longitude: toNumber(options.longitude, 'longitude'),
|
|
511
|
+
house_system: options.houseSystem,
|
|
512
|
+
include_ruler_basics: options.includeRulerBasics,
|
|
513
|
+
include_planetary_applications: options.planetaryApplications,
|
|
514
|
+
orb_degrees:
|
|
515
|
+
options.orbDegrees == null ? undefined : toNumber(options.orbDegrees, 'orb-degrees'),
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
emitExecution(io, result, options.pretty ?? false);
|
|
519
|
+
});
|
|
520
|
+
|
|
444
521
|
const profiles = program
|
|
445
522
|
.command('profiles')
|
|
446
523
|
.description('Inspect and validate CLI profile files');
|
|
@@ -533,6 +610,12 @@ export async function runCli(
|
|
|
533
610
|
'--days-ahead <number>',
|
|
534
611
|
toolSchemaProperty('get_transits', 'days_ahead').description ?? 'Days ahead'
|
|
535
612
|
)
|
|
613
|
+
.addOption(
|
|
614
|
+
new Option(
|
|
615
|
+
'--mode <mode>',
|
|
616
|
+
toolSchemaProperty('get_transits', 'mode').description ?? 'Transit mode'
|
|
617
|
+
).choices(['snapshot', 'best_hit', 'forecast'])
|
|
618
|
+
)
|
|
536
619
|
.option(
|
|
537
620
|
'--max-orb <number>',
|
|
538
621
|
toolSchemaProperty('get_transits', 'max_orb').description ?? 'Max orb'
|
|
@@ -557,6 +640,7 @@ export async function runCli(
|
|
|
557
640
|
include_mundane: options.includeMundane,
|
|
558
641
|
days_ahead:
|
|
559
642
|
options.daysAhead == null ? undefined : toNumber(options.daysAhead, 'days-ahead'),
|
|
643
|
+
mode: options.mode,
|
|
560
644
|
max_orb: options.maxOrb == null ? undefined : toNumber(options.maxOrb, 'max-orb'),
|
|
561
645
|
exact_only: options.exactOnly,
|
|
562
646
|
applying_only: options.applyingOnly,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isValidTimezone } from './time-utils.js';
|
|
3
|
+
import type { HouseSystem } from './types.js';
|
|
4
|
+
|
|
5
|
+
export interface McpStartupDefaults {
|
|
6
|
+
preferredTimezone?: string;
|
|
7
|
+
preferredHouseStyle?: HouseSystem;
|
|
8
|
+
weekdayLabels?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface EntrypointResolution {
|
|
12
|
+
mode: 'cli' | 'mcp';
|
|
13
|
+
cliArgv: string[];
|
|
14
|
+
mcpHelpRequested: boolean;
|
|
15
|
+
mcpStartupDefaults: Readonly<McpStartupDefaults>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const VALID_HOUSE_STYLES = new Set<HouseSystem>(['P', 'W', 'K', 'E']);
|
|
19
|
+
|
|
20
|
+
function readOptionValue(argv: string[], index: number, flag: string) {
|
|
21
|
+
const current = argv[index];
|
|
22
|
+
const prefix = `${flag}=`;
|
|
23
|
+
if (current.startsWith(prefix)) {
|
|
24
|
+
return {
|
|
25
|
+
value: current.slice(prefix.length),
|
|
26
|
+
nextIndex: index,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const next = argv[index + 1];
|
|
31
|
+
if (next === undefined || next.startsWith('--')) {
|
|
32
|
+
throw new Error(`Missing value for ${flag}`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
value: next,
|
|
36
|
+
nextIndex: index + 1,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveEntrypoint(
|
|
41
|
+
argv: string[],
|
|
42
|
+
invokedPath = process.argv[1] ?? ''
|
|
43
|
+
): EntrypointResolution {
|
|
44
|
+
let mode: 'cli' | 'mcp' = path.basename(invokedPath).startsWith('e2a-mcp') ? 'mcp' : 'cli';
|
|
45
|
+
const cliArgv: string[] = [];
|
|
46
|
+
const defaults: McpStartupDefaults = {};
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < argv.length; i++) {
|
|
49
|
+
const arg = argv[i];
|
|
50
|
+
|
|
51
|
+
if (arg === '--mcp') {
|
|
52
|
+
mode = 'mcp';
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (arg === '--weekday-labels') {
|
|
57
|
+
defaults.weekdayLabels = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (arg === '--no-weekday-labels') {
|
|
62
|
+
defaults.weekdayLabels = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (arg === '--preferred-tz' || arg.startsWith('--preferred-tz=')) {
|
|
67
|
+
const { value, nextIndex } = readOptionValue(argv, i, '--preferred-tz');
|
|
68
|
+
defaults.preferredTimezone = value;
|
|
69
|
+
i = nextIndex;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (arg === '--preferred-house-style' || arg.startsWith('--preferred-house-style=')) {
|
|
74
|
+
const { value, nextIndex } = readOptionValue(argv, i, '--preferred-house-style');
|
|
75
|
+
defaults.preferredHouseStyle = value as HouseSystem;
|
|
76
|
+
i = nextIndex;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cliArgv.push(arg);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const usedMcpOnlyFlag =
|
|
84
|
+
defaults.preferredTimezone !== undefined ||
|
|
85
|
+
defaults.preferredHouseStyle !== undefined ||
|
|
86
|
+
defaults.weekdayLabels !== undefined;
|
|
87
|
+
if (mode !== 'mcp' && usedMcpOnlyFlag) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
'MCP startup defaults require MCP mode. Use e2a --mcp, or launch via the e2a-mcp compatibility alias.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (defaults.preferredTimezone && !isValidTimezone(defaults.preferredTimezone)) {
|
|
94
|
+
throw new Error(`Invalid timezone: ${defaults.preferredTimezone}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (defaults.preferredHouseStyle && !VALID_HOUSE_STYLES.has(defaults.preferredHouseStyle)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Invalid preferred house style: ${defaults.preferredHouseStyle} (must be one of P, W, K, E)`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const mcpHelpRequested =
|
|
104
|
+
mode === 'mcp' &&
|
|
105
|
+
cliArgv.length > 0 &&
|
|
106
|
+
cliArgv.every((arg) => arg === '--help' || arg === '-h');
|
|
107
|
+
|
|
108
|
+
if (mode === 'mcp' && cliArgv.length > 0 && !mcpHelpRequested) {
|
|
109
|
+
throw new Error(`Unexpected CLI arguments in MCP mode: ${cliArgv.join(' ')}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
mode,
|
|
114
|
+
cliArgv,
|
|
115
|
+
mcpHelpRequested,
|
|
116
|
+
mcpStartupDefaults: Object.freeze({ ...defaults }),
|
|
117
|
+
};
|
|
118
|
+
}
|
package/src/ephemeris.ts
CHANGED
|
@@ -200,6 +200,50 @@ export class EphemerisCalculator {
|
|
|
200
200
|
return diff;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Convert ecliptic coordinates to local horizontal coordinates
|
|
205
|
+
*
|
|
206
|
+
* @param jd - Julian Day in UT
|
|
207
|
+
* @param position - Planetary ecliptic coordinates
|
|
208
|
+
* @param longitude - Observer longitude in degrees
|
|
209
|
+
* @param latitude - Observer latitude in degrees
|
|
210
|
+
* @param altitude - Observer altitude in meters (default: 0)
|
|
211
|
+
* @returns Horizontal coordinates including true and apparent altitude
|
|
212
|
+
*/
|
|
213
|
+
getHorizontalCoordinates(
|
|
214
|
+
jd: number,
|
|
215
|
+
position: Pick<PlanetPosition, 'longitude' | 'latitude' | 'distance'>,
|
|
216
|
+
longitude: number,
|
|
217
|
+
latitude: number,
|
|
218
|
+
altitude: number = 0
|
|
219
|
+
): { azimuth: number; trueAltitude: number; apparentAltitude: number } {
|
|
220
|
+
if (!this.eph) throw new Error('Ephemeris not initialized');
|
|
221
|
+
if (!Number.isFinite(jd)) {
|
|
222
|
+
throw new Error(`Invalid Julian Day: ${jd} (must be finite)`);
|
|
223
|
+
}
|
|
224
|
+
if (!Number.isFinite(longitude) || !Number.isFinite(latitude) || !Number.isFinite(altitude)) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
'Invalid geographic coordinates: longitude, latitude, and altitude must be finite'
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const result = this.eph.azalt(jd, Constants.SE_ECL2HOR, [longitude, latitude, altitude], 0, 0, [
|
|
231
|
+
position.longitude,
|
|
232
|
+
position.latitude,
|
|
233
|
+
position.distance,
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
if (!Array.isArray(result) || result.length < 3) {
|
|
237
|
+
throw new Error('Failed to calculate horizontal coordinates');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
azimuth: result[0],
|
|
242
|
+
trueAltitude: result[1],
|
|
243
|
+
apparentAltitude: result[2],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
203
247
|
/**
|
|
204
248
|
* Find all exact times when planet reaches a specific longitude
|
|
205
249
|
*
|
package/src/formatter.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
interface FormatInTimezoneOptions {
|
|
2
|
+
weekday?: boolean;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function formatInTimezone(
|
|
6
|
+
date: Date,
|
|
7
|
+
timezone: string,
|
|
8
|
+
formatOptions: FormatInTimezoneOptions = {}
|
|
9
|
+
): string {
|
|
2
10
|
const options: Intl.DateTimeFormatOptions = {
|
|
3
11
|
timeZone: timezone,
|
|
4
12
|
year: 'numeric',
|
|
@@ -10,6 +18,10 @@ export function formatInTimezone(date: Date, timezone: string): string {
|
|
|
10
18
|
timeZoneName: 'short',
|
|
11
19
|
};
|
|
12
20
|
|
|
21
|
+
if (formatOptions.weekday) {
|
|
22
|
+
options.weekday = 'short';
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
return new Intl.DateTimeFormat('en-US', options).format(date);
|
|
14
26
|
}
|
|
15
27
|
|
package/src/index.ts
CHANGED
|
@@ -16,142 +16,98 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
16
16
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
17
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
18
18
|
import { AstroService } from './astro-service.js';
|
|
19
|
+
import type { McpStartupDefaults } from './entrypoint.js';
|
|
19
20
|
import { logger } from './logger.js';
|
|
20
21
|
import { getToolSpec, MCP_TOOL_SPECS } from './tool-registry.js';
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
mapToolErrorMessageToCode,
|
|
24
|
+
mcpError,
|
|
25
|
+
mcpResult,
|
|
26
|
+
missingNatalChart,
|
|
27
|
+
} from './tool-result.js';
|
|
22
28
|
import type { NatalChart } from './types.js';
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
capabilities: {
|
|
31
|
-
tools: {},
|
|
30
|
+
function createServer(startupDefaults: Readonly<McpStartupDefaults> = {}) {
|
|
31
|
+
const server = new Server(
|
|
32
|
+
{
|
|
33
|
+
name: 'e2a-mcp',
|
|
34
|
+
version: '1.0.0',
|
|
32
35
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* In-memory natal chart — the server's sole piece of mutable state.
|
|
38
|
-
*
|
|
39
|
-
* Lifecycle:
|
|
40
|
-
* - Starts as `null` when the process launches.
|
|
41
|
-
* - Set by `set_natal_chart`; overwritten on each call.
|
|
42
|
-
* - Persists for the lifetime of this stdio process (one per MCP client).
|
|
43
|
-
* - Tools that require it (`get_transits`, `get_houses`, `get_rise_set_times`,
|
|
44
|
-
* `generate_natal_chart`, `generate_transit_chart`) return a structured
|
|
45
|
-
* MISSING_NATAL_CHART error if it is null.
|
|
46
|
-
* - Use `get_server_status` to inspect whether a chart is loaded.
|
|
47
|
-
*
|
|
48
|
-
* Thread safety: Each MCP client connection spawns a separate Node.js process
|
|
49
|
-
* via stdio transport, so this variable is isolated per client.
|
|
50
|
-
* No synchronization needed as requests are serialized within a single process.
|
|
51
|
-
*/
|
|
52
|
-
let natalChart: NatalChart | null = null;
|
|
53
|
-
|
|
54
|
-
// Calculator instances (initialized on demand)
|
|
55
|
-
const astroService = new AstroService();
|
|
56
|
-
|
|
57
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
58
|
-
return {
|
|
59
|
-
tools: MCP_TOOL_SPECS.map((spec) => ({
|
|
60
|
-
name: spec.name,
|
|
61
|
-
description: spec.description,
|
|
62
|
-
inputSchema: spec.inputSchema,
|
|
63
|
-
})),
|
|
64
|
-
};
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Handle MCP tool requests
|
|
69
|
-
*
|
|
70
|
-
* @param request - The MCP tool request
|
|
71
|
-
* @returns Tool response with data or error
|
|
72
|
-
* @throws Error for unhandled tools
|
|
73
|
-
*
|
|
74
|
-
* @remarks
|
|
75
|
-
* Routes requests to appropriate handlers. Initializes ephemeris on first use.
|
|
76
|
-
* All handlers return structured responses suitable for MCP clients.
|
|
77
|
-
*/
|
|
78
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
79
|
-
const { name, arguments: args = {} } = request.params;
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const spec = getToolSpec(name);
|
|
83
|
-
if (!spec) {
|
|
84
|
-
return mcpError({
|
|
85
|
-
code: 'INVALID_INPUT',
|
|
86
|
-
message: `Unknown tool: ${name}`,
|
|
87
|
-
retryable: false,
|
|
88
|
-
suggestedFix: 'Check the tool name against the list returned by ListTools.',
|
|
89
|
-
});
|
|
36
|
+
{
|
|
37
|
+
capabilities: {
|
|
38
|
+
tools: {},
|
|
39
|
+
},
|
|
90
40
|
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
let natalChart: NatalChart | null = null;
|
|
44
|
+
const astroService = new AstroService({ mcpStartupDefaults: startupDefaults });
|
|
45
|
+
|
|
46
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
47
|
+
return {
|
|
48
|
+
tools: MCP_TOOL_SPECS.map((spec) => ({
|
|
49
|
+
name: spec.name,
|
|
50
|
+
description: spec.description,
|
|
51
|
+
inputSchema: spec.inputSchema,
|
|
52
|
+
})),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
91
55
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
56
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
57
|
+
const { name, arguments: args = {} } = request.params;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const spec = getToolSpec(name);
|
|
61
|
+
if (!spec) {
|
|
62
|
+
return mcpError({
|
|
63
|
+
code: 'INVALID_INPUT',
|
|
64
|
+
message: `Unknown tool: ${name}`,
|
|
65
|
+
retryable: false,
|
|
66
|
+
suggestedFix: 'Check the tool name against the list returned by ListTools.',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
95
69
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
natalChart,
|
|
100
|
-
},
|
|
101
|
-
args as Record<string, unknown>
|
|
102
|
-
);
|
|
70
|
+
if (spec.requiresNatalChart && !natalChart) {
|
|
71
|
+
return mcpError(missingNatalChart());
|
|
72
|
+
}
|
|
103
73
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
74
|
+
const result = await spec.execute(
|
|
75
|
+
{
|
|
76
|
+
service: astroService,
|
|
77
|
+
natalChart,
|
|
78
|
+
},
|
|
79
|
+
args as Record<string, unknown>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (result.kind === 'state') {
|
|
83
|
+
if (result.natalChart !== undefined) {
|
|
84
|
+
natalChart = result.natalChart;
|
|
85
|
+
}
|
|
86
|
+
return mcpResult(result.data, result.text);
|
|
107
87
|
}
|
|
108
|
-
return mcpResult(result.data, result.text);
|
|
109
|
-
}
|
|
110
88
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
89
|
+
return { content: result.content };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
92
|
+
const code = mapToolErrorMessageToCode(errorMessage);
|
|
114
93
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
errorMessage.includes('Invalid day') ||
|
|
122
|
-
errorMessage.includes('days_ahead') ||
|
|
123
|
-
errorMessage.includes('max_orb') ||
|
|
124
|
-
errorMessage.includes('missing julianDay')
|
|
125
|
-
) {
|
|
126
|
-
code = 'INVALID_INPUT';
|
|
127
|
-
} else if (errorMessage.includes('Invalid timezone') || errorMessage.includes('timezone')) {
|
|
128
|
-
code = 'INVALID_TIMEZONE';
|
|
129
|
-
} else if (errorMessage.includes('Invalid house system')) {
|
|
130
|
-
code = 'INVALID_HOUSE_SYSTEM';
|
|
131
|
-
} else if (errorMessage.includes('Ephemeris') || errorMessage.includes('ephemeris')) {
|
|
132
|
-
code = 'EPHEMERIS_COMPUTE_FAILED';
|
|
133
|
-
} else if (
|
|
134
|
-
errorMessage.includes('write') ||
|
|
135
|
-
errorMessage.includes('ENOENT') ||
|
|
136
|
-
errorMessage.includes('EACCES')
|
|
137
|
-
) {
|
|
138
|
-
code = 'FILE_WRITE_FAILED';
|
|
139
|
-
} else if (errorMessage.includes('render') || errorMessage.includes('chart')) {
|
|
140
|
-
code = 'CHART_RENDER_FAILED';
|
|
141
|
-
} else {
|
|
142
|
-
code = 'INTERNAL_ERROR';
|
|
94
|
+
return mcpError({
|
|
95
|
+
code,
|
|
96
|
+
message: errorMessage,
|
|
97
|
+
retryable: code === 'EPHEMERIS_COMPUTE_FAILED' || code === 'FILE_WRITE_FAILED',
|
|
98
|
+
details: { tool: name },
|
|
99
|
+
});
|
|
143
100
|
}
|
|
101
|
+
});
|
|
144
102
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
});
|
|
103
|
+
return {
|
|
104
|
+
server,
|
|
105
|
+
astroService,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
153
108
|
|
|
154
|
-
export async function main() {
|
|
109
|
+
export async function main(startupDefaults: Readonly<McpStartupDefaults> = {}) {
|
|
110
|
+
const { server, astroService } = createServer(startupDefaults);
|
|
155
111
|
logger.info('Initializing Swiss Ephemeris');
|
|
156
112
|
await astroService.init();
|
|
157
113
|
logger.info('Ephemeris initialized');
|
package/src/loader.ts
CHANGED
|
@@ -4,33 +4,77 @@ import { readFile } from 'node:fs/promises';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
// Set up browser globals for astrochart library BEFORE any imports
|
|
6
6
|
import { JSDOM } from 'jsdom';
|
|
7
|
+
import { resolveEntrypoint } from './entrypoint.js';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
(globalThis as any).
|
|
11
|
-
(globalThis as any).
|
|
12
|
-
(globalThis as any).
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
9
|
+
function initializeRuntime() {
|
|
10
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
|
11
|
+
(globalThis as any).window = dom.window;
|
|
12
|
+
(globalThis as any).document = dom.window.document;
|
|
13
|
+
(globalThis as any).self = globalThis; // Critical: astrochart checks for 'self' - see https://github.com/AstroDraw/AstroChart/issues/85
|
|
14
|
+
(globalThis as any).SVGElement = dom.window.SVGElement;
|
|
15
|
+
|
|
16
|
+
// Polyfill fetch for file:// URLs used by chart tooling in Node.
|
|
17
|
+
const originalFetch = globalThis.fetch;
|
|
18
|
+
(globalThis as any).fetch = async (url: string | URL, ...args: any[]) => {
|
|
19
|
+
const urlStr = url.toString();
|
|
20
|
+
if (urlStr.startsWith('file://')) {
|
|
21
|
+
const filePath = fileURLToPath(urlStr);
|
|
22
|
+
const buffer = await readFile(filePath);
|
|
23
|
+
return {
|
|
24
|
+
ok: true,
|
|
25
|
+
arrayBuffer: async () =>
|
|
26
|
+
buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return originalFetch(url, ...args);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function emitMcpHelp(invokedPath = process.argv[1] ?? '') {
|
|
34
|
+
const invokedName = invokedPath.includes('e2a-mcp') ? 'e2a-mcp' : 'e2a';
|
|
35
|
+
const launchExample =
|
|
36
|
+
invokedName === 'e2a-mcp'
|
|
37
|
+
? 'e2a-mcp --preferred-tz America/Los_Angeles --preferred-house-style W --weekday-labels'
|
|
38
|
+
: 'e2a --mcp --preferred-tz America/Los_Angeles --preferred-house-style W --weekday-labels';
|
|
39
|
+
|
|
40
|
+
console.log(`Usage: ${invokedName === 'e2a-mcp' ? 'e2a-mcp' : 'e2a --mcp'} [options]
|
|
41
|
+
|
|
42
|
+
Start the ether-to-astro MCP server with optional deterministic startup defaults.
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
--preferred-tz <iana> Default reporting timezone for MCP surfaces
|
|
46
|
+
--preferred-house-style <P|W|K|E>
|
|
47
|
+
Default house-style preference for MCP surfaces
|
|
48
|
+
--weekday-labels Include weekday labels in human-readable MCP text output
|
|
49
|
+
--no-weekday-labels Disable weekday labels in human-readable MCP text output
|
|
50
|
+
-h, --help Show this help
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
${launchExample}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function runEntrypoint(argv = process.argv.slice(2), invokedPath = process.argv[1]) {
|
|
57
|
+
initializeRuntime();
|
|
58
|
+
const resolved = resolveEntrypoint(argv, invokedPath);
|
|
59
|
+
|
|
60
|
+
if (resolved.mode === 'mcp') {
|
|
61
|
+
if (resolved.mcpHelpRequested) {
|
|
62
|
+
emitMcpHelp(invokedPath);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const { main } = await import('./index.js');
|
|
66
|
+
await main(resolved.mcpStartupDefaults);
|
|
67
|
+
return;
|
|
26
68
|
}
|
|
27
|
-
return originalFetch(url, ...args);
|
|
28
|
-
};
|
|
29
69
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
70
|
+
const { runCli } = await import('./cli.js');
|
|
71
|
+
const code = await runCli(resolved.cliArgv);
|
|
72
|
+
process.exit(code);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
76
|
+
runEntrypoint().catch((error) => {
|
|
77
|
+
console.error('[ERROR] Failed to start program:', error);
|
|
34
78
|
process.exit(1);
|
|
35
79
|
});
|
|
36
|
-
}
|
|
80
|
+
}
|
package/src/mcp-alias.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runEntrypoint } from './loader.js';
|
|
4
|
+
|
|
5
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
6
|
+
runEntrypoint(['--mcp', ...process.argv.slice(2)], process.argv[1]).catch((error) => {
|
|
7
|
+
console.error('[ERROR] Failed to start program:', error);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
|
10
|
+
}
|
package/src/time-utils.ts
CHANGED
|
@@ -165,3 +165,21 @@ export function addLocalDays(local: LocalDateTime, timezone: string, days: numbe
|
|
|
165
165
|
const zonedDateTime = newPlainDateTime.toZonedDateTime(timezone);
|
|
166
166
|
return new Date(zonedDateTime.epochMilliseconds);
|
|
167
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Format a UTC instant as a local ISO-8601 timestamp with timezone offset.
|
|
171
|
+
*
|
|
172
|
+
* @param utc - UTC Date object
|
|
173
|
+
* @param timezone - IANA timezone string
|
|
174
|
+
* @returns Local timestamp in the form YYYY-MM-DDTHH:mm:ss±HH:MM
|
|
175
|
+
*/
|
|
176
|
+
export function formatLocalTimestampWithOffset(utc: Date, timezone: string): string {
|
|
177
|
+
const local = utcToLocal(utc, timezone);
|
|
178
|
+
const offsetMinutes = getTimezoneOffset(utc, timezone);
|
|
179
|
+
const offsetSign = offsetMinutes >= 0 ? '+' : '-';
|
|
180
|
+
const offsetAbs = Math.abs(offsetMinutes);
|
|
181
|
+
const offsetHours = String(Math.floor(offsetAbs / 60)).padStart(2, '0');
|
|
182
|
+
const offsetMins = String(offsetAbs % 60).padStart(2, '0');
|
|
183
|
+
|
|
184
|
+
return `${String(local.year).padStart(4, '0')}-${String(local.month).padStart(2, '0')}-${String(local.day).padStart(2, '0')}T${String(local.hour).padStart(2, '0')}:${String(local.minute).padStart(2, '0')}:${String(local.second ?? 0).padStart(2, '0')}${offsetSign}${offsetHours}:${offsetMins}`;
|
|
185
|
+
}
|