ex-brain 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp/server.ts CHANGED
@@ -5,6 +5,124 @@ import { BrainDb } from "../db/client";
5
5
  import { BrainRepository } from "../repositories/brain-repo";
6
6
  import { loadSettings } from "../settings";
7
7
 
8
+ // ============================================================================
9
+ // Error Handling Utilities
10
+ // ============================================================================
11
+
12
+ interface ToolError {
13
+ tool: string;
14
+ error: string;
15
+ message: string;
16
+ timestamp: string;
17
+ recoverable: boolean;
18
+ }
19
+
20
+ function formatError(toolName: string, error: unknown): ToolError {
21
+ const err = error as Error;
22
+ const errorType = err?.name ?? "UnknownError";
23
+ const errorMessage = err?.message ?? String(error);
24
+
25
+ // 判断是否可恢复
26
+ const recoverablePatterns = [
27
+ "ECONNREFUSED",
28
+ "timeout",
29
+ "ETIMEDOUT",
30
+ "rate limit",
31
+ "429",
32
+ "503",
33
+ "502",
34
+ "timeout",
35
+ ];
36
+ const isRecoverable = recoverablePatterns.some(p =>
37
+ errorMessage.toLowerCase().includes(p.toLowerCase())
38
+ );
39
+
40
+ return {
41
+ tool: toolName,
42
+ error: errorType,
43
+ message: errorMessage,
44
+ timestamp: new Date().toISOString(),
45
+ recoverable: isRecoverable,
46
+ };
47
+ }
48
+
49
+ function logError(toolName: string, error: unknown, params?: Record<string, unknown>): void {
50
+ const errInfo = formatError(toolName, error);
51
+ console.error(`[MCP Error] Tool: ${toolName}`);
52
+ console.error(` Type: ${errInfo.error}`);
53
+ console.error(` Message: ${errInfo.message}`);
54
+ console.error(` Recoverable: ${errInfo.recoverable}`);
55
+ if (params) {
56
+ console.error(` Params: ${JSON.stringify(params)}`);
57
+ }
58
+ console.error(` Timestamp: ${errInfo.timestamp}`);
59
+ }
60
+
61
+ /**
62
+ * 包装工具 handler,添加错误处理
63
+ * 确保工具错误不会导致 MCP Server 崩溃,返回友好的 JSON 错误信息
64
+ */
65
+ function withErrorHandling<T extends Record<string, unknown>>(
66
+ toolName: string,
67
+ handler: (params: T) => Promise<{ content: Array<{ type: string; text: string }> }>
68
+ ) {
69
+ return async (params: T): Promise<{ content: Array<{ type: string; text: string }> }> => {
70
+ try {
71
+ return await handler(params);
72
+ } catch (error) {
73
+ logError(toolName, error, params);
74
+ const errInfo = formatError(toolName, error);
75
+ return {
76
+ content: [
77
+ {
78
+ type: "text" as const,
79
+ text: JSON.stringify({
80
+ ok: false,
81
+ error: errInfo.error,
82
+ message: errInfo.message,
83
+ recoverable: errInfo.recoverable,
84
+ hint: errInfo.recoverable
85
+ ? "This is a temporary error. Please try again later."
86
+ : "Please check the input parameters or system configuration.",
87
+ tool: toolName,
88
+ }, null, 2),
89
+ },
90
+ ],
91
+ };
92
+ }
93
+ };
94
+ }
95
+
96
+ // 资源错误处理包装
97
+ function withResourceErrorHandling<T extends Record<string, string>>(
98
+ resourceName: string,
99
+ handler: (uri: URL, vars: T) => Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }>
100
+ ) {
101
+ return async (uri: URL, vars: T): Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }> => {
102
+ try {
103
+ return await handler(uri, vars);
104
+ } catch (error) {
105
+ logError(resourceName, error, vars as unknown as Record<string, unknown>);
106
+ const errInfo = formatError(resourceName, error);
107
+ return {
108
+ contents: [
109
+ {
110
+ uri: uri.href,
111
+ mimeType: "application/json",
112
+ text: JSON.stringify({
113
+ ok: false,
114
+ error: errInfo.error,
115
+ message: errInfo.message,
116
+ recoverable: errInfo.recoverable,
117
+ resource: resourceName,
118
+ }, null, 2),
119
+ },
120
+ ],
121
+ };
122
+ }
123
+ };
124
+ }
125
+
8
126
  export const TOOL_MANIFEST = [
9
127
  "brain_search",
10
128
  "brain_query",
@@ -38,6 +156,20 @@ export async function startMcpServer(dbPath: string): Promise<void> {
38
156
  // Search & Query Tools
39
157
  // ---------------------------------------------------------------------------
40
158
 
159
+ // Tool handler functions (wrapped with error handling below)
160
+ const brainSearchHandler = async ({ query, type, limit }: { query: string; type?: string; limit?: number }) => ({
161
+ content: [
162
+ {
163
+ type: "text",
164
+ text: JSON.stringify(
165
+ await repo.search(query, limit ?? 10, type),
166
+ null,
167
+ 2,
168
+ ),
169
+ },
170
+ ],
171
+ });
172
+
41
173
  server.registerTool(
42
174
  "brain_search",
43
175
  {
@@ -48,20 +180,18 @@ export async function startMcpServer(dbPath: string): Promise<void> {
48
180
  limit: z.number().int().positive().max(50).optional(),
49
181
  }),
50
182
  },
51
- async ({ query, type, limit }) => ({
52
- content: [
53
- {
54
- type: "text",
55
- text: JSON.stringify(
56
- await repo.search(query, limit ?? 10, type),
57
- null,
58
- 2,
59
- ),
60
- },
61
- ],
62
- }),
183
+ withErrorHandling("brain_search", brainSearchHandler),
63
184
  );
64
185
 
186
+ const brainQueryHandler = async ({ question, limit }: { question: string; limit?: number }) => ({
187
+ content: [
188
+ {
189
+ type: "text",
190
+ text: JSON.stringify(await repo.query(question, limit ?? 10), null, 2),
191
+ },
192
+ ],
193
+ });
194
+
65
195
  server.registerTool(
66
196
  "brain_query",
67
197
  {
@@ -71,33 +201,39 @@ export async function startMcpServer(dbPath: string): Promise<void> {
71
201
  limit: z.number().int().positive().max(50).optional(),
72
202
  }),
73
203
  },
74
- async ({ question, limit }) => ({
75
- content: [
76
- {
77
- type: "text",
78
- text: JSON.stringify(await repo.query(question, limit ?? 10), null, 2),
79
- },
80
- ],
81
- }),
204
+ withErrorHandling("brain_query", brainQueryHandler),
82
205
  );
