roam-research-mcp 0.24.1 → 0.24.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/README.md CHANGED
@@ -30,6 +30,16 @@ npm run build
30
30
 
31
31
  The server provides powerful tools for interacting with Roam Research:
32
32
 
33
+ - Environment variable handling with .env support
34
+ - Comprehensive input validation
35
+ - Case-insensitive page title matching
36
+ - Recursive block reference resolution
37
+ - Markdown parsing and conversion
38
+ - Daily page integration
39
+ - Detailed debug logging
40
+ - Efficient batch operations
41
+ - Hierarchical outline creation
42
+
33
43
  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
44
  2. `roam_create_page`: Create new pages with optional content
35
45
  3. `roam_create_block`: Create new blocks in a page (defaults to today's daily page)
@@ -38,7 +48,7 @@ The server provides powerful tools for interacting with Roam Research:
38
48
  6. `roam_create_outline`: Create hierarchical outlines with proper nesting and structure
39
49
  7. `roam_search_block_refs`: Search for block references within pages or across the graph
40
50
  8. `roam_search_hierarchy`: Navigate and search through block parent-child relationships
41
- 9. `find_pages_modified_today`: Find all pages that have been modified since midnight today
51
+ 9. `roam_find_pages_modified_today`: Find all pages that have been modified since midnight today
42
52
  10. `roam_search_by_text`: Search for blocks containing specific text across all pages or within a specific page
43
53
  11. `roam_update_block`: Update block content with direct text or pattern-based transformations
44
54
  12. `roam_search_by_date`: Search for blocks and pages based on creation or modification dates
@@ -545,7 +555,7 @@ Returns:
545
555
  Find all pages that have been modified since midnight today:
546
556
 
547
557
  ```typescript
548
- use_mcp_tool roam-research find_pages_modified_today {}
558
+ use_mcp_tool roam-research roam_find_pages_modified_today {}
549
559
  ```
550
560
 
551
561
  Features:
@@ -717,25 +727,39 @@ Each error response includes:
717
727
 
718
728
  ## Development
719
729
 
720
- The server is built with TypeScript and includes:
730
+ ### Building
721
731
 
722
- - Environment variable handling with .env support
723
- - Comprehensive input validation
724
- - Case-insensitive page title matching
725
- - Recursive block reference resolution
726
- - Markdown parsing and conversion
727
- - Daily page integration
728
- - Detailed debug logging
729
- - Efficient batch operations
730
- - Hierarchical outline creation
732
+ To build the server:
733
+
734
+ ```bash
735
+ npm install
736
+ npm run build
737
+ ```
738
+
739
+ This will:
740
+
741
+ 1. Install all required dependencies
742
+ 2. Compile TypeScript to JavaScript
743
+ 3. Make the output file executable
744
+
745
+ You can also use `npm run watch` during development to automatically recompile when files change.
746
+
747
+ ### Testing with MCP Inspector
748
+
749
+ The MCP Inspector is a tool that helps test and debug MCP servers. To test the server:
750
+
751
+ ```bash
752
+ # Inspect with npx:
753
+ npx @modelcontextprotocol/inspector node build/index.js
754
+ ```
731
755
 
732
- To modify or extend the server:
756
+ This will:
733
757
 
734
- 1. Clone the repository
735
- 2. Install dependencies with `npm install`
736
- 3. Make changes to the source files
737
- 4. Build with `npm run build`
738
- 5. Test locally by configuring environment variables
758
+ 1. Start the server in inspector mode
759
+ 2. Provide an interactive interface to:
760
+ - List available tools and resources
761
+ - Execute tools with custom parameters
762
+ - View tool responses and error handling
739
763
 
740
764
  ## License
741
765
 
@@ -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.17.0',
20
+ version: '0.24.5',
21
21
  }, {
22
22
  capabilities: {
23
23
  tools: {
@@ -33,10 +33,10 @@ export class RoamServer {
33
33
  roam_search_by_status: {},
34
34
  roam_search_block_refs: {},
35
35
  roam_search_hierarchy: {},
36
- find_pages_modified_today: {},
36
+ roam_find_pages_modified_today: {},
37
37
  roam_search_by_text: {},
38
38
  roam_update_block: {},
39
- roam_update_blocks: {},
39
+ roam_update_multiple_blocks: {},
40
40
  roam_search_by_date: {},
41
41
  roam_datomic_query: {}
42
42
  },
@@ -136,8 +136,9 @@ export class RoamServer {
136
136
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
137
137
  };
138
138
  }
139
- case 'find_pages_modified_today': {
140
- const result = await this.toolHandlers.findPagesModifiedToday();
139
+ case 'roam_find_pages_modified_today': {
140
+ const { max_num_pages } = request.params.arguments;
141
+ const result = await this.toolHandlers.findPagesModifiedToday(max_num_pages || 50);
141
142
  return {
142
143
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
143
144
  };
@@ -173,12 +174,13 @@ export class RoamServer {
173
174
  };
174
175
  }
175
176
  case 'roam_recall': {
176
- const result = await this.toolHandlers.recall();
177
+ const { sort_by = 'newest', filter_tag } = request.params.arguments;
178
+ const result = await this.toolHandlers.recall(sort_by, filter_tag);
177
179
  return {
178
180
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
179
181
  };
180
182
  }
181
- case 'roam_update_blocks': {
183
+ case 'roam_update_multiple_blocks': {
182
184
  const { updates } = request.params.arguments;
183
185
  const result = await this.toolHandlers.updateBlocks(updates);
184
186
  return {
@@ -66,7 +66,7 @@ export class MemoryOperations {
66
66
  }
67
67
  return { success: true };
68
68
  }
69
- async recall() {
69
+ async recall(sort_by = 'newest', filter_tag) {
70
70
  // Get memories tag from environment
71
71
  var memoriesTag = process.env.MEMORIES_TAG;
72
72
  if (!memoriesTag) {
@@ -76,27 +76,73 @@ export class MemoryOperations {
76
76
  const tagText = memoriesTag
77
77
  .replace(/^#/, '') // Remove leading #
78
78
  .replace(/^\[\[/, '').replace(/\]\]$/, ''); // Remove [[ and ]]
79
- // Get results from tag search
80
- const tagResults = await this.searchOps.searchForTag(tagText);
81
- // Get blocks from the memories page
82
- const pageQuery = `[:find ?string
83
- :in $ ?title
84
- :where [?p :node/title ?title]
85
- [?b :block/page ?p]
86
- [?b :block/string ?string]]`;
87
- const pageResults = await q(this.graph, pageQuery, [tagText]);
88
- // Combine both sets of results and remove the memories tag
89
- const allMemories = [
90
- ...tagResults.matches.map((match) => match.content),
91
- ...pageResults.map(([content]) => content)
92
- ].map(content => content.replace(`${memoriesTag} `, ''));
93
- // Resolve any block references in the combined memories
94
- const resolvedMemories = await Promise.all(allMemories.map(async (content) => resolveRefs(this.graph, content)));
95
- // Remove duplicates
96
- const uniqueMemories = [...new Set(resolvedMemories)];
97
- return {
98
- success: true,
99
- memories: uniqueMemories
100
- };
79
+ try {
80
+ // Get page blocks using query to access actual block content
81
+ const ancestorRule = `[
82
+ [ (ancestor ?b ?a)
83
+ [?a :block/children ?b] ]
84
+ [ (ancestor ?b ?a)
85
+ [?parent :block/children ?b]
86
+ (ancestor ?parent ?a) ]
87
+ ]`;
88
+ // Query to find all blocks on the page
89
+ const pageQuery = `[:find ?string ?time
90
+ :in $ % ?title
91
+ :where
92
+ [?page :node/title ?title]
93
+ [?block :block/string ?string]
94
+ [?block :create/time ?time]
95
+ (ancestor ?block ?page)]`;
96
+ // Execute query
97
+ const pageResults = await q(this.graph, pageQuery, [ancestorRule, tagText]);
98
+ // Process page blocks with sorting
99
+ let pageMemories = pageResults
100
+ .sort(([_, aTime], [__, bTime]) => sort_by === 'newest' ? bTime - aTime : aTime - bTime)
101
+ .map(([content]) => content);
102
+ // Get tagged blocks from across the graph
103
+ const tagResults = await this.searchOps.searchForTag(tagText);
104
+ // Process tagged blocks with sorting
105
+ let taggedMemories = tagResults.matches
106
+ .sort((a, b) => {
107
+ const aTime = a.block_uid ? parseInt(a.block_uid.split('-')[0], 16) : 0;
108
+ const bTime = b.block_uid ? parseInt(b.block_uid.split('-')[0], 16) : 0;
109
+ return sort_by === 'newest' ? bTime - aTime : aTime - bTime;
110
+ })
111
+ .map(match => match.content);
112
+ // Resolve any block references in both sets
113
+ const resolvedPageMemories = await Promise.all(pageMemories.map(async (content) => resolveRefs(this.graph, content)));
114
+ const resolvedTaggedMemories = await Promise.all(taggedMemories.map(async (content) => resolveRefs(this.graph, content)));
115
+ // Combine both sets and remove duplicates while preserving order
116
+ let uniqueMemories = [
117
+ ...resolvedPageMemories,
118
+ ...resolvedTaggedMemories
119
+ ].filter((memory, index, self) => self.indexOf(memory) === index);
120
+ // Format filter tag with exact Roam tag syntax
121
+ const filterTagFormatted = filter_tag ?
122
+ (filter_tag.includes(' ') ? `#[[${filter_tag}]]` : `#${filter_tag}`) : null;
123
+ // Filter by exact tag match if provided
124
+ if (filterTagFormatted) {
125
+ uniqueMemories = uniqueMemories.filter(memory => memory.includes(filterTagFormatted));
126
+ }
127
+ // Format memories tag for removal and clean up memories tag
128
+ const memoriesTagFormatted = tagText.includes(' ') || tagText.includes('/') ? `#[[${tagText}]]` : `#${tagText}`;
129
+ uniqueMemories = uniqueMemories.map(memory => memory.replace(memoriesTagFormatted, '').trim());
130
+ // return {
131
+ // success: true,
132
+ // memories: [
133
+ // `memoriesTag = ${memoriesTag}`,
134
+ // `filter_tag = ${filter_tag}`,
135
+ // `filterTagFormatted = ${filterTagFormatted}`,
136
+ // `memoriesTagFormatted = ${memoriesTagFormatted}`,
137
+ // ]
138
+ // }
139
+ return {
140
+ success: true,
141
+ memories: uniqueMemories
142
+ };
143
+ }
144
+ catch (error) {
145
+ throw new McpError(ErrorCode.InternalError, `Failed to recall memories: ${error.message}`);
146
+ }
101
147
  }
102
148
  }
@@ -159,6 +159,10 @@ export class OutlineOperations {
159
159
  return createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true);
160
160
  }
161
161
  };
162
+ // Helper function to check if string is a valid Roam UID (9 characters)
163
+ const isValidUid = (str) => {
164
+ return typeof str === 'string' && str.length === 9;
165
+ };
162
166
  // Get or create the parent block
163
167
  let targetParentUid;
164
168
  if (!block_text_uid) {
@@ -166,12 +170,28 @@ export class OutlineOperations {
166
170
  }
167
171
  else {
168
172
  try {
169
- // Create header block and get its UID
170
- targetParentUid = await createAndVerifyBlock(block_text_uid, targetPageUid);
173
+ if (isValidUid(block_text_uid)) {
174
+ // First try to find block by UID
175
+ const uidQuery = `[:find ?uid
176
+ :where [?e :block/uid "${block_text_uid}"]
177
+ [?e :block/uid ?uid]]`;
178
+ const uidResult = await q(this.graph, uidQuery, []);
179
+ if (uidResult && uidResult.length > 0) {
180
+ // Use existing block if found
181
+ targetParentUid = uidResult[0][0];
182
+ }
183
+ else {
184
+ throw new McpError(ErrorCode.InvalidRequest, `Block with UID "${block_text_uid}" not found`);
185
+ }
186
+ }
187
+ else {
188
+ // Create header block and get its UID if not a valid UID
189
+ targetParentUid = await createAndVerifyBlock(block_text_uid, targetPageUid);
190
+ }
171
191
  }
172
192
  catch (error) {
173
193
  const errorMessage = error instanceof Error ? error.message : String(error);
174
- throw new McpError(ErrorCode.InternalError, `Failed to create header block "${block_text_uid}": ${errorMessage}`);
194
+ throw new McpError(ErrorCode.InternalError, `Failed to ${isValidUid(block_text_uid) ? 'find' : 'create'} block "${block_text_uid}": ${errorMessage}`);
175
195
  }
176
196
  }
177
197
  // Initialize result variable
@@ -8,7 +8,7 @@ export class PageOperations {
8
8
  constructor(graph) {
9
9
  this.graph = graph;
10
10
  }
11
- async findPagesModifiedToday() {
11
+ async findPagesModifiedToday(max_num_pages = 50) {
12
12
  // Define ancestor rule for traversing block hierarchy
13
13
  const ancestorRule = `[
14
14
  [ (ancestor ?b ?a)
@@ -28,7 +28,8 @@ export class PageOperations {
28
28
  [?page :node/title ?title]
29
29
  (ancestor ?block ?page)
30
30
  [?block :edit/time ?time]
31
- [(> ?time ?start_of_day)]]`, [startOfDay.getTime(), ancestorRule]);
31
+ [(> ?time ?start_of_day)]]
32
+ :limit ${max_num_pages}`, [startOfDay.getTime(), ancestorRule]);
32
33
  if (!results || results.length === 0) {
33
34
  return {
34
35
  success: true,
@@ -20,13 +20,13 @@ export const toolSchemas = {
20
20
  },
21
21
  roam_fetch_page_by_title: {
22
22
  name: 'roam_fetch_page_by_title',
23
- description: 'Retrieve complete page contents by exact title, including all nested blocks and resolved block references. Use for reading and analyzing existing Roam pages.',
23
+ description: 'Retrieve complete page contents by exact title, including all nested blocks and resolved block references. Use for accessing daily pages, reading and analyzing existing Roam pages.',
24
24
  inputSchema: {
25
25
  type: 'object',
26
26
  properties: {
27
27
  title: {
28
28
  type: 'string',
29
- description: 'Title of the page to fetch and read',
29
+ description: 'Title of the page. For date pages, use ordinal date formats such as January 2nd, 2025',
30
30
  },
31
31
  },
32
32
  required: ['title'],
@@ -243,13 +243,18 @@ export const toolSchemas = {
243
243
  ]
244
244
  }
245
245
  },
246
- find_pages_modified_today: {
247
- name: 'find_pages_modified_today',
248
- description: 'Find all pages that have been modified today (since midnight).',
246
+ roam_find_pages_modified_today: {
247
+ name: 'roam_find_pages_modified_today',
248
+ description: 'Find pages that have been modified today (since midnight), with limit.',
249
249
  inputSchema: {
250
250
  type: 'object',
251
- properties: {},
252
- required: []
251
+ properties: {
252
+ max_num_pages: {
253
+ type: 'integer',
254
+ description: 'Max number of pages to retrieve (default: 50)',
255
+ default: 50
256
+ },
257
+ }
253
258
  }
254
259
  },
255
260
  roam_search_by_text: {
@@ -272,7 +277,7 @@ export const toolSchemas = {
272
277
  },
273
278
  roam_update_block: {
274
279
  name: 'roam_update_block',
275
- description: 'Update the content of an existing block identified by its UID. Can either provide new content directly or use a transform pattern to modify existing content.\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).',
280
+ description: 'Update a single block identified by its UID. Use this for individual block updates when you need to either replace the entire content or apply a transform pattern to modify specific parts of the content.\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).',
276
281
  inputSchema: {
277
282
  type: 'object',
278
283
  properties: {
@@ -312,9 +317,9 @@ export const toolSchemas = {
312
317
  ]
313
318
  }
314
319
  },
315
- roam_update_blocks: {
316
- name: 'roam_update_blocks',
317
- description: 'Update multiple blocks in a single batch operation. Each update can provide either new content directly or a transform pattern.\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).',
320
+ roam_update_multiple_blocks: {
321
+ name: 'roam_update_multiple_blocks',
322
+ description: 'Efficiently update multiple blocks in a single batch operation. Use this when you need to update several blocks at once to avoid making multiple separate API calls. Each block in the batch can independently either have its content replaced or transformed using a pattern.\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).',
318
323
  inputSchema: {
319
324
  type: 'object',
320
325
  properties: {
@@ -366,7 +371,7 @@ export const toolSchemas = {
366
371
  },
367
372
  roam_search_by_date: {
368
373
  name: 'roam_search_by_date',
369
- description: 'Search for blocks or pages based on creation or modification dates',
374
+ description: 'Search for blocks or pages based on creation or modification dates. Not for daily pages with ordinal date titles.',
370
375
  inputSchema: {
371
376
  type: 'object',
372
377
  properties: {
@@ -399,13 +404,13 @@ export const toolSchemas = {
399
404
  },
400
405
  roam_remember: {
401
406
  name: 'roam_remember',
402
- description: 'Add a memory or piece of information to remember, stored on the daily page with #[[LLM/Memories]] tag and optional categories. \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).',
407
+ description: 'Add a memory or piece of information to remember, stored on the daily page with MEMORIES_TAG tag and optional categories. \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).',
403
408
  inputSchema: {
404
409
  type: 'object',
405
410
  properties: {
406
411
  memory: {
407
412
  type: 'string',
408
- description: 'The memory or information to remember'
413
+ description: 'The memory detail or information to remember'
409
414
  },
410
415
  categories: {
411
416
  type: 'array',
@@ -420,11 +425,21 @@ export const toolSchemas = {
420
425
  },
421
426
  roam_recall: {
422
427
  name: 'roam_recall',
423
- description: 'Retrieve all stored memories by searching for blocks tagged with MEMORIES_TAG and content from the page with the same name. Returns a combined, deduplicated list of memories.',
428
+ description: 'Retrieve all stored memories on page titled MEMORIES_TAG, or tagged block content with the same name. Returns a combined, deduplicated list of memories. Optionally filter blcoks with a specified tag and sort by creation date.',
424
429
  inputSchema: {
425
430
  type: 'object',
426
- properties: {},
427
- required: []
431
+ properties: {
432
+ sort_by: {
433
+ type: 'string',
434
+ description: 'Sort order for memories based on creation date',
435
+ enum: ['newest', 'oldest'],
436
+ default: 'newest'
437
+ },
438
+ filter_tag: {
439
+ type: 'string',
440
+ description: 'Include only memories with a specific filter tag. For single word tags use format "tag", for multi-word tags use format "tag word" (without brackets)'
441
+ }
442
+ }
428
443
  }
429
444
  },
430
445
  roam_datomic_query: {
@@ -23,8 +23,8 @@ export class ToolHandlers {
23
23
  this.outlineOps = new OutlineOperations(graph);
24
24
  }
25
25
  // Page Operations
26
- async findPagesModifiedToday() {
27
- return this.pageOps.findPagesModifiedToday();
26
+ async findPagesModifiedToday(max_num_pages = 50) {
27
+ return this.pageOps.findPagesModifiedToday(max_num_pages);
28
28
  }
29
29
  async createPage(title, content) {
30
30
  return this.pageOps.createPage(title, content);
@@ -70,8 +70,8 @@ export class ToolHandlers {
70
70
  async remember(memory, categories) {
71
71
  return this.memoryOps.remember(memory, categories);
72
72
  }
73
- async recall() {
74
- return this.memoryOps.recall();
73
+ async recall(sort_by = 'newest', filter_tag) {
74
+ return this.memoryOps.recall(sort_by, filter_tag);
75
75
  }
76
76
  // Todo Operations
77
77
  async addTodos(todos) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roam-research-mcp",
3
- "version": "0.24.1",
3
+ "version": "0.24.5",
4
4
  "description": "A Model Context Protocol (MCP) server for Roam Research API integration",
5
5
  "private": false,
6
6
  "repository": {