roam-research-mcp 0.25.0 → 0.25.5
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/markdown-utils.js +10 -26
- package/build/search/block-ref-search.js +0 -1
- package/build/search/datomic-search.js +0 -1
- package/build/search/hierarchy-search.js +0 -1
- package/build/search/status-search.js +0 -1
- package/build/search/tag-search.js +0 -1
- package/build/search/text-search.js +0 -1
- package/build/search/types.js +0 -1
- package/build/server/roam-server.js +5 -8
- package/build/tools/operations/blocks.js +58 -25
- package/build/tools/operations/memory.js +0 -2
- package/build/tools/operations/outline.js +17 -1
- package/build/tools/operations/pages.js +40 -6
- package/build/tools/operations/search/handlers.js +0 -7
- package/build/tools/operations/search/index.js +0 -1
- package/build/tools/operations/todos.js +0 -1
- package/build/tools/schemas.js +35 -14
- package/build/tools/tool-handlers.js +4 -11
- package/package.json +1 -1
package/build/markdown-utils.js
CHANGED
|
@@ -176,39 +176,23 @@ function parseMarkdown(markdown) {
|
|
|
176
176
|
// Calculate indentation level (2 spaces = 1 level)
|
|
177
177
|
const indentation = line.match(/^\s*/)?.[0].length ?? 0;
|
|
178
178
|
let level = Math.floor(indentation / 2);
|
|
179
|
-
|
|
180
|
-
const { heading_level, content: headingContent } = parseMarkdownHeadingLevel(trimmedLine);
|
|
181
|
-
// Then handle bullet points if not a heading
|
|
182
|
-
let content;
|
|
183
|
-
if (heading_level > 0) {
|
|
184
|
-
content = headingContent; // Use clean heading content without # marks
|
|
185
|
-
level = 0; // Headings start at root level
|
|
186
|
-
stack.length = 1; // Reset stack but keep heading as parent
|
|
187
|
-
// Create heading node
|
|
188
|
-
const node = {
|
|
189
|
-
content,
|
|
190
|
-
level,
|
|
191
|
-
heading_level, // Store heading level in node
|
|
192
|
-
children: []
|
|
193
|
-
};
|
|
194
|
-
rootNodes.push(node);
|
|
195
|
-
stack[0] = node;
|
|
196
|
-
continue; // Skip to next line
|
|
197
|
-
}
|
|
198
|
-
// Handle non-heading content
|
|
179
|
+
let contentToParse;
|
|
199
180
|
const bulletMatch = trimmedLine.match(/^(\s*)[-*+]\s+/);
|
|
200
181
|
if (bulletMatch) {
|
|
201
|
-
//
|
|
202
|
-
content = trimmedLine.substring(bulletMatch[0].length);
|
|
182
|
+
// If it's a bullet point, adjust level based on bullet indentation
|
|
203
183
|
level = Math.floor(bulletMatch[1].length / 2);
|
|
184
|
+
contentToParse = trimmedLine.substring(bulletMatch[0].length); // Content after bullet
|
|
204
185
|
}
|
|
205
186
|
else {
|
|
206
|
-
|
|
187
|
+
contentToParse = trimmedLine; // No bullet, use trimmed line
|
|
207
188
|
}
|
|
208
|
-
//
|
|
189
|
+
// Now, from the content after bullet/initial indentation, check for heading
|
|
190
|
+
const { heading_level, content: finalContent } = parseMarkdownHeadingLevel(contentToParse);
|
|
191
|
+
// Create node
|
|
209
192
|
const node = {
|
|
210
|
-
content,
|
|
211
|
-
level,
|
|
193
|
+
content: finalContent, // Use content after heading parsing
|
|
194
|
+
level, // Use level derived from bullet/indentation
|
|
195
|
+
...(heading_level > 0 && { heading_level }),
|
|
212
196
|
children: []
|
|
213
197
|
};
|
|
214
198
|
// Pop stack until we find the parent level
|
|
@@ -3,7 +3,6 @@ import { BaseSearchHandler } from './types.js';
|
|
|
3
3
|
import { SearchUtils } from './utils.js';
|
|
4
4
|
import { resolveRefs } from '../tools/helpers/refs.js';
|
|
5
5
|
export class BlockRefSearchHandler extends BaseSearchHandler {
|
|
6
|
-
params;
|
|
7
6
|
constructor(graph, params) {
|
|
8
7
|
super(graph);
|
|
9
8
|
this.params = params;
|
|
@@ -3,7 +3,6 @@ import { BaseSearchHandler } from './types.js';
|
|
|
3
3
|
import { SearchUtils } from './utils.js';
|
|
4
4
|
import { resolveRefs } from '../tools/helpers/refs.js';
|
|
5
5
|
export class HierarchySearchHandler extends BaseSearchHandler {
|
|
6
|
-
params;
|
|
7
6
|
constructor(graph, params) {
|
|
8
7
|
super(graph);
|
|
9
8
|
this.params = params;
|
|
@@ -3,7 +3,6 @@ import { BaseSearchHandler } from './types.js';
|
|
|
3
3
|
import { SearchUtils } from './utils.js';
|
|
4
4
|
import { resolveRefs } from '../tools/helpers/refs.js';
|
|
5
5
|
export class StatusSearchHandler extends BaseSearchHandler {
|
|
6
|
-
params;
|
|
7
6
|
constructor(graph, params) {
|
|
8
7
|
super(graph);
|
|
9
8
|
this.params = params;
|
|
@@ -3,7 +3,6 @@ import { BaseSearchHandler } from './types.js';
|
|
|
3
3
|
import { SearchUtils } from './utils.js';
|
|
4
4
|
import { resolveRefs } from '../tools/helpers/refs.js';
|
|
5
5
|
export class TagSearchHandler extends BaseSearchHandler {
|
|
6
|
-
params;
|
|
7
6
|
constructor(graph, params) {
|
|
8
7
|
super(graph);
|
|
9
8
|
this.params = params;
|
|
@@ -3,7 +3,6 @@ import { BaseSearchHandler } from './types.js';
|
|
|
3
3
|
import { SearchUtils } from './utils.js';
|
|
4
4
|
import { resolveRefs } from '../tools/helpers/refs.js';
|
|
5
5
|
export class TextSearchHandler extends BaseSearchHandler {
|
|
6
|
-
params;
|
|
7
6
|
constructor(graph, params) {
|
|
8
7
|
super(graph);
|
|
9
8
|
this.params = params;
|
package/build/search/types.js
CHANGED
|
@@ -6,9 +6,6 @@ import { API_TOKEN, GRAPH_NAME } from '../config/environment.js';
|
|
|
6
6
|
import { toolSchemas } from '../tools/schemas.js';
|
|
7
7
|
import { ToolHandlers } from '../tools/tool-handlers.js';
|
|
8
8
|
export class RoamServer {
|
|
9
|
-
server;
|
|
10
|
-
toolHandlers;
|
|
11
|
-
graph;
|
|
12
9
|
constructor() {
|
|
13
10
|
this.graph = initializeGraph({
|
|
14
11
|
token: API_TOKEN,
|
|
@@ -17,7 +14,7 @@ export class RoamServer {
|
|
|
17
14
|
this.toolHandlers = new ToolHandlers(this.graph);
|
|
18
15
|
this.server = new Server({
|
|
19
16
|
name: 'roam-research',
|
|
20
|
-
version: '0.25.
|
|
17
|
+
version: '0.25.5',
|
|
21
18
|
}, {
|
|
22
19
|
capabilities: {
|
|
23
20
|
tools: {
|
|
@@ -67,8 +64,8 @@ export class RoamServer {
|
|
|
67
64
|
};
|
|
68
65
|
}
|
|
69
66
|
case 'roam_fetch_page_by_title': {
|
|
70
|
-
const { title } = request.params.arguments;
|
|
71
|
-
const content = await this.toolHandlers.fetchPageByTitle(title);
|
|
67
|
+
const { title, format } = request.params.arguments;
|
|
68
|
+
const content = await this.toolHandlers.fetchPageByTitle(title, format);
|
|
72
69
|
return {
|
|
73
70
|
content: [{ type: 'text', text: content }],
|
|
74
71
|
};
|
|
@@ -81,8 +78,8 @@ export class RoamServer {
|
|
|
81
78
|
};
|
|
82
79
|
}
|
|
83
80
|
case 'roam_create_block': {
|
|
84
|
-
const { content, page_uid, title } = request.params.arguments;
|
|
85
|
-
const result = await this.toolHandlers.createBlock(content, page_uid, title);
|
|
81
|
+
const { content, page_uid, title, heading } = request.params.arguments;
|
|
82
|
+
const result = await this.toolHandlers.createBlock(content, page_uid, title, heading);
|
|
86
83
|
return {
|
|
87
84
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
88
85
|
};
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { q,
|
|
1
|
+
import { q, updateBlock as updateRoamBlock, batchActions, createPage } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { formatRoamDate } from '../../utils/helpers.js';
|
|
4
4
|
import { parseMarkdown, convertToRoamActions, convertToRoamMarkdown } from '../../markdown-utils.js';
|
|
5
5
|
export class BlockOperations {
|
|
6
|
-
graph;
|
|
7
6
|
constructor(graph) {
|
|
8
7
|
this.graph = graph;
|
|
9
8
|
}
|
|
10
|
-
async createBlock(content, page_uid, title) {
|
|
9
|
+
async createBlock(content, page_uid, title, heading) {
|
|
11
10
|
// If page_uid provided, use it directly
|
|
12
11
|
let targetPageUid = page_uid;
|
|
13
12
|
// If no page_uid but title provided, search for page by title
|
|
@@ -68,9 +67,44 @@ export class BlockOperations {
|
|
|
68
67
|
try {
|
|
69
68
|
// If the content has multiple lines or is a table, use nested import
|
|
70
69
|
if (content.includes('\n')) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
let nodes;
|
|
71
|
+
// If heading parameter is provided, manually construct nodes to preserve heading
|
|
72
|
+
if (heading) {
|
|
73
|
+
const lines = content.split('\n');
|
|
74
|
+
const firstLine = lines[0].trim();
|
|
75
|
+
const remainingLines = lines.slice(1);
|
|
76
|
+
// Create the first node with heading formatting
|
|
77
|
+
const firstNode = {
|
|
78
|
+
content: firstLine,
|
|
79
|
+
level: 0,
|
|
80
|
+
heading_level: heading,
|
|
81
|
+
children: []
|
|
82
|
+
};
|
|
83
|
+
// If there are remaining lines, parse them as children or siblings
|
|
84
|
+
if (remainingLines.length > 0 && remainingLines.some(line => line.trim())) {
|
|
85
|
+
const remainingContent = remainingLines.join('\n');
|
|
86
|
+
const convertedRemainingContent = convertToRoamMarkdown(remainingContent);
|
|
87
|
+
const remainingNodes = parseMarkdown(convertedRemainingContent);
|
|
88
|
+
// Add remaining nodes as siblings to the first node
|
|
89
|
+
nodes = [firstNode, ...remainingNodes];
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
nodes = [firstNode];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// No heading parameter, use original parsing logic
|
|
97
|
+
const convertedContent = convertToRoamMarkdown(content);
|
|
98
|
+
nodes = parseMarkdown(convertedContent);
|
|
99
|
+
// If we have simple newline-separated content (no markdown formatting),
|
|
100
|
+
// and parseMarkdown created nodes at level 0, reverse them to maintain original order
|
|
101
|
+
if (nodes.every(node => node.level === 0 && !node.heading_level)) {
|
|
102
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
103
|
+
if (lines.length === nodes.length) {
|
|
104
|
+
nodes.reverse();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
74
108
|
const actions = convertToRoamActions(nodes, targetPageUid, 'last');
|
|
75
109
|
// Execute batch actions to create the nested structure
|
|
76
110
|
const result = await batchActions(this.graph, {
|
|
@@ -88,27 +122,26 @@ export class BlockOperations {
|
|
|
88
122
|
};
|
|
89
123
|
}
|
|
90
124
|
else {
|
|
91
|
-
// For
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
125
|
+
// For single block content, use the same convertToRoamActions approach that works in roam_create_page
|
|
126
|
+
const nodes = [{
|
|
127
|
+
content: content,
|
|
128
|
+
level: 0,
|
|
129
|
+
...(heading && typeof heading === 'number' && heading > 0 && { heading_level: heading }),
|
|
130
|
+
children: []
|
|
131
|
+
}];
|
|
132
|
+
if (!targetPageUid) {
|
|
133
|
+
throw new McpError(ErrorCode.InternalError, 'targetPageUid is undefined');
|
|
134
|
+
}
|
|
135
|
+
const actions = convertToRoamActions(nodes, targetPageUid, 'last');
|
|
136
|
+
// Execute batch actions to create the block
|
|
137
|
+
const result = await batchActions(this.graph, {
|
|
138
|
+
action: 'batch-actions',
|
|
139
|
+
actions
|
|
99
140
|
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
:in $ ?parent ?string
|
|
103
|
-
:where [?b :block/uid ?uid]
|
|
104
|
-
[?b :block/string ?string]
|
|
105
|
-
[?b :block/parents ?p]
|
|
106
|
-
[?p :block/uid ?parent]]`;
|
|
107
|
-
const blockResults = await q(this.graph, findBlockQuery, [targetPageUid, content]);
|
|
108
|
-
if (!blockResults || blockResults.length === 0) {
|
|
109
|
-
throw new Error('Could not find created block');
|
|
141
|
+
if (!result) {
|
|
142
|
+
throw new Error('Failed to create block');
|
|
110
143
|
}
|
|
111
|
-
const blockUid =
|
|
144
|
+
const blockUid = result.created_uids?.[0];
|
|
112
145
|
return {
|
|
113
146
|
success: true,
|
|
114
147
|
block_uid: blockUid,
|
|
@@ -4,8 +4,6 @@ import { formatRoamDate } from '../../utils/helpers.js';
|
|
|
4
4
|
import { resolveRefs } from '../helpers/refs.js';
|
|
5
5
|
import { SearchOperations } from './search/index.js';
|
|
6
6
|
export class MemoryOperations {
|
|
7
|
-
graph;
|
|
8
|
-
searchOps;
|
|
9
7
|
constructor(graph) {
|
|
10
8
|
this.graph = graph;
|
|
11
9
|
this.searchOps = new SearchOperations(graph);
|
|
@@ -4,10 +4,24 @@ import { formatRoamDate } from '../../utils/helpers.js';
|
|
|
4
4
|
import { capitalizeWords } from '../helpers/text.js';
|
|
5
5
|
import { parseMarkdown, convertToRoamActions, convertToRoamMarkdown } from '../../markdown-utils.js';
|
|
6
6
|
export class OutlineOperations {
|
|
7
|
-
graph;
|
|
8
7
|
constructor(graph) {
|
|
9
8
|
this.graph = graph;
|
|
10
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Creates an outline structure on a Roam Research page, optionally under a specific block.
|
|
12
|
+
*
|
|
13
|
+
* @param outline - An array of OutlineItem objects, each containing text and a level.
|
|
14
|
+
* Markdown heading syntax (#, ##, ###) in the text will be recognized
|
|
15
|
+
* and converted to Roam headings while preserving the outline's hierarchical
|
|
16
|
+
* structure based on indentation.
|
|
17
|
+
* @param page_title_uid - The title or UID of the page where the outline should be created.
|
|
18
|
+
* If not provided, today's daily page will be used.
|
|
19
|
+
* @param block_text_uid - Optional. The text content or UID of an existing block under which
|
|
20
|
+
* the outline should be inserted. If a text string is provided and
|
|
21
|
+
* no matching block is found, a new block with that text will be created
|
|
22
|
+
* on the page to serve as the parent. If a UID is provided and the block
|
|
23
|
+
* is not found, an error will be thrown.
|
|
24
|
+
*/
|
|
11
25
|
async createOutline(outline, page_title_uid, block_text_uid) {
|
|
12
26
|
// Validate input
|
|
13
27
|
if (!Array.isArray(outline) || outline.length === 0) {
|
|
@@ -210,6 +224,8 @@ export class OutlineOperations {
|
|
|
210
224
|
const markdownContent = validOutline
|
|
211
225
|
.map(item => {
|
|
212
226
|
const indent = ' '.repeat(item.level - 1);
|
|
227
|
+
// If the item text starts with a markdown heading (e.g., #, ##, ###),
|
|
228
|
+
// treat it as a direct heading without adding a bullet or outline indentation.
|
|
213
229
|
return `${indent}- ${item.text?.trim()}`;
|
|
214
230
|
})
|
|
215
231
|
.join('\n');
|
|
@@ -4,7 +4,6 @@ import { capitalizeWords } from '../helpers/text.js';
|
|
|
4
4
|
import { resolveRefs } from '../helpers/refs.js';
|
|
5
5
|
import { convertToRoamActions } from '../../markdown-utils.js';
|
|
6
6
|
export class PageOperations {
|
|
7
|
-
graph;
|
|
8
7
|
constructor(graph) {
|
|
9
8
|
this.graph = graph;
|
|
10
9
|
}
|
|
@@ -38,7 +37,7 @@ export class PageOperations {
|
|
|
38
37
|
};
|
|
39
38
|
}
|
|
40
39
|
// Extract unique page titles
|
|
41
|
-
const uniquePages =
|
|
40
|
+
const uniquePages = Array.from(new Set(results.map(([title]) => title)));
|
|
42
41
|
return {
|
|
43
42
|
success: true,
|
|
44
43
|
pages: uniquePages,
|
|
@@ -87,6 +86,7 @@ export class PageOperations {
|
|
|
87
86
|
const nodes = content.map(block => ({
|
|
88
87
|
content: block.text,
|
|
89
88
|
level: block.level,
|
|
89
|
+
...(block.heading && { heading_level: block.heading }),
|
|
90
90
|
children: []
|
|
91
91
|
}));
|
|
92
92
|
// Create hierarchical structure based on levels
|
|
@@ -126,7 +126,7 @@ export class PageOperations {
|
|
|
126
126
|
}
|
|
127
127
|
return { success: true, uid: pageUid };
|
|
128
128
|
}
|
|
129
|
-
async fetchPageByTitle(title) {
|
|
129
|
+
async fetchPageByTitle(title, format = 'raw') {
|
|
130
130
|
if (!title) {
|
|
131
131
|
throw new McpError(ErrorCode.InvalidRequest, 'title is required');
|
|
132
132
|
}
|
|
@@ -169,8 +169,26 @@ export class PageOperations {
|
|
|
169
169
|
[?parent :block/uid ?parent-uid]]`;
|
|
170
170
|
const blocks = await q(this.graph, blocksQuery, [ancestorRule, title]);
|
|
171
171
|
if (!blocks || blocks.length === 0) {
|
|
172
|
+
if (format === 'raw') {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
172
175
|
return `${title} (no content found)`;
|
|
173
176
|
}
|
|
177
|
+
// Get heading information for blocks that have it
|
|
178
|
+
const headingsQuery = `[:find ?block-uid ?heading
|
|
179
|
+
:in $ % ?page-title
|
|
180
|
+
:where [?page :node/title ?page-title]
|
|
181
|
+
[?block :block/uid ?block-uid]
|
|
182
|
+
[?block :block/heading ?heading]
|
|
183
|
+
(ancestor ?block ?page)]`;
|
|
184
|
+
const headings = await q(this.graph, headingsQuery, [ancestorRule, title]);
|
|
185
|
+
// Create a map of block UIDs to heading levels
|
|
186
|
+
const headingMap = new Map();
|
|
187
|
+
if (headings) {
|
|
188
|
+
for (const [blockUid, heading] of headings) {
|
|
189
|
+
headingMap.set(blockUid, heading);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
174
192
|
// Create a map of all blocks
|
|
175
193
|
const blockMap = new Map();
|
|
176
194
|
const rootBlocks = [];
|
|
@@ -181,6 +199,7 @@ export class PageOperations {
|
|
|
181
199
|
uid: blockUid,
|
|
182
200
|
string: resolvedString,
|
|
183
201
|
order: order,
|
|
202
|
+
heading: headingMap.get(blockUid) || null,
|
|
184
203
|
children: []
|
|
185
204
|
};
|
|
186
205
|
blockMap.set(blockUid, block);
|
|
@@ -209,16 +228,31 @@ export class PageOperations {
|
|
|
209
228
|
});
|
|
210
229
|
};
|
|
211
230
|
sortBlocks(rootBlocks);
|
|
231
|
+
if (format === 'raw') {
|
|
232
|
+
return JSON.stringify(rootBlocks);
|
|
233
|
+
}
|
|
212
234
|
// Convert to markdown with proper nesting
|
|
213
235
|
const toMarkdown = (blocks, level = 0) => {
|
|
214
|
-
return blocks
|
|
236
|
+
return blocks
|
|
237
|
+
.map(block => {
|
|
215
238
|
const indent = ' '.repeat(level);
|
|
216
|
-
let md
|
|
239
|
+
let md;
|
|
240
|
+
// Check block heading level and format accordingly
|
|
241
|
+
if (block.heading && block.heading > 0) {
|
|
242
|
+
// Format as heading with appropriate number of hashtags
|
|
243
|
+
const hashtags = '#'.repeat(block.heading);
|
|
244
|
+
md = `${indent}${hashtags} ${block.string}`;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
// No heading, use bullet point (current behavior)
|
|
248
|
+
md = `${indent}- ${block.string}`;
|
|
249
|
+
}
|
|
217
250
|
if (block.children.length > 0) {
|
|
218
251
|
md += '\n' + toMarkdown(block.children, level + 1);
|
|
219
252
|
}
|
|
220
253
|
return md;
|
|
221
|
-
})
|
|
254
|
+
})
|
|
255
|
+
.join('\n');
|
|
222
256
|
};
|
|
223
257
|
return `# ${title}\n\n${toMarkdown(rootBlocks)}`;
|
|
224
258
|
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
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
|
-
graph;
|
|
5
4
|
constructor(graph) {
|
|
6
5
|
this.graph = graph;
|
|
7
6
|
}
|
|
8
7
|
}
|
|
9
8
|
// Tag search handler
|
|
10
9
|
export class TagSearchHandlerImpl extends BaseSearchHandler {
|
|
11
|
-
params;
|
|
12
10
|
constructor(graph, params) {
|
|
13
11
|
super(graph);
|
|
14
12
|
this.params = params;
|
|
@@ -20,7 +18,6 @@ export class TagSearchHandlerImpl extends BaseSearchHandler {
|
|
|
20
18
|
}
|
|
21
19
|
// Block reference search handler
|
|
22
20
|
export class BlockRefSearchHandlerImpl extends BaseSearchHandler {
|
|
23
|
-
params;
|
|
24
21
|
constructor(graph, params) {
|
|
25
22
|
super(graph);
|
|
26
23
|
this.params = params;
|
|
@@ -32,7 +29,6 @@ export class BlockRefSearchHandlerImpl extends BaseSearchHandler {
|
|
|
32
29
|
}
|
|
33
30
|
// Hierarchy search handler
|
|
34
31
|
export class HierarchySearchHandlerImpl extends BaseSearchHandler {
|
|
35
|
-
params;
|
|
36
32
|
constructor(graph, params) {
|
|
37
33
|
super(graph);
|
|
38
34
|
this.params = params;
|
|
@@ -44,7 +40,6 @@ export class HierarchySearchHandlerImpl extends BaseSearchHandler {
|
|
|
44
40
|
}
|
|
45
41
|
// Text search handler
|
|
46
42
|
export class TextSearchHandlerImpl extends BaseSearchHandler {
|
|
47
|
-
params;
|
|
48
43
|
constructor(graph, params) {
|
|
49
44
|
super(graph);
|
|
50
45
|
this.params = params;
|
|
@@ -56,7 +51,6 @@ export class TextSearchHandlerImpl extends BaseSearchHandler {
|
|
|
56
51
|
}
|
|
57
52
|
// Status search handler
|
|
58
53
|
export class StatusSearchHandlerImpl extends BaseSearchHandler {
|
|
59
|
-
params;
|
|
60
54
|
constructor(graph, params) {
|
|
61
55
|
super(graph);
|
|
62
56
|
this.params = params;
|
|
@@ -68,7 +62,6 @@ export class StatusSearchHandlerImpl extends BaseSearchHandler {
|
|
|
68
62
|
}
|
|
69
63
|
// Datomic query handler
|
|
70
64
|
export class DatomicSearchHandlerImpl extends BaseSearchHandler {
|
|
71
|
-
params;
|
|
72
65
|
constructor(graph, params) {
|
|
73
66
|
super(graph);
|
|
74
67
|
this.params = params;
|
|
@@ -2,7 +2,6 @@ import { q, createBlock, createPage, batchActions } from '@roam-research/roam-ap
|
|
|
2
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { formatRoamDate } from '../../utils/helpers.js';
|
|
4
4
|
export class TodoOperations {
|
|
5
|
-
graph;
|
|
6
5
|
constructor(graph) {
|
|
7
6
|
this.graph = graph;
|
|
8
7
|
}
|
package/build/tools/schemas.js
CHANGED
|
@@ -19,22 +19,25 @@ export const toolSchemas = {
|
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
roam_fetch_page_by_title: {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
type: 'string',
|
|
29
|
-
description: 'Title of the page. For date pages, use ordinal date formats such as January 2nd, 2025',
|
|
30
|
-
},
|
|
22
|
+
description: 'Fetch page by title, defaults to raw JSON string.',
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
title: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Title of the page. For date pages, use ordinal date formats such as January 2nd, 2025'
|
|
31
28
|
},
|
|
32
|
-
|
|
29
|
+
format: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
enum: ['markdown', 'raw'],
|
|
32
|
+
default: 'raw',
|
|
33
|
+
description: "Format output as markdown or JSON. 'markdown' returns as string; 'raw' returns JSON string of the page's blocks"
|
|
34
|
+
}
|
|
33
35
|
},
|
|
36
|
+
required: ['title']
|
|
34
37
|
},
|
|
35
38
|
roam_create_page: {
|
|
36
39
|
name: 'roam_create_page',
|
|
37
|
-
description: 'Create
|
|
40
|
+
description: 'Create new standalone page in Roam with optional content using explicit nesting levels and headings (H1-H3). 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
41
|
inputSchema: {
|
|
39
42
|
type: 'object',
|
|
40
43
|
properties: {
|
|
@@ -57,6 +60,12 @@ export const toolSchemas = {
|
|
|
57
60
|
description: 'Indentation level (1-10, where 1 is top level)',
|
|
58
61
|
minimum: 1,
|
|
59
62
|
maximum: 10
|
|
63
|
+
},
|
|
64
|
+
heading: {
|
|
65
|
+
type: 'integer',
|
|
66
|
+
description: 'Optional: Heading formatting for this block (1-3)',
|
|
67
|
+
minimum: 1,
|
|
68
|
+
maximum: 3
|
|
60
69
|
}
|
|
61
70
|
},
|
|
62
71
|
required: ['text', 'level']
|
|
@@ -68,7 +77,7 @@ export const toolSchemas = {
|
|
|
68
77
|
},
|
|
69
78
|
roam_create_block: {
|
|
70
79
|
name: 'roam_create_block',
|
|
71
|
-
description: 'Add
|
|
80
|
+
description: 'Add 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).',
|
|
72
81
|
inputSchema: {
|
|
73
82
|
type: 'object',
|
|
74
83
|
properties: {
|
|
@@ -84,6 +93,12 @@ export const toolSchemas = {
|
|
|
84
93
|
type: 'string',
|
|
85
94
|
description: 'Optional: Title of the page to add block to (defaults to today\'s date if neither page_uid nor title provided)',
|
|
86
95
|
},
|
|
96
|
+
heading: {
|
|
97
|
+
type: 'integer',
|
|
98
|
+
description: 'Optional: Heading formatting for this block (1-3)',
|
|
99
|
+
minimum: 1,
|
|
100
|
+
maximum: 3
|
|
101
|
+
}
|
|
87
102
|
},
|
|
88
103
|
required: ['content'],
|
|
89
104
|
},
|
|
@@ -100,7 +115,7 @@ export const toolSchemas = {
|
|
|
100
115
|
},
|
|
101
116
|
block_text_uid: {
|
|
102
117
|
type: 'string',
|
|
103
|
-
description: 'A relevant title heading for the outline (or UID, if known) of the block under which outline content will be nested. If blank, content will be nested under the page
|
|
118
|
+
description: 'A relevant title heading for the outline (or UID, if known) of the block under which outline content will be nested. If blank, content will be nested directly under the page. This can be either the text content of the block or its UID.'
|
|
104
119
|
},
|
|
105
120
|
outline: {
|
|
106
121
|
type: 'array',
|
|
@@ -117,6 +132,12 @@ export const toolSchemas = {
|
|
|
117
132
|
description: 'Indentation level (1-10, where 1 is top level)',
|
|
118
133
|
minimum: 1,
|
|
119
134
|
maximum: 10
|
|
135
|
+
},
|
|
136
|
+
heading: {
|
|
137
|
+
type: 'integer',
|
|
138
|
+
description: 'Optional: Heading formatting for this block (1-3)',
|
|
139
|
+
minimum: 1,
|
|
140
|
+
maximum: 3
|
|
120
141
|
}
|
|
121
142
|
},
|
|
122
143
|
required: ['text', 'level']
|
|
@@ -398,7 +419,7 @@ export const toolSchemas = {
|
|
|
398
419
|
scope: {
|
|
399
420
|
type: 'string',
|
|
400
421
|
enum: ['blocks', 'pages', 'both'],
|
|
401
|
-
description: 'Whether to search blocks, pages
|
|
422
|
+
description: 'Whether to search blocks, pages',
|
|
402
423
|
},
|
|
403
424
|
include_content: {
|
|
404
425
|
type: 'boolean',
|
|
@@ -6,13 +6,6 @@ import { TodoOperations } from './operations/todos.js';
|
|
|
6
6
|
import { OutlineOperations } from './operations/outline.js';
|
|
7
7
|
import { DatomicSearchHandlerImpl } from './operations/search/handlers.js';
|
|
8
8
|
export class ToolHandlers {
|
|
9
|
-
graph;
|
|
10
|
-
pageOps;
|
|
11
|
-
blockOps;
|
|
12
|
-
searchOps;
|
|
13
|
-
memoryOps;
|
|
14
|
-
todoOps;
|
|
15
|
-
outlineOps;
|
|
16
9
|
constructor(graph) {
|
|
17
10
|
this.graph = graph;
|
|
18
11
|
this.pageOps = new PageOperations(graph);
|
|
@@ -29,12 +22,12 @@ export class ToolHandlers {
|
|
|
29
22
|
async createPage(title, content) {
|
|
30
23
|
return this.pageOps.createPage(title, content);
|
|
31
24
|
}
|
|
32
|
-
async fetchPageByTitle(title) {
|
|
33
|
-
return this.pageOps.fetchPageByTitle(title);
|
|
25
|
+
async fetchPageByTitle(title, format) {
|
|
26
|
+
return this.pageOps.fetchPageByTitle(title, format);
|
|
34
27
|
}
|
|
35
28
|
// Block Operations
|
|
36
|
-
async createBlock(content, page_uid, title) {
|
|
37
|
-
return this.blockOps.createBlock(content, page_uid, title);
|
|
29
|
+
async createBlock(content, page_uid, title, heading) {
|
|
30
|
+
return this.blockOps.createBlock(content, page_uid, title, heading);
|
|
38
31
|
}
|
|
39
32
|
async updateBlock(block_uid, content, transform) {
|
|
40
33
|
return this.blockOps.updateBlock(block_uid, content, transform);
|