83
206
 
84
207
  // ---------------------------------------------------------------------------
85
208
  // Page CRUD Tools
86
209
  // ---------------------------------------------------------------------------
87
210
 
211
+ const brainGetHandler = async ({ slug }: { slug: string }) => ({
212
+ content: [
213
+ { type: "text", text: JSON.stringify(await repo.getPage(slug), null, 2) },
214
+ ],
215
+ });
216
+
88
217
  server.registerTool(
89
218
  "brain_get",
90
219
  {
91
220
  description: "Read a page and return its full content",
92
221
  inputSchema: z.object({ slug: z.string() }),
93
222
  },
94
- async ({ slug }) => ({
95
- content: [
96
- { type: "text", text: JSON.stringify(await repo.getPage(slug), null, 2) },
97
- ],
98
- }),
223
+ withErrorHandling("brain_get", brainGetHandler),
99
224
  );
100
225
 
226
+ const brainPutHandler = async ({ slug, content, type, title }: { slug: string; content: string; type?: string; title?: string }) => {
227
+ const page = await repo.putPage({
228
+ slug,
229
+ type: type ?? "note",
230
+ title: title ?? slug,
231
+ compiledTruth: content,
232
+ timeline: "",
233
+ });
234
+ return { content: [{ type: "text", text: JSON.stringify(page, null, 2) }] };
235
+ };
236
+
101
237
  server.registerTool(
102
238
  "brain_put",
103
239
  {
@@ -109,30 +245,37 @@ export async function startMcpServer(dbPath: string): Promise<void> {
109
245
  title: z.string().optional(),
110
246
  }),
111
247
  },
112
- async ({ slug, content, type, title }) => {
113
- const page = await repo.putPage({
114
- slug,
115
- type: type ?? "note",
116
- title: title ?? slug,
117
- compiledTruth: content,
118
- timeline: "",
119
- });
120
- return { content: [{ type: "text", text: JSON.stringify(page, null, 2) }] };
121
- },
248
+ withErrorHandling("brain_put", brainPutHandler),
122
249
  );
