roam-research-mcp 0.18.0 → 0.20.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 +90 -20
- package/build/server/roam-server.js +16 -0
- package/build/tools/schemas.js +95 -6
- package/build/tools/tool-handlers.js +217 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ npm run build
|
|
|
28
28
|
|
|
29
29
|
## Features
|
|
30
30
|
|
|
31
|
-
The server provides
|
|
31
|
+
The server provides fourteen powerful tools for interacting with Roam Research:
|
|
32
32
|
|
|
33
33
|
1. `roam_fetch_page_by_title`: Fetch and read a page's content by title, recursively resolving block references up to 4 levels deep
|
|
34
34
|
2. `roam_create_page`: Create new pages with optional content
|
|
@@ -42,6 +42,8 @@ The server provides twelve powerful tools for interacting with Roam Research:
|
|
|
42
42
|
10. `roam_search_by_text`: Search for blocks containing specific text across all pages or within a specific page
|
|
43
43
|
11. `roam_update_block`: Update block content with direct text or pattern-based transformations
|
|
44
44
|
12. `roam_search_by_date`: Search for blocks and pages based on creation or modification dates
|
|
45
|
+
13. `roam_search_for_tag`: Search for blocks containing specific tags with optional filtering by nearby tags
|
|
46
|
+
14. `roam_remember`: Store and categorize memories or information with automatic tagging
|
|
45
47
|
|
|
46
48
|
## Setup
|
|
47
49
|
|
|
@@ -60,28 +62,14 @@ The server provides twelve powerful tools for interacting with Roam Research:
|
|
|
60
62
|
```
|
|
61
63
|
ROAM_API_TOKEN=your-api-token
|
|
62
64
|
ROAM_GRAPH_NAME=your-graph-name
|
|
65
|
+
MEMORIES_TAG='#[[LLM/Memories]]'
|
|
66
|
+
PROFILE_PAGE='LLM/Profile' (not yet implemented)
|
|
63
67
|
```
|
|
64
68
|
|
|
65
69
|
Option 2: Using MCP settings (Alternative method)
|
|
66
70
|
Add the configuration to your MCP settings file:
|
|
67
71
|
|
|
68
72
|
- For Cline (`~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`):
|
|
69
|
-
|
|
70
|
-
```json
|
|
71
|
-
{
|
|
72
|
-
"mcpServers": {
|
|
73
|
-
"roam-research": {
|
|
74
|
-
"command": "node",
|
|
75
|
-
"args": ["/path/to/roam-research/build/index.js"],
|
|
76
|
-
"env": {
|
|
77
|
-
"ROAM_API_TOKEN": "your-api-token",
|
|
78
|
-
"ROAM_GRAPH_NAME": "your-graph-name"
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
73
|
- For Claude desktop app (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
86
74
|
|
|
87
75
|
```json
|
|
@@ -92,7 +80,9 @@ The server provides twelve powerful tools for interacting with Roam Research:
|
|
|
92
80
|
"args": ["/path/to/roam-research/build/index.js"],
|
|
93
81
|
"env": {
|
|
94
82
|
"ROAM_API_TOKEN": "your-api-token",
|
|
95
|
-
"ROAM_GRAPH_NAME": "your-graph-name"
|
|
83
|
+
"ROAM_GRAPH_NAME": "your-graph-name",
|
|
84
|
+
"MEMORIES_TAG": "#[[LLM/Memories]]",
|
|
85
|
+
"PROFILE_PAGE": "LLM/Profile"
|
|
96
86
|
}
|
|
97
87
|
}
|
|
98
88
|
}
|
|
@@ -335,7 +325,7 @@ Search for blocks containing specific text across all pages or within a specific
|
|
|
335
325
|
use_mcp_tool roam-research roam_search_by_text {
|
|
336
326
|
"text": "search text",
|
|
337
327
|
"page_title_uid": "optional-page-title-or-uid",
|
|
338
|
-
"case_sensitive":
|
|
328
|
+
"case_sensitive": true
|
|
339
329
|
}
|
|
340
330
|
```
|
|
341
331
|
|
|
@@ -351,7 +341,7 @@ Parameters:
|
|
|
351
341
|
|
|
352
342
|
- `text`: The text to search for (required)
|
|
353
343
|
- `page_title_uid`: Title or UID of the page to search in (optional)
|
|
354
|
-
- `case_sensitive`: Whether to perform a case-sensitive search (optional, default:
|
|
344
|
+
- `case_sensitive`: Whether to perform a case-sensitive search (optional, default: true to match Roam's native behavior)
|
|
355
345
|
|
|
356
346
|
Returns:
|
|
357
347
|
|
|
@@ -421,6 +411,86 @@ Returns:
|
|
|
421
411
|
}
|
|
422
412
|
```
|
|
423
413
|
|
|
414
|
+
### Search For Tags
|
|
415
|
+
|
|
416
|
+
Search for blocks containing specific tags with optional filtering by nearby tags:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
use_mcp_tool roam-research roam_search_for_tag {
|
|
420
|
+
"primary_tag": "Project/Tasks",
|
|
421
|
+
"page_title_uid": "optional-page-title-or-uid",
|
|
422
|
+
"near_tag": "optional-secondary-tag",
|
|
423
|
+
"case_sensitive": true
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Features:
|
|
428
|
+
|
|
429
|
+
- Search for blocks containing specific tags
|
|
430
|
+
- Optional filtering by presence of another tag
|
|
431
|
+
- Page-scoped or graph-wide search
|
|
432
|
+
- Case-sensitive or case-insensitive search
|
|
433
|
+
- Returns block content with page context
|
|
434
|
+
- Efficient tag matching using Datalog queries
|
|
435
|
+
|
|
436
|
+
Parameters:
|
|
437
|
+
|
|
438
|
+
- `primary_tag`: The main tag to search for (required)
|
|
439
|
+
- `page_title_uid`: Title or UID of the page to search in (optional)
|
|
440
|
+
- `near_tag`: Another tag to filter results by (optional)
|
|
441
|
+
- `case_sensitive`: Whether to perform case-sensitive search (optional, default: true to match Roam's native behavior)
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
|
|
445
|
+
```json
|
|
446
|
+
{
|
|
447
|
+
"success": true,
|
|
448
|
+
"matches": [
|
|
449
|
+
{
|
|
450
|
+
"block_uid": "matching-block-uid",
|
|
451
|
+
"content": "Block content containing #[[primary_tag]]",
|
|
452
|
+
"page_title": "Page containing block"
|
|
453
|
+
}
|
|
454
|
+
],
|
|
455
|
+
"message": "Found N block(s) referencing \"primary_tag\""
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Remember Information
|
|
460
|
+
|
|
461
|
+
Store memories or important information with automatic tagging and categorization:
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
use_mcp_tool roam-research roam_remember {
|
|
465
|
+
"memory": "Important information to remember",
|
|
466
|
+
"categories": ["Work", "Project/Alpha"]
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Features:
|
|
471
|
+
|
|
472
|
+
- Store information with #[[LLM/Memories]] tag
|
|
473
|
+
- Add optional category tags for organization
|
|
474
|
+
- Automatically adds to today's daily page
|
|
475
|
+
- Supports multiple categories per memory
|
|
476
|
+
- Easy retrieval using roam_search_for_tag
|
|
477
|
+
- Maintains chronological order of memories
|
|
478
|
+
|
|
479
|
+
Parameters:
|
|
480
|
+
|
|
481
|
+
- `memory`: The information to remember (required)
|
|
482
|
+
- `categories`: Optional array of categories to tag the memory with
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
|
|
486
|
+
```json
|
|
487
|
+
{
|
|
488
|
+
"success": true,
|
|
489
|
+
"block_uid": "created-block-uid",
|
|
490
|
+
"content": "Memory content with tags"
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
424
494
|
### Search By Date
|
|
425
495
|
|
|
426
496
|
Search for blocks and pages based on creation or modification dates:
|
|
@@ -22,6 +22,7 @@ export class RoamServer {
|
|
|
22
22
|
}, {
|
|
23
23
|
capabilities: {
|
|
24
24
|
tools: {
|
|
25
|
+
roam_remember: {},
|
|
25
26
|
roam_add_todo: {},
|
|
26
27
|
roam_fetch_page_by_title: {},
|
|
27
28
|
roam_create_page: {},
|
|
@@ -35,6 +36,7 @@ export class RoamServer {
|
|
|
35
36
|
find_pages_modified_today: {},
|
|
36
37
|
roam_search_by_text: {},
|
|
37
38
|
roam_update_block: {},
|
|
39
|
+
roam_update_blocks: {},
|
|
38
40
|
roam_search_by_date: {}
|
|
39
41
|
},
|
|
40
42
|
},
|
|
@@ -56,6 +58,13 @@ export class RoamServer {
|
|
|
56
58
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
57
59
|
try {
|
|
58
60
|
switch (request.params.name) {
|
|
61
|
+
case 'roam_remember': {
|
|
62
|
+
const { memory, categories } = request.params.arguments;
|
|
63
|
+
const result = await this.toolHandlers.remember(memory, categories);
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
59
68
|
case 'roam_fetch_page_by_title': {
|
|
60
69
|
const { title } = request.params.arguments;
|
|
61
70
|
const content = await this.toolHandlers.fetchPageByTitle(title);
|
|
@@ -166,6 +175,13 @@ export class RoamServer {
|
|
|
166
175
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
167
176
|
};
|
|
168
177
|
}
|
|
178
|
+
case 'roam_update_blocks': {
|
|
179
|
+
const { updates } = request.params.arguments;
|
|
180
|
+
const result = await this.toolHandlers.updateBlocks(updates);
|
|
181
|
+
return {
|
|
182
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
169
185
|
default:
|
|
170
186
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
171
187
|
}
|
package/build/tools/schemas.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export const toolSchemas = {
|
|
3
3
|
roam_add_todo: {
|
|
4
4
|
name: 'roam_add_todo',
|
|
5
|
-
description: 'Add a list of todo items as individual blocks to today\'s daily page in Roam. Each item becomes its own actionable block with todo status.',
|
|
5
|
+
description: 'Add a list of todo items as individual blocks to today\'s daily page in Roam. Each item becomes its own actionable block with todo status.\nNOTE on Roam-flavored markdown: For direct linking: use [[link]] syntax. For aliased linking, use [alias]([[link]]) syntax. Do not concatenate words in links/hashtags - correct: #[[multiple words]] #self-esteem (for typically hyphenated words).',
|
|
6
6
|
inputSchema: {
|
|
7
7
|
type: 'object',
|
|
8
8
|
properties: {
|
|
@@ -52,7 +52,7 @@ export const toolSchemas = {
|
|
|
52
52
|
},
|
|
53
53
|
roam_create_block: {
|
|
54
54
|
name: 'roam_create_block',
|
|
55
|
-
description: 'Add a new block to an existing Roam page. If no page specified, adds to today\'s daily note. Best for capturing immediate thoughts, additions to discussions, or content that doesn\'t warrant its own page. Can specify page by title or UID.',
|
|
55
|
+
description: 'Add a new block to an existing Roam page. If no page specified, adds to today\'s daily note. Best for capturing immediate thoughts, additions to discussions, or content that doesn\'t warrant its own page. Can specify page by title or UID.\nNOTE on Roam-flavored markdown: For direct linking: use [[link]] syntax. For aliased linking, use [alias]([[link]]) syntax. Do not concatenate words in links/hashtags - correct: #[[multiple words]] #self-esteem (for typically hyphenated words).',
|
|
56
56
|
inputSchema: {
|
|
57
57
|
type: 'object',
|
|
58
58
|
properties: {
|
|
@@ -74,7 +74,7 @@ export const toolSchemas = {
|
|
|
74
74
|
},
|
|
75
75
|
roam_create_outline: {
|
|
76
76
|
name: 'roam_create_output_with_nested_structure',
|
|
77
|
-
description: 'Create a structured outline or output with nested structure in Roam from an array of items with explicit levels. Can be added on a specific page or under a specific block.',
|
|
77
|
+
description: 'Create a structured outline or output with nested structure in Roam from an array of items with explicit levels. Can be added on a specific page or under a specific block. Ideal for saving a conversation with an LLM response, research, or organizing thoughts.',
|
|
78
78
|
inputSchema: {
|
|
79
79
|
type: 'object',
|
|
80
80
|
properties: {
|
|
@@ -146,7 +146,7 @@ export const toolSchemas = {
|
|
|
146
146
|
},
|
|
147
147
|
roam_search_for_tag: {
|
|
148
148
|
name: 'roam_search_for_tag',
|
|
149
|
-
description: 'Search for blocks containing a specific tag and optionally filter by blocks that also contain another tag nearby.',
|
|
149
|
+
description: 'Search for blocks containing a specific tag and optionally filter by blocks that also contain another tag nearby. Example: Use this to search for memories that are tagged with the MEMORIES_TAG.',
|
|
150
150
|
inputSchema: {
|
|
151
151
|
type: 'object',
|
|
152
152
|
properties: {
|
|
@@ -161,6 +161,11 @@ export const toolSchemas = {
|
|
|
161
161
|
near_tag: {
|
|
162
162
|
type: 'string',
|
|
163
163
|
description: 'Optional: Another tag to filter results by - will only return blocks where both tags appear',
|
|
164
|
+
},
|
|
165
|
+
case_sensitive: {
|
|
166
|
+
type: 'boolean',
|
|
167
|
+
description: 'Optional: Whether to perform case-sensitive matching (default: true, matching Roam\'s native behavior)',
|
|
168
|
+
default: true
|
|
164
169
|
}
|
|
165
170
|
},
|
|
166
171
|
required: ['primary_tag']
|
|
@@ -188,6 +193,11 @@ export const toolSchemas = {
|
|
|
188
193
|
exclude: {
|
|
189
194
|
type: 'string',
|
|
190
195
|
description: 'Optional: Comma-separated list of terms to filter results by exclusion (matches content or page title)'
|
|
196
|
+
},
|
|
197
|
+
case_sensitive: {
|
|
198
|
+
type: 'boolean',
|
|
199
|
+
description: 'Optional: Whether to perform case-sensitive matching (default: true, matching Roam\'s native behavior)',
|
|
200
|
+
default: true
|
|
191
201
|
}
|
|
192
202
|
},
|
|
193
203
|
required: ['status']
|
|
@@ -266,7 +276,8 @@ export const toolSchemas = {
|
|
|
266
276
|
},
|
|
267
277
|
case_sensitive: {
|
|
268
278
|
type: 'boolean',
|
|
269
|
-
description: 'Optional: Whether to perform
|
|
279
|
+
description: 'Optional: Whether to perform case-sensitive search (default: true, matching Roam\'s native behavior)',
|
|
280
|
+
default: true
|
|
270
281
|
}
|
|
271
282
|
},
|
|
272
283
|
required: ['text']
|
|
@@ -274,7 +285,7 @@ export const toolSchemas = {
|
|
|
274
285
|
},
|
|
275
286
|
roam_update_block: {
|
|
276
287
|
name: 'roam_update_block',
|
|
277
|
-
description: 'Update the content of an existing block identified by its UID. Can either provide new content directly or use a transform pattern to modify existing content.',
|
|
288
|
+
description: 'Update the content of an existing block identified by its UID. Can either provide new content directly or use a transform pattern to modify existing content.\nNOTE on Roam-flavored markdown: For direct linking: use [[link]] syntax. For aliased linking, use [alias]([[link]]) syntax. Do not concatenate words in links/hashtags - correct: #[[multiple words]] #self-esteem (for typically hyphenated words).',
|
|
278
289
|
inputSchema: {
|
|
279
290
|
type: 'object',
|
|
280
291
|
properties: {
|
|
@@ -314,6 +325,58 @@ export const toolSchemas = {
|
|
|
314
325
|
]
|
|
315
326
|
}
|
|
316
327
|
},
|
|
328
|
+
roam_update_blocks: {
|
|
329
|
+
name: 'roam_update_blocks',
|
|
330
|
+
description: 'Update multiple blocks in a single batch operation. Each update can provide either new content directly or a transform pattern.\nNOTE on Roam-flavored markdown: For direct linking: use [[link]] syntax. For aliased linking, use [alias]([[link]]) syntax. Do not concatenate words in links/hashtags - correct: #[[multiple words]] #self-esteem (for typically hyphenated words).',
|
|
331
|
+
inputSchema: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
updates: {
|
|
335
|
+
type: 'array',
|
|
336
|
+
description: 'Array of block updates to perform',
|
|
337
|
+
items: {
|
|
338
|
+
type: 'object',
|
|
339
|
+
properties: {
|
|
340
|
+
block_uid: {
|
|
341
|
+
type: 'string',
|
|
342
|
+
description: 'UID of the block to update'
|
|
343
|
+
},
|
|
344
|
+
content: {
|
|
345
|
+
type: 'string',
|
|
346
|
+
description: 'New content for the block. If not provided, transform will be used.'
|
|
347
|
+
},
|
|
348
|
+
transform: {
|
|
349
|
+
type: 'object',
|
|
350
|
+
description: 'Pattern to transform the current content. Used if content is not provided.',
|
|
351
|
+
properties: {
|
|
352
|
+
find: {
|
|
353
|
+
type: 'string',
|
|
354
|
+
description: 'Text or regex pattern to find'
|
|
355
|
+
},
|
|
356
|
+
replace: {
|
|
357
|
+
type: 'string',
|
|
358
|
+
description: 'Text to replace with'
|
|
359
|
+
},
|
|
360
|
+
global: {
|
|
361
|
+
type: 'boolean',
|
|
362
|
+
description: 'Whether to replace all occurrences',
|
|
363
|
+
default: true
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
required: ['find', 'replace']
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
required: ['block_uid'],
|
|
370
|
+
oneOf: [
|
|
371
|
+
{ required: ['content'] },
|
|
372
|
+
{ required: ['transform'] }
|
|
373
|
+
]
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
required: ['updates']
|
|
378
|
+
}
|
|
379
|
+
},
|
|
317
380
|
roam_search_by_date: {
|
|
318
381
|
name: 'roam_search_by_date',
|
|
319
382
|
description: 'Search for blocks or pages based on creation or modification dates',
|
|
@@ -342,9 +405,35 @@ export const toolSchemas = {
|
|
|
342
405
|
type: 'boolean',
|
|
343
406
|
description: 'Whether to include the content of matching blocks/pages',
|
|
344
407
|
default: true,
|
|
408
|
+
},
|
|
409
|
+
case_sensitive: {
|
|
410
|
+
type: 'boolean',
|
|
411
|
+
description: 'Optional: Whether to perform case-sensitive matching (default: true, matching Roam\'s native behavior)',
|
|
412
|
+
default: true
|
|
345
413
|
}
|
|
346
414
|
},
|
|
347
415
|
required: ['start_date', 'type', 'scope']
|
|
348
416
|
}
|
|
417
|
+
},
|
|
418
|
+
roam_remember: {
|
|
419
|
+
name: 'roam_remember',
|
|
420
|
+
description: 'Add a memory or piece of information to remember, stored on the daily page with #[[LLM/Memories]] tag and optional categories. Use roam_search_for_tag with "LLM/Memories" to find stored memories.\nNOTE on Roam-flavored markdown: For direct linking: use [[link]] syntax. For aliased linking, use [alias]([[link]]) syntax. Do not concatenate words in links/hashtags - correct: #[[multiple words]] #self-esteem (for typically hyphenated words).',
|
|
421
|
+
inputSchema: {
|
|
422
|
+
type: 'object',
|
|
423
|
+
properties: {
|
|
424
|
+
memory: {
|
|
425
|
+
type: 'string',
|
|
426
|
+
description: 'The memory or information to remember'
|
|
427
|
+
},
|
|
428
|
+
categories: {
|
|
429
|
+
type: 'array',
|
|
430
|
+
items: {
|
|
431
|
+
type: 'string'
|
|
432
|
+
},
|
|
433
|
+
description: 'Optional categories to tag the memory with (will be converted to Roam tags)'
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
required: ['memory']
|
|
437
|
+
}
|
|
349
438
|
}
|
|
350
439
|
};
|
|
@@ -157,21 +157,23 @@ export class ToolHandlers {
|
|
|
157
157
|
// Get or create the target page
|
|
158
158
|
const targetPageUid = await findOrCreatePage(page_title_uid || formatRoamDate(new Date()));
|
|
159
159
|
// Helper function to find block with improved relationship checks
|
|
160
|
-
const findBlockWithRetry = async (pageUid, blockString, maxRetries = 5, initialDelay = 1000) => {
|
|
160
|
+
const findBlockWithRetry = async (pageUid, blockString, maxRetries = 5, initialDelay = 1000, case_sensitive = false) => {
|
|
161
161
|
// Try multiple query strategies
|
|
162
162
|
const queries = [
|
|
163
163
|
// Strategy 1: Direct page and string match
|
|
164
164
|
`[:find ?b-uid ?order
|
|
165
165
|
:where [?p :block/uid "${pageUid}"]
|
|
166
166
|
[?b :block/page ?p]
|
|
167
|
-
[?b :block/string
|
|
167
|
+
[?b :block/string ?block-str]
|
|
168
|
+
[(${case_sensitive ? '=' : 'clojure.string/equals-ignore-case'} ?block-str "${blockString}")]
|
|
168
169
|
[?b :block/order ?order]
|
|
169
170
|
[?b :block/uid ?b-uid]]`,
|
|
170
171
|
// Strategy 2: Parent-child relationship
|
|
171
172
|
`[:find ?b-uid ?order
|
|
172
173
|
:where [?p :block/uid "${pageUid}"]
|
|
173
174
|
[?b :block/parents ?p]
|
|
174
|
-
[?b :block/string
|
|
175
|
+
[?b :block/string ?block-str]
|
|
176
|
+
[(${case_sensitive ? '=' : 'clojure.string/equals-ignore-case'} ?block-str "${blockString}")]
|
|
175
177
|
[?b :block/order ?order]
|
|
176
178
|
[?b :block/uid ?b-uid]]`,
|
|
177
179
|
// Strategy 3: Broader page relationship
|
|
@@ -179,7 +181,8 @@ export class ToolHandlers {
|
|
|
179
181
|
:where [?p :block/uid "${pageUid}"]
|
|
180
182
|
[?b :block/page ?page]
|
|
181
183
|
[?p :block/page ?page]
|
|
182
|
-
[?b :block/string
|
|
184
|
+
[?b :block/string ?block-str]
|
|
185
|
+
[(${case_sensitive ? '=' : 'clojure.string/equals-ignore-case'} ?block-str "${blockString}")]
|
|
183
186
|
[?b :block/order ?order]
|
|
184
187
|
[?b :block/uid ?b-uid]]`
|
|
185
188
|
];
|
|
@@ -201,7 +204,7 @@ export class ToolHandlers {
|
|
|
201
204
|
throw new McpError(ErrorCode.InternalError, `Failed to find block "${blockString}" under page "${pageUid}" after trying multiple strategies`);
|
|
202
205
|
};
|
|
203
206
|
// Helper function to create and verify block with improved error handling
|
|
204
|
-
const createAndVerifyBlock = async (content, parentUid, maxRetries = 5, initialDelay = 1000, isRetry = false) => {
|
|
207
|
+
const createAndVerifyBlock = async (content, parentUid, maxRetries = 5, initialDelay = 1000, isRetry = false, case_sensitive = false) => {
|
|
205
208
|
try {
|
|
206
209
|
// Initial delay before any operations
|
|
207
210
|
if (!isRetry) {
|
|
@@ -223,7 +226,7 @@ export class ToolHandlers {
|
|
|
223
226
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
224
227
|
try {
|
|
225
228
|
// Try to find the block using our improved findBlockWithRetry
|
|
226
|
-
return await findBlockWithRetry(parentUid, content);
|
|
229
|
+
return await findBlockWithRetry(parentUid, content, maxRetries, initialDelay, case_sensitive);
|
|
227
230
|
}
|
|
228
231
|
catch (error) {
|
|
229
232
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -241,7 +244,7 @@ export class ToolHandlers {
|
|
|
241
244
|
// Otherwise, try one more time with a clean slate
|
|
242
245
|
console.log(`Retrying block creation for "${content}" with fresh attempt`);
|
|
243
246
|
await new Promise(resolve => setTimeout(resolve, initialDelay * 2));
|
|
244
|
-
return createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true);
|
|
247
|
+
return createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true, case_sensitive);
|
|
245
248
|
}
|
|
246
249
|
};
|
|
247
250
|
// Get or create the parent block
|
|
@@ -728,7 +731,110 @@ export class ToolHandlers {
|
|
|
728
731
|
throw new McpError(ErrorCode.InternalError, `Failed to update block: ${error.message}`);
|
|
729
732
|
}
|
|
730
733
|
}
|
|
731
|
-
async
|
|
734
|
+
async updateBlocks(updates) {
|
|
735
|
+
if (!Array.isArray(updates) || updates.length === 0) {
|
|
736
|
+
throw new McpError(ErrorCode.InvalidRequest, 'updates must be a non-empty array');
|
|
737
|
+
}
|
|
738
|
+
// Validate each update has required fields
|
|
739
|
+
updates.forEach((update, index) => {
|
|
740
|
+
if (!update.block_uid) {
|
|
741
|
+
throw new McpError(ErrorCode.InvalidRequest, `Update at index ${index} missing block_uid`);
|
|
742
|
+
}
|
|
743
|
+
if (!update.content && !update.transform) {
|
|
744
|
+
throw new McpError(ErrorCode.InvalidRequest, `Update at index ${index} must have either content or transform`);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
// Get current content for all blocks
|
|
748
|
+
const blockUids = updates.map(u => u.block_uid);
|
|
749
|
+
const blockQuery = `[:find ?uid ?string
|
|
750
|
+
:in $ [?uid ...]
|
|
751
|
+
:where [?b :block/uid ?uid]
|
|
752
|
+
[?b :block/string ?string]]`;
|
|
753
|
+
const blockResults = await q(this.graph, blockQuery, [blockUids]);
|
|
754
|
+
// Create map of uid -> current content
|
|
755
|
+
const contentMap = new Map();
|
|
756
|
+
blockResults.forEach(([uid, string]) => {
|
|
757
|
+
contentMap.set(uid, string);
|
|
758
|
+
});
|
|
759
|
+
// Prepare batch actions
|
|
760
|
+
const actions = [];
|
|
761
|
+
const results = [];
|
|
762
|
+
for (const update of updates) {
|
|
763
|
+
try {
|
|
764
|
+
const currentContent = contentMap.get(update.block_uid);
|
|
765
|
+
if (!currentContent) {
|
|
766
|
+
results.push({
|
|
767
|
+
block_uid: update.block_uid,
|
|
768
|
+
content: '',
|
|
769
|
+
success: false,
|
|
770
|
+
error: `Block with UID "${update.block_uid}" not found`
|
|
771
|
+
});
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
// Determine new content
|
|
775
|
+
let newContent;
|
|
776
|
+
if (update.content) {
|
|
777
|
+
newContent = update.content;
|
|
778
|
+
}
|
|
779
|
+
else if (update.transform) {
|
|
780
|
+
const regex = new RegExp(update.transform.find, update.transform.global ? 'g' : '');
|
|
781
|
+
newContent = currentContent.replace(regex, update.transform.replace);
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
// This shouldn't happen due to earlier validation
|
|
785
|
+
throw new Error('Invalid update configuration');
|
|
786
|
+
}
|
|
787
|
+
// Add to batch actions
|
|
788
|
+
actions.push({
|
|
789
|
+
action: 'update-block',
|
|
790
|
+
block: {
|
|
791
|
+
uid: update.block_uid,
|
|
792
|
+
string: newContent
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
results.push({
|
|
796
|
+
block_uid: update.block_uid,
|
|
797
|
+
content: newContent,
|
|
798
|
+
success: true
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
results.push({
|
|
803
|
+
block_uid: update.block_uid,
|
|
804
|
+
content: contentMap.get(update.block_uid) || '',
|
|
805
|
+
success: false,
|
|
806
|
+
error: error.message
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
// Execute batch update if we have any valid actions
|
|
811
|
+
if (actions.length > 0) {
|
|
812
|
+
try {
|
|
813
|
+
const batchResult = await batchActions(this.graph, {
|
|
814
|
+
action: 'batch-actions',
|
|
815
|
+
actions
|
|
816
|
+
});
|
|
817
|
+
if (!batchResult) {
|
|
818
|
+
throw new Error('Batch update failed');
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
catch (error) {
|
|
822
|
+
// Mark all previously successful results as failed
|
|
823
|
+
results.forEach(result => {
|
|
824
|
+
if (result.success) {
|
|
825
|
+
result.success = false;
|
|
826
|
+
result.error = `Batch update failed: ${error.message}`;
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return {
|
|
832
|
+
success: results.every(r => r.success),
|
|
833
|
+
results
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
async searchByStatus(status, page_title_uid, include, exclude, case_sensitive = true // Changed to true to match Roam's behavior
|
|
837
|
+
) {
|
|
732
838
|
// Get target page UID if provided
|
|
733
839
|
let targetPageUid;
|
|
734
840
|
if (page_title_uid) {
|
|
@@ -789,16 +895,26 @@ export class ToolHandlers {
|
|
|
789
895
|
...(pageTitle && { page_title: pageTitle })
|
|
790
896
|
};
|
|
791
897
|
});
|
|
792
|
-
// Post-query filtering
|
|
898
|
+
// Post-query filtering with case sensitivity option
|
|
793
899
|
if (include) {
|
|
794
|
-
const includeTerms = include.
|
|
795
|
-
matches = matches.filter(match =>
|
|
796
|
-
|
|
900
|
+
const includeTerms = include.split(',').map(term => term.trim());
|
|
901
|
+
matches = matches.filter(match => {
|
|
902
|
+
const matchContent = case_sensitive ? match.content : match.content.toLowerCase();
|
|
903
|
+
const matchTitle = match.page_title && (case_sensitive ? match.page_title : match.page_title.toLowerCase());
|
|
904
|
+
const terms = case_sensitive ? includeTerms : includeTerms.map(t => t.toLowerCase());
|
|
905
|
+
return terms.some(term => matchContent.includes(case_sensitive ? term : term.toLowerCase()) ||
|
|
906
|
+
(matchTitle && matchTitle.includes(case_sensitive ? term : term.toLowerCase())));
|
|
907
|
+
});
|
|
797
908
|
}
|
|
798
909
|
if (exclude) {
|
|
799
|
-
const excludeTerms = exclude.
|
|
800
|
-
matches = matches.filter(match =>
|
|
801
|
-
|
|
910
|
+
const excludeTerms = exclude.split(',').map(term => term.trim());
|
|
911
|
+
matches = matches.filter(match => {
|
|
912
|
+
const matchContent = case_sensitive ? match.content : match.content.toLowerCase();
|
|
913
|
+
const matchTitle = match.page_title && (case_sensitive ? match.page_title : match.page_title.toLowerCase());
|
|
914
|
+
const terms = case_sensitive ? excludeTerms : excludeTerms.map(t => t.toLowerCase());
|
|
915
|
+
return !terms.some(term => matchContent.includes(case_sensitive ? term : term.toLowerCase()) ||
|
|
916
|
+
(matchTitle && matchTitle.includes(case_sensitive ? term : term.toLowerCase())));
|
|
917
|
+
});
|
|
802
918
|
}
|
|
803
919
|
return {
|
|
804
920
|
success: true,
|
|
@@ -806,7 +922,8 @@ export class ToolHandlers {
|
|
|
806
922
|
message: `Found ${matches.length} block(s) with status ${status}${include ? ` including "${include}"` : ''}${exclude ? ` excluding "${exclude}"` : ''}`
|
|
807
923
|
};
|
|
808
924
|
}
|
|
809
|
-
async searchForTag(primary_tag, page_title_uid, near_tag
|
|
925
|
+
async searchForTag(primary_tag, page_title_uid, near_tag, case_sensitive = true // Changed to true to match Roam's behavior
|
|
926
|
+
) {
|
|
810
927
|
// Ensure tags are properly formatted with #
|
|
811
928
|
const formatTag = (tag) => tag.startsWith('#') ? tag : `#${tag}`;
|
|
812
929
|
const primaryTagFormatted = formatTag(primary_tag);
|
|
@@ -841,8 +958,12 @@ export class ToolHandlers {
|
|
|
841
958
|
[?b :block/page ?p]
|
|
842
959
|
[?b :block/string ?block-str]
|
|
843
960
|
[?b :block/uid ?block-uid]
|
|
844
|
-
[(clojure.string/includes?
|
|
845
|
-
|
|
961
|
+
[(clojure.string/includes?
|
|
962
|
+
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
963
|
+
${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]
|
|
964
|
+
[(clojure.string/includes?
|
|
965
|
+
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
966
|
+
${case_sensitive ? '?near-tag' : '(clojure.string/lower-case ?near-tag)'})]`;
|
|
846
967
|
queryParams = [primaryTagFormatted, nearTagFormatted, targetPageUid];
|
|
847
968
|
}
|
|
848
969
|
else {
|
|
@@ -852,7 +973,9 @@ export class ToolHandlers {
|
|
|
852
973
|
[?b :block/page ?p]
|
|
853
974
|
[?b :block/string ?block-str]
|
|
854
975
|
[?b :block/uid ?block-uid]
|
|
855
|
-
[(clojure.string/includes?
|
|
976
|
+
[(clojure.string/includes?
|
|
977
|
+
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
978
|
+
${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]`;
|
|
856
979
|
queryParams = [primaryTagFormatted, targetPageUid];
|
|
857
980
|
}
|
|
858
981
|
}
|
|
@@ -865,8 +988,12 @@ export class ToolHandlers {
|
|
|
865
988
|
[?b :block/uid ?block-uid]
|
|
866
989
|
[?b :block/page ?p]
|
|
867
990
|
[?p :node/title ?page-title]
|
|
868
|
-
[(clojure.string/includes?
|
|
869
|
-
|
|
991
|
+
[(clojure.string/includes?
|
|
992
|
+
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
993
|
+
${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]
|
|
994
|
+
[(clojure.string/includes?
|
|
995
|
+
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
996
|
+
${case_sensitive ? '?near-tag' : '(clojure.string/lower-case ?near-tag)'})]`;
|
|
870
997
|
queryParams = [primaryTagFormatted, nearTagFormatted];
|
|
871
998
|
}
|
|
872
999
|
else {
|
|
@@ -876,7 +1003,9 @@ export class ToolHandlers {
|
|
|
876
1003
|
[?b :block/uid ?block-uid]
|
|
877
1004
|
[?b :block/page ?p]
|
|
878
1005
|
[?p :node/title ?page-title]
|
|
879
|
-
[(clojure.string/includes?
|
|
1006
|
+
[(clojure.string/includes?
|
|
1007
|
+
${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
|
|
1008
|
+
${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]`;
|
|
880
1009
|
queryParams = [primaryTagFormatted];
|
|
881
1010
|
}
|
|
882
1011
|
}
|
|
@@ -945,6 +1074,20 @@ export class ToolHandlers {
|
|
|
945
1074
|
...(params.include_content && { content }),
|
|
946
1075
|
page_title: pageTitle
|
|
947
1076
|
}));
|
|
1077
|
+
// Apply case sensitivity if content is included
|
|
1078
|
+
if (params.include_content) {
|
|
1079
|
+
const case_sensitive = params.case_sensitive ?? true; // Default to true to match Roam's behavior
|
|
1080
|
+
if (!case_sensitive) {
|
|
1081
|
+
matches.forEach(match => {
|
|
1082
|
+
if (match.content) {
|
|
1083
|
+
match.content = match.content.toLowerCase();
|
|
1084
|
+
}
|
|
1085
|
+
if (match.page_title) {
|
|
1086
|
+
match.page_title = match.page_title.toLowerCase();
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
948
1091
|
// Sort by time
|
|
949
1092
|
const sortedMatches = matches.sort((a, b) => b.time - a.time);
|
|
950
1093
|
return {
|
|
@@ -953,6 +1096,58 @@ export class ToolHandlers {
|
|
|
953
1096
|
message: `Found ${sortedMatches.length} matches for the given date range and criteria`
|
|
954
1097
|
};
|
|
955
1098
|
}
|
|
1099
|
+
async remember(memory, categories) {
|
|
1100
|
+
// Get today's date
|
|
1101
|
+
const today = new Date();
|
|
1102
|
+
const dateStr = formatRoamDate(today);
|
|
1103
|
+
// Try to find today's page
|
|
1104
|
+
const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
|
|
1105
|
+
const findResults = await q(this.graph, findQuery, [dateStr]);
|
|
1106
|
+
let pageUid;
|
|
1107
|
+
if (findResults && findResults.length > 0) {
|
|
1108
|
+
pageUid = findResults[0][0];
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
// Create today's page if it doesn't exist
|
|
1112
|
+
const success = await createPage(this.graph, {
|
|
1113
|
+
action: 'create-page',
|
|
1114
|
+
page: { title: dateStr }
|
|
1115
|
+
});
|
|
1116
|
+
if (!success) {
|
|
1117
|
+
throw new McpError(ErrorCode.InternalError, 'Failed to create today\'s page');
|
|
1118
|
+
}
|
|
1119
|
+
// Get the new page's UID
|
|
1120
|
+
const results = await q(this.graph, findQuery, [dateStr]);
|
|
1121
|
+
if (!results || results.length === 0) {
|
|
1122
|
+
throw new McpError(ErrorCode.InternalError, 'Could not find created today\'s page');
|
|
1123
|
+
}
|
|
1124
|
+
pageUid = results[0][0];
|
|
1125
|
+
}
|
|
1126
|
+
// Get memories tag from environment
|
|
1127
|
+
const memoriesTag = process.env.MEMORIES_TAG;
|
|
1128
|
+
if (!memoriesTag) {
|
|
1129
|
+
throw new McpError(ErrorCode.InternalError, 'MEMORIES_TAG environment variable not set');
|
|
1130
|
+
}
|
|
1131
|
+
// Format categories as Roam tags if provided
|
|
1132
|
+
const categoryTags = categories?.map(cat => {
|
|
1133
|
+
// Handle multi-word categories
|
|
1134
|
+
return cat.includes(' ') ? `#[[${cat}]]` : `#${cat}`;
|
|
1135
|
+
}).join(' ') || '';
|
|
1136
|
+
// Create block with memory, memories tag, and optional categories
|
|
1137
|
+
const blockContent = `${memoriesTag} ${memory} ${categoryTags}`.trim();
|
|
1138
|
+
const success = await createBlock(this.graph, {
|
|
1139
|
+
action: 'create-block',
|
|
1140
|
+
location: {
|
|
1141
|
+
"parent-uid": pageUid,
|
|
1142
|
+
"order": "last"
|
|
1143
|
+
},
|
|
1144
|
+
block: { string: blockContent }
|
|
1145
|
+
});
|
|
1146
|
+
if (!success) {
|
|
1147
|
+
throw new McpError(ErrorCode.InternalError, 'Failed to create memory block');
|
|
1148
|
+
}
|
|
1149
|
+
return { success: true };
|
|
1150
|
+
}
|
|
956
1151
|
async addTodos(todos) {
|
|
957
1152
|
if (!Array.isArray(todos) || todos.length === 0) {
|
|
958
1153
|
throw new McpError(ErrorCode.InvalidRequest, 'todos must be a non-empty array');
|