roam-research-mcp 0.12.3 → 0.14.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.
@@ -69,18 +69,19 @@ function parseMarkdown(markdown) {
69
69
  let level = Math.floor(indentation / 2);
70
70
  // Extract content after bullet point or heading
71
71
  let content = trimmedLine;
72
- if (trimmedLine.startsWith('#') || trimmedLine.includes('{{table}}')) {
72
+ content = trimmedLine.replace(/^\s*[-*+]\s+/, '');
73
+ if (trimmedLine.startsWith('#') || trimmedLine.includes('{{table}}') || (trimmedLine.startsWith('**') && trimmedLine.endsWith('**'))) {
73
74
  // Remove bullet point if it precedes a table marker
74
- content = trimmedLine.replace(/^\s*[-*+]\s+/, '');
75
+ // content = trimmedLine.replace(/^\s*[-*+]\s+/, '');
75
76
  level = 0;
76
77
  // Reset stack but keep heading/table as parent
77
78
  stack.length = 1; // Keep only the heading/table
78
79
  }
79
- else if (stack[0]?.content.startsWith('#') || stack[0]?.content.includes('{{table}}')) {
80
- // If previous node was a heading or table marker, increase level by 1
80
+ else if (stack[0]?.content.startsWith('#') || stack[0]?.content.includes('{{table}}') || (stack[0]?.content.startsWith('**') && stack[0]?.content.endsWith('**'))) {
81
+ // If previous node was a heading or table marker or wrapped in double-asterisks, increase level by 1
81
82
  level = Math.max(level, 1);
82
83
  // Remove bullet point
83
- content = trimmedLine.replace(/^\s*[-*+]\s+/, '');
84
+ // content = trimmedLine.replace(/^\s*[-*+]\s+/, '');
84
85
  }
85
86
  else {
86
87
  // Remove bullet point
@@ -0,0 +1,72 @@
1
+ import { q } from '@roam-research/roam-api-sdk';
2
+ import { BaseSearchHandler } from './types.js';
3
+ import { SearchUtils } from './utils.js';
4
+ export class BlockRefSearchHandler extends BaseSearchHandler {
5
+ params;
6
+ constructor(graph, params) {
7
+ super(graph);
8
+ this.params = params;
9
+ }
10
+ async execute() {
11
+ const { block_uid, page_title_uid } = this.params;
12
+ // Get target page UID if provided
13
+ let targetPageUid;
14
+ if (page_title_uid) {
15
+ targetPageUid = await SearchUtils.findPageByTitleOrUid(this.graph, page_title_uid);
16
+ }
17
+ // Build query based on whether we're searching for references to a specific block
18
+ // or all block references within a page/graph
19
+ let queryStr;
20
+ let queryParams;
21
+ if (block_uid) {
22
+ // Search for references to a specific block
23
+ if (targetPageUid) {
24
+ queryStr = `[:find ?block-uid ?block-str
25
+ :in $ ?ref-uid ?page-uid
26
+ :where [?p :block/uid ?page-uid]
27
+ [?b :block/page ?p]
28
+ [?b :block/string ?block-str]
29
+ [?b :block/uid ?block-uid]
30
+ [(clojure.string/includes? ?block-str ?ref-uid)]]`;
31
+ queryParams = [`((${block_uid}))`, targetPageUid];
32
+ }
33
+ else {
34
+ queryStr = `[:find ?block-uid ?block-str ?page-title
35
+ :in $ ?ref-uid
36
+ :where [?b :block/string ?block-str]
37
+ [?b :block/uid ?block-uid]
38
+ [?b :block/page ?p]
39
+ [?p :node/title ?page-title]
40
+ [(clojure.string/includes? ?block-str ?ref-uid)]]`;
41
+ queryParams = [`((${block_uid}))`];
42
+ }
43
+ }
44
+ else {
45
+ // Search for any block references
46
+ if (targetPageUid) {
47
+ queryStr = `[:find ?block-uid ?block-str
48
+ :in $ ?page-uid
49
+ :where [?p :block/uid ?page-uid]
50
+ [?b :block/page ?p]
51
+ [?b :block/string ?block-str]
52
+ [?b :block/uid ?block-uid]
53
+ [(re-find #"\\(\\([^)]+\\)\\)" ?block-str)]]`;
54
+ queryParams = [targetPageUid];
55
+ }
56
+ else {
57
+ queryStr = `[:find ?block-uid ?block-str ?page-title
58
+ :where [?b :block/string ?block-str]
59
+ [?b :block/uid ?block-uid]
60
+ [?b :block/page ?p]
61
+ [?p :node/title ?page-title]
62
+ [(re-find #"\\(\\([^)]+\\)\\)" ?block-str)]]`;
63
+ queryParams = [];
64
+ }
65
+ }
66
+ const results = await q(this.graph, queryStr, queryParams);
67
+ const searchDescription = block_uid
68
+ ? `referencing block ((${block_uid}))`
69
+ : 'containing block references';
70
+ return SearchUtils.formatSearchResults(results, searchDescription, !targetPageUid);
71
+ }
72
+ }
@@ -0,0 +1,38 @@
1
+ import { q } from '@roam-research/roam-api-sdk';
2
+ import { BaseSearchHandler } from './types.js';
3
+ import { SearchUtils } from './utils.js';
4
+ export class DateSearchHandler extends BaseSearchHandler {
5
+ params;
6
+ constructor(graph, params) {
7
+ super(graph);
8
+ this.params = params;
9
+ }
10
+ async execute() {
11
+ const { start_date, end_date, filter_tag } = this.params;
12
+ const [startDateFormatted, endDateFormatted] = SearchUtils.parseDateRange(start_date, end_date);
13
+ const filterTagFormatted = filter_tag ? `[[${filter_tag}]]` : undefined;
14
+ // Build Roam query string
15
+ const dateQuery = `{between: [[${startDateFormatted}]] [[${endDateFormatted}]]}`;
16
+ const query = filterTagFormatted
17
+ ? `{{query: {and: ${filterTagFormatted} ${dateQuery}}}}`
18
+ : `{{query: ${dateQuery}}}`;
19
+ // Log the query for debugging
20
+ console.log('Roam query:', query);
21
+ // Find blocks matching the query
22
+ const queryStr = `[:find ?block-uid ?block-str ?page-title
23
+ :in $ ?query-str
24
+ :where [?b :block/string ?block-str]
25
+ [?b :block/uid ?block-uid]
26
+ [?b :block/page ?p]
27
+ [?p :node/title ?page-title]
28
+ [(clojure.string/includes? ?block-str ?query-str)]]`;
29
+ const results = await q(this.graph, queryStr, [query]);
30
+ const dateRange = start_date === end_date
31
+ ? `on ${SearchUtils.parseDate(start_date)}`
32
+ : `between ${SearchUtils.parseDate(start_date)} and ${SearchUtils.parseDate(end_date)}`;
33
+ const searchDescription = filterTagFormatted
34
+ ? `${dateRange} containing ${filterTagFormatted}`
35
+ : dateRange;
36
+ return SearchUtils.formatSearchResults(results, searchDescription, true);
37
+ }
38
+ }
@@ -0,0 +1,101 @@
1
+ import { q } from '@roam-research/roam-api-sdk';
2
+ import { BaseSearchHandler } from './types.js';
3
+ import { SearchUtils } from './utils.js';
4
+ export class HierarchySearchHandler extends BaseSearchHandler {
5
+ params;
6
+ constructor(graph, params) {
7
+ super(graph);
8
+ this.params = params;
9
+ }
10
+ async execute() {
11
+ const { parent_uid, child_uid, page_title_uid, max_depth = 1 } = this.params;
12
+ if (!parent_uid && !child_uid) {
13
+ return {
14
+ success: false,
15
+ matches: [],
16
+ message: 'Either parent_uid or child_uid must be provided'
17
+ };
18
+ }
19
+ // Get target page UID if provided
20
+ let targetPageUid;
21
+ if (page_title_uid) {
22
+ targetPageUid = await SearchUtils.findPageByTitleOrUid(this.graph, page_title_uid);
23
+ }
24
+ let queryStr;
25
+ let queryParams;
26
+ if (parent_uid) {
27
+ // Search for children of a specific block
28
+ if (targetPageUid) {
29
+ queryStr = `[:find ?block-uid ?block-str ?depth
30
+ :in $ ?parent-uid ?page-uid ?max-depth
31
+ :where [?p :block/uid ?page-uid]
32
+ [?parent :block/uid ?parent-uid]
33
+ [?b :block/parents ?parent]
34
+ [?b :block/string ?block-str]
35
+ [?b :block/uid ?block-uid]
36
+ [?b :block/page ?p]
37
+ [(get-else $ ?b :block/path-length 1) ?depth]
38
+ [(<= ?depth ?max-depth)]]`;
39
+ queryParams = [parent_uid, targetPageUid, max_depth];
40
+ }
41
+ else {
42
+ queryStr = `[:find ?block-uid ?block-str ?page-title ?depth
43
+ :in $ ?parent-uid ?max-depth
44
+ :where [?parent :block/uid ?parent-uid]
45
+ [?b :block/parents ?parent]
46
+ [?b :block/string ?block-str]
47
+ [?b :block/uid ?block-uid]
48
+ [?b :block/page ?p]
49
+ [?p :node/title ?page-title]
50
+ [(get-else $ ?b :block/path-length 1) ?depth]
51
+ [(<= ?depth ?max-depth)]]`;
52
+ queryParams = [parent_uid, max_depth];
53
+ }
54
+ }
55
+ else {
56
+ // Search for parents of a specific block
57
+ if (targetPageUid) {
58
+ queryStr = `[:find ?block-uid ?block-str ?depth
59
+ :in $ ?child-uid ?page-uid ?max-depth
60
+ :where [?p :block/uid ?page-uid]
61
+ [?child :block/uid ?child-uid]
62
+ [?child :block/parents ?b]
63
+ [?b :block/string ?block-str]
64
+ [?b :block/uid ?block-uid]
65
+ [?b :block/page ?p]
66
+ [(get-else $ ?b :block/path-length 1) ?depth]
67
+ [(<= ?depth ?max-depth)]]`;
68
+ queryParams = [child_uid, targetPageUid, max_depth];
69
+ }
70
+ else {
71
+ queryStr = `[:find ?block-uid ?block-str ?page-title ?depth
72
+ :in $ ?child-uid ?max-depth
73
+ :where [?child :block/uid ?child-uid]
74
+ [?child :block/parents ?b]
75
+ [?b :block/string ?block-str]
76
+ [?b :block/uid ?block-uid]
77
+ [?b :block/page ?p]
78
+ [?p :node/title ?page-title]
79
+ [(get-else $ ?b :block/path-length 1) ?depth]
80
+ [(<= ?depth ?max-depth)]]`;
81
+ queryParams = [child_uid, max_depth];
82
+ }
83
+ }
84
+ const results = await q(this.graph, queryStr, queryParams);
85
+ // Format results to include depth information
86
+ const matches = results.map(([uid, content, pageTitle, depth]) => ({
87
+ block_uid: uid,
88
+ content,
89
+ depth: depth || 1,
90
+ ...(pageTitle && { page_title: pageTitle })
91
+ }));
92
+ const searchDescription = parent_uid
93
+ ? `children of block ${parent_uid} (max depth: ${max_depth})`
94
+ : `parents of block ${child_uid} (max depth: ${max_depth})`;
95
+ return {
96
+ success: true,
97
+ matches,
98
+ message: `Found ${matches.length} block(s) as ${searchDescription}`
99
+ };
100
+ }
101
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './utils.js';
3
+ export * from './tag-search.js';
4
+ export * from './status-search.js';
5
+ export * from './block-ref-search.js';
6
+ export * from './hierarchy-search.js';
@@ -0,0 +1,43 @@
1
+ import { q } from '@roam-research/roam-api-sdk';
2
+ import { BaseSearchHandler } from './types.js';
3
+ import { SearchUtils } from './utils.js';
4
+ export class StatusSearchHandler extends BaseSearchHandler {
5
+ params;
6
+ constructor(graph, params) {
7
+ super(graph);
8
+ this.params = params;
9
+ }
10
+ async execute() {
11
+ const { status, page_title_uid } = this.params;
12
+ // Get target page UID if provided
13
+ let targetPageUid;
14
+ if (page_title_uid) {
15
+ targetPageUid = await SearchUtils.findPageByTitleOrUid(this.graph, page_title_uid);
16
+ }
17
+ // Build query based on whether we're searching in a specific page
18
+ let queryStr;
19
+ let queryParams;
20
+ if (targetPageUid) {
21
+ queryStr = `[:find ?block-uid ?block-str
22
+ :in $ ?status ?page-uid
23
+ :where [?p :block/uid ?page-uid]
24
+ [?b :block/page ?p]
25
+ [?b :block/string ?block-str]
26
+ [?b :block/uid ?block-uid]
27
+ [(clojure.string/includes? ?block-str (str "{{[[" ?status "]]}}"))]]`;
28
+ queryParams = [status, targetPageUid];
29
+ }
30
+ else {
31
+ queryStr = `[:find ?block-uid ?block-str ?page-title
32
+ :in $ ?status
33
+ :where [?b :block/string ?block-str]
34
+ [?b :block/uid ?block-uid]
35
+ [?b :block/page ?p]
36
+ [?p :node/title ?page-title]
37
+ [(clojure.string/includes? ?block-str (str "{{[[" ?status "]]}}"))]]`;
38
+ queryParams = [status];
39
+ }
40
+ const results = await q(this.graph, queryStr, queryParams);
41
+ return SearchUtils.formatSearchResults(results, `with status ${status}`, !targetPageUid);
42
+ }
43
+ }
@@ -0,0 +1,89 @@
1
+ import { q } from '@roam-research/roam-api-sdk';
2
+ import { BaseSearchHandler } from './types.js';
3
+ import { SearchUtils } from './utils.js';
4
+ export class TagSearchHandler extends BaseSearchHandler {
5
+ params;
6
+ constructor(graph, params) {
7
+ super(graph);
8
+ this.params = params;
9
+ }
10
+ async execute() {
11
+ const { primary_tag, page_title_uid, near_tag, exclude_tag } = this.params;
12
+ // Format tags to handle both # and [[]] formats
13
+ const primaryTagFormats = SearchUtils.formatTag(primary_tag);
14
+ const nearTagFormats = near_tag ? SearchUtils.formatTag(near_tag) : undefined;
15
+ const excludeTagFormats = exclude_tag ? SearchUtils.formatTag(exclude_tag) : undefined;
16
+ // Get target page UID if provided
17
+ let targetPageUid;
18
+ if (page_title_uid) {
19
+ targetPageUid = await SearchUtils.findPageByTitleOrUid(this.graph, page_title_uid);
20
+ }
21
+ // Build query based on whether we're searching in a specific page and/or for a nearby tag
22
+ let queryStr;
23
+ let queryParams;
24
+ if (targetPageUid) {
25
+ if (nearTagFormats) {
26
+ queryStr = `[:find ?block-uid ?block-str
27
+ :in $ [?primary-tag1 ?primary-tag2] [?near-tag1 ?near-tag2] [?exclude-tag1 ?exclude-tag2] ?page-uid
28
+ :where [?p :block/uid ?page-uid]
29
+ [?b :block/page ?p]
30
+ [?b :block/string ?block-str]
31
+ [?b :block/uid ?block-uid]
32
+ (or [(clojure.string/includes? ?block-str ?primary-tag1)]
33
+ [(clojure.string/includes? ?block-str ?primary-tag2)])
34
+ (or [(clojure.string/includes? ?block-str ?near-tag1)]
35
+ [(clojure.string/includes? ?block-str ?near-tag2)])
36
+ (not (or [(clojure.string/includes? ?block-str ?exclude-tag1)]
37
+ [(clojure.string/includes? ?block-str ?exclude-tag2)]))]`;
38
+ queryParams = [primaryTagFormats, nearTagFormats, excludeTagFormats || ['', ''], targetPageUid];
39
+ }
40
+ else {
41
+ queryStr = `[:find ?block-uid ?block-str
42
+ :in $ [?primary-tag1 ?primary-tag2] [?exclude-tag1 ?exclude-tag2] ?page-uid
43
+ :where [?p :block/uid ?page-uid]
44
+ [?b :block/page ?p]
45
+ [?b :block/string ?block-str]
46
+ [?b :block/uid ?block-uid]
47
+ (or [(clojure.string/includes? ?block-str ?primary-tag1)]
48
+ [(clojure.string/includes? ?block-str ?primary-tag2)])
49
+ (not (or [(clojure.string/includes? ?block-str ?exclude-tag1)]
50
+ [(clojure.string/includes? ?block-str ?exclude-tag2)]))]`;
51
+ queryParams = [primaryTagFormats, excludeTagFormats || ['', ''], targetPageUid];
52
+ }
53
+ }
54
+ else {
55
+ // Search across all pages
56
+ if (nearTagFormats) {
57
+ queryStr = `[:find ?block-uid ?block-str ?page-title
58
+ :in $ [?primary-tag1 ?primary-tag2] [?near-tag1 ?near-tag2] [?exclude-tag1 ?exclude-tag2]
59
+ :where [?b :block/string ?block-str]
60
+ [?b :block/uid ?block-uid]
61
+ [?b :block/page ?p]
62
+ [?p :node/title ?page-title]
63
+ (or [(clojure.string/includes? ?block-str ?primary-tag1)]
64
+ [(clojure.string/includes? ?block-str ?primary-tag2)])
65
+ (or [(clojure.string/includes? ?block-str ?near-tag1)]
66
+ [(clojure.string/includes? ?block-str ?near-tag2)])
67
+ (not (or [(clojure.string/includes? ?block-str ?exclude-tag1)]
68
+ [(clojure.string/includes? ?block-str ?exclude-tag2)]))]`;
69
+ queryParams = [primaryTagFormats, nearTagFormats, excludeTagFormats || ['', '']];
70
+ }
71
+ else {
72
+ queryStr = `[:find ?block-uid ?block-str ?page-title
73
+ :in $ [?primary-tag1 ?primary-tag2] [?exclude-tag1 ?exclude-tag2]
74
+ :where [?b :block/string ?block-str]
75
+ [?b :block/uid ?block-uid]
76
+ [?b :block/page ?p]
77
+ [?p :node/title ?page-title]
78
+ (or [(clojure.string/includes? ?block-str ?primary-tag1)]
79
+ [(clojure.string/includes? ?block-str ?primary-tag2)])
80
+ (not (or [(clojure.string/includes? ?block-str ?exclude-tag1)]
81
+ [(clojure.string/includes? ?block-str ?exclude-tag2)]))]`;
82
+ queryParams = [primaryTagFormats, excludeTagFormats || ['', '']];
83
+ }
84
+ }
85
+ const results = await q(this.graph, queryStr, queryParams);
86
+ const searchDescription = `containing ${primaryTagFormats.join(' or ')}${nearTagFormats ? ` near ${nearTagFormats.join(' or ')}` : ''}${excludeTagFormats ? ` excluding ${excludeTagFormats.join(' or ')}` : ''}`;
87
+ return SearchUtils.formatSearchResults(results, searchDescription, !targetPageUid);
88
+ }
89
+ }
@@ -0,0 +1,7 @@
1
+ // Base class for all search handlers
2
+ export class BaseSearchHandler {
3
+ graph;
4
+ constructor(graph) {
5
+ this.graph = graph;
6
+ }
7
+ }
@@ -0,0 +1,98 @@
1
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
+ import { q } from '@roam-research/roam-api-sdk';
3
+ export class SearchUtils {
4
+ /**
5
+ * Find a page by title or UID
6
+ */
7
+ static async findPageByTitleOrUid(graph, titleOrUid) {
8
+ // Try to find page by title
9
+ const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
10
+ const findResults = await q(graph, findQuery, [titleOrUid]);
11
+ if (findResults && findResults.length > 0) {
12
+ return findResults[0][0];
13
+ }
14
+ // Try as UID
15
+ const uidQuery = `[:find ?uid :where [?e :block/uid "${titleOrUid}"] [?e :block/uid ?uid]]`;
16
+ const uidResults = await q(graph, uidQuery, []);
17
+ if (!uidResults || uidResults.length === 0) {
18
+ throw new McpError(ErrorCode.InvalidRequest, `Page with title/UID "${titleOrUid}" not found`);
19
+ }
20
+ return uidResults[0][0];
21
+ }
22
+ /**
23
+ * Format search results into a standard structure
24
+ */
25
+ static formatSearchResults(results, searchDescription, includePageTitle = true) {
26
+ if (!results || results.length === 0) {
27
+ return {
28
+ success: true,
29
+ matches: [],
30
+ message: `No blocks found ${searchDescription}`
31
+ };
32
+ }
33
+ const matches = results.map(([uid, content, pageTitle]) => ({
34
+ block_uid: uid,
35
+ content,
36
+ ...(includePageTitle && pageTitle && { page_title: pageTitle })
37
+ }));
38
+ return {
39
+ success: true,
40
+ matches,
41
+ message: `Found ${matches.length} block(s) ${searchDescription}`
42
+ };
43
+ }
44
+ /**
45
+ * Format a tag for searching, handling both # and [[]] formats
46
+ * @param tag Tag without prefix
47
+ * @returns Array of possible formats to search for
48
+ */
49
+ static formatTag(tag) {
50
+ // Remove any existing prefixes
51
+ const cleanTag = tag.replace(/^#|\[\[|\]\]$/g, '');
52
+ // Return both formats for comprehensive search
53
+ return [`#${cleanTag}`, `[[${cleanTag}]]`];
54
+ }
55
+ /**
56
+ * Parse a date string into a Roam-formatted date
57
+ */
58
+ static parseDate(dateStr) {
59
+ const date = new Date(dateStr);
60
+ const months = [
61
+ 'January', 'February', 'March', 'April', 'May', 'June',
62
+ 'July', 'August', 'September', 'October', 'November', 'December'
63
+ ];
64
+ // Adjust for timezone to ensure consistent date comparison
65
+ const utcDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000);
66
+ return `${months[utcDate.getMonth()]} ${utcDate.getDate()}${this.getOrdinalSuffix(utcDate.getDate())}, ${utcDate.getFullYear()}`;
67
+ }
68
+ /**
69
+ * Parse a date string into a Roam-formatted date range
70
+ * Returns [startDate, endDate] with endDate being inclusive (end of day)
71
+ */
72
+ static parseDateRange(startStr, endStr) {
73
+ const startDate = new Date(startStr);
74
+ const endDate = new Date(endStr);
75
+ endDate.setHours(23, 59, 59, 999); // Make end date inclusive
76
+ const months = [
77
+ 'January', 'February', 'March', 'April', 'May', 'June',
78
+ 'July', 'August', 'September', 'October', 'November', 'December'
79
+ ];
80
+ // Adjust for timezone
81
+ const utcStart = new Date(startDate.getTime() + startDate.getTimezoneOffset() * 60000);
82
+ const utcEnd = new Date(endDate.getTime() + endDate.getTimezoneOffset() * 60000);
83
+ return [
84
+ `${months[utcStart.getMonth()]} ${utcStart.getDate()}${this.getOrdinalSuffix(utcStart.getDate())}, ${utcStart.getFullYear()}`,
85
+ `${months[utcEnd.getMonth()]} ${utcEnd.getDate()}${this.getOrdinalSuffix(utcEnd.getDate())}, ${utcEnd.getFullYear()}`
86
+ ];
87
+ }
88
+ static getOrdinalSuffix(day) {
89
+ if (day > 3 && day < 21)
90
+ return 'th';
91
+ switch (day % 10) {
92
+ case 1: return 'st';
93
+ case 2: return 'nd';
94
+ case 3: return 'rd';
95
+ default: return 'th';
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,145 @@
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
+ import { TagSearchHandler, BlockRefSearchHandler, HierarchySearchHandler } from '../search/index.js';
9
+ export class RoamServer {
10
+ server;
11
+ toolHandlers;
12
+ graph;
13
+ constructor() {
14
+ this.graph = initializeGraph({
15
+ token: API_TOKEN,
16
+ graph: GRAPH_NAME,
17
+ });
18
+ this.toolHandlers = new ToolHandlers(this.graph);
19
+ this.server = new Server({
20
+ name: 'roam-research',
21
+ version: '0.12.1',
22
+ }, {
23
+ capabilities: {
24
+ tools: {
25
+ roam_add_todo: {},
26
+ roam_fetch_page_by_title: {},
27
+ roam_create_page: {},
28
+ roam_create_block: {},
29
+ roam_import_markdown: {},
30
+ roam_create_outline: {},
31
+ roam_search_for_tag: {},
32
+ roam_search_by_status: {},
33
+ roam_search_block_refs: {},
34
+ roam_search_hierarchy: {}
35
+ },
36
+ },
37
+ });
38
+ this.setupRequestHandlers();
39
+ // Error handling
40
+ this.server.onerror = (error) => { };
41
+ process.on('SIGINT', async () => {
42
+ await this.server.close();
43
+ process.exit(0);
44
+ });
45
+ }
46
+ setupRequestHandlers() {
47
+ // List available tools
48
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
49
+ tools: Object.values(toolSchemas),
50
+ }));
51
+ // Handle tool calls
52
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
53
+ try {
54
+ switch (request.params.name) {
55
+ case 'roam_fetch_page_by_title': {
56
+ const { title } = request.params.arguments;
57
+ const content = await this.toolHandlers.fetchPageByTitle(title);
58
+ return {
59
+ content: [{ type: 'text', text: content }],
60
+ };
61
+ }
62
+ case 'roam_create_page': {
63
+ const { title, content } = request.params.arguments;
64
+ const result = await this.toolHandlers.createPage(title, content);
65
+ return {
66
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
67
+ };
68
+ }
69
+ case 'roam_create_block': {
70
+ const { content, page_uid, title } = request.params.arguments;
71
+ const result = await this.toolHandlers.createBlock(content, page_uid, title);
72
+ return {
73
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
74
+ };
75
+ }
76
+ case 'roam_import_markdown': {
77
+ const { content, page_uid, page_title, parent_uid, parent_string, order = 'first' } = request.params.arguments;
78
+ const result = await this.toolHandlers.importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order);
79
+ return {
80
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
81
+ };
82
+ }
83
+ case 'roam_add_todo': {
84
+ const { todos } = request.params.arguments;
85
+ const result = await this.toolHandlers.addTodos(todos);
86
+ return {
87
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
88
+ };
89
+ }
90
+ case 'roam_create_outline': {
91
+ const { outline, page_title_uid, block_text_uid } = request.params.arguments;
92
+ const result = await this.toolHandlers.createOutline(outline, page_title_uid, block_text_uid);
93
+ return {
94
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
95
+ };
96
+ }
97
+ case 'roam_search_for_tag': {
98
+ const params = request.params.arguments;
99
+ const handler = new TagSearchHandler(this.graph, params);
100
+ const result = await handler.execute();
101
+ return {
102
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
103
+ };
104
+ }
105
+ case 'roam_search_by_status': {
106
+ const { status, page_title_uid, include, exclude } = request.params.arguments;
107
+ const result = await this.toolHandlers.searchByStatus(status, page_title_uid, include, exclude);
108
+ return {
109
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
110
+ };
111
+ }
112
+ case 'roam_search_block_refs': {
113
+ const params = request.params.arguments;
114
+ const handler = new BlockRefSearchHandler(this.graph, params);
115
+ const result = await handler.execute();
116
+ return {
117
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
118
+ };
119
+ }
120
+ case 'roam_search_hierarchy': {
121
+ const params = request.params.arguments;
122
+ const handler = new HierarchySearchHandler(this.graph, params);
123
+ const result = await handler.execute();
124
+ return {
125
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
126
+ };
127
+ }
128
+ default:
129
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
130
+ }
131
+ }
132
+ catch (error) {
133
+ if (error instanceof McpError) {
134
+ throw error;
135
+ }
136
+ const errorMessage = error instanceof Error ? error.message : String(error);
137
+ throw new McpError(ErrorCode.InternalError, `Roam API error: ${errorMessage}`);
138
+ }
139
+ });
140
+ }
141
+ async run() {
142
+ const transport = new StdioServerTransport();
143
+ await this.server.connect(transport);
144
+ }
145
+ }
@@ -1,5 +1,5 @@
1
1
  import { initializeGraph, createPage, batchActions, q } from '@roam-research/roam-api-sdk';
2
- import { parseMarkdown, convertToRoamActions } from './markdown-utils.js';
2
+ import { parseMarkdown, convertToRoamActions } from '../src/markdown-utils.js';
3
3
  import * as dotenv from 'dotenv';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { dirname, join } from 'path';