@openephemeris/mcp-server 3.2.0 → 3.2.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ async function main() {
7
+ console.log("šŸš€ Starting MCP Integration Test...");
8
+ // Path to the primary entrypoint
9
+ const serverPath = path.resolve(__dirname, "../src/index.ts");
10
+ // Spin up the stdio transport simulating a client like Claude/Cursor
11
+ const transport = new StdioClientTransport({
12
+ command: "npx",
13
+ args: ["tsx", serverPath],
14
+ env: {
15
+ ...process.env,
16
+ // Pass the profile explicitly for predictable responses
17
+ OPENEPHEMERIS_PROFILE: "dev",
18
+ },
19
+ });
20
+ const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
21
+ console.log("šŸ”Œ Connecting to local MCP server via stdio...");
22
+ await client.connect(transport);
23
+ console.log("āœ… Handshake complete. Protocol negotiated successfully.");
24
+ // Test 1: List Tools
25
+ console.log("\nšŸ“” Polling server tools...");
26
+ const toolsResponse = await client.listTools();
27
+ console.log(`āœ… Discovered ${toolsResponse.tools.length} tools.`);
28
+ // Verify core tools exist
29
+ const toolNames = toolsResponse.tools.map((t) => t.name);
30
+ const coreTools = ["auth.login", "dev.call", "ephemeris.moon_phase", "ephemeris.natal_chart"];
31
+ for (const name of coreTools) {
32
+ if (!toolNames.includes(name)) {
33
+ throw new Error(`āŒ Missing expected tool: ${name}`);
34
+ }
35
+ }
36
+ // Test 2: Call an open endpoint that doesn't strictly require API Key for simple responses,
37
+ // or handle the graceful 401 prompt gracefully if it does.
38
+ // Tools returning standard 401s with markdown prompts are still valid successful MCP JSON-RPC calls.
39
+ console.log("\nāš™ļø Testing 'ephemeris.moon_phase' tool call...");
40
+ try {
41
+ const result = await client.callTool({
42
+ name: "ephemeris.moon_phase",
43
+ arguments: {}, // Defaults to current time
44
+ });
45
+ // An MCP tool response is usually { content: [ { type: 'text', text: '...' } ], isError: boolean }
46
+ const res = result;
47
+ if (res.isError) {
48
+ console.log("āš ļø Tool execution returned an expected application error (e.g. 401 Unauthorized expected without valid credentials):\n");
49
+ console.log(res.content);
50
+ }
51
+ else {
52
+ console.log("āœ… Tool executed successfully! Response preview:");
53
+ const content = res.content[0];
54
+ console.log(content.text ? content.text.slice(0, 300) + "..." : res.content);
55
+ }
56
+ }
57
+ catch (err) {
58
+ console.error("āŒ Exception during tool execution:", err);
59
+ process.exit(1);
60
+ }
61
+ console.log("\nšŸŽ‰ Integration Test Passed. Client and Server are communicating perfectly.");
62
+ // Cleanup gracefully
63
+ await transport.close();
64
+ process.exit(0);
65
+ }
66
+ main().catch((err) => {
67
+ console.error("Fatal Error:", err);
68
+ process.exit(1);
69
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,221 @@
1
+ /**
2
+ * test-sse-client.ts — Integration tests for the Remote SSE MCP server.
3
+ *
4
+ * Usage:
5
+ * Internal (local):
6
+ * Start the SSE server first: npm run dev:sse
7
+ * Then run: npx tsx scripts/test-sse-client.ts
8
+ *
9
+ * External (production):
10
+ * OPENEPHEMERIS_SSE_URL=https://mcp.openephemeris.com \
11
+ * OPENEPHEMERIS_API_KEY=opene-xxx \
12
+ * npx tsx scripts/test-sse-client.ts
13
+ *
14
+ * Tests:
15
+ * 1. Health endpoint responds correctly
16
+ * 2. SSE connection without API key is rejected (401)
17
+ * 3. SSE connection with invalid API key is rejected (403)
18
+ * 4. SSE connection + MCP handshake succeeds with valid key
19
+ * 5. Tool discovery returns expected core tools
20
+ * 6. Tool call (moon phase) executes end-to-end
21
+ */
22
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
23
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
24
+ import axios from "axios";
25
+ const SSE_BASE = process.env.OPENEPHEMERIS_SSE_URL?.replace(/\/+$/, "") ||
26
+ "http://localhost:3001";
27
+ const API_KEY = process.env.OPENEPHEMERIS_API_KEY || "";
28
+ const passed = [];
29
+ const failed = [];
30
+ function ok(name) {
31
+ passed.push(name);
32
+ console.log(` āœ… ${name}`);
33
+ }
34
+ function fail(name, err) {
35
+ failed.push(name);
36
+ console.error(` āŒ ${name}: ${err instanceof Error ? err.message : String(err)}`);
37
+ }
38
+ // ---------------------------------------------------------------------------
39
+ // Test 1: Health endpoint
40
+ // ---------------------------------------------------------------------------
41
+ async function testHealth() {
42
+ const name = "Health endpoint responds";
43
+ try {
44
+ const res = await axios.get(`${SSE_BASE}/health`, { timeout: 5_000 });
45
+ if (res.data.ok !== true)
46
+ throw new Error(`Expected ok:true, got ${JSON.stringify(res.data)}`);
47
+ if (!res.data.version)
48
+ throw new Error("Missing version in health response");
49
+ if (res.data.transport !== "sse")
50
+ throw new Error(`Expected transport:sse, got ${res.data.transport}`);
51
+ ok(name);
52
+ }
53
+ catch (err) {
54
+ fail(name, err);
55
+ }
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Test 2: No API key → 401
59
+ // ---------------------------------------------------------------------------
60
+ async function testNoApiKey() {
61
+ const name = "SSE without API key returns 401";
62
+ try {
63
+ const res = await axios.get(`${SSE_BASE}/sse`, {
64
+ timeout: 5_000,
65
+ validateStatus: () => true,
66
+ });
67
+ if (res.status !== 401)
68
+ throw new Error(`Expected 401, got ${res.status}`);
69
+ if (res.data.error !== "api_key_required")
70
+ throw new Error(`Unexpected error code: ${res.data.error}`);
71
+ ok(name);
72
+ }
73
+ catch (err) {
74
+ fail(name, err);
75
+ }
76
+ }
77
+ // ---------------------------------------------------------------------------
78
+ // Test 3: Invalid API key → 403 (or accepted if backend can't validate)
79
+ // ---------------------------------------------------------------------------
80
+ async function testInvalidApiKey() {
81
+ const name = "SSE with invalid API key is handled";
82
+ try {
83
+ const controller = new AbortController();
84
+ // Give the server 3s to respond with 403 or start SSE
85
+ const timeout = setTimeout(() => controller.abort(), 3_000);
86
+ const res = await axios.get(`${SSE_BASE}/sse?apiKey=opene-clearly-invalid-key-000`, {
87
+ validateStatus: () => true,
88
+ signal: controller.signal,
89
+ // Disable response buffering so the SSE stream doesn't hang
90
+ responseType: "stream",
91
+ });
92
+ clearTimeout(timeout);
93
+ if (res.status === 403) {
94
+ // Backend validated and rejected the key — ideal
95
+ ok(name + " (403 rejected)");
96
+ }
97
+ else if (res.status === 200) {
98
+ // Backend couldn't distinguish keys or endpoint doesn't require auth —
99
+ // SSE stream started. This is acceptable, the tool calls will fail later.
100
+ console.log(` āš ļø ${name}: Key accepted (backend may not validate at this layer)`);
101
+ passed.push(name);
102
+ }
103
+ else {
104
+ ok(name + ` (${res.status})`);
105
+ }
106
+ // Clean up the stream
107
+ try {
108
+ res.data?.destroy?.();
109
+ }
110
+ catch { /* ignore */ }
111
+ }
112
+ catch (err) {
113
+ if (err.code === "ERR_CANCELED" || err.name === "CanceledError") {
114
+ // The SSE stream started (200) and we aborted — that's valid
115
+ console.log(` āš ļø ${name}: SSE stream started (key was accepted), aborted cleanly`);
116
+ passed.push(name);
117
+ }
118
+ else {
119
+ fail(name, err);
120
+ }
121
+ }
122
+ }
123
+ // ---------------------------------------------------------------------------
124
+ // Test 4-6: Full SSE client connection + tool discovery + tool call
125
+ // ---------------------------------------------------------------------------
126
+ async function testFullSseFlow() {
127
+ if (!API_KEY) {
128
+ const skip = "Full SSE flow (skipped — no OPENEPHEMERIS_API_KEY set)";
129
+ console.log(` ā­ļø ${skip}`);
130
+ return;
131
+ }
132
+ // Test 4: Connect
133
+ const connectName = "SSE connection + MCP handshake";
134
+ let client = null;
135
+ let transport = null;
136
+ try {
137
+ const sseUrl = new URL(`${SSE_BASE}/sse?apiKey=${API_KEY}`);
138
+ transport = new SSEClientTransport(sseUrl);
139
+ client = new Client({ name: "sse-test-client", version: "1.0.0" }, { capabilities: {} });
140
+ await client.connect(transport);
141
+ ok(connectName);
142
+ }
143
+ catch (err) {
144
+ fail(connectName, err);
145
+ return; // Can't continue without a connection
146
+ }
147
+ // Test 5: Tool discovery
148
+ const discoveryName = "Tool discovery returns core tools";
149
+ try {
150
+ const tools = await client.listTools();
151
+ const names = tools.tools.map((t) => t.name);
152
+ const required = ["auth_login", "dev_call", "ephemeris_moon_phase", "ephemeris_natal_chart"];
153
+ const missing = required.filter((r) => !names.includes(r));
154
+ if (missing.length > 0)
155
+ throw new Error(`Missing tools: ${missing.join(", ")}`);
156
+ console.log(` Found ${tools.tools.length} tools`);
157
+ ok(discoveryName);
158
+ }
159
+ catch (err) {
160
+ fail(discoveryName, err);
161
+ }
162
+ // Test 6: Tool call
163
+ const callName = "Tool call (moon_phase) returns data";
164
+ try {
165
+ const result = await client.callTool({
166
+ name: "ephemeris_moon_phase",
167
+ arguments: {},
168
+ });
169
+ const res = result;
170
+ if (res.isError) {
171
+ // A 401 error is acceptable if the server's own key isn't set
172
+ const text = res.content?.[0]?.text || "";
173
+ if (text.includes("401") || text.includes("auth")) {
174
+ console.log(` āš ļø Tool returned auth error (server service key may not be configured)`);
175
+ ok(callName + " (auth expected)");
176
+ }
177
+ else {
178
+ throw new Error(`Tool returned error: ${text.slice(0, 200)}`);
179
+ }
180
+ }
181
+ else {
182
+ const text = res.content?.[0]?.text || "";
183
+ console.log(` Response preview: ${text.slice(0, 150)}...`);
184
+ ok(callName);
185
+ }
186
+ }
187
+ catch (err) {
188
+ fail(callName, err);
189
+ }
190
+ // Cleanup
191
+ try {
192
+ await transport?.close();
193
+ }
194
+ catch {
195
+ // ignore
196
+ }
197
+ }
198
+ // ---------------------------------------------------------------------------
199
+ // Runner
200
+ // ---------------------------------------------------------------------------
201
+ async function main() {
202
+ console.log(`\n🧪 SSE MCP Server Integration Tests`);
203
+ console.log(` Target: ${SSE_BASE}`);
204
+ console.log(` API Key: ${API_KEY ? API_KEY.slice(0, 10) + "..." : "(none — full flow skipped)"}\n`);
205
+ await testHealth();
206
+ await testNoApiKey();
207
+ await testInvalidApiKey();
208
+ await testFullSseFlow();
209
+ console.log(`\n${"─".repeat(50)}`);
210
+ console.log(` Passed: ${passed.length} | Failed: ${failed.length}`);
211
+ if (failed.length > 0) {
212
+ console.error(`\nāŒ ${failed.length} test(s) failed.`);
213
+ process.exit(1);
214
+ }
215
+ console.log(`\nšŸŽ‰ All tests passed!`);
216
+ process.exit(0);
217
+ }
218
+ main().catch((err) => {
219
+ console.error("Fatal:", err);
220
+ process.exit(1);
221
+ });
@@ -129,6 +129,9 @@ export class DeviceAuthFlow {
129
129
  console.error(`ā•‘ ${startResult.verification_uri_complete.slice(0, 48).padEnd(48)} ā•‘`);
130
130
  console.error("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•");
131
131
  console.error("");
132
+ console.error("By using the OpenEphemeris API, you agree to the Terms of Service");
133
+ console.error("at https://openephemeris.com/terms");
134
+ console.error("");
132
135
  console.error("Waiting for authorization...");
133
136
  const result = await this.poll(startResult.device_code, (attempt) => {
134
137
  if (attempt % 6 === 0) {
package/dist/src/index.js CHANGED
@@ -24,6 +24,12 @@ function resolveServerVersion() {
24
24
  const server = new Server({
25
25
  name: "openephemeris-mcp",
26
26
  version: resolveServerVersion(),
27
+ icons: [
28
+ {
29
+ src: "https://mcp.openephemeris.com/icon.png",
30
+ mimeType: "image/png",
31
+ }
32
+ ]
27
33
  }, {
28
34
  capabilities: {
29
35
  tools: {},
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,264 @@
1
+ /**
2
+ * server-sse.ts — Remote SSE transport for the OpenEphemeris MCP server.
3
+ *
4
+ * This wraps the exact same tool registry used by the local stdio server
5
+ * but exposes it over HTTP + Server-Sent Events so that Claude Web,
6
+ * Claude Mobile (future), and any remote MCP client can connect via URL.
7
+ *
8
+ * Architecture:
9
+ * - The SSE server validates the user's API key at connection time by
10
+ * making a lightweight call to the Go backend.
11
+ * - All subsequent tool calls use the server's OPENEPHEMERIS_SERVICE_KEY
12
+ * (set as a Fly secret) via the singleton BackendClient's X-Service-Key header.
13
+ * - This avoids the need to refactor all tool handlers for per-session auth.
14
+ */
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import express from "express";
19
+ import axios from "axios";
20
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
22
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
+ import { initTools, toolRegistry } from "./tools/index.js";
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+ function resolveServerVersion() {
28
+ try {
29
+ const here = path.dirname(fileURLToPath(import.meta.url));
30
+ // Try multiple levels up — compiled JS is at dist/src/, source is at src/
31
+ for (const rel of ["..", "../.."]) {
32
+ const pkgPath = path.resolve(here, rel, "package.json");
33
+ try {
34
+ const raw = fs.readFileSync(pkgPath, "utf-8");
35
+ const parsed = JSON.parse(raw);
36
+ if (typeof parsed.version === "string" && parsed.version.trim()) {
37
+ return parsed.version.trim();
38
+ }
39
+ }
40
+ catch {
41
+ continue;
42
+ }
43
+ }
44
+ }
45
+ catch {
46
+ // fall through
47
+ }
48
+ return "0.0.0-unknown";
49
+ }
50
+ const PORT = parseInt(process.env.PORT || "3001", 10);
51
+ const BACKEND_URL = process.env.OPENEPHEMERIS_BACKEND_URL ||
52
+ process.env.ASTROMCP_BACKEND_URL ||
53
+ "https://api.openephemeris.com";
54
+ const version = resolveServerVersion();
55
+ /**
56
+ * Validate a user-supplied API key by hitting an auth-gated endpoint on
57
+ * the Go sidecar. Returns true if the key yields a 2xx, false on 401/403.
58
+ */
59
+ async function validateApiKey(apiKey) {
60
+ try {
61
+ const resp = await axios.get(`${BACKEND_URL}/ephemeris/moon/phase`, {
62
+ headers: { "X-API-Key": apiKey },
63
+ timeout: 5_000,
64
+ validateStatus: () => true, // don't throw on any status
65
+ });
66
+ // 2xx = valid key, 401/403 = invalid key, anything else = let through
67
+ if (resp.status === 401 || resp.status === 403)
68
+ return false;
69
+ return true;
70
+ }
71
+ catch {
72
+ // Network error — let them through; tool calls will fail if key is bad.
73
+ console.warn("Could not validate API key against backend, allowing connection.");
74
+ return true;
75
+ }
76
+ }
77
+ // ---------------------------------------------------------------------------
78
+ // MCP Server factory — one per SSE session
79
+ // ---------------------------------------------------------------------------
80
+ function createMcpServer() {
81
+ const server = new Server({
82
+ name: "openephemeris-mcp",
83
+ version,
84
+ icons: [
85
+ {
86
+ src: "https://mcp.openephemeris.com/icon.png",
87
+ mimeType: "image/png",
88
+ }
89
+ ]
90
+ }, { capabilities: { tools: {} } });
91
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
92
+ tools: Object.values(toolRegistry).map((tool) => ({
93
+ name: tool.name,
94
+ description: tool.description,
95
+ inputSchema: tool.inputSchema,
96
+ annotations: tool.annotations ?? {
97
+ title: tool.name,
98
+ readOnlyHint: true,
99
+ destructiveHint: false,
100
+ },
101
+ })),
102
+ }));
103
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
104
+ const toolName = request.params.name;
105
+ const tool = toolRegistry[toolName];
106
+ if (!tool) {
107
+ throw new Error(`Unknown tool: ${toolName}`);
108
+ }
109
+ try {
110
+ const result = await tool.handler(request.params.arguments ?? {});
111
+ return {
112
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
113
+ };
114
+ }
115
+ catch (error) {
116
+ const errorMessage = error instanceof Error ? error.message : String(error);
117
+ return {
118
+ content: [
119
+ {
120
+ type: "text",
121
+ text: JSON.stringify({ success: false, error: "TOOL_EXECUTION_ERROR", message: errorMessage }, null, 2),
122
+ },
123
+ ],
124
+ isError: true,
125
+ };
126
+ }
127
+ });
128
+ return server;
129
+ }
130
+ // ---------------------------------------------------------------------------
131
+ // Express + SSE wiring
132
+ // ---------------------------------------------------------------------------
133
+ async function main() {
134
+ await initTools();
135
+ const app = express();
136
+ // NOTE: Do NOT use express.json() globally — SSEServerTransport.handlePostMessage
137
+ // reads the raw request stream itself. Pre-parsing with express.json() consumes it,
138
+ // causing "stream is not readable" errors.
139
+ // CORS — required for Claude Web cross-origin SSE connections
140
+ app.use((_req, res, next) => {
141
+ res.setHeader("Access-Control-Allow-Origin", "*");
142
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
143
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-Meridian-API-Key");
144
+ if (_req.method === "OPTIONS") {
145
+ res.status(204).end();
146
+ return;
147
+ }
148
+ next();
149
+ });
150
+ // Track active transports by session ID
151
+ const transports = new Map();
152
+ // Health check
153
+ app.get("/health", (_req, res) => {
154
+ res.json({ ok: true, version, transport: "sse", sessions: transports.size });
155
+ });
156
+ // Serve the MCP icon (earth + star)
157
+ // Compiled JS at dist/src/server-sse.js: ../../assets
158
+ // Source TS at src/server-sse.ts: ../assets
159
+ const here = path.dirname(fileURLToPath(import.meta.url));
160
+ const assetsDir = fs.existsSync(path.resolve(here, "..", "..", "assets"))
161
+ ? path.resolve(here, "..", "..", "assets")
162
+ : path.resolve(here, "..", "assets");
163
+ app.get("/icon.png", (_req, res) => {
164
+ const iconPath = path.join(assetsDir, "icon.png");
165
+ if (fs.existsSync(iconPath)) {
166
+ res.setHeader("Cache-Control", "public, max-age=86400");
167
+ res.sendFile(iconPath);
168
+ }
169
+ else {
170
+ res.status(404).end();
171
+ }
172
+ });
173
+ app.get("/favicon.ico", (_req, res) => {
174
+ const iconPath = path.join(assetsDir, "icon.png");
175
+ if (fs.existsSync(iconPath)) {
176
+ res.setHeader("Content-Type", "image/png");
177
+ res.setHeader("Cache-Control", "public, max-age=86400");
178
+ res.sendFile(iconPath);
179
+ }
180
+ else {
181
+ res.status(404).end();
182
+ }
183
+ });
184
+ // SSE endpoint — clients GET /sse to establish a stream
185
+ app.get("/sse", async (req, res) => {
186
+ // Extract API key from query param or header
187
+ const apiKey = req.query.apiKey ||
188
+ req.headers["x-api-key"] ||
189
+ req.headers["x-meridian-api-key"] ||
190
+ req.headers["authorization"]?.replace(/^Bearer\s+/i, "");
191
+ if (!apiKey) {
192
+ res.status(401).json({
193
+ error: "api_key_required",
194
+ message: "An OpenEphemeris API key is required. Generate one at https://openephemeris.com/dashboard?tab=apikeys",
195
+ });
196
+ return;
197
+ }
198
+ // Validate the user's key against the Go backend
199
+ const valid = await validateApiKey(apiKey);
200
+ if (!valid) {
201
+ res.status(403).json({
202
+ error: "invalid_api_key",
203
+ message: "The provided API key is invalid or inactive. Check your key at https://openephemeris.com/dashboard?tab=apikeys",
204
+ });
205
+ return;
206
+ }
207
+ const transport = new SSEServerTransport("/message", res);
208
+ const sessionId = transport.sessionId;
209
+ transports.set(sessionId, transport);
210
+ const server = createMcpServer();
211
+ // Guard: prevent double-close from crashing the process
212
+ let closed = false;
213
+ const cleanup = () => {
214
+ if (closed)
215
+ return;
216
+ closed = true;
217
+ transports.delete(sessionId);
218
+ try {
219
+ server.close().catch(() => { });
220
+ }
221
+ catch { /* ignore */ }
222
+ };
223
+ transport.onclose = cleanup;
224
+ // Handle abrupt client disconnects (browser tab closed, etc.)
225
+ res.on("close", cleanup);
226
+ try {
227
+ // server.connect() calls transport.start() internally in SDK v1.27+
228
+ await server.connect(transport);
229
+ }
230
+ catch (err) {
231
+ console.error(`SSE session ${sessionId} failed to start:`, err);
232
+ cleanup();
233
+ }
234
+ });
235
+ // Message endpoint — clients POST /message?sessionId=xxx
236
+ app.post("/message", async (req, res) => {
237
+ const sessionId = req.query.sessionId;
238
+ const transport = transports.get(sessionId);
239
+ if (!transport) {
240
+ res.status(400).json({
241
+ error: "invalid_session",
242
+ message: "Unknown or expired session. Re-connect to /sse first.",
243
+ });
244
+ return;
245
+ }
246
+ await transport.handlePostMessage(req, res);
247
+ });
248
+ app.listen(PORT, "0.0.0.0", () => {
249
+ console.log(`OpenEphemeris MCP SSE server v${version} listening on port ${PORT}`);
250
+ });
251
+ }
252
+ // Prevent SDK recursive close bugs from crashing the process
253
+ process.on("uncaughtException", (err) => {
254
+ if (err instanceof RangeError && err.message.includes("call stack")) {
255
+ console.error("Caught stack overflow (likely SDK close recursion), ignoring.");
256
+ return;
257
+ }
258
+ console.error("Uncaught exception:", err);
259
+ process.exit(1);
260
+ });
261
+ main().catch((error) => {
262
+ console.error("Fatal error:", error);
263
+ process.exit(1);
264
+ });
@@ -6,7 +6,7 @@ const credentialManager = new CredentialManager();
6
6
  // auth.login — Start the device authorization flow
7
7
  // ────────────────────────────────────────────────────────
8
8
  registerTool({
9
- name: "auth.login",
9
+ name: "auth_login",
10
10
  description: "Start the device authorization flow to connect this MCP server to your OpenEphemeris account. " +
11
11
  "Returns a verification URL and code for the user to enter in their browser. " +
12
12
  "The MCP server will then automatically receive credentials and all API calls will be " +
@@ -31,7 +31,7 @@ registerTool({
31
31
  email: status.email,
32
32
  user_id: status.userId,
33
33
  expires_at: status.expiresAt,
34
- message: `Already authenticated as ${status.email}. Use auth.logout to disconnect.`,
34
+ message: `Already authenticated as ${status.email}. Use auth_logout to disconnect.`,
35
35
  };
36
36
  }
37
37
  // Check if env var credentials are set
@@ -86,7 +86,7 @@ registerTool({
86
86
  // auth.status — Check current authentication state
87
87
  // ────────────────────────────────────────────────────────
88
88
  registerTool({
89
- name: "auth.status",
89
+ name: "auth_status",
90
90
  description: "Check the current authentication status of this MCP server. " +
91
91
  "Shows whether the server is authenticated, which account it's linked to, " +
92
92
  "the authentication method (API key, JWT, device auth), and token expiry.",
@@ -155,14 +155,14 @@ registerTool({
155
155
  expired_at: creds.expires_at,
156
156
  message: "Device auth credentials exist but are expired. " +
157
157
  "The server will attempt to refresh automatically on the next API call, " +
158
- "or you can run auth.login to re-authenticate.",
158
+ "or you can run auth_login to re-authenticate.",
159
159
  };
160
160
  }
161
161
  return {
162
162
  authenticated: false,
163
163
  method: "none",
164
164
  message: "Not authenticated. Set OPENEPHEMERIS_API_KEY in your MCP server config, " +
165
- "or run auth.login to connect your account interactively.",
165
+ "or run auth_login to connect your account interactively.",
166
166
  };
167
167
  },
168
168
  });
@@ -170,7 +170,7 @@ registerTool({
170
170
  // auth.logout — Clear cached credentials
171
171
  // ────────────────────────────────────────────────────────
172
172
  registerTool({
173
- name: "auth.logout",
173
+ name: "auth_logout",
174
174
  description: "Disconnect this MCP server from your OpenEphemeris account by clearing " +
175
175
  "cached credentials. Does NOT revoke the API key if one is set via environment " +
176
176
  "variable — only clears device-auth cached credentials.",