@aliceshimada/mica 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.2 - 2026-06-06
4
+
5
+ - Add `mica mcp` command: starts an MCP stdio server that proxies to an existing bridge or starts a new one.
6
+ - Add `/mcp/call` HTTP endpoint for proxied MCP tool execution.
7
+ - Refactor backend tools to export `MICA_BACKEND_TOOL_DEFINITIONS` and `executeBackendMcpTool()` for reuse.
8
+ - Update config snippets to use `mica mcp` instead of `mica start`.
9
+
3
10
  ## 1.0.1 - 2026-06-02
4
11
 
5
12
  - Implement `mica status`, including dashboard token recovery from the current session.
package/README.md CHANGED
@@ -149,7 +149,7 @@ For OpenCode, the snippet uses the validated local MCP shape:
149
149
  "mcp": {
150
150
  "mica": {
151
151
  "type": "local",
152
- "command": ["mica", "start"],
152
+ "command": ["mica", "mcp"],
153
153
  "enabled": true
154
154
  }
155
155
  }
@@ -163,7 +163,7 @@ For manual setup from a local checkout, use the built release entrypoint:
163
163
  ```toml
164
164
  [mcp_servers.mica]
165
165
  command = "node"
166
- args = ["/absolute/path/to/mica/dist/src/cli/index.js", "start"]
166
+ args = ["/absolute/path/to/mica/dist/src/cli/index.js", "mcp"]
167
167
  ```
168
168
 
169
169
  For development, you can point an MCP client at the TypeScript entrypoint:
@@ -171,7 +171,7 @@ For development, you can point an MCP client at the TypeScript entrypoint:
171
171
  ```toml
172
172
  [mcp_servers.mica]
173
173
  command = "npx"
174
- args = ["tsx", "/absolute/path/to/mica/src/bun/index.ts"]
174
+ args = ["tsx", "/absolute/path/to/mica/src/cli/index.ts", "mcp"]
175
175
  ```
176
176
 
177
177
  ## Agent Guide Prompt
package/README.zh-CN.md CHANGED
@@ -149,7 +149,7 @@ OpenCode 的 snippet 使用经过验证的 local MCP 形状:
149
149
  "mcp": {
150
150
  "mica": {
151
151
  "type": "local",
152
- "command": ["mica", "start"],
152
+ "command": ["mica", "mcp"],
153
153
  "enabled": true
154
154
  }
155
155
  }
@@ -163,7 +163,7 @@ OpenCode 的 snippet 使用经过验证的 local MCP 形状:
163
163
  ```toml
164
164
  [mcp_servers.mica]
165
165
  command = "node"
166
- args = ["/absolute/path/to/mica/dist/src/cli/index.js", "start"]
166
+ args = ["/absolute/path/to/mica/dist/src/cli/index.js", "mcp"]
167
167
  ```
168
168
 
169
169
  开发时也可以让 MCP client 指向 TypeScript 入口:
@@ -171,7 +171,7 @@ args = ["/absolute/path/to/mica/dist/src/cli/index.js", "start"]
171
171
  ```toml
172
172
  [mcp_servers.mica]
173
173
  command = "npx"
174
- args = ["tsx", "/absolute/path/to/mica/src/bun/index.ts"]
174
+ args = ["tsx", "/absolute/path/to/mica/src/cli/index.ts", "mcp"]
175
175
  ```
176
176
 
177
177
  ## Agent Guide Prompt
@@ -1,8 +1,9 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
2
  import http from "node:http";
3
+ import { executeBackendMcpTool } from "../mcp/backendTools.js";
3
4
  import { renderDashboard } from "./dashboard.js";
4
5
  const JSON_BODY_LIMIT_BYTES = 1024 * 1024;
5
- const DEFAULT_VERSION = "1.0.1";
6
+ const DEFAULT_VERSION = "1.0.2";
6
7
  export async function createBunHttpApp({ state, host = "127.0.0.1", port, authToken, version = DEFAULT_VERSION }) {
7
8
  const runtimeInfo = {
8
9
  host,
@@ -66,6 +67,14 @@ export function createFetchHandler(state, options = {}) {
66
67
  requests: summarizeRequests(state.queue.snapshot()),
67
68
  });
68
69
  }
