roam-research-mcp 0.36.0 → 1.0.0

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
@@ -7,18 +7,17 @@
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
  [![GitHub](https://img.shields.io/github/license/2b3pro/roam-research-mcp)](https://github.com/2b3pro/roam-research-mcp/blob/main/LICENSE)
9
9
 
10
- A Model Context Protocol (MCP) server that provides comprehensive access to Roam Research's API functionality. This server enables AI assistants like Claude to interact with your Roam Research graph through a standardized interface. It supports standard input/output (stdio), HTTP Stream, and Server-Sent Events (SSE) communication. (A WORK-IN-PROGRESS, personal project not officially endorsed by Roam Research)
10
+ A Model Context Protocol (MCP) server that provides comprehensive access to Roam Research's API functionality. This server enables AI assistants like Claude to interact with your Roam Research graph through a standardized interface. It supports standard input/output (stdio) and HTTP Stream communication. (A WORK-IN-PROGRESS, personal project not officially endorsed by Roam Research)
11
11
 
12
12
  <a href="https://glama.ai/mcp/servers/fzfznyaflu"><img width="380" height="200" src="https://glama.ai/mcp/servers/fzfznyaflu/badge" alt="Roam Research MCP server" /></a>
13
13
  <a href="https://mseep.ai/app/2b3pro-roam-research-mcp"><img width="380" height="200" src="https://mseep.net/pr/2b3pro-roam-research-mcp-badge.png" alt="MseeP.ai Security Assessment Badge" /></a>
14
14
 
15
15
  ## Installation and Usage
16
16
 
17
- This MCP server supports three primary communication methods:
17
+ This MCP server supports two primary communication methods:
18
18
 
19
19
  1. **Stdio (Standard Input/Output):** Ideal for local inter-process communication, command-line tools, and direct integration with applications running on the same machine. This is the default communication method when running the server directly.
20
20
  2. **HTTP Stream:** Provides network-based communication, suitable for web-based clients, remote applications, or scenarios requiring real-time updates over HTTP. The HTTP Stream endpoint runs on port `8088` by default.
21
- 3. **SSE (Server-Sent Events):** A transport for legacy clients that require SSE. The SSE endpoint runs on port `8087` by default. (NOTE: ⚠️ DEPRECATED: The SSE Transport has been deprecated as of MCP specification version 2025-03-26. HTTP Stream Transport preferred.)
22
21
 
23
22
  ### Running with Stdio
24
23
 
@@ -41,16 +40,16 @@ npm start
41
40
 
42
41
  ### Running with HTTP Stream
43
42
 
44
- To run the server with HTTP Stream or SSE support, you can either:
43
+ To run the server with HTTP Stream support, you can either:
45
44
 
46
- 1. **Use the default ports:** Run `npm start` after building (as shown above). The server will automatically listen on port `8088` for HTTP Stream and `8087` for SSE.
47
- 2. **Specify custom ports:** Set the `HTTP_STREAM_PORT` and/or `SSE_PORT` environment variables before starting the server.
45
+ 1. **Use the default ports:** Run `npm start` after building (as shown above). The server will automatically listen on port `8088` for HTTP Stream.
46
+ 2. **Specify custom ports:** Set the `HTTP_STREAM_PORT` environment variable before starting the server.
48
47
 
49
48
  ```bash
50
- HTTP_STREAM_PORT=9000 SSE_PORT=9001 npm start
49
+ HTTP_STREAM_PORT=9000 npm start
51
50
  ```
52
51
 
53
- Or, if using a `.env` file, add `HTTP_STREAM_PORT=9000` and/or `SSE_PORT=9001` to it.
52
+ Or, if using a `.env` file, add `HTTP_STREAM_PORT=9000` to it.
54
53
 
55
54
  ## Docker
56
55
 
@@ -66,16 +65,15 @@ docker build -t roam-research-mcp .
66
65
 
67
66
  ### Run the Docker Container
68
67
 
69
- To run the Docker container and map the necessary ports, you must also provide the required environment variables. Use the `-e` flag to pass `ROAM_API_TOKEN`, `ROAM_GRAPH_NAME`, and optionally `MEMORIES_TAG`, `HTTP_STREAM_PORT`, and `SSE_PORT`:
68
+ To run the Docker container and map the necessary ports, you must also provide the required environment variables. Use the `-e` flag to pass `ROAM_API_TOKEN`, `ROAM_GRAPH_NAME`, and optionally `MEMORIES_TAG` and `HTTP_STREAM_PORT`:
70
69
 
71
70
  ```bash
72
- docker run -p 3000:3000 -p 8088:8088 -p 8087:8087 \
71
+ docker run -p 3000:3000 -p 8088:8088 \
73
72
  -e ROAM_API_TOKEN="your-api-token" \
74
73
  -e ROAM_GRAPH_NAME="your-graph-name" \
75
74
  -e MEMORIES_TAG="#[[LLM/Memories]]" \
76
75
  -e CUSTOM_INSTRUCTIONS_PATH="/path/to/your/custom_instructions_file.md" \
77
76
  -e HTTP_STREAM_PORT="8088" \
78
- -e SSE_PORT="8087" \
79
77
  roam-research-mcp
80
78
  ```
81
79
 
@@ -117,19 +115,19 @@ The server provides powerful tools for interacting with Roam Research:
117
115
  6. `roam_create_outline`: Add a structured outline to an existing page or block, with support for `children_view_type`. Best for simpler, sequential outlines. For complex nesting (e.g., tables), consider `roam_process_batch_actions`. If `page_title_uid` and `block_text_uid` are both blank, content defaults to the daily page. (Internally uses `roam_process_batch_actions`.)
118
116
  7. `roam_search_block_refs`: Search for block references within a page or across the entire graph.
119
117
  8. `roam_search_hierarchy`: Search for parent or child blocks in the block hierarchy.
120
- 9. `roam_find_pages_modified_today`: Find pages that have been modified today (since midnight).
121
- 10. `roam_search_by_text`: Search for blocks containing specific text.
118
+ 9. `roam_find_pages_modified_today`: Find pages that have been modified today (since midnight), with pagination and sorting options.
119
+ 10. `roam_search_by_text`: Search for blocks containing specific text across all pages or within a specific page. This tool supports pagination via the `limit` and `offset` parameters.
122
120
  11. `roam_search_by_status`: Search for blocks with a specific status (TODO/DONE) across all pages or within a specific page.
123
121
  12. `roam_search_by_date`: Search for blocks or pages based on creation or modification dates.
124
- 13. `roam_search_for_tag`: Search for blocks containing a specific tag and optionally filter by blocks that also contain another tag nearby or exclude blocks with a specific tag.
122
+ 13. `roam_search_for_tag`: Search for blocks containing a specific tag and optionally filter by blocks that also contain another tag nearby or exclude blocks with a specific tag. This tool supports pagination via the `limit` and `offset` parameters.
125
123
  14. `roam_remember`: Add a memory or piece of information to remember. (Internally uses `roam_process_batch_actions`.)
126
124
  15. `roam_recall`: Retrieve all stored memories.
127
- 16. `roam_datomic_query`: Execute a custom Datomic query on the Roam graph for advanced data retrieval beyond the available search tools.
125
+ 16. `roam_datomic_query`: Execute a custom Datomic query on the Roam graph for advanced data retrieval beyond the available search tools. Now supports client-side regex filtering for enhanced post-query processing. Optimal for complex filtering (including regex), highly complex boolean logic, arbitrary sorting criteria, and proximity search.
128
126
  17. `roam_markdown_cheatsheet`: Provides the content of the Roam Markdown Cheatsheet resource, optionally concatenated with custom instructions if `CUSTOM_INSTRUCTIONS_PATH` environment variable is set.
129
127
  18. `roam_process_batch_actions`: Execute a sequence of low-level block actions (create, update, move, delete) in a single, non-transactional batch. Provides granular control for complex nesting like tables. (Note: For actions on existing blocks or within a specific page context, it is often necessary to first obtain valid page or block UIDs using tools like `roam_fetch_page_by_title`.)
130
128
 
131
129
  **Deprecated Tools**:
132
- The following tools have been deprecated as of `v1.36.0` in favor of the more powerful and flexible `roam_process_batch_actions`:
130
+ The following tools have been deprecated as of `v0.36.2` in favor of the more powerful and flexible `roam_process_batch_actions`:
133
131
 
134
132
  - `roam_create_block`: Use `roam_process_batch_actions` with the `create-block` action.
135
133
  - `roam_update_block`: Use `roam_process_batch_actions` with the `update-block` action.
@@ -176,20 +174,6 @@ Please note that while the `roam_process_batch_actions` tool can set block headi
176
174
 
177
175
  ---
178
176
 
179
- ## Propose Improvements
180
-
181
- ### Pagination for Search Tools
182
-
183
- The `roam_search_for_tag` and `roam_search_by_text` tools now support `limit` and `offset` parameters, enabling basic pagination. To achieve full, robust pagination (e.g., retrieving "page 2" of results), the client consuming these tools would need to:
184
-
185
- 1. Make an initial call with `limit` and `offset=0` to get the first set of results and the `total_count`.
186
- 2. Calculate the total number of pages based on `total_count` and the desired `limit`.
187
- 3. Make subsequent calls, incrementing the `offset` by `limit` for each "page" of results.
188
-
189
- Example: To get the second page of 10 results, the call would be `roam_search_by_text(text: "your query", limit: 10, offset: 10)`.
190
-
191
- ---
192
-
193
177
  ## Example Prompts
194
178
 
195
179
  Here are some examples of how to creatively use the Roam tool in an LLM to interact with your Roam graph, particularly leveraging `roam_process_batch_actions` for complex operations.
@@ -260,7 +244,6 @@ This demonstrates moving a block from one location to another and simultaneously
260
244
  MEMORIES_TAG='#[[LLM/Memories]]'
261
245
  CUSTOM_INSTRUCTIONS_PATH='/path/to/your/custom_instructions_file.md'
262
246
  HTTP_STREAM_PORT=8088 # Or your desired port for HTTP Stream communication
263
- SSE_PORT=8087 # Or your desired port for SSE communication
264
247
  ```
265
248
 
266
249
  Option 2: Using MCP settings (Alternative method)
@@ -280,8 +263,7 @@ This demonstrates moving a block from one location to another and simultaneously
280
263
  "ROAM_GRAPH_NAME": "your-graph-name",
281
264
  "MEMORIES_TAG": "#[[LLM/Memories]]",
282
265
  "CUSTOM_INSTRUCTIONS_PATH": "/path/to/your/custom_instructions_file.md",
283
- "HTTP_STREAM_PORT": "8088",
284
- "SSE_PORT": "8087"
266
+ "HTTP_STREAM_PORT": "8088"
285
267
  }
286
268
  }
287
269
  }
@@ -29,7 +29,7 @@ if (!API_TOKEN || !GRAPH_NAME) {
29
29
  ' "mcpServers": {\n' +
30
30
  ' "roam-research": {\n' +
31
31
  ' "command": "node",\n' +
32
- ' "args": ["/path/to/roam-research/build/index.js"],\n' +
32
+ ' "args": ["/path/to/roam-research-mcp/build/index.js"],\n' +
33
33
  ' "env": {\n' +
34
34
  ' "ROAM_API_TOKEN": "your-api-token",\n' +
35
35
  ' "ROAM_GRAPH_NAME": "your-graph-name"\n' +
@@ -42,6 +42,5 @@ if (!API_TOKEN || !GRAPH_NAME) {
42
42
  ' ROAM_GRAPH_NAME=your-graph-name');
43
43
  }
44
44
  const HTTP_STREAM_PORT = process.env.HTTP_STREAM_PORT || '8088'; // Default to 8088
45
- const SSE_PORT = process.env.SSE_PORT || '8087'; // Default to 8087
46
45
  const CORS_ORIGIN = process.env.CORS_ORIGIN || 'http://localhost:5678';
47
- export { API_TOKEN, GRAPH_NAME, HTTP_STREAM_PORT, SSE_PORT, CORS_ORIGIN };
46
+ export { API_TOKEN, GRAPH_NAME, HTTP_STREAM_PORT, CORS_ORIGIN };
package/build/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { RoamServer } from './server/roam-server.js';
3
3
  const server = new RoamServer();
4
- server.run().catch(() => { });
4
+ server.run().catch((error) => {
5
+ console.error("Fatal error running server:", error);
6
+ process.exit(1);
7
+ });
@@ -1,3 +1,4 @@
1
+ import { randomBytes } from 'crypto';
1
2
  /**
2
3
  * Check if text has a traditional markdown table
3
4
  */
@@ -240,11 +241,13 @@ function parseTableRows(lines) {
240
241
  return tableNodes;
241
242
  }
242
243
  function generateBlockUid() {
243
- // Generate a random string of 9 characters (Roam's format)
244
+ // Generate a random string of 9 characters (Roam's format) using crypto for better randomness
244
245
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_';
246
+ // 64 chars, which divides 256 evenly (256 = 64 * 4), so simple modulo is unbiased
247
+ const bytes = randomBytes(9);
245
248
  let uid = '';
246
249
  for (let i = 0; i < 9; i++) {
247
- uid += chars.charAt(Math.floor(Math.random() * chars.length));
250
+ uid += chars[bytes[i] % 64];
248
251
  }
249
252
  return uid;
250
253
  }
@@ -8,7 +8,46 @@ export class DatomicSearchHandler extends BaseSearchHandler {
8
8
  async execute() {
9
9
  try {
10
10
  // Execute the datomic query using the Roam API
11
- const results = await q(this.graph, this.params.query, this.params.inputs || []);
11
+ let results = await q(this.graph, this.params.query, this.params.inputs || []);
12
+ if (this.params.regexFilter) {
13
+ let regex;
14
+ try {
15
+ regex = new RegExp(this.params.regexFilter, this.params.regexFlags);
16
+ }
17
+ catch (e) {
18
+ return {
19
+ success: false,
20
+ matches: [],
21
+ message: `Invalid regex filter provided: ${e instanceof Error ? e.message : String(e)}`
22
+ };
23
+ }
24
+ results = results.filter(result => {
25
+ if (this.params.regexTargetField && this.params.regexTargetField.length > 0) {
26
+ for (const field of this.params.regexTargetField) {
27
+ // Access nested fields if path is provided (e.g., "prop.nested")
28
+ const fieldPath = field.split('.');
29
+ let value = result;
30
+ for (const part of fieldPath) {
31
+ if (typeof value === 'object' && value !== null && part in value) {
32
+ value = value[part];
33
+ }
34
+ else {
35
+ value = undefined; // Field not found
36
+ break;
37
+ }
38
+ }
39
+ if (typeof value === 'string' && regex.test(value)) {
40
+ return true;
41
+ }
42
+ }
43
+ return false;
44
+ }
45
+ else {
46
+ // Default to stringifying the whole result if no target field is specified
47
+ return regex.test(JSON.stringify(result));
48
+ }
49
+ });
50
+ }
12
51
  return {
13
52
  success: true,
14
53
  matches: results.map(result => ({
@@ -1,7 +1,7 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
- import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, ListToolsRequestSchema, ListPromptsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { initializeGraph } from '@roam-research/roam-api-sdk';
6
6
  import { API_TOKEN, GRAPH_NAME, HTTP_STREAM_PORT } from '../config/environment.js';
7
7
  import { toolSchemas } from '../tools/schemas.js';
@@ -20,7 +20,6 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
20
20
  const serverVersion = packageJson.version;
21
21
  export class RoamServer {
22
22
  constructor() {
23
- // console.log('RoamServer: Constructor started.');
24
23
  try {
25
24
  this.graph = initializeGraph({
26
25
  token: API_TOKEN,
@@ -42,7 +41,23 @@ export class RoamServer {
42
41
  if (Object.keys(toolSchemas).length === 0) {
43
42
  throw new McpError(ErrorCode.InternalError, 'No tool schemas defined in src/tools/schemas.ts');
44
43
  }
45
- // console.log('RoamServer: Constructor finished.');
44
+ }
45
+ // Helper to create and configure MCP server instance
46
+ createMcpServer(nameSuffix = '') {
47
+ const server = new Server({
48
+ name: `roam-research${nameSuffix}`,
49
+ version: serverVersion,
50
+ }, {
51
+ capabilities: {
52
+ tools: {
53
+ ...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName, toolSchemas[toolName].inputSchema])),
54
+ },
55
+ resources: {}, // No resources exposed via capabilities
56
+ prompts: {}, // No prompts exposed via capabilities
57
+ },
58
+ });
59
+ this.setupRequestHandlers(server);
60
+ return server;
46
61
  }
47
62
  // Refactored to accept a Server instance
48
63
  setupRequestHandlers(mcpServer) {
@@ -59,6 +74,10 @@ export class RoamServer {
59
74
  mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
60
75
  throw new McpError(ErrorCode.InternalError, `Resource not found: ${request.params.uri}`);
61
76
  });
77
+ // List available prompts
78
+ mcpServer.setRequestHandler(ListPromptsRequestSchema, async () => {
79
+ return { prompts: [] };
80
+ });
62
81
  // Handle tool calls
63
82
  mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
64
83
  try {
@@ -206,49 +225,15 @@ export class RoamServer {
206
225
  });
207
226
  }
208
227
  async run() {
209
- // console.log('RoamServer: run() method started.');
210
228
  try {
211
- // console.log('RoamServer: Attempting to create stdioMcpServer...');
212
- const stdioMcpServer = new Server({
213
- name: 'roam-research',
214
- version: serverVersion,
215
- }, {
216
- capabilities: {
217
- tools: {
218
- ...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName, toolSchemas[toolName].inputSchema])),
219
- },
220
- resources: {
221
- 'roam-markdown-cheatsheet.md': {}
222
- }
223
- },
224
- });
225
- // console.log('RoamServer: stdioMcpServer created. Setting up request handlers...');
226
- this.setupRequestHandlers(stdioMcpServer);
227
- // console.log('RoamServer: stdioMcpServer handlers setup complete. Connecting transport...');
229
+ const stdioMcpServer = this.createMcpServer();
228
230
  const stdioTransport = new StdioServerTransport();
229
231
  await stdioMcpServer.connect(stdioTransport);
230
- // console.log('RoamServer: stdioTransport connected. Attempting to create httpMcpServer...');
231
- const httpMcpServer = new Server({
232
- name: 'roam-research-http', // A distinct name for the HTTP server
233
- version: serverVersion,
234
- }, {
235
- capabilities: {
236
- tools: {
237
- ...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName, toolSchemas[toolName].inputSchema])),
238
- },
239
- resources: {
240
- 'roam-markdown-cheatsheet.md': {}
241
- }
242
- },
243
- });
244
- // console.log('RoamServer: httpMcpServer created. Setting up request handlers...');
245
- this.setupRequestHandlers(httpMcpServer);
246
- // console.log('RoamServer: httpMcpServer handlers setup complete. Connecting transport...');
232
+ const httpMcpServer = this.createMcpServer('-http');
247
233
  const httpStreamTransport = new StreamableHTTPServerTransport({
248
234
  sessionIdGenerator: () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15),
249
235
  });
250
236
  await httpMcpServer.connect(httpStreamTransport);
251
- // console.log('RoamServer: httpStreamTransport connected.');
252
237
  const httpServer = createServer(async (req, res) => {
253
238
  // Set CORS headers
254
239
  res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
@@ -264,7 +249,6 @@ export class RoamServer {
264
249
  await httpStreamTransport.handleRequest(req, res);
265
250
  }
266
251
  catch (error) {
267
- // // console.error('HTTP Stream Server error:', error);
268
252
  if (!res.headersSent) {
269
253
  res.writeHead(500, { 'Content-Type': 'application/json' });
270
254
  res.end(JSON.stringify({ error: 'Internal Server Error' }));
@@ -273,7 +257,6 @@ export class RoamServer {
273
257
  });
274
258
  const availableHttpPort = await findAvailablePort(parseInt(HTTP_STREAM_PORT));
275
259
  httpServer.listen(availableHttpPort, () => {
276
- // // console.log(`MCP Roam Research server running HTTP Stream on port ${availableHttpPort}`);
277
260
  });
278
261
  }
279
262
  catch (error) {
@@ -18,7 +18,7 @@ export class PageOperations {
18
18
  constructor(graph) {
19
19
  this.graph = graph;
20
20
  }
21
- async findPagesModifiedToday(max_num_pages = 50) {
21
+ async findPagesModifiedToday(limit = 50, offset = 0, sort_order = 'desc') {
22
22
  // Define ancestor rule for traversing block hierarchy
23
23
  const ancestorRule = `[
24
24
  [ (ancestor ?b ?a)
@@ -31,15 +31,21 @@ export class PageOperations {
31
31
  const startOfDay = new Date();
32
32
  startOfDay.setHours(0, 0, 0, 0);
33
33
  try {
34
- // Query for pages modified today
35
- const results = await q(this.graph, `[:find ?title
34
+ // Query for pages modified today, including modification time for sorting
35
+ let query = `[:find ?title ?time
36
36
  :in $ ?start_of_day %
37
37
  :where
38
38
  [?page :node/title ?title]
39
39
  (ancestor ?block ?page)
40
40
  [?block :edit/time ?time]
41
- [(> ?time ?start_of_day)]]
42
- :limit ${max_num_pages}`, [startOfDay.getTime(), ancestorRule]);
41
+ [(> ?time ?start_of_day)]]`;
42
+ if (limit !== -1) {
43
+ query += ` :limit ${limit}`;
44
+ }
45
+ if (offset > 0) {
46
+ query += ` :offset ${offset}`;
47
+ }
48
+ const results = await q(this.graph, query, [startOfDay.getTime(), ancestorRule]);
43
49
  if (!results || results.length === 0) {
44
50
  return {
45
51
  success: true,
@@ -47,7 +53,16 @@ export class PageOperations {
47
53
  message: 'No pages have been modified today'
48
54
  };
49
55
  }
50
- // Extract unique page titles
56
+ // Sort results by modification time
57
+ results.sort((a, b) => {
58
+ if (sort_order === 'desc') {
59
+ return b[1] - a[1]; // Newest first
60
+ }
61
+ else {
62
+ return a[1] - b[1]; // Oldest first
63
+ }
64
+ });
65
+ // Extract unique page titles from sorted results
51
66
  const uniquePages = Array.from(new Set(results.map(([title]) => title)));
52
67
  return {
53
68
  success: true,
@@ -173,21 +188,19 @@ export class PageOperations {
173
188
  throw new McpError(ErrorCode.InvalidRequest, 'title is required');
174
189
  }
175
190
  // Try different case variations
191
+ // Generate variations to check
176
192
  const variations = [
177
193
  title, // Original
178
194
  capitalizeWords(title), // Each word capitalized
179
195
  title.toLowerCase() // All lowercase
180
196
  ];
181
- let uid = null;
182
- for (const variation of variations) {
183
- const searchQuery = `[:find ?uid .
184
- :where [?e :node/title "${variation}"]
185
- [?e :block/uid ?uid]]`;
186
- const result = await q(this.graph, searchQuery, []);
187
- uid = (result === null || result === undefined) ? null : String(result);
188
- if (uid)
189
- break;
190
- }
197
+ // Create OR clause for query
198
+ const orClause = variations.map(v => `[?e :node/title "${v}"]`).join(' ');
199
+ const searchQuery = `[:find ?uid .
200
+ :where [?e :block/uid ?uid]
201
+ (or ${orClause})]`;
202
+ const result = await q(this.graph, searchQuery, []);
203
+ const uid = (result === null || result === undefined) ? null : String(result);
191
204
  if (!uid) {
192
205
  throw new McpError(ErrorCode.InvalidRequest, `Page with title "${title}" not found (tried original, capitalized words, and lowercase)`);
193
206
  }
@@ -188,8 +188,8 @@ export const toolSchemas = {
188
188
  },
189
189
  limit: {
190
190
  type: 'integer',
191
- description: 'Optional: The maximum number of results to return. Defaults to 1. Use -1 for no limit.',
192
- default: 1
191
+ description: 'Optional: The maximum number of results to return. Defaults to 50. Use -1 for no limit, but be aware that very large results sets can impact performance.',
192
+ default: 50
193
193
  },
194
194
  offset: {
195
195
  type: 'integer',
@@ -274,15 +274,26 @@ export const toolSchemas = {
274
274
  },
275
275
  roam_find_pages_modified_today: {
276
276
  name: 'roam_find_pages_modified_today',
277
- description: 'Find pages that have been modified today (since midnight), with limit.',
277
+ description: 'Find pages that have been modified today (since midnight), with pagination and sorting options.',
278
278
  inputSchema: {
279
279
  type: 'object',
280
280
  properties: {
281
- max_num_pages: {
281
+ limit: {
282
282
  type: 'integer',
283
- description: 'Max number of pages to retrieve (default: 50)',
283
+ description: 'The maximum number of pages to retrieve (default: 50). Use -1 for no limit, but be aware that very large result sets can impact performance.',
284
284
  default: 50
285
285
  },
286
+ offset: {
287
+ type: 'integer',
288
+ description: 'The number of pages to skip before returning matches. Useful for pagination. Defaults to 0.',
289
+ default: 0
290
+ },
291
+ sort_order: {
292
+ type: 'string',
293
+ description: 'Sort order for pages based on modification date. "desc" for most recent first, "asc" for oldest first.',
294
+ enum: ['asc', 'desc'],
295
+ default: 'desc'
296
+ }
286
297
  }
287
298
  }
288
299
  },
@@ -307,8 +318,8 @@ export const toolSchemas = {
307
318
  },
308
319
  limit: {
309
320
  type: 'integer',
310
- description: 'Optional: The maximum number of results to return. Defaults to 1. Use -1 for no limit.',
311
- default: 1
321
+ description: 'Optional: The maximum number of results to return. Defaults to 50. Use -1 for no limit, but be aware that very large results sets can impact performance.',
322
+ default: 50
312
323
  },
313
324
  offset: {
314
325
  type: 'integer',
@@ -403,7 +414,7 @@ export const toolSchemas = {
403
414
  },
404
415
  roam_datomic_query: {
405
416
  name: 'roam_datomic_query',
406
- description: 'Execute a custom Datomic query on the Roam graph for advanced data retrieval beyond the available search tools. This provides direct access to Roam\'s query engine. Note: Roam graph is case-sensitive.\n\n__Optimal Use Cases for `roam_datomic_query`:__\n- __Regex Search:__ Use for scenarios requiring regex, as Datalog does not natively support full regular expressions. It can fetch broader results for client-side post-processing.\n- __Highly Complex Boolean Logic:__ Ideal for intricate combinations of "AND", "OR", and "NOT" conditions across multiple terms or attributes.\n- __Arbitrary Sorting Criteria:__ The go-to for highly customized sorting needs beyond default options.\n- __Proximity Search:__ For advanced search capabilities involving proximity, which are difficult to implement efficiently with simpler tools.\n\nList of some of Roam\'s data model Namespaces and Attributes: ancestor (descendants), attrs (lookup), block (children, heading, open, order, page, parents, props, refs, string, text-align, uid), children (view-type), create (email, time), descendant (ancestors), edit (email, seen-by, time), entity (attrs), log (id), node (title), page (uid, title), refs (text).\nPredicates (clojure.string/includes?, clojure.string/starts-with?, clojure.string/ends-with?, <, >, <=, >=, =, not=, !=).\nAggregates (distinct, count, sum, max, min, avg, limit).\nTips: Use :block/parents for all ancestor levels, :block/children for direct descendants only; combine clojure.string for complex matching, use distinct to deduplicate, leverage Pull patterns for hierarchies, handle case-sensitivity carefully, and chain ancestry rules for multi-level queries.',
417
+ description: 'Execute a custom Datomic query on the Roam graph for advanced data retrieval beyond the available search tools. This provides direct access to Roam\'s query engine. Note: Roam graph is case-sensitive.\n\n__Optimal Use Cases for `roam_datomic_query`:__\n- __Advanced Filtering (including Regex):__ Use for scenarios requiring complex filtering, including regex matching on results post-query, which Datalog does not natively support for all data types. It can fetch broader results for client-side post-processing.\n- __Highly Complex Boolean Logic:__ Ideal for intricate combinations of "AND", "OR", and "NOT" conditions across multiple terms or attributes.\n- __Arbitrary Sorting Criteria:__ The go-to for highly customized sorting needs beyond default options.\n- __Proximity Search:__ For advanced search capabilities involving proximity, which are difficult to implement efficiently with simpler tools.\n\nList of some of Roam\'s data model Namespaces and Attributes: ancestor (descendants), attrs (lookup), block (children, heading, open, order, page, parents, props, refs, string, text-align, uid), children (view-type), create (email, time), descendant (ancestors), edit (email, seen-by, time), entity (attrs), log (id), node (title), page (uid, title), refs (text).\nPredicates (clojure.string/includes?, clojure.string/starts-with?, clojure.string/ends-with?, <, >, <=, >=, =, not=, !=).\nAggregates (distinct, count, sum, max, min, avg, limit).\nTips: Use :block/parents for all ancestor levels, :block/children for direct descendants only; combine clojure.string for complex matching, use distinct to deduplicate, leverage Pull patterns for hierarchies, handle case-sensitivity carefully, and chain ancestry rules for multi-level queries.',
407
418
  inputSchema: {
408
419
  type: 'object',
409
420
  properties: {
@@ -417,6 +428,21 @@ export const toolSchemas = {
417
428
  items: {
418
429
  type: 'string'
419
430
  }
431
+ },
432
+ regexFilter: {
433
+ type: 'string',
434
+ description: 'Optional: A regex pattern to filter the results client-side after the Datomic query. Applied to JSON.stringify(result) or specific fields if regexTargetField is provided.'
435
+ },
436
+ regexFlags: {
437
+ type: 'string',
438
+ description: 'Optional: Flags for the regex filter (e.g., "i" for case-insensitive, "g" for global).',
439
+ },
440
+ regexTargetField: {
441
+ type: 'array',
442
+ items: {
443
+ type: 'string'
444
+ },
445
+ description: 'Optional: An array of field paths (e.g., ["block_string", "page_title"]) within each Datomic result object to apply the regex filter to. If not provided, the regex is applied to the stringified full result.'
420
446
  }
421
447
  },
422
448
  required: ['query']
@@ -13,6 +13,7 @@ import { DatomicSearchHandlerImpl } from './operations/search/handlers.js';
13
13
  export class ToolHandlers {
14
14
  constructor(graph) {
15
15
  this.graph = graph;
16
+ this.cachedCheatsheet = null;
16
17
  this.pageOps = new PageOperations(graph);
17
18
  this.blockOps = new BlockOperations(graph);
18
19
  this.blockRetrievalOps = new BlockRetrievalOperations(graph); // Initialize new instance
@@ -23,8 +24,8 @@ export class ToolHandlers {
23
24
  this.batchOps = new BatchOperations(graph);
24
25
  }
25
26
  // Page Operations
26
- async findPagesModifiedToday(max_num_pages = 50) {
27
- return this.pageOps.findPagesModifiedToday(max_num_pages);
27
+ async findPagesModifiedToday(limit = 50, offset = 0, sort_order = 'desc') {
28
+ return this.pageOps.findPagesModifiedToday(limit, offset, sort_order);
28
29
  }
29
30
  async createPage(title, content) {
30
31
  return this.pageOps.createPage(title, content);
@@ -83,20 +84,34 @@ export class ToolHandlers {
83
84
  return this.batchOps.processBatch(actions);
84
85
  }
85
86
  async getRoamMarkdownCheatsheet() {
87
+ if (this.cachedCheatsheet) {
88
+ return this.cachedCheatsheet;
89
+ }
86
90
  const __filename = fileURLToPath(import.meta.url);
87
91
  const __dirname = path.dirname(__filename);
88
92
  const cheatsheetPath = path.join(__dirname, '../../Roam_Markdown_Cheatsheet.md');
89
- let cheatsheetContent = fs.readFileSync(cheatsheetPath, 'utf-8');
90
- const customInstructionsPath = process.env.CUSTOM_INSTRUCTIONS_PATH;
91
- if (customInstructionsPath && fs.existsSync(customInstructionsPath)) {
92
- try {
93
- const customInstructionsContent = fs.readFileSync(customInstructionsPath, 'utf-8');
94
- cheatsheetContent += `\n\n${customInstructionsContent}`;
95
- }
96
- catch (error) {
97
- console.warn(`Could not read custom instructions file at ${customInstructionsPath}: ${error}`);
93
+ try {
94
+ let cheatsheetContent = await fs.promises.readFile(cheatsheetPath, 'utf-8');
95
+ const customInstructionsPath = process.env.CUSTOM_INSTRUCTIONS_PATH;
96
+ if (customInstructionsPath) {
97
+ try {
98
+ // Check if file exists asynchronously
99
+ await fs.promises.access(customInstructionsPath);
100
+ const customInstructionsContent = await fs.promises.readFile(customInstructionsPath, 'utf-8');
101
+ cheatsheetContent += `\n\n${customInstructionsContent}`;
102
+ }
103
+ catch (error) {
104
+ // File doesn't exist or is not readable, ignore custom instructions
105
+ if (error.code !== 'ENOENT') {
106
+ console.warn(`Could not read custom instructions file at ${customInstructionsPath}: ${error}`);
107
+ }
108
+ }
98
109
  }
110
+ this.cachedCheatsheet = cheatsheetContent;
111
+ return cheatsheetContent;
112
+ }
113
+ catch (error) {
114
+ throw new Error(`Failed to read cheatsheet: ${error}`);
99
115
  }
100
- return cheatsheetContent;
101
116
  }
102
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roam-research-mcp",
3
- "version": "0.36.0",
3
+ "version": "1.0.0",
4
4
  "description": "A Model Context Protocol (MCP) server for Roam Research API integration",
5
5
  "private": false,
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "homepage": "https://github.com/2b3pro/roam-research-mcp#readme",
23
23
  "type": "module",
24
24
  "bin": {
25
- "roam-research": "./build/index.js"
25
+ "roam-research-mcp": "./build/index.js"
26
26
  },
27
27
  "files": [
28
28
  "build"
@@ -48,4 +48,4 @@
48
48
  "ts-node": "^10.9.2",
49
49
  "typescript": "^5.3.3"
50
50
  }
51
- }
51
+ }