123
250
 
251
+ const brainDeleteHandler = async ({ slug }: { slug: string }) => {
252
+ await repo.deletePage(slug);
253
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, action: "delete", slug }) }] };
254
+ };
255
+
124
256
  server.registerTool(
125
257
  "brain_delete",
126
258
  {
127
259
  description: "Delete a page and all its related data (links, tags, timeline, raw)",
128
260
  inputSchema: z.object({ slug: z.string() }),
129
261
  },
130
- async ({ slug }) => {
131
- await repo.deletePage(slug);
132
- return { content: [{ type: "text", text: JSON.stringify({ ok: true, action: "delete", slug }) }] };
133
- },
262
+ withErrorHandling("brain_delete", brainDeleteHandler),
134
263
  );
135
264
 
265
+ const brainIngestHandler = async ({ content, source_type, source_ref }: { content: string; source_type: string; source_ref: string }) => {
266
+ const safeRef = source_ref.replace(/[^a-zA-Z0-9/_-]+/g, "_").slice(0, 200);
267
+ const slug = `ingest/${safeRef || "untitled"}`;
268
+ const page = await repo.putPage({
269
+ slug,
270
+ type: source_type,
271
+ title: source_ref,
272
+ compiledTruth: content,
273
+ timeline: "",
274
+ frontmatter: { source_type, source_ref },
275
+ });
276
+ return { content: [{ type: "text", text: JSON.stringify(page, null, 2) }] };
277
+ };
278
+
136
279
  server.registerTool(
137
280
  "brain_ingest",
138
281
  {
@@ -143,25 +286,18 @@ export async function startMcpServer(dbPath: string): Promise<void> {
143
286
  source_ref: z.string(),
144
287
  }),
145
288
  },
146
- async ({ content, source_type, source_ref }) => {
147
- const safeRef = source_ref.replace(/[^a-zA-Z0-9/_-]+/g, "_").slice(0, 200);
148
- const slug = `ingest/${safeRef || "untitled"}`;
149
- const page = await repo.putPage({
150
- slug,
151
- type: source_type,
152
- title: source_ref,
153
- compiledTruth: content,
154
- timeline: "",
155
- frontmatter: { source_type, source_ref },
156
- });
157
- return { content: [{ type: "text", text: JSON.stringify(page, null, 2) }] };
158
- },
289
+ withErrorHandling("brain_ingest", brainIngestHandler),
159
290
  );
160
291
 
161
292
  // ---------------------------------------------------------------------------
162
293
  // Link Tools
163
294
  // ---------------------------------------------------------------------------
164
295
 
296
+ const brainLinkHandler = async ({ from, to, context }: { from: string; to: string; context?: string }) => {
297
+ await repo.link(from, to, context ?? "");
298
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
299
+ };
300
+
165
301
  server.registerTool(
166
302
  "brain_link",
167
303
  {
@@ -172,29 +308,37 @@ export async function startMcpServer(dbPath: string): Promise<void> {
172
308
  context: z.string().optional(),
173
309
  }),
174
310
  },
175
- async ({ from, to, context }) => {
176
- await repo.link(from, to, context ?? "");
177
- return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
178
- },
311
+ withErrorHandling("brain_link", brainLinkHandler),
179
312
  );
180
313
 
314
+ const brainBacklinksHandler = async ({ slug }: { slug: string }) => ({
315
+ content: [
316
+ { type: "text", text: JSON.stringify(await repo.backlinks(slug), null, 2) },
317
+ ],
318
+ });
319
+
181
320
  server.registerTool(
182
321
  "brain_backlinks",
183
322
  {
184
323
  description: "List pages that link to this page",
185
324
  inputSchema: z.object({ slug: z.string() }),
186
325
  },
187
- async ({ slug }) => ({
188
- content: [
189
- { type: "text", text: JSON.stringify(await repo.backlinks(slug), null, 2) },
190
- ],
191
- }),
326
+ withErrorHandling("brain_backlinks", brainBacklinksHandler),
192
327
  );
193
328
 
194
329
  // ---------------------------------------------------------------------------
195
330
  // Timeline Tools (Enhanced)
196
331
  // ---------------------------------------------------------------------------
197
332
 
