nebula-notebook-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -0
- package/bin/nebula-mcp.js +10 -0
- package/dist/circuit-breaker.d.ts +157 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker.js +237 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +314 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +13 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +31 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +237 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/notebook/client.d.ts +643 -0
- package/dist/notebook/client.d.ts.map +1 -0
- package/dist/notebook/client.js +1720 -0
- package/dist/notebook/client.js.map +1 -0
- package/dist/notebook/index.d.ts +6 -0
- package/dist/notebook/index.d.ts.map +1 -0
- package/dist/notebook/index.js +6 -0
- package/dist/notebook/index.js.map +1 -0
- package/dist/notebook/tools.d.ts +244 -0
- package/dist/notebook/tools.d.ts.map +1 -0
- package/dist/notebook/tools.js +279 -0
- package/dist/notebook/tools.js.map +1 -0
- package/dist/tools/execution.d.ts +38 -0
- package/dist/tools/execution.d.ts.map +1 -0
- package/dist/tools/execution.js +116 -0
- package/dist/tools/execution.js.map +1 -0
- package/dist/tools/files.d.ts +70 -0
- package/dist/tools/files.d.ts.map +1 -0
- package/dist/tools/files.js +286 -0
- package/dist/tools/files.js.map +1 -0
- package/dist/tools/index.d.ts +74 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +217 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/kernel.d.ts +36 -0
- package/dist/tools/kernel.d.ts.map +1 -0
- package/dist/tools/kernel.js +182 -0
- package/dist/tools/kernel.js.map +1 -0
- package/dist/tools/notebook.d.ts +252 -0
- package/dist/tools/notebook.d.ts.map +1 -0
- package/dist/tools/notebook.js +1089 -0
- package/dist/tools/notebook.js.map +1 -0
- package/dist/tools/types.d.ts +78 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +8 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/types.d.ts +473 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/imageResize.d.ts +24 -0
- package/dist/utils/imageResize.d.ts.map +1 -0
- package/dist/utils/imageResize.js +67 -0
- package/dist/utils/imageResize.js.map +1 -0
- package/dist/utils/polling.d.ts +40 -0
- package/dist/utils/polling.d.ts.map +1 -0
- package/dist/utils/polling.js +49 -0
- package/dist/utils/polling.js.map +1 -0
- package/package.json +61 -0
- package/setup-mcp.js +468 -0
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notebook Tools
|
|
3
|
+
*
|
|
4
|
+
* Tools for reading, writing, and manipulating notebook cells.
|
|
5
|
+
*/
|
|
6
|
+
import { createAdaptivePoller } from '../utils/polling.js';
|
|
7
|
+
import { resizeImageIfNeeded } from '../utils/imageResize.js';
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Shared Utilities
|
|
10
|
+
// =============================================================================
|
|
11
|
+
function formatCellPreview(cell, index) {
|
|
12
|
+
const typeTag = cell.type === 'code' ? '[code]' : '[md]';
|
|
13
|
+
const execCount = cell.executionCount ? ` [${cell.executionCount}]` : '';
|
|
14
|
+
const firstLine = cell.content.split('\n')[0].substring(0, 60);
|
|
15
|
+
const moreLines = cell.content.split('\n').length > 1 ? '...' : '';
|
|
16
|
+
// Use #N format (1-indexed) to match UI display
|
|
17
|
+
return `#${index + 1}: ${typeTag} id="${cell.id}"${execCount} ${firstLine}${moreLines}`;
|
|
18
|
+
}
|
|
19
|
+
async function formatOutputs(outputs, usePlaceholders = false) {
|
|
20
|
+
const results = [];
|
|
21
|
+
let imageIndex = 0;
|
|
22
|
+
for (const o of outputs) {
|
|
23
|
+
if (o.type === 'image') {
|
|
24
|
+
if (usePlaceholders) {
|
|
25
|
+
// Placeholder mode: show text description instead of base64 image
|
|
26
|
+
imageIndex++;
|
|
27
|
+
const sizeKB = Math.round(o.content.length * 0.75 / 1024);
|
|
28
|
+
results.push({ type: 'text', text: `[IMAGE ${imageIndex}: ~${sizeKB}KB PNG]` });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// Inline mode: resize images to fit Claude API limits (2000px max dimension)
|
|
32
|
+
const resizedData = await resizeImageIfNeeded(o.content);
|
|
33
|
+
results.push({ type: 'image', data: resizedData, mimeType: 'image/png' });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const prefix = o.type === 'error' ? '[ERROR] ' : '';
|
|
38
|
+
results.push({ type: 'text', text: prefix + o.content });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
export const readNotebookTool = {
|
|
44
|
+
definition: {
|
|
45
|
+
name: 'read_notebook',
|
|
46
|
+
description: 'Read all cells from a notebook. Formats: "content" (full code, no outputs), "detailed" (full code + inline images, default), "placeholder" (full code + text placeholders for images, recommended for Gemini). Outputs are truncated (100 lines regular, 200 lines errors).',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
path: { type: 'string', description: 'Path to the notebook file (.ipynb)' },
|
|
51
|
+
// NOTE: "brief" is intentionally omitted from schema to hide it from tool UIs.
|
|
52
|
+
// It remains supported in code for backward compatibility and is likely to be deprecated.
|
|
53
|
+
format: { type: 'string', enum: ['content', 'detailed', 'placeholder'], description: 'Output format: content (full code), detailed (code + inline images, default), placeholder (code + image placeholders, recommended for Gemini)' },
|
|
54
|
+
include_outputs: { type: 'boolean', description: 'Override output inclusion (default: true for detailed/placeholder, false for others)' },
|
|
55
|
+
max_lines: { type: 'number', description: 'Max lines per regular output (default: 100)' },
|
|
56
|
+
max_chars: { type: 'number', description: 'Max chars per regular output (default: 10000)' },
|
|
57
|
+
max_lines_error: { type: 'number', description: 'Max lines per error output (default: 200)' },
|
|
58
|
+
max_chars_error: { type: 'number', description: 'Max chars per error output (default: 20000)' },
|
|
59
|
+
},
|
|
60
|
+
required: ['path'],
|
|
61
|
+
},
|
|
62
|
+
annotations: { readOnlyHint: true },
|
|
63
|
+
},
|
|
64
|
+
async execute(params, client) {
|
|
65
|
+
const format = params.format ?? 'detailed';
|
|
66
|
+
// Default include_outputs based on format, but allow override
|
|
67
|
+
const includeOutputs = params.include_outputs ?? (format === 'detailed' || format === 'placeholder');
|
|
68
|
+
const result = await client.readNotebookViaRouter(params.path, {
|
|
69
|
+
includeOutputs,
|
|
70
|
+
maxLines: params.max_lines,
|
|
71
|
+
maxChars: params.max_chars,
|
|
72
|
+
maxLinesError: params.max_lines_error,
|
|
73
|
+
maxCharsError: params.max_chars_error,
|
|
74
|
+
});
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
return { success: false, error: result.error };
|
|
77
|
+
}
|
|
78
|
+
const notebook = result.data;
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
data: {
|
|
82
|
+
path: params.path,
|
|
83
|
+
cells: notebook.cells,
|
|
84
|
+
totalCells: notebook.cells.length,
|
|
85
|
+
backend: notebook.backend,
|
|
86
|
+
format,
|
|
87
|
+
includeOutputs,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
async formatForMCP(result) {
|
|
92
|
+
if (!result.success) {
|
|
93
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
94
|
+
}
|
|
95
|
+
const { path, cells, totalCells, format, includeOutputs } = result.data;
|
|
96
|
+
// Brief format: just show previews
|
|
97
|
+
if (format === 'brief') {
|
|
98
|
+
const lines = [`Notebook: ${path} (${totalCells} cells)\n`];
|
|
99
|
+
cells.forEach((cell, i) => lines.push(formatCellPreview(cell, i)));
|
|
100
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
101
|
+
}
|
|
102
|
+
// Content and Detailed formats: show full cell content
|
|
103
|
+
const content = [];
|
|
104
|
+
content.push({ type: 'text', text: `Notebook: ${path} (${totalCells} cells)\n` });
|
|
105
|
+
for (let i = 0; i < cells.length; i++) {
|
|
106
|
+
const cell = cells[i];
|
|
107
|
+
const typeTag = cell.type === 'code' ? '[code]' : '[markdown]';
|
|
108
|
+
const execCount = cell.executionCount ? ` In[${cell.executionCount}]` : '';
|
|
109
|
+
// Cell header and content (use #N format, 1-indexed, to match UI display)
|
|
110
|
+
const cellText = `\n${'─'.repeat(60)}\n#${i + 1} ${typeTag} id="${cell.id}"${execCount}\n${'─'.repeat(60)}\n${cell.content}`;
|
|
111
|
+
content.push({ type: 'text', text: cellText });
|
|
112
|
+
// Outputs (if included and present)
|
|
113
|
+
if (includeOutputs && cell.outputs && cell.outputs.length > 0) {
|
|
114
|
+
content.push({ type: 'text', text: `\n--- Output ---` });
|
|
115
|
+
const usePlaceholders = format === 'placeholder';
|
|
116
|
+
const outputContent = await formatOutputs(cell.outputs, usePlaceholders);
|
|
117
|
+
content.push(...outputContent);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return content;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
export const readCellTool = {
|
|
124
|
+
definition: {
|
|
125
|
+
name: 'read_cell',
|
|
126
|
+
description: 'Read a specific cell content and metadata (not outputs). Use read_output to get outputs.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
131
|
+
cell_index: { type: 'number', description: 'Cell index (0-based). Use this OR cell_id.' },
|
|
132
|
+
cell_id: { type: 'string', description: 'Stable cell ID. Use this OR cell_index.' },
|
|
133
|
+
},
|
|
134
|
+
required: ['path'],
|
|
135
|
+
},
|
|
136
|
+
annotations: { readOnlyHint: true },
|
|
137
|
+
},
|
|
138
|
+
async execute(params, client) {
|
|
139
|
+
if (params.cell_index === undefined && !params.cell_id) {
|
|
140
|
+
return { success: false, error: 'Must provide cell_index or cell_id' };
|
|
141
|
+
}
|
|
142
|
+
const result = await client.readCellOp(params.path, {
|
|
143
|
+
cellIndex: params.cell_index,
|
|
144
|
+
cellId: params.cell_id,
|
|
145
|
+
});
|
|
146
|
+
if (!result.success) {
|
|
147
|
+
return { success: false, error: result.error };
|
|
148
|
+
}
|
|
149
|
+
return { success: true, data: result.data };
|
|
150
|
+
},
|
|
151
|
+
formatForMCP(result) {
|
|
152
|
+
if (!result.success) {
|
|
153
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
154
|
+
}
|
|
155
|
+
const { cell, cellIndex } = result.data;
|
|
156
|
+
const execInfo = cell.executionCount ? ` execution [${cell.executionCount}]` : '';
|
|
157
|
+
// Use #N format (1-indexed) to match UI display
|
|
158
|
+
return [{ type: 'text', text: `#${cellIndex + 1} [${cell.type}] id="${cell.id}"${execInfo}\n---\n${cell.content}` }];
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
export const readOutputTool = {
|
|
162
|
+
definition: {
|
|
163
|
+
name: 'read_output',
|
|
164
|
+
description: `Read cell outputs. By default waits for completion or max_wait timeout (returns once). Set wait_for_completion=false to return as soon as new output appears. Regular outputs truncated to 100 lines, errors get 200 lines. Use save_to_file=true for complete output.`,
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
169
|
+
cell_index: { type: 'number', description: 'Cell index (0-based). Use this OR cell_id.' },
|
|
170
|
+
cell_id: { type: 'string', description: 'Stable cell ID. Use this OR cell_index.' },
|
|
171
|
+
output_offset: { type: 'number', description: 'Skip first N outputs (default: 0)' },
|
|
172
|
+
max_wait: { type: 'number', description: 'Wait up to N seconds for completion or timeout (default: 60). Set 0 for immediate read.' },
|
|
173
|
+
wait_for_completion: { type: 'boolean', description: 'Wait for completion or timeout (default: true). If false, return on new output.' },
|
|
174
|
+
max_lines: { type: 'number', description: 'Max lines per regular output (default: 100)' },
|
|
175
|
+
max_chars: { type: 'number', description: 'Max chars per regular output (default: 10000)' },
|
|
176
|
+
max_lines_error: { type: 'number', description: 'Max lines per error output (default: 200)' },
|
|
177
|
+
max_chars_error: { type: 'number', description: 'Max chars per error output (default: 20000)' },
|
|
178
|
+
line_offset: { type: 'number', description: 'Skip first N lines for pagination (default: 0). Use with truncation metadata to paginate through large outputs.' },
|
|
179
|
+
save_to_file: { type: 'boolean', description: 'Save full output to temp file for analysis (default: false).' },
|
|
180
|
+
},
|
|
181
|
+
required: ['path'],
|
|
182
|
+
},
|
|
183
|
+
annotations: { readOnlyHint: true },
|
|
184
|
+
},
|
|
185
|
+
async execute(params, client) {
|
|
186
|
+
if (params.cell_index === undefined && !params.cell_id) {
|
|
187
|
+
return { success: false, error: 'Must provide cell_index or cell_id' };
|
|
188
|
+
}
|
|
189
|
+
const waitForCompletion = params.wait_for_completion ?? true;
|
|
190
|
+
const maxWait = params.max_wait ?? (waitForCompletion ? 60 : 0);
|
|
191
|
+
// Build truncation options
|
|
192
|
+
const truncationOpts = {
|
|
193
|
+
maxLines: params.max_lines,
|
|
194
|
+
maxChars: params.max_chars,
|
|
195
|
+
maxLinesError: params.max_lines_error,
|
|
196
|
+
maxCharsError: params.max_chars_error,
|
|
197
|
+
lineOffset: params.line_offset,
|
|
198
|
+
saveToFile: params.save_to_file,
|
|
199
|
+
};
|
|
200
|
+
// For immediate reads, use efficient operation router
|
|
201
|
+
if (maxWait <= 0) {
|
|
202
|
+
const result = await client.readCellOutputOp(params.path, {
|
|
203
|
+
cellIndex: params.cell_index,
|
|
204
|
+
cellId: params.cell_id,
|
|
205
|
+
...truncationOpts,
|
|
206
|
+
});
|
|
207
|
+
if (!result.success) {
|
|
208
|
+
return { success: false, error: result.error };
|
|
209
|
+
}
|
|
210
|
+
const offset = params.output_offset ?? 0;
|
|
211
|
+
const allOutputs = result.data.outputs;
|
|
212
|
+
return {
|
|
213
|
+
success: true,
|
|
214
|
+
data: {
|
|
215
|
+
cellId: result.data.cellId,
|
|
216
|
+
cellIndex: result.data.cellIndex,
|
|
217
|
+
outputs: offset > 0 ? allOutputs.slice(offset) : allOutputs,
|
|
218
|
+
totalOutputs: allOutputs.length,
|
|
219
|
+
temp_files: result.data.temp_files,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// Adaptive polling: start with 50ms, exponentially increase to 1000ms
|
|
224
|
+
const startTime = Date.now();
|
|
225
|
+
const poller = createAdaptivePoller({ initialInterval: 50, maxInterval: 1000 });
|
|
226
|
+
let lastOutputCount = 0;
|
|
227
|
+
while (Date.now() - startTime < maxWait * 1000) {
|
|
228
|
+
const result = await client.readCellOutputOp(params.path, {
|
|
229
|
+
cellIndex: params.cell_index,
|
|
230
|
+
cellId: params.cell_id,
|
|
231
|
+
...truncationOpts,
|
|
232
|
+
});
|
|
233
|
+
if (!result.success) {
|
|
234
|
+
return { success: false, error: result.error };
|
|
235
|
+
}
|
|
236
|
+
const allOutputs = result.data.outputs;
|
|
237
|
+
const offset = params.output_offset ?? 0;
|
|
238
|
+
const newOutputs = offset > 0 ? allOutputs.slice(offset) : allOutputs;
|
|
239
|
+
const executionStatus = result.data.executionStatus;
|
|
240
|
+
// Return on new output only when wait_for_completion is false
|
|
241
|
+
if (!waitForCompletion && allOutputs.length > lastOutputCount) {
|
|
242
|
+
return {
|
|
243
|
+
success: true,
|
|
244
|
+
data: {
|
|
245
|
+
cellId: result.data.cellId,
|
|
246
|
+
cellIndex: result.data.cellIndex,
|
|
247
|
+
outputs: newOutputs,
|
|
248
|
+
totalOutputs: allOutputs.length,
|
|
249
|
+
temp_files: result.data.temp_files,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// If backend provides execution status and it's no longer busy, return current outputs
|
|
254
|
+
if (waitForCompletion && executionStatus && executionStatus !== 'busy') {
|
|
255
|
+
return {
|
|
256
|
+
success: true,
|
|
257
|
+
data: {
|
|
258
|
+
cellId: result.data.cellId,
|
|
259
|
+
cellIndex: result.data.cellIndex,
|
|
260
|
+
outputs: newOutputs,
|
|
261
|
+
totalOutputs: allOutputs.length,
|
|
262
|
+
temp_files: result.data.temp_files,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
lastOutputCount = allOutputs.length;
|
|
267
|
+
await poller.wait();
|
|
268
|
+
poller.incrementInterval();
|
|
269
|
+
}
|
|
270
|
+
// Timeout - return current outputs
|
|
271
|
+
const finalResult = await client.readCellOutputOp(params.path, {
|
|
272
|
+
cellIndex: params.cell_index,
|
|
273
|
+
cellId: params.cell_id,
|
|
274
|
+
...truncationOpts,
|
|
275
|
+
});
|
|
276
|
+
if (!finalResult.success) {
|
|
277
|
+
return { success: false, error: finalResult.error };
|
|
278
|
+
}
|
|
279
|
+
const offset = params.output_offset ?? 0;
|
|
280
|
+
const allOutputs = finalResult.data.outputs;
|
|
281
|
+
return {
|
|
282
|
+
success: true,
|
|
283
|
+
data: {
|
|
284
|
+
cellId: finalResult.data.cellId,
|
|
285
|
+
cellIndex: finalResult.data.cellIndex,
|
|
286
|
+
outputs: offset > 0 ? allOutputs.slice(offset) : allOutputs,
|
|
287
|
+
totalOutputs: allOutputs.length,
|
|
288
|
+
temp_files: finalResult.data.temp_files,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
async formatForMCP(result) {
|
|
293
|
+
if (!result.success) {
|
|
294
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
295
|
+
}
|
|
296
|
+
const { cellId, cellIndex, outputs, totalOutputs, temp_files } = result.data;
|
|
297
|
+
// Use #N format (1-indexed) to match UI display
|
|
298
|
+
const cellRef = `#${cellIndex + 1} (id="${cellId}")`;
|
|
299
|
+
if (outputs.length === 0) {
|
|
300
|
+
return [{ type: 'text', text: `${cellRef}: No outputs (total: ${totalOutputs})` }];
|
|
301
|
+
}
|
|
302
|
+
const mcpOutputs = [];
|
|
303
|
+
let headerInfo = `${cellRef}: ${outputs.length} outputs (total: ${totalOutputs})`;
|
|
304
|
+
// Add temp file info if present
|
|
305
|
+
if (temp_files && temp_files.length > 0) {
|
|
306
|
+
headerInfo += `\n📁 Large output saved to: ${temp_files.join(', ')}`;
|
|
307
|
+
}
|
|
308
|
+
mcpOutputs.push({ type: 'text', text: headerInfo + '\n' });
|
|
309
|
+
// Format each output with truncation metadata
|
|
310
|
+
for (const output of outputs) {
|
|
311
|
+
// Images are resized to fit Claude API limits (2000px max dimension)
|
|
312
|
+
if (output.type === 'image') {
|
|
313
|
+
const resizedData = await resizeImageIfNeeded(output.content);
|
|
314
|
+
mcpOutputs.push({ type: 'image', data: resizedData, mimeType: 'image/png' });
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
let text = output.type === 'error' ? '[ERROR] ' + output.content : output.content;
|
|
318
|
+
// Add truncation info if applicable
|
|
319
|
+
if (output.truncated) {
|
|
320
|
+
const range = output.returned_range;
|
|
321
|
+
const info = [
|
|
322
|
+
`\n--- [TRUNCATED: ${output.truncation_reason}]`,
|
|
323
|
+
`Lines ${range?.start_line ?? 0}-${range?.end_line ?? '?'} of ${output.total_lines ?? '?'}`,
|
|
324
|
+
`(${range?.char_count ?? '?'} of ${output.total_chars ?? '?'} chars)`,
|
|
325
|
+
];
|
|
326
|
+
if (output.temp_file) {
|
|
327
|
+
info.push(`Full output saved to: ${output.temp_file}`);
|
|
328
|
+
}
|
|
329
|
+
text += info.join(' | ') + ' ---';
|
|
330
|
+
}
|
|
331
|
+
mcpOutputs.push({ type: 'text', text });
|
|
332
|
+
}
|
|
333
|
+
return mcpOutputs;
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
export const insertCellTool = {
|
|
337
|
+
definition: {
|
|
338
|
+
name: 'insert_cell',
|
|
339
|
+
description: 'Insert a new cell into the notebook. Use position=-1 or omit to append at end.',
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: 'object',
|
|
342
|
+
properties: {
|
|
343
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
344
|
+
cell_id: { type: 'string', description: 'Unique ID for the new cell' },
|
|
345
|
+
content: { type: 'string', description: 'Cell content' },
|
|
346
|
+
cell_type: { type: 'string', enum: ['code', 'markdown'], description: 'Cell type (default: code)' },
|
|
347
|
+
position: { type: 'number', description: 'Position to insert at (0-based). Use -1 or omit to append.' },
|
|
348
|
+
},
|
|
349
|
+
required: ['path', 'cell_id', 'content'],
|
|
350
|
+
},
|
|
351
|
+
annotations: { destructiveHint: true },
|
|
352
|
+
},
|
|
353
|
+
async execute(params, client) {
|
|
354
|
+
const { path, cell_id, content, cell_type = 'code', position = -1 } = params;
|
|
355
|
+
const result = await client.insertCellOp(path, position, {
|
|
356
|
+
id: cell_id,
|
|
357
|
+
type: cell_type,
|
|
358
|
+
content,
|
|
359
|
+
});
|
|
360
|
+
if (!result.success) {
|
|
361
|
+
return { success: false, error: result.error };
|
|
362
|
+
}
|
|
363
|
+
return { success: true, data: result.data };
|
|
364
|
+
},
|
|
365
|
+
formatForMCP(result) {
|
|
366
|
+
if (!result.success) {
|
|
367
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
368
|
+
}
|
|
369
|
+
const { cellIndex, cellId, idModified, requestedId } = result.data;
|
|
370
|
+
let msg = `Inserted cell at #${cellIndex + 1}, id="${cellId}"`;
|
|
371
|
+
if (idModified) {
|
|
372
|
+
msg += ` (requested "${requestedId}" was auto-fixed)`;
|
|
373
|
+
}
|
|
374
|
+
return [{ type: 'text', text: msg }];
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
export const updateCellTool = {
|
|
378
|
+
definition: {
|
|
379
|
+
name: 'update_cell',
|
|
380
|
+
description: 'Update an existing cell by its stable ID. Can update content and/or type.',
|
|
381
|
+
inputSchema: {
|
|
382
|
+
type: 'object',
|
|
383
|
+
properties: {
|
|
384
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
385
|
+
cell_id: { type: 'string', description: 'Cell ID to update' },
|
|
386
|
+
content: { type: 'string', description: 'New cell content (optional)' },
|
|
387
|
+
cell_type: { type: 'string', enum: ['code', 'markdown'], description: 'New cell type (optional)' },
|
|
388
|
+
},
|
|
389
|
+
required: ['path', 'cell_id'],
|
|
390
|
+
},
|
|
391
|
+
annotations: { destructiveHint: true },
|
|
392
|
+
},
|
|
393
|
+
async execute(params, client) {
|
|
394
|
+
const { path, cell_id, content, cell_type } = params;
|
|
395
|
+
if (content === undefined && cell_type === undefined) {
|
|
396
|
+
return { success: false, error: 'Must provide content or cell_type to update' };
|
|
397
|
+
}
|
|
398
|
+
// If content is provided, update it
|
|
399
|
+
if (content !== undefined) {
|
|
400
|
+
const result = await client.updateContentOp(path, cell_id, content);
|
|
401
|
+
if (!result.success) {
|
|
402
|
+
return { success: false, error: result.error };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// If cell_type is provided, update metadata
|
|
406
|
+
if (cell_type !== undefined) {
|
|
407
|
+
const result = await client.updateMetadataOp(path, cell_id, { type: cell_type });
|
|
408
|
+
if (!result.success) {
|
|
409
|
+
return { success: false, error: result.error };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// Read back to get cell index
|
|
413
|
+
const readResult = await client.readCellOp(path, { cellId: cell_id });
|
|
414
|
+
if (!readResult.success) {
|
|
415
|
+
return { success: false, error: readResult.error };
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
success: true,
|
|
419
|
+
data: { cellIndex: readResult.data.cellIndex, cellId: cell_id },
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
formatForMCP(result) {
|
|
423
|
+
if (!result.success) {
|
|
424
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
425
|
+
}
|
|
426
|
+
const { cellIndex, cellId } = result.data;
|
|
427
|
+
return [{ type: 'text', text: `Updated cell #${cellIndex + 1}, id="${cellId}"` }];
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
export const deleteCellTool = {
|
|
431
|
+
definition: {
|
|
432
|
+
name: 'delete_cell',
|
|
433
|
+
description: 'Delete a cell from a notebook',
|
|
434
|
+
inputSchema: {
|
|
435
|
+
type: 'object',
|
|
436
|
+
properties: {
|
|
437
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
438
|
+
cell_index: { type: 'number', description: 'Cell index to delete (0-based)' },
|
|
439
|
+
cell_id: { type: 'string', description: 'Cell ID to delete' },
|
|
440
|
+
},
|
|
441
|
+
required: ['path'],
|
|
442
|
+
},
|
|
443
|
+
annotations: { destructiveHint: true },
|
|
444
|
+
},
|
|
445
|
+
async execute(params, client) {
|
|
446
|
+
if (params.cell_index === undefined && !params.cell_id) {
|
|
447
|
+
return { success: false, error: 'Must provide cell_index or cell_id' };
|
|
448
|
+
}
|
|
449
|
+
const result = await client.deleteCellOp(params.path, {
|
|
450
|
+
cellIndex: params.cell_index,
|
|
451
|
+
cellId: params.cell_id,
|
|
452
|
+
});
|
|
453
|
+
return result.success ? { success: true } : { success: false, error: result.error };
|
|
454
|
+
},
|
|
455
|
+
formatForMCP(result) {
|
|
456
|
+
if (!result.success) {
|
|
457
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
458
|
+
}
|
|
459
|
+
return [{ type: 'text', text: 'Cell deleted' }];
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
export const createNotebookTool = {
|
|
463
|
+
definition: {
|
|
464
|
+
name: 'create_notebook',
|
|
465
|
+
description: 'Create a new empty notebook. If the notebook is open in the browser UI, it will attempt to open the new notebook in a new tab.',
|
|
466
|
+
inputSchema: {
|
|
467
|
+
type: 'object',
|
|
468
|
+
properties: {
|
|
469
|
+
path: { type: 'string', description: 'Path for the new notebook file (.ipynb)' },
|
|
470
|
+
overwrite: { type: 'boolean', description: 'Allow overwriting existing file (default: false)' },
|
|
471
|
+
kernel_name: { type: 'string', description: 'Kernel name (default: python3)' },
|
|
472
|
+
kernel_display_name: { type: 'string', description: 'Display name for kernel (default: Python 3)' },
|
|
473
|
+
},
|
|
474
|
+
required: ['path'],
|
|
475
|
+
},
|
|
476
|
+
annotations: { destructiveHint: true },
|
|
477
|
+
},
|
|
478
|
+
async execute(params, client) {
|
|
479
|
+
const result = await client.createNotebookOp(params.path, {
|
|
480
|
+
overwrite: params.overwrite,
|
|
481
|
+
kernelName: params.kernel_name,
|
|
482
|
+
kernelDisplayName: params.kernel_display_name,
|
|
483
|
+
});
|
|
484
|
+
if (!result.success) {
|
|
485
|
+
return { success: false, error: result.error };
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
success: true,
|
|
489
|
+
data: {
|
|
490
|
+
path: result.data?.path ?? params.path,
|
|
491
|
+
mtime: result.data?.mtime,
|
|
492
|
+
popupBlocked: result.data?.popupBlocked,
|
|
493
|
+
popupMessage: result.data?.popupMessage,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
formatForMCP(result) {
|
|
498
|
+
if (!result.success) {
|
|
499
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
500
|
+
}
|
|
501
|
+
const { path, popupBlocked, popupMessage } = result.data;
|
|
502
|
+
let msg = `Created notebook: ${path}`;
|
|
503
|
+
if (popupBlocked && popupMessage) {
|
|
504
|
+
msg += `\n⚠️ ${popupMessage}`;
|
|
505
|
+
}
|
|
506
|
+
return [{ type: 'text', text: msg }];
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
export const moveCellTool = {
|
|
510
|
+
definition: {
|
|
511
|
+
name: 'move_cell',
|
|
512
|
+
description: 'Move a cell from one position to another. Supports two modes: by index (from_index, to_index) or by ID (cell_id with after_cell_id or to_index=-1 for start).',
|
|
513
|
+
inputSchema: {
|
|
514
|
+
type: 'object',
|
|
515
|
+
properties: {
|
|
516
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
517
|
+
from_index: { type: 'number', description: 'Current cell index (0-based). Use this OR cell_id.' },
|
|
518
|
+
to_index: { type: 'number', description: 'Target cell index (0-based). Use -1 to move to start.' },
|
|
519
|
+
cell_id: { type: 'string', description: 'ID of cell to move. Use this OR from_index.' },
|
|
520
|
+
after_cell_id: { type: 'string', description: 'Move after this cell ID. Alternative to to_index.' },
|
|
521
|
+
},
|
|
522
|
+
required: ['path'],
|
|
523
|
+
},
|
|
524
|
+
annotations: { destructiveHint: true },
|
|
525
|
+
},
|
|
526
|
+
async execute(params, client) {
|
|
527
|
+
const { path, from_index, to_index, cell_id, after_cell_id } = params;
|
|
528
|
+
// Validate parameters
|
|
529
|
+
if (from_index === undefined && !cell_id) {
|
|
530
|
+
return { success: false, error: 'Must provide from_index or cell_id' };
|
|
531
|
+
}
|
|
532
|
+
if (to_index === undefined && !after_cell_id) {
|
|
533
|
+
return { success: false, error: 'Must provide to_index or after_cell_id' };
|
|
534
|
+
}
|
|
535
|
+
const result = await client.moveCellOp(path, from_index ?? 0, to_index ?? 0, { cellId: cell_id, afterCellId: after_cell_id });
|
|
536
|
+
if (!result.success) {
|
|
537
|
+
return { success: false, error: result.error };
|
|
538
|
+
}
|
|
539
|
+
return { success: true, data: result.data };
|
|
540
|
+
},
|
|
541
|
+
formatForMCP(result) {
|
|
542
|
+
if (!result.success) {
|
|
543
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
544
|
+
}
|
|
545
|
+
const { cellId, fromIndex, toIndex } = result.data;
|
|
546
|
+
const idInfo = cellId ? ` (id="${cellId}")` : '';
|
|
547
|
+
return [{ type: 'text', text: `Cell moved from #${fromIndex + 1} to #${toIndex + 1}${idInfo}` }];
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
export const duplicateCellTool = {
|
|
551
|
+
definition: {
|
|
552
|
+
name: 'duplicate_cell',
|
|
553
|
+
description: 'Duplicate a cell, inserting the copy immediately after the original',
|
|
554
|
+
inputSchema: {
|
|
555
|
+
type: 'object',
|
|
556
|
+
properties: {
|
|
557
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
558
|
+
cell_index: { type: 'number', description: 'Cell index to duplicate (0-based)' },
|
|
559
|
+
},
|
|
560
|
+
required: ['path', 'cell_index'],
|
|
561
|
+
},
|
|
562
|
+
annotations: { destructiveHint: true },
|
|
563
|
+
},
|
|
564
|
+
async execute(params, client) {
|
|
565
|
+
// Generate a unique ID for the duplicated cell
|
|
566
|
+
const newCellId = `cell-dup-${Date.now()}`;
|
|
567
|
+
const result = await client.duplicateCellOp(params.path, params.cell_index, newCellId);
|
|
568
|
+
if (!result.success) {
|
|
569
|
+
return { success: false, error: result.error };
|
|
570
|
+
}
|
|
571
|
+
// Use metadata from response instead of extra read (Phase 1.2 optimization)
|
|
572
|
+
const totalCells = result.data.metadata?.totalCells ?? result.data.cellIndex + 1;
|
|
573
|
+
return {
|
|
574
|
+
success: true,
|
|
575
|
+
data: {
|
|
576
|
+
newCellIndex: result.data.cellIndex,
|
|
577
|
+
totalCells,
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
},
|
|
581
|
+
formatForMCP(result) {
|
|
582
|
+
if (!result.success) {
|
|
583
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
584
|
+
}
|
|
585
|
+
const { newCellIndex, totalCells } = result.data;
|
|
586
|
+
return [{ type: 'text', text: `Cell duplicated at #${newCellIndex + 1} (${totalCells} cells total)` }];
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
export const searchCellsTool = {
|
|
590
|
+
definition: {
|
|
591
|
+
name: 'search_cells',
|
|
592
|
+
description: 'Search notebook cells by keyword. Can search in cell source code and optionally in outputs.',
|
|
593
|
+
inputSchema: {
|
|
594
|
+
type: 'object',
|
|
595
|
+
properties: {
|
|
596
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
597
|
+
query: { type: 'string', description: 'Search query (keywords)' },
|
|
598
|
+
include_outputs: { type: 'boolean', description: 'Also search in cell outputs (default: false)' },
|
|
599
|
+
limit: { type: 'number', description: 'Maximum results (default: 10)' },
|
|
600
|
+
},
|
|
601
|
+
required: ['path', 'query'],
|
|
602
|
+
},
|
|
603
|
+
annotations: { readOnlyHint: true },
|
|
604
|
+
},
|
|
605
|
+
async execute(params, client) {
|
|
606
|
+
const result = await client.searchCellsOp(params.path, params.query, {
|
|
607
|
+
includeOutputs: params.include_outputs,
|
|
608
|
+
limit: params.limit,
|
|
609
|
+
});
|
|
610
|
+
if (!result.success) {
|
|
611
|
+
return { success: false, error: result.error };
|
|
612
|
+
}
|
|
613
|
+
return { success: true, data: result.data };
|
|
614
|
+
},
|
|
615
|
+
formatForMCP(result) {
|
|
616
|
+
if (!result.success) {
|
|
617
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
618
|
+
}
|
|
619
|
+
const { matches, matchCount, hasMore } = result.data;
|
|
620
|
+
if (matches.length === 0) {
|
|
621
|
+
return [{ type: 'text', text: 'No matching cells found' }];
|
|
622
|
+
}
|
|
623
|
+
const lines = [`Found ${matchCount} matches:\n`];
|
|
624
|
+
matches.forEach(m => {
|
|
625
|
+
const location = m.matchLocation === 'output'
|
|
626
|
+
? `output[${m.outputIndex}] (${m.outputType})`
|
|
627
|
+
: `source${m.matchLine !== undefined ? `:${m.matchLine}` : ''}`;
|
|
628
|
+
lines.push(`Cell ${m.cellIndex} [${location}] id="${m.cellId}"`);
|
|
629
|
+
lines.push(` ${m.preview}`);
|
|
630
|
+
});
|
|
631
|
+
if (hasMore) {
|
|
632
|
+
lines.push(`\n(more results available, increase limit)`);
|
|
633
|
+
}
|
|
634
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
export const updateMetadataTool = {
|
|
638
|
+
definition: {
|
|
639
|
+
name: 'update_metadata',
|
|
640
|
+
description: 'Update cell metadata (id, type, scrolled, scrolledHeight). Validates against schema.',
|
|
641
|
+
inputSchema: {
|
|
642
|
+
type: 'object',
|
|
643
|
+
properties: {
|
|
644
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
645
|
+
cell_id: { type: 'string', description: 'Current cell ID to update' },
|
|
646
|
+
changes: { type: 'object', description: 'Metadata changes: id, type, scrolled, scrolledHeight' },
|
|
647
|
+
},
|
|
648
|
+
required: ['path', 'cell_id', 'changes'],
|
|
649
|
+
},
|
|
650
|
+
annotations: { destructiveHint: true },
|
|
651
|
+
},
|
|
652
|
+
async execute(params, client) {
|
|
653
|
+
if (!params.changes || Object.keys(params.changes).length === 0) {
|
|
654
|
+
return { success: false, error: 'No changes provided' };
|
|
655
|
+
}
|
|
656
|
+
// Read cell before to get old values
|
|
657
|
+
const beforeResult = await client.readCellOp(params.path, { cellId: params.cell_id });
|
|
658
|
+
if (!beforeResult.success) {
|
|
659
|
+
return { success: false, error: beforeResult.error };
|
|
660
|
+
}
|
|
661
|
+
const oldCell = beforeResult.data.cell;
|
|
662
|
+
const cellIndex = beforeResult.data.cellIndex;
|
|
663
|
+
// Apply the update
|
|
664
|
+
const result = await client.updateMetadataOp(params.path, params.cell_id, params.changes);
|
|
665
|
+
if (!result.success) {
|
|
666
|
+
return { success: false, error: result.error };
|
|
667
|
+
}
|
|
668
|
+
// Compute changes from requested changes and old values
|
|
669
|
+
const changes = {};
|
|
670
|
+
const oldCellId = 'id' in params.changes ? params.cell_id : undefined;
|
|
671
|
+
let newCellId = params.cell_id;
|
|
672
|
+
for (const [key, newValue] of Object.entries(params.changes)) {
|
|
673
|
+
const oldValue = oldCell[key];
|
|
674
|
+
changes[key] = { old: oldValue, new: newValue };
|
|
675
|
+
if (key === 'id') {
|
|
676
|
+
newCellId = newValue;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
success: true,
|
|
681
|
+
data: {
|
|
682
|
+
cellId: newCellId,
|
|
683
|
+
cellIndex,
|
|
684
|
+
changes,
|
|
685
|
+
oldCellId,
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
},
|
|
689
|
+
formatForMCP(result) {
|
|
690
|
+
if (!result.success) {
|
|
691
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
692
|
+
}
|
|
693
|
+
const { cellIndex, changes, oldCellId, cellId } = result.data;
|
|
694
|
+
const changesSummary = Object.entries(changes)
|
|
695
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v.old)} → ${JSON.stringify(v.new)}`)
|
|
696
|
+
.join(', ');
|
|
697
|
+
let msg = `Cell ${cellIndex} metadata updated: ${changesSummary}`;
|
|
698
|
+
if (oldCellId) {
|
|
699
|
+
msg += ` (ID changed from "${oldCellId}" to "${cellId}")`;
|
|
700
|
+
}
|
|
701
|
+
return [{ type: 'text', text: msg }];
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
export const connectServerTool = {
|
|
705
|
+
definition: {
|
|
706
|
+
name: 'connect_server',
|
|
707
|
+
description: `Connect to a Nebula server. REQUIRED: call this once at the start of every MCP session before any other tool call. base_url is required; the server does not assume a default. All subsequent operations will use this connection.
|
|
708
|
+
|
|
709
|
+
RECOMMENDED WORKFLOW:
|
|
710
|
+
1. connect_server (required at session start)
|
|
711
|
+
2. For each response: start_agent_session → operations → end_agent_session
|
|
712
|
+
|
|
713
|
+
start_agent_session is required for notebook mutations; read-only operations do not require it.`,
|
|
714
|
+
inputSchema: {
|
|
715
|
+
type: 'object',
|
|
716
|
+
properties: {
|
|
717
|
+
base_url: { type: 'string', description: 'Nebula server URL (e.g., http://localhost:3000).' },
|
|
718
|
+
},
|
|
719
|
+
required: ['base_url'],
|
|
720
|
+
},
|
|
721
|
+
annotations: { destructiveHint: false },
|
|
722
|
+
},
|
|
723
|
+
async execute(params, client) {
|
|
724
|
+
// This is handled specially by the MCP server
|
|
725
|
+
// The client parameter here is not used - the MCP server creates a new client
|
|
726
|
+
return { success: true, data: { url: params.base_url } };
|
|
727
|
+
},
|
|
728
|
+
formatForMCP(result) {
|
|
729
|
+
if (!result.success) {
|
|
730
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
731
|
+
}
|
|
732
|
+
return [{ type: 'text', text: `Connected to ${result.data.url}` }];
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
export const startAgentSessionTool = {
|
|
736
|
+
definition: {
|
|
737
|
+
name: 'start_agent_session',
|
|
738
|
+
description: `Start an agent session, locking the notebook for agent use. The UI will show a locked indicator and prevent user edits. Always call end_agent_session when done.
|
|
739
|
+
|
|
740
|
+
See connect_server for required MCP workflow.
|
|
741
|
+
|
|
742
|
+
This call validates that the notebook path exists; it will error if the path is wrong or missing.
|
|
743
|
+
|
|
744
|
+
IMPORTANT: Call this at the START of each response before any notebook operations, and call end_agent_session at the END of each response. This ensures the notebook is only locked while you're actively working on it.
|
|
745
|
+
|
|
746
|
+
Example flow for each response:
|
|
747
|
+
start_agent_session → insert_cell/update_cell/execute_cell/etc → end_agent_session
|
|
748
|
+
|
|
749
|
+
FORCE OPTION (use only with explicit user permission):
|
|
750
|
+
If a previous agent session was not properly ended (e.g., due to a crash or timeout), the notebook may remain locked. Use force=true ONLY when the user explicitly asks you to "force" or "steal" the lock. This will forcibly end any existing session and start a new one.`,
|
|
751
|
+
inputSchema: {
|
|
752
|
+
type: 'object',
|
|
753
|
+
properties: {
|
|
754
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
755
|
+
agent_id: { type: 'string', description: 'Optional identifier for this agent session' },
|
|
756
|
+
force: {
|
|
757
|
+
type: 'boolean',
|
|
758
|
+
description: 'DANGEROUS: Force steal the lock even if another session is active. Use ONLY with explicit user permission to avoid disrupting another agent. Default: false',
|
|
759
|
+
},
|
|
760
|
+
last_session_timestamp: {
|
|
761
|
+
type: 'number',
|
|
762
|
+
description: 'Optional timestamp (ms since epoch) to fetch updates since last agent session',
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
required: ['path'],
|
|
766
|
+
},
|
|
767
|
+
annotations: { destructiveHint: true },
|
|
768
|
+
},
|
|
769
|
+
async execute(params, client) {
|
|
770
|
+
const result = await client.startAgentSession(params.path, params.agent_id, params.force, params.last_session_timestamp);
|
|
771
|
+
if (!result.success) {
|
|
772
|
+
return { success: false, error: result.error };
|
|
773
|
+
}
|
|
774
|
+
// Print warning if previous session wasn't ended
|
|
775
|
+
if (result.data?.warning) {
|
|
776
|
+
console.warn(`[start_agent_session] ${result.data.warning}`);
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
success: true,
|
|
780
|
+
data: {
|
|
781
|
+
warning: result.data?.warning,
|
|
782
|
+
updatesSince: result.data?.updatesSince,
|
|
783
|
+
},
|
|
784
|
+
};
|
|
785
|
+
},
|
|
786
|
+
formatForMCP(result) {
|
|
787
|
+
if (!result.success) {
|
|
788
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
789
|
+
}
|
|
790
|
+
let msg = 'Agent session started - notebook locked';
|
|
791
|
+
if (result.data?.warning) {
|
|
792
|
+
msg += `\n⚠️ ${result.data.warning}`;
|
|
793
|
+
}
|
|
794
|
+
if (result.data?.updatesSince && result.data.updatesSince.length > 0) {
|
|
795
|
+
const updatesSummary = result.data.updatesSince.map(u => ` • ${u.description} (${new Date(u.timestamp).toLocaleTimeString()})`).join('\n');
|
|
796
|
+
msg += `\n📝 Updates since last session:\n${updatesSummary}`;
|
|
797
|
+
}
|
|
798
|
+
return [{ type: 'text', text: msg }];
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
export const endAgentSessionTool = {
|
|
802
|
+
definition: {
|
|
803
|
+
name: 'end_agent_session',
|
|
804
|
+
description: `End an agent session, unlocking the notebook for user edits. Always call this at the END of each response after completing all notebook operations.
|
|
805
|
+
|
|
806
|
+
IMPORTANT: Always call this before finishing your response, even if operations failed. This ensures the notebook is unlocked for the user.`,
|
|
807
|
+
inputSchema: {
|
|
808
|
+
type: 'object',
|
|
809
|
+
properties: {
|
|
810
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
811
|
+
},
|
|
812
|
+
required: ['path'],
|
|
813
|
+
},
|
|
814
|
+
annotations: { destructiveHint: false },
|
|
815
|
+
},
|
|
816
|
+
async execute(params, client) {
|
|
817
|
+
const result = await client.endAgentSession(params.path);
|
|
818
|
+
if (!result.success) {
|
|
819
|
+
return { success: false, error: result.error };
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
success: true,
|
|
823
|
+
data: {
|
|
824
|
+
sessionDuration: result.data?.sessionDuration,
|
|
825
|
+
warning: result.data?.warning,
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
},
|
|
829
|
+
formatForMCP(result) {
|
|
830
|
+
if (!result.success) {
|
|
831
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
832
|
+
}
|
|
833
|
+
const duration = result.data?.sessionDuration
|
|
834
|
+
? ` (duration: ${Math.round(result.data.sessionDuration / 1000)}s)`
|
|
835
|
+
: '';
|
|
836
|
+
let msg = `Agent session ended${duration} - notebook unlocked`;
|
|
837
|
+
if (result.data?.warning) {
|
|
838
|
+
msg += `\n⚠️ ${result.data.warning}`;
|
|
839
|
+
}
|
|
840
|
+
return [{ type: 'text', text: msg }];
|
|
841
|
+
},
|
|
842
|
+
};
|
|
843
|
+
export const deleteCellsTool = {
|
|
844
|
+
definition: {
|
|
845
|
+
name: 'delete_cells',
|
|
846
|
+
description: 'Delete multiple cells by ID in a single operation. More efficient than multiple single deletes.',
|
|
847
|
+
inputSchema: {
|
|
848
|
+
type: 'object',
|
|
849
|
+
properties: {
|
|
850
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
851
|
+
cell_ids: {
|
|
852
|
+
type: 'array',
|
|
853
|
+
items: { type: 'string' },
|
|
854
|
+
description: 'Array of cell IDs to delete',
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
required: ['path', 'cell_ids'],
|
|
858
|
+
},
|
|
859
|
+
annotations: { destructiveHint: true },
|
|
860
|
+
},
|
|
861
|
+
async execute(params, client) {
|
|
862
|
+
if (!params.cell_ids || params.cell_ids.length === 0) {
|
|
863
|
+
return { success: false, error: 'Must provide at least one cell ID' };
|
|
864
|
+
}
|
|
865
|
+
// Implement as a series of delete_cell calls (shorthand at MCP level, no server-side batch needed)
|
|
866
|
+
const deletedIds = [];
|
|
867
|
+
const notFound = [];
|
|
868
|
+
for (const cellId of params.cell_ids) {
|
|
869
|
+
const result = await client.deleteCellOp(params.path, { cellId });
|
|
870
|
+
if (result.success) {
|
|
871
|
+
deletedIds.push(cellId);
|
|
872
|
+
}
|
|
873
|
+
else if (result.error?.includes('not found')) {
|
|
874
|
+
notFound.push(cellId);
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
// Stop on first real error
|
|
878
|
+
return {
|
|
879
|
+
success: false,
|
|
880
|
+
error: `Failed to delete cell ${cellId}: ${result.error}`,
|
|
881
|
+
data: { deletedCount: deletedIds.length, deletedIds, notFound, totalCells: -1 },
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// Get final cell count
|
|
886
|
+
const notebook = await client.readNotebookViaRouter(params.path);
|
|
887
|
+
const totalCells = notebook.success ? notebook.data.cells.length : -1;
|
|
888
|
+
return {
|
|
889
|
+
success: true,
|
|
890
|
+
data: {
|
|
891
|
+
deletedCount: deletedIds.length,
|
|
892
|
+
deletedIds,
|
|
893
|
+
notFound: notFound.length > 0 ? notFound : undefined,
|
|
894
|
+
totalCells,
|
|
895
|
+
},
|
|
896
|
+
};
|
|
897
|
+
},
|
|
898
|
+
formatForMCP(result) {
|
|
899
|
+
if (!result.success) {
|
|
900
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
901
|
+
}
|
|
902
|
+
const { deletedCount, deletedIds, notFound, totalCells } = result.data;
|
|
903
|
+
let msg = `Deleted ${deletedCount} cell${deletedCount !== 1 ? 's' : ''}: ${deletedIds.join(', ')}`;
|
|
904
|
+
if (notFound && notFound.length > 0) {
|
|
905
|
+
msg += `\n⚠️ Not found: ${notFound.join(', ')}`;
|
|
906
|
+
}
|
|
907
|
+
msg += ` (${totalCells} cell${totalCells !== 1 ? 's' : ''} remaining)`;
|
|
908
|
+
return [{ type: 'text', text: msg }];
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
export const insertCellsTool = {
|
|
912
|
+
definition: {
|
|
913
|
+
name: 'insert_cells',
|
|
914
|
+
description: 'Insert multiple cells at a position in a single operation. More efficient than multiple single inserts.',
|
|
915
|
+
inputSchema: {
|
|
916
|
+
type: 'object',
|
|
917
|
+
properties: {
|
|
918
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
919
|
+
cells: {
|
|
920
|
+
type: 'array',
|
|
921
|
+
items: { type: 'object' },
|
|
922
|
+
description: 'Array of cells to insert. Each cell: {id?: string, type?: "code"|"markdown", content: string}',
|
|
923
|
+
},
|
|
924
|
+
position: { type: 'number', description: 'Position to insert at (0-based). Use -1 or omit to append at end.' },
|
|
925
|
+
},
|
|
926
|
+
required: ['path', 'cells'],
|
|
927
|
+
},
|
|
928
|
+
annotations: { destructiveHint: true },
|
|
929
|
+
},
|
|
930
|
+
async execute(params, client) {
|
|
931
|
+
if (!params.cells || params.cells.length === 0) {
|
|
932
|
+
return { success: false, error: 'Must provide at least one cell' };
|
|
933
|
+
}
|
|
934
|
+
const result = await client.insertCellsOp(params.path, params.cells, params.position ?? -1);
|
|
935
|
+
if (!result.success) {
|
|
936
|
+
return { success: false, error: result.error };
|
|
937
|
+
}
|
|
938
|
+
return { success: true, data: result.data };
|
|
939
|
+
},
|
|
940
|
+
formatForMCP(result) {
|
|
941
|
+
if (!result.success) {
|
|
942
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
943
|
+
}
|
|
944
|
+
const { insertedCount, insertedIds, startIndex, totalCells } = result.data;
|
|
945
|
+
return [{ type: 'text', text: `Inserted ${insertedCount} cell${insertedCount !== 1 ? 's' : ''} at #${startIndex + 1}: ${insertedIds.join(', ')} (${totalCells} cell${totalCells !== 1 ? 's' : ''} total)` }];
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
export const clearNotebookTool = {
|
|
949
|
+
definition: {
|
|
950
|
+
name: 'clear_notebook',
|
|
951
|
+
description: 'Clear all cells from a notebook.',
|
|
952
|
+
inputSchema: {
|
|
953
|
+
type: 'object',
|
|
954
|
+
properties: {
|
|
955
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
956
|
+
},
|
|
957
|
+
required: ['path'],
|
|
958
|
+
},
|
|
959
|
+
annotations: { destructiveHint: true },
|
|
960
|
+
},
|
|
961
|
+
async execute(params, client) {
|
|
962
|
+
const result = await client.clearNotebookOp(params.path);
|
|
963
|
+
if (!result.success) {
|
|
964
|
+
return { success: false, error: result.error };
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
success: true,
|
|
968
|
+
data: {
|
|
969
|
+
deletedCount: result.data?.deletedCount ?? 0,
|
|
970
|
+
metadata: result.data?.metadata,
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
},
|
|
974
|
+
formatForMCP(result) {
|
|
975
|
+
if (!result.success) {
|
|
976
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
977
|
+
}
|
|
978
|
+
const { deletedCount, metadata } = result.data;
|
|
979
|
+
let msg = `Cleared notebook: deleted ${deletedCount} cell${deletedCount !== 1 ? 's' : ''}`;
|
|
980
|
+
if (metadata?.operationTime !== undefined) {
|
|
981
|
+
msg += ` (${metadata.operationTime}ms)`;
|
|
982
|
+
}
|
|
983
|
+
return [{ type: 'text', text: msg }];
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
export const clearOutputsTool = {
|
|
987
|
+
definition: {
|
|
988
|
+
name: 'clear_outputs',
|
|
989
|
+
description: 'Clear outputs from cells without re-executing. If no cell_ids provided, clears all cell outputs. Useful for cleanup before sharing notebooks.',
|
|
990
|
+
inputSchema: {
|
|
991
|
+
type: 'object',
|
|
992
|
+
properties: {
|
|
993
|
+
path: { type: 'string', description: 'Path to the notebook file' },
|
|
994
|
+
cell_ids: {
|
|
995
|
+
type: 'array',
|
|
996
|
+
items: { type: 'string' },
|
|
997
|
+
description: 'Optional array of cell IDs to clear. If omitted, clears all cells.',
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
required: ['path'],
|
|
1001
|
+
},
|
|
1002
|
+
annotations: { destructiveHint: true },
|
|
1003
|
+
},
|
|
1004
|
+
async execute(params, client) {
|
|
1005
|
+
// Implement as updateOutputsOp calls with empty outputs (shorthand at MCP level)
|
|
1006
|
+
const notebook = await client.readNotebookViaRouter(params.path);
|
|
1007
|
+
if (!notebook.success) {
|
|
1008
|
+
return { success: false, error: `Failed to read notebook: ${notebook.error}` };
|
|
1009
|
+
}
|
|
1010
|
+
const cells = notebook.data.cells;
|
|
1011
|
+
const clearedIds = [];
|
|
1012
|
+
const notFound = [];
|
|
1013
|
+
// Determine which cells to clear
|
|
1014
|
+
let targetCellIds;
|
|
1015
|
+
if (params.cell_ids && params.cell_ids.length > 0) {
|
|
1016
|
+
targetCellIds = params.cell_ids;
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
// Clear all code cells (markdown cells don't have outputs)
|
|
1020
|
+
targetCellIds = cells.filter(c => c.type === 'code').map(c => c.id);
|
|
1021
|
+
}
|
|
1022
|
+
for (const cellId of targetCellIds) {
|
|
1023
|
+
const cell = cells.find(c => c.id === cellId);
|
|
1024
|
+
if (!cell) {
|
|
1025
|
+
notFound.push(cellId);
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
// Skip if no outputs to clear
|
|
1029
|
+
if (!cell.outputs || cell.outputs.length === 0) {
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
const result = await client.updateOutputsOp(params.path, cellId, []);
|
|
1033
|
+
if (result.success) {
|
|
1034
|
+
clearedIds.push(cellId);
|
|
1035
|
+
}
|
|
1036
|
+
// Silently ignore failures for individual cells
|
|
1037
|
+
}
|
|
1038
|
+
return {
|
|
1039
|
+
success: true,
|
|
1040
|
+
data: {
|
|
1041
|
+
clearedCount: clearedIds.length,
|
|
1042
|
+
clearedIds,
|
|
1043
|
+
notFound: notFound.length > 0 ? notFound : undefined,
|
|
1044
|
+
},
|
|
1045
|
+
};
|
|
1046
|
+
},
|
|
1047
|
+
formatForMCP(result) {
|
|
1048
|
+
if (!result.success) {
|
|
1049
|
+
return [{ type: 'text', text: `Error: ${result.error}` }];
|
|
1050
|
+
}
|
|
1051
|
+
const { clearedCount, clearedIds, notFound } = result.data;
|
|
1052
|
+
if (clearedCount === 0) {
|
|
1053
|
+
return [{ type: 'text', text: 'No outputs to clear' }];
|
|
1054
|
+
}
|
|
1055
|
+
let msg = `Cleared outputs from ${clearedCount} cells`;
|
|
1056
|
+
if (clearedIds.length <= 5) {
|
|
1057
|
+
msg += `: ${clearedIds.join(', ')}`;
|
|
1058
|
+
}
|
|
1059
|
+
if (notFound && notFound.length > 0) {
|
|
1060
|
+
msg += `\n⚠️ Not found: ${notFound.join(', ')}`;
|
|
1061
|
+
}
|
|
1062
|
+
return [{ type: 'text', text: msg }];
|
|
1063
|
+
},
|
|
1064
|
+
};
|
|
1065
|
+
// =============================================================================
|
|
1066
|
+
// Export all notebook tools
|
|
1067
|
+
// =============================================================================
|
|
1068
|
+
export const notebookTools = [
|
|
1069
|
+
readNotebookTool,
|
|
1070
|
+
readCellTool,
|
|
1071
|
+
readOutputTool,
|
|
1072
|
+
insertCellTool,
|
|
1073
|
+
updateCellTool,
|
|
1074
|
+
deleteCellTool,
|
|
1075
|
+
clearNotebookTool,
|
|
1076
|
+
createNotebookTool,
|
|
1077
|
+
moveCellTool,
|
|
1078
|
+
duplicateCellTool,
|
|
1079
|
+
searchCellsTool,
|
|
1080
|
+
updateMetadataTool,
|
|
1081
|
+
connectServerTool,
|
|
1082
|
+
startAgentSessionTool,
|
|
1083
|
+
endAgentSessionTool,
|
|
1084
|
+
// Batch operations (Phase 1 enhancements)
|
|
1085
|
+
deleteCellsTool,
|
|
1086
|
+
insertCellsTool,
|
|
1087
|
+
clearOutputsTool,
|
|
1088
|
+
];
|
|
1089
|
+
//# sourceMappingURL=notebook.js.map
|