70
+ if (request.method === "POST" && url.pathname === "/mcp/call") {
71
+ const body = await readJsonObjectBody(request);
72
+ const tool = readRequiredString(body.tool, "tool");
73
+ const args = readOptionalRecord(body.arguments) ?? {};
74
+ const clientSessionId = readOptionalString(body.clientSessionId);
75
+ const result = await executeBackendMcpTool(state, tool, args, { sessionId: clientSessionId });
76
+ return jsonResponse(result);
77
+ }
69
78
  if (request.method === "POST" && url.pathname === "/agents/register") {
70
79
  const body = await readJsonObjectBody(request);
71
80
  const agentSessionId = readRequiredString(body.agentSessionId, "agentSessionId");
@@ -213,6 +222,8 @@ async function startNodeFallbackServer(fetchHandler, host, port) {
213
222
  await writeNodeResponse(outgoing, jsonResponse({ error: { code: "INTERNAL_ERROR", message } }, 500));
214
223
  }
215
224
  });
225
+ server.timeout = 0;
226
+ server.requestTimeout = 0;
216
227
  await new Promise((resolve, reject) => {
217
228
  server.once("error", reject);
218
229
  server.listen(port, host, () => {
@@ -313,6 +324,13 @@ function readOptionalString(value) {
313
324
  const text = value.trim();
314
325
  return text ? text : undefined;
315
326
  }
327
+ function readOptionalRecord(value) {
328
+ if (value === undefined)
329
+ return undefined;
330
+ if (typeof value !== "object" || value === null || Array.isArray(value))
331
+ throw new Error("BAD_REQUEST");
332
+ return value;
333
+ }
316
334
  function readOptionalNumber(value) {
317
335
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
318
336
  }
@@ -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.1";
11
+ const MICA_PACKAGE_VERSION = "1.0.2";
12
12
  export async function startBunRuntime(deps = {}) {
13
13
  const config = deps.runtimeConfig ?? loadRuntimeConfig();
14
14
  const bridgeOnly = deps.bridgeOnly ?? config.bridgeOnly;
@@ -8,7 +8,7 @@ export function runConfigCommand(argv) {
8
8
  };
9
9
  }
10
10
  if (client === "codex") {
11
- return ok(`[mcp_servers.mica]\ncommand = "mica"\nargs = ["start"]\n`);
11
+ return ok(`[mcp_servers.mica]\ncommand = "mica"\nargs = ["mcp"]\n`);
12
12
  }
13
13
  if (client === "opencode") {
14
14
  return ok(`${JSON.stringify({
@@ -16,7 +16,7 @@ export function runConfigCommand(argv) {
16
16
  mcp: {
17
17
  mica: {
18
18
  type: "local",
19
- command: ["mica", "start"],
19
+ command: ["mica", "mcp"],
20
20
  enabled: true,
21
21
  },
22
22
  },
@@ -26,7 +26,7 @@ export function runConfigCommand(argv) {
26
26
  mcpServers: {
27
27
  mica: {
28
28
  command: "mica",
29
- args: ["start"],
29
+ args: ["mcp"],
30
30
  },
31
31
  },
32
32
  }, null, 2)}\n`);
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath, pathToFileURL } from "node:url";
5
6
  import { startBunRuntime } from "../bun/index.js";
7
+ import { createMicaMcpServer, registerMicaPrompts } from "../mcp/prompts.js";
8
+ import { registerProxyMcpTools } from "../mcp/proxyTools.js";
9
+ import { defaultSessionFile } from "../runtime/config.js";
6
10
  import { runConfigCommand } from "./configSnippets.js";
7
11
  import { runDoctor } from "./doctor.js";
8
12
  import { runStatusCommand } from "./status.js";
@@ -20,6 +24,57 @@ function resolveProjectRoot() {
20
24
  }
21
25
  return dir;
22
26
  }
27
+ function sleep(ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+ function isAddressInUse(error) {
31
+ if (typeof error !== "object" || error === null)
32
+ return false;
33
+ const record = error;
34
+ return record.code === "EADDRINUSE" || String(record.message ?? "").includes("EADDRINUSE");
35
+ }
36
+ function sessionFileFromArgs(argv, env = process.env) {
37
+ const index = argv.indexOf("--session-file");
38
+ if (index >= 0 && argv[index + 1] && !argv[index + 1].startsWith("--")) {
39
+ return argv[index + 1];
40
+ }
41
+ return env.MICA_SESSION_FILE ?? defaultSessionFile(env);
42
+ }
43
+ export async function readLiveSession(argv = [], deps = {}) {
44
+ const env = deps.env ?? process.env;
45
+ const exists = deps.exists ?? existsSync;
46
+ const readFile = deps.readFile ?? ((filePath) => readFileSync(filePath, "utf8"));
47
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
48
+ const sessionFile = sessionFileFromArgs(argv, env);
49
+ if (!exists(sessionFile) || typeof fetchImpl !== "function")
50
+ return undefined;
51
+ let session;
52
+ try {
53
+ session = JSON.parse(readFile(sessionFile));
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ if (!session.baseUrl || !session.authToken)
59
+ return undefined;
60
+ try {
61
+ const response = await fetchImpl(`${session.baseUrl}/status`, {
62
+ headers: { authorization: `Bearer ${session.authToken}` },
63
+ });
64
+ return response.status === 200 ? { baseUrl: session.baseUrl, authToken: session.authToken } : undefined;
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
70
+ export async function startProxyMcpRuntime(session, deps = {}) {
71
+ const server = deps.createMcpServer?.() ?? createMicaMcpServer("mica-proxy");
72
+ registerProxyMcpTools(server, session);
73
+ registerMicaPrompts(server);
74
+ const transport = deps.createTransport?.() ?? new StdioServerTransport();
75
+ await server.connect(transport);
76
+ return { keepAlive: new Promise(() => { }) };
77
+ }
23
78
  // ---------------------------------------------------------------------------
24
79
  // Public API
25
80
  // ---------------------------------------------------------------------------
@@ -30,6 +85,7 @@ Commands:
30
85
  start Start the MICA bridge runtime (default)
31
86
  stop Stop the running MICA bridge runtime
32
87
  restart Stop then start the MICA bridge runtime
88
+ mcp Start MCP stdio server; proxy to an existing bridge if possible
33
89
  install [options] Install MICA bridge into Wolfram
34
90
  uninstall [options] Uninstall MICA bridge from Wolfram
35
91
  doctor Diagnose MICA bridge configuration
@@ -53,6 +109,9 @@ export async function runCli(argv, deps) {
53
109
  const _runStatus = deps?.runStatus;
54
110
  const _runConfig = deps?.runConfig;
55
111
  const _runStop = deps?.runStop;
112
+ const _readLiveSession = deps?.readLiveSession ?? (() => readLiveSession(argv.slice(1)));
113
+ const _startProxyRuntime = deps?.startProxyRuntime ?? ((session) => startProxyMcpRuntime(session));
114
+ const _sleep = deps?.sleep ?? sleep;
56
115
  const command = argv[0];
57
116
  // --help / -h
58
117
  if (command === "--help" || command === "-h") {
@@ -96,6 +155,40 @@ export async function runCli(argv, deps) {
96
155
  await runtime.keepAlive;
97
156
  return 0;
98
157
  }
158
+ // mcp
159
+ if (command === "mcp") {
160
+ const existingSession = await _readLiveSession();
161
+ if (existingSession) {
162
+ const proxyRuntime = await _startProxyRuntime(existingSession);
163
+ await proxyRuntime.keepAlive;
164
+ return 0;
165
+ }
166
+ if (!startRuntime) {
167
+ stderr.write("Error: startRuntime not available\n");
168
+ return 1;
169
+ }
170
+ try {
171
+ const runtime = await startRuntime();
172
+ await runtime.keepAlive;
173
+ return 0;
174
+ }
175
+ catch (error) {
176
+ if (!isAddressInUse(error))
177
+ throw error;
178
+ for (let attempt = 0; attempt < 20; attempt += 1) {
179
+ await _sleep(100);
180
+ const racedSession = await _readLiveSession();
181
+ if (racedSession) {
182
+ const proxyRuntime = await _startProxyRuntime(racedSession);
183
+ await proxyRuntime.keepAlive;
184
+ return 0;
185
+ }
186
+ }
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ stderr.write(`Error: MICA bridge port is already in use, but no live session became available. ${message}\n`);
189
+ return 1;
190
+ }
191
+ }
99
192
  // doctor
100
193
  if (command === "doctor") {
101
194
  if (!_runDoctor) {
@@ -166,6 +259,8 @@ async function main() {
166
259
  const { runInstaller, detectWolframUserBase } = (await import(installerUrl));
167
260
  const exitCode = await runCli(process.argv.slice(2), {
168
261
  startRuntime: async () => startBunRuntime(),
262
+ startProxyRuntime: async (session) => startProxyMcpRuntime(session),
263
+ readLiveSession: async () => readLiveSession(process.argv.slice(3)),
169
264
  runInstaller,
170
265
  runStatus: async () => runStatusCommand(),
171
266
  runConfig: runConfigCommand,
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { DEFAULT_TIMEOUTS_MS } from "../backend/protocol.js";
3
3
  import { abortEvaluationSchema, deleteCellSchema, getCellOutputSchema, insertCellSchema, listCellsSchema, modifyCellSchema, noArgsSchema, readArtifactSchema, readCellSchema, runCellSchema, selectNotebookSchema, saveNotebookSchema, symbolLookupSchema, } from "./toolSchemas.js";
4
4
  import { INSERT_ANCHOR_GUIDANCE, notebookToolDescription } from "./descriptions.js";
5
- import { toolSuccess, withToolErrors } from "./toolResults.js";
5
+ import { toolFailure, toolSuccess, withToolErrors } from "./toolResults.js";
6
6
  function assertLiveAgent(state) {
7
7
  if (!state.requireLiveAgent().ok) {
8
8
  throw new Error("NO_LIVE_AGENT");
@@ -75,26 +75,121 @@ function ensurePermission(notebook, tool, permission) {
75
75
  throw new Error(`PERMISSION_DENIED: ${tool}`);
76
76
  }
77
77
  }
78
- 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" }, () => {
78
+ const queuedNotebookTools = [
79
+ {
80
+ name: "mma_symbol_lookup",
81
+ summary: "Look up Wolfram Language symbol documentation. Provide an exact symbol name (e.g. 'Plot') for full details including usage, options, attributes, and documentation URL, or a partial name (e.g. 'integrate') for a list of matching symbols.",
82
+ schema: symbolLookupSchema.shape,
83
+ permission: "ReadNotebook",
84
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.symbolLookup,
85
+ },
86
+ {
87
+ name: "mma_list_cells",
88
+ summary: "List cells in the attached active Mathematica notebook.",
89
+ schema: listCellsSchema.shape,
90
+ permission: "ReadNotebook",
91
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.listCells,
92
+ },
93
+ {
94
+ name: "mma_read_cell",
95
+ summary: "Read one cell from the attached Mathematica notebook.",
96
+ schema: readCellSchema.shape,
97
+ permission: "ReadNotebook",
98
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.readCell,
99
+ },
100
+ {
101
+ name: "mma_insert_cell",
102
+ summary: "Insert a cell through the Mathematica FrontEnd bridge.",
103
+ schema: insertCellSchema.shape,
104
+ permission: "InsertCell",
105
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.insertCell,
106
+ extraGuidance: INSERT_ANCHOR_GUIDANCE,
107
+ requiresExplicitTarget: true,
108
+ },
109
+ {
110
+ name: "mma_modify_cell",
111
+ summary: "Modify one existing cell through the Mathematica FrontEnd bridge.",
112
+ schema: modifyCellSchema.shape,
113
+ permission: "ModifyCell",
114
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
115
+ requiresExplicitTarget: true,
116
+ },
117
+ {
118
+ name: "mma_delete_cell",
119
+ summary: "Delete one cell through the Mathematica FrontEnd bridge.",
120
+ schema: deleteCellSchema.shape,
121
+ permission: "DeleteCell",
122
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
123
+ requiresExplicitTarget: true,
124
+ },
125
+ {
126
+ name: "mma_run_cell",
127
+ summary: "Run one cell in the attached Mathematica notebook.",
128
+ schema: runCellSchema.shape,
129
+ permission: "RunCell",
130
+ timeoutMs: (args) => {
131
+ const timeoutSec = typeof args.timeoutSec === "number" ? args.timeoutSec : 120;
132
+ return timeoutSec * 1000;
133
+ },
134
+ requiresExplicitTarget: true,
135
+ },
136
+ {
137
+ name: "mma_abort_evaluation",
138
+ summary: "Abort the running Wolfram evaluation in the attached notebook.",
139
+ schema: abortEvaluationSchema.shape,
140
+ permission: "RunCell",
141
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
142
+ requiresExplicitTarget: true,
143
+ },
144
+ {
145
+ name: "mma_get_cell_output",
146
+ summary: "Read output and messages for one Mathematica notebook cell, refreshing completed run status when observed.",
147
+ schema: getCellOutputSchema.shape,
148
+ permission: "ReadNotebook",
149
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.readCell,
150
+ },
151
+ {
152
+ name: "mma_read_artifact",
153
+ summary: "Read one large output or message artifact by byte page. Artifact ids are resolved against current notebook state and may become stale after notebook edits or reruns.",
154
+ schema: readArtifactSchema.shape,
155
+ permission: "ReadNotebook",
156
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.readCell,
157
+ },
158
+ {
159
+ name: "mma_save_notebook",
160
+ summary: "Save the attached Mathematica notebook through the FrontEnd.",
161
+ schema: saveNotebookSchema.shape,
162
+ permission: "SaveNotebook",
163
+ timeoutMs: () => DEFAULT_TIMEOUTS_MS.mutation,
164
+ requiresExplicitTarget: true,
165
+ },
166
+ ];
167
+ export const MICA_BACKEND_TOOL_DEFINITIONS = [
168
+ {
169
+ name: "mma_status",
170
+ description: notebookToolDescription("Report backend status and notebook registry state."),
171
+ schema: noArgsSchema.shape,
172
+ },
173
+ {
174
+ name: "mma_list_notebooks",
175
+ description: notebookToolDescription("List notebooks registered with the Mathematica bridge Palette."),
176
+ schema: noArgsSchema.shape,
177
+ },
178
+ {
179
+ name: "mma_select_notebook",
180
+ description: notebookToolDescription("Select the active Mathematica notebook in the backend registry."),
181
+ schema: selectNotebookSchema.shape,
182
+ },
183
+ ...queuedNotebookTools.map((config) => ({
184
+ name: config.name,
185
+ description: notebookToolDescription(config.summary, config.extraGuidance),
186
+ schema: config.schema,
187
+ })),
188
+ ];
189
+ export async function executeBackendMcpTool(state, tool, args = {}, extra) {
190
+ const recordArgs = args && typeof args === "object" && !Array.isArray(args) ? args : {};
191
+ if (tool === "mma_status") {
192
+ return withToolErrors({ tool }, () => {
98
193
  sweepStateLiveness(state);
99
194
  return toolSuccess({
100
195
  server: "running",
@@ -103,16 +198,15 @@ export function registerBackendMcpTools(server, state) {
103
198
  agents: liveAgents(state),
104
199
  });
105
200
  });
106
- });
107
- 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" }, () => {
201
+ }
202
+ if (tool === "mma_list_notebooks") {
203
+ return withToolErrors({ tool }, () => {
109
204
  sweepStateLiveness(state);
110
205
  return toolSuccess({ notebooks: state.notebooks.listLive(), activeNotebookId: liveActiveNotebookId(state, extra?.sessionId) });
111
206
  });
112
- });
113
- 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 }, () => {
207
+ }
208
+ if (tool === "mma_select_notebook") {
209
+ return withToolErrors({ tool, args: recordArgs }, () => {
116
210
  const clientSessionId = extra?.sessionId;
117
211
  const target = resolveToolTarget(state, recordArgs, extra);
118
212
  state.setActiveNotebook(target.notebook.notebookId);
@@ -120,97 +214,26 @@ export function registerBackendMcpTools(server, state) {
120
214
  state.setActiveNotebook(target.notebook.notebookId, clientSessionId);
121
215
  return toolSuccess({ activeNotebookId: state.activeNotebookId, notebook: target.notebook });
122
216
  });
217
+ }
218
+ const queuedConfig = queuedNotebookTools.find((config) => config.name === tool);
219
+ if (!queuedConfig) {
220
+ return toolFailure(new Error(`UNKNOWN_TOOL: ${tool}`), { tool, args: recordArgs });
221
+ }
222
+ return withToolErrors({ tool: queuedConfig.name, args: recordArgs }, async () => {
223
+ if (process.env.MICA_STRICT_TARGETING === "1" && queuedConfig.requiresExplicitTarget) {
224
+ const hasNotebookId = typeof recordArgs.notebookId === "string" && recordArgs.notebookId.trim().length > 0;
225
+ const hasDisplayName = typeof recordArgs.displayName === "string" && recordArgs.displayName.trim().length > 0;
226
+ if (!hasNotebookId && !hasDisplayName) {
227
+ throw new Error("EXPLICIT_NOTEBOOK_REQUIRED");
228
+ }
229
+ }
230
+ const target = resolveToolTarget(state, recordArgs, extra);
231
+ ensurePermission(target.notebook, queuedConfig.name, queuedConfig.permission);
232
+ return queueNotebookOperation(state, target, queuedConfig.name, recordArgs, queuedConfig.timeoutMs(recordArgs), extra);
123
233
  });
124
- 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);
234
+ }
235
+ export function registerBackendMcpTools(server, state) {
236
+ for (const definition of MICA_BACKEND_TOOL_DEFINITIONS) {
237
+ server.tool(definition.name, definition.description, definition.schema, (args, extra) => executeBackendMcpTool(state, definition.name, args, extra));
215
238
  }
216
239
  }
@@ -33,7 +33,7 @@ export const MICA_AGENT_INSTRUCTIONS = [
33
33
  "Tools:",
34
34
  ...TOOL_GUIDE.map(([name, description]) => `- ${name}: ${description}`),
35
35
  ].join("\n");
36
- export function createMicaMcpServer(name, version = "1.0.1") {
36
+ export function createMicaMcpServer(name, version = "1.0.2") {
37
37
  return new McpServer({ name, version }, { instructions: MICA_AGENT_INSTRUCTIONS });
38
38
  }
39
39
  export function registerMicaPrompts(server) {
@@ -0,0 +1,36 @@
1
+ import { MICA_BACKEND_TOOL_DEFINITIONS } from "./backendTools.js";
2
+ import { toolFailure } from "./toolResults.js";
3
+ export function registerProxyMcpTools(server, session, deps = {}) {
4
+ for (const definition of MICA_BACKEND_TOOL_DEFINITIONS) {
5
+ server.tool(definition.name, definition.description, definition.schema, async (args, extra) => {
6
+ return callProxyTool(session, definition.name, args, extra, deps);
7
+ });
8
+ }
9
+ }
10
+ async function callProxyTool(session, tool, args, extra, deps) {
11
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
12
+ if (typeof fetchImpl !== "function") {
13
+ return toolFailure(new Error("FETCH_UNAVAILABLE"), { tool, args });
14
+ }
15
+ try {
16
+ const response = await fetchImpl(`${session.baseUrl}/mcp/call`, {
17
+ method: "POST",
18
+ headers: {
19
+ authorization: `Bearer ${session.authToken}`,
20
+ "content-type": "application/json",
21
+ },
22
+ body: JSON.stringify({
23
+ tool,
24
+ arguments: args ?? {},
25
+ ...(extra?.sessionId ? { clientSessionId: extra.sessionId } : {}),
26
+ }),
27
+ });
28
+ if (!response.ok) {
29
+ return toolFailure(new Error(`MCP_PROXY_HTTP_${response.status}`), { tool, args });
30
+ }
31
+ return (await response.json());
32
+ }
33
+ catch (error) {
34
+ return toolFailure(error, { tool, args });
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliceshimada/mica",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Local MCP bridge for controlling live Wolfram Desktop / Mathematica notebooks.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/bun/index.ts CHANGED
@@ -11,7 +11,7 @@ import { writeSessionFile } from "../runtime/session.js";
11
11
  import { createBunHttpApp } from "./httpServer.js";
12
12
 
13
13
  const MCP_SERVER_NAME = "mica-bun";
14
- const MICA_PACKAGE_VERSION = "1.0.1";
14
+ const MICA_PACKAGE_VERSION = "1.0.2";
15
15
 
16
16
  export type BunRuntimeDeps = {
17
17
  bridgeOnly?: boolean;