@retrotech71/appleii-agent 1.0.1 → 1.0.5
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/README.md +17 -3
- package/package.json +1 -1
- package/src/http-server.js +130 -4
- package/src/index.js +12 -1
- package/src/tools/disconnect-clients.js +27 -0
- package/src/tools/get-version.js +27 -0
- package/src/tools/index.js +6 -0
- package/src/tools/server-control.js +51 -12
- package/src/tools/shutdown-remote-server.js +113 -0
- package/src/version.js +20 -0
package/README.md
CHANGED
|
@@ -38,19 +38,23 @@ npm install
|
|
|
38
38
|
|
|
39
39
|
Add to your MCP client configuration. For Claude Code, edit `~/.claude/mcp.json`:
|
|
40
40
|
|
|
41
|
-
###
|
|
41
|
+
### Option 1: Using bunx (Recommended)
|
|
42
|
+
|
|
43
|
+
Runs the published package directly with Bun:
|
|
42
44
|
|
|
43
45
|
```json
|
|
44
46
|
{
|
|
45
47
|
"mcpServers": {
|
|
46
48
|
"appleii-agent": {
|
|
47
|
-
"
|
|
49
|
+
"type": "stdio",
|
|
50
|
+
"command": "bunx",
|
|
51
|
+
"args": ["-y", "@retrotech71/appleii-agent"]
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
```
|
|
52
56
|
|
|
53
|
-
###
|
|
57
|
+
### Option 2: Local development from source
|
|
54
58
|
|
|
55
59
|
```json
|
|
56
60
|
{
|
|
@@ -109,6 +113,9 @@ Save 256 bytes from memory address $0800 to ~/output.bin
|
|
|
109
113
|
| `set_https` | Toggle HTTPS mode |
|
|
110
114
|
| `set_debug` | Toggle debug logging |
|
|
111
115
|
| `get_state` | Get current server state |
|
|
116
|
+
| `get_version` | Get MCP server version information |
|
|
117
|
+
| `shutdown_remote_server` | Shutdown another MCP server instance on the same port |
|
|
118
|
+
| `disconnect_clients` | Gracefully disconnect all connected emulator clients |
|
|
112
119
|
|
|
113
120
|
## Environment Variables
|
|
114
121
|
|
|
@@ -154,6 +161,13 @@ Or toggle at runtime via the `set_https` tool.
|
|
|
154
161
|
- Click the sparkle icon to view connection details
|
|
155
162
|
- Check that port 3033 is not in use by another process
|
|
156
163
|
|
|
164
|
+
**Reclaiming the Apple II Agent port (multiple MCP instances)**
|
|
165
|
+
- The MCP server handles port conflicts gracefully and won't fail
|
|
166
|
+
- Use `server_control` with action `status` to check if port is in use
|
|
167
|
+
- Use `shutdown_remote_server` to reclaim the port by stopping the other instance
|
|
168
|
+
- After shutdown, use `server_control` with action `start` to start this instance on the reclaimed port
|
|
169
|
+
- Note: A server stopped via `shutdown_remote_server` can only be restarted by its owning MCP instance
|
|
170
|
+
|
|
157
171
|
**Tools return errors**
|
|
158
172
|
- The emulator must be powered on for most tools to work
|
|
159
173
|
- SmartPort tools require the SmartPort card to be installed in an expansion slot
|
package/package.json
CHANGED
package/src/http-server.js
CHANGED
|
@@ -30,6 +30,8 @@ export class HttpServer {
|
|
|
30
30
|
this.pendingToolResults = new Map();
|
|
31
31
|
this.eventQueue = [];
|
|
32
32
|
this.emulatorDomain = null; // Domain where the emulator is running
|
|
33
|
+
this.portInUse = false; // Track if port is in use by another instance
|
|
34
|
+
this.externallyShutdown = false; // Track if shutdown came from external command
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -69,6 +71,11 @@ export class HttpServer {
|
|
|
69
71
|
* Start the HTTP/HTTPS server
|
|
70
72
|
*/
|
|
71
73
|
async start() {
|
|
74
|
+
// Log if restarting after external shutdown (informational only)
|
|
75
|
+
if (this.externallyShutdown && this.debug) {
|
|
76
|
+
logger.log("[HTTP] Restarting after external shutdown");
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
return new Promise((resolve, reject) => {
|
|
73
80
|
const requestHandler = (req, res) => {
|
|
74
81
|
this._handleRequest(req, res);
|
|
@@ -97,11 +104,29 @@ export class HttpServer {
|
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
this.server.listen(this.port, () => {
|
|
107
|
+
// Successfully started - clear flags
|
|
108
|
+
this.portInUse = false;
|
|
109
|
+
this.externallyShutdown = false;
|
|
110
|
+
if (this.debug) {
|
|
111
|
+
logger.log(`[HTTP] Server listening on port ${this.port}`);
|
|
112
|
+
}
|
|
100
113
|
resolve();
|
|
101
114
|
});
|
|
102
115
|
|
|
103
116
|
this.server.on("error", (error) => {
|
|
104
|
-
|
|
117
|
+
// Handle port already in use gracefully
|
|
118
|
+
if (error.code === "EADDRINUSE") {
|
|
119
|
+
this.portInUse = true;
|
|
120
|
+
this.server = null;
|
|
121
|
+
if (this.debug) {
|
|
122
|
+
logger.log(`[HTTP] Port ${this.port} already in use - server not started`);
|
|
123
|
+
}
|
|
124
|
+
// Resolve instead of reject to keep MCP alive
|
|
125
|
+
resolve();
|
|
126
|
+
} else {
|
|
127
|
+
// Other errors still reject
|
|
128
|
+
reject(error);
|
|
129
|
+
}
|
|
105
130
|
});
|
|
106
131
|
});
|
|
107
132
|
}
|
|
@@ -117,7 +142,7 @@ export class HttpServer {
|
|
|
117
142
|
|
|
118
143
|
// Enable CORS with Private Network Access (required for public HTTPS → localhost)
|
|
119
144
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
120
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
145
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, HEAD");
|
|
121
146
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
122
147
|
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
123
148
|
|
|
@@ -127,6 +152,18 @@ export class HttpServer {
|
|
|
127
152
|
return;
|
|
128
153
|
}
|
|
129
154
|
|
|
155
|
+
if (req.method === "HEAD" && req.url.startsWith("/events")) {
|
|
156
|
+
// Check if connection is allowed (for single-client mode)
|
|
157
|
+
if (this.clients.size > 0) {
|
|
158
|
+
res.writeHead(409, { "Content-Type": "text/plain" });
|
|
159
|
+
res.end("Another Apple //e Emulator Already Connected");
|
|
160
|
+
} else {
|
|
161
|
+
res.writeHead(200, { "Content-Type": "text/event-stream" });
|
|
162
|
+
res.end();
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
130
167
|
if (req.method === "GET" && req.url.startsWith("/events")) {
|
|
131
168
|
// SSE endpoint for streaming events to frontend
|
|
132
169
|
this._handleEventStream(req, res);
|
|
@@ -161,6 +198,33 @@ export class HttpServer {
|
|
|
161
198
|
return;
|
|
162
199
|
}
|
|
163
200
|
|
|
201
|
+
if (req.method === "POST" && req.url === "/shutdown") {
|
|
202
|
+
// External shutdown command - stops server and prevents restart
|
|
203
|
+
if (this.debug) {
|
|
204
|
+
logger.log("[HTTP] Received external shutdown command");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Mark as externally shutdown
|
|
208
|
+
this.externallyShutdown = true;
|
|
209
|
+
|
|
210
|
+
// Send response before shutting down
|
|
211
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
212
|
+
res.end(JSON.stringify({
|
|
213
|
+
status: "shutting_down",
|
|
214
|
+
message: "Server shutting down. Can only be restarted by owning MCP instance."
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
// Stop the server after response is sent (internal=false to preserve externallyShutdown flag)
|
|
218
|
+
setTimeout(async () => {
|
|
219
|
+
await this.stop(false);
|
|
220
|
+
if (this.debug) {
|
|
221
|
+
logger.log("[HTTP] Server stopped by external shutdown");
|
|
222
|
+
}
|
|
223
|
+
}, 100);
|
|
224
|
+
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
164
228
|
res.writeHead(404);
|
|
165
229
|
res.end("Not Found");
|
|
166
230
|
}
|
|
@@ -169,6 +233,16 @@ export class HttpServer {
|
|
|
169
233
|
* Handle Server-Sent Events stream
|
|
170
234
|
*/
|
|
171
235
|
_handleEventStream(req, res) {
|
|
236
|
+
// Check if a client is already connected (single client mode)
|
|
237
|
+
if (this.clients.size > 0) {
|
|
238
|
+
if (this.debug) {
|
|
239
|
+
logger.log("[HTTP] Rejecting connection - another client already connected");
|
|
240
|
+
}
|
|
241
|
+
res.writeHead(409, { "Content-Type": "text/plain" });
|
|
242
|
+
res.end("Another Apple //e Emulator Already Connected");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
172
246
|
// Parse domain from query parameter
|
|
173
247
|
const url = new URL(req.url, `http://localhost:${this.port}`);
|
|
174
248
|
const domain = url.searchParams.get("domain");
|
|
@@ -196,7 +270,7 @@ export class HttpServer {
|
|
|
196
270
|
res.write(": connected\n\n");
|
|
197
271
|
|
|
198
272
|
// Add client to set
|
|
199
|
-
const client = { req, res };
|
|
273
|
+
const client = { req, res, domain };
|
|
200
274
|
this.clients.add(client);
|
|
201
275
|
|
|
202
276
|
// Send queued events to new client
|
|
@@ -376,7 +450,7 @@ export class HttpServer {
|
|
|
376
450
|
/**
|
|
377
451
|
* Stop the HTTP/HTTPS server
|
|
378
452
|
*/
|
|
379
|
-
async stop() {
|
|
453
|
+
async stop(internal = true) {
|
|
380
454
|
if (this.server) {
|
|
381
455
|
// Close all SSE connections
|
|
382
456
|
this.clients.forEach((client) => {
|
|
@@ -387,6 +461,14 @@ export class HttpServer {
|
|
|
387
461
|
return new Promise((resolve) => {
|
|
388
462
|
this.server.close(() => {
|
|
389
463
|
this.server = null;
|
|
464
|
+
// Only clear externallyShutdown flag if this is an internal stop
|
|
465
|
+
// (from the owning MCP instance)
|
|
466
|
+
if (internal && this.externallyShutdown) {
|
|
467
|
+
this.externallyShutdown = false;
|
|
468
|
+
if (this.debug) {
|
|
469
|
+
logger.log("[HTTP] External shutdown flag cleared by owning instance");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
390
472
|
resolve();
|
|
391
473
|
});
|
|
392
474
|
});
|
|
@@ -441,6 +523,8 @@ export class HttpServer {
|
|
|
441
523
|
url: `${this.useHttps ? "https" : "http"}://localhost:${this.port}`,
|
|
442
524
|
emulatorDomain: this.emulatorDomain,
|
|
443
525
|
llmsTxtUrl: this.emulatorDomain ? `${this.emulatorDomain}/llms.txt` : null,
|
|
526
|
+
portInUse: this.portInUse,
|
|
527
|
+
externallyShutdown: this.externallyShutdown,
|
|
444
528
|
};
|
|
445
529
|
}
|
|
446
530
|
|
|
@@ -450,4 +534,46 @@ export class HttpServer {
|
|
|
450
534
|
getLlmsTxtUrl() {
|
|
451
535
|
return this.emulatorDomain ? `${this.emulatorDomain}/llms.txt` : null;
|
|
452
536
|
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Gracefully disconnect all clients
|
|
540
|
+
*/
|
|
541
|
+
disconnectAllClients() {
|
|
542
|
+
if (this.clients.size === 0) {
|
|
543
|
+
if (this.debug) {
|
|
544
|
+
logger.log("[HTTP] No clients to disconnect");
|
|
545
|
+
}
|
|
546
|
+
return { disconnected: 0 };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const count = this.clients.size;
|
|
550
|
+
|
|
551
|
+
// Send disconnect event to all clients before closing
|
|
552
|
+
this.clients.forEach((client) => {
|
|
553
|
+
try {
|
|
554
|
+
// Send a custom disconnect event
|
|
555
|
+
this._writeSSE(client.res, {
|
|
556
|
+
type: "DISCONNECT",
|
|
557
|
+
reason: "Server requested disconnect",
|
|
558
|
+
graceful: true,
|
|
559
|
+
});
|
|
560
|
+
// Close the connection
|
|
561
|
+
client.res.end();
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (this.debug) {
|
|
564
|
+
logger.log(`[HTTP] Error disconnecting client: ${error.message}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
this.clients.clear();
|
|
570
|
+
this.eventQueue = [];
|
|
571
|
+
this.emulatorDomain = null;
|
|
572
|
+
|
|
573
|
+
if (this.debug) {
|
|
574
|
+
logger.log(`[HTTP] Gracefully disconnected ${count} client(s)`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return { disconnected: count };
|
|
578
|
+
}
|
|
453
579
|
}
|
package/src/index.js
CHANGED
|
@@ -29,7 +29,18 @@ async function main() {
|
|
|
29
29
|
|
|
30
30
|
const protocol = USE_HTTPS ? "https" : "http";
|
|
31
31
|
logger.log("Apple II MCP Agent initialized");
|
|
32
|
-
|
|
32
|
+
|
|
33
|
+
// Check if HTTP server actually started
|
|
34
|
+
const status = httpServer.getStatus();
|
|
35
|
+
if (status.running) {
|
|
36
|
+
logger.log(`${protocol.toUpperCase()} server listening on ${protocol}://localhost:${HTTP_PORT}`);
|
|
37
|
+
} else if (status.portInUse) {
|
|
38
|
+
logger.log(`Port ${HTTP_PORT} already in use - HTTP server not started`);
|
|
39
|
+
logger.log("Use server_control tool to attempt start again or shutdown_remote_server to stop the other instance");
|
|
40
|
+
} else if (status.externallyShutdown) {
|
|
41
|
+
logger.log("HTTP server cannot start - was externally shutdown");
|
|
42
|
+
logger.log("Restart the MCP instance to enable HTTP server");
|
|
43
|
+
}
|
|
33
44
|
|
|
34
45
|
} catch (error) {
|
|
35
46
|
logger.log("Failed to start Apple II MCP Agent:", error);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* disconnect-clients.js - Disconnect all connected clients
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Shawn Bullock <shawn@agenticexpert.ai>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const tool = {
|
|
9
|
+
name: "disconnect_clients",
|
|
10
|
+
description: "Gracefully disconnect all connected Apple //e emulator clients",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {},
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function handler(args, httpServer) {
|
|
18
|
+
const result = httpServer.disconnectAllClients();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
success: true,
|
|
22
|
+
disconnected: result.disconnected,
|
|
23
|
+
message: result.disconnected > 0
|
|
24
|
+
? `Disconnected ${result.disconnected} client(s)`
|
|
25
|
+
: "No clients were connected",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* get-version.js - Get MCP server version information
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Shawn Bullock <shawn@agenticexpert.ai>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { VERSION, NAME, DESCRIPTION } from "../version.js";
|
|
9
|
+
|
|
10
|
+
export const tool = {
|
|
11
|
+
name: "get_version",
|
|
12
|
+
description: "Get the Apple II MCP Agent version information",
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {},
|
|
16
|
+
required: [],
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function handler() {
|
|
21
|
+
return {
|
|
22
|
+
success: true,
|
|
23
|
+
name: NAME,
|
|
24
|
+
version: VERSION,
|
|
25
|
+
description: DESCRIPTION,
|
|
26
|
+
};
|
|
27
|
+
}
|
package/src/tools/index.js
CHANGED
|
@@ -10,6 +10,9 @@ import * as serverControl from "./server-control.js";
|
|
|
10
10
|
import * as setHttps from "./set-https.js";
|
|
11
11
|
import * as setDebug from "./set-debug.js";
|
|
12
12
|
import * as getState from "./get-state.js";
|
|
13
|
+
import * as getVersion from "./get-version.js";
|
|
14
|
+
import * as disconnectClients from "./disconnect-clients.js";
|
|
15
|
+
import * as shutdownRemoteServer from "./shutdown-remote-server.js";
|
|
13
16
|
import * as showWindow from "./show-window.js";
|
|
14
17
|
import * as hideWindow from "./hide-window.js";
|
|
15
18
|
import * as focusWindow from "./focus-window.js";
|
|
@@ -25,6 +28,9 @@ export const tools = [
|
|
|
25
28
|
setHttps,
|
|
26
29
|
setDebug,
|
|
27
30
|
getState,
|
|
31
|
+
getVersion,
|
|
32
|
+
disconnectClients,
|
|
33
|
+
shutdownRemoteServer,
|
|
28
34
|
showWindow,
|
|
29
35
|
hideWindow,
|
|
30
36
|
focusWindow,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
export const tool = {
|
|
9
9
|
name: "server_control",
|
|
10
|
-
description: "Control the AG-UI HTTP/HTTPS server (start, stop, restart)",
|
|
10
|
+
description: "Control the AG-UI HTTP/HTTPS server (start, stop, restart, status). To reclaim or take over a port when another instance is using it: (1) First call shutdown_remote_server to stop the other instance, then (2) Call this tool with action 'start' to start this instance.",
|
|
11
11
|
inputSchema: {
|
|
12
12
|
type: "object",
|
|
13
13
|
properties: {
|
|
@@ -25,26 +25,65 @@ export async function handler(args, httpServer) {
|
|
|
25
25
|
const { action } = args;
|
|
26
26
|
|
|
27
27
|
switch (action) {
|
|
28
|
-
case "start":
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
case "start": {
|
|
29
|
+
const currentStatus = httpServer.getStatus();
|
|
30
|
+
|
|
31
|
+
if (currentStatus.running) {
|
|
32
|
+
return { status: "already_running", ...currentStatus };
|
|
31
33
|
}
|
|
34
|
+
|
|
32
35
|
await httpServer.start();
|
|
33
|
-
|
|
36
|
+
const newStatus = httpServer.getStatus();
|
|
37
|
+
|
|
38
|
+
if (newStatus.portInUse) {
|
|
39
|
+
return {
|
|
40
|
+
status: "failed_to_start",
|
|
41
|
+
reason: "port_in_use",
|
|
42
|
+
message: `Port ${newStatus.port} is already in use by another instance. Use shutdown_remote_server tool to stop it.`,
|
|
43
|
+
...newStatus
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { status: "started", ...newStatus };
|
|
48
|
+
}
|
|
34
49
|
|
|
35
|
-
case "stop":
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
case "stop": {
|
|
51
|
+
const currentStatus = httpServer.getStatus();
|
|
52
|
+
if (!currentStatus.running) {
|
|
53
|
+
return { status: "already_stopped", ...currentStatus };
|
|
38
54
|
}
|
|
39
55
|
await httpServer.stop();
|
|
40
56
|
return { status: "stopped", ...httpServer.getStatus() };
|
|
57
|
+
}
|
|
41
58
|
|
|
42
|
-
case "restart":
|
|
59
|
+
case "restart": {
|
|
43
60
|
await httpServer.restart();
|
|
44
|
-
|
|
61
|
+
const newStatus = httpServer.getStatus();
|
|
62
|
+
|
|
63
|
+
if (newStatus.portInUse) {
|
|
64
|
+
return {
|
|
65
|
+
status: "failed_to_restart",
|
|
66
|
+
reason: "port_in_use",
|
|
67
|
+
message: `Port ${newStatus.port} is already in use by another instance. Use shutdown_remote_server tool to stop it.`,
|
|
68
|
+
...newStatus
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { status: "restarted", ...newStatus };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case "status": {
|
|
76
|
+
const status = httpServer.getStatus();
|
|
77
|
+
|
|
78
|
+
// Add helpful messages based on state
|
|
79
|
+
if (status.portInUse && !status.running) {
|
|
80
|
+
status.message = `Port ${status.port} is in use. Use shutdown_remote_server to stop the other instance.`;
|
|
81
|
+
} else if (status.externallyShutdown && !status.running) {
|
|
82
|
+
status.message = "Server was externally shutdown. Use server_control to restart.";
|
|
83
|
+
}
|
|
45
84
|
|
|
46
|
-
|
|
47
|
-
|
|
85
|
+
return status;
|
|
86
|
+
}
|
|
48
87
|
|
|
49
88
|
default:
|
|
50
89
|
throw new Error(`Unknown action: ${action}`);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* shutdown-remote-server.js - Shutdown remote MCP server instance
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Shawn Bullock <shawn@agenticexpert.ai>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import http from "http";
|
|
9
|
+
import https from "https";
|
|
10
|
+
|
|
11
|
+
export const tool = {
|
|
12
|
+
name: "shutdown_remote_server",
|
|
13
|
+
description: "Shutdown a remote Apple II MCP server instance running on the same port. Use this when port is already in use by another instance. To reclaim or take over a port: (1) Call this tool to shutdown the other instance, then (2) Call server_control with action 'start' to start this instance.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
port: {
|
|
18
|
+
type: "number",
|
|
19
|
+
description: "Port number of the remote server (default: 3033)",
|
|
20
|
+
default: 3033,
|
|
21
|
+
},
|
|
22
|
+
useHttps: {
|
|
23
|
+
type: "boolean",
|
|
24
|
+
description: "Whether the remote server is using HTTPS (default: false)",
|
|
25
|
+
default: false,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function handler(args, httpServer) {
|
|
32
|
+
const port = args.port || 3033;
|
|
33
|
+
const useHttps = args.useHttps || false;
|
|
34
|
+
const protocol = useHttps ? "https" : "http";
|
|
35
|
+
const url = `${protocol}://localhost:${port}/shutdown`;
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const requestModule = useHttps ? https : http;
|
|
39
|
+
|
|
40
|
+
const options = {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
},
|
|
45
|
+
// For self-signed certificates, ignore certificate errors
|
|
46
|
+
rejectUnauthorized: false,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const req = requestModule.request(url, options, (res) => {
|
|
50
|
+
let data = "";
|
|
51
|
+
|
|
52
|
+
res.on("data", (chunk) => {
|
|
53
|
+
data += chunk;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
res.on("end", () => {
|
|
57
|
+
try {
|
|
58
|
+
const response = JSON.parse(data);
|
|
59
|
+
resolve({
|
|
60
|
+
success: true,
|
|
61
|
+
port,
|
|
62
|
+
protocol,
|
|
63
|
+
message: `Remote server on ${protocol}://localhost:${port} successfully shutdown`,
|
|
64
|
+
response,
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
resolve({
|
|
68
|
+
success: true,
|
|
69
|
+
port,
|
|
70
|
+
protocol,
|
|
71
|
+
message: `Remote server on ${protocol}://localhost:${port} shutdown (raw response)`,
|
|
72
|
+
rawResponse: data,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
req.on("error", (error) => {
|
|
79
|
+
// Connection refused or other errors
|
|
80
|
+
if (error.code === "ECONNREFUSED") {
|
|
81
|
+
resolve({
|
|
82
|
+
success: false,
|
|
83
|
+
port,
|
|
84
|
+
protocol,
|
|
85
|
+
error: "connection_refused",
|
|
86
|
+
message: `No server found at ${protocol}://localhost:${port}`,
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
resolve({
|
|
90
|
+
success: false,
|
|
91
|
+
port,
|
|
92
|
+
protocol,
|
|
93
|
+
error: error.code || "unknown",
|
|
94
|
+
message: `Failed to connect to ${protocol}://localhost:${port}: ${error.message}`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
req.on("timeout", () => {
|
|
100
|
+
req.destroy();
|
|
101
|
+
resolve({
|
|
102
|
+
success: false,
|
|
103
|
+
port,
|
|
104
|
+
protocol,
|
|
105
|
+
error: "timeout",
|
|
106
|
+
message: `Request to ${protocol}://localhost:${port} timed out`,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
req.setTimeout(5000); // 5 second timeout
|
|
111
|
+
req.end();
|
|
112
|
+
});
|
|
113
|
+
}
|
package/src/version.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* version.js - Version information from package.json
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Shawn Bullock <shawn@agenticexpert.ai>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const packageJsonPath = join(__dirname, "..", "package.json");
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
17
|
+
|
|
18
|
+
export const VERSION = packageJson.version;
|
|
19
|
+
export const NAME = packageJson.name;
|
|
20
|
+
export const DESCRIPTION = packageJson.description;
|