@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,125 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { startBunRuntime } from "../bun/index.js";
6
+ import { runDoctor } from "./doctor.js";
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+ function resolveProjectRoot() {
11
+ let dir = path.dirname(fileURLToPath(import.meta.url));
12
+ while (!existsSync(path.join(dir, "package.json"))) {
13
+ const parent = path.dirname(dir);
14
+ if (parent === dir)
15
+ throw new Error("Cannot find project root (no package.json found)");
16
+ dir = parent;
17
+ }
18
+ return dir;
19
+ }
20
+ // ---------------------------------------------------------------------------
21
+ // Public API
22
+ // ---------------------------------------------------------------------------
23
+ export function helpText() {
24
+ return `Usage: mica <command> [options]
25
+
26
+ Commands:
27
+ start Start the MICA bridge runtime (default)
28
+ install [options] Install MICA bridge into Wolfram
29
+ uninstall [options] Uninstall MICA bridge from Wolfram
30
+ doctor Diagnose MICA bridge configuration
31
+ status Show MICA bridge status
32
+ config codex Configure for Codex
33
+ config claude-desktop Configure for Claude Desktop
34
+ config cursor Configure for Cursor
35
+
36
+ Options:
37
+ --help, -h Show this help message
38
+ --dry-run Preview changes without applying (install/uninstall)
39
+ `;
40
+ }
41
+ export async function runCli(argv, deps) {
42
+ const stdout = deps?.stdout ?? process.stdout;
43
+ const stderr = deps?.stderr ?? process.stderr;
44
+ const startRuntime = deps?.startRuntime;
45
+ const runInstaller = deps?.runInstaller;
46
+ const _runDoctor = deps?.runDoctor;
47
+ const command = argv[0];
48
+ // --help / -h
49
+ if (command === "--help" || command === "-h") {
50
+ stdout.write(helpText());
51
+ return 0;
52
+ }
53
+ // install
54
+ if (command === "install") {
55
+ if (!runInstaller) {
56
+ stderr.write("Error: runInstaller not available\n");
57
+ return 1;
58
+ }
59
+ const output = runInstaller(argv.slice(1));
60
+ stdout.write(output);
61
+ return 0;
62
+ }
63
+ // uninstall
64
+ if (command === "uninstall") {
65
+ if (!runInstaller) {
66
+ stderr.write("Error: runInstaller not available\n");
67
+ return 1;
68
+ }
69
+ const output = runInstaller(["--uninstall", ...argv.slice(1)]);
70
+ stdout.write(output);
71
+ return 0;
72
+ }
73
+ // start or no args
74
+ if (command === "start" || command === undefined) {
75
+ if (!startRuntime) {
76
+ stderr.write("Error: startRuntime not available\n");
77
+ return 1;
78
+ }
79
+ const runtime = await startRuntime();
80
+ await runtime.keepAlive;
81
+ return 0;
82
+ }
83
+ // doctor
84
+ if (command === "doctor") {
85
+ if (!_runDoctor) {
86
+ stderr.write("Error: runDoctor not available\n");
87
+ return 1;
88
+ }
89
+ const { exitCode, output } = await _runDoctor();
90
+ stdout.write(output);
91
+ return exitCode;
92
+ }
93
+ // Future commands (listed in help but not yet implemented)
94
+ if (command === "status" || command === "config") {
95
+ stderr.write(`Command '${command}' is not yet implemented. Use --help for available commands.\n`);
96
+ return 1;
97
+ }
98
+ // Unknown command
99
+ stderr.write(`Unknown command: ${command}\n`);
100
+ return 1;
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // Direct invocation (node dist/src/cli/index.js)
104
+ // ---------------------------------------------------------------------------
105
+ async function main() {
106
+ const projectRoot = resolveProjectRoot();
107
+ const installerUrl = pathToFileURL(path.join(projectRoot, "scripts", "install.js")).href;
108
+ const { runInstaller, detectWolframUserBase } = (await import(installerUrl));
109
+ const exitCode = await runCli(process.argv.slice(2), {
110
+ startRuntime: async () => startBunRuntime(),
111
+ runInstaller,
112
+ runDoctor: async () => runDoctor({
113
+ projectRoot,
114
+ detectWolframUserBase: () => detectWolframUserBase(),
115
+ }),
116
+ });
117
+ process.exitCode = exitCode;
118
+ }
119
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
120
+ main().catch((error) => {
121
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
122
+ process.stderr.write(`${message}\n`);
123
+ process.exitCode = 1;
124
+ });
125
+ }
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ // Legacy Palette compatibility path. Product entrypoint is src/bun/index.ts.
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { HttpBridge } from "./bridge/httpBridge.js";
5
+ import { RequestQueue } from "./bridge/requestQueue.js";
6
+ import { createMicaMcpServer, registerMicaPrompts } from "./mcp/prompts.js";
7
+ import { registerMmaTools } from "./mcp/tools.js";
8
+ import { runtimeModeFromArgs } from "./runtimeOptions.js";
9
+ async function main() {
10
+ const queue = new RequestQueue();
11
+ const bridge = new HttpBridge(queue);
12
+ const mode = runtimeModeFromArgs(process.argv.slice(2));
13
+ await bridge.start();
14
+ const server = createMicaMcpServer("mica");
15
+ registerMmaTools(server, queue, () => bridge.statusSnapshot());
16
+ registerMicaPrompts(server);
17
+ let shuttingDown = false;
18
+ async function shutdown(exitCode) {
19
+ if (shuttingDown)
20
+ return;
21
+ shuttingDown = true;
22
+ console.error("MICA shutting down...");
23
+ await bridge.stop();
24
+ if (exitCode !== undefined) {
25
+ process.exit(exitCode);
26
+ }
27
+ }
28
+ process.on("SIGINT", () => {
29
+ shutdown(130).catch((error) => {
30
+ console.error(error);
31
+ process.exit(1);
32
+ });
33
+ });
34
+ process.on("SIGTERM", () => {
35
+ shutdown(143).catch((error) => {
36
+ console.error(error);
37
+ process.exit(1);
38
+ });
39
+ });
40
+ // Log only to stderr — stdout is reserved for MCP stdio JSON-RPC.
41
+ console.error(`MICA HTTP listening on http://127.0.0.1:${bridge.port}`);
42
+ if (mode === "bridge-only") {
43
+ console.error("MICA running in bridge-only development mode.");
44
+ return;
45
+ }
46
+ await server.connect(new StdioServerTransport());
47
+ // server.connect() completes stdio transport setup; the HTTP bridge keeps
48
+ // the process alive while the MCP client owns the MCP server. Signal handlers above
49
+ // perform cleanup on explicit process termination.
50
+ }
51
+ main().catch((error) => {
52
+ console.error(error);
53
+ process.exit(1);
54
+ });
@@ -0,0 +1,216 @@
1
+ import { randomUUID } from "node:crypto";
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";
4
+ import { INSERT_ANCHOR_GUIDANCE, notebookToolDescription } from "./descriptions.js";
5
+ import { toolSuccess, withToolErrors } from "./toolResults.js";
6
+ function assertLiveAgent(state) {
7
+ if (!state.requireLiveAgent().ok) {
8
+ throw new Error("NO_LIVE_AGENT");
9
+ }
10
+ }
11
+ function sweepStateLiveness(state) {
12
+ state.sweepLiveness(Date.now());
13
+ }
14
+ function liveActiveNotebookId(state, clientSessionId) {
15
+ const resolution = state.resolveNotebook({}, clientSessionId);
16
+ return resolution.ok ? resolution.record.notebookId : null;
17
+ }
18
+ function liveAgents(state) {
19
+ return state.agents.list().filter((agent) => !agent.offline && !agent.retired);
20
+ }
21
+ function resolveNotebookTarget(state, args, clientSessionId) {
22
+ const resolution = state.resolveNotebook(args, clientSessionId);
23
+ if (!resolution.ok) {
24
+ throw new Error(resolution.error);
25
+ }
26
+ return resolution.record;
27
+ }
28
+ export function resolveToolTarget(state, args, extra) {
29
+ sweepStateLiveness(state);
30
+ assertLiveAgent(state);
31
+ const notebook = resolveNotebookTarget(state, {
32
+ notebookId: typeof args.notebookId === "string" ? args.notebookId : undefined,
33
+ displayName: typeof args.displayName === "string" ? args.displayName : undefined,
34
+ }, extra?.sessionId);
35
+ const notebookAgent = state.agents.get(notebook.agentSessionId);
36
+ if (!notebookAgent || notebookAgent.offline) {
37
+ throw new Error("NO_LIVE_AGENT");
38
+ }
39
+ return { agentSessionId: notebook.agentSessionId, notebook };
40
+ }
41
+ function queueNotebookOperation(state, target, tool, args, timeoutMs, extra) {
42
+ const requestId = randomUUID();
43
+ const createdAt = Date.now();
44
+ state.queue.enqueue({
45
+ requestId,
46
+ tool,
47
+ arguments: args,
48
+ targetNotebookId: target.notebook.notebookId,
49
+ agentSessionId: target.agentSessionId,
50
+ timeoutMs,
51
+ createdAt,
52
+ });
53
+ const abortHandler = () => {
54
+ state.queue.cancel(requestId, "MCP client cancelled operation", Date.now());
55
+ };
56
+ if (extra?.signal?.aborted) {
57
+ abortHandler();
58
+ }
59
+ else if (extra?.signal) {
60
+ extra.signal.addEventListener("abort", abortHandler, { once: true });
61
+ }
62
+ const timeoutHandle = setTimeout(() => {
63
+ state.queue.markTimedOut(Date.now());
64
+ }, timeoutMs);
65
+ return state.queue
66
+ .waitForResult(requestId)
67
+ .then((result) => toolSuccess(result))
68
+ .finally(() => {
69
+ clearTimeout(timeoutHandle);
70
+ extra?.signal?.removeEventListener("abort", abortHandler);
71
+ });
72
+ }
73
+ function ensurePermission(notebook, tool, permission) {
74
+ if (!notebook.permissions[permission]) {
75
+ throw new Error(`PERMISSION_DENIED: ${tool}`);
76
+ }
77
+ }
78
+ function registerQueuedNotebookTool(server, state, config) {
79
+ server.tool(config.name, notebookToolDescription(config.summary, config.extraGuidance), config.schema, async (args, extra) => {
80
+ const recordArgs = args;
81
+ return withToolErrors({ tool: config.name, args: recordArgs }, async () => {
82
+ if (process.env.MICA_STRICT_TARGETING === "1" && config.requiresExplicitTarget) {
83
+ const hasNotebookId = typeof recordArgs.notebookId === "string" && recordArgs.notebookId.trim().length > 0;
84
+ const hasDisplayName = typeof recordArgs.displayName === "string" && recordArgs.displayName.trim().length > 0;
85
+ if (!hasNotebookId && !hasDisplayName) {
86
+ throw new Error("EXPLICIT_NOTEBOOK_REQUIRED");
87
+ }
88
+ }
89
+ const target = resolveToolTarget(state, recordArgs, extra);
90
+ ensurePermission(target.notebook, config.name, config.permission);
91
+ return queueNotebookOperation(state, target, config.name, recordArgs, config.timeoutMs(recordArgs), extra);
92
+ });
93
+ });
94
+ }
95
+ export function registerBackendMcpTools(server, state) {
96
+ server.tool("mma_status", notebookToolDescription("Report backend status and notebook registry state."), noArgsSchema.shape, async (_args, extra) => {
97
+ return withToolErrors({ tool: "mma_status" }, () => {
98
+ sweepStateLiveness(state);
99
+ return toolSuccess({
100
+ server: "running",
101
+ activeNotebookId: liveActiveNotebookId(state, extra?.sessionId),
102
+ notebooks: state.notebooks.listLive(),
103
+ agents: liveAgents(state),
104
+ });
105
+ });
106
+ });
107
+ server.tool("mma_list_notebooks", notebookToolDescription("List notebooks registered with the Mathematica bridge Palette."), noArgsSchema.shape, async (_args, extra) => {
108
+ return withToolErrors({ tool: "mma_list_notebooks" }, () => {
109
+ sweepStateLiveness(state);
110
+ return toolSuccess({ notebooks: state.notebooks.listLive(), activeNotebookId: liveActiveNotebookId(state, extra?.sessionId) });
111
+ });
112
+ });
113
+ server.tool("mma_select_notebook", notebookToolDescription("Select the active Mathematica notebook in the backend registry."), selectNotebookSchema.shape, async (args, extra) => {
114
+ const recordArgs = args;
115
+ return withToolErrors({ tool: "mma_select_notebook", args: recordArgs }, () => {
116
+ const clientSessionId = extra?.sessionId;
117
+ const target = resolveToolTarget(state, recordArgs, extra);
118
+ state.setActiveNotebook(target.notebook.notebookId);
119
+ if (clientSessionId)
120
+ state.setActiveNotebook(target.notebook.notebookId, clientSessionId);
121
+ return toolSuccess({ activeNotebookId: state.activeNotebookId, notebook: target.notebook });
122
+ });
123
+ });
124
+ const queuedNotebookTools = [
125
+ {
126
+ name: "mma_symbol_lookup",
127
+ 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.",
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);
215
+ }
216
+ }
@@ -0,0 +1,6 @@
1
+ const NOTEBOOK_WORKFLOW_GUIDANCE = "Start by calling mma_status or mma_list_notebooks. Use the latest notebookId because notebookIds change across sessions/restarts. Restart your MCP client or the MICA MCP server after changing this MCP server code or tool descriptions.";
2
+ const LIVE_NOTEBOOK_DEBUG_GUIDANCE = "Debug live notebooks only through MCP notebook cells: insert cells, run cells, and read output/messages. Do not use detached wolframscript for live-notebook debugging or mutation.";
3
+ export const INSERT_ANCHOR_GUIDANCE = 'For append or unknown anchors, use afterCellId="__end__"; empty notebooks are supported.';
4
+ export function notebookToolDescription(summary, extraGuidance) {
5
+ return [summary, NOTEBOOK_WORKFLOW_GUIDANCE, extraGuidance, LIVE_NOTEBOOK_DEBUG_GUIDANCE].filter(Boolean).join(" ");
6
+ }
@@ -0,0 +1,52 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export const MICA_TOOL_GUIDE_PROMPT_NAME = "mica_notebook_workflow";
3
+ const TOOL_GUIDE = [
4
+ ["mma_status", "Report server, agent, and notebook registry state."],
5
+ ["mma_list_notebooks", "List live notebooks and the active notebook id."],
6
+ ["mma_select_notebook", "Select the active notebook by notebookId or displayName."],
7
+ ["mma_symbol_lookup", "Look up Wolfram Language symbol usage, options, attributes, and documentation URLs."],
8
+ ["mma_list_cells", "List cells in the selected notebook."],
9
+ ["mma_read_cell", "Read one cell's content and metadata."],
10
+ ["mma_insert_cell", "Insert a notebook cell; use afterCellId=\"__end__\" to append."],
11
+ ["mma_modify_cell", "Modify an existing cell."],
12
+ ["mma_delete_cell", "Delete an existing cell."],
13
+ ["mma_run_cell", "Evaluate one cell and wait for completion or timeout."],
14
+ ["mma_abort_evaluation", "Abort a running notebook evaluation."],
15
+ ["mma_get_cell_output", "Read output and messages produced by one cell; this may refresh completed run status."],
16
+ ["mma_read_artifact", "Read large output or message artifacts by byte page; ids may become stale after notebook edits or reruns."],
17
+ ["mma_save_notebook", "Save the selected notebook when SaveNotebook permission is granted."],
18
+ ];
19
+ export const MICA_AGENT_INSTRUCTIONS = [
20
+ "MICA controls already-open Mathematica / Wolfram Desktop notebooks through MCP.",
21
+ "",
22
+ "Workflow rules:",
23
+ "1. Start with mma_status or mma_list_notebooks. Use the latest notebookId because notebookIds change across Mathematica restarts.",
24
+ "2. Work only with notebooks returned by mma_list_notebooks unless the user explicitly asks for a different external action. Do not create hidden or offscreen notebooks.",
25
+ "3. Prefer notebookId for targeting. Use displayName only when the notebook name is unambiguous.",
26
+ "4. For all mutating operations, pass notebookId explicitly.",
27
+ "5. For live notebook debugging, use MCP notebook cells: insert cells, run cells, read cells, and inspect outputs/messages. Do not use detached wolframscript for live notebook mutation or debugging.",
28
+ "6. Cell ids are session-local. Refresh with mma_list_cells after large edits, deletes, or notebook restarts.",
29
+ "7. For appending cells, pass afterCellId=\"__end__\". Empty notebooks are supported.",
30
+ "8. All tool results are structured. Success returns ok: true. Expected failures return ok: false with error.code, error.message, error.retryable, error.tool, and sometimes error.notebookId.",
31
+ "9. Respect notebook permissions. SaveNotebook is commonly disabled; handle PERMISSION_DENIED instead of retrying blindly.",
32
+ "",
33
+ "Tools:",
34
+ ...TOOL_GUIDE.map(([name, description]) => `- ${name}: ${description}`),
35
+ ].join("\n");
36
+ export function createMicaMcpServer(name, version = "0.1.0") {
37
+ return new McpServer({ name, version }, { instructions: MICA_AGENT_INSTRUCTIONS });
38
+ }
39
+ export function registerMicaPrompts(server) {
40
+ server.prompt(MICA_TOOL_GUIDE_PROMPT_NAME, "How an agent should use MICA's Mathematica notebook MCP tools.", () => ({
41
+ description: "MICA Mathematica notebook workflow and tool guide.",
42
+ messages: [
43
+ {
44
+ role: "user",
45
+ content: {
46
+ type: "text",
47
+ text: MICA_AGENT_INSTRUCTIONS,
48
+ },
49
+ },
50
+ ],
51
+ }));
52
+ }
@@ -0,0 +1,183 @@
1
+ const ERROR_DEFINITIONS = {
2
+ AMBIGUOUS_NOTEBOOK_NAME: {
3
+ code: "AMBIGUOUS_NOTEBOOK_NAME",
4
+ message: "More than one live notebook matches that display name.",
5
+ retryable: false,
6
+ },
7
+ EXPLICIT_NOTEBOOK_REQUIRED: {
8
+ code: "EXPLICIT_NOTEBOOK_REQUIRED",
9
+ message: "Strict targeting is enabled. Provide notebookId or displayName explicitly for mutating operations.",
10
+ retryable: false,
11
+ },
12
+ NOTEBOOK_CLOSED: {
13
+ code: "NOTEBOOK_CLOSED",
14
+ message: "The selected Mathematica notebook has been closed.",
15
+ retryable: true,
16
+ },
17
+ NOTEBOOK_NOT_ATTACHED: {
18
+ code: "NOTEBOOK_NOT_ATTACHED",
19
+ message: "No Mathematica notebook is attached to the bridge.",
20
+ retryable: true,
21
+ },
22
+ NOTEBOOK_NOT_FOUND: {
23
+ code: "NOTEBOOK_NOT_FOUND",
24
+ message: "No live Mathematica notebook matches the requested selector.",
25
+ retryable: true,
26
+ },
27
+ NOTEBOOK_NOT_SELECTED: {
28
+ code: "NOTEBOOK_NOT_SELECTED",
29
+ message: "No Mathematica notebook is selected.",
30
+ retryable: true,
31
+ },
32
+ NOTEBOOK_STALE: {
33
+ code: "NOTEBOOK_STALE",
34
+ message: "The selected Mathematica notebook has stopped sending heartbeats.",
35
+ retryable: true,
36
+ },
37
+ NO_LIVE_AGENT: {
38
+ code: "NO_LIVE_AGENT",
39
+ message: "No live Mathematica control agent is registered.",
40
+ retryable: true,
41
+ },
42
+ PALETTE_NOT_CONNECTED: {
43
+ code: "PALETTE_NOT_CONNECTED",
44
+ message: "The Mathematica bridge palette is not connected.",
45
+ retryable: true,
46
+ },
47
+ PERMISSION_DENIED: {
48
+ code: "PERMISSION_DENIED",
49
+ message: "The selected notebook did not grant permission for this tool.",
50
+ retryable: false,
51
+ },
52
+ REQUEST_CANCELLED: {
53
+ code: "REQUEST_CANCELLED",
54
+ message: "The MCP client cancelled the operation.",
55
+ retryable: false,
56
+ },
57
+ REQUEST_TIMED_OUT: {
58
+ code: "REQUEST_TIMED_OUT",
59
+ message: "The Mathematica control agent did not answer before the tool timeout.",
60
+ retryable: true,
61
+ },
62
+ UNSUPPORTED_SELECTOR: {
63
+ code: "UNSUPPORTED_SELECTOR",
64
+ message: "That notebook selector is not supported by this MCP transport.",
65
+ retryable: false,
66
+ },
67
+ WOLFRAM_AGENT_ERROR: {
68
+ code: "WOLFRAM_AGENT_ERROR",
69
+ message: "The Mathematica control agent reported an error.",
70
+ retryable: true,
71
+ },
72
+ };
73
+ export function toolSuccess(value) {
74
+ const { ok: _ignoredOk, ...payload } = objectPayload(value);
75
+ return textResult({ ok: true, ...payload });
76
+ }
77
+ export function toolFailure(error, context = {}) {
78
+ return { ...textResult({ ok: false, error: normalizeToolError(error, context) }), isError: true };
79
+ }
80
+ export async function withToolErrors(context, handler) {
81
+ try {
82
+ return await handler();
83
+ }
84
+ catch (error) {
85
+ return toolFailure(error, context);
86
+ }
87
+ }
88
+ function textResult(structuredContent) {
89
+ return {
90
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
91
+ structuredContent,
92
+ };
93
+ }
94
+ function objectPayload(value) {
95
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
96
+ return value;
97
+ }
98
+ return { result: value };
99
+ }
100
+ function normalizeToolError(error, context) {
101
+ const raw = readErrorFields(error);
102
+ const definition = classifyError(raw.code ?? raw.message);
103
+ const notebookId = context.notebookId ?? notebookIdFromArgs(context.args);
104
+ const tool = raw.tool ?? context.tool;
105
+ return {
106
+ code: definition.code,
107
+ message: raw.message && !isBareCode(raw.message) ? raw.message : definition.message,
108
+ retryable: raw.retryable ?? definition.retryable,
109
+ ...(tool ? { tool } : {}),
110
+ ...(notebookId ? { notebookId } : {}),
111
+ ...(raw.details ? { details: raw.details } : {}),
112
+ };
113
+ }
114
+ function readErrorFields(error) {
115
+ if (error instanceof Error) {
116
+ return parseMessage(error.message);
117
+ }
118
+ if (typeof error === "string") {
119
+ return parseMessage(error);
120
+ }
121
+ if (error !== null && typeof error === "object") {
122
+ const record = error;
123
+ const nested = record.error !== null && typeof record.error === "object" ? record.error : record;
124
+ const code = readString(nested.code);
125
+ const message = readString(nested.message);
126
+ const retryable = typeof nested.retryable === "boolean" ? nested.retryable : undefined;
127
+ const tool = readString(nested.tool);
128
+ const details = nested.details !== null && typeof nested.details === "object" && !Array.isArray(nested.details)
129
+ ? nested.details
130
+ : undefined;
131
+ return { code, message, retryable, tool, details };
132
+ }
133
+ return { message: String(error) };
134
+ }
135
+ function parseMessage(message) {
136
+ const trimmed = message.trim();
137
+ if (!trimmed)
138
+ return {};
139
+ const permissionMatch = trimmed.match(/^PERMISSION_DENIED:\s*(.+)$/);
140
+ if (permissionMatch) {
141
+ return { code: "PERMISSION_DENIED", message: ERROR_DEFINITIONS.PERMISSION_DENIED.message, tool: permissionMatch[1].trim() };
142
+ }
143
+ if (trimmed === "MCP client cancelled operation") {
144
+ return { code: "REQUEST_CANCELLED", message: trimmed };
145
+ }
146
+ if (trimmed.startsWith("Mathematica Palette is not connected")) {
147
+ return { code: "PALETTE_NOT_CONNECTED", message: trimmed };
148
+ }
149
+ if (trimmed.startsWith("No Mathematica notebook is attached")) {
150
+ return { code: "NOTEBOOK_NOT_ATTACHED", message: trimmed };
151
+ }
152
+ if (trimmed.startsWith("No Mathematica notebook is selected")) {
153
+ return { code: "NOTEBOOK_NOT_SELECTED", message: trimmed };
154
+ }
155
+ if (trimmed.startsWith("Unknown notebookId:")) {
156
+ return { code: "NOTEBOOK_NOT_FOUND", message: trimmed };
157
+ }
158
+ if (trimmed.startsWith("Display-name notebook selection is not supported")) {
159
+ return { code: "UNSUPPORTED_SELECTOR", message: trimmed };
160
+ }
161
+ const codeMatch = trimmed.match(/^([A-Z][A-Z0-9_]+)(?::\s*(.+))?$/);
162
+ if (codeMatch) {
163
+ return { code: codeMatch[1], message: codeMatch[2] ?? codeMatch[1] };
164
+ }
165
+ return { message: trimmed };
166
+ }
167
+ function classifyError(input) {
168
+ if (input && ERROR_DEFINITIONS[input])
169
+ return ERROR_DEFINITIONS[input];
170
+ return { code: "INTERNAL_ERROR", message: "The MCP tool failed unexpectedly.", retryable: false };
171
+ }
172
+ function notebookIdFromArgs(args) {
173
+ return readString(args?.notebookId);
174
+ }
175
+ function readString(value) {
176
+ if (typeof value !== "string")
177
+ return undefined;
178
+ const trimmed = value.trim();
179
+ return trimmed ? trimmed : undefined;
180
+ }
181
+ function isBareCode(message) {
182
+ return /^[A-Z][A-Z0-9_]+$/.test(message);
183
+ }