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.
@@ -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);