@retrotech71/appleii-agent 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/LICENSE +21 -0
- package/README.md +164 -0
- package/package.json +48 -0
- package/src/http-server.js +432 -0
- package/src/index.js +53 -0
- package/src/logger.js +75 -0
- package/src/mcp-server.js +91 -0
- package/src/tools/emma-command.js +55 -0
- package/src/tools/focus-window.js +45 -0
- package/src/tools/get-state.js +34 -0
- package/src/tools/hide-window.js +45 -0
- package/src/tools/index.js +38 -0
- package/src/tools/load-disk-image.js +98 -0
- package/src/tools/load-file.js +82 -0
- package/src/tools/load-smartport-image.js +98 -0
- package/src/tools/save-asm-file.js +107 -0
- package/src/tools/save-basic-file.js +107 -0
- package/src/tools/save-disk-file.js +85 -0
- package/src/tools/server-control.js +52 -0
- package/src/tools/set-debug.js +33 -0
- package/src/tools/set-https.js +34 -0
- package/src/tools/show-window.js +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mike Daley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @appleii/mcp-agent
|
|
2
|
+
|
|
3
|
+
MCP server for the [Apple //e browser emulator](https://github.com/mikedaley/web-a2e) — control the emulator with AI agents via the AG-UI protocol.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
This MCP (Model Context Protocol) server bridges AI agents like Claude with the Apple //e browser emulator. Through natural language, agents can:
|
|
8
|
+
|
|
9
|
+
- **Manage windows** — show, hide, and focus emulator windows (BASIC editor, CPU debugger, disk drives, etc.)
|
|
10
|
+
- **Load disk images** — insert floppy disks and SmartPort hard drive images from the filesystem
|
|
11
|
+
- **Write BASIC programs** — read, edit, and load Applesoft BASIC programs into the emulator
|
|
12
|
+
- **Assemble code** — write 65C02 assembly, assemble, and load into memory
|
|
13
|
+
- **Control the emulator** — power on/off, reset, type text, manage expansion slots
|
|
14
|
+
- **Inspect state** — read memory, CPU registers, and emulator status
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- Node.js 18+
|
|
19
|
+
- The [Apple //e emulator](https://github.com/mikedaley/web-a2e) running in your browser
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### From npm
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g @appleii/mcp-agent
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### From source
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/mikedaley/appleii-agent.git
|
|
33
|
+
cd appleii-agent
|
|
34
|
+
npm install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
Add to your MCP client configuration. For Claude Code, edit `~/.claude/mcp.json`:
|
|
40
|
+
|
|
41
|
+
### If installed globally
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"appleii-agent": {
|
|
47
|
+
"command": "appleii-agent"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### If installed from source
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"appleii-agent": {
|
|
59
|
+
"command": "node",
|
|
60
|
+
"args": ["/path/to/appleii-agent/src/index.js"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
1. Start the Apple //e emulator in your browser (`npm run dev` in the emulator repo)
|
|
69
|
+
2. Open your MCP client (e.g., Claude Code)
|
|
70
|
+
3. The MCP server starts automatically when the client connects
|
|
71
|
+
4. Click the sparkle icon in the emulator toolbar to verify the connection (yellow = connected)
|
|
72
|
+
|
|
73
|
+
### Example Prompts
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Show the CPU debugger window
|
|
77
|
+
Load ~/Documents/ProDOS_2_4_2.dsk into drive 1
|
|
78
|
+
Write a BASIC program that draws a sine wave
|
|
79
|
+
Install the SmartPort card in slot 7
|
|
80
|
+
Load ~/Images/Total_Replay.hdv into SmartPort device 1
|
|
81
|
+
Turn on the emulator and boot from disk
|
|
82
|
+
Save 256 bytes from memory address $0800 to ~/output.bin
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Available Tools
|
|
86
|
+
|
|
87
|
+
### Emulator Control
|
|
88
|
+
| Tool | Description |
|
|
89
|
+
|------|-------------|
|
|
90
|
+
| `emma_command` | Generic command wrapper — routes to all frontend tools |
|
|
91
|
+
| `showWindow` | Show and bring a window to front |
|
|
92
|
+
| `hideWindow` | Hide a window |
|
|
93
|
+
| `focusWindow` | Bring a window to front |
|
|
94
|
+
|
|
95
|
+
### File Operations
|
|
96
|
+
| Tool | Description |
|
|
97
|
+
|------|-------------|
|
|
98
|
+
| `load_disk_image` | Load a floppy disk image (.dsk, .do, .po, .nib, .woz) |
|
|
99
|
+
| `load_smartport_image` | Load a SmartPort hard drive image (.hdv, .po, .2mg) |
|
|
100
|
+
| `load_file` | Load any file (binary or text) |
|
|
101
|
+
| `save_basic_file` | Save a BASIC program to a .bas file |
|
|
102
|
+
| `save_asm_file` | Save assembly source to a .s or .asm file |
|
|
103
|
+
| `save_disk_file` | Save binary disk data to a file |
|
|
104
|
+
|
|
105
|
+
### Server Management
|
|
106
|
+
| Tool | Description |
|
|
107
|
+
|------|-------------|
|
|
108
|
+
| `server_control` | Start, stop, restart, or check server status |
|
|
109
|
+
| `set_https` | Toggle HTTPS mode |
|
|
110
|
+
| `set_debug` | Toggle debug logging |
|
|
111
|
+
| `get_state` | Get current server state |
|
|
112
|
+
|
|
113
|
+
## Environment Variables
|
|
114
|
+
|
|
115
|
+
| Variable | Default | Description |
|
|
116
|
+
|----------|---------|-------------|
|
|
117
|
+
| `PORT` | `3033` | HTTP server port |
|
|
118
|
+
| `HTTPS` | `false` | Set to `true` for HTTPS mode |
|
|
119
|
+
|
|
120
|
+
## How It Works
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Claude Code ──MCP (stdio)──> MCP Server ──HTTP/SSE──> Browser Emulator
|
|
124
|
+
│
|
|
125
|
+
AG-UI Protocol
|
|
126
|
+
(event-based, bidirectional)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The MCP server communicates with Claude Code over stdio (standard MCP transport) and with the browser emulator over HTTP using Server-Sent Events (AG-UI protocol). Tool calls from the AI agent are forwarded to the emulator frontend, which executes them and returns results.
|
|
130
|
+
|
|
131
|
+
## HTTPS Mode
|
|
132
|
+
|
|
133
|
+
For HTTPS support:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Generate self-signed certificate
|
|
137
|
+
npm run generate-cert
|
|
138
|
+
|
|
139
|
+
# Start with HTTPS
|
|
140
|
+
HTTPS=true npm start
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Or toggle at runtime via the `set_https` tool.
|
|
144
|
+
|
|
145
|
+
## Troubleshooting
|
|
146
|
+
|
|
147
|
+
**MCP server won't connect**
|
|
148
|
+
- Ensure Node.js 18+ is installed
|
|
149
|
+
- Check that the path in your MCP config is correct
|
|
150
|
+
- Restart your MCP client
|
|
151
|
+
|
|
152
|
+
**Emulator shows disconnected (gray sparkle)**
|
|
153
|
+
- Make sure the emulator is running in your browser
|
|
154
|
+
- Click the sparkle icon to view connection details
|
|
155
|
+
- Check that port 3033 is not in use by another process
|
|
156
|
+
|
|
157
|
+
**Tools return errors**
|
|
158
|
+
- The emulator must be powered on for most tools to work
|
|
159
|
+
- SmartPort tools require the SmartPort card to be installed in an expansion slot
|
|
160
|
+
- Disk tools validate file formats before loading
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@retrotech71/appleii-agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for the Apple //e browser emulator — control the emulator and integrate with AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"appleii-agent": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node src/index.js",
|
|
20
|
+
"start:https": "HTTPS=true node src/index.js",
|
|
21
|
+
"generate-cert": "openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' -keyout key.pem -out cert.pem -days 365"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"ag-ui",
|
|
27
|
+
"apple-ii",
|
|
28
|
+
"apple-iie",
|
|
29
|
+
"emulator",
|
|
30
|
+
"retro",
|
|
31
|
+
"6502",
|
|
32
|
+
"claude",
|
|
33
|
+
"ai-agent"
|
|
34
|
+
],
|
|
35
|
+
"author": "Shawn Bullock <shawn@agenticexpert.ai>, Mike Daley <michael_daley@icloud.com>",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/mikedaley/appleii-agent.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/mikedaley/appleii-agent/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/mikedaley/appleii-agent#readme",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.0.4"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* http-server.js - HTTP/HTTPS server for AG-UI event communication
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Shawn Bullock <shawn@agenticexpert.ai>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import http from "http";
|
|
9
|
+
import https from "https";
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
import { logger } from "./logger.js";
|
|
15
|
+
import { tools } from "./tools/index.js";
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* HTTP/HTTPS Server for AG-UI protocol events
|
|
22
|
+
*/
|
|
23
|
+
export class HttpServer {
|
|
24
|
+
constructor(port, useHttps = false, debug = true) {
|
|
25
|
+
this.port = port;
|
|
26
|
+
this.useHttps = useHttps;
|
|
27
|
+
this.debug = debug;
|
|
28
|
+
this.server = null;
|
|
29
|
+
this.clients = new Set();
|
|
30
|
+
this.pendingToolResults = new Map();
|
|
31
|
+
this.eventQueue = [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate self-signed certificate for HTTPS
|
|
36
|
+
*/
|
|
37
|
+
_generateCertificate(certPath, keyPath) {
|
|
38
|
+
// Try mkcert first (locally-trusted certs), fall back to openssl (self-signed)
|
|
39
|
+
try {
|
|
40
|
+
execSync("mkcert -version", { stdio: "pipe" });
|
|
41
|
+
logger.log("Generating locally-trusted certificate with mkcert...");
|
|
42
|
+
execSync(`mkcert -key-file "${keyPath}" -cert-file "${certPath}" localhost 127.0.0.1 ::1`, { stdio: "pipe" });
|
|
43
|
+
logger.log("Certificate generated successfully (trusted by browser)");
|
|
44
|
+
return;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// mkcert not available, fall back to openssl
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.log("Generating self-signed HTTPS certificate with openssl...");
|
|
50
|
+
logger.log("Tip: Install mkcert for browser-trusted certs: brew install mkcert && mkcert -install");
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const cmd = `openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' -keyout "${keyPath}" -out "${certPath}" -days 365`;
|
|
54
|
+
execSync(cmd, { stdio: "pipe" });
|
|
55
|
+
logger.log("Certificate generated (self-signed — browser may not trust it)");
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"Failed to generate certificate. Install mkcert (recommended) or OpenSSL:\n" +
|
|
59
|
+
" mkcert: brew install mkcert && mkcert -install\n" +
|
|
60
|
+
" macOS: brew install openssl\n" +
|
|
61
|
+
" Linux: sudo apt-get install openssl\n" +
|
|
62
|
+
" Windows: https://slproweb.com/products/Win32OpenSSL.html"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Start the HTTP/HTTPS server
|
|
69
|
+
*/
|
|
70
|
+
async start() {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const requestHandler = (req, res) => {
|
|
73
|
+
this._handleRequest(req, res);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (this.useHttps) {
|
|
77
|
+
const certPath = path.join(__dirname, "cert.pem");
|
|
78
|
+
const keyPath = path.join(__dirname, "key.pem");
|
|
79
|
+
|
|
80
|
+
// Auto-generate certificates if they don't exist
|
|
81
|
+
if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
|
|
82
|
+
try {
|
|
83
|
+
this._generateCertificate(certPath, keyPath);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
reject(error);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.server = https.createServer({
|
|
91
|
+
key: fs.readFileSync(keyPath),
|
|
92
|
+
cert: fs.readFileSync(certPath),
|
|
93
|
+
}, requestHandler);
|
|
94
|
+
} else {
|
|
95
|
+
this.server = http.createServer(requestHandler);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.server.listen(this.port, () => {
|
|
99
|
+
resolve();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.server.on("error", (error) => {
|
|
103
|
+
reject(error);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle incoming HTTP requests
|
|
110
|
+
*/
|
|
111
|
+
async _handleRequest(req, res) {
|
|
112
|
+
// Log requests in debug mode, but skip heartbeat to reduce noise
|
|
113
|
+
if (this.debug && req.url !== "/heartbeat") {
|
|
114
|
+
logger.log(`[HTTP] ${req.method} ${req.url}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Enable CORS with Private Network Access (required for public HTTPS → localhost)
|
|
118
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
119
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
120
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
121
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
122
|
+
|
|
123
|
+
if (req.method === "OPTIONS") {
|
|
124
|
+
res.writeHead(204);
|
|
125
|
+
res.end();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (req.method === "GET" && req.url === "/events") {
|
|
130
|
+
// SSE endpoint for streaming events to frontend
|
|
131
|
+
this._handleEventStream(req, res);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (req.method === "POST" && req.url === "/tool-result") {
|
|
136
|
+
// Receive TOOL_CALL_RESULT from frontend
|
|
137
|
+
await this._handleToolResult(req, res);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
142
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
143
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (req.method === "GET" && req.url === "/heartbeat") {
|
|
148
|
+
// Lightweight heartbeat endpoint for checking if server is running
|
|
149
|
+
res.writeHead(200, {
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
"Access-Control-Allow-Origin": "*",
|
|
152
|
+
});
|
|
153
|
+
res.end(JSON.stringify({ alive: true }));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (req.method === "POST" && req.url === "/call-tool") {
|
|
158
|
+
// Call an MCP tool from the frontend
|
|
159
|
+
await this._handleCallTool(req, res);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
res.writeHead(404);
|
|
164
|
+
res.end("Not Found");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Handle Server-Sent Events stream
|
|
169
|
+
*/
|
|
170
|
+
_handleEventStream(req, res) {
|
|
171
|
+
if (this.debug) {
|
|
172
|
+
logger.log("[HTTP] SSE client connected");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Set SSE headers
|
|
176
|
+
res.writeHead(200, {
|
|
177
|
+
"Content-Type": "text/event-stream",
|
|
178
|
+
"Cache-Control": "no-cache",
|
|
179
|
+
"Connection": "keep-alive",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Send initial comment to establish connection
|
|
183
|
+
// This ensures the browser fires the 'onopen' event
|
|
184
|
+
res.write(": connected\n\n");
|
|
185
|
+
|
|
186
|
+
// Add client to set
|
|
187
|
+
const client = { req, res };
|
|
188
|
+
this.clients.add(client);
|
|
189
|
+
|
|
190
|
+
// Send queued events to new client
|
|
191
|
+
this.eventQueue.forEach((event) => {
|
|
192
|
+
this._writeSSE(res, event);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Handle client disconnect
|
|
196
|
+
req.on("close", () => {
|
|
197
|
+
if (this.debug) {
|
|
198
|
+
logger.log("[HTTP] SSE client disconnected");
|
|
199
|
+
}
|
|
200
|
+
this.clients.delete(client);
|
|
201
|
+
|
|
202
|
+
// Clear event queue when all clients disconnect
|
|
203
|
+
// This prevents replaying old commands to new sessions
|
|
204
|
+
if (this.clients.size === 0) {
|
|
205
|
+
this.eventQueue = [];
|
|
206
|
+
if (this.debug) {
|
|
207
|
+
logger.log("[HTTP] All clients disconnected, cleared event queue");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Handle tool result from frontend
|
|
215
|
+
*/
|
|
216
|
+
async _handleToolResult(req, res) {
|
|
217
|
+
const body = await this._readBody(req);
|
|
218
|
+
|
|
219
|
+
if (this.debug) {
|
|
220
|
+
logger.log("[HTTP] Received:", body);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const event = JSON.parse(body);
|
|
225
|
+
|
|
226
|
+
if (event.type === "TOOL_CALL_RESULT") {
|
|
227
|
+
const { tool_call_id, content } = event;
|
|
228
|
+
|
|
229
|
+
if (this.debug) {
|
|
230
|
+
logger.log(`[HTTP] Tool result for ${tool_call_id}:`, content);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Resolve pending promise
|
|
234
|
+
const pending = this.pendingToolResults.get(tool_call_id);
|
|
235
|
+
if (pending) {
|
|
236
|
+
pending.resolve(content);
|
|
237
|
+
this.pendingToolResults.delete(tool_call_id);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
242
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
243
|
+
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (this.debug) {
|
|
246
|
+
logger.log("[HTTP] Error:", error.message);
|
|
247
|
+
}
|
|
248
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
249
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Handle call tool request from frontend
|
|
255
|
+
*/
|
|
256
|
+
async _handleCallTool(req, res) {
|
|
257
|
+
const body = await this._readBody(req);
|
|
258
|
+
|
|
259
|
+
if (this.debug) {
|
|
260
|
+
logger.log("[HTTP] Call tool request:", body);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const request = JSON.parse(body);
|
|
265
|
+
const { tool: toolName, args = {} } = request;
|
|
266
|
+
|
|
267
|
+
if (!toolName) {
|
|
268
|
+
throw new Error("tool parameter is required");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Find the tool
|
|
272
|
+
const toolModule = tools.find(t => t.tool.name === toolName);
|
|
273
|
+
if (!toolModule) {
|
|
274
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Call the tool handler
|
|
278
|
+
const result = await toolModule.handler(args, this);
|
|
279
|
+
|
|
280
|
+
if (this.debug) {
|
|
281
|
+
logger.log(`[HTTP] Tool ${toolName} result:`, JSON.stringify(result));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
285
|
+
res.end(JSON.stringify(result));
|
|
286
|
+
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (this.debug) {
|
|
289
|
+
logger.log("[HTTP] Error:", error.message);
|
|
290
|
+
}
|
|
291
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
292
|
+
res.end(JSON.stringify({
|
|
293
|
+
success: false,
|
|
294
|
+
error: error.message
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Read request body
|
|
301
|
+
*/
|
|
302
|
+
_readBody(req) {
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
let body = "";
|
|
305
|
+
req.on("data", (chunk) => {
|
|
306
|
+
body += chunk.toString();
|
|
307
|
+
});
|
|
308
|
+
req.on("end", () => {
|
|
309
|
+
resolve(body);
|
|
310
|
+
});
|
|
311
|
+
req.on("error", (error) => {
|
|
312
|
+
reject(error);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Send AG-UI event to all connected clients
|
|
319
|
+
*/
|
|
320
|
+
async sendEvent(event) {
|
|
321
|
+
if (this.debug) {
|
|
322
|
+
logger.log("[HTTP] Sending event:", JSON.stringify(event));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Add to queue (keep last 100 events for reconnecting clients)
|
|
326
|
+
this.eventQueue.push(event);
|
|
327
|
+
if (this.eventQueue.length > 100) {
|
|
328
|
+
this.eventQueue.shift();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Send to all connected clients
|
|
332
|
+
this.clients.forEach((client) => {
|
|
333
|
+
this._writeSSE(client.res, event);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Write Server-Sent Event
|
|
339
|
+
*/
|
|
340
|
+
_writeSSE(res, event) {
|
|
341
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Wait for tool result from frontend
|
|
346
|
+
*/
|
|
347
|
+
waitForToolResult(toolCallId, timeoutMs = 5000) {
|
|
348
|
+
return new Promise((resolve, reject) => {
|
|
349
|
+
const timeout = setTimeout(() => {
|
|
350
|
+
this.pendingToolResults.delete(toolCallId);
|
|
351
|
+
reject(new Error("Tool result timeout"));
|
|
352
|
+
}, timeoutMs);
|
|
353
|
+
|
|
354
|
+
this.pendingToolResults.set(toolCallId, {
|
|
355
|
+
resolve: (result) => {
|
|
356
|
+
clearTimeout(timeout);
|
|
357
|
+
resolve(result);
|
|
358
|
+
},
|
|
359
|
+
reject,
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Stop the HTTP/HTTPS server
|
|
366
|
+
*/
|
|
367
|
+
async stop() {
|
|
368
|
+
if (this.server) {
|
|
369
|
+
// Close all SSE connections
|
|
370
|
+
this.clients.forEach((client) => {
|
|
371
|
+
client.res.end();
|
|
372
|
+
});
|
|
373
|
+
this.clients.clear();
|
|
374
|
+
|
|
375
|
+
return new Promise((resolve) => {
|
|
376
|
+
this.server.close(() => {
|
|
377
|
+
this.server = null;
|
|
378
|
+
resolve();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Restart the HTTP/HTTPS server
|
|
386
|
+
*/
|
|
387
|
+
async restart() {
|
|
388
|
+
await this.stop();
|
|
389
|
+
await this.start();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Change HTTPS mode and restart
|
|
394
|
+
*/
|
|
395
|
+
async setHttps(enabled) {
|
|
396
|
+
const wasRunning = this.server !== null;
|
|
397
|
+
if (wasRunning) {
|
|
398
|
+
await this.stop();
|
|
399
|
+
}
|
|
400
|
+
this.useHttps = enabled;
|
|
401
|
+
if (wasRunning) {
|
|
402
|
+
await this.start();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Set debug mode
|
|
408
|
+
*/
|
|
409
|
+
setDebug(enabled) {
|
|
410
|
+
this.debug = enabled;
|
|
411
|
+
if (this.debug) {
|
|
412
|
+
logger.log("[HTTP] Debug mode enabled");
|
|
413
|
+
} else {
|
|
414
|
+
logger.log("[HTTP] Debug mode disabled");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get server status
|
|
420
|
+
*/
|
|
421
|
+
getStatus() {
|
|
422
|
+
return {
|
|
423
|
+
running: this.server !== null,
|
|
424
|
+
https: this.useHttps,
|
|
425
|
+
debug: this.debug,
|
|
426
|
+
port: this.port,
|
|
427
|
+
clients: this.clients.size,
|
|
428
|
+
protocol: this.useHttps ? "https" : "http",
|
|
429
|
+
url: `${this.useHttps ? "https" : "http"}://localhost:${this.port}`,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|