333
+ const brainTimelineHandler = async ({ slug, limit }: { slug: string; limit?: number }) => ({
334
+ content: [
335
+ {
336
+ type: "text",
337
+ text: JSON.stringify(await repo.timeline(slug, limit ?? 50), null, 2),
338
+ },
339
+ ],
340
+ });
341
+
198
342
  server.registerTool(
199
343
  "brain_timeline",
200
344
  {
@@ -204,16 +348,20 @@ export async function startMcpServer(dbPath: string): Promise<void> {
204
348
  limit: z.number().int().positive().max(200).optional(),
205
349
  }),
206
350
  },
207
- async ({ slug, limit }) => ({
208
- content: [
209
- {
210
- type: "text",
211
- text: JSON.stringify(await repo.timeline(slug, limit ?? 50), null, 2),
212
- },
213
- ],
214
- }),
351
+ withErrorHandling("brain_timeline", brainTimelineHandler),
215
352
  );
216
353
 
354
+ const brainTimelineAddHandler = async ({ slug, date, summary, source, detail }: { slug: string; date: string; summary: string; source?: string; detail?: string }) => {
355
+ await repo.timelineAdd({
356
+ pageSlug: slug,
357
+ date,
358
+ summary,
359
+ source: source ?? "manual",
360
+ detail: detail ?? "",
361
+ });
362
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
363
+ };
364
+
217
365
  server.registerTool(
218
366
  "brain_timeline_add",
219
367
  {
@@ -226,18 +374,18 @@ export async function startMcpServer(dbPath: string): Promise<void> {
226
374
  detail: z.string().optional().describe("Optional markdown detail"),
227
375
  }),
228
376
  },
229
- async ({ slug, date, summary, source, detail }) => {
230
- await repo.timelineAdd({
231
- pageSlug: slug,
232
- date,
233
- summary,
234
- source: source ?? "manual",
235
- detail: detail ?? "",
236
- });
237
- return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
238
- },
377
+ withErrorHandling("brain_timeline_add", brainTimelineAddHandler),
239
378
  );
240
379
 
380
+ const brainTimelineListHandler = async ({ limit }: { limit?: number }) => ({
381
+ content: [
382
+ {
383
+ type: "text",
384
+ text: JSON.stringify(await repo.timelineGlobal(limit ?? 100), null, 2),
385
+ },
386
+ ],
387
+ });
388
+
241
389
  server.registerTool(
242
390
  "brain_timeline_list",
243
391
  {
@@ -246,16 +394,14 @@ export async function startMcpServer(dbPath: string): Promise<void> {
246
394
  limit: z.number().int().positive().max(200).optional(),
247
395
  }),
248
396
  },
249
- async ({ limit }) => ({
250
- content: [
251
- {
252
- type: "text",
253
- text: JSON.stringify(await repo.timelineGlobal(limit ?? 100), null, 2),
254
- },
255
- ],
256
- }),
397
+ withErrorHandling("brain_timeline_list", brainTimelineListHandler),
257
398
  );
258
399
 
400
+ const brainTimelineDeleteHandler = async ({ id }: { id: number }) => {
401
+ await repo.timelineDelete(id);
402
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, action: "timeline-delete", id }) }] };
403
+ };
404
+
259
405
  server.registerTool(
260
406
  "brain_timeline_delete",
261
407
  {
@@ -264,12 +410,32 @@ export async function startMcpServer(dbPath: string): Promise<void> {
264
410
  id: z.number().int().positive().describe("Timeline entry ID to delete"),
265
411
  }),
266
412
  },
267
- async ({ id }) => {
268
- await repo.timelineDelete(id);
269
- return { content: [{ type: "text", text: JSON.stringify({ ok: true, action: "timeline-delete", id }) }] };
270
- },
413
+ withErrorHandling("brain_timeline_delete", brainTimelineDeleteHandler),
271
414
  );
272
415
 
416
+ const brainTimelineExtractHandler = async ({ slug, content, source, default_date }: { slug: string; content: string; source?: string; default_date?: string }) => {
417
+ const result = await repo.extractAndAddTimeline(
418
+ slug,
419
+ content,
420
+ source ?? "extracted",
421
+ default_date ?? new Date().toISOString().slice(0, 10),
422
+ settings.llm,
423
+ );
424
+ return {
425
+ content: [
426
+ {
427
+ type: "text",
428
+ text: JSON.stringify({
429
+ ok: true,
430
+ entriesAdded: result.entries.length,
431
+ entries: result.entries,
432
+ confidence: result.confidence,
433
+ }, null, 2),
434
+ },
435
+ ],
436
+ };
437
+ };
438
+
273
439
  server.registerTool(
274
440
  "brain_timeline_extract",
275
441
  {
@@ -281,34 +447,40 @@ export async function startMcpServer(dbPath: string): Promise<void> {
281
447
  default_date: z.string().optional().describe("Default date (YYYY-MM-DD) for entries without explicit dates"),
282
448
  }),
283
449
  },
