@aliceshimada/mica 1.0.2 → 1.0.4
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/CHANGELOG.md +11 -0
- package/README.md +23 -25
- package/README.zh-CN.md +6 -8
- package/dist/src/bun/httpServer.js +1 -1
- package/dist/src/bun/index.js +1 -1
- package/dist/src/cli/index.js +3 -53
- package/dist/src/cli/status.js +6 -6
- package/dist/src/mcp/prompts.js +1 -1
- package/package.json +8 -3
- package/paclet/Kernel/MMAAgentBridge.wl +1 -1
- package/scripts/install.js +1 -1
- package/dist/src/bridge/httpBridge.js +0 -366
- package/dist/src/bridge/requestQueue.js +0 -200
- package/dist/src/cli/stop.js +0 -36
- package/dist/src/index.js +0 -54
- package/dist/src/mcp/tools.js +0 -161
- package/dist/src/runtimeOptions.js +0 -3
- package/dist/src/types.js +0 -2
- package/src/bun/index.ts +0 -120
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.4 - 2026-06-06
|
|
4
|
+
|
|
5
|
+
- Remove legacy dead code from npm package (bridge, legacy tools, stop command, runtimeOptions, types).
|
|
6
|
+
- Tighten `files` field to only include active runtime directories.
|
|
7
|
+
|
|
8
|
+
## 1.0.3 - 2026-06-06
|
|
9
|
+
|
|
10
|
+
- Deprecate `mica start`, `mica stop`, and `mica restart` commands.
|
|
11
|
+
- `mica start` and bare `mica` (no args) are now aliases for `mica mcp`.
|
|
12
|
+
- `mica mcp` starts an MCP stdio server, proxying to an existing bridge or starting a new one.
|
|
13
|
+
|
|
3
14
|
## 1.0.2 - 2026-06-06
|
|
4
15
|
|
|
5
16
|
- Add `mica mcp` command: starts an MCP stdio server that proxies to an existing bridge or starts a new one.
|
package/README.md
CHANGED
|
@@ -77,34 +77,32 @@ mica install
|
|
|
77
77
|
Then fully quit and restart Wolfram Desktop. Open a notebook, start the MCP server, and connect your MCP client:
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
|
-
mica
|
|
80
|
+
mica mcp
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
Or from a release checkout:
|
|
83
|
+
Or from a release checkout:
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
86
|
git clone https://github.com/Alice-Shimada/mica.git
|
|
87
87
|
cd mica
|
|
88
|
-
npm ci
|
|
89
|
-
npm run build
|
|
90
|
-
node dist/src/cli/index.js install
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Then fully quit and restart Wolfram Desktop. Open a notebook, start the MCP server, and connect your MCP client:
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
node dist/src/cli/index.js
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
If MICA is installed on your `PATH`, the same release commands are:
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
mica install
|
|
103
|
-
mica
|
|
104
|
-
mica doctor
|
|
105
|
-
mica status
|
|
106
|
-
mica stop
|
|
107
|
-
mica restart
|
|
88
|
+
npm ci
|
|
89
|
+
npm run build
|
|
90
|
+
node dist/src/cli/index.js install
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Then fully quit and restart Wolfram Desktop. Open a notebook, start the MCP server, and connect your MCP client:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
node dist/src/cli/index.js mcp
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If MICA is installed on your `PATH`, the same release commands are:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
mica install
|
|
103
|
+
mica mcp
|
|
104
|
+
mica doctor
|
|
105
|
+
mica status
|
|
108
106
|
```
|
|
109
107
|
|
|
110
108
|
Dashboard:
|
|
@@ -124,7 +122,7 @@ node dist/src/cli/index.js install --dry-run
|
|
|
124
122
|
node dist/src/cli/index.js uninstall
|
|
125
123
|
```
|
|
126
124
|
|
|
127
|
-
`mica status` prints the current session file, server URL, version, PID, live agent/notebook counts, and the token-bearing dashboard URL. If a server is already running, `mica
|
|
125
|
+
`mica status` prints the current session file, server URL, version, PID, live agent/notebook counts, and the token-bearing dashboard URL. If a server is already running, `mica mcp` proxies to it instead of failing with a port-in-use error, so you can recover the dashboard token at any time.
|
|
128
126
|
|
|
129
127
|
The legacy installer entry remains available for compatibility: `node scripts/install.js --dry-run`.
|
|
130
128
|
|
|
@@ -302,9 +300,9 @@ The doctor checks Node version, package build, session file, auth token, server
|
|
|
302
300
|
|
|
303
301
|
| Doctor output | Likely cause | Action |
|
|
304
302
|
| --- | --- | --- |
|
|
305
|
-
| `FAIL Session file` | Server never started | `mica
|
|
303
|
+
| `FAIL Session file` | Server never started | `mica mcp` |
|
|
306
304
|
| `FAIL Auth token` | Token mismatch or expired | Restart the server |
|
|
307
|
-
| `FAIL Server /status reachable` | Server not running | `mica
|
|
305
|
+
| `FAIL Server /status reachable` | Server not running | `mica mcp` |
|
|
308
306
|
| `FAIL Live agent count: 0` | Wolfram not running or bridge not loaded | Restart Wolfram Desktop after install |
|
|
309
307
|
| `FAIL Live notebook count: 0` | No notebook open or registered | Open a notebook in Wolfram Desktop |
|
|
310
308
|
| `FAIL Kernel/init.m` | Installer not run | `mica install` |
|
package/README.zh-CN.md
CHANGED
|
@@ -77,7 +77,7 @@ mica install
|
|
|
77
77
|
然后完全退出并重启 Wolfram Desktop。打开一个 Notebook,启动 MCP server,并连接你的 MCP client:
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
|
-
mica
|
|
80
|
+
mica mcp
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
或者从发布版 checkout 开始:
|
|
@@ -93,18 +93,16 @@ node dist/src/cli/index.js install
|
|
|
93
93
|
然后完全退出并重启 Wolfram Desktop。打开一个 Notebook,启动 MCP server,并连接你的 MCP client:
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
|
-
node dist/src/cli/index.js
|
|
96
|
+
node dist/src/cli/index.js mcp
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
如果 MICA 已经安装在你的 `PATH` 中,也可以使用等价的发布版命令:
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
102
|
mica install
|
|
103
|
-
mica
|
|
103
|
+
mica mcp
|
|
104
104
|
mica doctor
|
|
105
105
|
mica status
|
|
106
|
-
mica stop
|
|
107
|
-
mica restart
|
|
108
106
|
```
|
|
109
107
|
|
|
110
108
|
Dashboard:
|
|
@@ -124,7 +122,7 @@ node dist/src/cli/index.js install --dry-run
|
|
|
124
122
|
node dist/src/cli/index.js uninstall
|
|
125
123
|
```
|
|
126
124
|
|
|
127
|
-
`mica status` 会打印当前 session file、server URL、version、PID、live agent/notebook 数量,以及带 token 的 dashboard URL。如果 server 已经在运行,`mica
|
|
125
|
+
`mica status` 会打印当前 session file、server URL、version、PID、live agent/notebook 数量,以及带 token 的 dashboard URL。如果 server 已经在运行,`mica mcp` 会代理到已有后端,而不是因为端口占用直接失败;因此你随时可以用它找回 dashboard token。
|
|
128
126
|
|
|
129
127
|
兼容用的 legacy 安装入口仍然可用:`node scripts/install.js --dry-run`。
|
|
130
128
|
|
|
@@ -302,9 +300,9 @@ Doctor 会检查 Node 版本、package build、session file、auth token、serve
|
|
|
302
300
|
|
|
303
301
|
| Doctor output | 可能原因 | 操作 |
|
|
304
302
|
| --- | --- | --- |
|
|
305
|
-
| `FAIL Session file` | Server 尚未启动 | `mica
|
|
303
|
+
| `FAIL Session file` | Server 尚未启动 | `mica mcp` |
|
|
306
304
|
| `FAIL Auth token` | Token 不匹配或已过期 | 重启 server |
|
|
307
|
-
| `FAIL Server /status reachable` | Server 未运行 | `mica
|
|
305
|
+
| `FAIL Server /status reachable` | Server 未运行 | `mica mcp` |
|
|
308
306
|
| `FAIL Live agent count: 0` | Wolfram 未运行或 bridge 未加载 | 安装后重启 Wolfram Desktop |
|
|
309
307
|
| `FAIL Live notebook count: 0` | 没有打开或注册的 Notebook | 在 Wolfram Desktop 中打开 Notebook |
|
|
310
308
|
| `FAIL Kernel/init.m` | 尚未运行安装器 | `mica install` |
|
|
@@ -3,7 +3,7 @@ import http from "node:http";
|
|
|
3
3
|
import { executeBackendMcpTool } from "../mcp/backendTools.js";
|
|
4
4
|
import { renderDashboard } from "./dashboard.js";
|
|
5
5
|
const JSON_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
6
|
-
const DEFAULT_VERSION = "1.0.
|
|
6
|
+
const DEFAULT_VERSION = "1.0.4";
|
|
7
7
|
export async function createBunHttpApp({ state, host = "127.0.0.1", port, authToken, version = DEFAULT_VERSION }) {
|
|
8
8
|
const runtimeInfo = {
|
|
9
9
|
host,
|
package/dist/src/bun/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { loadRuntimeConfig } from "../runtime/config.js";
|
|
|
8
8
|
import { writeSessionFile } from "../runtime/session.js";
|
|
9
9
|
import { createBunHttpApp } from "./httpServer.js";
|
|
10
10
|
const MCP_SERVER_NAME = "mica-bun";
|
|
11
|
-
const MICA_PACKAGE_VERSION = "1.0.
|
|
11
|
+
const MICA_PACKAGE_VERSION = "1.0.4";
|
|
12
12
|
export async function startBunRuntime(deps = {}) {
|
|
13
13
|
const config = deps.runtimeConfig ?? loadRuntimeConfig();
|
|
14
14
|
const bridgeOnly = deps.bridgeOnly ?? config.bridgeOnly;
|
package/dist/src/cli/index.js
CHANGED
|
@@ -10,7 +10,6 @@ import { defaultSessionFile } from "../runtime/config.js";
|
|
|
10
10
|
import { runConfigCommand } from "./configSnippets.js";
|
|
11
11
|
import { runDoctor } from "./doctor.js";
|
|
12
12
|
import { runStatusCommand } from "./status.js";
|
|
13
|
-
import { runStopCommand } from "./stop.js";
|
|
14
13
|
// ---------------------------------------------------------------------------
|
|
15
14
|
// Helpers
|
|
16
15
|
// ---------------------------------------------------------------------------
|
|
@@ -82,10 +81,7 @@ export function helpText() {
|
|
|
82
81
|
return `Usage: mica <command> [options]
|
|
83
82
|
|
|
84
83
|
Commands:
|
|
85
|
-
|
|
86
|
-
stop Stop the running MICA bridge runtime
|
|
87
|
-
restart Stop then start the MICA bridge runtime
|
|
88
|
-
mcp Start MCP stdio server; proxy to an existing bridge if possible
|
|
84
|
+
mcp Run MCP stdio server (proxy to existing bridge or launch new)
|
|
89
85
|
install [options] Install MICA bridge into Wolfram
|
|
90
86
|
uninstall [options] Uninstall MICA bridge from Wolfram
|
|
91
87
|
doctor Diagnose MICA bridge configuration
|
|
@@ -108,7 +104,6 @@ export async function runCli(argv, deps) {
|
|
|
108
104
|
const _runDoctor = deps?.runDoctor;
|
|
109
105
|
const _runStatus = deps?.runStatus;
|
|
110
106
|
const _runConfig = deps?.runConfig;
|
|
111
|
-
const _runStop = deps?.runStop;
|
|
112
107
|
const _readLiveSession = deps?.readLiveSession ?? (() => readLiveSession(argv.slice(1)));
|
|
113
108
|
const _startProxyRuntime = deps?.startProxyRuntime ?? ((session) => startProxyMcpRuntime(session));
|
|
114
109
|
const _sleep = deps?.sleep ?? sleep;
|
|
@@ -138,25 +133,8 @@ export async function runCli(argv, deps) {
|
|
|
138
133
|
stdout.write(output);
|
|
139
134
|
return 0;
|
|
140
135
|
}
|
|
141
|
-
// start or no args
|
|
142
|
-
if (command === "start" || command === undefined) {
|
|
143
|
-
if (_runStatus) {
|
|
144
|
-
const status = await _runStatus();
|
|
145
|
-
if (status.running) {
|
|
146
|
-
stdout.write(status.output);
|
|
147
|
-
return status.exitCode;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
if (!startRuntime) {
|
|
151
|
-
stderr.write("Error: startRuntime not available\n");
|
|
152
|
-
return 1;
|
|
153
|
-
}
|
|
154
|
-
const runtime = await startRuntime();
|
|
155
|
-
await runtime.keepAlive;
|
|
156
|
-
return 0;
|
|
157
|
-
}
|
|
158
|
-
// mcp
|
|
159
|
-
if (command === "mcp") {
|
|
136
|
+
// mcp (also start, or no args)
|
|
137
|
+
if (command === "mcp" || command === "start" || command === undefined) {
|
|
160
138
|
const existingSession = await _readLiveSession();
|
|
161
139
|
if (existingSession) {
|
|
162
140
|
const proxyRuntime = await _startProxyRuntime(existingSession);
|
|
@@ -219,33 +197,6 @@ export async function runCli(argv, deps) {
|
|
|
219
197
|
stdout.write(output);
|
|
220
198
|
return exitCode;
|
|
221
199
|
}
|
|
222
|
-
// stop
|
|
223
|
-
if (command === "stop") {
|
|
224
|
-
if (!_runStop) {
|
|
225
|
-
stderr.write("Error: runStop not available\n");
|
|
226
|
-
return 1;
|
|
227
|
-
}
|
|
228
|
-
const { exitCode, output } = await _runStop();
|
|
229
|
-
stdout.write(output);
|
|
230
|
-
return exitCode;
|
|
231
|
-
}
|
|
232
|
-
// restart
|
|
233
|
-
if (command === "restart") {
|
|
234
|
-
if (!_runStop) {
|
|
235
|
-
stderr.write("Error: runStop not available\n");
|
|
236
|
-
return 1;
|
|
237
|
-
}
|
|
238
|
-
if (!startRuntime) {
|
|
239
|
-
stderr.write("Error: startRuntime not available\n");
|
|
240
|
-
return 1;
|
|
241
|
-
}
|
|
242
|
-
const stopResult = await _runStop();
|
|
243
|
-
if (stopResult.output)
|
|
244
|
-
stdout.write(stopResult.output);
|
|
245
|
-
const runtime = await startRuntime();
|
|
246
|
-
await runtime.keepAlive;
|
|
247
|
-
return 0;
|
|
248
|
-
}
|
|
249
200
|
// Unknown command
|
|
250
201
|
stderr.write(`Unknown command: ${command}\n`);
|
|
251
202
|
return 1;
|
|
@@ -264,7 +215,6 @@ async function main() {
|
|
|
264
215
|
runInstaller,
|
|
265
216
|
runStatus: async () => runStatusCommand(),
|
|
266
217
|
runConfig: runConfigCommand,
|
|
267
|
-
runStop: async () => runStopCommand(),
|
|
268
218
|
runDoctor: async () => runDoctor({
|
|
269
219
|
projectRoot,
|
|
270
220
|
detectWolframUserBase: () => detectWolframUserBase(),
|
package/dist/src/cli/status.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function runStatusCommand(deps = {}) {
|
|
|
14
14
|
const ok = (label, detail) => lines.push(`OK ${label}: ${detail}`);
|
|
15
15
|
if (!_exists(sessionFile)) {
|
|
16
16
|
fail("Session file", `${sessionFile} (not found)`);
|
|
17
|
-
lines.push("FIX Run: mica
|
|
17
|
+
lines.push("FIX Run: mica mcp");
|
|
18
18
|
return result(1, lines, false);
|
|
19
19
|
}
|
|
20
20
|
let session;
|
|
@@ -23,7 +23,7 @@ export async function runStatusCommand(deps = {}) {
|
|
|
23
23
|
}
|
|
24
24
|
catch (error) {
|
|
25
25
|
fail("Session file", error instanceof Error ? error.message : String(error));
|
|
26
|
-
lines.push("FIX Run: mica
|
|
26
|
+
lines.push("FIX Run: mica mcp");
|
|
27
27
|
return result(1, lines, false);
|
|
28
28
|
}
|
|
29
29
|
const baseUrl = session.baseUrl ?? `http://${session.host ?? "127.0.0.1"}:${session.port ?? 19791}`;
|
|
@@ -31,7 +31,7 @@ export async function runStatusCommand(deps = {}) {
|
|
|
31
31
|
ok("Session target", baseUrl);
|
|
32
32
|
if (!session.authToken) {
|
|
33
33
|
fail("Auth token", "missing in session file");
|
|
34
|
-
lines.push("FIX Restart MICA with: mica
|
|
34
|
+
lines.push("FIX Restart MICA with: mica mcp");
|
|
35
35
|
return result(1, lines, false);
|
|
36
36
|
}
|
|
37
37
|
if (!_fetch) {
|
|
@@ -44,12 +44,12 @@ export async function runStatusCommand(deps = {}) {
|
|
|
44
44
|
});
|
|
45
45
|
if (response.status === 401) {
|
|
46
46
|
fail("Auth token", "401 Unauthorized");
|
|
47
|
-
lines.push("FIX Restart MICA with: mica
|
|
47
|
+
lines.push("FIX Restart MICA with: mica mcp");
|
|
48
48
|
return result(1, lines, false);
|
|
49
49
|
}
|
|
50
50
|
if (response.status !== 200) {
|
|
51
51
|
fail("Server /status reachable", `HTTP ${response.status}`);
|
|
52
|
-
lines.push("FIX Run: mica
|
|
52
|
+
lines.push("FIX Run: mica mcp");
|
|
53
53
|
return result(1, lines, false);
|
|
54
54
|
}
|
|
55
55
|
const body = (await response.json());
|
|
@@ -67,7 +67,7 @@ export async function runStatusCommand(deps = {}) {
|
|
|
67
67
|
}
|
|
68
68
|
catch (error) {
|
|
69
69
|
fail("Server /status reachable", error instanceof Error ? error.message : String(error));
|
|
70
|
-
lines.push("FIX Run: mica
|
|
70
|
+
lines.push("FIX Run: mica mcp");
|
|
71
71
|
return result(1, lines, false);
|
|
72
72
|
}
|
|
73
73
|
}
|
package/dist/src/mcp/prompts.js
CHANGED
|
@@ -33,7 +33,7 @@ export const MICA_AGENT_INSTRUCTIONS = [
|
|
|
33
33
|
"Tools:",
|
|
34
34
|
...TOOL_GUIDE.map(([name, description]) => `- ${name}: ${description}`),
|
|
35
35
|
].join("\n");
|
|
36
|
-
export function createMicaMcpServer(name, version = "1.0.
|
|
36
|
+
export function createMicaMcpServer(name, version = "1.0.4") {
|
|
37
37
|
return new McpServer({ name, version }, { instructions: MICA_AGENT_INSTRUCTIONS });
|
|
38
38
|
}
|
|
39
39
|
export function registerMicaPrompts(server) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliceshimada/mica",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Local MCP bridge for controlling live Wolfram Desktop / Mathematica notebooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -22,8 +22,13 @@
|
|
|
22
22
|
"mica": "dist/src/cli/index.js"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"dist/src",
|
|
26
|
-
"src/bun
|
|
25
|
+
"dist/src/backend",
|
|
26
|
+
"dist/src/bun",
|
|
27
|
+
"dist/src/cli",
|
|
28
|
+
"!dist/src/cli/stop.js",
|
|
29
|
+
"dist/src/mcp",
|
|
30
|
+
"!dist/src/mcp/tools.js",
|
|
31
|
+
"dist/src/runtime",
|
|
27
32
|
"paclet",
|
|
28
33
|
"README.md",
|
|
29
34
|
"README.zh-CN.md",
|
|
@@ -50,7 +50,7 @@ $PollingInProgress = False;
|
|
|
50
50
|
$MaxArtifactScanCells = 20;
|
|
51
51
|
$DefaultMaxCellPayloadBytes = 262144;
|
|
52
52
|
$MaxCellPayloadBytes = 1024 * 1024;
|
|
53
|
-
$BridgeHTTPTimeoutSeconds =
|
|
53
|
+
$BridgeHTTPTimeoutSeconds = 30;
|
|
54
54
|
$BridgeHTTPRetryCount = 3;
|
|
55
55
|
$BridgeHTTPRetryDelaySeconds = 0.25;
|
|
56
56
|
$BridgeInbox = {};
|
package/scripts/install.js
CHANGED
|
@@ -22,7 +22,7 @@ export const STANDARD_INIT_HEADER =
|
|
|
22
22
|
|
|
23
23
|
const REQUIRED_BRIDGE_FILES = [
|
|
24
24
|
"package.json",
|
|
25
|
-
path.join("src", "bun", "index.
|
|
25
|
+
path.join("dist", "src", "bun", "index.js"),
|
|
26
26
|
path.join("paclet", "Kernel", "MMAAgentBridge.wl"),
|
|
27
27
|
path.join("paclet", "PacletInfo.wl"),
|
|
28
28
|
];
|
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
import http from "node:http";
|
|
2
|
-
import { DEFAULT_BRIDGE_HOST, DEFAULT_BRIDGE_PORT } from "../types.js";
|
|
3
|
-
const DEFAULT_PALETTE_STALE_TIMEOUT_MS = 30_000;
|
|
4
|
-
export class HttpBridge {
|
|
5
|
-
queue;
|
|
6
|
-
options;
|
|
7
|
-
server;
|
|
8
|
-
attachedNotebook;
|
|
9
|
-
permissions;
|
|
10
|
-
notebooks = new Map();
|
|
11
|
-
activeNotebookId;
|
|
12
|
-
/** Timestamp (ms) of the last Palette heartbeat. 0 = never connected. */
|
|
13
|
-
lastPaletteHeartbeat = 0;
|
|
14
|
-
paletteStaleTimeoutMs;
|
|
15
|
-
static MAX_BODY_BYTES = 1024 * 1024; // 1 MiB
|
|
16
|
-
constructor(queue, options = {}) {
|
|
17
|
-
this.queue = queue;
|
|
18
|
-
this.options = options;
|
|
19
|
-
this.paletteStaleTimeoutMs =
|
|
20
|
-
options.paletteStaleTimeoutMs ?? DEFAULT_PALETTE_STALE_TIMEOUT_MS;
|
|
21
|
-
this.server = http.createServer((request, response) => {
|
|
22
|
-
this.handle(request, response);
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
get port() {
|
|
26
|
-
const address = this.server.address();
|
|
27
|
-
return address?.port ?? this.options.port ?? DEFAULT_BRIDGE_PORT;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Return a snapshot of the current bridge status.
|
|
31
|
-
*
|
|
32
|
-
* Exposed so the MCP `mma_status` tool can report real Palette connection
|
|
33
|
-
* and notebook attachment state instead of hardcoded false values.
|
|
34
|
-
*/
|
|
35
|
-
statusSnapshot() {
|
|
36
|
-
return this.status();
|
|
37
|
-
}
|
|
38
|
-
async start() {
|
|
39
|
-
const host = this.options.host ?? DEFAULT_BRIDGE_HOST;
|
|
40
|
-
const port = this.options.port ?? DEFAULT_BRIDGE_PORT;
|
|
41
|
-
await new Promise((resolve) => this.server.listen(port, host, resolve));
|
|
42
|
-
}
|
|
43
|
-
async stop() {
|
|
44
|
-
// Close the HTTP server first to prevent new interactions, then drain
|
|
45
|
-
// the queue so pending MCP promises don't hang.
|
|
46
|
-
if (this.server.listening) {
|
|
47
|
-
await new Promise((resolve, reject) => {
|
|
48
|
-
this.server.close((error) => (error ? reject(error) : resolve()));
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
this.queue.drain("HTTP bridge stopped");
|
|
52
|
-
}
|
|
53
|
-
async handle(request, response) {
|
|
54
|
-
try {
|
|
55
|
-
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
56
|
-
if (request.method === "GET" && url.pathname === "/status") {
|
|
57
|
-
this.sendJson(response, 200, this.status());
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (request.method === "POST" && url.pathname === "/attach") {
|
|
61
|
-
const body = await this.readJson(request);
|
|
62
|
-
if (typeof body !== "object" || body === null || Object.keys(body).length === 0) {
|
|
63
|
-
this.sendJson(response, 400, {
|
|
64
|
-
error: { code: "BAD_REQUEST", message: "attach body must be a non-empty JSON object" }
|
|
65
|
-
});
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const attach = body;
|
|
69
|
-
const permissions = Object.prototype.hasOwnProperty.call(attach, "permissions")
|
|
70
|
-
? this.parsePermissions(attach.permissions)
|
|
71
|
-
: undefined;
|
|
72
|
-
if (Object.prototype.hasOwnProperty.call(attach, "permissions") && !permissions) {
|
|
73
|
-
this.sendJson(response, 400, {
|
|
74
|
-
error: { code: "BAD_REQUEST", message: "permissions must include boolean values" }
|
|
75
|
-
});
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
this.attachedNotebook = permissions ? { ...attach, permissions } : attach;
|
|
79
|
-
this.permissions = permissions;
|
|
80
|
-
if (typeof attach.notebookId === "string" && attach.notebookId.length > 0) {
|
|
81
|
-
this.upsertNotebook({
|
|
82
|
-
notebookId: attach.notebookId,
|
|
83
|
-
notebookTitle: attach.notebookTitle,
|
|
84
|
-
notebookPath: attach.notebookPath,
|
|
85
|
-
wolframVersion: attach.wolframVersion,
|
|
86
|
-
platform: attach.platform,
|
|
87
|
-
permissions,
|
|
88
|
-
lastSeenAt: Date.now()
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
this.heartbeat();
|
|
92
|
-
this.sendJson(response, 200, { ok: true });
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (request.method === "POST" && url.pathname === "/notebooks/upsert") {
|
|
96
|
-
const body = await this.readJson(request);
|
|
97
|
-
if (!this.isNotebookPayload(body)) {
|
|
98
|
-
this.sendJson(response, 400, {
|
|
99
|
-
error: { code: "BAD_REQUEST", message: "notebookId is required" }
|
|
100
|
-
});
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
this.upsertNotebook({
|
|
104
|
-
notebookId: body.notebookId,
|
|
105
|
-
notebookTitle: body.notebookTitle,
|
|
106
|
-
notebookPath: body.notebookPath,
|
|
107
|
-
wolframVersion: body.wolframVersion,
|
|
108
|
-
platform: body.platform,
|
|
109
|
-
permissions: this.parsePermissions(body),
|
|
110
|
-
lastSeenAt: Date.now()
|
|
111
|
-
});
|
|
112
|
-
if (!this.activeNotebookId) {
|
|
113
|
-
this.activeNotebookId = body.notebookId;
|
|
114
|
-
}
|
|
115
|
-
this.heartbeat();
|
|
116
|
-
this.sendJson(response, 200, { ok: true, activeNotebookId: this.activeNotebookId });
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (request.method === "GET" && url.pathname === "/notebooks") {
|
|
120
|
-
this.sendJson(response, 200, {
|
|
121
|
-
notebooks: this.listNotebooks(),
|
|
122
|
-
activeNotebookId: this.activeNotebookId
|
|
123
|
-
});
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (request.method === "POST" && url.pathname === "/notebooks/select") {
|
|
127
|
-
const body = await this.readJson(request);
|
|
128
|
-
const notebookId = this.getNotebookId(body);
|
|
129
|
-
if (!notebookId) {
|
|
130
|
-
this.sendJson(response, 400, {
|
|
131
|
-
error: { code: "BAD_REQUEST", message: "notebookId is required" }
|
|
132
|
-
});
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (!this.notebooks.has(notebookId)) {
|
|
136
|
-
this.sendJson(response, 404, {
|
|
137
|
-
error: { code: "NOTEBOOK_NOT_FOUND", message: `Unknown notebookId: ${notebookId}` }
|
|
138
|
-
});
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
this.activeNotebookId = notebookId;
|
|
142
|
-
this.heartbeat();
|
|
143
|
-
this.sendJson(response, 200, { ok: true, activeNotebookId: this.activeNotebookId });
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (request.method === "GET" && url.pathname === "/poll") {
|
|
147
|
-
const activeNotebookId = url.searchParams.get("activeNotebookId");
|
|
148
|
-
if (activeNotebookId && this.notebooks.has(activeNotebookId)) {
|
|
149
|
-
this.activeNotebookId = activeNotebookId;
|
|
150
|
-
}
|
|
151
|
-
this.heartbeat();
|
|
152
|
-
const requestInfo = this.queue.claimNext();
|
|
153
|
-
const body = {
|
|
154
|
-
status: this.status(),
|
|
155
|
-
cancelRequests: this.queue.listCancellations(),
|
|
156
|
-
request: requestInfo
|
|
157
|
-
};
|
|
158
|
-
this.sendJson(response, 200, body);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
if (request.method === "POST" && url.pathname === "/permissions") {
|
|
162
|
-
const body = await this.readJson(request);
|
|
163
|
-
const permissions = this.parsePermissions(body);
|
|
164
|
-
if (!permissions) {
|
|
165
|
-
this.sendJson(response, 400, {
|
|
166
|
-
error: { code: "BAD_REQUEST", message: "permissions must include boolean values" }
|
|
167
|
-
});
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
this.permissions = permissions;
|
|
171
|
-
if (this.attachedNotebook) {
|
|
172
|
-
this.attachedNotebook = { ...this.attachedNotebook, permissions };
|
|
173
|
-
}
|
|
174
|
-
const attachedNotebookId = this.getAttachedNotebookId();
|
|
175
|
-
if (attachedNotebookId) {
|
|
176
|
-
this.upsertNotebook({
|
|
177
|
-
notebookId: attachedNotebookId,
|
|
178
|
-
notebookTitle: this.attachedNotebook?.notebookTitle,
|
|
179
|
-
notebookPath: this.attachedNotebook?.notebookPath,
|
|
180
|
-
wolframVersion: this.attachedNotebook?.wolframVersion,
|
|
181
|
-
platform: this.attachedNotebook?.platform,
|
|
182
|
-
permissions,
|
|
183
|
-
lastSeenAt: Date.now()
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
this.sendJson(response, 200, { ok: true });
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (request.method === "GET" && url.pathname === "/requests") {
|
|
190
|
-
this.heartbeat();
|
|
191
|
-
this.sendJson(response, 200, { request: this.queue.claimNext() });
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (request.method === "POST" && url.pathname === "/result") {
|
|
195
|
-
const body = (await this.readJson(request));
|
|
196
|
-
const accepted = body.ok
|
|
197
|
-
? this.queue.resolveSuccess(body.requestId, body.result)
|
|
198
|
-
: this.queue.resolveFailure(body.requestId, body.error.code, body.error.message);
|
|
199
|
-
this.sendJson(response, accepted ? 200 : 404, { ok: accepted });
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (request.method === "GET" && url.pathname === "/cancellations") {
|
|
203
|
-
this.sendJson(response, 200, { cancelRequests: this.queue.listCancellations() });
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
if (request.method === "POST" && url.pathname === "/cancel") {
|
|
207
|
-
const body = (await this.readJson(request));
|
|
208
|
-
if (!body.requestId) {
|
|
209
|
-
this.sendJson(response, 400, { error: { code: "BAD_REQUEST", message: "requestId is required" } });
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const accepted = this.queue.cancelFromPalette(body.requestId, body.reason ?? "USER_CANCELLED_IN_PALETTE");
|
|
213
|
-
this.sendJson(response, accepted ? 200 : 404, { ok: accepted });
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
this.sendJson(response, 404, { error: { code: "NOT_FOUND", message: `${request.method} ${url.pathname}` } });
|
|
217
|
-
}
|
|
218
|
-
catch (error) {
|
|
219
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
220
|
-
if (msg === "PAYLOAD_TOO_LARGE") {
|
|
221
|
-
this.sendJson(response, 413, { error: { code: "PAYLOAD_TOO_LARGE", message: "Request body exceeds 1 MiB limit" } });
|
|
222
|
-
}
|
|
223
|
-
else if (msg === "MALFORMED_JSON") {
|
|
224
|
-
this.sendJson(response, 400, { error: { code: "BAD_REQUEST", message: "Malformed JSON body" } });
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
this.sendJson(response, 500, { error: { code: "INTERNAL_ERROR", message: msg } });
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
status() {
|
|
232
|
-
const permissions = this.permissions ?? this.attachedNotebook?.permissions;
|
|
233
|
-
const runningRequest = this.queue.runningRequestSnapshot();
|
|
234
|
-
const notebookAttached = Boolean(this.attachedNotebook) || Boolean(this.activeNotebookId && this.notebooks.has(this.activeNotebookId));
|
|
235
|
-
return {
|
|
236
|
-
server: "running",
|
|
237
|
-
paletteConnected: this.isPaletteConnected(),
|
|
238
|
-
notebookAttached,
|
|
239
|
-
attachedNotebook: this.attachedNotebook,
|
|
240
|
-
permissions,
|
|
241
|
-
activeNotebookId: this.activeNotebookId,
|
|
242
|
-
notebooks: this.listNotebooks(),
|
|
243
|
-
transportMode: "main-kernel",
|
|
244
|
-
executorState: runningRequest ? "running" : "idle",
|
|
245
|
-
runningRequest,
|
|
246
|
-
pendingRequests: this.queue.pendingCount()
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
upsertNotebook(notebook) {
|
|
250
|
-
const existing = this.notebooks.get(notebook.notebookId);
|
|
251
|
-
this.notebooks.set(notebook.notebookId, {
|
|
252
|
-
...(existing ?? { notebookId: notebook.notebookId, lastSeenAt: notebook.lastSeenAt }),
|
|
253
|
-
...notebook
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
listNotebooks() {
|
|
257
|
-
return [...this.notebooks.values()];
|
|
258
|
-
}
|
|
259
|
-
getNotebookId(payload) {
|
|
260
|
-
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
261
|
-
return undefined;
|
|
262
|
-
}
|
|
263
|
-
const record = payload;
|
|
264
|
-
return typeof record.notebookId === "string" && record.notebookId.length > 0 ? record.notebookId : undefined;
|
|
265
|
-
}
|
|
266
|
-
isNotebookPayload(payload) {
|
|
267
|
-
return this.getNotebookId(payload) !== undefined;
|
|
268
|
-
}
|
|
269
|
-
getAttachedNotebookId() {
|
|
270
|
-
if (!this.attachedNotebook)
|
|
271
|
-
return undefined;
|
|
272
|
-
return typeof this.attachedNotebook.notebookId === "string"
|
|
273
|
-
? this.attachedNotebook.notebookId
|
|
274
|
-
: undefined;
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Record a Palette heartbeat, refreshing the connection timestamp.
|
|
278
|
-
*
|
|
279
|
-
* Called on POST /attach and GET /requests — the two endpoints the Palette
|
|
280
|
-
* uses to interact with the bridge. If the Palette stops polling, the
|
|
281
|
-
* heartbeat expires and {@link isPaletteConnected} returns false.
|
|
282
|
-
*/
|
|
283
|
-
heartbeat() {
|
|
284
|
-
this.lastPaletteHeartbeat = Date.now();
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Whether the Palette is considered connected based on heartbeat freshness.
|
|
288
|
-
*
|
|
289
|
-
* Returns true only if the Palette has sent a heartbeat within
|
|
290
|
-
* `paletteStaleTimeoutMs`. This prevents agents from enqueuing requests
|
|
291
|
-
* that will never be claimed after the Palette dies or disconnects.
|
|
292
|
-
*/
|
|
293
|
-
isPaletteConnected() {
|
|
294
|
-
if (this.lastPaletteHeartbeat === 0)
|
|
295
|
-
return false;
|
|
296
|
-
return Date.now() - this.lastPaletteHeartbeat < this.paletteStaleTimeoutMs;
|
|
297
|
-
}
|
|
298
|
-
parsePermissions(payload) {
|
|
299
|
-
const source = this.getPermissionsSource(payload);
|
|
300
|
-
if (!source)
|
|
301
|
-
return undefined;
|
|
302
|
-
const keys = [
|
|
303
|
-
"ReadNotebook",
|
|
304
|
-
"InsertCell",
|
|
305
|
-
"ModifyCell",
|
|
306
|
-
"DeleteCell",
|
|
307
|
-
"RunCell",
|
|
308
|
-
"SaveNotebook"
|
|
309
|
-
];
|
|
310
|
-
const permissions = {};
|
|
311
|
-
for (const key of keys) {
|
|
312
|
-
if (typeof source[key] !== "boolean") {
|
|
313
|
-
return undefined;
|
|
314
|
-
}
|
|
315
|
-
permissions[key] = source[key];
|
|
316
|
-
}
|
|
317
|
-
return permissions;
|
|
318
|
-
}
|
|
319
|
-
getPermissionsSource(payload) {
|
|
320
|
-
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
const record = payload;
|
|
324
|
-
if (Object.prototype.hasOwnProperty.call(record, "permissions")) {
|
|
325
|
-
const nested = record.permissions;
|
|
326
|
-
if (typeof nested !== "object" || nested === null || Array.isArray(nested)) {
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
return nested;
|
|
330
|
-
}
|
|
331
|
-
return record;
|
|
332
|
-
}
|
|
333
|
-
async readJson(request) {
|
|
334
|
-
const chunks = [];
|
|
335
|
-
let totalBytes = 0;
|
|
336
|
-
let exceeded = false;
|
|
337
|
-
for await (const chunk of request) {
|
|
338
|
-
if (exceeded)
|
|
339
|
-
continue; // drain remaining chunks
|
|
340
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
341
|
-
totalBytes += buf.length;
|
|
342
|
-
if (totalBytes > HttpBridge.MAX_BODY_BYTES) {
|
|
343
|
-
exceeded = true;
|
|
344
|
-
chunks.length = 0; // discard accumulated data
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
chunks.push(buf);
|
|
348
|
-
}
|
|
349
|
-
if (exceeded) {
|
|
350
|
-
throw new Error("PAYLOAD_TOO_LARGE");
|
|
351
|
-
}
|
|
352
|
-
const text = Buffer.concat(chunks).toString("utf8");
|
|
353
|
-
if (text.length === 0)
|
|
354
|
-
return {};
|
|
355
|
-
try {
|
|
356
|
-
return JSON.parse(text);
|
|
357
|
-
}
|
|
358
|
-
catch {
|
|
359
|
-
throw new Error("MALFORMED_JSON");
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
sendJson(response, statusCode, body) {
|
|
363
|
-
response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
|
|
364
|
-
response.end(JSON.stringify(body));
|
|
365
|
-
}
|
|
366
|
-
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Serial request queue for the MMA MCP bridge.
|
|
3
|
-
*
|
|
4
|
-
* Only one notebook operation is processed at a time. The MCP client enqueues
|
|
5
|
-
* tool calls, the Mathematica Palette polls and claims them, then posts results
|
|
6
|
-
* back. Cancellation is supported from both sides.
|
|
7
|
-
*
|
|
8
|
-
* ## Cancellation semantics
|
|
9
|
-
*
|
|
10
|
-
* **MCP-client cancellation** (`cancelFromMcp`):
|
|
11
|
-
* - Queued requests are rejected immediately and removed.
|
|
12
|
-
* - Claimed requests are rejected immediately and removed, and a one-shot
|
|
13
|
-
* cancellation notification is stored for the Palette to pick up via
|
|
14
|
-
* `listCancellations`. This ensures the MCP call terminates instead of
|
|
15
|
-
* hanging indefinitely.
|
|
16
|
-
*
|
|
17
|
-
* **Palette-originated cancellation** (`cancelFromPalette`):
|
|
18
|
-
* - The Palette already knows it is cancelling, so no notification is stored.
|
|
19
|
-
* The active call is rejected and removed immediately.
|
|
20
|
-
*/
|
|
21
|
-
export class RequestQueue {
|
|
22
|
-
counter = 0;
|
|
23
|
-
calls = new Map();
|
|
24
|
-
order = [];
|
|
25
|
-
/**
|
|
26
|
-
* One-shot cancellation notifications for the Palette.
|
|
27
|
-
* Populated by cancelFromMcp on claimed requests, drained by listCancellations.
|
|
28
|
-
*/
|
|
29
|
-
cancellations = [];
|
|
30
|
-
/**
|
|
31
|
-
* Enqueue a tool call and return only the promise.
|
|
32
|
-
*
|
|
33
|
-
* Convenience wrapper around {@link enqueueWithId} for callers that don't
|
|
34
|
-
* need the requestId (e.g. tests, simple fire-and-forget usage).
|
|
35
|
-
*/
|
|
36
|
-
enqueue(tool, args) {
|
|
37
|
-
return this.enqueueWithId(tool, args).promise;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Enqueue a tool call and return both the requestId and the promise.
|
|
41
|
-
*
|
|
42
|
-
* The requestId is needed by MCP tool handlers so they can wire an
|
|
43
|
-
* AbortSignal listener to {@link cancelFromMcp}.
|
|
44
|
-
*/
|
|
45
|
-
enqueueWithId(tool, args) {
|
|
46
|
-
const notebookId = typeof args.notebookId === "string" && args.notebookId.length > 0 ? args.notebookId : undefined;
|
|
47
|
-
const requestId = `req_${++this.counter}`;
|
|
48
|
-
const request = {
|
|
49
|
-
requestId,
|
|
50
|
-
tool,
|
|
51
|
-
arguments: args,
|
|
52
|
-
notebookId,
|
|
53
|
-
state: "queued",
|
|
54
|
-
createdAt: Date.now()
|
|
55
|
-
};
|
|
56
|
-
const promise = new Promise((resolve, reject) => {
|
|
57
|
-
this.calls.set(requestId, { request, resolve, reject });
|
|
58
|
-
this.order.push(requestId);
|
|
59
|
-
});
|
|
60
|
-
return { requestId, promise };
|
|
61
|
-
}
|
|
62
|
-
claimNext() {
|
|
63
|
-
if ([...this.calls.values()].some((call) => call.request.state === "claimed")) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
const requestId = this.order.find((id) => this.calls.get(id)?.request.state === "queued");
|
|
67
|
-
if (!requestId)
|
|
68
|
-
return null;
|
|
69
|
-
const call = this.calls.get(requestId);
|
|
70
|
-
if (!call)
|
|
71
|
-
return null;
|
|
72
|
-
call.request.state = "claimed";
|
|
73
|
-
call.request.claimedAt = Date.now();
|
|
74
|
-
return { ...call.request, arguments: { ...call.request.arguments } };
|
|
75
|
-
}
|
|
76
|
-
runningRequestSnapshot() {
|
|
77
|
-
const running = this.order
|
|
78
|
-
.map((id) => this.calls.get(id)?.request)
|
|
79
|
-
.find((request) => request !== undefined && request.state === "claimed");
|
|
80
|
-
if (!running || running.claimedAt === undefined)
|
|
81
|
-
return null;
|
|
82
|
-
return {
|
|
83
|
-
requestId: running.requestId,
|
|
84
|
-
tool: running.tool,
|
|
85
|
-
arguments: { ...running.arguments },
|
|
86
|
-
notebookId: running.notebookId,
|
|
87
|
-
state: "claimed",
|
|
88
|
-
createdAt: running.createdAt,
|
|
89
|
-
claimedAt: running.claimedAt
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
peekQueued() {
|
|
93
|
-
return this.order
|
|
94
|
-
.map((id) => this.calls.get(id)?.request)
|
|
95
|
-
.filter((request) => request !== undefined && request.state === "queued")
|
|
96
|
-
.map((request) => ({ ...request, arguments: { ...request.arguments } }));
|
|
97
|
-
}
|
|
98
|
-
pendingCount() {
|
|
99
|
-
return [...this.calls.values()].filter((call) => call.request.state === "queued" || call.request.state === "claimed").length;
|
|
100
|
-
}
|
|
101
|
-
resolveSuccess(requestId, result) {
|
|
102
|
-
const call = this.calls.get(requestId);
|
|
103
|
-
if (!call)
|
|
104
|
-
return false;
|
|
105
|
-
if (call.request.state !== "claimed")
|
|
106
|
-
return false;
|
|
107
|
-
call.request.state = "completed";
|
|
108
|
-
call.resolve(result);
|
|
109
|
-
this.remove(requestId);
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
resolveFailure(requestId, code, message) {
|
|
113
|
-
const call = this.calls.get(requestId);
|
|
114
|
-
if (!call)
|
|
115
|
-
return false;
|
|
116
|
-
if (call.request.state !== "claimed")
|
|
117
|
-
return false;
|
|
118
|
-
call.request.state = "failed";
|
|
119
|
-
call.reject(new Error(`${code}: ${message}`));
|
|
120
|
-
this.remove(requestId);
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Cancel a request from the MCP client side.
|
|
125
|
-
*
|
|
126
|
-
* - Queued: rejects the promise and removes the call immediately.
|
|
127
|
-
* - Claimed: rejects the promise, removes the call, and stores a one-shot
|
|
128
|
-
* cancellation notification for the Palette to discover via listCancellations.
|
|
129
|
-
* - Unknown/removed: returns false.
|
|
130
|
-
*/
|
|
131
|
-
cancelFromMcp(requestId, reason) {
|
|
132
|
-
const call = this.calls.get(requestId);
|
|
133
|
-
if (!call)
|
|
134
|
-
return false;
|
|
135
|
-
if (call.request.state === "queued") {
|
|
136
|
-
call.request.state = "cancelled";
|
|
137
|
-
call.reject(new Error(reason));
|
|
138
|
-
this.remove(requestId);
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
// Claimed: reject immediately so the MCP call terminates, and store a
|
|
142
|
-
// one-shot notification for the Palette to pick up.
|
|
143
|
-
call.request.state = "cancelled";
|
|
144
|
-
call.reject(new Error(reason));
|
|
145
|
-
this.cancellations.push({ requestId, reason });
|
|
146
|
-
this.remove(requestId);
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Cancel a request originating from the Palette (user action in Mathematica).
|
|
151
|
-
*
|
|
152
|
-
* Rejects the promise and removes the call immediately. No cancellation
|
|
153
|
-
* notification is stored because the Palette already knows it cancelled.
|
|
154
|
-
* Returns false if the requestId is unknown or already removed.
|
|
155
|
-
*/
|
|
156
|
-
cancelFromPalette(requestId, reason) {
|
|
157
|
-
const call = this.calls.get(requestId);
|
|
158
|
-
if (!call)
|
|
159
|
-
return false;
|
|
160
|
-
call.request.state = "cancelled";
|
|
161
|
-
call.reject(new Error(reason));
|
|
162
|
-
this.remove(requestId);
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Return pending one-shot MCP cancellation notifications and clear them.
|
|
167
|
-
*
|
|
168
|
-
* The Palette polls this endpoint to discover claimed requests that were
|
|
169
|
-
* cancelled by the MCP client. Each cancellation is reported exactly once.
|
|
170
|
-
*/
|
|
171
|
-
listCancellations() {
|
|
172
|
-
if (this.cancellations.length === 0)
|
|
173
|
-
return [];
|
|
174
|
-
const result = [...this.cancellations];
|
|
175
|
-
this.cancellations.length = 0;
|
|
176
|
-
return result;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Reject all outstanding promises and clear all internal state.
|
|
180
|
-
*
|
|
181
|
-
* Used when the HTTP bridge shuts down so pending MCP tool-call promises
|
|
182
|
-
* don't hang indefinitely. Clears active calls, FIFO order, and one-shot
|
|
183
|
-
* cancellation notifications.
|
|
184
|
-
*/
|
|
185
|
-
drain(reason) {
|
|
186
|
-
for (const call of this.calls.values()) {
|
|
187
|
-
call.request.state = "cancelled";
|
|
188
|
-
call.reject(new Error(reason));
|
|
189
|
-
}
|
|
190
|
-
this.calls.clear();
|
|
191
|
-
this.order.length = 0;
|
|
192
|
-
this.cancellations.length = 0;
|
|
193
|
-
}
|
|
194
|
-
remove(requestId) {
|
|
195
|
-
this.calls.delete(requestId);
|
|
196
|
-
const index = this.order.indexOf(requestId);
|
|
197
|
-
if (index >= 0)
|
|
198
|
-
this.order.splice(index, 1);
|
|
199
|
-
}
|
|
200
|
-
}
|
package/dist/src/cli/stop.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { defaultSessionFile } from "../runtime/config.js";
|
|
3
|
-
export async function runStopCommand(deps = {}) {
|
|
4
|
-
const env = deps.env ?? process.env;
|
|
5
|
-
const sessionFile = env.MICA_SESSION_FILE ?? defaultSessionFile(env);
|
|
6
|
-
const _exists = deps.exists ?? existsSync;
|
|
7
|
-
const _readFile = deps.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
8
|
-
const _kill = deps.kill ?? ((pid, signal) => { process.kill(pid, signal); });
|
|
9
|
-
const _unlink = deps.unlink ?? ((p) => { unlinkSync(p); });
|
|
10
|
-
if (!_exists(sessionFile)) {
|
|
11
|
-
return { exitCode: 1, output: `MICA is not running\nSession file not found: ${sessionFile}\n` };
|
|
12
|
-
}
|
|
13
|
-
let session;
|
|
14
|
-
try {
|
|
15
|
-
session = JSON.parse(_readFile(sessionFile));
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
return { exitCode: 1, output: `Cannot read session file: ${error instanceof Error ? error.message : String(error)}\n` };
|
|
19
|
-
}
|
|
20
|
-
if (typeof session.pid !== "number" || !Number.isInteger(session.pid) || session.pid <= 0) {
|
|
21
|
-
return { exitCode: 1, output: "Cannot stop MICA: session file has no valid pid\n" };
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
_kill(session.pid, "SIGTERM");
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
return { exitCode: 1, output: `Cannot stop MICA pid ${session.pid}: ${error instanceof Error ? error.message : String(error)}\n` };
|
|
28
|
-
}
|
|
29
|
-
try {
|
|
30
|
-
_unlink(sessionFile);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
// The process may have already removed or replaced it; stopping still succeeded.
|
|
34
|
-
}
|
|
35
|
-
return { exitCode: 0, output: `MICA stopped\nPID: ${session.pid}\n` };
|
|
36
|
-
}
|
package/dist/src/index.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Legacy Palette compatibility path. Product entrypoint is src/bun/index.ts.
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { HttpBridge } from "./bridge/httpBridge.js";
|
|
5
|
-
import { RequestQueue } from "./bridge/requestQueue.js";
|
|
6
|
-
import { createMicaMcpServer, registerMicaPrompts } from "./mcp/prompts.js";
|
|
7
|
-
import { registerMmaTools } from "./mcp/tools.js";
|
|
8
|
-
import { runtimeModeFromArgs } from "./runtimeOptions.js";
|
|
9
|
-
async function main() {
|
|
10
|
-
const queue = new RequestQueue();
|
|
11
|
-
const bridge = new HttpBridge(queue);
|
|
12
|
-
const mode = runtimeModeFromArgs(process.argv.slice(2));
|
|
13
|
-
await bridge.start();
|
|
14
|
-
const server = createMicaMcpServer("mica");
|
|
15
|
-
registerMmaTools(server, queue, () => bridge.statusSnapshot());
|
|
16
|
-
registerMicaPrompts(server);
|
|
17
|
-
let shuttingDown = false;
|
|
18
|
-
async function shutdown(exitCode) {
|
|
19
|
-
if (shuttingDown)
|
|
20
|
-
return;
|
|
21
|
-
shuttingDown = true;
|
|
22
|
-
console.error("MICA shutting down...");
|
|
23
|
-
await bridge.stop();
|
|
24
|
-
if (exitCode !== undefined) {
|
|
25
|
-
process.exit(exitCode);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
process.on("SIGINT", () => {
|
|
29
|
-
shutdown(130).catch((error) => {
|
|
30
|
-
console.error(error);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
process.on("SIGTERM", () => {
|
|
35
|
-
shutdown(143).catch((error) => {
|
|
36
|
-
console.error(error);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
// Log only to stderr — stdout is reserved for MCP stdio JSON-RPC.
|
|
41
|
-
console.error(`MICA HTTP listening on http://127.0.0.1:${bridge.port}`);
|
|
42
|
-
if (mode === "bridge-only") {
|
|
43
|
-
console.error("MICA running in bridge-only development mode.");
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
await server.connect(new StdioServerTransport());
|
|
47
|
-
// server.connect() completes stdio transport setup; the HTTP bridge keeps
|
|
48
|
-
// the process alive while the MCP client owns the MCP server. Signal handlers above
|
|
49
|
-
// perform cleanup on explicit process termination.
|
|
50
|
-
}
|
|
51
|
-
main().catch((error) => {
|
|
52
|
-
console.error(error);
|
|
53
|
-
process.exit(1);
|
|
54
|
-
});
|
package/dist/src/mcp/tools.js
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { abortEvaluationSchema, deleteCellSchema, getCellOutputSchema, listCellsSchema, insertCellSchema, modifyCellSchema, noArgsSchema, readArtifactSchema, readCellSchema, selectNotebookSchema, saveNotebookSchema, runCellSchema, symbolLookupSchema } from "./toolSchemas.js";
|
|
2
|
-
import { INSERT_ANCHOR_GUIDANCE, notebookToolDescription } from "./descriptions.js";
|
|
3
|
-
import { toolSuccess, withToolErrors } from "./toolResults.js";
|
|
4
|
-
export function assertBridgeReadyForTool(status) {
|
|
5
|
-
if (!status.paletteConnected) {
|
|
6
|
-
throw new Error("Mathematica Palette is not connected. Open the MMA Agent Bridge palette and click 'Allow control of current Notebook'.");
|
|
7
|
-
}
|
|
8
|
-
if (!status.notebookAttached) {
|
|
9
|
-
throw new Error("No Mathematica notebook is attached. Click 'Allow control of current Notebook' in the MMA Agent Bridge palette.");
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
export function resolveNotebookTarget(args, status) {
|
|
13
|
-
const explicit = args.notebookId;
|
|
14
|
-
if (typeof explicit === "string" && explicit.length > 0) {
|
|
15
|
-
const knownNotebook = status.notebooks?.some((notebook) => notebook.notebookId === explicit) ?? false;
|
|
16
|
-
if (!knownNotebook) {
|
|
17
|
-
throw new Error(`Unknown notebookId: ${explicit}`);
|
|
18
|
-
}
|
|
19
|
-
return explicit;
|
|
20
|
-
}
|
|
21
|
-
const displayName = args.displayName;
|
|
22
|
-
if (typeof displayName === "string" && displayName.trim().length > 0) {
|
|
23
|
-
throw new Error("Display-name notebook selection is not supported in the Node MCP tool path yet.");
|
|
24
|
-
}
|
|
25
|
-
if (typeof status.activeNotebookId === "string" && status.activeNotebookId.length > 0) {
|
|
26
|
-
return status.activeNotebookId;
|
|
27
|
-
}
|
|
28
|
-
throw new Error("No Mathematica notebook is selected");
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Enqueue a tool call through the bridge queue and wire MCP-client
|
|
32
|
-
* cancellation via the AbortSignal provided by the SDK.
|
|
33
|
-
*
|
|
34
|
-
* When the MCP client cancels the tool call the SDK fires the signal's
|
|
35
|
-
* `abort` event. We call `queue.cancelFromMcp` so the request is rejected
|
|
36
|
-
* immediately (queued) or a one-shot cancellation notification is stored
|
|
37
|
-
* for the Palette (claimed). The listener is removed after the promise
|
|
38
|
-
* settles to avoid leaks.
|
|
39
|
-
*/
|
|
40
|
-
async function enqueueRequestWithCancellation(queue, tool, args, extra) {
|
|
41
|
-
const { requestId, promise } = queue.enqueueWithId(tool, args);
|
|
42
|
-
// If the signal was already aborted before we enqueued, cancel immediately.
|
|
43
|
-
if (extra?.signal?.aborted) {
|
|
44
|
-
queue.cancelFromMcp(requestId, "MCP client cancelled operation");
|
|
45
|
-
}
|
|
46
|
-
const onAbort = () => {
|
|
47
|
-
queue.cancelFromMcp(requestId, "MCP client cancelled operation");
|
|
48
|
-
};
|
|
49
|
-
// Only add the listener if the signal exists and isn't already aborted.
|
|
50
|
-
if (extra?.signal && !extra.signal.aborted) {
|
|
51
|
-
extra.signal.addEventListener("abort", onAbort, { once: true });
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
const result = await promise;
|
|
55
|
-
return toolSuccess(result);
|
|
56
|
-
}
|
|
57
|
-
finally {
|
|
58
|
-
extra?.signal?.removeEventListener("abort", onAbort);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
async function enqueueWithCancellation(queue, getStatus, tool, args, extra) {
|
|
62
|
-
const status = getStatus();
|
|
63
|
-
assertBridgeReadyForTool(status);
|
|
64
|
-
const notebookId = resolveNotebookTarget(args, status);
|
|
65
|
-
return enqueueRequestWithCancellation(queue, tool, { ...args, notebookId }, extra);
|
|
66
|
-
}
|
|
67
|
-
function registerLegacyQueuedTool(server, queue, getStatus, config) {
|
|
68
|
-
server.tool(config.name, notebookToolDescription(config.summary, config.extraGuidance), config.schema, async (args, extra) => {
|
|
69
|
-
const recordArgs = args;
|
|
70
|
-
return withToolErrors({ tool: config.name, args: recordArgs }, () => enqueueWithCancellation(queue, getStatus, config.name, recordArgs, extra));
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
export function registerMmaTools(server, queue, getStatus) {
|
|
74
|
-
server.tool("mma_status", notebookToolDescription("Report Mathematica bridge, Palette, and notebook attachment status."), noArgsSchema.shape, async () => withToolErrors({ tool: "mma_status" }, () => toolSuccess(getStatus())));
|
|
75
|
-
server.tool("mma_list_notebooks", notebookToolDescription("List notebooks registered with the Mathematica bridge Palette."), noArgsSchema.shape, async () => {
|
|
76
|
-
return withToolErrors({ tool: "mma_list_notebooks" }, () => {
|
|
77
|
-
const status = getStatus();
|
|
78
|
-
return toolSuccess({ notebooks: status.notebooks ?? [], activeNotebookId: status.activeNotebookId });
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
server.tool("mma_select_notebook", notebookToolDescription("Select the active Mathematica notebook in the Palette registry."), selectNotebookSchema.shape, async (args, extra) => {
|
|
82
|
-
const recordArgs = args;
|
|
83
|
-
return withToolErrors({ tool: "mma_select_notebook", args: recordArgs }, () => {
|
|
84
|
-
const status = getStatus();
|
|
85
|
-
if (!status.paletteConnected) {
|
|
86
|
-
throw new Error("Mathematica Palette is not connected. Open the MMA Agent Bridge palette and click 'Allow control of current Notebook'.");
|
|
87
|
-
}
|
|
88
|
-
const notebookId = args.notebookId;
|
|
89
|
-
if (typeof args.displayName === "string" && args.displayName.trim().length > 0 && typeof notebookId !== "string") {
|
|
90
|
-
throw new Error("Display-name notebook selection is not supported in the Node MCP tool path yet.");
|
|
91
|
-
}
|
|
92
|
-
if (typeof notebookId !== "string" ||
|
|
93
|
-
notebookId.length === 0 ||
|
|
94
|
-
!(status.notebooks?.some((notebook) => notebook.notebookId === notebookId) ?? false)) {
|
|
95
|
-
throw new Error(`Unknown notebookId: ${notebookId}`);
|
|
96
|
-
}
|
|
97
|
-
return enqueueRequestWithCancellation(queue, "mma_select_notebook", recordArgs, extra);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
const queuedTools = [
|
|
101
|
-
{
|
|
102
|
-
name: "mma_list_cells",
|
|
103
|
-
summary: "List cells in the attached active Mathematica notebook.",
|
|
104
|
-
schema: listCellsSchema.shape,
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: "mma_read_cell",
|
|
108
|
-
summary: "Read one cell from the attached Mathematica notebook.",
|
|
109
|
-
schema: readCellSchema.shape,
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
name: "mma_insert_cell",
|
|
113
|
-
summary: "Insert a cell through the Mathematica FrontEnd bridge.",
|
|
114
|
-
schema: insertCellSchema.shape,
|
|
115
|
-
extraGuidance: INSERT_ANCHOR_GUIDANCE,
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
name: "mma_modify_cell",
|
|
119
|
-
summary: "Modify one existing cell through the Mathematica FrontEnd bridge.",
|
|
120
|
-
schema: modifyCellSchema.shape,
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
name: "mma_delete_cell",
|
|
124
|
-
summary: "Delete one cell through the Mathematica FrontEnd bridge.",
|
|
125
|
-
schema: deleteCellSchema.shape,
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: "mma_run_cell",
|
|
129
|
-
summary: "Run one cell in the attached Mathematica notebook.",
|
|
130
|
-
schema: runCellSchema.shape,
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
name: "mma_abort_evaluation",
|
|
134
|
-
summary: "Abort the running Wolfram evaluation in the attached notebook.",
|
|
135
|
-
schema: abortEvaluationSchema.shape,
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
name: "mma_get_cell_output",
|
|
139
|
-
summary: "Read output and messages for one Mathematica notebook cell, refreshing completed run status when observed.",
|
|
140
|
-
schema: getCellOutputSchema.shape,
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
name: "mma_read_artifact",
|
|
144
|
-
summary: "Read one large output or message artifact by byte page. Artifact ids are resolved against current notebook state and may become stale after notebook edits or reruns.",
|
|
145
|
-
schema: readArtifactSchema.shape,
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
name: "mma_save_notebook",
|
|
149
|
-
summary: "Save the attached Mathematica notebook through the FrontEnd.",
|
|
150
|
-
schema: saveNotebookSchema.shape,
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
name: "mma_symbol_lookup",
|
|
154
|
-
summary: "Look up Wolfram Language symbol documentation. Provide an exact symbol name (e.g. 'Plot') for full details including usage, options, attributes, and documentation URL, or a partial name (e.g. 'integrate') for a list of matching symbols.",
|
|
155
|
-
schema: symbolLookupSchema.shape,
|
|
156
|
-
},
|
|
157
|
-
];
|
|
158
|
-
for (const config of queuedTools) {
|
|
159
|
-
registerLegacyQueuedTool(server, queue, getStatus, config);
|
|
160
|
-
}
|
|
161
|
-
}
|
package/dist/src/types.js
DELETED
package/src/bun/index.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { BackendState } from "../backend/backendState.js";
|
|
6
|
-
import { registerBackendMcpTools } from "../mcp/backendTools.js";
|
|
7
|
-
import { createMicaMcpServer, registerMicaPrompts } from "../mcp/prompts.js";
|
|
8
|
-
import type { MicaRuntimeConfig } from "../runtime/config.js";
|
|
9
|
-
import { loadRuntimeConfig } from "../runtime/config.js";
|
|
10
|
-
import { writeSessionFile } from "../runtime/session.js";
|
|
11
|
-
import { createBunHttpApp } from "./httpServer.js";
|
|
12
|
-
|
|
13
|
-
const MCP_SERVER_NAME = "mica-bun";
|
|
14
|
-
const MICA_PACKAGE_VERSION = "1.0.2";
|
|
15
|
-
|
|
16
|
-
export type BunRuntimeDeps = {
|
|
17
|
-
bridgeOnly?: boolean;
|
|
18
|
-
createHttpApp?: typeof createBunHttpApp;
|
|
19
|
-
createMcpServer?: () => Pick<McpServer, "connect" | "prompt" | "tool">;
|
|
20
|
-
createTransport?: () => StdioServerTransport;
|
|
21
|
-
installSignalHandlers?: (onSignal: (signal: NodeJS.Signals) => void) => () => void;
|
|
22
|
-
runtimeConfig?: MicaRuntimeConfig;
|
|
23
|
-
state?: BackendState;
|
|
24
|
-
version?: string;
|
|
25
|
-
writeSessionFile?: typeof writeSessionFile;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type BunRuntime = {
|
|
29
|
-
state: BackendState;
|
|
30
|
-
httpApp: Awaited<ReturnType<typeof createBunHttpApp>>;
|
|
31
|
-
stop: () => Promise<void>;
|
|
32
|
-
keepAlive: Promise<void>;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export async function startBunRuntime(deps: BunRuntimeDeps = {}): Promise<BunRuntime> {
|
|
36
|
-
const config = deps.runtimeConfig ?? loadRuntimeConfig();
|
|
37
|
-
const bridgeOnly = deps.bridgeOnly ?? config.bridgeOnly;
|
|
38
|
-
const state = deps.state ?? new BackendState(() => `notebook-${randomUUID()}`);
|
|
39
|
-
const createHttpApp = deps.createHttpApp ?? createBunHttpApp;
|
|
40
|
-
const createMcpServer = deps.createMcpServer ?? (() => createMicaMcpServer(MCP_SERVER_NAME));
|
|
41
|
-
const createTransport = deps.createTransport ?? (() => new StdioServerTransport());
|
|
42
|
-
const writeSession = deps.writeSessionFile ?? writeSessionFile;
|
|
43
|
-
const version = deps.version ?? MICA_PACKAGE_VERSION;
|
|
44
|
-
const installSignalHandlers =
|
|
45
|
-
deps.installSignalHandlers ??
|
|
46
|
-
((onSignal: (signal: NodeJS.Signals) => void) => {
|
|
47
|
-
process.once("SIGINT", onSignal);
|
|
48
|
-
process.once("SIGTERM", onSignal);
|
|
49
|
-
return () => {
|
|
50
|
-
process.off("SIGINT", onSignal);
|
|
51
|
-
process.off("SIGTERM", onSignal);
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
const httpApp = await createHttpApp({ state, host: config.host, port: config.preferredPort, authToken: config.authToken, version });
|
|
55
|
-
let cleanupSignals = () => {};
|
|
56
|
-
let stopped = false;
|
|
57
|
-
let stopPromise: Promise<void> | undefined;
|
|
58
|
-
|
|
59
|
-
const stop = async (): Promise<void> => {
|
|
60
|
-
if (stopPromise) return stopPromise;
|
|
61
|
-
stopPromise = (async () => {
|
|
62
|
-
if (stopped) return;
|
|
63
|
-
stopped = true;
|
|
64
|
-
cleanupSignals();
|
|
65
|
-
await httpApp.stop();
|
|
66
|
-
})();
|
|
67
|
-
return stopPromise;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const onSignal = (signal: NodeJS.Signals) => {
|
|
71
|
-
void stop().finally(() => process.exit(0));
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
cleanupSignals = installSignalHandlers(onSignal);
|
|
76
|
-
await writeSession(config.sessionFile, {
|
|
77
|
-
host: config.host,
|
|
78
|
-
port: httpApp.port,
|
|
79
|
-
authToken: config.authToken,
|
|
80
|
-
pid: process.pid,
|
|
81
|
-
version,
|
|
82
|
-
status: "running",
|
|
83
|
-
});
|
|
84
|
-
const server = createMcpServer();
|
|
85
|
-
registerBackendMcpTools(server as McpServer, state);
|
|
86
|
-
registerMicaPrompts(server);
|
|
87
|
-
|
|
88
|
-
console.error(`Bun HTTP server listening on http://${config.host}:${httpApp.port}`);
|
|
89
|
-
console.error(`Dashboard: http://${config.host}:${httpApp.port}/#token=${config.authToken}`);
|
|
90
|
-
if (!bridgeOnly) {
|
|
91
|
-
console.error("Bun MCP mode enabled; connecting stdio transport.");
|
|
92
|
-
await server.connect(createTransport());
|
|
93
|
-
}
|
|
94
|
-
} catch (error) {
|
|
95
|
-
await stop();
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
state,
|
|
101
|
-
httpApp,
|
|
102
|
-
stop,
|
|
103
|
-
keepAlive: new Promise<void>(() => {})
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function main(): Promise<void> {
|
|
108
|
-
const runtime = await startBunRuntime();
|
|
109
|
-
await runtime.keepAlive;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
113
|
-
main().catch((error) => {
|
|
114
|
-
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
115
|
-
console.error(message);
|
|
116
|
-
process.exitCode = 1;
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export { MCP_SERVER_NAME };
|