roam-research-mcp 0.27.0 → 0.30.2
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 +106 -17
- package/build/markdown-utils.js +3 -2
- package/build/server/roam-server.js +56 -45
- package/build/tools/operations/batch.js +37 -0
- package/build/tools/operations/memory.js +16 -7
- package/build/tools/operations/outline.js +33 -14
- package/build/tools/operations/todos.js +16 -37
- package/build/tools/schemas.js +89 -134
- package/build/tools/tool-handlers.js +6 -9
- package/build/utils/net.js +38 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -102,25 +102,112 @@ The server provides powerful tools for interacting with Roam Research:
|
|
|
102
102
|
- Detailed debug logging
|
|
103
103
|
- Efficient batch operations
|
|
104
104
|
- Hierarchical outline creation
|
|
105
|
+
- Enhanced documentation for Roam Tables in `Roam_Markdown_Cheatsheet.md` for clearer guidance on nesting.
|
|
105
106
|
|
|
106
|
-
1. `roam_fetch_page_by_title`: Fetch page content by title.
|
|
107
|
+
1. `roam_fetch_page_by_title`: Fetch page content by title. Returns content in the specified format.
|
|
107
108
|
2. `roam_create_page`: Create new pages with optional content and headings.
|
|
108
|
-
3. `
|
|
109
|
-
4. `
|
|
110
|
-
5. `
|
|
111
|
-
6. `
|
|
112
|
-
7. `
|
|
113
|
-
8. `
|
|
114
|
-
9. `
|
|
115
|
-
10. `
|
|
116
|
-
11. `
|
|
117
|
-
12. `
|
|
118
|
-
13. `
|
|
119
|
-
14. `
|
|
120
|
-
15. `
|
|
121
|
-
16. `
|
|
122
|
-
|
|
123
|
-
|
|
109
|
+
3. `roam_import_markdown`: Import nested markdown content under a specific block. (Internally uses `roam_process_batch_actions`.)
|
|
110
|
+
4. `roam_add_todo`: Add a list of todo items to today's daily page. (Internally uses `roam_process_batch_actions`.)
|
|
111
|
+
5. `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`.)
|
|
112
|
+
6. `roam_search_block_refs`: Search for block references within a page or across the entire graph.
|
|
113
|
+
7. `roam_search_hierarchy`: Search for parent or child blocks in the block hierarchy.
|
|
114
|
+
8. `roam_find_pages_modified_today`: Find pages that have been modified today (since midnight).
|
|
115
|
+
9. `roam_search_by_text`: Search for blocks containing specific text.
|
|
116
|
+
10. `roam_search_by_status`: Search for blocks with a specific status (TODO/DONE) across all pages or within a specific page.
|
|
117
|
+
11. `roam_search_by_date`: Search for blocks or pages based on creation or modification dates.
|
|
118
|
+
12. `roam_search_for_tag`: Search for blocks containing a specific tag and optionally filter by blocks that also contain another tag nearby.
|
|
119
|
+
13. `roam_remember`: Add a memory or piece of information to remember. (Internally uses `roam_process_batch_actions`.)
|
|
120
|
+
14. `roam_recall`: Retrieve all stored memories.
|
|
121
|
+
15. `roam_datomic_query`: Execute a custom Datomic query on the Roam graph for advanced data retrieval beyond the available search tools.
|
|
122
|
+
16. `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`.)
|
|
123
|
+
|
|
124
|
+
**Deprecated Tools**:
|
|
125
|
+
The following tools have been deprecated as of `v.0.30.0` in favor of the more powerful and flexible `roam_process_batch_actions`:
|
|
126
|
+
|
|
127
|
+
- `roam_create_block`: Use `roam_process_batch_actions` with the `create-block` action.
|
|
128
|
+
- `roam_update_block`: Use `roam_process_batch_actions` with the `update-block` action.
|
|
129
|
+
- `roam_update_multiple_blocks`: Use `roam_process_batch_actions` with multiple `update-block` actions.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### Tool Usage Guidelines and Best Practices
|
|
134
|
+
|
|
135
|
+
**Pre-computation and Context Loading:**
|
|
136
|
+
✅ Before attempting any Roam operations, **it is highly recommended** to load the `Roam Markdown Cheatsheet` resource into your context. This ensures you have immediate access to the correct Roam-flavored Markdown syntax, including details for tables, block references, and other special formatting. Example prompt: "Read the Roam cheatsheet first. Then, … <rest of your instructions>"
|
|
137
|
+
|
|
138
|
+
**Identifying Pages and Blocks for Manipulation:**
|
|
139
|
+
To ensure accurate operations, always strive to identify target pages and blocks using their Unique Identifiers (UIDs) whenever possible. While some tools accept case-sensitive text titles or content, UIDs provide unambiguous references, reducing the risk of errors due to ambiguity or changes in text.
|
|
140
|
+
|
|
141
|
+
- **For Pages:** Use `roam_fetch_page_by_title` to retrieve a page's UID if you only have its title. Example: "Read the page titled 'Trip to Las Vegas'"
|
|
142
|
+
- **For Blocks:** If you need to manipulate an existing block, first use search tools like `roam_search_by_text`, `roam_search_for_tag`, or `roam_fetch_page_by_title` (with raw format) to find the block and obtain its UID. If the block exists on a page that has already been read, then a search isn't necessary.
|
|
143
|
+
|
|
144
|
+
**Case-Sensitivity:**
|
|
145
|
+
Be aware that text-based inputs (e.g., page titles, block content for search) are generally case-sensitive in Roam. Always match the exact casing of the text as it appears in your graph.
|
|
146
|
+
|
|
147
|
+
**Iterative Refinement and Verification:**
|
|
148
|
+
For complex operations, especially those involving nested structures or multiple changes, it is often beneficial to break down the task into smaller, verifiable steps. After each significant tool call, consider fetching the affected content to verify the changes before proceeding.
|
|
149
|
+
|
|
150
|
+
**Understanding Tool Nuances:**
|
|
151
|
+
Familiarize yourself with the specific behaviors and limitations of each tool. For instance, `roam_create_outline` is best for sequential outlines, while `roam_process_batch_actions` offers granular control for complex structures like tables. Refer to the individual tool descriptions for detailed usage notes.
|
|
152
|
+
|
|
153
|
+
When making changes to your Roam graph, precision in your requests is crucial for achieving desired outcomes.
|
|
154
|
+
|
|
155
|
+
**Specificity in Requests:**
|
|
156
|
+
Some tools allow for identifying blocks or pages by their text content (e.g., `parent_string`, `title`). While convenient, using **Unique Identifiers (UIDs)** is always preferred for accuracy and reliability. Text-based matching can be prone to errors if there are multiple blocks with similar content or if the content changes. Tools are designed to work best when provided with explicit UIDs where available.
|
|
157
|
+
|
|
158
|
+
**Example of Specificity:**
|
|
159
|
+
Instead of:
|
|
160
|
+
`"parent_string": "My project notes"`
|
|
161
|
+
|
|
162
|
+
Prefer:
|
|
163
|
+
`"parent_uid": "((some-unique-uid))"`
|
|
164
|
+
|
|
165
|
+
**Caveat Regarding Heading Formatting:**
|
|
166
|
+
Please note that while the `roam_process_batch_actions` tool can set block headings (H1, H2, H3), directly **removing** an existing heading (i.e., reverting a heading block to a plain text block) through this tool is not currently supported by the Roam API. The `heading` attribute persists its value once set, and attempting to remove it by setting `heading` to `0`, `null`, or omitting the property will not unset the heading.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Example Prompts
|
|
171
|
+
|
|
172
|
+
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.
|
|
173
|
+
|
|
174
|
+
### Example 1: Creating a Project Outline
|
|
175
|
+
|
|
176
|
+
This prompt demonstrates creating a new page and populating it with a structured outline using a single `roam_process_batch_actions` call.
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
"Create a new Roam page titled 'Project Alpha Planning' and add the following outline:
|
|
180
|
+
- Overview
|
|
181
|
+
- Goals
|
|
182
|
+
- Scope
|
|
183
|
+
- Team Members
|
|
184
|
+
- John Doe
|
|
185
|
+
- Jane Smith
|
|
186
|
+
- Tasks
|
|
187
|
+
- Task 1
|
|
188
|
+
- Subtask 1.1
|
|
189
|
+
- Subtask 1.2
|
|
190
|
+
- Task 2
|
|
191
|
+
- Deadlines"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Example 2: Updating Multiple To-Dos and Adding a New One
|
|
195
|
+
|
|
196
|
+
This example shows how to mark existing to-do items as `DONE` and add a new one, all within a single batch.
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
"Mark 'Finish report' and 'Review presentation' as done on today's daily page, and add a new todo 'Prepare for meeting'."
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Example 3: Moving and Updating a Block
|
|
203
|
+
|
|
204
|
+
This demonstrates moving a block from one location to another and simultaneously updating its content.
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
"Move the block 'Important note about client feedback' (from page 'Meeting Notes 2025-06-30') under the 'Action Items' section on the 'Project Alpha Planning' page, and change its content to 'Client feedback reviewed and incorporated'."
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
124
211
|
|
|
125
212
|
## Setup
|
|
126
213
|
|
|
@@ -201,6 +288,8 @@ Each error response includes:
|
|
|
201
288
|
- Detailed error message
|
|
202
289
|
- Suggestions for resolution when applicable
|
|
203
290
|
|
|
291
|
+
---
|
|
292
|
+
|
|
204
293
|
## Development
|
|
205
294
|
|
|
206
295
|
### Building
|
package/build/markdown-utils.js
CHANGED
|
@@ -21,7 +21,7 @@ function convertTableToRoamFormat(text) {
|
|
|
21
21
|
.replace(/^\||\|$/g, '')
|
|
22
22
|
.split('|')
|
|
23
23
|
.map(cell => cell.trim()));
|
|
24
|
-
let roamTable = '{{table}}\n';
|
|
24
|
+
let roamTable = '{{[[table]]}}\n';
|
|
25
25
|
// First row becomes column headers
|
|
26
26
|
const headers = rows[0];
|
|
27
27
|
for (let i = 0; i < headers.length; i++) {
|
|
@@ -284,7 +284,8 @@ function convertToRoamActions(nodes, parentUid, order = 'last') {
|
|
|
284
284
|
block: {
|
|
285
285
|
uid: block.uid,
|
|
286
286
|
string: block.content,
|
|
287
|
-
...(block.heading_level && { heading: block.heading_level })
|
|
287
|
+
...(block.heading_level && { heading: block.heading_level }),
|
|
288
|
+
...(block.children_view_type && { 'children-view-type': block.children_view_type })
|
|
288
289
|
}
|
|
289
290
|
};
|
|
290
291
|
actions.push(action);
|
|
@@ -2,7 +2,7 @@ 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
4
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
5
|
-
import { CallToolRequestSchema, ErrorCode,
|
|
5
|
+
import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
6
6
|
import { initializeGraph } from '@roam-research/roam-api-sdk';
|
|
7
7
|
import { API_TOKEN, GRAPH_NAME, HTTP_STREAM_PORT, SSE_PORT } from '../config/environment.js';
|
|
8
8
|
import { toolSchemas } from '../tools/schemas.js';
|
|
@@ -11,6 +11,7 @@ import { readFileSync } from 'node:fs';
|
|
|
11
11
|
import { join, dirname } from 'node:path';
|
|
12
12
|
import { createServer } from 'node:http';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { findAvailablePort } from '../utils/net.js';
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
16
|
const __dirname = dirname(__filename);
|
|
16
17
|
// Read package.json to get the version
|
|
@@ -19,6 +20,7 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
|
19
20
|
const serverVersion = packageJson.version;
|
|
20
21
|
export class RoamServer {
|
|
21
22
|
constructor() {
|
|
23
|
+
console.log('RoamServer: Constructor started.');
|
|
22
24
|
try {
|
|
23
25
|
this.graph = initializeGraph({
|
|
24
26
|
token: API_TOKEN,
|
|
@@ -40,6 +42,7 @@ export class RoamServer {
|
|
|
40
42
|
if (Object.keys(toolSchemas).length === 0) {
|
|
41
43
|
throw new McpError(ErrorCode.InternalError, 'No tool schemas defined in src/tools/schemas.ts');
|
|
42
44
|
}
|
|
45
|
+
console.log('RoamServer: Constructor finished.');
|
|
43
46
|
}
|
|
44
47
|
// Refactored to accept a Server instance
|
|
45
48
|
setupRequestHandlers(mcpServer) {
|
|
@@ -47,6 +50,32 @@ export class RoamServer {
|
|
|
47
50
|
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
48
51
|
tools: Object.values(toolSchemas),
|
|
49
52
|
}));
|
|
53
|
+
// List available resources
|
|
54
|
+
mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
55
|
+
const resources = [
|
|
56
|
+
{
|
|
57
|
+
name: 'Roam Markdown Cheatsheet',
|
|
58
|
+
uri: 'roam-markdown-cheatsheet.md',
|
|
59
|
+
type: 'text', // Changed from ResourceType.Text to string literal 'text'
|
|
60
|
+
description: 'A cheatsheet for Roam-flavored Markdown syntax.',
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
return { resources };
|
|
64
|
+
});
|
|
65
|
+
// Access resource
|
|
66
|
+
mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
67
|
+
if (request.params.uri === 'roam-markdown-cheatsheet.md') {
|
|
68
|
+
const cheatsheetPath = join(__dirname, '../../Roam_Markdown_Cheatsheet.md');
|
|
69
|
+
try {
|
|
70
|
+
const content = readFileSync(cheatsheetPath, 'utf8');
|
|
71
|
+
return { contents: [{ type: 'text', text: content, uri: request.params.uri }] };
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new McpError(ErrorCode.InternalError, `Resource not found: ${request.params.uri}`); // Changed to InternalError
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
throw new McpError(ErrorCode.InternalError, `Resource not found: ${request.params.uri}`); // Changed to InternalError
|
|
78
|
+
});
|
|
50
79
|
// Handle tool calls
|
|
51
80
|
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
52
81
|
try {
|
|
@@ -72,13 +101,6 @@ export class RoamServer {
|
|
|
72
101
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
73
102
|
};
|
|
74
103
|
}
|
|
75
|
-
case 'roam_create_block': {
|
|
76
|
-
const { content, page_uid, title, heading } = request.params.arguments;
|
|
77
|
-
const result = await this.toolHandlers.createBlock(content, page_uid, title, heading);
|
|
78
|
-
return {
|
|
79
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
104
|
case 'roam_import_markdown': {
|
|
83
105
|
const { content, page_uid, page_title, parent_uid, parent_string, order = 'first' } = request.params.arguments;
|
|
84
106
|
const result = await this.toolHandlers.importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order);
|
|
@@ -153,27 +175,6 @@ export class RoamServer {
|
|
|
153
175
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
154
176
|
};
|
|
155
177
|
}
|
|
156
|
-
case 'roam_update_block': {
|
|
157
|
-
const { block_uid, content, transform_pattern } = request.params.arguments;
|
|
158
|
-
// Validate that either content or transform_pattern is provided, but not both or neither
|
|
159
|
-
if ((!content && !transform_pattern) || (content && transform_pattern)) {
|
|
160
|
-
throw new McpError(ErrorCode.InvalidRequest, 'Either content or transform_pattern must be provided, but not both or neither');
|
|
161
|
-
}
|
|
162
|
-
let result;
|
|
163
|
-
if (content) {
|
|
164
|
-
result = await this.toolHandlers.updateBlock(block_uid, content);
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
// We know transform_pattern exists due to validation above
|
|
168
|
-
result = await this.toolHandlers.updateBlock(block_uid, undefined, (currentContent) => {
|
|
169
|
-
const regex = new RegExp(transform_pattern.find, transform_pattern.global !== false ? 'g' : '');
|
|
170
|
-
return currentContent.replace(regex, transform_pattern.replace);
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
return {
|
|
174
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
178
|
case 'roam_recall': {
|
|
178
179
|
const { sort_by = 'newest', filter_tag } = request.params.arguments;
|
|
179
180
|
const result = await this.toolHandlers.recall(sort_by, filter_tag);
|
|
@@ -181,22 +182,16 @@ export class RoamServer {
|
|
|
181
182
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
182
183
|
};
|
|
183
184
|
}
|
|
184
|
-
case '
|
|
185
|
-
const {
|
|
186
|
-
|
|
187
|
-
for (const update of updates) {
|
|
188
|
-
if ((!update.content && !update.transform) || (update.content && update.transform)) {
|
|
189
|
-
throw new McpError(ErrorCode.InvalidRequest, 'For each update, either content or transform must be provided, but not both or neither');
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
const result = await this.toolHandlers.updateBlocks(updates);
|
|
185
|
+
case 'roam_datomic_query': {
|
|
186
|
+
const { query, inputs } = request.params.arguments;
|
|
187
|
+
const result = await this.toolHandlers.executeDatomicQuery({ query, inputs });
|
|
193
188
|
return {
|
|
194
189
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
195
190
|
};
|
|
196
191
|
}
|
|
197
|
-
case '
|
|
198
|
-
const {
|
|
199
|
-
const result = await this.toolHandlers.
|
|
192
|
+
case 'roam_process_batch_actions': {
|
|
193
|
+
const { actions } = request.params.arguments;
|
|
194
|
+
const result = await this.toolHandlers.processBatch(actions);
|
|
200
195
|
return {
|
|
201
196
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
202
197
|
};
|
|
@@ -215,7 +210,9 @@ export class RoamServer {
|
|
|
215
210
|
});
|
|
216
211
|
}
|
|
217
212
|
async run() {
|
|
213
|
+
console.log('RoamServer: run() method started.');
|
|
218
214
|
try {
|
|
215
|
+
console.log('RoamServer: Attempting to create stdioMcpServer...');
|
|
219
216
|
const stdioMcpServer = new Server({
|
|
220
217
|
name: 'roam-research',
|
|
221
218
|
version: serverVersion,
|
|
@@ -224,11 +221,17 @@ export class RoamServer {
|
|
|
224
221
|
tools: {
|
|
225
222
|
...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName, {}])),
|
|
226
223
|
},
|
|
224
|
+
resources: {
|
|
225
|
+
'roam-markdown-cheatsheet.md': {}
|
|
226
|
+
}
|
|
227
227
|
},
|
|
228
228
|
});
|
|
229
|
+
console.log('RoamServer: stdioMcpServer created. Setting up request handlers...');
|
|
229
230
|
this.setupRequestHandlers(stdioMcpServer);
|
|
231
|
+
console.log('RoamServer: stdioMcpServer handlers setup complete. Connecting transport...');
|
|
230
232
|
const stdioTransport = new StdioServerTransport();
|
|
231
233
|
await stdioMcpServer.connect(stdioTransport);
|
|
234
|
+
console.log('RoamServer: stdioTransport connected. Attempting to create httpMcpServer...');
|
|
232
235
|
const httpMcpServer = new Server({
|
|
233
236
|
name: 'roam-research-http', // A distinct name for the HTTP server
|
|
234
237
|
version: serverVersion,
|
|
@@ -237,13 +240,19 @@ export class RoamServer {
|
|
|
237
240
|
tools: {
|
|
238
241
|
...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName, {}])),
|
|
239
242
|
},
|
|
243
|
+
resources: {
|
|
244
|
+
'roam-markdown-cheatsheet.md': {}
|
|
245
|
+
}
|
|
240
246
|
},
|
|
241
247
|
});
|
|
248
|
+
console.log('RoamServer: httpMcpServer created. Setting up request handlers...');
|
|
242
249
|
this.setupRequestHandlers(httpMcpServer);
|
|
250
|
+
console.log('RoamServer: httpMcpServer handlers setup complete. Connecting transport...');
|
|
243
251
|
const httpStreamTransport = new StreamableHTTPServerTransport({
|
|
244
252
|
sessionIdGenerator: () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15),
|
|
245
253
|
});
|
|
246
254
|
await httpMcpServer.connect(httpStreamTransport);
|
|
255
|
+
console.log('RoamServer: httpStreamTransport connected.');
|
|
247
256
|
const httpServer = createServer(async (req, res) => {
|
|
248
257
|
try {
|
|
249
258
|
await httpStreamTransport.handleRequest(req, res);
|
|
@@ -256,8 +265,9 @@ export class RoamServer {
|
|
|
256
265
|
}
|
|
257
266
|
}
|
|
258
267
|
});
|
|
259
|
-
|
|
260
|
-
|
|
268
|
+
const availableHttpPort = await findAvailablePort(parseInt(HTTP_STREAM_PORT));
|
|
269
|
+
httpServer.listen(availableHttpPort, () => {
|
|
270
|
+
// console.log(`MCP Roam Research server running HTTP Stream on port ${availableHttpPort}`);
|
|
261
271
|
});
|
|
262
272
|
// SSE Server setup
|
|
263
273
|
const sseMcpServer = new Server({
|
|
@@ -318,8 +328,9 @@ export class RoamServer {
|
|
|
318
328
|
}
|
|
319
329
|
}
|
|
320
330
|
});
|
|
321
|
-
|
|
322
|
-
|
|
331
|
+
const availableSsePort = await findAvailablePort(parseInt(SSE_PORT));
|
|
332
|
+
sseHttpServer.listen(availableSsePort, () => {
|
|
333
|
+
// console.log(`MCP Roam Research server running SSE on port ${availableSsePort}`);
|
|
323
334
|
});
|
|
324
335
|
}
|
|
325
336
|
catch (error) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { batchActions as roamBatchActions } from '@roam-research/roam-api-sdk';
|
|
2
|
+
export class BatchOperations {
|
|
3
|
+
constructor(graph) {
|
|
4
|
+
this.graph = graph;
|
|
5
|
+
}
|
|
6
|
+
async processBatch(actions) {
|
|
7
|
+
const batchActions = actions.map(action => {
|
|
8
|
+
const { action: actionType, ...rest } = action;
|
|
9
|
+
const roamAction = { action: actionType };
|
|
10
|
+
if (rest.location) {
|
|
11
|
+
roamAction.location = {
|
|
12
|
+
'parent-uid': rest.location['parent-uid'],
|
|
13
|
+
order: rest.location.order,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const block = {};
|
|
17
|
+
if (rest.string)
|
|
18
|
+
block.string = rest.string;
|
|
19
|
+
if (rest.uid)
|
|
20
|
+
block.uid = rest.uid;
|
|
21
|
+
if (rest.open !== undefined)
|
|
22
|
+
block.open = rest.open;
|
|
23
|
+
if (rest.heading !== undefined && rest.heading !== null && rest.heading !== 0) {
|
|
24
|
+
block.heading = rest.heading;
|
|
25
|
+
}
|
|
26
|
+
if (rest['text-align'])
|
|
27
|
+
block['text-align'] = rest['text-align'];
|
|
28
|
+
if (rest['children-view-type'])
|
|
29
|
+
block['children-view-type'] = rest['children-view-type'];
|
|
30
|
+
if (Object.keys(block).length > 0) {
|
|
31
|
+
roamAction.block = block;
|
|
32
|
+
}
|
|
33
|
+
return roamAction;
|
|
34
|
+
});
|
|
35
|
+
return await roamBatchActions(this.graph, { actions: batchActions });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { q,
|
|
1
|
+
import { q, createPage, batchActions } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { formatRoamDate } from '../../utils/helpers.js';
|
|
4
4
|
import { resolveRefs } from '../helpers/refs.js';
|
|
@@ -49,18 +49,27 @@ export class MemoryOperations {
|
|
|
49
49
|
}).join(' ') || '';
|
|
50
50
|
// Create block with memory, memories tag, and optional categories
|
|
51
51
|
const blockContent = `${memoriesTag} ${memory} ${categoryTags}`.trim();
|
|
52
|
-
|
|
53
|
-
await createBlock(this.graph, {
|
|
52
|
+
const actions = [{
|
|
54
53
|
action: 'create-block',
|
|
55
54
|
location: {
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
'parent-uid': pageUid,
|
|
56
|
+
order: 'last'
|
|
58
57
|
},
|
|
59
|
-
block: {
|
|
58
|
+
block: {
|
|
59
|
+
string: blockContent
|
|
60
|
+
}
|
|
61
|
+
}];
|
|
62
|
+
try {
|
|
63
|
+
const result = await batchActions(this.graph, {
|
|
64
|
+
action: 'batch-actions',
|
|
65
|
+
actions
|
|
60
66
|
});
|
|
67
|
+
if (!result) {
|
|
68
|
+
throw new McpError(ErrorCode.InternalError, 'Failed to create memory block via batch action');
|
|
69
|
+
}
|
|
61
70
|
}
|
|
62
71
|
catch (error) {
|
|
63
|
-
throw new McpError(ErrorCode.InternalError,
|
|
72
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create memory block: ${error instanceof Error ? error.message : String(error)}`);
|
|
64
73
|
}
|
|
65
74
|
return { success: true };
|
|
66
75
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { q, createPage,
|
|
1
|
+
import { q, createPage, batchActions } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { formatRoamDate } from '../../utils/helpers.js';
|
|
4
4
|
import { capitalizeWords } from '../helpers/text.js';
|
|
@@ -138,15 +138,21 @@ export class OutlineOperations {
|
|
|
138
138
|
}
|
|
139
139
|
for (let retry = 0; retry < maxRetries; retry++) {
|
|
140
140
|
console.log(`Attempt ${retry + 1}/${maxRetries} to create block "${content}" under "${parentUid}"`);
|
|
141
|
-
// Create block
|
|
142
|
-
const
|
|
143
|
-
action: '
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
}]
|
|
149
152
|
});
|
|
153
|
+
if (!batchResult) {
|
|
154
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create block "${content}" via batch action`);
|
|
155
|
+
}
|
|
150
156
|
// Wait with exponential backoff
|
|
151
157
|
const delay = initialDelay * Math.pow(2, retry);
|
|
152
158
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
@@ -232,7 +238,13 @@ export class OutlineOperations {
|
|
|
232
238
|
// Convert to Roam markdown format
|
|
233
239
|
const convertedContent = convertToRoamMarkdown(markdownContent);
|
|
234
240
|
// Parse markdown into hierarchical structure
|
|
235
|
-
|
|
241
|
+
// We pass the original OutlineItem properties (heading, children_view_type)
|
|
242
|
+
// along with the parsed content to the nodes.
|
|
243
|
+
const nodes = parseMarkdown(convertedContent).map((node, index) => ({
|
|
244
|
+
...node,
|
|
245
|
+
...(validOutline[index].heading && { heading_level: validOutline[index].heading }),
|
|
246
|
+
...(validOutline[index].children_view_type && { children_view_type: validOutline[index].children_view_type })
|
|
247
|
+
}));
|
|
236
248
|
// Convert nodes to batch actions
|
|
237
249
|
const actions = convertToRoamActions(nodes, targetParentUid, 'first');
|
|
238
250
|
if (actions.length === 0) {
|
|
@@ -350,19 +362,26 @@ export class OutlineOperations {
|
|
|
350
362
|
};
|
|
351
363
|
}
|
|
352
364
|
else {
|
|
353
|
-
// Create a simple block for non-nested content
|
|
354
|
-
|
|
355
|
-
await createBlock(this.graph, {
|
|
365
|
+
// Create a simple block for non-nested content using batchActions
|
|
366
|
+
const actions = [{
|
|
356
367
|
action: 'create-block',
|
|
357
368
|
location: {
|
|
358
369
|
"parent-uid": targetParentUid,
|
|
359
370
|
order
|
|
360
371
|
},
|
|
361
372
|
block: { string: content }
|
|
373
|
+
}];
|
|
374
|
+
try {
|
|
375
|
+
const result = await batchActions(this.graph, {
|
|
376
|
+
action: 'batch-actions',
|
|
377
|
+
actions
|
|
362
378
|
});
|
|
379
|
+
if (!result) {
|
|
380
|
+
throw new McpError(ErrorCode.InternalError, 'Failed to create content block via batch action');
|
|
381
|
+
}
|
|
363
382
|
}
|
|
364
383
|
catch (error) {
|
|
365
|
-
throw new McpError(ErrorCode.InternalError,
|
|
384
|
+
throw new McpError(ErrorCode.InternalError, `Failed to create content block: ${error instanceof Error ? error.message : String(error)}`);
|
|
366
385
|
}
|
|
367
386
|
return {
|
|
368
387
|
success: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { q,
|
|
1
|
+
import { q, createPage, batchActions } from '@roam-research/roam-api-sdk';
|
|
2
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { formatRoamDate } from '../../utils/helpers.js';
|
|
4
4
|
export class TodoOperations {
|
|
@@ -37,44 +37,23 @@ export class TodoOperations {
|
|
|
37
37
|
throw new Error('Failed to create today\'s page');
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
// If more than 10 todos, use batch actions
|
|
41
40
|
const todo_tag = "{{TODO}}";
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
string: `${todo_tag} ${todo}`
|
|
51
|
-
}
|
|
52
|
-
}));
|
|
53
|
-
const result = await batchActions(this.graph, {
|
|
54
|
-
action: 'batch-actions',
|
|
55
|
-
actions
|
|
56
|
-
});
|
|
57
|
-
if (!result) {
|
|
58
|
-
throw new Error('Failed to create todo blocks');
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
// Create todos sequentially
|
|
63
|
-
for (const todo of todos) {
|
|
64
|
-
try {
|
|
65
|
-
await createBlock(this.graph, {
|
|
66
|
-
action: 'create-block',
|
|
67
|
-
location: {
|
|
68
|
-
"parent-uid": targetPageUid,
|
|
69
|
-
"order": "last"
|
|
70
|
-
},
|
|
71
|
-
block: { string: `${todo_tag} ${todo}` }
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
throw new Error('Failed to create todo block');
|
|
76
|
-
}
|
|
41
|
+
const actions = todos.map((todo, index) => ({
|
|
42
|
+
action: 'create-block',
|
|
43
|
+
location: {
|
|
44
|
+
'parent-uid': targetPageUid,
|
|
45
|
+
order: index
|
|
46
|
+
},
|
|
47
|
+
block: {
|
|
48
|
+
string: `${todo_tag} ${todo}`
|
|
77
49
|
}
|
|
50
|
+
}));
|
|
51
|
+
const result = await batchActions(this.graph, {
|
|
52
|
+
action: 'batch-actions',
|
|
53
|
+
actions
|
|
54
|
+
});
|
|
55
|
+
if (!result) {
|
|
56
|
+
throw new Error('Failed to create todo blocks');
|
|
78
57
|
}
|
|
79
58
|
return { success: true };
|
|
80
59
|
}
|
package/build/tools/schemas.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export const toolSchemas = {
|
|
3
3
|
roam_add_todo: {
|
|
4
4
|
name: 'roam_add_todo',
|
|
5
|
-
description: 'Add a list of todo items as individual blocks to today\'s daily page in Roam. Each item becomes its own actionable block with todo status.\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).',
|
|
5
|
+
description: 'Add a list of todo items as individual blocks to today\'s daily page in Roam. Each item becomes its own actionable block with todo status.\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).\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
6
6
|
inputSchema: {
|
|
7
7
|
type: 'object',
|
|
8
8
|
properties: {
|
|
@@ -20,7 +20,7 @@ export const toolSchemas = {
|
|
|
20
20
|
},
|
|
21
21
|
roam_fetch_page_by_title: {
|
|
22
22
|
name: 'roam_fetch_page_by_title',
|
|
23
|
-
description: 'Fetch page by title
|
|
23
|
+
description: 'Fetch page by title. Returns content in the specified format.',
|
|
24
24
|
inputSchema: {
|
|
25
25
|
type: 'object',
|
|
26
26
|
properties: {
|
|
@@ -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.',
|
|
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.',
|
|
44
44
|
inputSchema: {
|
|
45
45
|
type: 'object',
|
|
46
46
|
properties: {
|
|
@@ -78,47 +78,19 @@ export const toolSchemas = {
|
|
|
78
78
|
required: ['title'],
|
|
79
79
|
},
|
|
80
80
|
},
|
|
81
|
-
roam_create_block: {
|
|
82
|
-
name: 'roam_create_block',
|
|
83
|
-
description: 'Add new block to an existing Roam page. If no page specified, adds to today\'s daily note. Best for capturing immediate thoughts, additions to discussions, or content that doesn\'t warrant its own page. Can specify page by title or UID.\nNOTE on Roam-flavored markdown: For direct linking: use [[link]] syntax. For aliased linking, use [alias]([[link]]) syntax. Do not concatenate words in links/hashtags - correct: #[[multiple words]] #self-esteem (for typically hyphenated words).',
|
|
84
|
-
inputSchema: {
|
|
85
|
-
type: 'object',
|
|
86
|
-
properties: {
|
|
87
|
-
content: {
|
|
88
|
-
type: 'string',
|
|
89
|
-
description: 'Content of the block',
|
|
90
|
-
},
|
|
91
|
-
page_uid: {
|
|
92
|
-
type: 'string',
|
|
93
|
-
description: 'Optional: UID of the page to add block to',
|
|
94
|
-
},
|
|
95
|
-
title: {
|
|
96
|
-
type: 'string',
|
|
97
|
-
description: 'Optional: Title of the page to add block to (defaults to today\'s date if neither page_uid nor title provided)',
|
|
98
|
-
},
|
|
99
|
-
heading: {
|
|
100
|
-
type: 'integer',
|
|
101
|
-
description: 'Optional: Heading formatting for this block (1-3)',
|
|
102
|
-
minimum: 1,
|
|
103
|
-
maximum: 3
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
required: ['content'],
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
81
|
roam_create_outline: {
|
|
110
82
|
name: 'roam_create_outline',
|
|
111
|
-
description: 'Add a structured outline to an existing page or block (by title text or uid), with customizable nesting levels. 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',
|
|
83
|
+
description: 'Add a structured outline to an existing page or block (by title text or uid), with customizable nesting levels. 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.',
|
|
112
84
|
inputSchema: {
|
|
113
85
|
type: 'object',
|
|
114
86
|
properties: {
|
|
115
87
|
page_title_uid: {
|
|
116
88
|
type: 'string',
|
|
117
|
-
description: 'Title
|
|
89
|
+
description: 'Title or UID of the page (UID is preferred for accuracy). Leave blank to use the default daily page.'
|
|
118
90
|
},
|
|
119
91
|
block_text_uid: {
|
|
120
92
|
type: 'string',
|
|
121
|
-
description: '
|
|
93
|
+
description: 'The text content or UID of the block to nest the outline under (UID is preferred for accuracy). If blank, content is nested directly under the page (or the default daily page if page_title_uid is also blank).'
|
|
122
94
|
},
|
|
123
95
|
outline: {
|
|
124
96
|
type: 'array',
|
|
@@ -141,6 +113,11 @@ export const toolSchemas = {
|
|
|
141
113
|
description: 'Optional: Heading formatting for this block (1-3)',
|
|
142
114
|
minimum: 1,
|
|
143
115
|
maximum: 3
|
|
116
|
+
},
|
|
117
|
+
children_view_type: {
|
|
118
|
+
type: 'string',
|
|
119
|
+
description: 'Optional: The view type for children of this block ("bullet", "document", or "numbered")',
|
|
120
|
+
enum: ["bullet", "document", "numbered"]
|
|
144
121
|
}
|
|
145
122
|
},
|
|
146
123
|
required: ['text', 'level']
|
|
@@ -152,7 +129,7 @@ export const toolSchemas = {
|
|
|
152
129
|
},
|
|
153
130
|
roam_import_markdown: {
|
|
154
131
|
name: 'roam_import_markdown',
|
|
155
|
-
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.',
|
|
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.',
|
|
156
133
|
inputSchema: {
|
|
157
134
|
type: 'object',
|
|
158
135
|
properties: {
|
|
@@ -162,19 +139,19 @@ export const toolSchemas = {
|
|
|
162
139
|
},
|
|
163
140
|
page_uid: {
|
|
164
141
|
type: 'string',
|
|
165
|
-
description: 'Optional: UID of the page containing the parent block'
|
|
142
|
+
description: 'Optional: UID of the page containing the parent block (preferred for accuracy).'
|
|
166
143
|
},
|
|
167
144
|
page_title: {
|
|
168
145
|
type: 'string',
|
|
169
|
-
description: 'Optional: Title of the page containing the parent block (
|
|
146
|
+
description: 'Optional: Title of the page containing the parent block (used if page_uid is not provided).'
|
|
170
147
|
},
|
|
171
148
|
parent_uid: {
|
|
172
149
|
type: 'string',
|
|
173
|
-
description: 'Optional: UID of the parent block to add content under'
|
|
150
|
+
description: 'Optional: UID of the parent block to add content under (preferred for accuracy).'
|
|
174
151
|
},
|
|
175
152
|
parent_string: {
|
|
176
153
|
type: 'string',
|
|
177
|
-
description: 'Optional: Exact string content of the parent block to add content under (
|
|
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).'
|
|
178
155
|
},
|
|
179
156
|
order: {
|
|
180
157
|
type: 'string',
|
|
@@ -198,7 +175,7 @@ export const toolSchemas = {
|
|
|
198
175
|
},
|
|
199
176
|
page_title_uid: {
|
|
200
177
|
type: 'string',
|
|
201
|
-
description: 'Optional: Title or UID of the page to search in. Defaults to today\'s daily page if not provided',
|
|
178
|
+
description: 'Optional: Title or UID of the page to search in (UID is preferred for accuracy). Defaults to today\'s daily page if not provided.',
|
|
202
179
|
},
|
|
203
180
|
near_tag: {
|
|
204
181
|
type: 'string',
|
|
@@ -221,7 +198,7 @@ export const toolSchemas = {
|
|
|
221
198
|
},
|
|
222
199
|
page_title_uid: {
|
|
223
200
|
type: 'string',
|
|
224
|
-
description: 'Optional: Title or UID of the page to search in. If not provided, searches across all pages'
|
|
201
|
+
description: 'Optional: Title or UID of the page to search in (UID is preferred for accuracy). If not provided, searches across all pages.'
|
|
225
202
|
},
|
|
226
203
|
include: {
|
|
227
204
|
type: 'string',
|
|
@@ -247,7 +224,7 @@ export const toolSchemas = {
|
|
|
247
224
|
},
|
|
248
225
|
page_title_uid: {
|
|
249
226
|
type: 'string',
|
|
250
|
-
description: 'Optional: Title or UID of the page to search in. If not provided, searches across all pages'
|
|
227
|
+
description: 'Optional: Title or UID of the page to search in (UID is preferred for accuracy). If not provided, searches across all pages.'
|
|
251
228
|
}
|
|
252
229
|
}
|
|
253
230
|
}
|
|
@@ -268,7 +245,7 @@ export const toolSchemas = {
|
|
|
268
245
|
},
|
|
269
246
|
page_title_uid: {
|
|
270
247
|
type: 'string',
|
|
271
|
-
description: 'Optional: Title or UID of the page to search in'
|
|
248
|
+
description: 'Optional: Title or UID of the page to search in (UID is preferred for accuracy).'
|
|
272
249
|
},
|
|
273
250
|
max_depth: {
|
|
274
251
|
type: 'integer',
|
|
@@ -306,100 +283,12 @@ export const toolSchemas = {
|
|
|
306
283
|
},
|
|
307
284
|
page_title_uid: {
|
|
308
285
|
type: 'string',
|
|
309
|
-
description: 'Optional: Title or UID of the page to search in. If not provided, searches across all pages'
|
|
286
|
+
description: 'Optional: Title or UID of the page to search in (UID is preferred for accuracy). If not provided, searches across all pages.'
|
|
310
287
|
}
|
|
311
288
|
},
|
|
312
289
|
required: ['text']
|
|
313
290
|
}
|
|
314
291
|
},
|
|
315
|
-
roam_update_block: {
|
|
316
|
-
name: 'roam_update_block',
|
|
317
|
-
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 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
|
-
inputSchema: {
|
|
319
|
-
type: 'object',
|
|
320
|
-
properties: {
|
|
321
|
-
block_uid: {
|
|
322
|
-
type: 'string',
|
|
323
|
-
description: 'UID of the block to update'
|
|
324
|
-
},
|
|
325
|
-
content: {
|
|
326
|
-
type: 'string',
|
|
327
|
-
description: 'New content for the block. If not provided, transform_pattern will be used.'
|
|
328
|
-
},
|
|
329
|
-
transform_pattern: {
|
|
330
|
-
type: 'object',
|
|
331
|
-
description: 'Pattern to transform the current content. Used if content is not provided.',
|
|
332
|
-
properties: {
|
|
333
|
-
find: {
|
|
334
|
-
type: 'string',
|
|
335
|
-
description: 'Text or regex pattern to find'
|
|
336
|
-
},
|
|
337
|
-
replace: {
|
|
338
|
-
type: 'string',
|
|
339
|
-
description: 'Text to replace with'
|
|
340
|
-
},
|
|
341
|
-
global: {
|
|
342
|
-
type: 'boolean',
|
|
343
|
-
description: 'Whether to replace all occurrences',
|
|
344
|
-
default: true
|
|
345
|
-
}
|
|
346
|
-
},
|
|
347
|
-
required: ['find', 'replace']
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
required: ['block_uid']
|
|
351
|
-
// Note: Validation for either content or transform_pattern is handled in the server code
|
|
352
|
-
}
|
|
353
|
-
},
|
|
354
|
-
roam_update_multiple_blocks: {
|
|
355
|
-
name: 'roam_update_multiple_blocks',
|
|
356
|
-
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).',
|
|
357
|
-
inputSchema: {
|
|
358
|
-
type: 'object',
|
|
359
|
-
properties: {
|
|
360
|
-
updates: {
|
|
361
|
-
type: 'array',
|
|
362
|
-
description: 'Array of block updates to perform',
|
|
363
|
-
items: {
|
|
364
|
-
type: 'object',
|
|
365
|
-
properties: {
|
|
366
|
-
block_uid: {
|
|
367
|
-
type: 'string',
|
|
368
|
-
description: 'UID of the block to update'
|
|
369
|
-
},
|
|
370
|
-
content: {
|
|
371
|
-
type: 'string',
|
|
372
|
-
description: 'New content for the block. If not provided, transform will be used.'
|
|
373
|
-
},
|
|
374
|
-
transform: {
|
|
375
|
-
type: 'object',
|
|
376
|
-
description: 'Pattern to transform the current content. Used if content is not provided.',
|
|
377
|
-
properties: {
|
|
378
|
-
find: {
|
|
379
|
-
type: 'string',
|
|
380
|
-
description: 'Text or regex pattern to find'
|
|
381
|
-
},
|
|
382
|
-
replace: {
|
|
383
|
-
type: 'string',
|
|
384
|
-
description: 'Text to replace with'
|
|
385
|
-
},
|
|
386
|
-
global: {
|
|
387
|
-
type: 'boolean',
|
|
388
|
-
description: 'Whether to replace all occurrences',
|
|
389
|
-
default: true
|
|
390
|
-
}
|
|
391
|
-
},
|
|
392
|
-
required: ['find', 'replace']
|
|
393
|
-
}
|
|
394
|
-
},
|
|
395
|
-
required: ['block_uid']
|
|
396
|
-
// Note: Validation for either content or transform is handled in the server code
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
required: ['updates']
|
|
401
|
-
}
|
|
402
|
-
},
|
|
403
292
|
roam_search_by_date: {
|
|
404
293
|
name: 'roam_search_by_date',
|
|
405
294
|
description: 'Search for blocks or pages based on creation or modification dates. Not for daily pages with ordinal date titles.',
|
|
@@ -435,7 +324,7 @@ export const toolSchemas = {
|
|
|
435
324
|
},
|
|
436
325
|
roam_remember: {
|
|
437
326
|
name: 'roam_remember',
|
|
438
|
-
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).',
|
|
327
|
+
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).\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
439
328
|
inputSchema: {
|
|
440
329
|
type: 'object',
|
|
441
330
|
properties: {
|
|
@@ -475,7 +364,7 @@ export const toolSchemas = {
|
|
|
475
364
|
},
|
|
476
365
|
roam_datomic_query: {
|
|
477
366
|
name: 'roam_datomic_query',
|
|
478
|
-
description: 'Execute a custom Datomic query on the Roam graph beyond the available search tools. This provides direct access to Roam\'s query engine
|
|
367
|
+
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.\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.',
|
|
479
368
|
inputSchema: {
|
|
480
369
|
type: 'object',
|
|
481
370
|
properties: {
|
|
@@ -493,5 +382,71 @@ export const toolSchemas = {
|
|
|
493
382
|
},
|
|
494
383
|
required: ['query']
|
|
495
384
|
}
|
|
385
|
+
},
|
|
386
|
+
roam_process_batch_actions: {
|
|
387
|
+
name: 'roam_process_batch_actions',
|
|
388
|
+
description: 'Executes a sequence of low-level block actions (create, update, move, delete) in a single, non-transactional batch. Actions are executed in the provided order. For creating nested blocks, you can use a temporary client-side UID in a parent block and refer to it in a child block within the same batch. For actions on existing blocks, a valid block UID is required. Note: Roam-flavored markdown, including block embedding with `((UID))` syntax, is supported within the `string` property for `create-block` and `update-block` actions. For actions on existing blocks or within a specific page context, it is often necessary to first obtain valid page or block UIDs. Tools like `roam_fetch_page_by_title` or other search tools can be used to retrieve these UIDs before executing batch actions. For simpler, sequential outlines, `roam_create_outline` is often more suitable.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: 'object',
|
|
391
|
+
properties: {
|
|
392
|
+
actions: {
|
|
393
|
+
type: 'array',
|
|
394
|
+
description: 'An array of action objects to execute in order.',
|
|
395
|
+
items: {
|
|
396
|
+
type: 'object',
|
|
397
|
+
properties: {
|
|
398
|
+
"action": {
|
|
399
|
+
type: 'string',
|
|
400
|
+
description: 'The specific action to perform.',
|
|
401
|
+
enum: ['create-block', 'update-block', 'move-block', 'delete-block']
|
|
402
|
+
},
|
|
403
|
+
"uid": {
|
|
404
|
+
type: 'string',
|
|
405
|
+
description: 'The UID of the block to target for "update-block", "move-block", or "delete-block" actions.'
|
|
406
|
+
},
|
|
407
|
+
"string": {
|
|
408
|
+
type: 'string',
|
|
409
|
+
description: 'The content for the block, used in "create-block" and "update-block" actions.'
|
|
410
|
+
},
|
|
411
|
+
"open": {
|
|
412
|
+
type: "boolean",
|
|
413
|
+
description: "Optional: Sets the open/closed state of a block, used in 'update-block' or 'create-block'. Defaults to true."
|
|
414
|
+
},
|
|
415
|
+
"heading": {
|
|
416
|
+
type: "integer",
|
|
417
|
+
description: "Optional: The heading level (1, 2, or 3) for 'create-block' or 'update-block'.",
|
|
418
|
+
enum: [1, 2, 3]
|
|
419
|
+
},
|
|
420
|
+
"text-align": {
|
|
421
|
+
type: "string",
|
|
422
|
+
description: "Optional: The text alignment for 'create-block' or 'update-block'.",
|
|
423
|
+
enum: ["left", "center", "right", "justify"]
|
|
424
|
+
},
|
|
425
|
+
"children-view-type": {
|
|
426
|
+
type: "string",
|
|
427
|
+
description: "Optional: The view type for children of the block, for 'create-block' or 'update-block'.",
|
|
428
|
+
enum: ["bullet", "document", "numbered"]
|
|
429
|
+
},
|
|
430
|
+
"location": {
|
|
431
|
+
type: 'object',
|
|
432
|
+
description: 'Specifies where to place a block, used in "create-block" and "move-block" actions.',
|
|
433
|
+
properties: {
|
|
434
|
+
"parent-uid": {
|
|
435
|
+
type: 'string',
|
|
436
|
+
description: 'The UID of the parent block or page.'
|
|
437
|
+
},
|
|
438
|
+
"order": {
|
|
439
|
+
type: ['number', 'string'],
|
|
440
|
+
description: 'The position of the block under its parent (0 for top, "last" for bottom).'
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
required: ['action']
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
required: ['actions']
|
|
450
|
+
}
|
|
496
451
|
}
|
|
497
452
|
};
|
|
@@ -4,6 +4,7 @@ import { SearchOperations } from './operations/search/index.js';
|
|
|
4
4
|
import { MemoryOperations } from './operations/memory.js';
|
|
5
5
|
import { TodoOperations } from './operations/todos.js';
|
|
6
6
|
import { OutlineOperations } from './operations/outline.js';
|
|
7
|
+
import { BatchOperations } from './operations/batch.js';
|
|
7
8
|
import { DatomicSearchHandlerImpl } from './operations/search/handlers.js';
|
|
8
9
|
export class ToolHandlers {
|
|
9
10
|
constructor(graph) {
|
|
@@ -14,6 +15,7 @@ export class ToolHandlers {
|
|
|
14
15
|
this.memoryOps = new MemoryOperations(graph);
|
|
15
16
|
this.todoOps = new TodoOperations(graph);
|
|
16
17
|
this.outlineOps = new OutlineOperations(graph);
|
|
18
|
+
this.batchOps = new BatchOperations(graph);
|
|
17
19
|
}
|
|
18
20
|
// Page Operations
|
|
19
21
|
async findPagesModifiedToday(max_num_pages = 50) {
|
|
@@ -26,15 +28,6 @@ export class ToolHandlers {
|
|
|
26
28
|
return this.pageOps.fetchPageByTitle(title, format);
|
|
27
29
|
}
|
|
28
30
|
// Block Operations
|
|
29
|
-
async createBlock(content, page_uid, title, heading) {
|
|
30
|
-
return this.blockOps.createBlock(content, page_uid, title, heading);
|
|
31
|
-
}
|
|
32
|
-
async updateBlock(block_uid, content, transform) {
|
|
33
|
-
return this.blockOps.updateBlock(block_uid, content, transform);
|
|
34
|
-
}
|
|
35
|
-
async updateBlocks(updates) {
|
|
36
|
-
return this.blockOps.updateBlocks(updates);
|
|
37
|
-
}
|
|
38
31
|
// Search Operations
|
|
39
32
|
async searchByStatus(status, page_title_uid, include, exclude) {
|
|
40
33
|
return this.searchOps.searchByStatus(status, page_title_uid, include, exclude);
|
|
@@ -77,4 +70,8 @@ export class ToolHandlers {
|
|
|
77
70
|
async importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order = 'first') {
|
|
78
71
|
return this.outlineOps.importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order);
|
|
79
72
|
}
|
|
73
|
+
// Batch Operations
|
|
74
|
+
async processBatch(actions) {
|
|
75
|
+
return this.batchOps.processBatch(actions);
|
|
76
|
+
}
|
|
80
77
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
/**
|
|
3
|
+
* Checks if a given port is currently in use.
|
|
4
|
+
* @param port The port to check.
|
|
5
|
+
* @returns A promise that resolves to true if the port is in use, and false otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export function isPortInUse(port) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const server = createServer();
|
|
10
|
+
server.once('error', (err) => {
|
|
11
|
+
if (err.code === 'EADDRINUSE') {
|
|
12
|
+
resolve(true);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
// Handle other errors if necessary, but for this check, we assume other errors mean the port is available.
|
|
16
|
+
resolve(false);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
server.once('listening', () => {
|
|
20
|
+
server.close();
|
|
21
|
+
resolve(false);
|
|
22
|
+
});
|
|
23
|
+
server.listen(port);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Finds an available port, starting from a given port and incrementing by a specified amount.
|
|
28
|
+
* @param startPort The port to start checking from.
|
|
29
|
+
* @param incrementBy The amount to increment the port by if it's in use. Defaults to 2.
|
|
30
|
+
* @returns A promise that resolves to an available port number.
|
|
31
|
+
*/
|
|
32
|
+
export async function findAvailablePort(startPort, incrementBy = 2) {
|
|
33
|
+
let port = startPort;
|
|
34
|
+
while (await isPortInUse(port)) {
|
|
35
|
+
port += incrementBy;
|
|
36
|
+
}
|
|
37
|
+
return port;
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roam-research-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.2",
|
|
4
4
|
"description": "A Model Context Protocol (MCP) server for Roam Research API integration",
|
|
5
5
|
"private": false,
|
|
6
6
|
"repository": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"start": "node build/index.js"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.13.2",
|
|
38
38
|
"@roam-research/roam-api-sdk": "^0.10.0",
|
|
39
39
|
"dotenv": "^16.4.7"
|
|
40
40
|
},
|