284
- async ({ slug, content, source, default_date }) => {
285
- const result = await repo.extractAndAddTimeline(
286
- slug,
287
- content,
288
- source ?? "extracted",
289
- default_date ?? new Date().toISOString().slice(0, 10),
290
- settings.llm,
291
- );
292
- return {
293
- content: [
294
- {
295
- type: "text",
296
- text: JSON.stringify({
297
- ok: true,
298
- entriesAdded: result.entries.length,
299
- entries: result.entries,
300
- confidence: result.confidence,
301
- }, null, 2),
302
- },
303
- ],
304
- };
305
- },
450
+ withErrorHandling("brain_timeline_extract", brainTimelineExtractHandler),
306
451
  );
307
452
 
308
453
  // ---------------------------------------------------------------------------
309
454
  // Smart Compilation Tools (Core Brain Function)
310
455
  // ---------------------------------------------------------------------------
311
456
 
457
+ const brainCompileHandler = async ({ slug, new_info, source, date }: { slug: string; new_info: string; source?: string; date?: string }) => {
458
+ const result = await repo.compilePage(
459
+ slug,
460
+ new_info,
461
+ source ?? "user",
462
+ date ?? new Date().toISOString().slice(0, 10),
463
+ settings.llm,
464
+ );
465
+ return {
466
+ content: [
467
+ {
468
+ type: "text",
469
+ text: JSON.stringify({
470
+ ok: true,
471
+ slug,
472
+ changed: result.changed,
473
+ changeType: result.changeType,
474
+ changeSummary: result.changeSummary,
475
+ timelineEntriesAdded: result.timelineEntries.length,
476
+ confidence: result.confidence,
477
+ compiledTruthPreview: result.compiledTruth.slice(0, 500),
478
+ }, null, 2),
479
+ },
480
+ ],
481
+ };
482
+ };
483
+
312
484
  server.registerTool(
313
485
  "brain_compile",
314
486
  {
@@ -320,34 +492,41 @@ export async function startMcpServer(dbPath: string): Promise<void> {
320
492
  date: z.string().optional().describe("Date of information (YYYY-MM-DD)"),
321
493
  }),
322
494
  },
323
- async ({ slug, new_info, source, date }) => {
324
- const result = await repo.compilePage(
325
- slug,
326
- new_info,
327
- source ?? "user",
328
- date ?? new Date().toISOString().slice(0, 10),
329
- settings.llm,
330
- );
331
- return {
332
- content: [
333
- {
334
- type: "text",
335
- text: JSON.stringify({
336
- ok: true,
337
- slug,
338
- changed: result.changed,
339
- changeType: result.changeType,
340
- changeSummary: result.changeSummary,
341
- timelineEntriesAdded: result.timelineEntries.length,
342
- confidence: result.confidence,
343
- compiledTruthPreview: result.compiledTruth.slice(0, 500),
344
- }, null, 2),
345
- },
346
- ],
347
- };
348
- },
495
+ withErrorHandling("brain_compile", brainCompileHandler),
349
496
  );
350
497
 
498
+ const brainSmartIngestHandler = async ({ slug, content, source, type }: { slug: string; content: string; source?: string; type?: string }) => {
499
+ const result = await repo.ingestContent(
500
+ slug,
501
+ content,
502
+ source ?? "ingest",
503
+ type ?? "note",
504
+ settings.llm,
505
+ );
506
+ return {
507
+ content: [
508
+ {
509
+ type: "text",
510
+ text: JSON.stringify({
511
+ ok: true,
512
+ slug: result.page.slug,
513
+ compileResult: {
514
+ changed: result.compileResult.changed,
515
+ changeType: result.compileResult.changeType,
516
+ changeSummary: result.compileResult.changeSummary,
517
+ confidence: result.compileResult.confidence,
518
+ },
519
+ timelineResult: {
520
+ entriesAdded: result.timelineResult.entries.length,
521
+ confidence: result.timelineResult.confidence,
522
+ },
523
+ updatedAt: result.page.updatedAt,
524
+ }, null, 2),
525
+ },
526
+ ],
527
+ };
528
+ };
529
+
351
530
  server.registerTool(
352
531
  "brain_smart_ingest",
353
532
  {
@@ -359,56 +538,37 @@ export async function startMcpServer(dbPath: string): Promise<void> {
359
538
  type: z.string().optional().describe("Page type (person, company, project, note, etc.)"),
360
539
  }),
361
540
  },
