@aliceshimada/mica 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/CONTRIBUTING.md +22 -0
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/README.zh-CN.md +308 -0
- package/SECURITY.md +22 -0
- package/dist/src/backend/agentRegistry.js +115 -0
- package/dist/src/backend/backendQueue.js +212 -0
- package/dist/src/backend/backendState.js +99 -0
- package/dist/src/backend/notebookRegistry.js +136 -0
- package/dist/src/backend/protocol.js +32 -0
- package/dist/src/bridge/httpBridge.js +366 -0
- package/dist/src/bridge/requestQueue.js +200 -0
- package/dist/src/bun/dashboard.js +387 -0
- package/dist/src/bun/httpServer.js +356 -0
- package/dist/src/bun/index.js +91 -0
- package/dist/src/cli/doctor.js +235 -0
- package/dist/src/cli/index.js +125 -0
- package/dist/src/index.js +54 -0
- package/dist/src/mcp/backendTools.js +216 -0
- package/dist/src/mcp/descriptions.js +6 -0
- package/dist/src/mcp/prompts.js +52 -0
- package/dist/src/mcp/toolResults.js +183 -0
- package/dist/src/mcp/toolSchemas.js +60 -0
- package/dist/src/mcp/tools.js +161 -0
- package/dist/src/runtime/config.js +76 -0
- package/dist/src/runtime/session.js +14 -0
- package/dist/src/runtimeOptions.js +3 -0
- package/dist/src/types.js +2 -0
- package/package.json +63 -0
- package/paclet/FrontEnd/Palettes/MMAAgentBridge.nb +22 -0
- package/paclet/Kernel/MMAAgentBridge.wl +1831 -0
- package/paclet/Kernel/init.wl +1 -0
- package/paclet/PacletInfo.wl +14 -0
- package/scripts/install.js +526 -0
- package/src/bun/index.ts +120 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Get[FileNameJoin[{DirectoryName[$InputFileName], "MMAAgentBridge.wl"}]];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
PacletObject[
|
|
2
|
+
<|
|
|
3
|
+
"Name" -> "MMAAgentBridge",
|
|
4
|
+
"Version" -> "0.1.0",
|
|
5
|
+
(* Supported: 14.1+. Mathematica 13.x / 14.0 are experimental and not declared here. *)
|
|
6
|
+
"WolframVersion" -> "14.1+",
|
|
7
|
+
"Description" -> "Local Palette bridge that lets an MCP agent operate the active notebook through FrontEnd APIs.",
|
|
8
|
+
"Creator" -> "MMA MCP Bridge",
|
|
9
|
+
"Extensions" -> {
|
|
10
|
+
{"Kernel", "Root" -> "Kernel", "Context" -> "MMAAgentBridge`"},
|
|
11
|
+
{"FrontEnd"}
|
|
12
|
+
}
|
|
13
|
+
|>
|
|
14
|
+
]
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
export const HIDDEN_BEGIN =
|
|
9
|
+
"(* BEGIN MICA hidden-agent autoload *)";
|
|
10
|
+
export const HIDDEN_END =
|
|
11
|
+
"(* END MICA hidden-agent autoload *)";
|
|
12
|
+
export const CONTROL_BEGIN =
|
|
13
|
+
"(* BEGIN MICA control-kernel autoload *)";
|
|
14
|
+
export const CONTROL_END =
|
|
15
|
+
"(* END MICA control-kernel autoload *)";
|
|
16
|
+
const OLD_HIDDEN_BEGIN = "(* BEGIN MMA MCP Bridge hidden-agent autoload *)";
|
|
17
|
+
const OLD_HIDDEN_END = "(* END MMA MCP Bridge hidden-agent autoload *)";
|
|
18
|
+
const OLD_CONTROL_BEGIN = "(* BEGIN MMA MCP Bridge control-kernel autoload *)";
|
|
19
|
+
const OLD_CONTROL_END = "(* END MMA MCP Bridge control-kernel autoload *)";
|
|
20
|
+
export const STANDARD_INIT_HEADER =
|
|
21
|
+
"(* User Wolfram Kernel/init.m. MICA preserves user content outside marked blocks. *)\n";
|
|
22
|
+
|
|
23
|
+
const REQUIRED_BRIDGE_FILES = [
|
|
24
|
+
"package.json",
|
|
25
|
+
path.join("src", "bun", "index.ts"),
|
|
26
|
+
path.join("paclet", "Kernel", "MMAAgentBridge.wl"),
|
|
27
|
+
path.join("paclet", "PacletInfo.wl"),
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function parseArgs(argv) {
|
|
31
|
+
const options = { dryRun: false, uninstall: false, help: false };
|
|
32
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
33
|
+
const arg = argv[index];
|
|
34
|
+
if (arg === "--dry-run") {
|
|
35
|
+
options.dryRun = true;
|
|
36
|
+
} else if (arg === "--uninstall") {
|
|
37
|
+
options.uninstall = true;
|
|
38
|
+
} else if (arg === "--bridge-root") {
|
|
39
|
+
const value = argv[index + 1];
|
|
40
|
+
if (!value || value.startsWith("--"))
|
|
41
|
+
throw new Error("--bridge-root requires a value");
|
|
42
|
+
options.bridgeRoot = value;
|
|
43
|
+
index += 1;
|
|
44
|
+
} else if (arg === "--wolfram-userbase") {
|
|
45
|
+
const value = argv[index + 1];
|
|
46
|
+
if (!value || value.startsWith("--"))
|
|
47
|
+
throw new Error("--wolfram-userbase requires a value");
|
|
48
|
+
options.wolframUserBase = value;
|
|
49
|
+
index += 1;
|
|
50
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
51
|
+
options.help = true;
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return options;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function helpText() {
|
|
60
|
+
return `Usage: node scripts/install.js [options]
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
--dry-run Preview init.m changes without writing files
|
|
64
|
+
--uninstall Remove MICA marked autoload blocks
|
|
65
|
+
--wolfram-userbase <path> Use a specific Wolfram user base directory
|
|
66
|
+
--bridge-root <path> Use a specific MICA checkout
|
|
67
|
+
-h, --help Show this help
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ensureNode20(version = process.versions.node) {
|
|
72
|
+
const major = Number.parseInt(version.split(".")[0] ?? "0", 10);
|
|
73
|
+
if (!Number.isFinite(major) || major < 20) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Node >=20 is required. Current Node version is ${version}.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function defaultBridgeRoot() {
|
|
81
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function validateBridgeRoot(bridgeRoot, exists = existsSync) {
|
|
85
|
+
const absoluteRoot = path.resolve(bridgeRoot);
|
|
86
|
+
const missing = REQUIRED_BRIDGE_FILES.filter(
|
|
87
|
+
(relativePath) => !exists(path.join(absoluteRoot, relativePath))
|
|
88
|
+
);
|
|
89
|
+
if (missing.length > 0) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Invalid bridge root: ${absoluteRoot}\nMissing required files:\n${missing
|
|
92
|
+
.map((file) => `- ${file}`)
|
|
93
|
+
.join("\n")}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return absoluteRoot;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function trimWolframscriptPath(stdout) {
|
|
100
|
+
const line = String(stdout ?? "")
|
|
101
|
+
.split(/\r?\n/)
|
|
102
|
+
.map((part) => part.trim())
|
|
103
|
+
.find(Boolean);
|
|
104
|
+
if (!line) return undefined;
|
|
105
|
+
return line.replace(/^"|"$/g, "");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function readWolframscriptUserBase(runner = spawnSync) {
|
|
109
|
+
const result = runner("wolframscript", ["-code", "$UserBaseDirectory"], {
|
|
110
|
+
encoding: "utf8",
|
|
111
|
+
});
|
|
112
|
+
if (result.error || result.status !== 0) return undefined;
|
|
113
|
+
return trimWolframscriptPath(result.stdout);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function detectWolframUserBase({
|
|
117
|
+
override,
|
|
118
|
+
platform = process.platform,
|
|
119
|
+
env = process.env,
|
|
120
|
+
homedir = os.homedir(),
|
|
121
|
+
exists = existsSync,
|
|
122
|
+
runWolframscript = () => readWolframscriptUserBase(),
|
|
123
|
+
} = {}) {
|
|
124
|
+
if (override) {
|
|
125
|
+
return {
|
|
126
|
+
userBase: path.resolve(override),
|
|
127
|
+
source: "--wolfram-userbase",
|
|
128
|
+
warnings: [],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const fromWolframscript = runWolframscript();
|
|
133
|
+
if (fromWolframscript) {
|
|
134
|
+
return {
|
|
135
|
+
userBase: path.resolve(fromWolframscript),
|
|
136
|
+
source: "wolframscript",
|
|
137
|
+
warnings: [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const warnings = [
|
|
142
|
+
"wolframscript was not available or did not return $UserBaseDirectory; using platform fallback.",
|
|
143
|
+
];
|
|
144
|
+
if (platform === "win32") {
|
|
145
|
+
const appData = env.APPDATA;
|
|
146
|
+
if (!appData)
|
|
147
|
+
throw new Error(
|
|
148
|
+
"Cannot resolve Wolfram user base: APPDATA is not set and wolframscript was unavailable."
|
|
149
|
+
);
|
|
150
|
+
const wolframBase = path.join(appData, "Wolfram");
|
|
151
|
+
const mathematicaBase = path.join(appData, "Mathematica");
|
|
152
|
+
return {
|
|
153
|
+
userBase: exists(wolframBase) ? wolframBase : mathematicaBase,
|
|
154
|
+
source: "platform fallback",
|
|
155
|
+
warnings,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (platform === "darwin") {
|
|
159
|
+
return {
|
|
160
|
+
userBase: path.join(homedir, "Library", "Wolfram"),
|
|
161
|
+
source: "platform fallback",
|
|
162
|
+
warnings,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
userBase: path.join(homedir, ".Wolfram"),
|
|
167
|
+
source: "platform fallback",
|
|
168
|
+
warnings,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function wolframString(value) {
|
|
173
|
+
return `"${String(value)
|
|
174
|
+
.replace(/\\/g, "\\\\")
|
|
175
|
+
.replace(/"/g, '\\"')
|
|
176
|
+
.replace(/\r/g, "\\r")
|
|
177
|
+
.replace(/\n/g, "\\n")
|
|
178
|
+
.replace(/\t/g, "\\t")}"`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function generateAutoloadBlock(bridgeSourcePath) {
|
|
182
|
+
return `${CONTROL_BEGIN}
|
|
183
|
+
Quiet @ Check[
|
|
184
|
+
With[{bridgePath = ${wolframString(bridgeSourcePath)}},
|
|
185
|
+
If[
|
|
186
|
+
TrueQ[$Notebooks] &&
|
|
187
|
+
FileExistsQ[bridgePath] &&
|
|
188
|
+
!TrueQ[Quiet @ Check[CurrentValue[$FrontEndSession, {TaggingRules, "MMAAgentBridge", "ControlKernelBooting"}], False]] &&
|
|
189
|
+
!TrueQ[Quiet @ Check[CurrentValue[$FrontEndSession, {TaggingRules, "MMAAgentBridge", "AgentRunning"}], False]],
|
|
190
|
+
Get[bridgePath];
|
|
191
|
+
MMAAgentBridge\`Private\`$BridgePermissions = <|
|
|
192
|
+
"ReadNotebook" -> True,
|
|
193
|
+
"InsertCell" -> True,
|
|
194
|
+
"ModifyCell" -> True,
|
|
195
|
+
"DeleteCell" -> True,
|
|
196
|
+
"RunCell" -> True,
|
|
197
|
+
"SaveNotebook" -> False
|
|
198
|
+
|>;
|
|
199
|
+
MMAAgentBridge\`StartMMAAgentControlKernel[];
|
|
200
|
+
];
|
|
201
|
+
];
|
|
202
|
+
Null,
|
|
203
|
+
Null
|
|
204
|
+
];
|
|
205
|
+
${CONTROL_END}
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function removeOneBlockPair(content, begin, end) {
|
|
210
|
+
let result = content;
|
|
211
|
+
let removed = 0;
|
|
212
|
+
while (result.includes(begin)) {
|
|
213
|
+
const start = result.indexOf(begin);
|
|
214
|
+
const endStart = result.indexOf(end, start + begin.length);
|
|
215
|
+
if (endStart < 0)
|
|
216
|
+
throw new Error(`Found ${begin} without matching ${end}`);
|
|
217
|
+
let removeEnd = endStart + end.length;
|
|
218
|
+
if (result.slice(removeEnd, removeEnd + 2) === "\r\n") removeEnd += 2;
|
|
219
|
+
else if (result.slice(removeEnd, removeEnd + 1) === "\n") removeEnd += 1;
|
|
220
|
+
result = result.slice(0, start) + result.slice(removeEnd);
|
|
221
|
+
removed += 1;
|
|
222
|
+
}
|
|
223
|
+
return { content: result, removed };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function removeBridgeBlocks(content) {
|
|
227
|
+
const withoutHidden = removeOneBlockPair(
|
|
228
|
+
content,
|
|
229
|
+
HIDDEN_BEGIN,
|
|
230
|
+
HIDDEN_END
|
|
231
|
+
);
|
|
232
|
+
const withoutOldHidden = removeOneBlockPair(
|
|
233
|
+
withoutHidden.content,
|
|
234
|
+
OLD_HIDDEN_BEGIN,
|
|
235
|
+
OLD_HIDDEN_END
|
|
236
|
+
);
|
|
237
|
+
const withoutControl = removeOneBlockPair(
|
|
238
|
+
withoutOldHidden.content,
|
|
239
|
+
CONTROL_BEGIN,
|
|
240
|
+
CONTROL_END
|
|
241
|
+
);
|
|
242
|
+
const withoutOldControl = removeOneBlockPair(
|
|
243
|
+
withoutControl.content,
|
|
244
|
+
OLD_CONTROL_BEGIN,
|
|
245
|
+
OLD_CONTROL_END
|
|
246
|
+
);
|
|
247
|
+
return {
|
|
248
|
+
content: withoutOldControl.content.replace(/(?:\r?\n){3,}/g, "\n\n"),
|
|
249
|
+
removed: withoutHidden.removed + withoutOldHidden.removed + withoutControl.removed + withoutOldControl.removed,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function ensureTrailingNewline(content) {
|
|
254
|
+
if (content.length === 0 || content.endsWith("\n")) return content;
|
|
255
|
+
return `${content}\n`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function applyInstallToContent(existingContent, autoloadBlock) {
|
|
259
|
+
const originalBase =
|
|
260
|
+
existingContent.length > 0 ? existingContent : STANDARD_INIT_HEADER;
|
|
261
|
+
const removed = removeBridgeBlocks(originalBase);
|
|
262
|
+
const preserved = ensureTrailingNewline(removed.content.trimEnd());
|
|
263
|
+
const separator = preserved.length > 0 ? "\n" : "";
|
|
264
|
+
const content = `${preserved}${separator}${autoloadBlock}`;
|
|
265
|
+
return {
|
|
266
|
+
content,
|
|
267
|
+
removed: removed.removed,
|
|
268
|
+
changed: content !== existingContent,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function applyUninstallToContent(existingContent) {
|
|
273
|
+
const removed = removeBridgeBlocks(existingContent);
|
|
274
|
+
if (removed.removed === 0) {
|
|
275
|
+
return { content: existingContent, removed: 0, changed: false };
|
|
276
|
+
}
|
|
277
|
+
const content = ensureTrailingNewline(removed.content.trimEnd());
|
|
278
|
+
return {
|
|
279
|
+
content,
|
|
280
|
+
removed: removed.removed,
|
|
281
|
+
changed: content !== existingContent,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function countLines(content) {
|
|
286
|
+
if (content.length === 0) return 0;
|
|
287
|
+
return content.split(/\r?\n/).length;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function countCurrentBlocks(content) {
|
|
291
|
+
return content.split(CONTROL_BEGIN).length - 1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function summarizeContentChange(before, after, removed) {
|
|
295
|
+
const controlBlocks = countCurrentBlocks(after);
|
|
296
|
+
return [
|
|
297
|
+
`Before lines: ${countLines(before)}`,
|
|
298
|
+
`After lines: ${countLines(after)}`,
|
|
299
|
+
`Bridge blocks to remove: ${removed}`,
|
|
300
|
+
`New control block present after change: ${
|
|
301
|
+
controlBlocks > 0 ? "yes" : "no"
|
|
302
|
+
}`,
|
|
303
|
+
`Control blocks after change: ${controlBlocks}`,
|
|
304
|
+
].join("\n");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function timestamp(date = new Date()) {
|
|
308
|
+
return date
|
|
309
|
+
.toISOString()
|
|
310
|
+
.replace(/[-:]/g, "")
|
|
311
|
+
.replace(/\.\d{3}Z$/, "Z");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function nextBackupPath(initPath, exists = existsSync, date = new Date()) {
|
|
315
|
+
const base = `${initPath}.${timestamp(date)}.bak`;
|
|
316
|
+
if (!exists(base)) return base;
|
|
317
|
+
for (let index = 1; index < 1000; index += 1) {
|
|
318
|
+
const candidate = `${base}.${index}`;
|
|
319
|
+
if (!exists(candidate)) return candidate;
|
|
320
|
+
}
|
|
321
|
+
throw new Error(
|
|
322
|
+
`Could not choose an unused backup path next to ${initPath}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function commandAvailable(command) {
|
|
327
|
+
const result = spawnSync(command, ["--version"], { encoding: "utf8" });
|
|
328
|
+
return !result.error && result.status === 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function shellQuote(value) {
|
|
332
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function resolveCommandPath(command, runner = spawnSync) {
|
|
336
|
+
if (path.isAbsolute(command) || /[\\/]/.test(command)) return command;
|
|
337
|
+
|
|
338
|
+
const result =
|
|
339
|
+
process.platform === "win32"
|
|
340
|
+
? runner("where", [command], { encoding: "utf8" })
|
|
341
|
+
: runner("sh", ["-lc", `command -v ${shellQuote(command)}`], {
|
|
342
|
+
encoding: "utf8",
|
|
343
|
+
});
|
|
344
|
+
if (result.error || result.status !== 0) return undefined;
|
|
345
|
+
return trimWolframscriptPath(result.stdout);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function renderWolframStartupSnippet(bridgeRoot) {
|
|
349
|
+
const bridgeSourcePath = path.join(
|
|
350
|
+
bridgeRoot,
|
|
351
|
+
"paclet",
|
|
352
|
+
"Kernel",
|
|
353
|
+
"MMAAgentBridge.wl"
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
return [
|
|
357
|
+
"Manual Wolfram Desktop startup fallback:",
|
|
358
|
+
`Get[${wolframString(bridgeSourcePath)}];`,
|
|
359
|
+
"MMAAgentBridge`Private`$BridgePermissions = <|",
|
|
360
|
+
' "ReadNotebook" -> True,',
|
|
361
|
+
' "InsertCell" -> True,',
|
|
362
|
+
' "ModifyCell" -> True,',
|
|
363
|
+
' "DeleteCell" -> True,',
|
|
364
|
+
' "RunCell" -> True,',
|
|
365
|
+
' "SaveNotebook" -> False',
|
|
366
|
+
"|>;",
|
|
367
|
+
"MMAAgentBridge`StartMMAAgentControlKernel[]",
|
|
368
|
+
].join("\n");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function renderMcpSnippets(
|
|
372
|
+
bridgeRoot,
|
|
373
|
+
{ bunCommand = "bun", nodeCommand = "node" } = {}
|
|
374
|
+
) {
|
|
375
|
+
const nodeSnippet = {
|
|
376
|
+
mcpServers: {
|
|
377
|
+
"mica": {
|
|
378
|
+
command: nodeCommand,
|
|
379
|
+
args: [path.join(bridgeRoot, "dist", "src", "bun", "index.js")],
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
const bunSnippet = {
|
|
384
|
+
mcpServers: {
|
|
385
|
+
"mica": {
|
|
386
|
+
command: bunCommand,
|
|
387
|
+
args: ["run", path.join(bridgeRoot, "src", "bun", "index.ts")],
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
return [
|
|
392
|
+
"MCP config snippets (copy into your MCP client config; this installer does not edit it):",
|
|
393
|
+
"",
|
|
394
|
+
"Production (built Node):",
|
|
395
|
+
JSON.stringify(nodeSnippet, null, 2),
|
|
396
|
+
"",
|
|
397
|
+
"Development (Bun):",
|
|
398
|
+
JSON.stringify(bunSnippet, null, 2),
|
|
399
|
+
"",
|
|
400
|
+
renderWolframStartupSnippet(bridgeRoot),
|
|
401
|
+
].join("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function renderVersionSupport() {
|
|
405
|
+
return [
|
|
406
|
+
"Version support:",
|
|
407
|
+
" Supported: Wolfram Desktop / Mathematica 14.1+",
|
|
408
|
+
" Experimental: Mathematica 13.x / 14.0",
|
|
409
|
+
" Unsupported: Headless Wolfram Engine for live notebook control",
|
|
410
|
+
].join("\n");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function runInstaller(argv = process.argv.slice(2)) {
|
|
414
|
+
const options = parseArgs(argv);
|
|
415
|
+
if (options.help) return helpText();
|
|
416
|
+
ensureNode20();
|
|
417
|
+
|
|
418
|
+
const bridgeRoot = validateBridgeRoot(
|
|
419
|
+
options.bridgeRoot ?? defaultBridgeRoot()
|
|
420
|
+
);
|
|
421
|
+
const detection = detectWolframUserBase({
|
|
422
|
+
override: options.wolframUserBase,
|
|
423
|
+
});
|
|
424
|
+
const kernelDir = path.join(detection.userBase, "Kernel");
|
|
425
|
+
const initPath = path.join(kernelDir, "init.m");
|
|
426
|
+
const initExists = existsSync(initPath);
|
|
427
|
+
const before = initExists ? readFileSync(initPath, "utf8") : "";
|
|
428
|
+
const bridgeSourcePath = path.join(
|
|
429
|
+
bridgeRoot,
|
|
430
|
+
"paclet",
|
|
431
|
+
"Kernel",
|
|
432
|
+
"MMAAgentBridge.wl"
|
|
433
|
+
);
|
|
434
|
+
const change = options.uninstall
|
|
435
|
+
? applyUninstallToContent(before)
|
|
436
|
+
: applyInstallToContent(before, generateAutoloadBlock(bridgeSourcePath));
|
|
437
|
+
|
|
438
|
+
const warnings = [...detection.warnings];
|
|
439
|
+
if (!commandAvailable("bun"))
|
|
440
|
+
warnings.push(
|
|
441
|
+
"bun was not found on PATH; the Bun MCP snippet is still printed for machines that have Bun installed."
|
|
442
|
+
);
|
|
443
|
+
const nodeFallbackPath = path.join(bridgeRoot, "dist", "src", "index.js");
|
|
444
|
+
if (!existsSync(nodeFallbackPath))
|
|
445
|
+
warnings.push(
|
|
446
|
+
`Built Node fallback is missing: ${nodeFallbackPath}\nRun npm run build before using the Node fallback snippet.`
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const lines = [];
|
|
450
|
+
lines.push(
|
|
451
|
+
options.dryRun
|
|
452
|
+
? "Dry run: no files written"
|
|
453
|
+
: !change.changed && options.uninstall
|
|
454
|
+
? "No MICA Wolfram autoload was present"
|
|
455
|
+
: options.uninstall
|
|
456
|
+
? "Removed MICA Wolfram autoload"
|
|
457
|
+
: "Installed MICA Wolfram autoload"
|
|
458
|
+
);
|
|
459
|
+
lines.push(`Bridge root: ${bridgeRoot}`);
|
|
460
|
+
lines.push(
|
|
461
|
+
`Wolfram user base: ${detection.userBase} (${detection.source})`
|
|
462
|
+
);
|
|
463
|
+
lines.push(`Target init.m: ${initPath}`);
|
|
464
|
+
lines.push(
|
|
465
|
+
summarizeContentChange(before, change.content, change.removed)
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
if (!options.uninstall) {
|
|
469
|
+
lines.push(renderVersionSupport());
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!options.dryRun && change.changed) {
|
|
473
|
+
mkdirSync(kernelDir, { recursive: true });
|
|
474
|
+
const backupPath = nextBackupPath(initPath);
|
|
475
|
+
writeFileSync(backupPath, before, "utf8");
|
|
476
|
+
writeFileSync(initPath, change.content, "utf8");
|
|
477
|
+
lines.push(`Backup written: ${backupPath}`);
|
|
478
|
+
} else if (!change.changed) {
|
|
479
|
+
if (!initExists && options.uninstall) {
|
|
480
|
+
lines.push("No init.m existed; nothing was written.");
|
|
481
|
+
} else {
|
|
482
|
+
lines.push("No changes needed.");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (warnings.length > 0) {
|
|
487
|
+
lines.push("Warnings:");
|
|
488
|
+
for (const warning of warnings) lines.push(`- ${warning}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!options.uninstall) {
|
|
492
|
+
lines.push(
|
|
493
|
+
"Restart Wolfram Desktop after installing or reinstalling the autoload block."
|
|
494
|
+
);
|
|
495
|
+
lines.push(
|
|
496
|
+
renderMcpSnippets(bridgeRoot, {
|
|
497
|
+
bunCommand: resolveCommandPath("bun") ?? "bun",
|
|
498
|
+
nodeCommand: process.execPath || resolveCommandPath("node") || "node",
|
|
499
|
+
})
|
|
500
|
+
);
|
|
501
|
+
lines.push("Verification commands:");
|
|
502
|
+
lines.push(" npm test");
|
|
503
|
+
lines.push(" npm run typecheck");
|
|
504
|
+
lines.push(" npm run build");
|
|
505
|
+
lines.push(" node scripts/install.js --dry-run");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return `${lines.join("\n")}\n`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function main() {
|
|
512
|
+
try {
|
|
513
|
+
process.stdout.write(runInstaller(process.argv.slice(2)));
|
|
514
|
+
} catch (error) {
|
|
515
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
516
|
+
process.stderr.write(`ERROR: ${message}\n`);
|
|
517
|
+
process.exitCode = 1;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const invokedPath = process.argv[1]
|
|
522
|
+
? pathToFileURL(path.resolve(process.argv[1])).href
|
|
523
|
+
: "";
|
|
524
|
+
if (import.meta.url === invokedPath) {
|
|
525
|
+
main();
|
|
526
|
+
}
|
package/src/bun/index.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
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 = "0.1.0";
|
|
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 };
|