roam-research-mcp 0.19.0 → 0.22.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,56 @@
1
+ import { TagSearchHandler, BlockRefSearchHandler, HierarchySearchHandler, TextSearchHandler } from '../../../search/index.js';
2
+ // Base class for all search handlers
3
+ export class BaseSearchHandler {
4
+ graph;
5
+ constructor(graph) {
6
+ this.graph = graph;
7
+ }
8
+ }
9
+ // Tag search handler
10
+ export class TagSearchHandlerImpl extends BaseSearchHandler {
11
+ params;
12
+ constructor(graph, params) {
13
+ super(graph);
14
+ this.params = params;
15
+ }
16
+ async execute() {
17
+ const handler = new TagSearchHandler(this.graph, this.params);
18
+ return handler.execute();
19
+ }
20
+ }
21
+ // Block reference search handler
22
+ export class BlockRefSearchHandlerImpl extends BaseSearchHandler {
23
+ params;
24
+ constructor(graph, params) {
25
+ super(graph);
26
+ this.params = params;
27
+ }
28
+ async execute() {
29
+ const handler = new BlockRefSearchHandler(this.graph, this.params);
30
+ return handler.execute();
31
+ }
32
+ }
33
+ // Hierarchy search handler
34
+ export class HierarchySearchHandlerImpl extends BaseSearchHandler {
35
+ params;
36
+ constructor(graph, params) {
37
+ super(graph);
38
+ this.params = params;
39
+ }
40
+ async execute() {
41
+ const handler = new HierarchySearchHandler(this.graph, this.params);
42
+ return handler.execute();
43
+ }
44
+ }
45
+ // Text search handler
46
+ export class TextSearchHandlerImpl extends BaseSearchHandler {
47
+ params;
48
+ constructor(graph, params) {
49
+ super(graph);
50
+ this.params = params;
51
+ }
52
+ async execute() {
53
+ const handler = new TextSearchHandler(this.graph, this.params);
54
+ return handler.execute();
55
+ }
56
+ }
@@ -0,0 +1,95 @@
1
+ import { TagSearchHandlerImpl, BlockRefSearchHandlerImpl, HierarchySearchHandlerImpl, TextSearchHandlerImpl } from './handlers.js';
2
+ export class SearchOperations {
3
+ graph;
4
+ constructor(graph) {
5
+ this.graph = graph;
6
+ }
7
+ async searchByStatus(status, page_title_uid, include, exclude) {
8
+ const handler = new TagSearchHandlerImpl(this.graph, {
9
+ primary_tag: `{{[[${status}]]}}`,
10
+ page_title_uid,
11
+ });
12
+ const result = await handler.execute();
13
+ // Post-process results with include/exclude filters
14
+ let matches = result.matches;
15
+ if (include) {
16
+ const includeTerms = include.split(',').map(term => term.trim());
17
+ matches = matches.filter(match => {
18
+ const matchContent = match.content;
19
+ const matchTitle = match.page_title;
20
+ const terms = includeTerms;
21
+ return terms.some(term => matchContent.includes(term) ||
22
+ (matchTitle && matchTitle.includes(term)));
23
+ });
24
+ }
25
+ if (exclude) {
26
+ const excludeTerms = exclude.split(',').map(term => term.trim());
27
+ matches = matches.filter(match => {
28
+ const matchContent = match.content;
29
+ const matchTitle = match.page_title;
30
+ const terms = excludeTerms;
31
+ return !terms.some(term => matchContent.includes(term) ||
32
+ (matchTitle && matchTitle.includes(term)));
33
+ });
34
+ }
35
+ return {
36
+ success: true,
37
+ matches,
38
+ message: `Found ${matches.length} block(s) with status ${status}${include ? ` including "${include}"` : ''}${exclude ? ` excluding "${exclude}"` : ''}`
39
+ };
40
+ }
41
+ async searchForTag(primary_tag, page_title_uid, near_tag) {
42
+ const handler = new TagSearchHandlerImpl(this.graph, {
43
+ primary_tag,
44
+ page_title_uid,
45
+ near_tag,
46
+ });
47
+ return handler.execute();
48
+ }
49
+ async searchBlockRefs(params) {
50
+ const handler = new BlockRefSearchHandlerImpl(this.graph, params);
51
+ return handler.execute();
52
+ }
53
+ async searchHierarchy(params) {
54
+ const handler = new HierarchySearchHandlerImpl(this.graph, params);
55
+ return handler.execute();
56
+ }
57
+ async searchByText(params) {
58
+ const handler = new TextSearchHandlerImpl(this.graph, params);
59
+ return handler.execute();
60
+ }
61
+ async searchByDate(params) {
62
+ // Convert dates to timestamps
63
+ const startTimestamp = new Date(`${params.start_date}T00:00:00`).getTime();
64
+ const endTimestamp = params.end_date ? new Date(`${params.end_date}T23:59:59`).getTime() : undefined;
65
+ // Use text search handler for content-based filtering
66
+ const handler = new TextSearchHandlerImpl(this.graph, {
67
+ text: '', // Empty text to match all blocks
68
+ });
69
+ const result = await handler.execute();
70
+ // Filter results by date
71
+ const matches = result.matches
72
+ .filter(match => {
73
+ const time = params.type === 'created' ?
74
+ new Date(match.content || '').getTime() : // Use content date for creation time
75
+ Date.now(); // Use current time for modification time (simplified)
76
+ return time >= startTimestamp && (!endTimestamp || time <= endTimestamp);
77
+ })
78
+ .map(match => ({
79
+ uid: match.block_uid,
80
+ type: 'block',
81
+ time: params.type === 'created' ?
82
+ new Date(match.content || '').getTime() :
83
+ Date.now(),
84
+ ...(params.include_content && { content: match.content }),
85
+ page_title: match.page_title
86
+ }));
87
+ // Sort by time
88
+ const sortedMatches = matches.sort((a, b) => b.time - a.time);
89
+ return {
90
+ success: true,
91
+ matches: sortedMatches,
92
+ message: `Found ${sortedMatches.length} matches for the given date range and criteria`
93
+ };
94
+ }
95
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,285 @@
1
+ import { q } from '@roam-research/roam-api-sdk';
2
+ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
3
+ import { BlockRefSearchHandler, HierarchySearchHandler, TextSearchHandler } from '../../search/index.js';
4
+ export class SearchOperations {
5
+ graph;
6
+ constructor(graph) {
7
+ this.graph = graph;
8
+ }
9
+ async searchBlockRefs(params) {
10
+ const handler = new BlockRefSearchHandler(this.graph, params);
11
+ return handler.execute();
12
+ }
13
+ async searchHierarchy(params) {
14
+ const handler = new HierarchySearchHandler(this.graph, params);
15
+ return handler.execute();
16
+ }
17
+ async searchByText(params) {
18
+ const handler = new TextSearchHandler(this.graph, params);
19
+ return handler.execute();
20
+ }
21
+ async searchByStatus(status, page_title_uid, include, exclude, case_sensitive = true) {
22
+ // Get target page UID if provided
23
+ let targetPageUid;
24
+ if (page_title_uid) {
25
+ // Try to find page by title or UID
26
+ const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
27
+ const findResults = await q(this.graph, findQuery, [page_title_uid]);
28
+ if (findResults && findResults.length > 0) {
29
+ targetPageUid = findResults[0][0];
30
+ }
31
+ else {
32
+ // Try as UID
33
+ const uidQuery = `[:find ?uid :where [?e :block/uid "${page_title_uid}"] [?e :block/uid ?uid]]`;
34
+ const uidResults = await q(this.graph, uidQuery, []);
35
+ if (!uidResults || uidResults.length === 0) {
36
+ throw new McpError(ErrorCode.InvalidRequest, `Page with title/UID "${page_title_uid}" not found`);
37
+ }
38
+ targetPageUid = uidResults[0][0];
39
+ }
40
+ }
41
+ // Build query based on whether we're searching in a specific page
42
+ let queryStr;
43
+ let queryParams;
44
+ const statusPattern = `{{[[${status}]]}}`;
45
+ if (targetPageUid) {
46
+ queryStr = `[:find ?block-uid ?block-str
47
+ :in $ ?status-pattern ?page-uid
48
+ :where [?p :block/uid ?page-uid]
49
+ [?b :block/page ?p]
50
+ [?b :block/string ?block-str]
51
+ [?b :block/uid ?block-uid]
52
+ [(clojure.string/includes? ?block-str ?status-pattern)]]`;
53
+ queryParams = [statusPattern, targetPageUid];
54
+ }
55
+ else {
56
+ queryStr = `[:find ?block-uid ?block-str ?page-title
57
+ :in $ ?status-pattern
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
+ [(clojure.string/includes? ?block-str ?status-pattern)]]`;
63
+ queryParams = [statusPattern];
64
+ }
65
+ const results = await q(this.graph, queryStr, queryParams);
66
+ if (!results || results.length === 0) {
67
+ return {
68
+ success: true,
69
+ matches: [],
70
+ message: `No blocks found with status ${status}`
71
+ };
72
+ }
73
+ // Format initial results
74
+ let matches = results.map(result => {
75
+ const [uid, content, pageTitle] = result;
76
+ return {
77
+ block_uid: uid,
78
+ content,
79
+ ...(pageTitle && { page_title: pageTitle })
80
+ };
81
+ });
82
+ // Post-query filtering with case sensitivity option
83
+ if (include) {
84
+ const includeTerms = include.split(',').map(term => term.trim());
85
+ matches = matches.filter(match => {
86
+ const matchContent = case_sensitive ? match.content : match.content.toLowerCase();
87
+ const matchTitle = match.page_title && (case_sensitive ? match.page_title : match.page_title.toLowerCase());
88
+ const terms = case_sensitive ? includeTerms : includeTerms.map(t => t.toLowerCase());
89
+ return terms.some(term => matchContent.includes(case_sensitive ? term : term.toLowerCase()) ||
90
+ (matchTitle && matchTitle.includes(case_sensitive ? term : term.toLowerCase())));
91
+ });
92
+ }
93
+ if (exclude) {
94
+ const excludeTerms = exclude.split(',').map(term => term.trim());
95
+ matches = matches.filter(match => {
96
+ const matchContent = case_sensitive ? match.content : match.content.toLowerCase();
97
+ const matchTitle = match.page_title && (case_sensitive ? match.page_title : match.page_title.toLowerCase());
98
+ const terms = case_sensitive ? excludeTerms : excludeTerms.map(t => t.toLowerCase());
99
+ return !terms.some(term => matchContent.includes(case_sensitive ? term : term.toLowerCase()) ||
100
+ (matchTitle && matchTitle.includes(case_sensitive ? term : term.toLowerCase())));
101
+ });
102
+ }
103
+ return {
104
+ success: true,
105
+ matches,
106
+ message: `Found ${matches.length} block(s) with status ${status}${include ? ` including "${include}"` : ''}${exclude ? ` excluding "${exclude}"` : ''}`
107
+ };
108
+ }
109
+ async searchForTag(primary_tag, page_title_uid, near_tag, case_sensitive = true) {
110
+ // Ensure tags are properly formatted with #
111
+ const formatTag = (tag) => {
112
+ return tag.replace(/^#/, '').replace(/^\[\[/, '').replace(/\]\]$/, '');
113
+ };
114
+ // Extract the tag text, removing any formatting
115
+ const primaryTagFormatted = formatTag(primary_tag);
116
+ const nearTagFormatted = near_tag ? formatTag(near_tag) : undefined;
117
+ // Get target page UID if provided
118
+ let targetPageUid;
119
+ if (page_title_uid) {
120
+ // Try to find page by title or UID
121
+ const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
122
+ const findResults = await q(this.graph, findQuery, [page_title_uid]);
123
+ if (findResults && findResults.length > 0) {
124
+ targetPageUid = findResults[0][0];
125
+ }
126
+ else {
127
+ // Try as UID
128
+ const uidQuery = `[:find ?uid :where [?e :block/uid "${page_title_uid}"] [?e :block/uid ?uid]]`;
129
+ const uidResults = await q(this.graph, uidQuery, []);
130
+ if (!uidResults || uidResults.length === 0) {
131
+ throw new McpError(ErrorCode.InvalidRequest, `Page with title/UID "${page_title_uid}" not found`);
132
+ }
133
+ targetPageUid = uidResults[0][0];
134
+ }
135
+ }
136
+ // Build query based on whether we're searching in a specific page and/or for a nearby tag
137
+ let queryStr;
138
+ let queryParams;
139
+ if (targetPageUid) {
140
+ if (nearTagFormatted) {
141
+ queryStr = `[:find ?block-uid ?block-str
142
+ :in $ ?primary-tag ?near-tag ?page-uid
143
+ :where [?p :block/uid ?page-uid]
144
+ [?b :block/page ?p]
145
+ [?b :block/string ?block-str]
146
+ [?b :block/uid ?block-uid]
147
+ [(clojure.string/includes?
148
+ ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
149
+ ${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]
150
+ [(clojure.string/includes?
151
+ ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
152
+ ${case_sensitive ? '?near-tag' : '(clojure.string/lower-case ?near-tag)'})]`;
153
+ queryParams = [primaryTagFormatted, nearTagFormatted, targetPageUid];
154
+ }
155
+ else {
156
+ queryStr = `[:find ?block-uid ?block-str
157
+ :in $ ?primary-tag ?page-uid
158
+ :where [?p :block/uid ?page-uid]
159
+ [?b :block/page ?p]
160
+ [?b :block/string ?block-str]
161
+ [?b :block/uid ?block-uid]
162
+ [(clojure.string/includes?
163
+ ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
164
+ ${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]`;
165
+ queryParams = [primaryTagFormatted, targetPageUid];
166
+ }
167
+ }
168
+ else {
169
+ // Search across all pages
170
+ if (nearTagFormatted) {
171
+ queryStr = `[:find ?block-uid ?block-str ?page-title
172
+ :in $ ?primary-tag ?near-tag
173
+ :where [?b :block/string ?block-str]
174
+ [?b :block/uid ?block-uid]
175
+ [?b :block/page ?p]
176
+ [?p :node/title ?page-title]
177
+ [(clojure.string/includes?
178
+ ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
179
+ ${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]
180
+ [(clojure.string/includes?
181
+ ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
182
+ ${case_sensitive ? '?near-tag' : '(clojure.string/lower-case ?near-tag)'})]`;
183
+ queryParams = [primaryTagFormatted, nearTagFormatted];
184
+ }
185
+ else {
186
+ queryStr = `[:find ?block-uid ?block-str ?page-title
187
+ :in $ ?primary-tag
188
+ :where [?b :block/string ?block-str]
189
+ [?b :block/uid ?block-uid]
190
+ [?b :block/page ?p]
191
+ [?p :node/title ?page-title]
192
+ [(clojure.string/includes?
193
+ ${case_sensitive ? '?block-str' : '(clojure.string/lower-case ?block-str)'}
194
+ ${case_sensitive ? '?primary-tag' : '(clojure.string/lower-case ?primary-tag)'})]`;
195
+ queryParams = [primaryTagFormatted];
196
+ }
197
+ }
198
+ const results = await q(this.graph, queryStr, queryParams);
199
+ if (!results || results.length === 0) {
200
+ return {
201
+ success: true,
202
+ matches: [],
203
+ message: `No blocks found containing ${primaryTagFormatted}${nearTagFormatted ? ` near ${nearTagFormatted}` : ''}`
204
+ };
205
+ }
206
+ // Format results
207
+ const matches = results.map(([uid, content, pageTitle]) => ({
208
+ block_uid: uid,
209
+ content,
210
+ ...(pageTitle && { page_title: pageTitle })
211
+ }));
212
+ return {
213
+ success: true,
214
+ matches,
215
+ message: `Found ${matches.length} block(s) containing ${primaryTagFormatted}${nearTagFormatted ? ` near ${nearTagFormatted}` : ''}`
216
+ };
217
+ }
218
+ async searchByDate(params) {
219
+ // Convert dates to timestamps
220
+ const startTimestamp = new Date(`${params.start_date}T00:00:00`).getTime();
221
+ const endTimestamp = params.end_date ? new Date(`${params.end_date}T23:59:59`).getTime() : undefined;
222
+ // Define rule for entity type
223
+ const entityRule = `[
224
+ [(block? ?e)
225
+ [?e :block/string]
226
+ [?e :block/page ?p]
227
+ [?p :node/title]]
228
+ [(page? ?e)
229
+ [?e :node/title]]
230
+ ]`;
231
+ // Build query based on cheatsheet pattern
232
+ const timeAttr = params.type === 'created' ? ':create/time' : ':edit/time';
233
+ let queryStr = `[:find ?block-uid ?string ?time ?page-title
234
+ :in $ ?start-ts ${endTimestamp ? '?end-ts' : ''}
235
+ :where
236
+ [?b ${timeAttr} ?time]
237
+ [(>= ?time ?start-ts)]
238
+ ${endTimestamp ? '[(<= ?time ?end-ts)]' : ''}
239
+ [?b :block/uid ?block-uid]
240
+ [?b :block/string ?string]
241
+ [?b :block/page ?p]
242
+ [?p :node/title ?page-title]]`;
243
+ // Execute query
244
+ const queryParams = endTimestamp ?
245
+ [startTimestamp, endTimestamp] :
246
+ [startTimestamp];
247
+ const results = await q(this.graph, queryStr, queryParams);
248
+ if (!results || results.length === 0) {
249
+ return {
250
+ success: true,
251
+ matches: [],
252
+ message: 'No matches found for the given date range and criteria'
253
+ };
254
+ }
255
+ // Process results - now we get [block-uid, string, time, page-title]
256
+ const matches = results.map(([uid, content, time, pageTitle]) => ({
257
+ uid,
258
+ type: 'block',
259
+ time,
260
+ ...(params.include_content && { content }),
261
+ page_title: pageTitle
262
+ }));
263
+ // Apply case sensitivity if content is included
264
+ if (params.include_content) {
265
+ const case_sensitive = params.case_sensitive ?? true; // Default to true to match Roam's behavior
266
+ if (!case_sensitive) {
267
+ matches.forEach(match => {
268
+ if (match.content) {
269
+ match.content = match.content.toLowerCase();
270
+ }
271
+ if (match.page_title) {
272
+ match.page_title = match.page_title.toLowerCase();
273
+ }
274
+ });
275
+ }
276
+ }
277
+ // Sort by time
278
+ const sortedMatches = matches.sort((a, b) => b.time - a.time);
279
+ return {
280
+ success: true,
281
+ matches: sortedMatches,
282
+ message: `Found ${sortedMatches.length} matches for the given date range and criteria`
283
+ };
284
+ }
285
+ }
@@ -0,0 +1,82 @@
1
+ import { q, createBlock, createPage, batchActions } from '@roam-research/roam-api-sdk';
2
+ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
3
+ import { formatRoamDate } from '../../utils/helpers.js';
4
+ export class TodoOperations {
5
+ graph;
6
+ constructor(graph) {
7
+ this.graph = graph;
8
+ }
9
+ async addTodos(todos) {
10
+ if (!Array.isArray(todos) || todos.length === 0) {
11
+ throw new McpError(ErrorCode.InvalidRequest, 'todos must be a non-empty array');
12
+ }
13
+ // Get today's date
14
+ const today = new Date();
15
+ const dateStr = formatRoamDate(today);
16
+ // Try to find today's page
17
+ const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
18
+ const findResults = await q(this.graph, findQuery, [dateStr]);
19
+ let targetPageUid;
20
+ if (findResults && findResults.length > 0) {
21
+ targetPageUid = findResults[0][0];
22
+ }
23
+ else {
24
+ // Create today's page if it doesn't exist
25
+ try {
26
+ await createPage(this.graph, {
27
+ action: 'create-page',
28
+ page: { title: dateStr }
29
+ });
30
+ // Get the new page's UID
31
+ const results = await q(this.graph, findQuery, [dateStr]);
32
+ if (!results || results.length === 0) {
33
+ throw new Error('Could not find created today\'s page');
34
+ }
35
+ targetPageUid = results[0][0];
36
+ }
37
+ catch (error) {
38
+ throw new Error('Failed to create today\'s page');
39
+ }
40
+ }
41
+ // If more than 10 todos, use batch actions
42
+ const todo_tag = "{{TODO}}";
43
+ if (todos.length > 10) {
44
+ const actions = todos.map((todo, index) => ({
45
+ action: 'create-block',
46
+ location: {
47
+ 'parent-uid': targetPageUid,
48
+ order: index
49
+ },
50
+ block: {
51
+ string: `${todo_tag} ${todo}`
52
+ }
53
+ }));
54
+ const result = await batchActions(this.graph, {
55
+ action: 'batch-actions',
56
+ actions
57
+ });
58
+ if (!result) {
59
+ throw new Error('Failed to create todo blocks');
60
+ }
61
+ }
62
+ else {
63
+ // Create todos sequentially
64
+ for (const todo of todos) {
65
+ try {
66
+ await createBlock(this.graph, {
67
+ action: 'create-block',
68
+ location: {
69
+ "parent-uid": targetPageUid,
70
+ "order": "last"
71
+ },
72
+ block: { string: `${todo_tag} ${todo}` }
73
+ });
74
+ }
75
+ catch (error) {
76
+ throw new Error('Failed to create todo block');
77
+ }
78
+ }
79
+ }
80
+ return { success: true };
81
+ }
82
+ }
@@ -73,8 +73,8 @@ export const toolSchemas = {
73
73
  },
74
74
  },
75
75
  roam_create_outline: {
76
- name: 'roam_create_output_with_nested_structure',
77
- description: 'Create a structured outline or output with nested structure in Roam from an array of items with explicit levels. Can be added on a specific page or under a specific block.',
76
+ name: 'roam_create_outline',
77
+ description: 'Create a structured outline with nested structure in Roam from an array of items with explicit levels. Can be added on a specific page or under a specific block. Ideal for saving a conversation with an LLM response, research, or organizing thoughts.',
78
78
  inputSchema: {
79
79
  type: 'object',
80
80
  properties: {
@@ -98,7 +98,9 @@ export const toolSchemas = {
98
98
  },
99
99
  level: {
100
100
  type: 'integer',
101
- description: 'Indentation level (1-5, where 1 is top level)'
101
+ description: 'Indentation level (1-10, where 1 is top level)',
102
+ minimum: 1,
103
+ maximum: 10
102
104
  }
103
105
  },
104
106
  required: ['text', 'level']
@@ -146,7 +148,7 @@ export const toolSchemas = {
146
148
  },
147
149
  roam_search_for_tag: {
148
150
  name: 'roam_search_for_tag',
149
- description: 'Search for blocks containing a specific tag and optionally filter by blocks that also contain another tag nearby.',
151
+ description: 'Search for blocks containing a specific tag and optionally filter by blocks that also contain another tag nearby. Example: Use this to search for memories that are tagged with the MEMORIES_TAG.',
150
152
  inputSchema: {
151
153
  type: 'object',
152
154
  properties: {
@@ -161,11 +163,6 @@ export const toolSchemas = {
161
163
  near_tag: {
162
164
  type: 'string',
163
165
  description: 'Optional: Another tag to filter results by - will only return blocks where both tags appear',
164
- },
165
- case_sensitive: {
166
- type: 'boolean',
167
- description: 'Optional: Whether to perform case-sensitive matching (default: true, matching Roam\'s native behavior)',
168
- default: true
169
166
  }
170
167
  },
171
168
  required: ['primary_tag']
@@ -193,11 +190,6 @@ export const toolSchemas = {
193
190
  exclude: {
194
191
  type: 'string',
195
192
  description: 'Optional: Comma-separated list of terms to filter results by exclusion (matches content or page title)'
196
- },
197
- case_sensitive: {
198
- type: 'boolean',
199
- description: 'Optional: Whether to perform case-sensitive matching (default: true, matching Roam\'s native behavior)',
200
- default: true
201
193
  }
202
194
  },
203
195
  required: ['status']
@@ -273,11 +265,6 @@ export const toolSchemas = {
273
265
  page_title_uid: {
274
266
  type: 'string',
275
267
  description: 'Optional: Title or UID of the page to search in. If not provided, searches across all pages'
276
- },
277
- case_sensitive: {
278
- type: 'boolean',
279
- description: 'Optional: Whether to perform case-sensitive search (default: true, matching Roam\'s native behavior)',
280
- default: true
281
268
  }
282
269
  },
283
270
  required: ['text']
@@ -405,14 +392,39 @@ export const toolSchemas = {
405
392
  type: 'boolean',
406
393
  description: 'Whether to include the content of matching blocks/pages',
407
394
  default: true,
408
- },
409
- case_sensitive: {
410
- type: 'boolean',
411
- description: 'Optional: Whether to perform case-sensitive matching (default: true, matching Roam\'s native behavior)',
412
- default: true
413
395
  }
414
396
  },
415
397
  required: ['start_date', 'type', 'scope']
416
398
  }
399
+ },
400
+ roam_remember: {
401
+ name: 'roam_remember',
402
+ description: 'Add a memory or piece of information to remember, stored on the daily page with #[[LLM/Memories]] tag and optional categories. Use roam_search_for_tag with "LLM/Memories" to find stored memories.\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).',
403
+ inputSchema: {
404
+ type: 'object',
405
+ properties: {
406
+ memory: {
407
+ type: 'string',
408
+ description: 'The memory or information to remember'
409
+ },
410
+ categories: {
411
+ type: 'array',
412
+ items: {
413
+ type: 'string'
414
+ },
415
+ description: 'Optional categories to tag the memory with (will be converted to Roam tags)'
416
+ }
417
+ },
418
+ required: ['memory']
419
+ }
420
+ },
421
+ roam_recall: {
422
+ name: 'roam_recall',
423
+ description: 'Retrieve all stored memories by searching for blocks tagged with MEMORIES_TAG and content from the page with the same name. Returns a combined, deduplicated list of memories.',
424
+ inputSchema: {
425
+ type: 'object',
426
+ properties: {},
427
+ required: []
428
+ }
417
429
  }
418
430
  };