roam-research-mcp 0.2.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/LICENSE +21 -0
- package/README.md +329 -0
- package/build/config/environment.js +44 -0
- package/build/index.js +4 -0
- package/build/markdown-utils.js +205 -0
- package/build/server/roam-server.js +108 -0
- package/build/test-addMarkdownText.js +87 -0
- package/build/test-queries.js +116 -0
- package/build/tools/handlers.js +706 -0
- package/build/tools/schemas.js +147 -0
- package/build/types/roam.js +1 -0
- package/build/utils/helpers.js +19 -0
- package/package.json +46 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { initializeGraph } from '@roam-research/roam-api-sdk';
|
|
5
|
+
import { API_TOKEN, GRAPH_NAME } from '../config/environment.js';
|
|
6
|
+
import { toolSchemas } from '../tools/schemas.js';
|
|
7
|
+
import { ToolHandlers } from '../tools/handlers.js';
|
|
8
|
+
export class RoamServer {
|
|
9
|
+
server;
|
|
10
|
+
toolHandlers;
|
|
11
|
+
constructor() {
|
|
12
|
+
const graph = initializeGraph({
|
|
13
|
+
token: API_TOKEN,
|
|
14
|
+
graph: GRAPH_NAME,
|
|
15
|
+
});
|
|
16
|
+
this.toolHandlers = new ToolHandlers(graph);
|
|
17
|
+
this.server = new Server({
|
|
18
|
+
name: 'roam-research',
|
|
19
|
+
version: '0.12.1',
|
|
20
|
+
}, {
|
|
21
|
+
capabilities: {
|
|
22
|
+
tools: {
|
|
23
|
+
roam_add_todo: {},
|
|
24
|
+
roam_fetch_page_by_title: {},
|
|
25
|
+
roam_create_page: {},
|
|
26
|
+
roam_create_block: {},
|
|
27
|
+
roam_import_markdown: {},
|
|
28
|
+
roam_create_outline: {}
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
this.setupRequestHandlers();
|
|
33
|
+
// Error handling
|
|
34
|
+
this.server.onerror = (error) => { };
|
|
35
|
+
process.on('SIGINT', async () => {
|
|
36
|
+
await this.server.close();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
setupRequestHandlers() {
|
|
41
|
+
// List available tools
|
|
42
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
43
|
+
tools: Object.values(toolSchemas),
|
|
44
|
+
}));
|
|
45
|
+
// Handle tool calls
|
|
46
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
47
|
+
try {
|
|
48
|
+
switch (request.params.name) {
|
|
49
|
+
case 'roam_fetch_page_by_title': {
|
|
50
|
+
const { title } = request.params.arguments;
|
|
51
|
+
const content = await this.toolHandlers.fetchPageByTitle(title);
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: 'text', text: content }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
case 'roam_create_page': {
|
|
57
|
+
const { title, content } = request.params.arguments;
|
|
58
|
+
const result = await this.toolHandlers.createPage(title, content);
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
case 'roam_create_block': {
|
|
64
|
+
const { content, page_uid, title } = request.params.arguments;
|
|
65
|
+
const result = await this.toolHandlers.createBlock(content, page_uid, title);
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
case 'roam_import_markdown': {
|
|
71
|
+
const { content, page_uid, page_title, parent_uid, parent_string, order = 'first' } = request.params.arguments;
|
|
72
|
+
const result = await this.toolHandlers.importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order);
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
case 'roam_add_todo': {
|
|
78
|
+
const { todos } = request.params.arguments;
|
|
79
|
+
const result = await this.toolHandlers.addTodos(todos);
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
case 'roam_create_outline': {
|
|
85
|
+
const { outline, page_title_uid, block_text_uid } = request.params.arguments;
|
|
86
|
+
const result = await this.toolHandlers.createOutline(outline, page_title_uid, block_text_uid);
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof McpError) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
100
|
+
throw new McpError(ErrorCode.InternalError, `Roam API error: ${errorMessage}`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async run() {
|
|
105
|
+
const transport = new StdioServerTransport();
|
|
106
|
+
await this.server.connect(transport);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { initializeGraph, createPage, batchActions, q } from '@roam-research/roam-api-sdk';
|
|
2
|
+
import { parseMarkdown, convertToRoamActions } from '../src/markdown-utils.js';
|
|
3
|
+
import * as dotenv from 'dotenv';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
// Load environment variables
|
|
7
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
8
|
+
const projectRoot = dirname(dirname(scriptPath));
|
|
9
|
+
const envPath = join(projectRoot, '.env');
|
|
10
|
+
dotenv.config({ path: envPath });
|
|
11
|
+
const API_TOKEN = process.env.ROAM_API_TOKEN;
|
|
12
|
+
const GRAPH_NAME = process.env.ROAM_GRAPH_NAME;
|
|
13
|
+
if (!API_TOKEN || !GRAPH_NAME) {
|
|
14
|
+
throw new Error('Missing required environment variables: ROAM_API_TOKEN and/or ROAM_GRAPH_NAME');
|
|
15
|
+
}
|
|
16
|
+
async function testAddMarkdownText() {
|
|
17
|
+
try {
|
|
18
|
+
// Initialize graph
|
|
19
|
+
console.log('Initializing graph...');
|
|
20
|
+
const graph = initializeGraph({
|
|
21
|
+
token: API_TOKEN,
|
|
22
|
+
graph: GRAPH_NAME,
|
|
23
|
+
});
|
|
24
|
+
// Test markdown content
|
|
25
|
+
const testPageTitle = `Test Markdown Import ${new Date().toISOString()}`;
|
|
26
|
+
console.log(`Using test page title: ${testPageTitle}`);
|
|
27
|
+
const markdownContent = `
|
|
28
|
+
| Month | Savings |
|
|
29
|
+
| -------- | ------- |
|
|
30
|
+
| January | $250 |
|
|
31
|
+
| February | $80 |
|
|
32
|
+
| March | $420 |
|
|
33
|
+
|
|
34
|
+
# Main Topic
|
|
35
|
+
- First point
|
|
36
|
+
- Nested point A
|
|
37
|
+
- Deep nested point
|
|
38
|
+
- Nested point B
|
|
39
|
+
- Second point
|
|
40
|
+
1. Numbered subpoint
|
|
41
|
+
2. Another numbered point
|
|
42
|
+
- Mixed list type
|
|
43
|
+
- Third point
|
|
44
|
+
- With some **bold** text
|
|
45
|
+
- And *italic* text
|
|
46
|
+
- And a [[Page Reference]]
|
|
47
|
+
- And a #[[Page Tag]]
|
|
48
|
+
`;
|
|
49
|
+
// First create the page
|
|
50
|
+
console.log('\nCreating page...');
|
|
51
|
+
const success = await createPage(graph, {
|
|
52
|
+
action: 'create-page',
|
|
53
|
+
page: {
|
|
54
|
+
title: testPageTitle
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (!success) {
|
|
58
|
+
throw new Error('Failed to create test page');
|
|
59
|
+
}
|
|
60
|
+
// Get the page UID
|
|
61
|
+
const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
|
|
62
|
+
const findResults = await q(graph, findQuery, [testPageTitle]);
|
|
63
|
+
if (!findResults || findResults.length === 0) {
|
|
64
|
+
throw new Error('Could not find created page');
|
|
65
|
+
}
|
|
66
|
+
const pageUid = findResults[0][0];
|
|
67
|
+
console.log('Page UID:', pageUid);
|
|
68
|
+
// Import markdown
|
|
69
|
+
console.log('\nImporting markdown...');
|
|
70
|
+
const nodes = parseMarkdown(markdownContent);
|
|
71
|
+
console.log('Parsed nodes:', JSON.stringify(nodes, null, 2));
|
|
72
|
+
// Convert and add markdown content
|
|
73
|
+
const actions = convertToRoamActions(nodes, pageUid, 'last');
|
|
74
|
+
const result = await batchActions(graph, {
|
|
75
|
+
action: 'batch-actions',
|
|
76
|
+
actions
|
|
77
|
+
});
|
|
78
|
+
console.log('\nImport result:', JSON.stringify(result, null, 2));
|
|
79
|
+
console.log('\nTest completed successfully!');
|
|
80
|
+
console.log(`Check your Roam graph for the page titled: ${testPageTitle}`);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error('Error:', error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Run the test
|
|
87
|
+
testAddMarkdownText();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { initializeGraph, q } from '@roam-research/roam-api-sdk';
|
|
3
|
+
import * as dotenv from 'dotenv';
|
|
4
|
+
// Load environment variables
|
|
5
|
+
dotenv.config();
|
|
6
|
+
const API_TOKEN = process.env.ROAM_API_TOKEN;
|
|
7
|
+
const GRAPH_NAME = process.env.ROAM_GRAPH_NAME;
|
|
8
|
+
if (!API_TOKEN || !GRAPH_NAME) {
|
|
9
|
+
throw new Error('Missing required environment variables: ROAM_API_TOKEN and ROAM_GRAPH_NAME');
|
|
10
|
+
}
|
|
11
|
+
async function main() {
|
|
12
|
+
const graph = initializeGraph({
|
|
13
|
+
token: API_TOKEN,
|
|
14
|
+
graph: GRAPH_NAME,
|
|
15
|
+
});
|
|
16
|
+
try {
|
|
17
|
+
// First verify we can find the page
|
|
18
|
+
console.log('Finding page...');
|
|
19
|
+
const searchQuery = `[:find ?uid .
|
|
20
|
+
:where [?e :node/title "December 18th, 2024"]
|
|
21
|
+
[?e :block/uid ?uid]]`;
|
|
22
|
+
const uid = await q(graph, searchQuery, []);
|
|
23
|
+
console.log('Page UID:', uid);
|
|
24
|
+
if (!uid) {
|
|
25
|
+
throw new Error('Page not found');
|
|
26
|
+
}
|
|
27
|
+
// Get all blocks under this page with their order
|
|
28
|
+
console.log('\nGetting blocks...');
|
|
29
|
+
const blocksQuery = `[:find ?block-uid ?block-str ?order ?parent-uid
|
|
30
|
+
:where [?p :block/uid "${uid}"]
|
|
31
|
+
[?b :block/page ?p]
|
|
32
|
+
[?b :block/uid ?block-uid]
|
|
33
|
+
[?b :block/string ?block-str]
|
|
34
|
+
[?b :block/order ?order]
|
|
35
|
+
[?b :block/parents ?parent]
|
|
36
|
+
[?parent :block/uid ?parent-uid]]`;
|
|
37
|
+
const blocks = await q(graph, blocksQuery, []);
|
|
38
|
+
console.log('Found', blocks.length, 'blocks');
|
|
39
|
+
// Create a map of all blocks
|
|
40
|
+
const blockMap = new Map();
|
|
41
|
+
blocks.forEach(([uid, string, order]) => {
|
|
42
|
+
if (!blockMap.has(uid)) {
|
|
43
|
+
blockMap.set(uid, {
|
|
44
|
+
uid,
|
|
45
|
+
string,
|
|
46
|
+
order: order,
|
|
47
|
+
children: []
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
console.log('Created block map with', blockMap.size, 'entries');
|
|
52
|
+
// Build parent-child relationships
|
|
53
|
+
let relationshipsBuilt = 0;
|
|
54
|
+
blocks.forEach(([childUid, _, __, parentUid]) => {
|
|
55
|
+
const child = blockMap.get(childUid);
|
|
56
|
+
const parent = blockMap.get(parentUid);
|
|
57
|
+
if (child && parent && !parent.children.includes(child)) {
|
|
58
|
+
parent.children.push(child);
|
|
59
|
+
relationshipsBuilt++;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
console.log('Built', relationshipsBuilt, 'parent-child relationships');
|
|
63
|
+
// Get top-level blocks (those directly under the page)
|
|
64
|
+
console.log('\nGetting top-level blocks...');
|
|
65
|
+
const topQuery = `[:find ?block-uid ?block-str ?order
|
|
66
|
+
:where [?p :block/uid "${uid}"]
|
|
67
|
+
[?b :block/page ?p]
|
|
68
|
+
[?b :block/uid ?block-uid]
|
|
69
|
+
[?b :block/string ?block-str]
|
|
70
|
+
[?b :block/order ?order]
|
|
71
|
+
(not-join [?b]
|
|
72
|
+
[?b :block/parents ?parent]
|
|
73
|
+
[?parent :block/page ?p])]`;
|
|
74
|
+
const topBlocks = await q(graph, topQuery, []);
|
|
75
|
+
console.log('Found', topBlocks.length, 'top-level blocks');
|
|
76
|
+
// Create root blocks
|
|
77
|
+
const rootBlocks = topBlocks
|
|
78
|
+
.map(([uid, string, order]) => ({
|
|
79
|
+
uid,
|
|
80
|
+
string,
|
|
81
|
+
order: order,
|
|
82
|
+
children: blockMap.get(uid)?.children || []
|
|
83
|
+
}))
|
|
84
|
+
.sort((a, b) => a.order - b.order);
|
|
85
|
+
// Log block hierarchy
|
|
86
|
+
console.log('\nBlock hierarchy:');
|
|
87
|
+
const logHierarchy = (blocks, level = 0) => {
|
|
88
|
+
blocks.forEach(block => {
|
|
89
|
+
console.log(' '.repeat(level) + '- ' + block.string.substring(0, 50) + '...');
|
|
90
|
+
if (block.children.length > 0) {
|
|
91
|
+
logHierarchy(block.children.sort((a, b) => a.order - b.order), level + 1);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
logHierarchy(rootBlocks);
|
|
96
|
+
// Convert to markdown
|
|
97
|
+
console.log('\nConverting to markdown...');
|
|
98
|
+
const toMarkdown = (blocks, level = 0) => {
|
|
99
|
+
return blocks.map(block => {
|
|
100
|
+
const indent = ' '.repeat(level);
|
|
101
|
+
let md = `${indent}- ${block.string}\n`;
|
|
102
|
+
if (block.children.length > 0) {
|
|
103
|
+
md += toMarkdown(block.children.sort((a, b) => a.order - b.order), level + 1);
|
|
104
|
+
}
|
|
105
|
+
return md;
|
|
106
|
+
}).join('');
|
|
107
|
+
};
|
|
108
|
+
const markdown = toMarkdown(rootBlocks);
|
|
109
|
+
console.log('\nMarkdown output:');
|
|
110
|
+
console.log(markdown);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
console.error('Error:', error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
main().catch(console.error);
|