roam-research-mcp 0.22.1 → 0.23.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/README.md +84 -6
- package/build/search/datomic-search.js +31 -0
- package/build/search/index.js +1 -0
- package/build/server/roam-server.js +9 -1
- package/build/tools/operations/memory.js +2 -2
- package/build/tools/operations/outline.js +28 -37
- package/build/tools/operations/search/handlers.js +13 -1
- package/build/tools/schemas.js +21 -0
- package/build/tools/tool-handlers.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://github.com/2b3pro/roam-research-mcp/blob/main/LICENSE)
|
|
7
7
|
|
|
8
|
-
A Model Context Protocol (MCP) server that provides comprehensive access to Roam Research's API functionality. This server enables AI assistants like Claude to interact with your Roam Research graph through a standardized interface. (A WORK-IN-PROGRESS)
|
|
8
|
+
A Model Context Protocol (MCP) server that provides comprehensive access to Roam Research's API functionality. This server enables AI assistants like Claude to interact with your Roam Research graph through a standardized interface. (A WORK-IN-PROGRESS, personal project not officially endorsed by Roam Research)
|
|
9
9
|
|
|
10
10
|
<a href="https://glama.ai/mcp/servers/fzfznyaflu"><img width="380" height="200" src="https://glama.ai/mcp/servers/fzfznyaflu/badge" alt="Roam Research MCP server" /></a>
|
|
11
11
|
|
|
@@ -28,7 +28,7 @@ npm run build
|
|
|
28
28
|
|
|
29
29
|
## Features
|
|
30
30
|
|
|
31
|
-
The server provides
|
|
31
|
+
The server provides 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
|
|
@@ -44,7 +44,8 @@ The server provides fourteen powerful tools for interacting with Roam Research:
|
|
|
44
44
|
12. `roam_search_by_date`: Search for blocks and pages based on creation or modification dates
|
|
45
45
|
13. `roam_search_for_tag`: Search for blocks containing specific tags with optional filtering by nearby tags
|
|
46
46
|
14. `roam_remember`: Store and categorize memories or information with automatic tagging
|
|
47
|
-
15. `roam_recall`: Recall memories of blocks marked with tag MEMORIES_TAG (see below) or blocks on page title of the same name
|
|
47
|
+
15. `roam_recall`: Recall memories of blocks marked with tag MEMORIES_TAG (see below) or blocks on page title of the same name
|
|
48
|
+
16. `roam_datomic_query`: Execute custom Datalog queries on the Roam graph for advanced data retrieval and analysis
|
|
48
49
|
|
|
49
50
|
## Setup
|
|
50
51
|
|
|
@@ -64,7 +65,6 @@ The server provides fourteen powerful tools for interacting with Roam Research:
|
|
|
64
65
|
ROAM_API_TOKEN=your-api-token
|
|
65
66
|
ROAM_GRAPH_NAME=your-graph-name
|
|
66
67
|
MEMORIES_TAG='#[[LLM/Memories]]'
|
|
67
|
-
PROFILE_PAGE='LLM/Profile' (not yet implemented)
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
Option 2: Using MCP settings (Alternative method)
|
|
@@ -82,8 +82,7 @@ The server provides fourteen powerful tools for interacting with Roam Research:
|
|
|
82
82
|
"env": {
|
|
83
83
|
"ROAM_API_TOKEN": "your-api-token",
|
|
84
84
|
"ROAM_GRAPH_NAME": "your-graph-name",
|
|
85
|
-
"MEMORIES_TAG": "#[[LLM/Memories]]"
|
|
86
|
-
"PROFILE_PAGE": "LLM/Profile"
|
|
85
|
+
"MEMORIES_TAG": "#[[LLM/Memories]]"
|
|
87
86
|
}
|
|
88
87
|
}
|
|
89
88
|
}
|
|
@@ -567,6 +566,85 @@ Returns:
|
|
|
567
566
|
}
|
|
568
567
|
```
|
|
569
568
|
|
|
569
|
+
### Execute Datomic Queries
|
|
570
|
+
|
|
571
|
+
Execute custom Datalog queries on your Roam graph for advanced data retrieval and analysis:
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
use_mcp_tool roam-research roam_datomic_query {
|
|
575
|
+
"query": "[:find (count ?p)\n :where [?p :node/title]]",
|
|
576
|
+
"inputs": []
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Features:
|
|
581
|
+
|
|
582
|
+
- Direct access to Roam's query engine
|
|
583
|
+
- Support for all Datalog query features:
|
|
584
|
+
- Complex pattern matching
|
|
585
|
+
- Aggregation functions (count, sum, max, min, avg, distinct)
|
|
586
|
+
- String operations (includes?, starts-with?, ends-with?)
|
|
587
|
+
- Logical operations (<, >, <=, >=, =, not=)
|
|
588
|
+
- Rules for recursive queries
|
|
589
|
+
- Case-sensitive and case-insensitive search capabilities
|
|
590
|
+
- Efficient querying across the entire graph
|
|
591
|
+
|
|
592
|
+
Parameters:
|
|
593
|
+
|
|
594
|
+
- `query`: The Datalog query to execute (required)
|
|
595
|
+
- `inputs`: Optional array of input parameters for the query
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
|
|
599
|
+
```json
|
|
600
|
+
{
|
|
601
|
+
"success": true,
|
|
602
|
+
"matches": [
|
|
603
|
+
{
|
|
604
|
+
"content": "[result data]",
|
|
605
|
+
"block_uid": "",
|
|
606
|
+
"page_title": ""
|
|
607
|
+
}
|
|
608
|
+
],
|
|
609
|
+
"message": "Query executed successfully. Found N results."
|
|
610
|
+
}
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Example Queries:
|
|
614
|
+
|
|
615
|
+
1. Count all pages:
|
|
616
|
+
|
|
617
|
+
```clojure
|
|
618
|
+
[:find (count ?p)
|
|
619
|
+
:where [?p :node/title]]
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
2. Case-insensitive text search:
|
|
623
|
+
|
|
624
|
+
```clojure
|
|
625
|
+
[:find ?string ?title
|
|
626
|
+
:where
|
|
627
|
+
[?b :block/string ?string]
|
|
628
|
+
[(clojure.string/lower-case ?string) ?lower]
|
|
629
|
+
[(clojure.string/includes? ?lower "search term")]
|
|
630
|
+
[?b :block/page ?p]
|
|
631
|
+
[?p :node/title ?title]]
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
3. Find blocks modified after a date:
|
|
635
|
+
|
|
636
|
+
```clojure
|
|
637
|
+
[:find ?block_ref ?string
|
|
638
|
+
:in $ ?start_of_day
|
|
639
|
+
:where
|
|
640
|
+
[?b :edit/time ?time]
|
|
641
|
+
[(> ?time ?start_of_day)]
|
|
642
|
+
[?b :block/uid ?block_ref]
|
|
643
|
+
[?b :block/string ?string]]
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
See Roam_Research_Datalog_Cheatsheet.md for more query examples and syntax documentation.
|
|
647
|
+
|
|
570
648
|
### Search Block Hierarchy
|
|
571
649
|
|
|
572
650
|
Navigate and search through block parent-child relationships:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { q } from '@roam-research/roam-api-sdk';
|
|
2
|
+
import { BaseSearchHandler } from './types.js';
|
|
3
|
+
export class DatomicSearchHandler extends BaseSearchHandler {
|
|
4
|
+
params;
|
|
5
|
+
constructor(graph, params) {
|
|
6
|
+
super(graph);
|
|
7
|
+
this.params = params;
|
|
8
|
+
}
|
|
9
|
+
async execute() {
|
|
10
|
+
try {
|
|
11
|
+
// Execute the datomic query using the Roam API
|
|
12
|
+
const results = await q(this.graph, this.params.query, this.params.inputs || []);
|
|
13
|
+
return {
|
|
14
|
+
success: true,
|
|
15
|
+
matches: results.map(result => ({
|
|
16
|
+
content: JSON.stringify(result),
|
|
17
|
+
block_uid: '', // Datomic queries may not always return block UIDs
|
|
18
|
+
page_title: '' // Datomic queries may not always return page titles
|
|
19
|
+
})),
|
|
20
|
+
message: `Query executed successfully. Found ${results.length} results.`
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
success: false,
|
|
26
|
+
matches: [],
|
|
27
|
+
message: `Failed to execute query: ${error instanceof Error ? error.message : String(error)}`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/build/search/index.js
CHANGED
|
@@ -37,7 +37,8 @@ export class RoamServer {
|
|
|
37
37
|
roam_search_by_text: {},
|
|
38
38
|
roam_update_block: {},
|
|
39
39
|
roam_update_blocks: {},
|
|
40
|
-
roam_search_by_date: {}
|
|
40
|
+
roam_search_by_date: {},
|
|
41
|
+
roam_datomic_query: {}
|
|
41
42
|
},
|
|
42
43
|
},
|
|
43
44
|
});
|
|
@@ -184,6 +185,13 @@ export class RoamServer {
|
|
|
184
185
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
185
186
|
};
|
|
186
187
|
}
|
|
188
|
+
case 'roam_datomic_query': {
|
|
189
|
+
const { query, inputs } = request.params.arguments;
|
|
190
|
+
const result = await this.toolHandlers.executeDatomicQuery({ query, inputs });
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
187
195
|
default:
|
|
188
196
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
189
197
|
}
|
|
@@ -68,9 +68,9 @@ export class MemoryOperations {
|
|
|
68
68
|
}
|
|
69
69
|
async recall() {
|
|
70
70
|
// Get memories tag from environment
|
|
71
|
-
|
|
71
|
+
var memoriesTag = process.env.MEMORIES_TAG;
|
|
72
72
|
if (!memoriesTag) {
|
|
73
|
-
|
|
73
|
+
memoriesTag = "Memories";
|
|
74
74
|
}
|
|
75
75
|
// Extract the tag text, removing any formatting
|
|
76
76
|
const tagText = memoriesTag
|
|
@@ -54,20 +54,14 @@ export class OutlineOperations {
|
|
|
54
54
|
}
|
|
55
55
|
// If still not found and this is the first retry, try to create the page
|
|
56
56
|
if (retry === 0) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
console.error('Error creating page:', error);
|
|
68
|
-
// Continue to next retry
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
57
|
+
const success = await createPage(this.graph, {
|
|
58
|
+
action: 'create-page',
|
|
59
|
+
page: { title: titleOrUid }
|
|
60
|
+
});
|
|
61
|
+
// Even if createPage returns false, the page might still have been created
|
|
62
|
+
// Wait a bit and continue to next retry
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
64
|
+
continue;
|
|
71
65
|
}
|
|
72
66
|
if (retry < maxRetries - 1) {
|
|
73
67
|
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
@@ -78,23 +72,21 @@ export class OutlineOperations {
|
|
|
78
72
|
// Get or create the target page
|
|
79
73
|
const targetPageUid = await findOrCreatePage(page_title_uid || formatRoamDate(new Date()));
|
|
80
74
|
// Helper function to find block with improved relationship checks
|
|
81
|
-
const findBlockWithRetry = async (pageUid, blockString, maxRetries = 5, initialDelay = 1000
|
|
75
|
+
const findBlockWithRetry = async (pageUid, blockString, maxRetries = 5, initialDelay = 1000) => {
|
|
82
76
|
// Try multiple query strategies
|
|
83
77
|
const queries = [
|
|
84
78
|
// Strategy 1: Direct page and string match
|
|
85
79
|
`[:find ?b-uid ?order
|
|
86
80
|
:where [?p :block/uid "${pageUid}"]
|
|
87
81
|
[?b :block/page ?p]
|
|
88
|
-
[?b :block/string
|
|
89
|
-
[(${case_sensitive ? '=' : 'clojure.string/equals-ignore-case'} ?block-str "${blockString}")]
|
|
82
|
+
[?b :block/string "${blockString}"]
|
|
90
83
|
[?b :block/order ?order]
|
|
91
84
|
[?b :block/uid ?b-uid]]`,
|
|
92
85
|
// Strategy 2: Parent-child relationship
|
|
93
86
|
`[:find ?b-uid ?order
|
|
94
87
|
:where [?p :block/uid "${pageUid}"]
|
|
95
88
|
[?b :block/parents ?p]
|
|
96
|
-
[?b :block/string
|
|
97
|
-
[(${case_sensitive ? '=' : 'clojure.string/equals-ignore-case'} ?block-str "${blockString}")]
|
|
89
|
+
[?b :block/string "${blockString}"]
|
|
98
90
|
[?b :block/order ?order]
|
|
99
91
|
[?b :block/uid ?b-uid]]`,
|
|
100
92
|
// Strategy 3: Broader page relationship
|
|
@@ -102,8 +94,7 @@ export class OutlineOperations {
|
|
|
102
94
|
:where [?p :block/uid "${pageUid}"]
|
|
103
95
|
[?b :block/page ?page]
|
|
104
96
|
[?p :block/page ?page]
|
|
105
|
-
[?b :block/string
|
|
106
|
-
[(${case_sensitive ? '=' : 'clojure.string/equals-ignore-case'} ?block-str "${blockString}")]
|
|
97
|
+
[?b :block/string "${blockString}"]
|
|
107
98
|
[?b :block/order ?order]
|
|
108
99
|
[?b :block/uid ?b-uid]]`
|
|
109
100
|
];
|
|
@@ -125,7 +116,7 @@ export class OutlineOperations {
|
|
|
125
116
|
throw new McpError(ErrorCode.InternalError, `Failed to find block "${blockString}" under page "${pageUid}" after trying multiple strategies`);
|
|
126
117
|
};
|
|
127
118
|
// Helper function to create and verify block with improved error handling
|
|
128
|
-
const createAndVerifyBlock = async (content, parentUid, maxRetries = 5, initialDelay = 1000, isRetry = false
|
|
119
|
+
const createAndVerifyBlock = async (content, parentUid, maxRetries = 5, initialDelay = 1000, isRetry = false) => {
|
|
129
120
|
try {
|
|
130
121
|
// Initial delay before any operations
|
|
131
122
|
if (!isRetry) {
|
|
@@ -133,25 +124,25 @@ export class OutlineOperations {
|
|
|
133
124
|
}
|
|
134
125
|
for (let retry = 0; retry < maxRetries; retry++) {
|
|
135
126
|
console.log(`Attempt ${retry + 1}/${maxRetries} to create block "${content}" under "${parentUid}"`);
|
|
127
|
+
// Create block
|
|
128
|
+
const success = await createBlock(this.graph, {
|
|
129
|
+
action: 'create-block',
|
|
130
|
+
location: {
|
|
131
|
+
'parent-uid': parentUid,
|
|
132
|
+
order: 'last'
|
|
133
|
+
},
|
|
134
|
+
block: { string: content }
|
|
135
|
+
});
|
|
136
|
+
// Wait with exponential backoff
|
|
137
|
+
const delay = initialDelay * Math.pow(2, retry);
|
|
138
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
136
139
|
try {
|
|
137
|
-
// Create block
|
|
138
|
-
await createBlock(this.graph, {
|
|
139
|
-
action: 'create-block',
|
|
140
|
-
location: {
|
|
141
|
-
'parent-uid': parentUid,
|
|
142
|
-
order: 'first'
|
|
143
|
-
},
|
|
144
|
-
block: { string: content }
|
|
145
|
-
});
|
|
146
|
-
// Wait with exponential backoff
|
|
147
|
-
const delay = initialDelay * Math.pow(2, retry);
|
|
148
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
149
140
|
// Try to find the block using our improved findBlockWithRetry
|
|
150
|
-
return await findBlockWithRetry(parentUid, content
|
|
141
|
+
return await findBlockWithRetry(parentUid, content);
|
|
151
142
|
}
|
|
152
143
|
catch (error) {
|
|
153
144
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
154
|
-
console.log(`Failed to
|
|
145
|
+
console.log(`Failed to find block on attempt ${retry + 1}: ${errorMessage}`);
|
|
155
146
|
if (retry === maxRetries - 1)
|
|
156
147
|
throw error;
|
|
157
148
|
}
|
|
@@ -165,7 +156,7 @@ export class OutlineOperations {
|
|
|
165
156
|
// Otherwise, try one more time with a clean slate
|
|
166
157
|
console.log(`Retrying block creation for "${content}" with fresh attempt`);
|
|
167
158
|
await new Promise(resolve => setTimeout(resolve, initialDelay * 2));
|
|
168
|
-
return createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true
|
|
159
|
+
return createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true);
|
|
169
160
|
}
|
|
170
161
|
};
|
|
171
162
|
// Get or create the parent block
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TagSearchHandler, BlockRefSearchHandler, HierarchySearchHandler, TextSearchHandler } from '../../../search/index.js';
|
|
1
|
+
import { TagSearchHandler, BlockRefSearchHandler, HierarchySearchHandler, TextSearchHandler, DatomicSearchHandler } from '../../../search/index.js';
|
|
2
2
|
// Base class for all search handlers
|
|
3
3
|
export class BaseSearchHandler {
|
|
4
4
|
graph;
|
|
@@ -54,3 +54,15 @@ export class TextSearchHandlerImpl extends BaseSearchHandler {
|
|
|
54
54
|
return handler.execute();
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
// Datomic query handler
|
|
58
|
+
export class DatomicSearchHandlerImpl extends BaseSearchHandler {
|
|
59
|
+
params;
|
|
60
|
+
constructor(graph, params) {
|
|
61
|
+
super(graph);
|
|
62
|
+
this.params = params;
|
|
63
|
+
}
|
|
64
|
+
async execute() {
|
|
65
|
+
const handler = new DatomicSearchHandler(this.graph, this.params);
|
|
66
|
+
return handler.execute();
|
|
67
|
+
}
|
|
68
|
+
}
|
package/build/tools/schemas.js
CHANGED
|
@@ -426,5 +426,26 @@ export const toolSchemas = {
|
|
|
426
426
|
properties: {},
|
|
427
427
|
required: []
|
|
428
428
|
}
|
|
429
|
+
},
|
|
430
|
+
roam_datomic_query: {
|
|
431
|
+
name: 'roam_datomic_query',
|
|
432
|
+
description: 'Execute a custom Datomic query on the Roam graph beyond the available search tools. This provides direct access to Roam\'s query engine for advanced data retrieval. Note: The Roam graph is case-sensitive.\nA list of some of Roam\'s data model Namespaces and Attributes: ancestor (descendants), attrs (lookup), block (children, heading, open, order, page, parents, props, refs, string, text-align, uid), children (view-type), create (email, time), descendant (ancestors), edit (email, seen-by, time), entity (attrs), log (id), node (title), page (uid, title), refs (text).\nPredicates (clojure.string/includes?, clojure.string/starts-with?, clojure.string/ends-with?, <, >, <=, >=, =, not=, !=).\nAggregates (distinct, count, sum, max, min, avg).\nTips: Use :block/parents for all ancestor levels, :block/children for direct descendants only; combine clojure.string for complex matching, use distinct to deduplicate, leverage Pull patterns for hierarchies, handle case-sensitivity carefully, and chain ancestry rules for multi-level queries.',
|
|
433
|
+
inputSchema: {
|
|
434
|
+
type: 'object',
|
|
435
|
+
properties: {
|
|
436
|
+
query: {
|
|
437
|
+
type: 'string',
|
|
438
|
+
description: 'The Datomic query to execute (in Datalog syntax)'
|
|
439
|
+
},
|
|
440
|
+
inputs: {
|
|
441
|
+
type: 'array',
|
|
442
|
+
description: 'Optional array of input parameters for the query',
|
|
443
|
+
items: {
|
|
444
|
+
type: 'string'
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
required: ['query']
|
|
449
|
+
}
|
|
429
450
|
}
|
|
430
451
|
};
|
|
@@ -4,6 +4,7 @@ import { SearchOperations } from './operations/search/index.js';
|
|
|
4
4
|
import { MemoryOperations } from './operations/memory.js';
|
|
5
5
|
import { TodoOperations } from './operations/todos.js';
|
|
6
6
|
import { OutlineOperations } from './operations/outline.js';
|
|
7
|
+
import { DatomicSearchHandlerImpl } from './operations/search/handlers.js';
|
|
7
8
|
export class ToolHandlers {
|
|
8
9
|
graph;
|
|
9
10
|
pageOps;
|
|
@@ -60,6 +61,11 @@ export class ToolHandlers {
|
|
|
60
61
|
async searchByDate(params) {
|
|
61
62
|
return this.searchOps.searchByDate(params);
|
|
62
63
|
}
|
|
64
|
+
// Datomic query
|
|
65
|
+
async executeDatomicQuery(params) {
|
|
66
|
+
const handler = new DatomicSearchHandlerImpl(this.graph, params);
|
|
67
|
+
return handler.execute();
|
|
68
|
+
}
|
|
63
69
|
// Memory Operations
|
|
64
70
|
async remember(memory, categories) {
|
|
65
71
|
return this.memoryOps.remember(memory, categories);
|