@aliceshimada/mica 1.0.1 → 1.0.3
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 +13 -0
- package/README.md +26 -28
- package/README.zh-CN.md +9 -11
- package/dist/src/bun/httpServer.js +19 -1
- package/dist/src/bun/index.js +1 -1
- package/dist/src/cli/configSnippets.js +3 -3
- package/dist/src/cli/index.js +90 -45
- package/dist/src/cli/status.js +6 -6
- package/dist/src/mcp/backendTools.js +142 -119
- package/dist/src/mcp/prompts.js +1 -1
- package/dist/src/mcp/proxyTools.js +36 -0
- package/package.json +1 -2
- package/src/bun/index.ts +0 -120
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.3 - 2026-06-06
|
|
4
|
+
|
|
5
|
+
- Deprecate `mica start`, `mica stop`, and `mica restart` commands.
|
|
6
|
+
- `mica start` and bare `mica` (no args) are now aliases for `mica mcp`.
|
|
7
|
+
- `mica mcp` starts an MCP stdio server, proxying to an existing bridge or starting a new one.
|
|
8
|
+
|
|
9
|
+
## 1.0.2 - 2026-06-06
|
|
10
|
+
|
|
11
|
+
- Add `mica mcp` command: starts an MCP stdio server that proxies to an existing bridge or starts a new one.
|
|
12
|
+
- Add `/mcp/call` HTTP endpoint for proxied MCP tool execution.
|
|
13
|
+
- Refactor backend tools to export `MICA_BACKEND_TOOL_DEFINITIONS` and `executeBackendMcpTool()` for reuse.
|
|
14
|
+
- Update config snippets to use `mica mcp` instead of `mica start`.
|
|
15
|
+
|
|
3
16
|
## 1.0.1 - 2026-06-02
|
|
4
17
|
|
|
5
18
|
- Implement `mica status`, including dashboard token recovery from the current session.
|
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
|
|
|
@@ -149,7 +147,7 @@ For OpenCode, the snippet uses the validated local MCP shape:
|
|
|
149
147
|
"mcp": {
|
|
150
148
|
"mica": {
|
|
151
149
|
"type": "local",
|
|
152
|
-
"command": ["mica", "
|
|
150
|
+
"command": ["mica", "mcp"],
|
|
153
151
|
"enabled": true
|
|
154
152
|
}
|
|
155
153
|
}
|
|
@@ -163,7 +161,7 @@ For manual setup from a local checkout, use the built release entrypoint:
|
|
|
163
161
|
```toml
|
|
164
162
|
[mcp_servers.mica]
|
|
165
163
|
command = "node"
|
|
166
|
-
args = ["/absolute/path/to/mica/dist/src/cli/index.js", "
|
|
164
|
+
args = ["/absolute/path/to/mica/dist/src/cli/index.js", "mcp"]
|
|
167
165
|
```
|
|
168
166
|
|
|
169
167
|
For development, you can point an MCP client at the TypeScript entrypoint:
|
|
@@ -171,7 +169,7 @@ For development, you can point an MCP client at the TypeScript entrypoint:
|
|
|
171
169
|
```toml
|
|
172
170
|
[mcp_servers.mica]
|
|
173
171
|
command = "npx"
|
|
174
|
-
args = ["tsx", "/absolute/path/to/mica/src/
|
|
172
|
+
args = ["tsx", "/absolute/path/to/mica/src/cli/index.ts", "mcp"]
|
|
175
173
|
```
|
|
176
174
|
|
|
177
175
|
## Agent Guide Prompt
|
|
@@ -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
|
|
|
@@ -149,7 +147,7 @@ OpenCode 的 snippet 使用经过验证的 local MCP 形状:
|
|
|
149
147
|
"mcp": {
|
|
150
148
|
"mica": {
|
|
151
149
|
"type": "local",
|
|
152
|
-
"command": ["mica", "
|
|
150
|
+
"command": ["mica", "mcp"],
|
|
153
151
|
"enabled": true
|
|
154
152
|
}
|
|
155
153
|
}
|
|
@@ -163,7 +161,7 @@ OpenCode 的 snippet 使用经过验证的 local MCP 形状:
|
|
|
163
161
|
```toml
|
|
164
162
|
[mcp_servers.mica]
|
|
165
163
|
command = "node"
|
|
166
|
-
args = ["/absolute/path/to/mica/dist/src/cli/index.js", "
|
|
164
|
+
args = ["/absolute/path/to/mica/dist/src/cli/index.js", "mcp"]
|
|
167
165
|
```
|
|
168
166
|
|
|
169
167
|
开发时也可以让 MCP client 指向 TypeScript 入口:
|
|
@@ -171,7 +169,7 @@ args = ["/absolute/path/to/mica/dist/src/cli/index.js", "start"]
|
|
|
171
169
|
```toml
|
|
172
170
|
[mcp_servers.mica]
|
|
173
171
|
command = "npx"
|
|
174
|
-
args = ["tsx", "/absolute/path/to/mica/src/
|
|
172
|
+
args = ["tsx", "/absolute/path/to/mica/src/cli/index.ts", "mcp"]
|
|
175
173
|
```
|
|
176
174
|
|
|
177
175
|
## Agent Guide Prompt
|
|
@@ -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` |
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
2
|
import http from "node:http";
|
|
3
|
+
import { executeBackendMcpTool } from "../mcp/backendTools.js";
|
|
3
4
|
import { renderDashboard } from "./dashboard.js";
|
|
4
5
|
const JSON_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
5
|
-
const DEFAULT_VERSION = "1.0.
|
|
6
|
+
const DEFAULT_VERSION = "1.0.3";
|
|
6
7
|
export async function createBunHttpApp({ state, host = "127.0.0.1", port, authToken, version = DEFAULT_VERSION }) {
|
|
7
8
|
const runtimeInfo = {
|
|
8
9
|
host,
|
|
@@ -66,6 +67,14 @@ export function createFetchHandler(state, options = {}) {
|
|
|
66
67
|
requests: summarizeRequests(state.queue.snapshot()),
|
|
67
68
|
});
|
|
68
69
|
}
|
|
70
|
+
if (request.method === "POST" && url.pathname === "/mcp/call") {
|
|
71
|
+
const body = await readJsonObjectBody(request);
|
|
72
|
+
const tool = readRequiredString(body.tool, "tool");
|
|
73
|
+
const args = readOptionalRecord(body.arguments) ?? {};
|
|
74
|
+
const clientSessionId = readOptionalString(body.clientSessionId);
|
|
75
|
+
const result = await executeBackendMcpTool(state, tool, args, { sessionId: clientSessionId });
|
|
76
|
+
return jsonResponse(result);
|
|
77
|
+
}
|
|
69
78
|
if (request.method === "POST" && url.pathname === "/agents/register") {
|
|
70
79
|
const body = await readJsonObjectBody(request);
|
|
71
80
|
const agentSessionId = readRequiredString(body.agentSessionId, "agentSessionId");
|
|
@@ -213,6 +222,8 @@ async function startNodeFallbackServer(fetchHandler, host, port) {
|
|
|
213
222
|
await writeNodeResponse(outgoing, jsonResponse({ error: { code: "INTERNAL_ERROR", message } }, 500));
|
|
214
223
|
}
|
|
215
224
|
});
|
|
225
|
+
server.timeout = 0;
|
|
226
|
+
server.requestTimeout = 0;
|
|
216
227
|
await new Promise((resolve, reject) => {
|
|
217
228
|
server.once("error", reject);
|
|
218
229
|
server.listen(port, host, () => {
|
|
@@ -313,6 +324,13 @@ function readOptionalString(value) {
|
|
|
313
324
|
const text = value.trim();
|
|
314
325
|
return text ? text : undefined;
|
|
315
326
|
}
|
|
327
|
+
function readOptionalRecord(value) {
|
|
328
|
+
if (value === undefined)
|
|
329
|
+
return undefined;
|
|
330
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
331
|
+
throw new Error("BAD_REQUEST");
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
316
334
|
function readOptionalNumber(value) {
|
|
317
335
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
318
336
|
}
|
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.3";
|
|
12
12
|
export async function startBunRuntime(deps = {}) {
|
|
13
13
|
const config = deps.runtimeConfig ?? loadRuntimeConfig();
|
|
14
14
|
const bridgeOnly = deps.bridgeOnly ?? config.bridgeOnly;
|
|
@@ -8,7 +8,7 @@ export function runConfigCommand(argv) {
|
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
10
|
if (client === "codex") {
|
|
11
|
-
return ok(`[mcp_servers.mica]\ncommand = "mica"\nargs = ["
|
|
11
|
+
return ok(`[mcp_servers.mica]\ncommand = "mica"\nargs = ["mcp"]\n`);
|
|
12
12
|
}
|
|
13
13
|
if (client === "opencode") {
|
|
14
14
|
return ok(`${JSON.stringify({
|
|
@@ -16,7 +16,7 @@ export function runConfigCommand(argv) {
|
|
|
16
16
|
mcp: {
|
|
17
17
|
mica: {
|
|
18
18
|
type: "local",
|
|
19
|
-
command: ["mica", "
|
|
19
|
+
command: ["mica", "mcp"],
|
|
20
20
|
enabled: true,
|
|
21
21
|
},
|
|
22
22
|
},
|
|
@@ -26,7 +26,7 @@ export function runConfigCommand(argv) {
|
|
|
26
26
|
mcpServers: {
|
|
27
27
|
mica: {
|
|
28
28
|
command: "mica",
|
|
29
|
-
args: ["
|
|
29
|
+
args: ["mcp"],
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
32
|
}, null, 2)}\n`);
|
package/dist/src/cli/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
6
|
import { startBunRuntime } from "../bun/index.js";
|
|
7
|
+
import { createMicaMcpServer, registerMicaPrompts } from "../mcp/prompts.js";
|
|
8
|
+
import { registerProxyMcpTools } from "../mcp/proxyTools.js";
|
|
9
|
+
import { defaultSessionFile } from "../runtime/config.js";
|
|
6
10
|
import { runConfigCommand } from "./configSnippets.js";
|
|
7
11
|
import { runDoctor } from "./doctor.js";
|
|
8
12
|
import { runStatusCommand } from "./status.js";
|
|
9
|
-
import { runStopCommand } from "./stop.js";
|
|
10
13
|
// ---------------------------------------------------------------------------
|
|
11
14
|
// Helpers
|
|
12
15
|
// ---------------------------------------------------------------------------
|
|
@@ -20,6 +23,57 @@ function resolveProjectRoot() {
|
|
|
20
23
|
}
|
|
21
24
|
return dir;
|
|
22
25
|
}
|
|
26
|
+
function sleep(ms) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
29
|
+
function isAddressInUse(error) {
|
|
30
|
+
if (typeof error !== "object" || error === null)
|
|
31
|
+
return false;
|
|
32
|
+
const record = error;
|
|
33
|
+
return record.code === "EADDRINUSE" || String(record.message ?? "").includes("EADDRINUSE");
|
|
34
|
+
}
|
|
35
|
+
function sessionFileFromArgs(argv, env = process.env) {
|
|
36
|
+
const index = argv.indexOf("--session-file");
|
|
37
|
+
if (index >= 0 && argv[index + 1] && !argv[index + 1].startsWith("--")) {
|
|
38
|
+
return argv[index + 1];
|
|
39
|
+
}
|
|
40
|
+
return env.MICA_SESSION_FILE ?? defaultSessionFile(env);
|
|
41
|
+
}
|
|
42
|
+
export async function readLiveSession(argv = [], deps = {}) {
|
|
43
|
+
const env = deps.env ?? process.env;
|
|
44
|
+
const exists = deps.exists ?? existsSync;
|
|
45
|
+
const readFile = deps.readFile ?? ((filePath) => readFileSync(filePath, "utf8"));
|
|
46
|
+
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
47
|
+
const sessionFile = sessionFileFromArgs(argv, env);
|
|
48
|
+
if (!exists(sessionFile) || typeof fetchImpl !== "function")
|
|
49
|
+
return undefined;
|
|
50
|
+
let session;
|
|
51
|
+
try {
|
|
52
|
+
session = JSON.parse(readFile(sessionFile));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (!session.baseUrl || !session.authToken)
|
|
58
|
+
return undefined;
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetchImpl(`${session.baseUrl}/status`, {
|
|
61
|
+
headers: { authorization: `Bearer ${session.authToken}` },
|
|
62
|
+
});
|
|
63
|
+
return response.status === 200 ? { baseUrl: session.baseUrl, authToken: session.authToken } : undefined;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function startProxyMcpRuntime(session, deps = {}) {
|
|
70
|
+
const server = deps.createMcpServer?.() ?? createMicaMcpServer("mica-proxy");
|
|
71
|
+
registerProxyMcpTools(server, session);
|
|
72
|
+
registerMicaPrompts(server);
|
|
73
|
+
const transport = deps.createTransport?.() ?? new StdioServerTransport();
|
|
74
|
+
await server.connect(transport);
|
|
75
|
+
return { keepAlive: new Promise(() => { }) };
|
|
76
|
+
}
|
|
23
77
|
// ---------------------------------------------------------------------------
|
|
24
78
|
// Public API
|
|
25
79
|
// ---------------------------------------------------------------------------
|
|
@@ -27,9 +81,7 @@ export function helpText() {
|
|
|
27
81
|
return `Usage: mica <command> [options]
|
|
28
82
|
|
|
29
83
|
Commands:
|
|
30
|
-
|
|
31
|
-
stop Stop the running MICA bridge runtime
|
|
32
|
-
restart Stop then start the MICA bridge runtime
|
|
84
|
+
mcp Run MCP stdio server (proxy to existing bridge or launch new)
|
|
33
85
|
install [options] Install MICA bridge into Wolfram
|
|
34
86
|
uninstall [options] Uninstall MICA bridge from Wolfram
|
|
35
87
|
doctor Diagnose MICA bridge configuration
|
|
@@ -52,7 +104,9 @@ export async function runCli(argv, deps) {
|
|
|
52
104
|
const _runDoctor = deps?.runDoctor;
|
|
53
105
|
const _runStatus = deps?.runStatus;
|
|
54
106
|
const _runConfig = deps?.runConfig;
|
|
55
|
-
const
|
|
107
|
+
const _readLiveSession = deps?.readLiveSession ?? (() => readLiveSession(argv.slice(1)));
|
|
108
|
+
const _startProxyRuntime = deps?.startProxyRuntime ?? ((session) => startProxyMcpRuntime(session));
|
|
109
|
+
const _sleep = deps?.sleep ?? sleep;
|
|
56
110
|
const command = argv[0];
|
|
57
111
|
// --help / -h
|
|
58
112
|
if (command === "--help" || command === "-h") {
|
|
@@ -79,22 +133,39 @@ export async function runCli(argv, deps) {
|
|
|
79
133
|
stdout.write(output);
|
|
80
134
|
return 0;
|
|
81
135
|
}
|
|
82
|
-
// start or no args
|
|
83
|
-
if (command === "start" || command === undefined) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
136
|
+
// mcp (also start, or no args)
|
|
137
|
+
if (command === "mcp" || command === "start" || command === undefined) {
|
|
138
|
+
const existingSession = await _readLiveSession();
|
|
139
|
+
if (existingSession) {
|
|
140
|
+
const proxyRuntime = await _startProxyRuntime(existingSession);
|
|
141
|
+
await proxyRuntime.keepAlive;
|
|
142
|
+
return 0;
|
|
90
143
|
}
|
|
91
144
|
if (!startRuntime) {
|
|
92
145
|
stderr.write("Error: startRuntime not available\n");
|
|
93
146
|
return 1;
|
|
94
147
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
148
|
+
try {
|
|
149
|
+
const runtime = await startRuntime();
|
|
150
|
+
await runtime.keepAlive;
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (!isAddressInUse(error))
|
|
155
|
+
throw error;
|
|
156
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
157
|
+
await _sleep(100);
|
|
158
|
+
const racedSession = await _readLiveSession();
|
|
159
|
+
if (racedSession) {
|
|
160
|
+
const proxyRuntime = await _startProxyRuntime(racedSession);
|
|
161
|
+
await proxyRuntime.keepAlive;
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
stderr.write(`Error: MICA bridge port is already in use, but no live session became available. ${message}\n`);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
98
169
|
}
|
|
99
170
|
// doctor
|
|
100
171
|
if (command === "doctor") {
|
|
@@ -126,33 +197,6 @@ export async function runCli(argv, deps) {
|
|
|
126
197
|
stdout.write(output);
|
|
127
198
|
return exitCode;
|
|
128
199
|
}
|
|
129
|
-
// stop
|
|
130
|
-
if (command === "stop") {
|
|
131
|
-
if (!_runStop) {
|
|
132
|
-
stderr.write("Error: runStop not available\n");
|
|
133
|
-
return 1;
|
|
134
|
-
}
|
|
135
|
-
const { exitCode, output } = await _runStop();
|
|
136
|
-
stdout.write(output);
|
|
137
|
-
return exitCode;
|
|
138
|
-
}
|
|
139
|
-
// restart
|
|
140
|
-
if (command === "restart") {
|
|
141
|
-
if (!_runStop) {
|
|
142
|
-
stderr.write("Error: runStop not available\n");
|
|
143
|
-
return 1;
|
|
144
|
-
}
|
|
145
|
-
if (!startRuntime) {
|
|
146
|
-
stderr.write("Error: startRuntime not available\n");
|
|
147
|
-
return 1;
|
|
148
|
-
}
|
|
149
|
-
const stopResult = await _runStop();
|
|
150
|
-
if (stopResult.output)
|
|
151
|
-
stdout.write(stopResult.output);
|
|
152
|
-
const runtime = await startRuntime();
|
|
153
|
-
await runtime.keepAlive;
|
|
154
|
-
return 0;
|
|
155
|
-
}
|
|
156
200
|
// Unknown command
|
|
157
201
|
stderr.write(`Unknown command: ${command}\n`);
|
|
158
202
|
return 1;
|
|
@@ -166,10 +210,11 @@ async function main() {
|
|
|
166
210
|
const { runInstaller, detectWolframUserBase } = (await import(installerUrl));
|
|
167
211
|
const exitCode = await runCli(process.argv.slice(2), {
|
|
168
212
|
startRuntime: async () => startBunRuntime(),
|
|
213
|
+
startProxyRuntime: async (session) => startProxyMcpRuntime(session),
|
|
214
|
+
readLiveSession: async () => readLiveSession(process.argv.slice(3)),
|
|
169
215
|
runInstaller,
|
|
170
216
|
runStatus: async () => runStatusCommand(),
|
|
171
217
|
runConfig: runConfigCommand,
|
|
172
|
-
runStop: async () => runStopCommand(),
|
|
173
218
|
runDoctor: async () => runDoctor({
|
|
174
219
|
projectRoot,
|
|
175
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
|
}
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { DEFAULT_TIMEOUTS_MS } from "../backend/protocol.js";
|
|
3
3
|
import { abortEvaluationSchema, deleteCellSchema, getCellOutputSchema, insertCellSchema, listCellsSchema, modifyCellSchema, noArgsSchema, readArtifactSchema, readCellSchema, runCellSchema, selectNotebookSchema, saveNotebookSchema, symbolLookupSchema, } from "./toolSchemas.js";
|
|
4
4
|
import { INSERT_ANCHOR_GUIDANCE, notebookToolDescription } from "./descriptions.js";
|
|
5
|
-
import { toolSuccess, withToolErrors } from "./toolResults.js";
|
|
5
|
+
import { toolFailure, toolSuccess, withToolErrors } from "./toolResults.js";
|
|
6
6
|
function assertLiveAgent(state) {
|
|
7
7
|
if (!state.requireLiveAgent().ok) {
|
|
8
8
|
throw new Error("NO_LIVE_AGENT");
|
|
@@ -75,26 +75,121 @@ function ensurePermission(notebook, tool, permission) {
|
|
|
75
75
|
throw new Error(`PERMISSION_DENIED: ${tool}`);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
78
|
+
const queuedNotebookTools = [
|
|
79
|
+
{
|
|
80
|
+
name: "mma_symbol_lookup",
|
|
81
|
+
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.",
|
|
82
|
+
schema: symbolLookupSchema.shape,
|
|
83
|
+
permission: "ReadNotebook",
|
|
84
|
+
timeoutMs: () => DEFAULT_TIMEOUTS_MS.symbolLookup,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "mma_list_cells",
|
|
88
|
+
summary: "List cells in the attached active Mathematica notebook.",
|
|
89
|
+
schema: listCellsSchema.shape,
|
|
90
|
+
permission: "ReadNotebook",
|
|
91
|
+
timeoutMs: () => DEFAULT_TIMEOUTS_MS.listCells,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "mma_read_cell",
|
|
95
|
+
summary: "Read one cell from the attached Mathematica notebook.",
|
|
96
|
+
schema: readCellSchema.shape,
|
|
97
|
+
permission: "ReadNotebook",
|
|
98
|
+
timeoutMs: () => DEFAULT_TIMEOUTS_MS.readCell,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "mma_insert_cell",
|
|
102
|
+
summary: "Insert a cell through the Mathematica FrontEnd bridge.",
|
|
103
|
+
schema: insertCellSchema.shape,
|
|
104
|
+
permission: "InsertCell",
|
|
105
|
+
timeoutMs: () => DEFAULT_TIMEOUTS_MS.insertCell,
|
|
106
|
+
extraGuidance: INSERT_ANCHOR_GUIDANCE,
|
|
107
|
+
requiresExplicitTarget: true,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "mma_modify_cell",
|
|
111
|
+
summary: "Modify one existing cell through the Mathematica FrontEnd bridge.",
|
|
112
|
+
schema: modifyCellSchema.shape,
|
|
113
|
+
permission: "ModifyCell",
|
|
114
|
+
timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
|
|
115
|
+
requiresExplicitTarget: true,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "mma_delete_cell",
|
|
119
|
+
summary: "Delete one cell through the Mathematica FrontEnd bridge.",
|
|
120
|
+
schema: deleteCellSchema.shape,
|
|
121
|
+
permission: "DeleteCell",
|
|
122
|
+
timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
|
|
123
|
+
requiresExplicitTarget: true,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "mma_run_cell",
|
|
127
|
+
summary: "Run one cell in the attached Mathematica notebook.",
|
|
128
|
+
schema: runCellSchema.shape,
|
|
129
|
+
permission: "RunCell",
|
|
130
|
+
timeoutMs: (args) => {
|
|
131
|
+
const timeoutSec = typeof args.timeoutSec === "number" ? args.timeoutSec : 120;
|
|
132
|
+
return timeoutSec * 1000;
|
|
133
|
+
},
|
|
134
|
+
requiresExplicitTarget: true,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "mma_abort_evaluation",
|
|
138
|
+
summary: "Abort the running Wolfram evaluation in the attached notebook.",
|
|
139
|
+
schema: abortEvaluationSchema.shape,
|
|
140
|
+
permission: "RunCell",
|
|
141
|
+
timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
|
|
142
|
+
requiresExplicitTarget: true,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "mma_get_cell_output",
|
|
146
|
+
summary: "Read output and messages for one Mathematica notebook cell, refreshing completed run status when observed.",
|
|
147
|
+
schema: getCellOutputSchema.shape,
|
|
148
|
+
permission: "ReadNotebook",
|
|
149
|
+
timeoutMs: () => DEFAULT_TIMEOUTS_MS.readCell,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "mma_read_artifact",
|
|
153
|
+
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.",
|
|
154
|
+
schema: readArtifactSchema.shape,
|
|
155
|
+
permission: "ReadNotebook",
|
|
156
|
+
timeoutMs: () => DEFAULT_TIMEOUTS_MS.readCell,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "mma_save_notebook",
|
|
160
|
+
summary: "Save the attached Mathematica notebook through the FrontEnd.",
|
|
161
|
+
schema: saveNotebookSchema.shape,
|
|
162
|
+
permission: "SaveNotebook",
|
|
163
|
+
timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
|
|
164
|
+
requiresExplicitTarget: true,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
export const MICA_BACKEND_TOOL_DEFINITIONS = [
|
|
168
|
+
{
|
|
169
|
+
name: "mma_status",
|
|
170
|
+
description: notebookToolDescription("Report backend status and notebook registry state."),
|
|
171
|
+
schema: noArgsSchema.shape,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "mma_list_notebooks",
|
|
175
|
+
description: notebookToolDescription("List notebooks registered with the Mathematica bridge Palette."),
|
|
176
|
+
schema: noArgsSchema.shape,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "mma_select_notebook",
|
|
180
|
+
description: notebookToolDescription("Select the active Mathematica notebook in the backend registry."),
|
|
181
|
+
schema: selectNotebookSchema.shape,
|
|
182
|
+
},
|
|
183
|
+
...queuedNotebookTools.map((config) => ({
|
|
184
|
+
name: config.name,
|
|
185
|
+
description: notebookToolDescription(config.summary, config.extraGuidance),
|
|
186
|
+
schema: config.schema,
|
|
187
|
+
})),
|
|
188
|
+
];
|
|
189
|
+
export async function executeBackendMcpTool(state, tool, args = {}, extra) {
|
|
190
|
+
const recordArgs = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
191
|
+
if (tool === "mma_status") {
|
|
192
|
+
return withToolErrors({ tool }, () => {
|
|
98
193
|
sweepStateLiveness(state);
|
|
99
194
|
return toolSuccess({
|
|
100
195
|
server: "running",
|
|
@@ -103,16 +198,15 @@ export function registerBackendMcpTools(server, state) {
|
|
|
103
198
|
agents: liveAgents(state),
|
|
104
199
|
});
|
|
105
200
|
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return withToolErrors({ tool
|
|
201
|
+
}
|
|
202
|
+
if (tool === "mma_list_notebooks") {
|
|
203
|
+
return withToolErrors({ tool }, () => {
|
|
109
204
|
sweepStateLiveness(state);
|
|
110
205
|
return toolSuccess({ notebooks: state.notebooks.listLive(), activeNotebookId: liveActiveNotebookId(state, extra?.sessionId) });
|
|
111
206
|
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return withToolErrors({ tool: "mma_select_notebook", args: recordArgs }, () => {
|
|
207
|
+
}
|
|
208
|
+
if (tool === "mma_select_notebook") {
|
|
209
|
+
return withToolErrors({ tool, args: recordArgs }, () => {
|
|
116
210
|
const clientSessionId = extra?.sessionId;
|
|
117
211
|
const target = resolveToolTarget(state, recordArgs, extra);
|
|
118
212
|
state.setActiveNotebook(target.notebook.notebookId);
|
|
@@ -120,97 +214,26 @@ export function registerBackendMcpTools(server, state) {
|
|
|
120
214
|
state.setActiveNotebook(target.notebook.notebookId, clientSessionId);
|
|
121
215
|
return toolSuccess({ activeNotebookId: state.activeNotebookId, notebook: target.notebook });
|
|
122
216
|
});
|
|
217
|
+
}
|
|
218
|
+
const queuedConfig = queuedNotebookTools.find((config) => config.name === tool);
|
|
219
|
+
if (!queuedConfig) {
|
|
220
|
+
return toolFailure(new Error(`UNKNOWN_TOOL: ${tool}`), { tool, args: recordArgs });
|
|
221
|
+
}
|
|
222
|
+
return withToolErrors({ tool: queuedConfig.name, args: recordArgs }, async () => {
|
|
223
|
+
if (process.env.MICA_STRICT_TARGETING === "1" && queuedConfig.requiresExplicitTarget) {
|
|
224
|
+
const hasNotebookId = typeof recordArgs.notebookId === "string" && recordArgs.notebookId.trim().length > 0;
|
|
225
|
+
const hasDisplayName = typeof recordArgs.displayName === "string" && recordArgs.displayName.trim().length > 0;
|
|
226
|
+
if (!hasNotebookId && !hasDisplayName) {
|
|
227
|
+
throw new Error("EXPLICIT_NOTEBOOK_REQUIRED");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const target = resolveToolTarget(state, recordArgs, extra);
|
|
231
|
+
ensurePermission(target.notebook, queuedConfig.name, queuedConfig.permission);
|
|
232
|
+
return queueNotebookOperation(state, target, queuedConfig.name, recordArgs, queuedConfig.timeoutMs(recordArgs), extra);
|
|
123
233
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
schema: symbolLookupSchema.shape,
|
|
129
|
-
permission: "ReadNotebook",
|
|
130
|
-
timeoutMs: () => DEFAULT_TIMEOUTS_MS.symbolLookup,
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
name: "mma_list_cells",
|
|
134
|
-
summary: "List cells in the attached active Mathematica notebook.",
|
|
135
|
-
schema: listCellsSchema.shape,
|
|
136
|
-
permission: "ReadNotebook",
|
|
137
|
-
timeoutMs: () => DEFAULT_TIMEOUTS_MS.listCells,
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
name: "mma_read_cell",
|
|
141
|
-
summary: "Read one cell from the attached Mathematica notebook.",
|
|
142
|
-
schema: readCellSchema.shape,
|
|
143
|
-
permission: "ReadNotebook",
|
|
144
|
-
timeoutMs: () => DEFAULT_TIMEOUTS_MS.readCell,
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
name: "mma_insert_cell",
|
|
148
|
-
summary: "Insert a cell through the Mathematica FrontEnd bridge.",
|
|
149
|
-
schema: insertCellSchema.shape,
|
|
150
|
-
permission: "InsertCell",
|
|
151
|
-
timeoutMs: () => DEFAULT_TIMEOUTS_MS.insertCell,
|
|
152
|
-
extraGuidance: INSERT_ANCHOR_GUIDANCE,
|
|
153
|
-
requiresExplicitTarget: true,
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
name: "mma_modify_cell",
|
|
157
|
-
summary: "Modify one existing cell through the Mathematica FrontEnd bridge.",
|
|
158
|
-
schema: modifyCellSchema.shape,
|
|
159
|
-
permission: "ModifyCell",
|
|
160
|
-
timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
|
|
161
|
-
requiresExplicitTarget: true,
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
name: "mma_delete_cell",
|
|
165
|
-
summary: "Delete one cell through the Mathematica FrontEnd bridge.",
|
|
166
|
-
schema: deleteCellSchema.shape,
|
|
167
|
-
permission: "DeleteCell",
|
|
168
|
-
timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
|
|
169
|
-
requiresExplicitTarget: true,
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
name: "mma_run_cell",
|
|
173
|
-
summary: "Run one cell in the attached Mathematica notebook.",
|
|
174
|
-
schema: runCellSchema.shape,
|
|
175
|
-
permission: "RunCell",
|
|
176
|
-
timeoutMs: (args) => {
|
|
177
|
-
const timeoutSec = typeof args.timeoutSec === "number" ? args.timeoutSec : 120;
|
|
178
|
-
return timeoutSec * 1000;
|
|
179
|
-
},
|
|
180
|
-
requiresExplicitTarget: true,
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
name: "mma_abort_evaluation",
|
|
184
|
-
summary: "Abort the running Wolfram evaluation in the attached notebook.",
|
|
185
|
-
schema: abortEvaluationSchema.shape,
|
|
186
|
-
permission: "RunCell",
|
|
187
|
-
timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
|
|
188
|
-
requiresExplicitTarget: true,
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
name: "mma_get_cell_output",
|
|
192
|
-
summary: "Read output and messages for one Mathematica notebook cell, refreshing completed run status when observed.",
|
|
193
|
-
schema: getCellOutputSchema.shape,
|
|
194
|
-
permission: "ReadNotebook",
|
|
195
|
-
timeoutMs: () => DEFAULT_TIMEOUTS_MS.readCell,
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
name: "mma_read_artifact",
|
|
199
|
-
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.",
|
|
200
|
-
schema: readArtifactSchema.shape,
|
|
201
|
-
permission: "ReadNotebook",
|
|
202
|
-
timeoutMs: () => DEFAULT_TIMEOUTS_MS.readCell,
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
name: "mma_save_notebook",
|
|
206
|
-
summary: "Save the attached Mathematica notebook through the FrontEnd.",
|
|
207
|
-
schema: saveNotebookSchema.shape,
|
|
208
|
-
permission: "SaveNotebook",
|
|
209
|
-
timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
|
|
210
|
-
requiresExplicitTarget: true,
|
|
211
|
-
},
|
|
212
|
-
];
|
|
213
|
-
for (const config of queuedNotebookTools) {
|
|
214
|
-
registerQueuedNotebookTool(server, state, config);
|
|
234
|
+
}
|
|
235
|
+
export function registerBackendMcpTools(server, state) {
|
|
236
|
+
for (const definition of MICA_BACKEND_TOOL_DEFINITIONS) {
|
|
237
|
+
server.tool(definition.name, definition.description, definition.schema, (args, extra) => executeBackendMcpTool(state, definition.name, args, extra));
|
|
215
238
|
}
|
|
216
239
|
}
|
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.3") {
|
|
37
37
|
return new McpServer({ name, version }, { instructions: MICA_AGENT_INSTRUCTIONS });
|
|
38
38
|
}
|
|
39
39
|
export function registerMicaPrompts(server) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { MICA_BACKEND_TOOL_DEFINITIONS } from "./backendTools.js";
|
|
2
|
+
import { toolFailure } from "./toolResults.js";
|
|
3
|
+
export function registerProxyMcpTools(server, session, deps = {}) {
|
|
4
|
+
for (const definition of MICA_BACKEND_TOOL_DEFINITIONS) {
|
|
5
|
+
server.tool(definition.name, definition.description, definition.schema, async (args, extra) => {
|
|
6
|
+
return callProxyTool(session, definition.name, args, extra, deps);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
async function callProxyTool(session, tool, args, extra, deps) {
|
|
11
|
+
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
12
|
+
if (typeof fetchImpl !== "function") {
|
|
13
|
+
return toolFailure(new Error("FETCH_UNAVAILABLE"), { tool, args });
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetchImpl(`${session.baseUrl}/mcp/call`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
authorization: `Bearer ${session.authToken}`,
|
|
20
|
+
"content-type": "application/json",
|
|
21
|
+
},
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
tool,
|
|
24
|
+
arguments: args ?? {},
|
|
25
|
+
...(extra?.sessionId ? { clientSessionId: extra.sessionId } : {}),
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
return toolFailure(new Error(`MCP_PROXY_HTTP_${response.status}`), { tool, args });
|
|
30
|
+
}
|
|
31
|
+
return (await response.json());
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return toolFailure(error, { tool, args });
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliceshimada/mica",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Local MCP bridge for controlling live Wolfram Desktop / Mathematica notebooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist/src",
|
|
26
|
-
"src/bun/index.ts",
|
|
27
26
|
"paclet",
|
|
28
27
|
"README.md",
|
|
29
28
|
"README.zh-CN.md",
|
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.1";
|
|
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 };
|