@aliceshimada/mica 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +36 -3
- package/README.zh-CN.md +36 -3
- package/dist/src/bun/httpServer.js +19 -1
- package/dist/src/bun/index.js +1 -1
- package/dist/src/cli/configSnippets.js +36 -0
- package/dist/src/cli/index.js +161 -5
- package/dist/src/cli/status.js +81 -0
- package/dist/src/cli/stop.js +36 -0
- 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 -1
- package/src/bun/index.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.2 - 2026-06-06
|
|
4
|
+
|
|
5
|
+
- Add `mica mcp` command: starts an MCP stdio server that proxies to an existing bridge or starts a new one.
|
|
6
|
+
- Add `/mcp/call` HTTP endpoint for proxied MCP tool execution.
|
|
7
|
+
- Refactor backend tools to export `MICA_BACKEND_TOOL_DEFINITIONS` and `executeBackendMcpTool()` for reuse.
|
|
8
|
+
- Update config snippets to use `mica mcp` instead of `mica start`.
|
|
9
|
+
|
|
10
|
+
## 1.0.1 - 2026-06-02
|
|
11
|
+
|
|
12
|
+
- Implement `mica status`, including dashboard token recovery from the current session.
|
|
13
|
+
- Implement `mica config codex|claude-desktop|cursor|opencode` snippets.
|
|
14
|
+
- Add `mica stop` and `mica restart`.
|
|
15
|
+
- Make `mica start` print current status instead of failing when a server is already running.
|
|
16
|
+
|
|
3
17
|
## 1.0.0 - 2026-06-02
|
|
4
18
|
|
|
5
19
|
- Initial public release as `@aliceshimada/mica`.
|
package/README.md
CHANGED
|
@@ -102,6 +102,9 @@ If MICA is installed on your `PATH`, the same release commands are:
|
|
|
102
102
|
mica install
|
|
103
103
|
mica start
|
|
104
104
|
mica doctor
|
|
105
|
+
mica status
|
|
106
|
+
mica stop
|
|
107
|
+
mica restart
|
|
105
108
|
```
|
|
106
109
|
|
|
107
110
|
Dashboard:
|
|
@@ -121,16 +124,46 @@ node dist/src/cli/index.js install --dry-run
|
|
|
121
124
|
node dist/src/cli/index.js uninstall
|
|
122
125
|
```
|
|
123
126
|
|
|
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 start` prints that same status instead of failing with a port-in-use error, so you can recover the dashboard token at any time.
|
|
128
|
+
|
|
124
129
|
The legacy installer entry remains available for compatibility: `node scripts/install.js --dry-run`.
|
|
125
130
|
|
|
126
131
|
## MCP Client Config
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
Print a copy-pasteable MCP config snippet:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
mica config codex
|
|
137
|
+
mica config claude-desktop
|
|
138
|
+
mica config cursor
|
|
139
|
+
mica config opencode
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
MICA only prints snippets; it does not edit client config files for you.
|
|
143
|
+
|
|
144
|
+
For OpenCode, the snippet uses the validated local MCP shape:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"$schema": "https://opencode.ai/config.json",
|
|
149
|
+
"mcp": {
|
|
150
|
+
"mica": {
|
|
151
|
+
"type": "local",
|
|
152
|
+
"command": ["mica", "mcp"],
|
|
153
|
+
"enabled": true
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
When editing OpenCode config, restart OpenCode after saving; config is loaded at startup.
|
|
160
|
+
|
|
161
|
+
For manual setup from a local checkout, use the built release entrypoint:
|
|
129
162
|
|
|
130
163
|
```toml
|
|
131
164
|
[mcp_servers.mica]
|
|
132
165
|
command = "node"
|
|
133
|
-
args = ["/absolute/path/to/mica/dist/src/cli/index.js", "
|
|
166
|
+
args = ["/absolute/path/to/mica/dist/src/cli/index.js", "mcp"]
|
|
134
167
|
```
|
|
135
168
|
|
|
136
169
|
For development, you can point an MCP client at the TypeScript entrypoint:
|
|
@@ -138,7 +171,7 @@ For development, you can point an MCP client at the TypeScript entrypoint:
|
|
|
138
171
|
```toml
|
|
139
172
|
[mcp_servers.mica]
|
|
140
173
|
command = "npx"
|
|
141
|
-
args = ["tsx", "/absolute/path/to/mica/src/
|
|
174
|
+
args = ["tsx", "/absolute/path/to/mica/src/cli/index.ts", "mcp"]
|
|
142
175
|
```
|
|
143
176
|
|
|
144
177
|
## Agent Guide Prompt
|
package/README.zh-CN.md
CHANGED
|
@@ -102,6 +102,9 @@ node dist/src/cli/index.js start
|
|
|
102
102
|
mica install
|
|
103
103
|
mica start
|
|
104
104
|
mica doctor
|
|
105
|
+
mica status
|
|
106
|
+
mica stop
|
|
107
|
+
mica restart
|
|
105
108
|
```
|
|
106
109
|
|
|
107
110
|
Dashboard:
|
|
@@ -121,16 +124,46 @@ node dist/src/cli/index.js install --dry-run
|
|
|
121
124
|
node dist/src/cli/index.js uninstall
|
|
122
125
|
```
|
|
123
126
|
|
|
127
|
+
`mica status` 会打印当前 session file、server URL、version、PID、live agent/notebook 数量,以及带 token 的 dashboard URL。如果 server 已经在运行,`mica start` 会打印同样的 status,而不是因为端口占用直接失败;因此你随时可以用它找回 dashboard token。
|
|
128
|
+
|
|
124
129
|
兼容用的 legacy 安装入口仍然可用:`node scripts/install.js --dry-run`。
|
|
125
130
|
|
|
126
131
|
## MCP Client 配置
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
打印可复制的 MCP config snippet:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
mica config codex
|
|
137
|
+
mica config claude-desktop
|
|
138
|
+
mica config cursor
|
|
139
|
+
mica config opencode
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
MICA 只打印配置片段,不会替你编辑 client config 文件。
|
|
143
|
+
|
|
144
|
+
OpenCode 的 snippet 使用经过验证的 local MCP 形状:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"$schema": "https://opencode.ai/config.json",
|
|
149
|
+
"mcp": {
|
|
150
|
+
"mica": {
|
|
151
|
+
"type": "local",
|
|
152
|
+
"command": ["mica", "mcp"],
|
|
153
|
+
"enabled": true
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
编辑 OpenCode config 后需要重启 OpenCode;config 只在启动时加载。
|
|
160
|
+
|
|
161
|
+
如果要从本地 checkout 手动设置,请使用构建后的发布版入口:
|
|
129
162
|
|
|
130
163
|
```toml
|
|
131
164
|
[mcp_servers.mica]
|
|
132
165
|
command = "node"
|
|
133
|
-
args = ["/absolute/path/to/mica/dist/src/cli/index.js", "
|
|
166
|
+
args = ["/absolute/path/to/mica/dist/src/cli/index.js", "mcp"]
|
|
134
167
|
```
|
|
135
168
|
|
|
136
169
|
开发时也可以让 MCP client 指向 TypeScript 入口:
|
|
@@ -138,7 +171,7 @@ args = ["/absolute/path/to/mica/dist/src/cli/index.js", "start"]
|
|
|
138
171
|
```toml
|
|
139
172
|
[mcp_servers.mica]
|
|
140
173
|
command = "npx"
|
|
141
|
-
args = ["tsx", "/absolute/path/to/mica/src/
|
|
174
|
+
args = ["tsx", "/absolute/path/to/mica/src/cli/index.ts", "mcp"]
|
|
142
175
|
```
|
|
143
176
|
|
|
144
177
|
## Agent Guide Prompt
|
|
@@ -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 = "
|
|
6
|
+
const DEFAULT_VERSION = "1.0.2";
|
|
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 = "
|
|
11
|
+
const MICA_PACKAGE_VERSION = "1.0.2";
|
|
12
12
|
export async function startBunRuntime(deps = {}) {
|
|
13
13
|
const config = deps.runtimeConfig ?? loadRuntimeConfig();
|
|
14
14
|
const bridgeOnly = deps.bridgeOnly ?? config.bridgeOnly;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const CLIENTS = ["codex", "claude-desktop", "cursor", "opencode"];
|
|
2
|
+
export function runConfigCommand(argv) {
|
|
3
|
+
const client = argv[0];
|
|
4
|
+
if (!client || !CLIENTS.includes(client)) {
|
|
5
|
+
return {
|
|
6
|
+
exitCode: 1,
|
|
7
|
+
output: `Usage: mica config <${CLIENTS.join("|")}>\n`,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
if (client === "codex") {
|
|
11
|
+
return ok(`[mcp_servers.mica]\ncommand = "mica"\nargs = ["mcp"]\n`);
|
|
12
|
+
}
|
|
13
|
+
if (client === "opencode") {
|
|
14
|
+
return ok(`${JSON.stringify({
|
|
15
|
+
$schema: "https://opencode.ai/config.json",
|
|
16
|
+
mcp: {
|
|
17
|
+
mica: {
|
|
18
|
+
type: "local",
|
|
19
|
+
command: ["mica", "mcp"],
|
|
20
|
+
enabled: true,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}, null, 2)}\n`);
|
|
24
|
+
}
|
|
25
|
+
return ok(`${JSON.stringify({
|
|
26
|
+
mcpServers: {
|
|
27
|
+
mica: {
|
|
28
|
+
command: "mica",
|
|
29
|
+
args: ["mcp"],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}, null, 2)}\n`);
|
|
33
|
+
}
|
|
34
|
+
function ok(output) {
|
|
35
|
+
return { exitCode: 0, output };
|
|
36
|
+
}
|
package/dist/src/cli/index.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
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";
|
|
10
|
+
import { runConfigCommand } from "./configSnippets.js";
|
|
6
11
|
import { runDoctor } from "./doctor.js";
|
|
12
|
+
import { runStatusCommand } from "./status.js";
|
|
13
|
+
import { runStopCommand } from "./stop.js";
|
|
7
14
|
// ---------------------------------------------------------------------------
|
|
8
15
|
// Helpers
|
|
9
16
|
// ---------------------------------------------------------------------------
|
|
@@ -17,6 +24,57 @@ function resolveProjectRoot() {
|
|
|
17
24
|
}
|
|
18
25
|
return dir;
|
|
19
26
|
}
|
|
27
|
+
function sleep(ms) {
|
|
28
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
}
|
|
30
|
+
function isAddressInUse(error) {
|
|
31
|
+
if (typeof error !== "object" || error === null)
|
|
32
|
+
return false;
|
|
33
|
+
const record = error;
|
|
34
|
+
return record.code === "EADDRINUSE" || String(record.message ?? "").includes("EADDRINUSE");
|
|
35
|
+
}
|
|
36
|
+
function sessionFileFromArgs(argv, env = process.env) {
|
|
37
|
+
const index = argv.indexOf("--session-file");
|
|
38
|
+
if (index >= 0 && argv[index + 1] && !argv[index + 1].startsWith("--")) {
|
|
39
|
+
return argv[index + 1];
|
|
40
|
+
}
|
|
41
|
+
return env.MICA_SESSION_FILE ?? defaultSessionFile(env);
|
|
42
|
+
}
|
|
43
|
+
export async function readLiveSession(argv = [], deps = {}) {
|
|
44
|
+
const env = deps.env ?? process.env;
|
|
45
|
+
const exists = deps.exists ?? existsSync;
|
|
46
|
+
const readFile = deps.readFile ?? ((filePath) => readFileSync(filePath, "utf8"));
|
|
47
|
+
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
48
|
+
const sessionFile = sessionFileFromArgs(argv, env);
|
|
49
|
+
if (!exists(sessionFile) || typeof fetchImpl !== "function")
|
|
50
|
+
return undefined;
|
|
51
|
+
let session;
|
|
52
|
+
try {
|
|
53
|
+
session = JSON.parse(readFile(sessionFile));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (!session.baseUrl || !session.authToken)
|
|
59
|
+
return undefined;
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetchImpl(`${session.baseUrl}/status`, {
|
|
62
|
+
headers: { authorization: `Bearer ${session.authToken}` },
|
|
63
|
+
});
|
|
64
|
+
return response.status === 200 ? { baseUrl: session.baseUrl, authToken: session.authToken } : undefined;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function startProxyMcpRuntime(session, deps = {}) {
|
|
71
|
+
const server = deps.createMcpServer?.() ?? createMicaMcpServer("mica-proxy");
|
|
72
|
+
registerProxyMcpTools(server, session);
|
|
73
|
+
registerMicaPrompts(server);
|
|
74
|
+
const transport = deps.createTransport?.() ?? new StdioServerTransport();
|
|
75
|
+
await server.connect(transport);
|
|
76
|
+
return { keepAlive: new Promise(() => { }) };
|
|
77
|
+
}
|
|
20
78
|
// ---------------------------------------------------------------------------
|
|
21
79
|
// Public API
|
|
22
80
|
// ---------------------------------------------------------------------------
|
|
@@ -25,6 +83,9 @@ export function helpText() {
|
|
|
25
83
|
|
|
26
84
|
Commands:
|
|
27
85
|
start Start the MICA bridge runtime (default)
|
|
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
|
|
28
89
|
install [options] Install MICA bridge into Wolfram
|
|
29
90
|
uninstall [options] Uninstall MICA bridge from Wolfram
|
|
30
91
|
doctor Diagnose MICA bridge configuration
|
|
@@ -32,6 +93,7 @@ Commands:
|
|
|
32
93
|
config codex Configure for Codex
|
|
33
94
|
config claude-desktop Configure for Claude Desktop
|
|
34
95
|
config cursor Configure for Cursor
|
|
96
|
+
config opencode Configure for OpenCode
|
|
35
97
|
|
|
36
98
|
Options:
|
|
37
99
|
--help, -h Show this help message
|
|
@@ -44,6 +106,12 @@ export async function runCli(argv, deps) {
|
|
|
44
106
|
const startRuntime = deps?.startRuntime;
|
|
45
107
|
const runInstaller = deps?.runInstaller;
|
|
46
108
|
const _runDoctor = deps?.runDoctor;
|
|
109
|
+
const _runStatus = deps?.runStatus;
|
|
110
|
+
const _runConfig = deps?.runConfig;
|
|
111
|
+
const _runStop = deps?.runStop;
|
|
112
|
+
const _readLiveSession = deps?.readLiveSession ?? (() => readLiveSession(argv.slice(1)));
|
|
113
|
+
const _startProxyRuntime = deps?.startProxyRuntime ?? ((session) => startProxyMcpRuntime(session));
|
|
114
|
+
const _sleep = deps?.sleep ?? sleep;
|
|
47
115
|
const command = argv[0];
|
|
48
116
|
// --help / -h
|
|
49
117
|
if (command === "--help" || command === "-h") {
|
|
@@ -72,6 +140,13 @@ export async function runCli(argv, deps) {
|
|
|
72
140
|
}
|
|
73
141
|
// start or no args
|
|
74
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
|
+
}
|
|
75
150
|
if (!startRuntime) {
|
|
76
151
|
stderr.write("Error: startRuntime not available\n");
|
|
77
152
|
return 1;
|
|
@@ -80,6 +155,40 @@ export async function runCli(argv, deps) {
|
|
|
80
155
|
await runtime.keepAlive;
|
|
81
156
|
return 0;
|
|
82
157
|
}
|
|
158
|
+
// mcp
|
|
159
|
+
if (command === "mcp") {
|
|
160
|
+
const existingSession = await _readLiveSession();
|
|
161
|
+
if (existingSession) {
|
|
162
|
+
const proxyRuntime = await _startProxyRuntime(existingSession);
|
|
163
|
+
await proxyRuntime.keepAlive;
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
if (!startRuntime) {
|
|
167
|
+
stderr.write("Error: startRuntime not available\n");
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const runtime = await startRuntime();
|
|
172
|
+
await runtime.keepAlive;
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (!isAddressInUse(error))
|
|
177
|
+
throw error;
|
|
178
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
179
|
+
await _sleep(100);
|
|
180
|
+
const racedSession = await _readLiveSession();
|
|
181
|
+
if (racedSession) {
|
|
182
|
+
const proxyRuntime = await _startProxyRuntime(racedSession);
|
|
183
|
+
await proxyRuntime.keepAlive;
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
+
stderr.write(`Error: MICA bridge port is already in use, but no live session became available. ${message}\n`);
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
83
192
|
// doctor
|
|
84
193
|
if (command === "doctor") {
|
|
85
194
|
if (!_runDoctor) {
|
|
@@ -90,10 +199,52 @@ export async function runCli(argv, deps) {
|
|
|
90
199
|
stdout.write(output);
|
|
91
200
|
return exitCode;
|
|
92
201
|
}
|
|
93
|
-
//
|
|
94
|
-
if (command === "status"
|
|
95
|
-
|
|
96
|
-
|
|
202
|
+
// status
|
|
203
|
+
if (command === "status") {
|
|
204
|
+
if (!_runStatus) {
|
|
205
|
+
stderr.write("Error: runStatus not available\n");
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
const { exitCode, output } = await _runStatus();
|
|
209
|
+
stdout.write(output);
|
|
210
|
+
return exitCode;
|
|
211
|
+
}
|
|
212
|
+
// config
|
|
213
|
+
if (command === "config") {
|
|
214
|
+
if (!_runConfig) {
|
|
215
|
+
stderr.write("Error: runConfig not available\n");
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
const { exitCode, output } = _runConfig(argv.slice(1));
|
|
219
|
+
stdout.write(output);
|
|
220
|
+
return exitCode;
|
|
221
|
+
}
|
|
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;
|
|
97
248
|
}
|
|
98
249
|
// Unknown command
|
|
99
250
|
stderr.write(`Unknown command: ${command}\n`);
|
|
@@ -108,7 +259,12 @@ async function main() {
|
|
|
108
259
|
const { runInstaller, detectWolframUserBase } = (await import(installerUrl));
|
|
109
260
|
const exitCode = await runCli(process.argv.slice(2), {
|
|
110
261
|
startRuntime: async () => startBunRuntime(),
|
|
262
|
+
startProxyRuntime: async (session) => startProxyMcpRuntime(session),
|
|
263
|
+
readLiveSession: async () => readLiveSession(process.argv.slice(3)),
|
|
111
264
|
runInstaller,
|
|
265
|
+
runStatus: async () => runStatusCommand(),
|
|
266
|
+
runConfig: runConfigCommand,
|
|
267
|
+
runStop: async () => runStopCommand(),
|
|
112
268
|
runDoctor: async () => runDoctor({
|
|
113
269
|
projectRoot,
|
|
114
270
|
detectWolframUserBase: () => detectWolframUserBase(),
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { defaultSessionFile } from "../runtime/config.js";
|
|
3
|
+
export async function runStatusCommand(deps = {}) {
|
|
4
|
+
const env = deps.env ?? process.env;
|
|
5
|
+
const _exists = deps.exists ?? existsSync;
|
|
6
|
+
const _readFile = deps.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
7
|
+
const _fetch = deps.fetch ??
|
|
8
|
+
(typeof globalThis.fetch === "function"
|
|
9
|
+
? (url, init) => globalThis.fetch(url, init)
|
|
10
|
+
: undefined);
|
|
11
|
+
const lines = ["MICA status", ""];
|
|
12
|
+
const sessionFile = env.MICA_SESSION_FILE ?? defaultSessionFile(env);
|
|
13
|
+
const fail = (label, detail) => lines.push(`FAIL ${label}: ${detail}`);
|
|
14
|
+
const ok = (label, detail) => lines.push(`OK ${label}: ${detail}`);
|
|
15
|
+
if (!_exists(sessionFile)) {
|
|
16
|
+
fail("Session file", `${sessionFile} (not found)`);
|
|
17
|
+
lines.push("FIX Run: mica start");
|
|
18
|
+
return result(1, lines, false);
|
|
19
|
+
}
|
|
20
|
+
let session;
|
|
21
|
+
try {
|
|
22
|
+
session = JSON.parse(_readFile(sessionFile));
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
fail("Session file", error instanceof Error ? error.message : String(error));
|
|
26
|
+
lines.push("FIX Run: mica start");
|
|
27
|
+
return result(1, lines, false);
|
|
28
|
+
}
|
|
29
|
+
const baseUrl = session.baseUrl ?? `http://${session.host ?? "127.0.0.1"}:${session.port ?? 19791}`;
|
|
30
|
+
ok("Session file", sessionFile);
|
|
31
|
+
ok("Session target", baseUrl);
|
|
32
|
+
if (!session.authToken) {
|
|
33
|
+
fail("Auth token", "missing in session file");
|
|
34
|
+
lines.push("FIX Restart MICA with: mica start");
|
|
35
|
+
return result(1, lines, false);
|
|
36
|
+
}
|
|
37
|
+
if (!_fetch) {
|
|
38
|
+
fail("Server /status reachable", "fetch unavailable");
|
|
39
|
+
return result(1, lines, false);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const response = await _fetch(`${baseUrl}/status`, {
|
|
43
|
+
headers: { Authorization: `Bearer ${session.authToken}` },
|
|
44
|
+
});
|
|
45
|
+
if (response.status === 401) {
|
|
46
|
+
fail("Auth token", "401 Unauthorized");
|
|
47
|
+
lines.push("FIX Restart MICA with: mica start");
|
|
48
|
+
return result(1, lines, false);
|
|
49
|
+
}
|
|
50
|
+
if (response.status !== 200) {
|
|
51
|
+
fail("Server /status reachable", `HTTP ${response.status}`);
|
|
52
|
+
lines.push("FIX Run: mica start");
|
|
53
|
+
return result(1, lines, false);
|
|
54
|
+
}
|
|
55
|
+
const body = (await response.json());
|
|
56
|
+
const server = readRecord(body.server);
|
|
57
|
+
const agents = Array.isArray(body.agents) ? body.agents : [];
|
|
58
|
+
const notebooks = Array.isArray(body.notebooks) ? body.notebooks : [];
|
|
59
|
+
ok("Server", String(server.state ?? "running"));
|
|
60
|
+
ok("Version", String(server.version ?? session.version ?? "unknown"));
|
|
61
|
+
if (typeof session.pid === "number")
|
|
62
|
+
ok("PID", String(session.pid));
|
|
63
|
+
ok("Agents", String(agents.length));
|
|
64
|
+
ok("Notebooks", String(notebooks.length));
|
|
65
|
+
lines.push(`Dashboard: ${baseUrl}/#token=${session.authToken}`);
|
|
66
|
+
return result(0, lines, true);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
fail("Server /status reachable", error instanceof Error ? error.message : String(error));
|
|
70
|
+
lines.push("FIX Run: mica start");
|
|
71
|
+
return result(1, lines, false);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function readRecord(value) {
|
|
75
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
76
|
+
? value
|
|
77
|
+
: {};
|
|
78
|
+
}
|
|
79
|
+
function result(exitCode, lines, running) {
|
|
80
|
+
return { exitCode, output: `${lines.join("\n")}\n`, running };
|
|
81
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
}
|
|
@@ -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 = "
|
|
36
|
+
export function createMicaMcpServer(name, version = "1.0.2") {
|
|
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
package/src/bun/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { writeSessionFile } from "../runtime/session.js";
|
|
|
11
11
|
import { createBunHttpApp } from "./httpServer.js";
|
|
12
12
|
|
|
13
13
|
const MCP_SERVER_NAME = "mica-bun";
|
|
14
|
-
const MICA_PACKAGE_VERSION = "
|
|
14
|
+
const MICA_PACKAGE_VERSION = "1.0.2";
|
|
15
15
|
|
|
16
16
|
export type BunRuntimeDeps = {
|
|
17
17
|
bridgeOnly?: boolean;
|