roam-research-mcp 0.25.0 → 0.25.3
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.
|
@@ -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.25.
|
|
20
|
+
version: '0.25.3',
|
|
21
21
|
}, {
|
|
22
22
|
capabilities: {
|
|
23
23
|
tools: {
|
|
@@ -81,8 +81,8 @@ export class RoamServer {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
case 'roam_create_block': {
|
|
84
|
-
const { content, page_uid, title } = request.params.arguments;
|
|
85
|
-
const result = await this.toolHandlers.createBlock(content, page_uid, title);
|
|
84
|
+
const { content, page_uid, title, heading } = request.params.arguments;
|
|
85
|
+
const result = await this.toolHandlers.createBlock(content, page_uid, title, heading);
|
|
86
86
|
return {
|
|
87
87
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
88
88
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
@@ -7,7 +7,7 @@ export class BlockOperations {
|
|
|
7
7
|
constructor(graph) {
|
|
8
8
|
this.graph = graph;
|
|
9
9
|
}
|
|
10
|
-
async createBlock(content, page_uid, title) {
|
|
10
|
+
async createBlock(content, page_uid, title, heading) {
|
|
11
11
|
// If page_uid provided, use it directly
|
|
12
12
|
let targetPageUid = page_uid;
|
|
13
13
|
// If no page_uid but title provided, search for page by title
|
|
@@ -68,9 +68,44 @@ export class BlockOperations {
|
|
|
68
68
|
try {
|
|
69
69
|
// If the content has multiple lines or is a table, use nested import
|
|
70
70
|
if (content.includes('\n')) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
let nodes;
|
|
72
|
+
// If heading parameter is provided, manually construct nodes to preserve heading
|
|
73
|
+
if (heading) {
|
|
74
|
+
const lines = content.split('\n');
|
|
75
|
+
const firstLine = lines[0].trim();
|
|
76
|
+
const remainingLines = lines.slice(1);
|
|
77
|
+
// Create the first node with heading formatting
|
|
78
|
+
const firstNode = {
|
|
79
|
+
content: firstLine,
|
|
80
|
+
level: 0,
|
|
81
|
+
heading_level: heading,
|
|
82
|
+
children: []
|
|
83
|
+
};
|
|
84
|
+
// If there are remaining lines, parse them as children or siblings
|
|
85
|
+
if (remainingLines.length > 0 && remainingLines.some(line => line.trim())) {
|
|
86
|
+
const remainingContent = remainingLines.join('\n');
|
|
87
|
+
const convertedRemainingContent = convertToRoamMarkdown(remainingContent);
|
|
88
|
+
const remainingNodes = parseMarkdown(convertedRemainingContent);
|
|
89
|
+
// Add remaining nodes as siblings to the first node
|
|
90
|
+
nodes = [firstNode, ...remainingNodes];
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
nodes = [firstNode];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// No heading parameter, use original parsing logic
|
|
98
|
+
const convertedContent = convertToRoamMarkdown(content);
|
|
99
|
+
nodes = parseMarkdown(convertedContent);
|
|
100
|
+
// If we have simple newline-separated content (no markdown formatting),
|
|
101
|
+
// and parseMarkdown created nodes at level 0, reverse them to maintain original order
|
|
102
|
+
if (nodes.every(node => node.level === 0 && !node.heading_level)) {
|
|
103
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
104
|
+
if (lines.length === nodes.length) {
|
|
105
|
+
nodes.reverse();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
74
109
|
const actions = convertToRoamActions(nodes, targetPageUid, 'last');
|
|
75
110
|
// Execute batch actions to create the nested structure
|
|
76
111
|
const result = await batchActions(this.graph, {
|
|
@@ -88,27 +123,26 @@ export class BlockOperations {
|
|
|
88
123
|
};
|
|
89
124
|
}
|
|
90
125
|
else {
|
|
91
|
-
// For
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
126
|
+
// For single block content, use the same convertToRoamActions approach that works in roam_create_page
|
|
127
|
+
const nodes = [{
|
|
128
|
+
content: content,
|
|
129
|
+
level: 0,
|
|
130
|
+
...(heading && typeof heading === 'number' && heading > 0 && { heading_level: heading }),
|
|
131
|
+
children: []
|
|
132
|
+
}];
|
|
133
|
+
if (!targetPageUid) {
|
|
134
|
+
throw new McpError(ErrorCode.InternalError, 'targetPageUid is undefined');
|
|
135
|
+
}
|
|
136
|
+
const actions = convertToRoamActions(nodes, targetPageUid, 'last');
|
|
137
|
+
// Execute batch actions to create the block
|
|
138
|
+
const result = await batchActions(this.graph, {
|
|
139
|
+
action: 'batch-actions',
|
|
140
|
+
actions
|
|
99
141
|
});
|
|
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');
|
|
142
|
+
if (!result) {
|
|
143
|
+
throw new Error('Failed to create block');
|
|
110
144
|
}
|
|
111
|
-
const blockUid =
|
|
145
|
+
const blockUid = result.created_uids?.[0];
|
|
112
146
|
return {
|
|
113
147
|
success: true,
|
|
114
148
|
block_uid: blockUid,
|
|
@@ -87,6 +87,7 @@ export class PageOperations {
|
|
|
87
87
|
const nodes = content.map(block => ({
|
|
88
88
|
content: block.text,
|
|
89
89
|
level: block.level,
|
|
90
|
+
...(block.heading && { heading_level: block.heading }),
|
|
90
91
|
children: []
|
|
91
92
|
}));
|
|
92
93
|
// Create hierarchical structure based on levels
|
|
@@ -171,6 +172,21 @@ export class PageOperations {
|
|
|
171
172
|
if (!blocks || blocks.length === 0) {
|
|
172
173
|
return `${title} (no content found)`;
|
|
173
174
|
}
|
|
175
|
+
// Get heading information for blocks that have it
|
|
176
|
+
const headingsQuery = `[:find ?block-uid ?heading
|
|
177
|
+
:in $ % ?page-title
|
|
178
|
+
:where [?page :node/title ?page-title]
|
|
179
|
+
[?block :block/uid ?block-uid]
|
|
180
|
+
[?block :block/heading ?heading]
|
|
181
|
+
(ancestor ?block ?page)]`;
|
|
182
|
+
const headings = await q(this.graph, headingsQuery, [ancestorRule, title]);
|
|
183
|
+
// Create a map of block UIDs to heading levels
|
|
184
|
+
const headingMap = new Map();
|
|
185
|
+
if (headings) {
|
|
186
|
+
for (const [blockUid, heading] of headings) {
|
|
187
|
+
headingMap.set(blockUid, heading);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
174
190
|
// Create a map of all blocks
|
|
175
191
|
const blockMap = new Map();
|
|
176
192
|
const rootBlocks = [];
|
|
@@ -181,6 +197,7 @@ export class PageOperations {
|
|
|
181
197
|
uid: blockUid,
|
|
182
198
|
string: resolvedString,
|
|
183
199
|
order: order,
|
|
200
|
+
heading: headingMap.get(blockUid) || null,
|
|
184
201
|
children: []
|
|
185
202
|
};
|
|
186
203
|
blockMap.set(blockUid, block);
|
|
@@ -213,7 +230,17 @@ export class PageOperations {
|
|
|
213
230
|
const toMarkdown = (blocks, level = 0) => {
|
|
214
231
|
return blocks.map(block => {
|
|
215
232
|
const indent = ' '.repeat(level);
|
|
216
|
-
let md
|
|
233
|
+
let md;
|
|
234
|
+
// Check block heading level and format accordingly
|
|
235
|
+
if (block.heading && block.heading > 0) {
|
|
236
|
+
// Format as heading with appropriate number of hashtags
|
|
237
|
+
const hashtags = '#'.repeat(block.heading);
|
|
238
|
+
md = `${indent}${hashtags} ${block.string}`;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// No heading, use bullet point (current behavior)
|
|
242
|
+
md = `${indent}- ${block.string}`;
|
|
243
|
+
}
|
|
217
244
|
if (block.children.length > 0) {
|
|
218
245
|
md += '\n' + toMarkdown(block.children, level + 1);
|
|
219
246
|
}
|
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
|
|
37
|
+
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
38
|
inputSchema: {
|
|
39
39
|
type: 'object',
|
|
40
40
|
properties: {
|
|
@@ -57,6 +57,12 @@ export const toolSchemas = {
|
|
|
57
57
|
description: 'Indentation level (1-10, where 1 is top level)',
|
|
58
58
|
minimum: 1,
|
|
59
59
|
maximum: 10
|
|
60
|
+
},
|
|
61
|
+
heading: {
|
|
62
|
+
type: 'integer',
|
|
63
|
+
description: 'Optional: Heading formatting for this block (1-3)',
|
|
64
|
+
minimum: 1,
|
|
65
|
+
maximum: 3
|
|
60
66
|
}
|
|
61
67
|
},
|
|
62
68
|
required: ['text', 'level']
|
|
@@ -68,7 +74,7 @@ export const toolSchemas = {
|
|
|
68
74
|
},
|
|
69
75
|
roam_create_block: {
|
|
70
76
|
name: 'roam_create_block',
|
|
71
|
-
description: 'Add
|
|
77
|
+
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
78
|
inputSchema: {
|
|
73
79
|
type: 'object',
|
|
74
80
|
properties: {
|
|
@@ -84,6 +90,12 @@ export const toolSchemas = {
|
|
|
84
90
|
type: 'string',
|
|
85
91
|
description: 'Optional: Title of the page to add block to (defaults to today\'s date if neither page_uid nor title provided)',
|
|
86
92
|
},
|
|
93
|
+
heading: {
|
|
94
|
+
type: 'integer',
|
|
95
|
+
description: 'Optional: Heading formatting for this block (1-3)',
|
|
96
|
+
minimum: 1,
|
|
97
|
+
maximum: 3
|
|
98
|
+
}
|
|
87
99
|
},
|
|
88
100
|
required: ['content'],
|
|
89
101
|
},
|
|
@@ -33,8 +33,8 @@ export class ToolHandlers {
|
|
|
33
33
|
return this.pageOps.fetchPageByTitle(title);
|
|
34
34
|
}
|
|
35
35
|
// Block Operations
|
|
36
|
-
async createBlock(content, page_uid, title) {
|
|
37
|
-
return this.blockOps.createBlock(content, page_uid, title);
|
|
36
|
+
async createBlock(content, page_uid, title, heading) {
|
|
37
|
+
return this.blockOps.createBlock(content, page_uid, title, heading);
|
|
38
38
|
}
|
|
39
39
|
async updateBlock(block_uid, content, transform) {
|
|
40
40
|
return this.blockOps.updateBlock(block_uid, content, transform);
|