@aliceshimada/mica 1.0.5 → 1.1.1

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.1 - 2026-06-16
4
+
5
+ - Fix `RestartKernelRequest`: kill kernel via `Quit[]` before restarting.
6
+ - Fix `realpathSync` crash when `process.argv[1]` doesn't resolve.
7
+ - Fix `$BridgeNotebookPermissions` memory leak: prune on notebook close.
8
+
9
+ ## 1.1.0 - 2026-06-09
10
+
11
+ - Add `mma_kill_kernel` tool: quit a notebook's Wolfram kernel (control agent kernel is protected).
12
+ - Add `mma_restart_kernel` tool: restart a notebook's Wolfram kernel so it can evaluate cells again.
13
+
3
14
  ## 1.0.5 - 2026-06-09
4
15
 
5
16
  - Fix abort disconnection: give control evaluator a separate kernel via `LinkLaunch` instead of cloning `"Local"`.
@@ -3,7 +3,7 @@ import http from "node:http";
3
3
  import { executeBackendMcpTool } from "../mcp/backendTools.js";
4
4
  import { renderDashboard } from "./dashboard.js";
5
5
  const JSON_BODY_LIMIT_BYTES = 1024 * 1024;
6
- const DEFAULT_VERSION = "1.0.5";
6
+ const DEFAULT_VERSION = "1.1.1";
7
7
  export async function createBunHttpApp({ state, host = "127.0.0.1", port, authToken, version = DEFAULT_VERSION }) {
8
8
  const runtimeInfo = {
9
9
  host,
@@ -8,7 +8,7 @@ import { loadRuntimeConfig } from "../runtime/config.js";
8
8
  import { writeSessionFile } from "../runtime/session.js";
9
9
  import { createBunHttpApp } from "./httpServer.js";
10
10
  const MCP_SERVER_NAME = "mica-bun";
11
- const MICA_PACKAGE_VERSION = "1.0.5";
11
+ const MICA_PACKAGE_VERSION = "1.1.1";
12
12
  export async function startBunRuntime(deps = {}) {
13
13
  const config = deps.runtimeConfig ?? loadRuntimeConfig();
14
14
  const bridgeOnly = deps.bridgeOnly ?? config.bridgeOnly;
@@ -83,7 +83,7 @@ export async function runDoctor(deps = {}) {
83
83
  let sessionBaseUrl;
84
84
  if (!_exists(sessionFile)) {
85
85
  fail("Session file", `${sessionFile} (not found)`);
86
- fix("Run: mica start");
86
+ fix("Run: mica mcp");
87
87
  }
88
88
  else {
89
89
  try {
@@ -97,7 +97,7 @@ export async function runDoctor(deps = {}) {
97
97
  }
98
98
  catch (e) {
99
99
  fail("Session file", `${sessionFile} (${e instanceof Error ? e.message : String(e)})`);
100
- fix("Run: mica start");
100
+ fix("Run: mica mcp");
101
101
  }
102
102
  }
103
103
  // -----------------------------------------------------------------------
@@ -173,7 +173,7 @@ export async function runDoctor(deps = {}) {
173
173
  catch (e) {
174
174
  fail("Auth token", "server not reachable");
175
175
  fail("Server /status reachable", e instanceof Error ? e.message : String(e));
176
- fix("Run: mica start");
176
+ fix("Run: mica mcp");
177
177
  fail("Live agent count", "server not reachable");
178
178
  fail("Live notebook count", "server not reachable");
179
179
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync } from "node:fs";
2
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -222,10 +222,19 @@ async function main() {
222
222
  });
223
223
  process.exitCode = exitCode;
224
224
  }
225
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
226
- main().catch((error) => {
227
- const message = error instanceof Error ? error.stack ?? error.message : String(error);
228
- process.stderr.write(`${message}\n`);
229
- process.exitCode = 1;
230
- });
225
+ if (process.argv[1]) {
226
+ try {
227
+ const scriptReal = realpathSync(fileURLToPath(import.meta.url));
228
+ const argReal = realpathSync(process.argv[1]);
229
+ if (scriptReal === argReal) {
230
+ main().catch((error) => {
231
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
232
+ process.stderr.write(`${message}\n`);
233
+ process.exitCode = 1;
234
+ });
235
+ }
236
+ }
237
+ catch {
238
+ // process.argv[1] may not resolve (e.g. node -e, shebang edge cases)
239
+ }
231
240
  }
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { DEFAULT_TIMEOUTS_MS } from "../backend/protocol.js";
3
- import { abortEvaluationSchema, deleteCellSchema, getCellOutputSchema, insertCellSchema, listCellsSchema, modifyCellSchema, noArgsSchema, readArtifactSchema, readCellSchema, runCellSchema, selectNotebookSchema, saveNotebookSchema, symbolLookupSchema, } from "./toolSchemas.js";
3
+ import { abortEvaluationSchema, deleteCellSchema, getCellOutputSchema, insertCellSchema, killKernelSchema, listCellsSchema, modifyCellSchema, noArgsSchema, readArtifactSchema, readCellSchema, restartKernelSchema, runCellSchema, selectNotebookSchema, saveNotebookSchema, symbolLookupSchema, } from "./toolSchemas.js";
4
4
  import { INSERT_ANCHOR_GUIDANCE, notebookToolDescription } from "./descriptions.js";
5
5
  import { toolFailure, toolSuccess, withToolErrors } from "./toolResults.js";
6
6
  function assertLiveAgent(state) {
@@ -141,6 +141,22 @@ const queuedNotebookTools = [
141
141
  timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
142
142
  requiresExplicitTarget: true,
143
143
  },
144
+ {
145
+ name: "mma_kill_kernel",
146
+ summary: "Quit the Wolfram kernel for a notebook. The control agent kernel is protected and cannot be killed.",
147
+ schema: killKernelSchema.shape,
148
+ permission: "RunCell",
149
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
150
+ requiresExplicitTarget: true,
151
+ },
152
+ {
153
+ name: "mma_restart_kernel",
154
+ summary: "Restart the Wolfram kernel for a notebook so it can evaluate cells again.",
155
+ schema: restartKernelSchema.shape,
156
+ permission: "RunCell",
157
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
158
+ requiresExplicitTarget: true,
159
+ },
144
160
  {
145
161
  name: "mma_get_cell_output",
146
162
  summary: "Read output and messages for one Mathematica notebook cell, refreshing completed run status when observed.",
@@ -12,6 +12,8 @@ const TOOL_GUIDE = [
12
12
  ["mma_delete_cell", "Delete an existing cell."],
13
13
  ["mma_run_cell", "Evaluate one cell and wait for completion or timeout."],
14
14
  ["mma_abort_evaluation", "Abort a running notebook evaluation."],
15
+ ["mma_kill_kernel", "Quit the Wolfram kernel for a notebook (control agent kernel is protected)."],
16
+ ["mma_restart_kernel", "Restart the Wolfram kernel for a notebook so it can evaluate cells again."],
15
17
  ["mma_get_cell_output", "Read output and messages produced by one cell; this may refresh completed run status."],
16
18
  ["mma_read_artifact", "Read large output or message artifacts by byte page; ids may become stale after notebook edits or reruns."],
17
19
  ["mma_save_notebook", "Save the selected notebook when SaveNotebook permission is granted."],
@@ -33,7 +35,7 @@ export const MICA_AGENT_INSTRUCTIONS = [
33
35
  "Tools:",
34
36
  ...TOOL_GUIDE.map(([name, description]) => `- ${name}: ${description}`),
35
37
  ].join("\n");
36
- export function createMicaMcpServer(name, version = "1.0.5") {
38
+ export function createMicaMcpServer(name, version = "1.1.1") {
37
39
  return new McpServer({ name, version }, { instructions: MICA_AGENT_INSTRUCTIONS });
38
40
  }
39
41
  export function registerMicaPrompts(server) {
@@ -38,6 +38,12 @@ export const runCellSchema = z.object({
38
38
  export const abortEvaluationSchema = z.object({
39
39
  ...notebookSelectorFields
40
40
  }).strict();
41
+ export const killKernelSchema = z.object({
42
+ ...notebookSelectorFields
43
+ }).strict();
44
+ export const restartKernelSchema = z.object({
45
+ ...notebookSelectorFields
46
+ }).strict();
41
47
  export const getCellOutputSchema = z.object({
42
48
  ...notebookSelectorFields,
43
49
  cellId: z.string().min(1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliceshimada/mica",
3
- "version": "1.0.5",
3
+ "version": "1.1.1",
4
4
  "description": "Local MCP bridge for controlling live Wolfram Desktop / Mathematica notebooks.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -122,11 +122,31 @@ If[!AssociationQ[Quiet @ Check[$BridgePermissions, None]],
122
122
  $BridgePermissions = $DefaultBridgePermissions
123
123
  ];
124
124
 
125
- PalettePermissionRow[label_String, key_String] := Row[{
126
- Checkbox[Dynamic[$BridgePermissions[key], ($BridgePermissions[key] = #; Quiet @ Check[PostPermissions[], Null]) &]],
127
- Spacer[8],
128
- Style[label, 11]
129
- }];
125
+ If[!AssociationQ[Quiet @ Check[$BridgeNotebookPermissions, None]],
126
+ $BridgeNotebookPermissions = <||>
127
+ ];
128
+
129
+ NotebookPermissions[notebookId_String] := Lookup[$BridgeNotebookPermissions, notebookId, $DefaultBridgePermissions];
130
+
131
+ SetNotebookPermissions[notebookId_String, perms_Association] := (
132
+ $BridgeNotebookPermissions[notebookId] = perms
133
+ );
134
+
135
+ PalettePermissionRow[label_String, key_String, notebookId_:None] := Module[{perms, setter},
136
+ perms = If[StringQ[notebookId] && StringLength[notebookId] > 0,
137
+ NotebookPermissions[notebookId],
138
+ $BridgePermissions
139
+ ];
140
+ setter = If[StringQ[notebookId] && StringLength[notebookId] > 0,
141
+ Function[val, SetNotebookPermissions[notebookId, Join[perms, <|key -> val|>]]],
142
+ Function[val, $BridgePermissions[key] = val; Quiet @ Check[PostPermissions[], Null]]
143
+ ];
144
+ Row[{
145
+ Checkbox[Dynamic[perms[key], (setter[#]) &]],
146
+ Spacer[8],
147
+ Style[label, 11]
148
+ }]
149
+ ];
130
150
 
131
151
  PaletteStatusSummary[] := Module[{server, paletteConnected, notebookAttached, pendingRequests, attachedNotebook, error},
132
152
  server = Lookup[$LastStatus, "server", "unknown"];
@@ -332,14 +352,14 @@ PermissionsPanel[] := Panel[
332
352
  Column[
333
353
  {
334
354
  Style["Permissions", Bold],
335
- Grid[
355
+ Dynamic @ Grid[
336
356
  {
337
- {PalettePermissionRow["Read notebook", "ReadNotebook"]},
338
- {PalettePermissionRow["Insert cell", "InsertCell"]},
339
- {PalettePermissionRow["Modify cell", "ModifyCell"]},
340
- {PalettePermissionRow["Delete cell", "DeleteCell"]},
341
- {PalettePermissionRow["Run cell", "RunCell"]},
342
- {PalettePermissionRow["Save notebook", "SaveNotebook"]}
357
+ {PalettePermissionRow["Read notebook", "ReadNotebook", $ActiveNotebookId]},
358
+ {PalettePermissionRow["Insert cell", "InsertCell", $ActiveNotebookId]},
359
+ {PalettePermissionRow["Modify cell", "ModifyCell", $ActiveNotebookId]},
360
+ {PalettePermissionRow["Delete cell", "DeleteCell", $ActiveNotebookId]},
361
+ {PalettePermissionRow["Run cell", "RunCell", $ActiveNotebookId]},
362
+ {PalettePermissionRow["Save notebook", "SaveNotebook", $ActiveNotebookId]}
343
363
  },
344
364
  Alignment -> Left,
345
365
  Spacings -> {1, 0.35}
@@ -590,10 +610,16 @@ PostPermissions[] := Module[{payload = <|"permissions" -> $BridgePermissions|>},
590
610
  Quiet @ Check[BridgePost["/permissions", payload], $Failed]
591
611
  ];
592
612
 
593
- NeedsConfirmationQ[action_String] := Not @ TrueQ[$BridgePermissions[action]];
613
+ NeedsConfirmationQ[action_String, notebookId_:None] := Module[{perms},
614
+ perms = If[StringQ[notebookId] && StringLength[notebookId] > 0,
615
+ NotebookPermissions[notebookId],
616
+ $BridgePermissions
617
+ ];
618
+ Not @ TrueQ[perms[action]]
619
+ ];
594
620
 
595
- ConfirmAction[action_String, message_String] := If[
596
- NeedsConfirmationQ[action],
621
+ ConfirmAction[action_String, message_String, notebookId_:None] := If[
622
+ NeedsConfirmationQ[action, notebookId],
597
623
  ChoiceDialog[message, {"Allow" -> True, "Deny" -> False}],
598
624
  True
599
625
  ];
@@ -606,8 +632,8 @@ FailedRequestCode[failure_Failure] := Module[{code = failure[[1]]},
606
632
 
607
633
  FailedRequestMessage[failure_Failure] := Lookup[failure[[2]], "Message", Lookup[failure[[2]], "message", "The Wolfram bridge rejected the request."]];
608
634
 
609
- RequireReadPermission[] := If[
610
- Not @ ConfirmAction["ReadNotebook", "AI requests reading the notebook. Allow?"],
635
+ RequireReadPermission[notebookId_:None] := If[
636
+ Not @ ConfirmAction["ReadNotebook", "AI requests reading the notebook. Allow?", notebookId],
611
637
  $Canceled,
612
638
  True
613
639
  ];
@@ -628,7 +654,7 @@ NotebookInfo[nb_NotebookObject, notebookId_String] := <|
628
654
  "wolframVersion" -> ToString @ $VersionNumber,
629
655
  "platform" -> $OperatingSystem,
630
656
  "paletteId" -> $PaletteId,
631
- "permissions" -> $BridgePermissions
657
+ "permissions" -> NotebookPermissions[notebookId]
632
658
  |>;
633
659
 
634
660
  NotebookRecord[notebookId_String] := Lookup[$BridgeNotebooks, notebookId, <||>];
@@ -1137,9 +1163,9 @@ RefreshCellMap[notebookId_String] := Module[{record, nb, cells, idByCell, previo
1137
1163
  payload
1138
1164
  ];
1139
1165
 
1140
- ReadCellById[args_Association] := Module[{notebookId, record, cellId, cell, maxBytes, payload},
1141
- If[RequireReadPermission[] === $Canceled, Return[$Canceled]];
1142
- notebookId = TargetNotebookId[args];
1166
+ ReadCellById[args_Association] := Module[{notebookId, record, cellId, cell, maxBytes, payload},
1167
+ notebookId = TargetNotebookId[args];
1168
+ If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1143
1169
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1144
1170
  record = NotebookRecord[notebookId];
1145
1171
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1158,9 +1184,9 @@ ReadCellById[args_Association] := Module[{notebookId, record, cellId, cell, maxB
1158
1184
  ]
1159
1185
  ];
1160
1186
 
1161
- GetCellOutputById[args_Association] := Module[{notebookId, record, cellId, cell, artifacts, maxBytes, payload},
1162
- If[RequireReadPermission[] === $Canceled, Return[$Canceled]];
1163
- notebookId = TargetNotebookId[args];
1187
+ GetCellOutputById[args_Association] := Module[{notebookId, record, cellId, cell, artifacts, maxBytes, payload},
1188
+ notebookId = TargetNotebookId[args];
1189
+ If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1164
1190
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1165
1191
  record = NotebookRecord[notebookId];
1166
1192
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1178,9 +1204,9 @@ GetCellOutputById[args_Association] := Module[{notebookId, record, cellId, cell,
1178
1204
  ]
1179
1205
  ];
1180
1206
 
1181
- ReadArtifactById[args_Association] := Module[{notebookId, record, artifactId, parts, cellId, kind, indexText, index, cell, artifacts, artifactList, text, offset, limit, page, nextOffset, done},
1182
- If[RequireReadPermission[] === $Canceled, Return[$Canceled]];
1183
- notebookId = TargetNotebookId[args];
1207
+ ReadArtifactById[args_Association] := Module[{notebookId, record, artifactId, parts, cellId, kind, indexText, index, cell, artifacts, artifactList, text, offset, limit, page, nextOffset, done},
1208
+ notebookId = TargetNotebookId[args];
1209
+ If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1184
1210
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1185
1211
  record = NotebookRecord[notebookId];
1186
1212
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1255,8 +1281,8 @@ InsertCellAtBeginning[notebook_NotebookObject, newCell_] := Module[{beforeCount,
1255
1281
  ];
1256
1282
 
1257
1283
  InsertCellRequest[args_Association] := Module[{notebookId, record, afterId, style, content, newCell, notebook, anchor, cells, inserted, refreshed},
1258
- If[Not @ ConfirmAction["InsertCell", "AI requests inserting 1 cell. Allow?"], Return[$Canceled]];
1259
1284
  notebookId = TargetNotebookId[args];
1285
+ If[Not @ ConfirmAction["InsertCell", "AI requests inserting 1 cell. Allow?", notebookId], Return[$Canceled]];
1260
1286
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1261
1287
  record = NotebookRecord[notebookId];
1262
1288
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1291,8 +1317,8 @@ InsertCellRequest[args_Association] := Module[{notebookId, record, afterId, styl
1291
1317
  ];
1292
1318
 
1293
1319
  ModifyCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId, cellMap, cell, content, style, newCell, cells, cellIndex, updatedCells, updatedCell, writeResult},
1294
- If[Not @ ConfirmAction["ModifyCell", "AI requests modifying 1 cell. Allow?"], Return[$Canceled]];
1295
1320
  notebookId = TargetNotebookId[args];
1321
+ If[Not @ ConfirmAction["ModifyCell", "AI requests modifying 1 cell. Allow?", notebookId], Return[$Canceled]];
1296
1322
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1297
1323
  record = NotebookRecord[notebookId];
1298
1324
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1322,8 +1348,8 @@ ModifyCellRequest[args_Association] := Module[{notebookId, record, notebook, cel
1322
1348
  ];
1323
1349
 
1324
1350
  DeleteCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId, cell, cellMap, artifacts, artifactIds, newCellMap},
1325
- If[Not @ ConfirmAction["DeleteCell", "AI requests deleting 1 cell. Allow?"], Return[$Canceled]];
1326
1351
  notebookId = TargetNotebookId[args];
1352
+ If[Not @ ConfirmAction["DeleteCell", "AI requests deleting 1 cell. Allow?", notebookId], Return[$Canceled]];
1327
1353
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1328
1354
  record = NotebookRecord[notebookId];
1329
1355
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1345,8 +1371,8 @@ DeleteCellRequest[args_Association] := Module[{notebookId, record, notebook, cel
1345
1371
  ];
1346
1372
 
1347
1373
  RunCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId, cell, timeoutSec = Lookup[args, "timeoutSec", 120], installedEpilog, evaluateResult},
1348
- If[Not @ ConfirmAction["RunCell", "AI requests running 1 cell. Allow?"], Return[$Canceled]];
1349
1374
  notebookId = TargetNotebookId[args];
1375
+ If[Not @ ConfirmAction["RunCell", "AI requests running 1 cell. Allow?", notebookId], Return[$Canceled]];
1350
1376
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1351
1377
  record = NotebookRecord[notebookId];
1352
1378
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1384,8 +1410,8 @@ RunCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId
1384
1410
  ];
1385
1411
 
1386
1412
  AbortEvaluationRequest[args_Association] := Module[{notebookId, record, notebook, runningCellId, runningRequestId, wasRunning},
1387
- If[Not @ ConfirmAction["RunCell", "AI requests aborting the running evaluation. Allow?"], Return[$Canceled]];
1388
1413
  notebookId = TargetNotebookId[args];
1414
+ If[Not @ ConfirmAction["RunCell", "AI requests aborting the running evaluation. Allow?", notebookId], Return[$Canceled]];
1389
1415
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1390
1416
  record = NotebookRecord[notebookId];
1391
1417
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1407,11 +1433,43 @@ AbortEvaluationRequest[args_Association] := Module[{notebookId, record, notebook
1407
1433
  <|"status" -> "abort_requested", "cellId" -> runningCellId, "requestId" -> runningRequestId|>,
1408
1434
  <|"status" -> "idle"|>
1409
1435
  ]
1410
- ];
1436
+ ];
1437
+
1438
+ KillKernelRequest[args_Association] := Module[{notebookId, record, notebook, evaluatorName},
1439
+ notebookId = TargetNotebookId[args];
1440
+ If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1441
+ record = NotebookRecord[notebookId];
1442
+ If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1443
+ notebook = Lookup[record, "notebook", None];
1444
+ If[Head[notebook] =!= NotebookObject, Return[Failure["BAD_REQUEST", <|"message" -> "Notebook is unavailable."|>]]];
1445
+ evaluatorName = Quiet @ Check[CurrentValue[notebook, Evaluator], $ControlAgentEvaluatorName];
1446
+ If[evaluatorName === $ControlAgentEvaluatorName,
1447
+ Return[Failure["PROTECTED_EVALUATOR", <|"message" -> "Cannot kill the MICA control agent evaluator."|>]]
1448
+ ];
1449
+ Quiet @ Check[NotebookEvaluate[notebook, "Quit[]", InsertResults -> False], Null];
1450
+ <|"status" -> "killed", "notebookId" -> notebookId|>
1451
+ ];
1452
+
1453
+ RestartKernelRequest[args_Association] := Module[{notebookId, record, notebook, evaluatorName},
1454
+ notebookId = TargetNotebookId[args];
1455
+ If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1456
+ record = NotebookRecord[notebookId];
1457
+ If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1458
+ notebook = Lookup[record, "notebook", None];
1459
+ If[Head[notebook] =!= NotebookObject, Return[Failure["BAD_REQUEST", <|"message" -> "Notebook is unavailable."|>]]];
1460
+ evaluatorName = Quiet @ Check[CurrentValue[notebook, Evaluator], $ControlAgentEvaluatorName];
1461
+ If[evaluatorName === $ControlAgentEvaluatorName,
1462
+ Return[Failure["PROTECTED_EVALUATOR", <|"message" -> "Cannot restart the MICA control agent evaluator."|>]]
1463
+ ];
1464
+ Quiet @ Check[NotebookEvaluate[notebook, "Quit[]", InsertResults -> False], Null];
1465
+ Pause[0.5];
1466
+ Quiet @ Check[NotebookEvaluate[notebook, "Null", InsertResults -> False], Null];
1467
+ <|"status" -> "restarted", "notebookId" -> notebookId|>
1468
+ ];
1411
1469
 
1412
1470
  SaveNotebookRequest[args_Association] := Module[{notebookId, record, notebook},
1413
- If[Not @ ConfirmAction["SaveNotebook", "AI requests saving the notebook. Allow?"], Return[$Canceled]];
1414
1471
  notebookId = TargetNotebookId[args];
1472
+ If[Not @ ConfirmAction["SaveNotebook", "AI requests saving the notebook. Allow?", notebookId], Return[$Canceled]];
1415
1473
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1416
1474
  record = NotebookRecord[notebookId];
1417
1475
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1466,11 +1524,15 @@ AgentHeartbeat[] := BridgePost[
1466
1524
  |>
1467
1525
  ];
1468
1526
 
1469
- NotebookHeartbeatPayload[nb_NotebookObject] := Module[{savedPath, windowTitle, displayName, frontendObjectKey},
1527
+ NotebookHeartbeatPayload[nb_NotebookObject, notebookId_:None] := Module[{savedPath, windowTitle, displayName, frontendObjectKey, perms},
1470
1528
  savedPath = Quiet @ Check[ToString[Replace[NotebookFileName[nb], $Failed -> ""]], ""];
1471
1529
  frontendObjectKey = FrontendObjectKey[nb];
1472
1530
  windowTitle = NotebookWindowTitle[nb];
1473
1531
  displayName = NotebookDisplayNameForHeartbeat[nb, savedPath, frontendObjectKey];
1532
+ perms = If[StringQ[notebookId] && StringLength[notebookId] > 0,
1533
+ NotebookPermissions[notebookId],
1534
+ $DefaultBridgePermissions
1535
+ ];
1474
1536
  <|
1475
1537
  "agentSessionId" -> $AgentSessionId,
1476
1538
  "frontendObjectKey" -> frontendObjectKey,
@@ -1480,7 +1542,7 @@ NotebookHeartbeatPayload[nb_NotebookObject] := Module[{savedPath, windowTitle, d
1480
1542
  "savedPath" -> savedPath,
1481
1543
  "wolframVersion" -> ToString[$VersionNumber],
1482
1544
  "platform" -> $OperatingSystem,
1483
- "permissions" -> $BridgePermissions,
1545
+ "permissions" -> perms,
1484
1546
  "seenAt" -> UnixTimeMilliseconds[]
1485
1547
  |>
1486
1548
  ];
@@ -1514,7 +1576,8 @@ AgentHeartbeatNotebookClosure[notebookId_String] := Module[{record = Lookup[$Bri
1514
1576
  response = Quiet @ Check[BridgePost["/notebooks/" <> URLComponentEncodeString[notebookId] <> "/closed", <|"agentSessionId" -> $AgentSessionId|>], $Failed];
1515
1577
  If[AssociationQ[record] && AssociationQ[response] && TrueQ[Lookup[response, "ok", False]],
1516
1578
  If[!TrueQ[Lookup[record, "closed", False]],
1517
- $BridgeNotebooks[notebookId] = Join[record, <|"closed" -> True|>]
1579
+ $BridgeNotebooks[notebookId] = Join[record, <|"closed" -> True|>];
1580
+ KeyDropFrom[$BridgeNotebookPermissions, notebookId]
1518
1581
  ]
1519
1582
  ];
1520
1583
  response
@@ -1538,7 +1601,7 @@ HeartbeatNotebooks[] := Module[{notebooks = AgentVisibleNotebooks[], visibleNote
1538
1601
  None
1539
1602
  ];
1540
1603
  If[StringQ[localNotebookId] && StringLength[localNotebookId] > 0, AppendTo[visibleNotebookIds, localNotebookId]];
1541
- payload = NotebookHeartbeatPayload[nb];
1604
+ payload = NotebookHeartbeatPayload[nb, localNotebookId];
1542
1605
  response = Quiet @ Check[BridgePost["/notebooks/heartbeat", payload], $Failed];
1543
1606
  If[AssociationQ[response],
1544
1607
  notebookId = Lookup[Lookup[response, "notebook", <||>], "notebookId", Lookup[response, "notebookId", None]];
@@ -1771,13 +1834,15 @@ ExecuteRequest[request_Association] := Module[{requestId, tool, args, result},
1771
1834
  !AssociationQ[args], Failure["BAD_REQUEST", <|"Message" -> "Request arguments must be an association."|>],
1772
1835
  True,
1773
1836
  Switch[tool,
1774
- "mma_list_cells", If[RequireReadPermission[] === $Canceled, $Canceled, Module[{notebookId = TargetNotebookId[args], refresh}, If[!StringQ[notebookId] || StringLength[notebookId] == 0, Failure["BAD_REQUEST", <|"Message" -> "No notebook is selected."|>], refresh = RefreshCellMap[notebookId]; If[MatchQ[refresh, _Failure], refresh, <|"cells" -> refresh|>]]]],
1837
+ "mma_list_cells", If[RequireReadPermission[TargetNotebookId[args]] === $Canceled, $Canceled, Module[{notebookId = TargetNotebookId[args], refresh}, If[!StringQ[notebookId] || StringLength[notebookId] == 0, Failure["BAD_REQUEST", <|"Message" -> "No notebook is selected."|>], refresh = RefreshCellMap[notebookId]; If[MatchQ[refresh, _Failure], refresh, <|"cells" -> refresh|>]]]],
1775
1838
  "mma_read_cell", ReadCellById[args],
1776
1839
  "mma_insert_cell", InsertCellRequest[args],
1777
1840
  "mma_modify_cell", ModifyCellRequest[args],
1778
1841
  "mma_delete_cell", DeleteCellRequest[args],
1779
1842
  "mma_run_cell", RunCellRequest[args],
1780
- "mma_abort_evaluation", AbortEvaluationRequest[args],
1843
+ "mma_abort_evaluation", AbortEvaluationRequest[args],
1844
+ "mma_kill_kernel", KillKernelRequest[args],
1845
+ "mma_restart_kernel", RestartKernelRequest[args],
1781
1846
  "mma_get_cell_output", GetCellOutputById[args],
1782
1847
  "mma_read_artifact", ReadArtifactById[args],
1783
1848
  "mma_save_notebook", SaveNotebookRequest[args],