@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.
@@ -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
+ }
@@ -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 };