roam-research-mcp 0.25.3 → 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.
@@ -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
- // First check for headings
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
- // For bullet points, use the bullet's indentation for level
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
- content = trimmedLine;
187
+ contentToParse = trimmedLine; // No bullet, use trimmed line
207
188
  }
208
- // Create regular node
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;
@@ -1,7 +1,6 @@
1
1
  import { q } from '@roam-research/roam-api-sdk';
2
2
  import { BaseSearchHandler } from './types.js';
3
3
  export class DatomicSearchHandler extends BaseSearchHandler {
4
- params;
5
4
  constructor(graph, params) {
6
5
  super(graph);
7
6
  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;
@@ -1,6 +1,5 @@
1
1
  // Base class for all search handlers
2
2
  export class BaseSearchHandler {
3
- graph;
4
3
  constructor(graph) {
5
4
  this.graph = graph;
6
5
  }
@@ -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.3',
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
  };
@@ -3,7 +3,6 @@ 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
  }
@@ -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 = [...new Set(results.map(([title]) => title))];
40
+ const uniquePages = Array.from(new Set(results.map(([title]) => title)));
42
41
  return {
43
42
  success: true,
44
43
  pages: uniquePages,
@@ -127,7 +126,7 @@ export class PageOperations {
127
126
  }
128
127
  return { success: true, uid: pageUid };
129
128
  }
130
- async fetchPageByTitle(title) {
129
+ async fetchPageByTitle(title, format = 'raw') {
131
130
  if (!title) {
132
131
  throw new McpError(ErrorCode.InvalidRequest, 'title is required');
133
132
  }
@@ -170,6 +169,9 @@ export class PageOperations {
170
169
  [?parent :block/uid ?parent-uid]]`;
171
170
  const blocks = await q(this.graph, blocksQuery, [ancestorRule, title]);
172
171
  if (!blocks || blocks.length === 0) {
172
+ if (format === 'raw') {
173
+ return [];
174
+ }
173
175
  return `${title} (no content found)`;
174
176
  }
175
177
  // Get heading information for blocks that have it
@@ -226,9 +228,13 @@ export class PageOperations {
226
228
  });
227
229
  };
228
230
  sortBlocks(rootBlocks);
231
+ if (format === 'raw') {
232
+ return JSON.stringify(rootBlocks);
233
+ }
229
234
  // Convert to markdown with proper nesting
230
235
  const toMarkdown = (blocks, level = 0) => {
231
- return blocks.map(block => {
236
+ return blocks
237
+ .map(block => {
232
238
  const indent = ' '.repeat(level);
233
239
  let md;
234
240
  // Check block heading level and format accordingly
@@ -245,7 +251,8 @@ export class PageOperations {
245
251
  md += '\n' + toMarkdown(block.children, level + 1);
246
252
  }
247
253
  return md;
248
- }).join('\n');
254
+ })
255
+ .join('\n');
249
256
  };
250
257
  return `# ${title}\n\n${toMarkdown(rootBlocks)}`;
251
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;
@@ -1,6 +1,5 @@
1
1
  import { TagSearchHandlerImpl, BlockRefSearchHandlerImpl, HierarchySearchHandlerImpl, TextSearchHandlerImpl, StatusSearchHandlerImpl } from './handlers.js';
2
2
  export class SearchOperations {
3
- graph;
4
3
  constructor(graph) {
5
4
  this.graph = graph;
6
5
  }
@@ -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
  }
@@ -19,18 +19,21 @@ export const toolSchemas = {
19
19
  },
20
20
  },
21
21
  roam_fetch_page_by_title: {
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 accessing daily pages, reading and analyzing existing Roam pages.',
24
- inputSchema: {
25
- type: 'object',
26
- properties: {
27
- title: {
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
- required: ['title'],
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',
@@ -112,7 +115,7 @@ export const toolSchemas = {
112
115
  },
113
116
  block_text_uid: {
114
117
  type: 'string',
115
- 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 title.'
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.'
116
119
  },
117
120
  outline: {
118
121
  type: 'array',
@@ -129,6 +132,12 @@ export const toolSchemas = {
129
132
  description: 'Indentation level (1-10, where 1 is top level)',
130
133
  minimum: 1,
131
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
132
141
  }
133
142
  },
134
143
  required: ['text', 'level']
@@ -410,7 +419,7 @@ export const toolSchemas = {
410
419
  scope: {
411
420
  type: 'string',
412
421
  enum: ['blocks', 'pages', 'both'],
413
- description: 'Whether to search blocks, pages, or both',
422
+ description: 'Whether to search blocks, pages',
414
423
  },
415
424
  include_content: {
416
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,8 +22,8 @@ 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
29
  async createBlock(content, page_uid, title, heading) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roam-research-mcp",
3
- "version": "0.25.3",
3
+ "version": "0.25.5",
4
4
  "description": "A Model Context Protocol (MCP) server for Roam Research API integration",
5
5
  "private": false,
6
6
  "repository": {