362
- async ({ slug, content, source, type }) => {
363
- const result = await repo.ingestContent(
364
- slug,
365
- content,
366
- source ?? "ingest",
367
- type ?? "note",
368
- settings.llm,
369
- );
370
- return {
371
- content: [
372
- {
373
- type: "text",
374
- text: JSON.stringify({
375
- ok: true,
376
- slug: result.page.slug,
377
- compileResult: {
378
- changed: result.compileResult.changed,
379
- changeType: result.compileResult.changeType,
380
- changeSummary: result.compileResult.changeSummary,
381
- confidence: result.compileResult.confidence,
382
- },
383
- timelineResult: {
384
- entriesAdded: result.timelineResult.entries.length,
385
- confidence: result.timelineResult.confidence,
386
- },
387
- updatedAt: result.page.updatedAt,
388
- }, null, 2),
389
- },
390
- ],
391
- };
392
- },
541
+ withErrorHandling("brain_smart_ingest", brainSmartIngestHandler),
393
542
  );
394
543
 
395
544
  // ---------------------------------------------------------------------------
396
545
  // Tag Tools
397
546
  // ---------------------------------------------------------------------------
398
547
 
548
+ const brainTagsHandler = async ({ slug }: { slug: string }) => ({
549
+ content: [
550
+ { type: "text", text: JSON.stringify(await repo.tags(slug), null, 2) },
551
+ ],
552
+ });
553
+
399
554
  server.registerTool(
400
555
  "brain_tags",
401
556
  {
402
557
  description: "List tags on a page",
403
558
  inputSchema: z.object({ slug: z.string() }),
404
559
  },
405
- async ({ slug }) => ({
406
- content: [
407
- { type: "text", text: JSON.stringify(await repo.tags(slug), null, 2) },
408
- ],
409
- }),
560
+ withErrorHandling("brain_tags", brainTagsHandler),
410
561
  );
411
562
 
563
+ const brainTagHandler = async ({ slug, tag, remove }: { slug: string; tag: string; remove?: boolean }) => {
564
+ if (remove) {
565
+ await repo.untag(slug, tag);
566
+ } else {
567
+ await repo.tag(slug, tag);
568
+ }
569
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
570
+ };
571
+
412
572
  server.registerTool(
413
573
  "brain_tag",
414
574
  {
@@ -419,20 +579,26 @@ export async function startMcpServer(dbPath: string): Promise<void> {
419
579
  remove: z.boolean().optional(),
420
580
  }),
421
581
  },
422
- async ({ slug, tag, remove }) => {
423
- if (remove) {
424
- await repo.untag(slug, tag);
425
- } else {
426
- await repo.tag(slug, tag);
427
- }
428
- return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
429
- },
582
+ withErrorHandling("brain_tag", brainTagHandler),
430
583
  );
431
584
 
432
585
  // ---------------------------------------------------------------------------
433
586
  // Query & List Tools
434
587
  // ---------------------------------------------------------------------------
435
588
 
589
+ const brainListHandler = async ({ type, tag, limit }: { type?: string; tag?: string; limit?: number }) => ({
590
+ content: [
591
+ {
592
+ type: "text",
593
+ text: JSON.stringify(
594
+ await repo.listPages({ type, tag, limit: limit ?? 50 }),
595
+ null,
596
+ 2,
597
+ ),
598
+ },
599
+ ],
600
+ });
601
+
436
602
  server.registerTool(
437
603
  "brain_list",
438
604
  {
@@ -443,28 +609,34 @@ export async function startMcpServer(dbPath: string): Promise<void> {
443
609
  limit: z.number().int().positive().optional(),
444
610
  }),
445
611
  },
446
- async ({ type, tag, limit }) => ({
447
- content: [
448
- {
449
- type: "text",
450
- text: JSON.stringify(
451
- await repo.listPages({ type, tag, limit: limit ?? 50 }),
452
- null,
453
- 2,
454
- ),
455
- },
456
- ],
457
- }),
612
+ withErrorHandling("brain_list", brainListHandler),
458
613
  );
