roam-research-mcp 0.32.4 → 0.35.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 CHANGED
@@ -351,3 +351,9 @@ This will:
351
351
  ## License
352
352
 
353
353
  MIT License
354
+
355
+ ---
356
+
357
+ ## About the Author
358
+
359
+ This project is maintained by [Ian Shen](https://github.com/2b3pro).
@@ -15,6 +15,7 @@
15
15
  - {{[[TODO]]}} todo text
16
16
  - {{[[DONE]]}} todo text
17
17
  - LaTeX: `$$E=mc^2$$` or `$$\sum_{i=1}^{n} i = \frac{n(n+1)}{2}$$`
18
+ - Bullet points use dashes not asterisks.
18
19
 
19
20
  ## Roam-specific Markdown:
20
21
 
@@ -262,13 +262,14 @@ function convertToRoamActions(nodes, parentUid, order = 'last') {
262
262
  const actions = [];
263
263
  // Helper function to recursively create actions
264
264
  function createBlockActions(blocks, parentUid, order) {
265
- for (const block of blocks) {
265
+ for (let i = 0; i < blocks.length; i++) {
266
+ const block = blocks[i];
266
267
  // Create the current block
267
268
  const action = {
268
269
  action: 'create-block',
269
270
  location: {
270
271
  'parent-uid': parentUid,
271
- order
272
+ order: typeof order === 'number' ? order + i : i
272
273
  },
273
274
  block: {
274
275
  uid: block.uid,
@@ -1,6 +1,65 @@
1
+ /**
2
+ * Capitalizes each word in a string
3
+ */
4
+ import { q } from '@roam-research/roam-api-sdk';
1
5
  /**
2
6
  * Capitalizes each word in a string
3
7
  */
4
8
  export const capitalizeWords = (str) => {
5
9
  return str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
6
10
  };
11
+ /**
12
+ * Retrieves a block's UID based on its exact text content.
13
+ * This function is intended for internal use by other MCP tools.
14
+ * @param graph The Roam graph instance.
15
+ * @param blockText The exact text content of the block to find.
16
+ * @returns The UID of the block if found, otherwise null.
17
+ */
18
+ export const getBlockUidByText = async (graph, blockText) => {
19
+ const query = `[:find ?uid .
20
+ :in $ ?blockString
21
+ :where [?b :block/string ?blockString]
22
+ [?b :block/uid ?uid]]`;
23
+ const result = await q(graph, query, [blockText]);
24
+ return result && result.length > 0 ? result[0][0] : null;
25
+ };
26
+ /**
27
+ * Retrieves all UIDs nested under a given block_uid or block_text (exact match).
28
+ * This function is intended for internal use by other MCP tools.
29
+ * @param graph The Roam graph instance.
30
+ * @param rootIdentifier The UID or exact text content of the root block.
31
+ * @returns An array of UIDs of all descendant blocks, including the root block's UID.
32
+ */
33
+ export const getNestedUids = async (graph, rootIdentifier) => {
34
+ let rootUid = rootIdentifier;
35
+ // If the rootIdentifier is not a UID (simple check for 9 alphanumeric characters), try to resolve it as block text
36
+ if (!rootIdentifier.match(/^[a-zA-Z0-9]{9}$/)) {
37
+ rootUid = await getBlockUidByText(graph, rootIdentifier);
38
+ }
39
+ if (!rootUid) {
40
+ return []; // No root block found
41
+ }
42
+ const query = `[:find ?child-uid
43
+ :in $ ?root-uid
44
+ :where
45
+ [?root-block :block/uid ?root-uid]
46
+ [?root-block :block/children ?child-block]
47
+ [?child-block :block/uid ?child-uid]]`;
48
+ const results = await q(graph, query, [rootUid]);
49
+ return results.map(r => r[0]);
50
+ };
51
+ /**
52
+ * Retrieves all UIDs nested under a given block_text (exact match).
53
+ * This function is intended for internal use by other MCP tools.
54
+ * It strictly requires an exact text match for the root block.
55
+ * @param graph The Roam graph instance.
56
+ * @param blockText The exact text content of the root block.
57
+ * @returns An array of UIDs of all descendant blocks, including the root block's UID.
58
+ */
59
+ export const getNestedUidsByText = async (graph, blockText) => {
60
+ const rootUid = await getBlockUidByText(graph, blockText);
61
+ if (!rootUid) {
62
+ return []; // No root block found with exact text match
63
+ }
64
+ return getNestedUids(graph, rootUid);
65
+ };
@@ -16,7 +16,7 @@ export class BlockRetrievalOperations {
16
16
  const childrenQuery = `[:find ?parentUid ?childUid ?childString ?childOrder ?childHeading
17
17
  :in $ [?parentUid ...]
18
18
  :where [?parent :block/uid ?parentUid]
19
- [?child :block/parents ?parent]
19
+ [?parent :block/children ?child]
20
20
  [?child :block/uid ?childUid]
21
21
  [?child :block/string ?childString]
22
22
  [?child :block/order ?childOrder]
@@ -6,6 +6,188 @@ import { parseMarkdown, convertToRoamActions, convertToRoamMarkdown } from '../.
6
6
  export class OutlineOperations {
7
7
  constructor(graph) {
8
8
  this.graph = graph;
9
+ /**
10
+ * Helper function to check if string is a valid Roam UID (9 characters)
11
+ */
12
+ this.isValidUid = (str) => {
13
+ return typeof str === 'string' && str.length === 9;
14
+ };
15
+ }
16
+ /**
17
+ * Helper function to find block with improved relationship checks
18
+ */
19
+ async findBlockWithRetry(pageUid, blockString, maxRetries = 5, initialDelay = 1000) {
20
+ // Try multiple query strategies
21
+ const queries = [
22
+ // Strategy 1: Direct page and string match
23
+ `[:find ?b-uid ?order
24
+ :where [?p :block/uid "${pageUid}"]
25
+ [?b :block/page ?p]
26
+ [?b :block/string "${blockString}"]
27
+ [?b :block/order ?order]
28
+ [?b :block/uid ?b-uid]]`,
29
+ // Strategy 2: Parent-child relationship
30
+ `[:find ?b-uid ?order
31
+ :where [?p :block/uid "${pageUid}"]
32
+ [?b :block/parents ?p]
33
+ [?b :block/string "${blockString}"]
34
+ [?b :block/order ?order]
35
+ [?b :block/uid ?b-uid]]`,
36
+ // Strategy 3: Broader page relationship
37
+ `[:find ?b-uid ?order
38
+ :where [?p :block/uid "${pageUid}"]
39
+ [?b :block/page ?page]
40
+ [?p :block/page ?page]
41
+ [?b :block/string "${blockString}"]
42
+ [?b :block/order ?order]
43
+ [?b :block/uid ?b-uid]]`
44
+ ];
45
+ for (let retry = 0; retry < maxRetries; retry++) {
46
+ // Try each query strategy
47
+ for (const queryStr of queries) {
48
+ const blockResults = await q(this.graph, queryStr, []);
49
+ if (blockResults && blockResults.length > 0) {
50
+ // Use the most recently created block
51
+ const sorted = blockResults.sort((a, b) => b[1] - a[1]);
52
+ return sorted[0][0];
53
+ }
54
+ }
55
+ // Exponential backoff
56
+ const delay = initialDelay * Math.pow(2, retry);
57
+ await new Promise(resolve => setTimeout(resolve, delay));
58
+ }
59
+ throw new McpError(ErrorCode.InternalError, `Failed to find block "${blockString}" under page "${pageUid}" after trying multiple strategies`);
60
+ }
61
+ ;
62
+ /**
63
+ * Helper function to create and verify block with improved error handling
64
+ */
65
+ async createAndVerifyBlock(content, parentUid, maxRetries = 5, initialDelay = 1000, isRetry = false) {
66
+ try {
67
+ // Initial delay before any operations
68
+ if (!isRetry) {
69
+ await new Promise(resolve => setTimeout(resolve, initialDelay));
70
+ }
71
+ for (let retry = 0; retry < maxRetries; retry++) {
72
+ console.log(`Attempt ${retry + 1}/${maxRetries} to create block "${content}" under "${parentUid}"`);
73
+ // Create block using batchActions
74
+ const batchResult = await batchActions(this.graph, {
75
+ action: 'batch-actions',
76
+ actions: [{
77
+ action: 'create-block',
78
+ location: {
79
+ 'parent-uid': parentUid,
80
+ order: 'last'
81
+ },
82
+ block: { string: content }
83
+ }]
84
+ });
85
+ if (!batchResult) {
86
+ throw new McpError(ErrorCode.InternalError, `Failed to create block "${content}" via batch action`);
87
+ }
88
+ // Wait with exponential backoff
89
+ const delay = initialDelay * Math.pow(2, retry);
90
+ await new Promise(resolve => setTimeout(resolve, delay));
91
+ try {
92
+ // Try to find the block using our improved findBlockWithRetry
93
+ return await this.findBlockWithRetry(parentUid, content);
94
+ }
95
+ catch (error) {
96
+ const errorMessage = error instanceof Error ? error.message : String(error);
97
+ // console.log(`Failed to find block on attempt ${retry + 1}: ${errorMessage}`); // Removed console.log
98
+ if (retry === maxRetries - 1)
99
+ throw error;
100
+ }
101
+ }
102
+ throw new McpError(ErrorCode.InternalError, `Failed to create and verify block "${content}" after ${maxRetries} attempts`);
103
+ }
104
+ catch (error) {
105
+ // If this is already a retry, throw the error
106
+ if (isRetry)
107
+ throw error;
108
+ // Otherwise, try one more time with a clean slate
109
+ // console.log(`Retrying block creation for "${content}" with fresh attempt`); // Removed console.log
110
+ await new Promise(resolve => setTimeout(resolve, initialDelay * 2));
111
+ return this.createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true);
112
+ }
113
+ }
114
+ ;
115
+ /**
116
+ * Helper function to fetch a block and its children recursively
117
+ */
118
+ async fetchBlockWithChildren(blockUid, level = 1) {
119
+ const query = `
120
+ [:find ?childUid ?childString ?childOrder
121
+ :in $ ?parentUid
122
+ :where
123
+ [?parentEntity :block/uid ?parentUid]
124
+ [?parentEntity :block/children ?childEntity] ; This ensures direct children
125
+ [?childEntity :block/uid ?childUid]
126
+ [?childEntity :block/string ?childString]
127
+ [?childEntity :block/order ?childOrder]]
128
+ `;
129
+ const blockQuery = `
130
+ [:find ?string
131
+ :in $ ?uid
132
+ :where
133
+ [?e :block/uid ?uid]
134
+ [?e :block/string ?string]]
135
+ `;
136
+ try {
137
+ const blockStringResult = await q(this.graph, blockQuery, [blockUid]);
138
+ if (!blockStringResult || blockStringResult.length === 0) {
139
+ return null;
140
+ }
141
+ const text = blockStringResult[0][0];
142
+ const childrenResults = await q(this.graph, query, [blockUid]);
143
+ const children = [];
144
+ if (childrenResults && childrenResults.length > 0) {
145
+ // Sort children by order
146
+ const sortedChildren = childrenResults.sort((a, b) => a[2] - b[2]);
147
+ for (const childResult of sortedChildren) {
148
+ const childUid = childResult[0];
149
+ const nestedChild = await this.fetchBlockWithChildren(childUid, level + 1);
150
+ if (nestedChild) {
151
+ children.push(nestedChild);
152
+ }
153
+ }
154
+ }
155
+ // The order of the root block is not available from this query, so we set it to 0
156
+ return { uid: blockUid, text, level, order: 0, children: children.length > 0 ? children : undefined };
157
+ }
158
+ catch (error) {
159
+ throw new McpError(ErrorCode.InternalError, `Failed to fetch block with children for UID "${blockUid}": ${error.message}`);
160
+ }
161
+ }
162
+ ;
163
+ /**
164
+ * Recursively fetches a nested structure of blocks under a given root block UID.
165
+ */
166
+ async fetchNestedStructure(rootUid) {
167
+ const query = `[:find ?child-uid ?child-string ?child-order
168
+ :in $ ?parent-uid
169
+ :where
170
+ [?parent :block/uid ?parent-uid]
171
+ [?parent :block/children ?child]
172
+ [?child :block/uid ?child-uid]
173
+ [?child :block/string ?child-string]
174
+ [?child :block/order ?child-order]]`;
175
+ const directChildrenResult = await q(this.graph, query, [rootUid]);
176
+ if (directChildrenResult.length === 0) {
177
+ return [];
178
+ }
179
+ const nestedBlocks = [];
180
+ for (const [childUid, childString, childOrder] of directChildrenResult) {
181
+ const children = await this.fetchNestedStructure(childUid);
182
+ nestedBlocks.push({
183
+ uid: childUid,
184
+ text: childString,
185
+ level: 0, // Level is not easily determined here, so we set it to 0
186
+ children: children,
187
+ order: childOrder
188
+ });
189
+ }
190
+ return nestedBlocks.sort((a, b) => a.order - b.order);
9
191
  }
10
192
  /**
11
193
  * Creates an outline structure on a Roam Research page, optionally under a specific block.
@@ -86,147 +268,6 @@ export class OutlineOperations {
86
268
  };
87
269
  // Get or create the target page
88
270
  const targetPageUid = await findOrCreatePage(page_title_uid || formatRoamDate(new Date()));
89
- // Helper function to find block with improved relationship checks
90
- const findBlockWithRetry = async (pageUid, blockString, maxRetries = 5, initialDelay = 1000) => {
91
- // Try multiple query strategies
92
- const queries = [
93
- // Strategy 1: Direct page and string match
94
- `[:find ?b-uid ?order
95
- :where [?p :block/uid "${pageUid}"]
96
- [?b :block/page ?p]
97
- [?b :block/string "${blockString}"]
98
- [?b :block/order ?order]
99
- [?b :block/uid ?b-uid]]`,
100
- // Strategy 2: Parent-child relationship
101
- `[:find ?b-uid ?order
102
- :where [?p :block/uid "${pageUid}"]
103
- [?b :block/parents ?p]
104
- [?b :block/string "${blockString}"]
105
- [?b :block/order ?order]
106
- [?b :block/uid ?b-uid]]`,
107
- // Strategy 3: Broader page relationship
108
- `[:find ?b-uid ?order
109
- :where [?p :block/uid "${pageUid}"]
110
- [?b :block/page ?page]
111
- [?p :block/page ?page]
112
- [?b :block/string "${blockString}"]
113
- [?b :block/order ?order]
114
- [?b :block/uid ?b-uid]]`
115
- ];
116
- for (let retry = 0; retry < maxRetries; retry++) {
117
- // Try each query strategy
118
- for (const queryStr of queries) {
119
- const blockResults = await q(this.graph, queryStr, []);
120
- if (blockResults && blockResults.length > 0) {
121
- // Use the most recently created block
122
- const sorted = blockResults.sort((a, b) => b[1] - a[1]);
123
- return sorted[0][0];
124
- }
125
- }
126
- // Exponential backoff
127
- const delay = initialDelay * Math.pow(2, retry);
128
- await new Promise(resolve => setTimeout(resolve, delay));
129
- }
130
- throw new McpError(ErrorCode.InternalError, `Failed to find block "${blockString}" under page "${pageUid}" after trying multiple strategies`);
131
- };
132
- // Helper function to create and verify block with improved error handling
133
- const createAndVerifyBlock = async (content, parentUid, maxRetries = 5, initialDelay = 1000, isRetry = false) => {
134
- try {
135
- // Initial delay before any operations
136
- if (!isRetry) {
137
- await new Promise(resolve => setTimeout(resolve, initialDelay));
138
- }
139
- for (let retry = 0; retry < maxRetries; retry++) {
140
- console.log(`Attempt ${retry + 1}/${maxRetries} to create block "${content}" under "${parentUid}"`);
141
- // Create block using batchActions
142
- const batchResult = await batchActions(this.graph, {
143
- action: 'batch-actions',
144
- actions: [{
145
- action: 'create-block',
146
- location: {
147
- 'parent-uid': parentUid,
148
- order: 'last'
149
- },
150
- block: { string: content }
151
- }]
152
- });
153
- if (!batchResult) {
154
- throw new McpError(ErrorCode.InternalError, `Failed to create block "${content}" via batch action`);
155
- }
156
- // Wait with exponential backoff
157
- const delay = initialDelay * Math.pow(2, retry);
158
- await new Promise(resolve => setTimeout(resolve, delay));
159
- try {
160
- // Try to find the block using our improved findBlockWithRetry
161
- return await findBlockWithRetry(parentUid, content);
162
- }
163
- catch (error) {
164
- const errorMessage = error instanceof Error ? error.message : String(error);
165
- // console.log(`Failed to find block on attempt ${retry + 1}: ${errorMessage}`); // Removed console.log
166
- if (retry === maxRetries - 1)
167
- throw error;
168
- }
169
- }
170
- throw new McpError(ErrorCode.InternalError, `Failed to create and verify block "${content}" after ${maxRetries} attempts`);
171
- }
172
- catch (error) {
173
- // If this is already a retry, throw the error
174
- if (isRetry)
175
- throw error;
176
- // Otherwise, try one more time with a clean slate
177
- // console.log(`Retrying block creation for "${content}" with fresh attempt`); // Removed console.log
178
- await new Promise(resolve => setTimeout(resolve, initialDelay * 2));
179
- return createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true);
180
- }
181
- };
182
- // Helper function to check if string is a valid Roam UID (9 characters)
183
- const isValidUid = (str) => {
184
- return typeof str === 'string' && str.length === 9;
185
- };
186
- // Helper function to fetch a block and its children recursively
187
- const fetchBlockWithChildren = async (blockUid, level = 1) => {
188
- const query = `
189
- [:find ?childUid ?childString ?childOrder
190
- :in $ ?parentUid
191
- :where
192
- [?parentEntity :block/uid ?parentUid]
193
- [?parentEntity :block/children ?childEntity] ; This ensures direct children
194
- [?childEntity :block/uid ?childUid]
195
- [?childEntity :block/string ?childString]
196
- [?childEntity :block/order ?childOrder]]
197
- `;
198
- const blockQuery = `
199
- [:find ?string
200
- :in $ ?uid
201
- :where
202
- [?e :block/uid ?uid]
203
- [?e :block/string ?string]]
204
- `;
205
- try {
206
- const blockStringResult = await q(this.graph, blockQuery, [blockUid]);
207
- if (!blockStringResult || blockStringResult.length === 0) {
208
- return null;
209
- }
210
- const text = blockStringResult[0][0];
211
- const childrenResults = await q(this.graph, query, [blockUid]);
212
- const children = [];
213
- if (childrenResults && childrenResults.length > 0) {
214
- // Sort children by order
215
- const sortedChildren = childrenResults.sort((a, b) => a[2] - b[2]);
216
- for (const childResult of sortedChildren) {
217
- const childUid = childResult[0];
218
- const nestedChild = await fetchBlockWithChildren(childUid, level + 1);
219
- if (nestedChild) {
220
- children.push(nestedChild);
221
- }
222
- }
223
- }
224
- return { uid: blockUid, text, level, children: children.length > 0 ? children : undefined };
225
- }
226
- catch (error) {
227
- throw new McpError(ErrorCode.InternalError, `Failed to fetch block with children for UID "${blockUid}": ${error.message}`);
228
- }
229
- };
230
271
  // Get or create the parent block
231
272
  let targetParentUid;
232
273
  if (!block_text_uid) {
@@ -234,7 +275,7 @@ export class OutlineOperations {
234
275
  }
235
276
  else {
236
277
  try {
237
- if (isValidUid(block_text_uid)) {
278
+ if (this.isValidUid(block_text_uid)) {
238
279
  // First try to find block by UID
239
280
  const uidQuery = `[:find ?uid
240
281
  :where [?e :block/uid "${block_text_uid}"]
@@ -250,12 +291,12 @@ export class OutlineOperations {
250
291
  }
251
292
  else {
252
293
  // Create header block and get its UID if not a valid UID
253
- targetParentUid = await createAndVerifyBlock(block_text_uid, targetPageUid);
294
+ targetParentUid = await this.createAndVerifyBlock(block_text_uid, targetPageUid);
254
295
  }
255
296
  }
256
297
  catch (error) {
257
298
  const errorMessage = error instanceof Error ? error.message : String(error);
258
- throw new McpError(ErrorCode.InternalError, `Failed to ${isValidUid(block_text_uid) ? 'find' : 'create'} block "${block_text_uid}": ${errorMessage}`);
299
+ throw new McpError(ErrorCode.InternalError, `Failed to ${this.isValidUid(block_text_uid) ? 'find' : 'create'} block "${block_text_uid}": ${errorMessage}`);
259
300
  }
260
301
  }
261
302
  // Initialize result variable
@@ -325,9 +366,9 @@ export class OutlineOperations {
325
366
  for (const item of topLevelOutlineItems) {
326
367
  try {
327
368
  // Assert item.text is a string as it's filtered earlier to be non-undefined and non-empty
328
- const foundUid = await findBlockWithRetry(targetParentUid, item.text);
369
+ const foundUid = await this.findBlockWithRetry(targetParentUid, item.text);
329
370
  if (foundUid) {
330
- const nestedBlock = await fetchBlockWithChildren(foundUid);
371
+ const nestedBlock = await this.fetchBlockWithChildren(foundUid);
331
372
  if (nestedBlock) {
332
373
  createdBlocks.push(nestedBlock);
333
374
  }
@@ -346,7 +387,7 @@ export class OutlineOperations {
346
387
  created_uids: createdBlocks
347
388
  };
348
389
  }
349
- async importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order = 'first') {
390
+ async importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order = 'last') {
350
391
  // First get the page UID
351
392
  let targetPageUid = page_uid;
352
393
  if (!targetPageUid && page_title) {
@@ -393,15 +434,20 @@ export class OutlineOperations {
393
434
  throw new McpError(ErrorCode.InvalidRequest, 'Must provide either page_uid or page_title when using parent_string');
394
435
  }
395
436
  // Find block by exact string match within the page
396
- const findBlockQuery = `[:find ?uid
397
- :where [?p :block/uid "${targetPageUid}"]
437
+ const findBlockQuery = `[:find ?b-uid
438
+ :in $ ?page-uid ?block-string
439
+ :where [?p :block/uid ?page-uid]
398
440
  [?b :block/page ?p]
399
- [?b :block/string "${parent_string}"]]`;
400
- const blockResults = await q(this.graph, findBlockQuery, []);
401
- if (!blockResults || blockResults.length === 0) {
402
- throw new McpError(ErrorCode.InvalidRequest, `Block with content "${parent_string}" not found on specified page`);
441
+ [?b :block/string ?block-string]
442
+ [?b :block/uid ?b-uid]]`;
443
+ const blockResults = await q(this.graph, findBlockQuery, [targetPageUid, parent_string]);
444
+ if (blockResults && blockResults.length > 0) {
445
+ targetParentUid = blockResults[0][0];
446
+ }
447
+ else {
448
+ // If parent_string block doesn't exist, create it
449
+ targetParentUid = await this.createAndVerifyBlock(parent_string, targetPageUid);
403
450
  }
404
- targetParentUid = blockResults[0][0];
405
451
  }
406
452
  // If no parent specified, use page as parent
407
453
  if (!targetParentUid) {
@@ -423,8 +469,8 @@ export class OutlineOperations {
423
469
  if (!result) {
424
470
  throw new McpError(ErrorCode.InternalError, 'Failed to import nested markdown content');
425
471
  }
426
- // Get the created block UIDs
427
- const createdUids = result.created_uids || [];
472
+ // After successful batch action, get all nested UIDs under the parent
473
+ const createdUids = await this.fetchNestedStructure(targetParentUid);
428
474
  return {
429
475
  success: true,
430
476
  page_uid: targetPageUid,
@@ -438,26 +484,42 @@ export class OutlineOperations {
438
484
  action: 'create-block',
439
485
  location: {
440
486
  "parent-uid": targetParentUid,
441
- order
487
+ "order": order
442
488
  },
443
489
  block: { string: content }
444
490
  }];
445
491
  try {
446
- const result = await batchActions(this.graph, {
492
+ await batchActions(this.graph, {
447
493
  action: 'batch-actions',
448
494
  actions
449
495
  });
450
- if (!result) {
451
- throw new McpError(ErrorCode.InternalError, 'Failed to create content block via batch action');
452
- }
453
496
  }
454
497
  catch (error) {
455
498
  throw new McpError(ErrorCode.InternalError, `Failed to create content block: ${error instanceof Error ? error.message : String(error)}`);
456
499
  }
500
+ // For single-line content, we still need to fetch the UID and construct a NestedBlock
501
+ const createdUids = [];
502
+ try {
503
+ const foundUid = await this.findBlockWithRetry(targetParentUid, content);
504
+ if (foundUid) {
505
+ createdUids.push({
506
+ uid: foundUid,
507
+ text: content,
508
+ level: 0,
509
+ order: 0,
510
+ children: []
511
+ });
512
+ }
513
+ }
514
+ catch (error) {
515
+ // Log warning but don't re-throw, as the block might be created, just not immediately verifiable
516
+ // console.warn(`Could not verify single block creation for "${content}": ${error.message}`);
517
+ }
457
518
  return {
458
519
  success: true,
459
520
  page_uid: targetPageUid,
460
- parent_uid: targetParentUid
521
+ parent_uid: targetParentUid,
522
+ created_uids: createdUids
461
523
  };
462
524
  }
463
525
  }
@@ -40,7 +40,7 @@ export const toolSchemas = {
40
40
  },
41
41
  roam_create_page: {
42
42
  name: 'roam_create_page',
43
- 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.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
43
+ description: 'Create a new standalone page in Roam with optional content, including structured outlines, using explicit nesting levels and headings (H1-H3). This is the preferred method for creating a new page with an outline in a single step. 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.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
44
44
  inputSchema: {
45
45
  type: 'object',
46
46
  properties: {
@@ -80,7 +80,7 @@ export const toolSchemas = {
80
80
  },
81
81
  roam_create_outline: {
82
82
  name: 'roam_create_outline',
83
- description: 'Add a structured outline to an existing page or block (by title text or uid), with customizable nesting levels. The `outline` parameter defines *new* blocks to be created. To nest content under an *existing* block, provide its UID or exact text in `block_text_uid`, and ensure the `outline` array contains only the child blocks with levels relative to that parent. Including the parent block\'s text in the `outline` array will create a duplicate block. 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\nBest for simpler, contiguous hierarchical content. For complex nesting (e.g., tables) or granular control over block placement, consider `roam_process_batch_actions` instead.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
83
+ description: 'Add a structured outline to an existing page or block (by title text or uid), with customizable nesting levels. To create a new page with an outline, use the `roam_create_page` tool instead. The `outline` parameter defines *new* blocks to be created. To nest content under an *existing* block, provide its UID or exact text in `block_text_uid`, and ensure the `outline` array contains only the child blocks with levels relative to that parent. Including the parent block\'s text in the `outline` array will create a duplicate block. 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\nBest for simpler, contiguous hierarchical content. For complex nesting (e.g., tables) or granular control over block placement, consider `roam_process_batch_actions` instead.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
84
84
  inputSchema: {
85
85
  type: 'object',
86
86
  properties: {
@@ -129,7 +129,7 @@ export const toolSchemas = {
129
129
  },
130
130
  roam_import_markdown: {
131
131
  name: 'roam_import_markdown',
132
- description: 'Import nested markdown content into Roam under a specific block. Can locate the parent block by UID (preferred) or by exact string match within a specific page.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
132
+ description: 'Import nested markdown content into Roam under a specific block. Can locate the parent block by UID (preferred) or by exact string match within a specific page. If a `parent_string` is provided and the block does not exist, it will be created. Returns a nested structure of the created blocks.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
133
133
  inputSchema: {
134
134
  type: 'object',
135
135
  properties: {
@@ -151,7 +151,7 @@ export const toolSchemas = {
151
151
  },
152
152
  parent_string: {
153
153
  type: 'string',
154
- description: 'Optional: Exact string content of the parent block to add content under (used if parent_uid is not provided; requires page_uid or page_title).'
154
+ description: 'Optional: Exact string content of an existing parent block to add content under (used if parent_uid is not provided; requires page_uid or page_title). If the block does not exist, it will be created.'
155
155
  },
156
156
  order: {
157
157
  type: 'string',
@@ -463,7 +463,7 @@ export const toolSchemas = {
463
463
  },
464
464
  roam_fetch_block_with_children: {
465
465
  name: 'roam_fetch_block_with_children',
466
- description: 'Fetch a block by its UID along with its hierarchical children down to a specified depth.',
466
+ description: 'Fetch a block by its UID along with its hierarchical children down to a specified depth. Returns a nested object structure containing the block\'s UID, text, order, and an array of its children.',
467
467
  inputSchema: {
468
468
  type: 'object',
469
469
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roam-research-mcp",
3
- "version": "0.32.4",
3
+ "version": "0.35.1",
4
4
  "description": "A Model Context Protocol (MCP) server for Roam Research API integration",
5
5
  "private": false,
6
6
  "repository": {