roam-research-mcp 0.24.1 → 0.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -30,6 +30,16 @@ npm run build
|
|
|
30
30
|
|
|
31
31
|
The server provides powerful tools for interacting with Roam Research:
|
|
32
32
|
|
|
33
|
+
- Environment variable handling with .env support
|
|
34
|
+
- Comprehensive input validation
|
|
35
|
+
- Case-insensitive page title matching
|
|
36
|
+
- Recursive block reference resolution
|
|
37
|
+
- Markdown parsing and conversion
|
|
38
|
+
- Daily page integration
|
|
39
|
+
- Detailed debug logging
|
|
40
|
+
- Efficient batch operations
|
|
41
|
+
- Hierarchical outline creation
|
|
42
|
+
|
|
33
43
|
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
44
|
2. `roam_create_page`: Create new pages with optional content
|
|
35
45
|
3. `roam_create_block`: Create new blocks in a page (defaults to today's daily page)
|
|
@@ -38,7 +48,7 @@ The server provides powerful tools for interacting with Roam Research:
|
|
|
38
48
|
6. `roam_create_outline`: Create hierarchical outlines with proper nesting and structure
|
|
39
49
|
7. `roam_search_block_refs`: Search for block references within pages or across the graph
|
|
40
50
|
8. `roam_search_hierarchy`: Navigate and search through block parent-child relationships
|
|
41
|
-
9. `
|
|
51
|
+
9. `roam_find_pages_modified_today`: Find all pages that have been modified since midnight today
|
|
42
52
|
10. `roam_search_by_text`: Search for blocks containing specific text across all pages or within a specific page
|
|
43
53
|
11. `roam_update_block`: Update block content with direct text or pattern-based transformations
|
|
44
54
|
12. `roam_search_by_date`: Search for blocks and pages based on creation or modification dates
|
|
@@ -545,7 +555,7 @@ Returns:
|
|
|
545
555
|
Find all pages that have been modified since midnight today:
|
|
546
556
|
|
|
547
557
|
```typescript
|
|
548
|
-
use_mcp_tool roam-research
|
|
558
|
+
use_mcp_tool roam-research roam_find_pages_modified_today {}
|
|
549
559
|
```
|
|
550
560
|
|
|
551
561
|
Features:
|
|
@@ -717,25 +727,39 @@ Each error response includes:
|
|
|
717
727
|
|
|
718
728
|
## Development
|
|
719
729
|
|
|
720
|
-
|
|
730
|
+
### Building
|
|
721
731
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
732
|
+
To build the server:
|
|
733
|
+
|
|
734
|
+
```bash
|
|
735
|
+
npm install
|
|
736
|
+
npm run build
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
This will:
|
|
740
|
+
|
|
741
|
+
1. Install all required dependencies
|
|
742
|
+
2. Compile TypeScript to JavaScript
|
|
743
|
+
3. Make the output file executable
|
|
744
|
+
|
|
745
|
+
You can also use `npm run watch` during development to automatically recompile when files change.
|
|
746
|
+
|
|
747
|
+
### Testing with MCP Inspector
|
|
748
|
+
|
|
749
|
+
The MCP Inspector is a tool that helps test and debug MCP servers. To test the server:
|
|
750
|
+
|
|
751
|
+
```bash
|
|
752
|
+
# Inspect with npx:
|
|
753
|
+
npx @modelcontextprotocol/inspector node build/index.js
|
|
754
|
+
```
|
|
731
755
|
|
|
732
|
-
|
|
756
|
+
This will:
|
|
733
757
|
|
|
734
|
-
1.
|
|
735
|
-
2.
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
758
|
+
1. Start the server in inspector mode
|
|
759
|
+
2. Provide an interactive interface to:
|
|
760
|
+
- List available tools and resources
|
|
761
|
+
- Execute tools with custom parameters
|
|
762
|
+
- View tool responses and error handling
|
|
739
763
|
|
|
740
764
|
## License
|
|
741
765
|
|
|
@@ -17,7 +17,7 @@ export class RoamServer {
|
|
|
17
17
|
this.toolHandlers = new ToolHandlers(this.graph);
|
|
18
18
|
this.server = new Server({
|
|
19
19
|
name: 'roam-research',
|
|
20
|
-
version: '0.
|
|
20
|
+
version: '0.24.2',
|
|
21
21
|
}, {
|
|
22
22
|
capabilities: {
|
|
23
23
|
tools: {
|
|
@@ -33,7 +33,7 @@ export class RoamServer {
|
|
|
33
33
|
roam_search_by_status: {},
|
|
34
34
|
roam_search_block_refs: {},
|
|
35
35
|
roam_search_hierarchy: {},
|
|
36
|
-
|
|
36
|
+
roam_find_pages_modified_today: {},
|
|
37
37
|
roam_search_by_text: {},
|
|
38
38
|
roam_update_block: {},
|
|
39
39
|
roam_update_blocks: {},
|
|
@@ -136,7 +136,7 @@ export class RoamServer {
|
|
|
136
136
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
|
-
case '
|
|
139
|
+
case 'roam_find_pages_modified_today': {
|
|
140
140
|
const result = await this.toolHandlers.findPagesModifiedToday();
|
|
141
141
|
return {
|
|
142
142
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -173,7 +173,8 @@ export class RoamServer {
|
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
case 'roam_recall': {
|
|
176
|
-
const
|
|
176
|
+
const { sort_by = 'newest', filter_tag } = request.params.arguments;
|
|
177
|
+
const result = await this.toolHandlers.recall(sort_by, filter_tag);
|
|
177
178
|
return {
|
|
178
179
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
179
180
|
};
|
|
@@ -66,7 +66,7 @@ export class MemoryOperations {
|
|
|
66
66
|
}
|
|
67
67
|
return { success: true };
|
|
68
68
|
}
|
|
69
|
-
async recall() {
|
|
69
|
+
async recall(sort_by = 'newest', filter_tag) {
|
|
70
70
|
// Get memories tag from environment
|
|
71
71
|
var memoriesTag = process.env.MEMORIES_TAG;
|
|
72
72
|
if (!memoriesTag) {
|
|
@@ -76,27 +76,73 @@ export class MemoryOperations {
|
|
|
76
76
|
const tagText = memoriesTag
|
|
77
77
|
.replace(/^#/, '') // Remove leading #
|
|
78
78
|
.replace(/^\[\[/, '').replace(/\]\]$/, ''); // Remove [[ and ]]
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
79
|
+
try {
|
|
80
|
+
// Get page blocks using query to access actual block content
|
|
81
|
+
const ancestorRule = `[
|
|
82
|
+
[ (ancestor ?b ?a)
|
|
83
|
+
[?a :block/children ?b] ]
|
|
84
|
+
[ (ancestor ?b ?a)
|
|
85
|
+
[?parent :block/children ?b]
|
|
86
|
+
(ancestor ?parent ?a) ]
|
|
87
|
+
]`;
|
|
88
|
+
// Query to find all blocks on the page
|
|
89
|
+
const pageQuery = `[:find ?string ?time
|
|
90
|
+
:in $ % ?title
|
|
91
|
+
:where
|
|
92
|
+
[?page :node/title ?title]
|
|
93
|
+
[?block :block/string ?string]
|
|
94
|
+
[?block :create/time ?time]
|
|
95
|
+
(ancestor ?block ?page)]`;
|
|
96
|
+
// Execute query
|
|
97
|
+
const pageResults = await q(this.graph, pageQuery, [ancestorRule, tagText]);
|
|
98
|
+
// Process page blocks with sorting
|
|
99
|
+
let pageMemories = pageResults
|
|
100
|
+
.sort(([_, aTime], [__, bTime]) => sort_by === 'newest' ? bTime - aTime : aTime - bTime)
|
|
101
|
+
.map(([content]) => content);
|
|
102
|
+
// Get tagged blocks from across the graph
|
|
103
|
+
const tagResults = await this.searchOps.searchForTag(tagText);
|
|
104
|
+
// Process tagged blocks with sorting
|
|
105
|
+
let taggedMemories = tagResults.matches
|
|
106
|
+
.sort((a, b) => {
|
|
107
|
+
const aTime = a.block_uid ? parseInt(a.block_uid.split('-')[0], 16) : 0;
|
|
108
|
+
const bTime = b.block_uid ? parseInt(b.block_uid.split('-')[0], 16) : 0;
|
|
109
|
+
return sort_by === 'newest' ? bTime - aTime : aTime - bTime;
|
|
110
|
+
})
|
|
111
|
+
.map(match => match.content);
|
|
112
|
+
// Resolve any block references in both sets
|
|
113
|
+
const resolvedPageMemories = await Promise.all(pageMemories.map(async (content) => resolveRefs(this.graph, content)));
|
|
114
|
+
const resolvedTaggedMemories = await Promise.all(taggedMemories.map(async (content) => resolveRefs(this.graph, content)));
|
|
115
|
+
// Combine both sets and remove duplicates while preserving order
|
|
116
|
+
let uniqueMemories = [
|
|
117
|
+
...resolvedPageMemories,
|
|
118
|
+
...resolvedTaggedMemories
|
|
119
|
+
].filter((memory, index, self) => self.indexOf(memory) === index);
|
|
120
|
+
// Format filter tag with exact Roam tag syntax
|
|
121
|
+
const filterTagFormatted = filter_tag ?
|
|
122
|
+
(filter_tag.includes(' ') ? `#[[${filter_tag}]]` : `#${filter_tag}`) : null;
|
|
123
|
+
// Filter by exact tag match if provided
|
|
124
|
+
if (filterTagFormatted) {
|
|
125
|
+
uniqueMemories = uniqueMemories.filter(memory => memory.includes(filterTagFormatted));
|
|
126
|
+
}
|
|
127
|
+
// Format memories tag for removal and clean up memories tag
|
|
128
|
+
const memoriesTagFormatted = tagText.includes(' ') || tagText.includes('/') ? `#[[${tagText}]]` : `#${tagText}`;
|
|
129
|
+
uniqueMemories = uniqueMemories.map(memory => memory.replace(memoriesTagFormatted, '').trim());
|
|
130
|
+
// return {
|
|
131
|
+
// success: true,
|
|
132
|
+
// memories: [
|
|
133
|
+
// `memoriesTag = ${memoriesTag}`,
|
|
134
|
+
// `filter_tag = ${filter_tag}`,
|
|
135
|
+
// `filterTagFormatted = ${filterTagFormatted}`,
|
|
136
|
+
// `memoriesTagFormatted = ${memoriesTagFormatted}`,
|
|
137
|
+
// ]
|
|
138
|
+
// }
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
memories: uniqueMemories
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
throw new McpError(ErrorCode.InternalError, `Failed to recall memories: ${error.message}`);
|
|
146
|
+
}
|
|
101
147
|
}
|
|
102
148
|
}
|
|
@@ -8,7 +8,7 @@ export class PageOperations {
|
|
|
8
8
|
constructor(graph) {
|
|
9
9
|
this.graph = graph;
|
|
10
10
|
}
|
|
11
|
-
async findPagesModifiedToday() {
|
|
11
|
+
async findPagesModifiedToday(num_pages = 10) {
|
|
12
12
|
// Define ancestor rule for traversing block hierarchy
|
|
13
13
|
const ancestorRule = `[
|
|
14
14
|
[ (ancestor ?b ?a)
|
|
@@ -28,7 +28,8 @@ export class PageOperations {
|
|
|
28
28
|
[?page :node/title ?title]
|
|
29
29
|
(ancestor ?block ?page)
|
|
30
30
|
[?block :edit/time ?time]
|
|
31
|
-
[(> ?time ?start_of_day)]]
|
|
31
|
+
[(> ?time ?start_of_day)]]
|
|
32
|
+
:limit ${num_pages}`, [startOfDay.getTime(), ancestorRule]);
|
|
32
33
|
if (!results || results.length === 0) {
|
|
33
34
|
return {
|
|
34
35
|
success: true,
|
package/build/tools/schemas.js
CHANGED
|
@@ -243,13 +243,19 @@ export const toolSchemas = {
|
|
|
243
243
|
]
|
|
244
244
|
}
|
|
245
245
|
},
|
|
246
|
-
|
|
247
|
-
name: '
|
|
248
|
-
description: 'Find
|
|
246
|
+
roam_find_pages_modified_today: {
|
|
247
|
+
name: 'roam_find_pages_modified_today',
|
|
248
|
+
description: 'Find pages that have been modified today (since midnight), with limit.',
|
|
249
249
|
inputSchema: {
|
|
250
250
|
type: 'object',
|
|
251
|
-
properties: {
|
|
252
|
-
|
|
251
|
+
properties: {
|
|
252
|
+
num_pages: {
|
|
253
|
+
type: 'integer',
|
|
254
|
+
description: 'Number of result pages to retrieve (default: 10)',
|
|
255
|
+
default: 10
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
required: ['num_pages']
|
|
253
259
|
}
|
|
254
260
|
},
|
|
255
261
|
roam_search_by_text: {
|
|
@@ -399,13 +405,13 @@ export const toolSchemas = {
|
|
|
399
405
|
},
|
|
400
406
|
roam_remember: {
|
|
401
407
|
name: 'roam_remember',
|
|
402
|
-
description: 'Add a memory or piece of information to remember, stored on the daily page with
|
|
408
|
+
description: 'Add a memory or piece of information to remember, stored on the daily page with MEMORIES_TAG tag and optional categories. \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).',
|
|
403
409
|
inputSchema: {
|
|
404
410
|
type: 'object',
|
|
405
411
|
properties: {
|
|
406
412
|
memory: {
|
|
407
413
|
type: 'string',
|
|
408
|
-
description: 'The memory or information to remember'
|
|
414
|
+
description: 'The memory detail or information to remember'
|
|
409
415
|
},
|
|
410
416
|
categories: {
|
|
411
417
|
type: 'array',
|
|
@@ -420,11 +426,21 @@ export const toolSchemas = {
|
|
|
420
426
|
},
|
|
421
427
|
roam_recall: {
|
|
422
428
|
name: 'roam_recall',
|
|
423
|
-
description: 'Retrieve all stored memories
|
|
429
|
+
description: 'Retrieve all stored memories on page titled MEMORIES_TAG, or tagged block content with the same name. Returns a combined, deduplicated list of memories. Optionally filter blcoks with a specified tag and sort by creation date.',
|
|
424
430
|
inputSchema: {
|
|
425
431
|
type: 'object',
|
|
426
|
-
properties: {
|
|
427
|
-
|
|
432
|
+
properties: {
|
|
433
|
+
sort_by: {
|
|
434
|
+
type: 'string',
|
|
435
|
+
description: 'Sort order for memories based on creation date',
|
|
436
|
+
enum: ['newest', 'oldest'],
|
|
437
|
+
default: 'newest'
|
|
438
|
+
},
|
|
439
|
+
filter_tag: {
|
|
440
|
+
type: 'string',
|
|
441
|
+
description: 'Include only memories with a specific filter tag. For single word tags use format "tag", for multi-word tags use format "tag word" (without brackets)'
|
|
442
|
+
}
|
|
443
|
+
}
|
|
428
444
|
}
|
|
429
445
|
},
|
|
430
446
|
roam_datomic_query: {
|
|
@@ -23,8 +23,8 @@ export class ToolHandlers {
|
|
|
23
23
|
this.outlineOps = new OutlineOperations(graph);
|
|
24
24
|
}
|
|
25
25
|
// Page Operations
|
|
26
|
-
async findPagesModifiedToday() {
|
|
27
|
-
return this.pageOps.findPagesModifiedToday();
|
|
26
|
+
async findPagesModifiedToday(num_pages = 10) {
|
|
27
|
+
return this.pageOps.findPagesModifiedToday(num_pages);
|
|
28
28
|
}
|
|
29
29
|
async createPage(title, content) {
|
|
30
30
|
return this.pageOps.createPage(title, content);
|
|
@@ -70,8 +70,8 @@ export class ToolHandlers {
|
|
|
70
70
|
async remember(memory, categories) {
|
|
71
71
|
return this.memoryOps.remember(memory, categories);
|
|
72
72
|
}
|
|
73
|
-
async recall() {
|
|
74
|
-
return this.memoryOps.recall();
|
|
73
|
+
async recall(sort_by = 'newest', filter_tag) {
|
|
74
|
+
return this.memoryOps.recall(sort_by, filter_tag);
|
|
75
75
|
}
|
|
76
76
|
// Todo Operations
|
|
77
77
|
async addTodos(todos) {
|