459
614
 
615
+ const brainStatsHandler = async () => ({
616
+ content: [{ type: "text", text: JSON.stringify(await repo.stats(), null, 2) }],
617
+ });
618
+
460
619
  server.registerTool(
461
620
  "brain_stats",
462
621
  { description: "Show knowledge base statistics", inputSchema: z.object({}) },
463
- async () => ({
464
- content: [{ type: "text", text: JSON.stringify(await repo.stats(), null, 2) }],
465
- }),
622
+ withErrorHandling("brain_stats", brainStatsHandler),
466
623
  );
467
624
 
625
+ const brainRawHandler = async ({ slug, source, data }: { slug: string; source?: string; data?: unknown }) => {
626
+ if (data !== undefined) {
627
+ await repo.writeRaw(slug, source ?? "manual", data);
628
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
629
+ }
630
+ return {
631
+ content: [
632
+ {
633
+ type: "text",
634
+ text: JSON.stringify(await repo.readRaw(slug, source), null, 2),
635
+ },
636
+ ],
637
+ };
638
+ };
639
+
468
640
  server.registerTool(
469
641
  "brain_raw",
470
642
  {
@@ -475,44 +647,47 @@ export async function startMcpServer(dbPath: string): Promise<void> {
475
647
  data: z.unknown().optional(),
476
648
  }),
477
649
  },
478
- async ({ slug, source, data }) => {
479
- if (data !== undefined) {
480
- await repo.writeRaw(slug, source ?? "manual", data);
481
- return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
482
- }
483
- return {
484
- content: [
485
- {
486
- type: "text",
487
- text: JSON.stringify(await repo.readRaw(slug, source), null, 2),
488
- },
489
- ],
490
- };
491
- },
650
+ withErrorHandling("brain_raw", brainRawHandler),
492
651
  );
493
652
 
494
653
  // ---------------------------------------------------------------------------
495
654
  // Resources
496
655
  // ---------------------------------------------------------------------------
497
656
 
657
+ const brainIndexHandler = async () => {
658
+ const slugs = await repo.allSlugs();
659
+ return {
660
+ contents: [
661
+ {
662
+ uri: "brain://index",
663
+ mimeType: "text/plain",
664
+ text: slugs.join("\n"),
665
+ },
666
+ ],
667
+ };
668
+ };
669
+
498
670
  server.registerResource(
499
671
  "brain-index",
500
672
  "brain://index",
501
673
  { title: "Brain Index", description: "All page slugs grouped in plain list." },
502
- async () => {
503
- const slugs = await repo.allSlugs();
504
- return {
505
- contents: [
506
- {
507
- uri: "brain://index",
508
- mimeType: "text/plain",
509
- text: slugs.join("\n"),
510
- },
511
- ],
512
- };
513
- },
674
+ withResourceErrorHandling("brain-index", brainIndexHandler),
514
675
  );
515
676
 
677
+ const brainPageHandler = async (uri: URL, vars: { slug?: string }) => {
678
+ const slug = String(vars.slug ?? "");
679
+ const page = await repo.getPage(slug);
680
+ return {
681
+ contents: [
682
+ {
683
+ uri: uri.href,
684
+ mimeType: "application/json",
685
+ text: JSON.stringify(page, null, 2),
686
+ },
687
+ ],
688
+ };
689
+ };
690
+
516
691
  const pageTemplate = new ResourceTemplate("brain://pages/{slug}", {
517
692
  list: undefined,
518
693
  });
@@ -520,19 +695,7 @@ export async function startMcpServer(dbPath: string): Promise<void> {
520
695
  "brain-page",
521
696
  pageTemplate,
522
697
  { title: "Brain Page", description: "Single page JSON resource." },
523
- async (uri, vars) => {
524
- const slug = String(vars.slug ?? "");
525
- const page = await repo.getPage(slug);
526
- return {
527
- contents: [
528
- {
529
- uri: uri.href,
530
- mimeType: "application/json",
531
- text: JSON.stringify(page, null, 2),
532
- },
533
- ],
534
- };
535
- },
698
+ withResourceErrorHandling("brain-page", brainPageHandler),
536
699
  );
537
700
 
538
701
  const transport = new StdioServerTransport();