@disco_trooper/apple-notes-mcp 1.0.0 → 1.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 +8 -0
- package/package.json +1 -1
- package/src/notes/crud.test.ts +61 -1
- package/src/notes/crud.ts +76 -0
- package/src/notes/tables.test.ts +70 -0
- package/src/notes/tables.ts +146 -0
- package/src/server.ts +42 -1
package/README.md
CHANGED
|
@@ -19,6 +19,14 @@ MCP server for Apple Notes with semantic search and CRUD operations. Claude sear
|
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
|
+
### npm (recommended)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g @disco_trooper/apple-notes-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### From source
|
|
29
|
+
|
|
22
30
|
```bash
|
|
23
31
|
git clone https://github.com/disco-trooper/apple-notes-mcp.git
|
|
24
32
|
cd apple-notes-mcp
|
package/package.json
CHANGED
package/src/notes/crud.test.ts
CHANGED
|
@@ -22,9 +22,15 @@ vi.mock("../utils/debug.js", () => ({
|
|
|
22
22
|
createDebugLogger: vi.fn(() => vi.fn()),
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
|
+
vi.mock("./tables.js", () => ({
|
|
26
|
+
findTables: vi.fn(),
|
|
27
|
+
updateTableCell: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
25
30
|
import { runJxa } from "run-jxa";
|
|
26
|
-
import { checkReadOnly, createNote, updateNote, deleteNote, moveNote } from "./crud.js";
|
|
31
|
+
import { checkReadOnly, createNote, updateNote, deleteNote, moveNote, editTable } from "./crud.js";
|
|
27
32
|
import { resolveNoteTitle } from "./read.js";
|
|
33
|
+
import { findTables, updateTableCell } from "./tables.js";
|
|
28
34
|
|
|
29
35
|
describe("checkReadOnly", () => {
|
|
30
36
|
const originalEnv = process.env.READONLY_MODE;
|
|
@@ -197,3 +203,57 @@ describe("moveNote", () => {
|
|
|
197
203
|
await expect(moveNote("Note", "Archive")).rejects.toThrow("Suggestions: Work/Note, Personal/Note");
|
|
198
204
|
});
|
|
199
205
|
});
|
|
206
|
+
|
|
207
|
+
describe("editTable", () => {
|
|
208
|
+
beforeEach(() => {
|
|
209
|
+
vi.clearAllMocks();
|
|
210
|
+
delete process.env.READONLY_MODE;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should throw if READONLY_MODE is enabled", async () => {
|
|
214
|
+
process.env.READONLY_MODE = "true";
|
|
215
|
+
await expect(editTable("Test", 0, [])).rejects.toThrow("read-only mode");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should throw if note not found", async () => {
|
|
219
|
+
vi.mocked(resolveNoteTitle).mockResolvedValueOnce({
|
|
220
|
+
success: false,
|
|
221
|
+
error: "Note not found",
|
|
222
|
+
});
|
|
223
|
+
await expect(editTable("Missing", 0, [{ row: 0, column: 0, value: "x" }])).rejects.toThrow("Note not found");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should throw if table index out of bounds", async () => {
|
|
227
|
+
vi.mocked(resolveNoteTitle).mockResolvedValueOnce({
|
|
228
|
+
success: true,
|
|
229
|
+
note: { id: "123", title: "Test", folder: "Work" },
|
|
230
|
+
});
|
|
231
|
+
vi.mocked(runJxa).mockResolvedValueOnce(JSON.stringify({ html: "<div>No tables</div>" }));
|
|
232
|
+
vi.mocked(findTables).mockReturnValueOnce([]);
|
|
233
|
+
|
|
234
|
+
await expect(editTable("Test", 0, [{ row: 0, column: 0, value: "x" }]))
|
|
235
|
+
.rejects.toThrow("Table index 0 out of bounds");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should update table cells and save", async () => {
|
|
239
|
+
vi.mocked(resolveNoteTitle).mockResolvedValueOnce({
|
|
240
|
+
success: true,
|
|
241
|
+
note: { id: "123", title: "Test", folder: "Work" },
|
|
242
|
+
});
|
|
243
|
+
vi.mocked(runJxa)
|
|
244
|
+
.mockResolvedValueOnce(JSON.stringify({ html: "<div><object><table></table></object></div>" }))
|
|
245
|
+
.mockResolvedValueOnce("ok");
|
|
246
|
+
vi.mocked(findTables).mockReturnValueOnce(["<object><table></table></object>"]);
|
|
247
|
+
vi.mocked(updateTableCell).mockReturnValueOnce("<object><table>updated</table></object>");
|
|
248
|
+
|
|
249
|
+
await expect(editTable("Test", 0, [{ row: 1, column: 0, value: "✅ Done" }]))
|
|
250
|
+
.resolves.toBeUndefined();
|
|
251
|
+
|
|
252
|
+
expect(updateTableCell).toHaveBeenCalledWith(
|
|
253
|
+
"<object><table></table></object>",
|
|
254
|
+
1,
|
|
255
|
+
0,
|
|
256
|
+
"✅ Done"
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
});
|
package/src/notes/crud.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { runJxa } from "run-jxa";
|
|
|
9
9
|
import { marked } from "marked";
|
|
10
10
|
import { resolveNoteTitle } from "./read.js";
|
|
11
11
|
import { createDebugLogger } from "../utils/debug.js";
|
|
12
|
+
import { findTables, updateTableCell } from "./tables.js";
|
|
12
13
|
|
|
13
14
|
// Debug logging
|
|
14
15
|
const debug = createDebugLogger("CRUD");
|
|
@@ -255,3 +256,78 @@ export async function moveNote(title: string, folder: string): Promise<void> {
|
|
|
255
256
|
|
|
256
257
|
debug(`Note moved: "${title}" -> "${folder}"`);
|
|
257
258
|
}
|
|
259
|
+
|
|
260
|
+
export interface TableEdit {
|
|
261
|
+
row: number;
|
|
262
|
+
column: number;
|
|
263
|
+
value: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Edit cells in a table within a note.
|
|
268
|
+
*
|
|
269
|
+
* @param title - Note title (supports folder prefix)
|
|
270
|
+
* @param tableIndex - Which table to edit (0-based)
|
|
271
|
+
* @param edits - Array of cell edits to apply
|
|
272
|
+
*/
|
|
273
|
+
export async function editTable(
|
|
274
|
+
title: string,
|
|
275
|
+
tableIndex: number,
|
|
276
|
+
edits: TableEdit[]
|
|
277
|
+
): Promise<void> {
|
|
278
|
+
checkReadOnly();
|
|
279
|
+
|
|
280
|
+
debug(`Editing table ${tableIndex} in note: "${title}"`);
|
|
281
|
+
|
|
282
|
+
// Resolve the note
|
|
283
|
+
const resolved = await resolveNoteTitle(title);
|
|
284
|
+
if (!resolved.success || !resolved.note) {
|
|
285
|
+
if (resolved.suggestions && resolved.suggestions.length > 0) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`${resolved.error} Suggestions: ${resolved.suggestions.join(", ")}`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
throw new Error(resolved.error || `Note not found: "${title}"`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Get current HTML content
|
|
294
|
+
const escapedNoteId = JSON.stringify(resolved.note.id);
|
|
295
|
+
const htmlResult = await runJxa(`
|
|
296
|
+
const app = Application('Notes');
|
|
297
|
+
const note = app.notes.byId(${escapedNoteId});
|
|
298
|
+
if (!note.exists()) {
|
|
299
|
+
throw new Error("Note no longer exists");
|
|
300
|
+
}
|
|
301
|
+
return JSON.stringify({ html: note.body() });
|
|
302
|
+
`);
|
|
303
|
+
|
|
304
|
+
const { html } = JSON.parse(htmlResult as string);
|
|
305
|
+
|
|
306
|
+
// Find all tables
|
|
307
|
+
const tables = findTables(html);
|
|
308
|
+
if (tableIndex >= tables.length) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Table index ${tableIndex} out of bounds (note has ${tables.length} tables)`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Apply edits to the target table
|
|
315
|
+
let updatedTable = tables[tableIndex];
|
|
316
|
+
for (const edit of edits) {
|
|
317
|
+
updatedTable = updateTableCell(updatedTable, edit.row, edit.column, edit.value);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Replace the table in the full HTML
|
|
321
|
+
const updatedHtml = html.replace(tables[tableIndex], updatedTable);
|
|
322
|
+
|
|
323
|
+
// Save back to Apple Notes
|
|
324
|
+
const escapedHtml = JSON.stringify(updatedHtml);
|
|
325
|
+
await runJxa(`
|
|
326
|
+
const app = Application('Notes');
|
|
327
|
+
const note = app.notes.byId(${escapedNoteId});
|
|
328
|
+
note.body = ${escapedHtml};
|
|
329
|
+
return "ok";
|
|
330
|
+
`);
|
|
331
|
+
|
|
332
|
+
debug(`Table ${tableIndex} updated in note: "${title}"`);
|
|
333
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseTable, updateTableCell, findTables } from "./tables.js";
|
|
3
|
+
|
|
4
|
+
const SAMPLE_TABLE_HTML = `<object><table cellspacing="0" cellpadding="0" style="border-collapse: collapse">
|
|
5
|
+
<tbody>
|
|
6
|
+
<tr><td valign="top" style="border-style: solid"><div><b>Typ</b></div></td><td valign="top" style="border-style: solid"><div><b>Částka</b></div></td></tr>
|
|
7
|
+
<tr><td valign="top" style="border-style: solid"><div>Sociální</div></td><td valign="top" style="border-style: solid"><div>9 154 Kč</div></td></tr>
|
|
8
|
+
<tr><td valign="top" style="border-style: solid"><div>Zdravotní</div></td><td valign="top" style="border-style: solid"><div>3 848 Kč</div></td></tr>
|
|
9
|
+
</tbody>
|
|
10
|
+
</table></object>`;
|
|
11
|
+
|
|
12
|
+
describe("parseTable", () => {
|
|
13
|
+
it("should parse table rows and cells", () => {
|
|
14
|
+
const result = parseTable(SAMPLE_TABLE_HTML);
|
|
15
|
+
expect(result.rows).toHaveLength(3);
|
|
16
|
+
expect(result.rows[0]).toEqual(["Typ", "Částka"]);
|
|
17
|
+
expect(result.rows[1]).toEqual(["Sociální", "9 154 Kč"]);
|
|
18
|
+
expect(result.rows[2]).toEqual(["Zdravotní", "3 848 Kč"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should preserve bold formatting info", () => {
|
|
22
|
+
const result = parseTable(SAMPLE_TABLE_HTML);
|
|
23
|
+
expect(result.formatting[0][0].bold).toBe(true);
|
|
24
|
+
expect(result.formatting[1][0].bold).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should return empty for non-table HTML", () => {
|
|
28
|
+
const result = parseTable("<div>Not a table</div>");
|
|
29
|
+
expect(result.rows).toHaveLength(0);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("updateTableCell", () => {
|
|
34
|
+
it("should update cell content", () => {
|
|
35
|
+
const updated = updateTableCell(SAMPLE_TABLE_HTML, 1, 0, "✅ Sociální");
|
|
36
|
+
expect(updated).toContain("✅ Sociální");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should preserve table structure", () => {
|
|
40
|
+
const updated = updateTableCell(SAMPLE_TABLE_HTML, 1, 0, "✅ Sociální");
|
|
41
|
+
expect(updated).toContain("<object>");
|
|
42
|
+
expect(updated).toContain("</table></object>");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should throw for out of bounds row", () => {
|
|
46
|
+
expect(() => updateTableCell(SAMPLE_TABLE_HTML, 10, 0, "test")).toThrow("Row 10 out of bounds");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should throw for out of bounds column", () => {
|
|
50
|
+
expect(() => updateTableCell(SAMPLE_TABLE_HTML, 0, 10, "test")).toThrow("Column 10 out of bounds");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("findTables", () => {
|
|
55
|
+
it("should find single table", () => {
|
|
56
|
+
const tables = findTables(SAMPLE_TABLE_HTML);
|
|
57
|
+
expect(tables).toHaveLength(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should find multiple tables", () => {
|
|
61
|
+
const html = `<div>${SAMPLE_TABLE_HTML}</div><p>text</p>${SAMPLE_TABLE_HTML}`;
|
|
62
|
+
const tables = findTables(html);
|
|
63
|
+
expect(tables).toHaveLength(2);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should return empty for no tables", () => {
|
|
67
|
+
const tables = findTables("<div>No tables here</div>");
|
|
68
|
+
expect(tables).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple Notes table HTML parsing and editing utilities.
|
|
3
|
+
*
|
|
4
|
+
* Apple Notes wraps tables in <object> tags with a specific structure:
|
|
5
|
+
* <object><table><tbody><tr><td><div>content</div></td>...</tr>...</tbody></table></object>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface CellFormatting {
|
|
9
|
+
bold: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TableData {
|
|
13
|
+
rows: string[][];
|
|
14
|
+
formatting: CellFormatting[][];
|
|
15
|
+
raw: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse Apple Notes table HTML into structured data.
|
|
20
|
+
*/
|
|
21
|
+
export function parseTable(html: string): TableData {
|
|
22
|
+
const result: TableData = {
|
|
23
|
+
rows: [],
|
|
24
|
+
formatting: [],
|
|
25
|
+
raw: html,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Match table content inside <object>
|
|
29
|
+
const tableMatch = html.match(/<object[^>]*>[\s\S]*?<table[\s\S]*?<tbody>([\s\S]*?)<\/tbody>[\s\S]*?<\/table>[\s\S]*?<\/object>/i);
|
|
30
|
+
if (!tableMatch) {
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const tbodyContent = tableMatch[1];
|
|
35
|
+
|
|
36
|
+
// Match all rows
|
|
37
|
+
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
|
38
|
+
let rowMatch;
|
|
39
|
+
|
|
40
|
+
while ((rowMatch = rowRegex.exec(tbodyContent)) !== null) {
|
|
41
|
+
const rowContent = rowMatch[1];
|
|
42
|
+
const cells: string[] = [];
|
|
43
|
+
const cellFormats: CellFormatting[] = [];
|
|
44
|
+
|
|
45
|
+
// Match all cells in this row
|
|
46
|
+
const cellRegex = /<td[^>]*>[\s\S]*?<div[^>]*>([\s\S]*?)<\/div>[\s\S]*?<\/td>/gi;
|
|
47
|
+
let cellMatch;
|
|
48
|
+
|
|
49
|
+
while ((cellMatch = cellRegex.exec(rowContent)) !== null) {
|
|
50
|
+
const cellContent = cellMatch[1];
|
|
51
|
+
// Extract text, stripping HTML tags
|
|
52
|
+
const text = cellContent.replace(/<[^>]+>/g, "").trim();
|
|
53
|
+
const isBold = /<b>/i.test(cellContent);
|
|
54
|
+
|
|
55
|
+
cells.push(text);
|
|
56
|
+
cellFormats.push({ bold: isBold });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (cells.length > 0) {
|
|
60
|
+
result.rows.push(cells);
|
|
61
|
+
result.formatting.push(cellFormats);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Update a specific cell in an Apple Notes table HTML.
|
|
70
|
+
*
|
|
71
|
+
* @param html - The table HTML
|
|
72
|
+
* @param row - Row index (0-based, 0 = header)
|
|
73
|
+
* @param column - Column index (0-based)
|
|
74
|
+
* @param value - New cell value
|
|
75
|
+
* @returns Updated HTML
|
|
76
|
+
*/
|
|
77
|
+
export function updateTableCell(html: string, row: number, column: number, value: string): string {
|
|
78
|
+
const parsed = parseTable(html);
|
|
79
|
+
|
|
80
|
+
if (row >= parsed.rows.length) {
|
|
81
|
+
throw new Error(`Row ${row} out of bounds (table has ${parsed.rows.length} rows)`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (column >= parsed.rows[row].length) {
|
|
85
|
+
throw new Error(`Column ${column} out of bounds (row has ${parsed.rows[row].length} columns)`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Find and replace the specific cell
|
|
89
|
+
let currentRow = 0;
|
|
90
|
+
let result = html;
|
|
91
|
+
|
|
92
|
+
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
|
93
|
+
let rowMatch;
|
|
94
|
+
|
|
95
|
+
while ((rowMatch = rowRegex.exec(html)) !== null) {
|
|
96
|
+
if (currentRow === row) {
|
|
97
|
+
const rowContent = rowMatch[1];
|
|
98
|
+
let currentCol = 0;
|
|
99
|
+
let newRowContent = rowContent;
|
|
100
|
+
|
|
101
|
+
const cellRegex = /(<td[^>]*>[\s\S]*?<div[^>]*>)([\s\S]*?)(<\/div>[\s\S]*?<\/td>)/gi;
|
|
102
|
+
let cellMatch;
|
|
103
|
+
const replacements: Array<{original: string; replacement: string}> = [];
|
|
104
|
+
|
|
105
|
+
while ((cellMatch = cellRegex.exec(rowContent)) !== null) {
|
|
106
|
+
if (currentCol === column) {
|
|
107
|
+
const prefix = cellMatch[1];
|
|
108
|
+
const suffix = cellMatch[3];
|
|
109
|
+
const isBold = parsed.formatting[row][column].bold;
|
|
110
|
+
const newContent = isBold ? `<b>${value}</b>` : value;
|
|
111
|
+
replacements.push({
|
|
112
|
+
original: cellMatch[0],
|
|
113
|
+
replacement: `${prefix}${newContent}${suffix}`
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
currentCol++;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const r of replacements) {
|
|
120
|
+
newRowContent = newRowContent.replace(r.original, r.replacement);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
result = result.replace(rowMatch[0], `<tr>${newRowContent}</tr>`);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
currentRow++;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Find all tables in note HTML content.
|
|
134
|
+
* Returns array of table HTML strings.
|
|
135
|
+
*/
|
|
136
|
+
export function findTables(html: string): string[] {
|
|
137
|
+
const tables: string[] = [];
|
|
138
|
+
const tableRegex = /<object[^>]*>[\s\S]*?<table[\s\S]*?<\/table>[\s\S]*?<\/object>/gi;
|
|
139
|
+
let match;
|
|
140
|
+
|
|
141
|
+
while ((match = tableRegex.exec(html)) !== null) {
|
|
142
|
+
tables.push(match[0]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return tables;
|
|
146
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { validateEnv } from "./config/env.js";
|
|
|
18
18
|
// Import implementations
|
|
19
19
|
import { getVectorStore } from "./db/lancedb.js";
|
|
20
20
|
import { getNoteByTitle, getAllFolders } from "./notes/read.js";
|
|
21
|
-
import { createNote, updateNote, deleteNote, moveNote } from "./notes/crud.js";
|
|
21
|
+
import { createNote, updateNote, deleteNote, moveNote, editTable } from "./notes/crud.js";
|
|
22
22
|
import { searchNotes } from "./search/index.js";
|
|
23
23
|
import { indexNotes, reindexNote } from "./search/indexer.js";
|
|
24
24
|
|
|
@@ -72,6 +72,16 @@ const MoveNoteSchema = z.object({
|
|
|
72
72
|
folder: z.string(),
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
const EditTableSchema = z.object({
|
|
76
|
+
title: z.string(),
|
|
77
|
+
table_index: z.number().min(0).default(0),
|
|
78
|
+
edits: z.array(z.object({
|
|
79
|
+
row: z.number().min(0),
|
|
80
|
+
column: z.number().min(0),
|
|
81
|
+
value: z.string(),
|
|
82
|
+
})).min(1),
|
|
83
|
+
});
|
|
84
|
+
|
|
75
85
|
// Helper to create text responses
|
|
76
86
|
function textResponse(text: string) {
|
|
77
87
|
return {
|
|
@@ -245,6 +255,31 @@ export default function createServer() {
|
|
|
245
255
|
required: ["title", "folder"],
|
|
246
256
|
},
|
|
247
257
|
},
|
|
258
|
+
{
|
|
259
|
+
name: "edit-table",
|
|
260
|
+
description: "Edit cells in a table within a note. Use for updating table data without rewriting the entire note.",
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: "object",
|
|
263
|
+
properties: {
|
|
264
|
+
title: { type: "string", description: "Note title (use folder/title for disambiguation)" },
|
|
265
|
+
table_index: { type: "number", description: "Which table to edit (0 = first table, default: 0)" },
|
|
266
|
+
edits: {
|
|
267
|
+
type: "array",
|
|
268
|
+
description: "Array of cell edits",
|
|
269
|
+
items: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
row: { type: "number", description: "Row index (0 = header row)" },
|
|
273
|
+
column: { type: "number", description: "Column index (0 = first column)" },
|
|
274
|
+
value: { type: "string", description: "New cell value" },
|
|
275
|
+
},
|
|
276
|
+
required: ["row", "column", "value"],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
required: ["title", "edits"],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
248
283
|
],
|
|
249
284
|
};
|
|
250
285
|
});
|
|
@@ -367,6 +402,12 @@ export default function createServer() {
|
|
|
367
402
|
return textResponse(`Moved note: "${params.title}" to folder "${params.folder}"`);
|
|
368
403
|
}
|
|
369
404
|
|
|
405
|
+
case "edit-table": {
|
|
406
|
+
const params = EditTableSchema.parse(args);
|
|
407
|
+
await editTable(params.title, params.table_index, params.edits);
|
|
408
|
+
return textResponse(`Updated ${params.edits.length} cell(s) in table ${params.table_index}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
370
411
|
default:
|
|
371
412
|
return errorResponse(`Unknown tool: ${name}`);
|
|
372
413
|
}
|