roam-research-mcp 0.25.7 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -615
- package/build/config/environment.js +3 -1
- package/build/markdown-utils.js +2 -1
- package/build/server/roam-server.js +122 -70
- 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 +82 -127
- package/build/tools/tool-handlers.js +6 -9
- package/build/utils/net.js +38 -0
- package/package.json +2 -3
|
@@ -41,4 +41,6 @@ if (!API_TOKEN || !GRAPH_NAME) {
|
|
|
41
41
|
' ROAM_API_TOKEN=your-api-token\n' +
|
|
42
42
|
' ROAM_GRAPH_NAME=your-graph-name');
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
const HTTP_STREAM_PORT = process.env.HTTP_STREAM_PORT || '8088'; // Default to 8080
|
|
45
|
+
const SSE_PORT = process.env.SSE_PORT || '8087'; // Default to 8087
|
|
46
|
+
export { API_TOKEN, GRAPH_NAME, HTTP_STREAM_PORT, SSE_PORT };
|
package/build/markdown-utils.js
CHANGED
|
@@ -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);
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
3
5
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
6
|
import { initializeGraph } from '@roam-research/roam-api-sdk';
|
|
5
|
-
import { API_TOKEN, GRAPH_NAME } from '../config/environment.js';
|
|
7
|
+
import { API_TOKEN, GRAPH_NAME, HTTP_STREAM_PORT, SSE_PORT } from '../config/environment.js';
|
|
6
8
|
import { toolSchemas } from '../tools/schemas.js';
|
|
7
9
|
import { ToolHandlers } from '../tools/tool-handlers.js';
|
|
8
10
|
import { readFileSync } from 'node:fs';
|
|
9
11
|
import { join, dirname } from 'node:path';
|
|
12
|
+
import { createServer } from 'node:http';
|
|
10
13
|
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { findAvailablePort } from '../utils/net.js';
|
|
11
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
16
|
const __dirname = dirname(__filename);
|
|
13
17
|
// Read package.json to get the version
|
|
@@ -37,38 +41,15 @@ export class RoamServer {
|
|
|
37
41
|
if (Object.keys(toolSchemas).length === 0) {
|
|
38
42
|
throw new McpError(ErrorCode.InternalError, 'No tool schemas defined in src/tools/schemas.ts');
|
|
39
43
|
}
|
|
40
|
-
this.server = new Server({
|
|
41
|
-
name: 'roam-research',
|
|
42
|
-
version: serverVersion, // Use the version from package.json
|
|
43
|
-
}, {
|
|
44
|
-
capabilities: {
|
|
45
|
-
tools: {
|
|
46
|
-
...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName, {}])),
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
this.setupRequestHandlers();
|
|
51
|
-
// Error handling
|
|
52
|
-
this.server.onerror = (error) => {
|
|
53
|
-
// Re-throw as McpError to be caught by the MCP client
|
|
54
|
-
if (error instanceof McpError) {
|
|
55
|
-
throw error;
|
|
56
|
-
}
|
|
57
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
58
|
-
throw new McpError(ErrorCode.InternalError, `MCP server internal error: ${errorMessage}`);
|
|
59
|
-
};
|
|
60
|
-
process.on('SIGINT', async () => {
|
|
61
|
-
await this.server.close();
|
|
62
|
-
process.exit(0);
|
|
63
|
-
});
|
|
64
44
|
}
|
|
65
|
-
|
|
45
|
+
// Refactored to accept a Server instance
|
|
46
|
+
setupRequestHandlers(mcpServer) {
|
|
66
47
|
// List available tools
|
|
67
|
-
|
|
48
|
+
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
68
49
|
tools: Object.values(toolSchemas),
|
|
69
50
|
}));
|
|
70
51
|
// Handle tool calls
|
|
71
|
-
|
|
52
|
+
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
72
53
|
try {
|
|
73
54
|
switch (request.params.name) {
|
|
74
55
|
case 'roam_remember': {
|
|
@@ -92,13 +73,6 @@ export class RoamServer {
|
|
|
92
73
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
93
74
|
};
|
|
94
75
|
}
|
|
95
|
-
case 'roam_create_block': {
|
|
96
|
-
const { content, page_uid, title, heading } = request.params.arguments;
|
|
97
|
-
const result = await this.toolHandlers.createBlock(content, page_uid, title, heading);
|
|
98
|
-
return {
|
|
99
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
76
|
case 'roam_import_markdown': {
|
|
103
77
|
const { content, page_uid, page_title, parent_uid, parent_string, order = 'first' } = request.params.arguments;
|
|
104
78
|
const result = await this.toolHandlers.importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order);
|
|
@@ -173,27 +147,6 @@ export class RoamServer {
|
|
|
173
147
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
174
148
|
};
|
|
175
149
|
}
|
|
176
|
-
case 'roam_update_block': {
|
|
177
|
-
const { block_uid, content, transform_pattern } = request.params.arguments;
|
|
178
|
-
// Validate that either content or transform_pattern is provided, but not both or neither
|
|
179
|
-
if ((!content && !transform_pattern) || (content && transform_pattern)) {
|
|
180
|
-
throw new McpError(ErrorCode.InvalidRequest, 'Either content or transform_pattern must be provided, but not both or neither');
|
|
181
|
-
}
|
|
182
|
-
let result;
|
|
183
|
-
if (content) {
|
|
184
|
-
result = await this.toolHandlers.updateBlock(block_uid, content);
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
// We know transform_pattern exists due to validation above
|
|
188
|
-
result = await this.toolHandlers.updateBlock(block_uid, undefined, (currentContent) => {
|
|
189
|
-
const regex = new RegExp(transform_pattern.find, transform_pattern.global !== false ? 'g' : '');
|
|
190
|
-
return currentContent.replace(regex, transform_pattern.replace);
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
150
|
case 'roam_recall': {
|
|
198
151
|
const { sort_by = 'newest', filter_tag } = request.params.arguments;
|
|
199
152
|
const result = await this.toolHandlers.recall(sort_by, filter_tag);
|
|
@@ -201,22 +154,16 @@ export class RoamServer {
|
|
|
201
154
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
202
155
|
};
|
|
203
156
|
}
|
|
204
|
-
case '
|
|
205
|
-
const {
|
|
206
|
-
|
|
207
|
-
for (const update of updates) {
|
|
208
|
-
if ((!update.content && !update.transform) || (update.content && update.transform)) {
|
|
209
|
-
throw new McpError(ErrorCode.InvalidRequest, 'For each update, either content or transform must be provided, but not both or neither');
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const result = await this.toolHandlers.updateBlocks(updates);
|
|
157
|
+
case 'roam_datomic_query': {
|
|
158
|
+
const { query, inputs } = request.params.arguments;
|
|
159
|
+
const result = await this.toolHandlers.executeDatomicQuery({ query, inputs });
|
|
213
160
|
return {
|
|
214
161
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
215
162
|
};
|
|
216
163
|
}
|
|
217
|
-
case '
|
|
218
|
-
const {
|
|
219
|
-
const result = await this.toolHandlers.
|
|
164
|
+
case 'roam_process_batch_actions': {
|
|
165
|
+
const { actions } = request.params.arguments;
|
|
166
|
+
const result = await this.toolHandlers.processBatch(actions);
|
|
220
167
|
return {
|
|
221
168
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
222
169
|
};
|
|
@@ -236,8 +183,113 @@ export class RoamServer {
|
|
|
236
183
|
}
|
|
237
184
|
async run() {
|
|
238
185
|
try {
|
|
239
|
-
const
|
|
240
|
-
|
|
186
|
+
const stdioMcpServer = new Server({
|
|
187
|
+
name: 'roam-research',
|
|
188
|
+
version: serverVersion,
|
|
189
|
+
}, {
|
|
190
|
+
capabilities: {
|
|
191
|
+
tools: {
|
|
192
|
+
...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName, {}])),
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
this.setupRequestHandlers(stdioMcpServer);
|
|
197
|
+
const stdioTransport = new StdioServerTransport();
|
|
198
|
+
await stdioMcpServer.connect(stdioTransport);
|
|
199
|
+
const httpMcpServer = new Server({
|
|
200
|
+
name: 'roam-research-http', // A distinct name for the HTTP server
|
|
201
|
+
version: serverVersion,
|
|
202
|
+
}, {
|
|
203
|
+
capabilities: {
|
|
204
|
+
tools: {
|
|
205
|
+
...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName, {}])),
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
this.setupRequestHandlers(httpMcpServer);
|
|
210
|
+
const httpStreamTransport = new StreamableHTTPServerTransport({
|
|
211
|
+
sessionIdGenerator: () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15),
|
|
212
|
+
});
|
|
213
|
+
await httpMcpServer.connect(httpStreamTransport);
|
|
214
|
+
const httpServer = createServer(async (req, res) => {
|
|
215
|
+
try {
|
|
216
|
+
await httpStreamTransport.handleRequest(req, res);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// console.error('HTTP Stream Server error:', error);
|
|
220
|
+
if (!res.headersSent) {
|
|
221
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
222
|
+
res.end(JSON.stringify({ error: 'Internal Server Error' }));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
const availableHttpPort = await findAvailablePort(parseInt(HTTP_STREAM_PORT));
|
|
227
|
+
httpServer.listen(availableHttpPort, () => {
|
|
228
|
+
// console.log(`MCP Roam Research server running HTTP Stream on port ${availableHttpPort}`);
|
|
229
|
+
});
|
|
230
|
+
// SSE Server setup
|
|
231
|
+
const sseMcpServer = new Server({
|
|
232
|
+
name: 'roam-research-sse', // Distinct name for SSE server
|
|
233
|
+
version: serverVersion,
|
|
234
|
+
}, {
|
|
235
|
+
capabilities: {
|
|
236
|
+
tools: {
|
|
237
|
+
...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName, {}])),
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
this.setupRequestHandlers(sseMcpServer);
|
|
242
|
+
const sseHttpServer = createServer(async (req, res) => {
|
|
243
|
+
const parseBody = (request) => {
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
let body = '';
|
|
246
|
+
request.on('data', (chunk) => {
|
|
247
|
+
body += chunk.toString();
|
|
248
|
+
});
|
|
249
|
+
request.on('end', () => {
|
|
250
|
+
try {
|
|
251
|
+
resolve(body ? JSON.parse(body) : {});
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
reject(error);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
request.on('error', reject);
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
try {
|
|
261
|
+
if (req.url === '/sse') {
|
|
262
|
+
const sseTransport = new SSEServerTransport('/sse', res);
|
|
263
|
+
await sseMcpServer.connect(sseTransport);
|
|
264
|
+
if (req.method === 'GET') {
|
|
265
|
+
await sseTransport.start();
|
|
266
|
+
}
|
|
267
|
+
else if (req.method === 'POST') {
|
|
268
|
+
const parsedBody = await parseBody(req);
|
|
269
|
+
await sseTransport.handlePostMessage(req, res, parsedBody);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
res.writeHead(405, { 'Content-Type': 'text/plain' });
|
|
273
|
+
res.end('Method Not Allowed');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
278
|
+
res.end('Not Found');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
// console.error('SSE HTTP Server error:', error);
|
|
283
|
+
if (!res.headersSent) {
|
|
284
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
285
|
+
res.end(JSON.stringify({ error: 'Internal Server Error' }));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
const availableSsePort = await findAvailablePort(parseInt(SSE_PORT));
|
|
290
|
+
sseHttpServer.listen(availableSsePort, () => {
|
|
291
|
+
// console.log(`MCP Roam Research server running SSE on port ${availableSsePort}`);
|
|
292
|
+
});
|
|
241
293
|
}
|
|
242
294
|
catch (error) {
|
|
243
295
|
const errorMessage = error instanceof Error ? error.message : String(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
|
}
|