roam-research-mcp 0.23.0 → 0.24.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/build/search/block-ref-search.js +8 -2
- package/build/search/hierarchy-search.js +11 -7
- package/build/search/status-search.js +8 -2
- package/build/search/tag-search.js +8 -2
- package/build/search/text-search.js +8 -2
- package/build/tools/operations/outline.js +28 -37
- package/build/tools/operations/pages.js +1 -1
- package/build/tools/operations/search/handlers.js +13 -1
- package/build/tools/operations/search/index.js +5 -5
- package/build/tools/schemas.js +5 -5
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { q } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { BaseSearchHandler } from './types.js';
|
|
3
3
|
import { SearchUtils } from './utils.js';
|
|
4
|
+
import { resolveRefs } from '../tools/helpers/refs.js';
|
|
4
5
|
export class BlockRefSearchHandler extends BaseSearchHandler {
|
|
5
6
|
params;
|
|
6
7
|
constructor(graph, params) {
|
|
@@ -63,10 +64,15 @@ export class BlockRefSearchHandler extends BaseSearchHandler {
|
|
|
63
64
|
queryParams = [];
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
|
-
const
|
|
67
|
+
const rawResults = await q(this.graph, queryStr, queryParams);
|
|
68
|
+
// Resolve block references in content
|
|
69
|
+
const resolvedResults = await Promise.all(rawResults.map(async ([uid, content, pageTitle]) => {
|
|
70
|
+
const resolvedContent = await resolveRefs(this.graph, content);
|
|
71
|
+
return [uid, resolvedContent, pageTitle];
|
|
72
|
+
}));
|
|
67
73
|
const searchDescription = block_uid
|
|
68
74
|
? `referencing block ((${block_uid}))`
|
|
69
75
|
: 'containing block references';
|
|
70
|
-
return SearchUtils.formatSearchResults(
|
|
76
|
+
return SearchUtils.formatSearchResults(resolvedResults, searchDescription, !targetPageUid);
|
|
71
77
|
}
|
|
72
78
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { q } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { BaseSearchHandler } from './types.js';
|
|
3
3
|
import { SearchUtils } from './utils.js';
|
|
4
|
+
import { resolveRefs } from '../tools/helpers/refs.js';
|
|
4
5
|
export class HierarchySearchHandler extends BaseSearchHandler {
|
|
5
6
|
params;
|
|
6
7
|
constructor(graph, params) {
|
|
@@ -85,13 +86,16 @@ export class HierarchySearchHandler extends BaseSearchHandler {
|
|
|
85
86
|
queryParams = [ancestorRule, child_uid];
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
|
-
const
|
|
89
|
-
//
|
|
90
|
-
const matches =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
const rawResults = await q(this.graph, queryStr, queryParams);
|
|
90
|
+
// Resolve block references and format results to include depth information
|
|
91
|
+
const matches = await Promise.all(rawResults.map(async ([uid, content, pageTitle, depth]) => {
|
|
92
|
+
const resolvedContent = await resolveRefs(this.graph, content);
|
|
93
|
+
return {
|
|
94
|
+
block_uid: uid,
|
|
95
|
+
content: resolvedContent,
|
|
96
|
+
depth: depth || 1,
|
|
97
|
+
...(pageTitle && { page_title: pageTitle })
|
|
98
|
+
};
|
|
95
99
|
}));
|
|
96
100
|
const searchDescription = parent_uid
|
|
97
101
|
? `descendants of block ${parent_uid}`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { q } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { BaseSearchHandler } from './types.js';
|
|
3
3
|
import { SearchUtils } from './utils.js';
|
|
4
|
+
import { resolveRefs } from '../tools/helpers/refs.js';
|
|
4
5
|
export class StatusSearchHandler extends BaseSearchHandler {
|
|
5
6
|
params;
|
|
6
7
|
constructor(graph, params) {
|
|
@@ -37,7 +38,12 @@ export class StatusSearchHandler extends BaseSearchHandler {
|
|
|
37
38
|
[(clojure.string/includes? ?block-str (str "{{[[" ?status "]]}}"))]]`;
|
|
38
39
|
queryParams = [status];
|
|
39
40
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
41
|
+
const rawResults = await q(this.graph, queryStr, queryParams);
|
|
42
|
+
// Resolve block references in content
|
|
43
|
+
const resolvedResults = await Promise.all(rawResults.map(async ([uid, content, pageTitle]) => {
|
|
44
|
+
const resolvedContent = await resolveRefs(this.graph, content);
|
|
45
|
+
return [uid, resolvedContent, pageTitle];
|
|
46
|
+
}));
|
|
47
|
+
return SearchUtils.formatSearchResults(resolvedResults, `with status ${status}`, !targetPageUid);
|
|
42
48
|
}
|
|
43
49
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { q } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { BaseSearchHandler } from './types.js';
|
|
3
3
|
import { SearchUtils } from './utils.js';
|
|
4
|
+
import { resolveRefs } from '../tools/helpers/refs.js';
|
|
4
5
|
export class TagSearchHandler extends BaseSearchHandler {
|
|
5
6
|
params;
|
|
6
7
|
constructor(graph, params) {
|
|
@@ -28,8 +29,13 @@ export class TagSearchHandler extends BaseSearchHandler {
|
|
|
28
29
|
[?b :block/page ?p]
|
|
29
30
|
[?p :node/title ?page-title]]`;
|
|
30
31
|
const queryParams = [primary_tag];
|
|
31
|
-
const
|
|
32
|
+
const rawResults = await q(this.graph, queryStr, queryParams);
|
|
33
|
+
// Resolve block references in content
|
|
34
|
+
const resolvedResults = await Promise.all(rawResults.map(async ([uid, content, pageTitle]) => {
|
|
35
|
+
const resolvedContent = await resolveRefs(this.graph, content);
|
|
36
|
+
return [uid, resolvedContent, pageTitle];
|
|
37
|
+
}));
|
|
32
38
|
const searchDescription = `referencing "${primary_tag}"`;
|
|
33
|
-
return SearchUtils.formatSearchResults(
|
|
39
|
+
return SearchUtils.formatSearchResults(resolvedResults, searchDescription, !targetPageUid);
|
|
34
40
|
}
|
|
35
41
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { q } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { BaseSearchHandler } from './types.js';
|
|
3
3
|
import { SearchUtils } from './utils.js';
|
|
4
|
+
import { resolveRefs } from '../tools/helpers/refs.js';
|
|
4
5
|
export class TextSearchHandler extends BaseSearchHandler {
|
|
5
6
|
params;
|
|
6
7
|
constructor(graph, params) {
|
|
@@ -24,8 +25,13 @@ export class TextSearchHandler extends BaseSearchHandler {
|
|
|
24
25
|
[?b :block/page ?p]
|
|
25
26
|
[?p :node/title ?page-title]]`;
|
|
26
27
|
const queryParams = [text];
|
|
27
|
-
const
|
|
28
|
+
const rawResults = await q(this.graph, queryStr, queryParams);
|
|
29
|
+
// Resolve block references in content
|
|
30
|
+
const resolvedResults = await Promise.all(rawResults.map(async ([uid, content, pageTitle]) => {
|
|
31
|
+
const resolvedContent = await resolveRefs(this.graph, content);
|
|
32
|
+
return [uid, resolvedContent, pageTitle];
|
|
33
|
+
}));
|
|
28
34
|
const searchDescription = `containing "${text}"`;
|
|
29
|
-
return SearchUtils.formatSearchResults(
|
|
35
|
+
return SearchUtils.formatSearchResults(resolvedResults, searchDescription, !targetPageUid);
|
|
30
36
|
}
|
|
31
37
|
}
|
|
@@ -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
|
|
@@ -87,7 +87,7 @@ export class PageOperations {
|
|
|
87
87
|
// Use import_nested_markdown functionality
|
|
88
88
|
const convertedContent = convertToRoamMarkdown(content);
|
|
89
89
|
const nodes = parseMarkdown(convertedContent);
|
|
90
|
-
const actions = convertToRoamActions(nodes, pageUid, '
|
|
90
|
+
const actions = convertToRoamActions(nodes, pageUid, 'first');
|
|
91
91
|
const result = await batchActions(this.graph, {
|
|
92
92
|
action: 'batch-actions',
|
|
93
93
|
actions
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TagSearchHandler, BlockRefSearchHandler, HierarchySearchHandler, TextSearchHandler, DatomicSearchHandler } from '../../../search/index.js';
|
|
1
|
+
import { TagSearchHandler, BlockRefSearchHandler, HierarchySearchHandler, TextSearchHandler, DatomicSearchHandler, StatusSearchHandler } from '../../../search/index.js';
|
|
2
2
|
// Base class for all search handlers
|
|
3
3
|
export class BaseSearchHandler {
|
|
4
4
|
graph;
|
|
@@ -54,6 +54,18 @@ export class TextSearchHandlerImpl extends BaseSearchHandler {
|
|
|
54
54
|
return handler.execute();
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
// Status search handler
|
|
58
|
+
export class StatusSearchHandlerImpl extends BaseSearchHandler {
|
|
59
|
+
params;
|
|
60
|
+
constructor(graph, params) {
|
|
61
|
+
super(graph);
|
|
62
|
+
this.params = params;
|
|
63
|
+
}
|
|
64
|
+
async execute() {
|
|
65
|
+
const handler = new StatusSearchHandler(this.graph, this.params);
|
|
66
|
+
return handler.execute();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
57
69
|
// Datomic query handler
|
|
58
70
|
export class DatomicSearchHandlerImpl extends BaseSearchHandler {
|
|
59
71
|
params;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { TagSearchHandlerImpl, BlockRefSearchHandlerImpl, HierarchySearchHandlerImpl, TextSearchHandlerImpl } from './handlers.js';
|
|
1
|
+
import { TagSearchHandlerImpl, BlockRefSearchHandlerImpl, HierarchySearchHandlerImpl, TextSearchHandlerImpl, StatusSearchHandlerImpl } from './handlers.js';
|
|
2
2
|
export class SearchOperations {
|
|
3
3
|
graph;
|
|
4
4
|
constructor(graph) {
|
|
5
5
|
this.graph = graph;
|
|
6
6
|
}
|
|
7
7
|
async searchByStatus(status, page_title_uid, include, exclude) {
|
|
8
|
-
const handler = new
|
|
9
|
-
|
|
8
|
+
const handler = new StatusSearchHandlerImpl(this.graph, {
|
|
9
|
+
status,
|
|
10
10
|
page_title_uid,
|
|
11
11
|
});
|
|
12
12
|
const result = await handler.execute();
|
|
@@ -14,7 +14,7 @@ export class SearchOperations {
|
|
|
14
14
|
let matches = result.matches;
|
|
15
15
|
if (include) {
|
|
16
16
|
const includeTerms = include.split(',').map(term => term.trim());
|
|
17
|
-
matches = matches.filter(match => {
|
|
17
|
+
matches = matches.filter((match) => {
|
|
18
18
|
const matchContent = match.content;
|
|
19
19
|
const matchTitle = match.page_title;
|
|
20
20
|
const terms = includeTerms;
|
|
@@ -24,7 +24,7 @@ export class SearchOperations {
|
|
|
24
24
|
}
|
|
25
25
|
if (exclude) {
|
|
26
26
|
const excludeTerms = exclude.split(',').map(term => term.trim());
|
|
27
|
-
matches = matches.filter(match => {
|
|
27
|
+
matches = matches.filter((match) => {
|
|
28
28
|
const matchContent = match.content;
|
|
29
29
|
const matchTitle = match.page_title;
|
|
30
30
|
const terms = excludeTerms;
|
package/build/tools/schemas.js
CHANGED
|
@@ -34,7 +34,7 @@ export const toolSchemas = {
|
|
|
34
34
|
},
|
|
35
35
|
roam_create_page: {
|
|
36
36
|
name: 'roam_create_page',
|
|
37
|
-
description: 'Create a new standalone page in Roam from markdown
|
|
37
|
+
description: 'Create a new standalone page in Roam with optional content using markdown-style formatting. The nesting structure is inferred from markdown indentation (spaces). Best for:\n- Creating foundational concept pages that other pages will link to/from\n- Establishing new topic areas that need their own namespace\n- Setting up reference materials or documentation\n- Making permanent collections of information.',
|
|
38
38
|
inputSchema: {
|
|
39
39
|
type: 'object',
|
|
40
40
|
properties: {
|
|
@@ -44,7 +44,7 @@ export const toolSchemas = {
|
|
|
44
44
|
},
|
|
45
45
|
content: {
|
|
46
46
|
type: 'string',
|
|
47
|
-
description: 'Initial content for the page (optional)',
|
|
47
|
+
description: 'Initial content for the page (optional). Each line becomes a separate block. Indentation (using spaces or tabs) determines the nesting level of each block.',
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
required: ['title'],
|
|
@@ -74,7 +74,7 @@ export const toolSchemas = {
|
|
|
74
74
|
},
|
|
75
75
|
roam_create_outline: {
|
|
76
76
|
name: 'roam_create_outline',
|
|
77
|
-
description: '
|
|
77
|
+
description: 'Add a structured outline to an existing page or block (by title text or uid), with customizable nesting levels. Best for:\n- Adding supplementary structured content to existing pages\n- Creating temporary or working outlines (meeting notes, brainstorms)\n- Organizing thoughts or research under a specific topic\n- Breaking down subtopics or components of a larger concept',
|
|
78
78
|
inputSchema: {
|
|
79
79
|
type: 'object',
|
|
80
80
|
properties: {
|
|
@@ -88,7 +88,7 @@ export const toolSchemas = {
|
|
|
88
88
|
},
|
|
89
89
|
outline: {
|
|
90
90
|
type: 'array',
|
|
91
|
-
description: 'Array of outline items with block text and level',
|
|
91
|
+
description: 'Array of outline items with block text and explicit nesting level',
|
|
92
92
|
items: {
|
|
93
93
|
type: 'object',
|
|
94
94
|
properties: {
|
|
@@ -429,7 +429,7 @@ export const toolSchemas = {
|
|
|
429
429
|
},
|
|
430
430
|
roam_datomic_query: {
|
|
431
431
|
name: 'roam_datomic_query',
|
|
432
|
-
description: 'Execute a custom Datomic query on the Roam graph. This provides direct access to Roam\'s query engine for advanced data retrieval. Note:
|
|
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: Roam graph is case-sensitive.\nList 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, limit).\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
433
|
inputSchema: {
|
|
434
434
|
type: 'object',
|
|
435
435
|
properties: {
|