roam-research-mcp 0.30.1 → 0.32.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/README.md +103 -128
- package/build/Roam_Markdown_Cheatsheet.md +118 -0
- package/build/config/environment.js +3 -2
- package/build/markdown-utils.js +3 -3
- package/build/search/tag-search.js +15 -6
- package/build/server/roam-server.js +55 -70
- package/build/tools/operations/block-retrieval.js +74 -0
- package/build/tools/operations/blocks.js +0 -8
- package/build/tools/operations/outline.js +3 -0
- package/build/tools/operations/pages.js +2 -2
- package/build/tools/schemas.js +48 -16
- package/build/tools/tool-handlers.js +25 -0
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# Roam Research MCP Server
|
|
2
4
|
|
|
3
5
|
[](https://badge.fury.io/js/roam-research-mcp)
|
|
@@ -8,6 +10,7 @@
|
|
|
8
10
|
A Model Context Protocol (MCP) server that provides comprehensive access to Roam Research's API functionality. This server enables AI assistants like Claude to interact with your Roam Research graph through a standardized interface. It supports standard input/output (stdio), HTTP Stream, and Server-Sent Events (SSE) communication. (A WORK-IN-PROGRESS, personal project not officially endorsed by Roam Research)
|
|
9
11
|
|
|
10
12
|
<a href="https://glama.ai/mcp/servers/fzfznyaflu"><img width="380" height="200" src="https://glama.ai/mcp/servers/fzfznyaflu/badge" alt="Roam Research MCP server" /></a>
|
|
13
|
+
<a href="https://mseep.ai/app/2b3pro-roam-research-mcp"><img width="380" height="200" src="https://mseep.net/pr/2b3pro-roam-research-mcp-badge.png" alt="MseeP.ai Security Assessment Badge" /></a>
|
|
11
14
|
|
|
12
15
|
## Installation and Usage
|
|
13
16
|
|
|
@@ -102,23 +105,26 @@ The server provides powerful tools for interacting with Roam Research:
|
|
|
102
105
|
- Detailed debug logging
|
|
103
106
|
- Efficient batch operations
|
|
104
107
|
- Hierarchical outline creation
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
108
|
+
- Enhanced documentation for Roam Tables in `Roam_Markdown_Cheatsheet.md` for clearer guidance on nesting.
|
|
109
|
+
|
|
110
|
+
1. `roam_fetch_page_by_title`: Fetch page content by title. Returns content in the specified format.
|
|
111
|
+
2. `roam_fetch_block_with_children`: Fetch a block by its UID along with its hierarchical children down to a specified depth. Automatically handles `((UID))` formatting.
|
|
112
|
+
3. `roam_create_page`: Create new pages with optional content and headings.
|
|
113
|
+
4. `roam_import_markdown`: Import nested markdown content under a specific block. (Internally uses `roam_process_batch_actions`.)
|
|
114
|
+
5. `roam_add_todo`: Add a list of todo items to today's daily page. (Internally uses `roam_process_batch_actions`.)
|
|
115
|
+
6. `roam_create_outline`: Add a structured outline to an existing page or block, with support for `children_view_type`. Best for simpler, sequential outlines. For complex nesting (e.g., tables), consider `roam_process_batch_actions`. If `page_title_uid` and `block_text_uid` are both blank, content defaults to the daily page. (Internally uses `roam_process_batch_actions`.)
|
|
116
|
+
7. `roam_search_block_refs`: Search for block references within a page or across the entire graph.
|
|
117
|
+
8. `roam_search_hierarchy`: Search for parent or child blocks in the block hierarchy.
|
|
118
|
+
9. `roam_find_pages_modified_today`: Find pages that have been modified today (since midnight).
|
|
119
|
+
10. `roam_search_by_text`: Search for blocks containing specific text.
|
|
120
|
+
11. `roam_search_by_status`: Search for blocks with a specific status (TODO/DONE) across all pages or within a specific page.
|
|
121
|
+
12. `roam_search_by_date`: Search for blocks or pages based on creation or modification dates.
|
|
122
|
+
13. `roam_search_for_tag`: Search for blocks containing a specific tag and optionally filter by blocks that also contain another tag nearby.
|
|
123
|
+
14. `roam_remember`: Add a memory or piece of information to remember. (Internally uses `roam_process_batch_actions`.)
|
|
124
|
+
15. `roam_recall`: Retrieve all stored memories.
|
|
125
|
+
16. `roam_datomic_query`: Execute a custom Datomic query on the Roam graph for advanced data retrieval beyond the available search tools.
|
|
126
|
+
17. `roam_markdown_cheatsheet`: Provides the content of the Roam Markdown Cheatsheet resource, optionally concatenated with custom instructions if `CUSTOM_INSTRUCTIONS_PATH` environment variable is set.
|
|
127
|
+
18. `roam_process_batch_actions`: Execute a sequence of low-level block actions (create, update, move, delete) in a single, non-transactional batch. Provides granular control for complex nesting like tables. (Note: For actions on existing blocks or within a specific page context, it is often necessary to first obtain valid page or block UIDs using tools like `roam_fetch_page_by_title`.)
|
|
122
128
|
|
|
123
129
|
**Deprecated Tools**:
|
|
124
130
|
The following tools have been deprecated as of `v.0.30.0` in favor of the more powerful and flexible `roam_process_batch_actions`:
|
|
@@ -127,7 +133,29 @@ The following tools have been deprecated as of `v.0.30.0` in favor of the more p
|
|
|
127
133
|
- `roam_update_block`: Use `roam_process_batch_actions` with the `update-block` action.
|
|
128
134
|
- `roam_update_multiple_blocks`: Use `roam_process_batch_actions` with multiple `update-block` actions.
|
|
129
135
|
|
|
130
|
-
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### Tool Usage Guidelines and Best Practices
|
|
139
|
+
|
|
140
|
+
**Pre-computation and Context Loading:**
|
|
141
|
+
✅ Before attempting any Roam operations, **it is highly recommended** to load the `Roam Markdown Cheatsheet` resource into your context. This ensures you have immediate access to the correct Roam-flavored Markdown syntax, including details for tables, block references, and other special formatting. Example prompt: "Read the Roam cheatsheet first. Then, … <rest of your instructions>"
|
|
142
|
+
|
|
143
|
+
- **Specific notes and preferences** concerning my Roam Research graph. Users can add their own specific notes and preferences for working with their own graph in the Cheatsheet.
|
|
144
|
+
|
|
145
|
+
**Identifying Pages and Blocks for Manipulation:**
|
|
146
|
+
To ensure accurate operations, always strive to identify target pages and blocks using their Unique Identifiers (UIDs) whenever possible. While some tools accept case-sensitive text titles or content, UIDs provide unambiguous references, reducing the risk of errors due to ambiguity or changes in text.
|
|
147
|
+
|
|
148
|
+
- **For Pages:** Use `roam_fetch_page_by_title` to retrieve a page's UID if you only have its title. Example: "Read the page titled 'Trip to Las Vegas'"
|
|
149
|
+
- **For Blocks:** If you need to manipulate an existing block, first use search tools like `roam_search_by_text`, `roam_search_for_tag`, or `roam_fetch_page_by_title` (with raw format) to find the block and obtain its UID. If the block exists on a page that has already been read, then a search isn't necessary.
|
|
150
|
+
|
|
151
|
+
**Case-Sensitivity:**
|
|
152
|
+
Be aware that text-based inputs (e.g., page titles, block content for search) are generally case-sensitive in Roam. Always match the exact casing of the text as it appears in your graph.
|
|
153
|
+
|
|
154
|
+
**Iterative Refinement and Verification:**
|
|
155
|
+
For complex operations, especially those involving nested structures or multiple changes, it is often beneficial to break down the task into smaller, verifiable steps. After each significant tool call, consider fetching the affected content to verify the changes before proceeding.
|
|
156
|
+
|
|
157
|
+
**Understanding Tool Nuances:**
|
|
158
|
+
Familiarize yourself with the specific behaviors and limitations of each tool. For instance, `roam_create_outline` is best for sequential outlines, while `roam_process_batch_actions` offers granular control for complex structures like tables. Refer to the individual tool descriptions for detailed usage notes.
|
|
131
159
|
|
|
132
160
|
When making changes to your Roam graph, precision in your requests is crucial for achieving desired outcomes.
|
|
133
161
|
|
|
@@ -141,119 +169,61 @@ Instead of:
|
|
|
141
169
|
Prefer:
|
|
142
170
|
`"parent_uid": "((some-unique-uid))"`
|
|
143
171
|
|
|
144
|
-
**Migrating from Deprecated Tools:**
|
|
145
|
-
The following examples demonstrate how to achieve the functionality of the deprecated tools using `roam_process_batch_actions`.
|
|
146
|
-
|
|
147
|
-
**1. Replacing `roam_create_block`:**
|
|
148
|
-
|
|
149
|
-
- **Old (Deprecated):**
|
|
150
|
-
```json
|
|
151
|
-
{
|
|
152
|
-
"tool_name": "roam_create_block",
|
|
153
|
-
"arguments": {
|
|
154
|
-
"content": "New block content",
|
|
155
|
-
"page_uid": "((page-uid))"
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
- **New (Recommended):**
|
|
160
|
-
```json
|
|
161
|
-
{
|
|
162
|
-
"tool_name": "roam_process_batch_actions",
|
|
163
|
-
"arguments": {
|
|
164
|
-
"actions": [
|
|
165
|
-
{
|
|
166
|
-
"action": "create-block",
|
|
167
|
-
"location": {
|
|
168
|
-
"parent-uid": "((page-uid))",
|
|
169
|
-
"order": "last"
|
|
170
|
-
},
|
|
171
|
-
"block": {
|
|
172
|
-
"string": "New block content"
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
]
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
**2. Replacing `roam_update_block`:**
|
|
181
|
-
|
|
182
|
-
- **Old (Deprecated):**
|
|
183
|
-
```json
|
|
184
|
-
{
|
|
185
|
-
"tool_name": "roam_update_block",
|
|
186
|
-
"arguments": {
|
|
187
|
-
"block_uid": "((block-uid))",
|
|
188
|
-
"content": "Updated block content"
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
- **New (Recommended):**
|
|
193
|
-
```json
|
|
194
|
-
{
|
|
195
|
-
"tool_name": "roam_process_batch_actions",
|
|
196
|
-
"arguments": {
|
|
197
|
-
"actions": [
|
|
198
|
-
{
|
|
199
|
-
"action": "update-block",
|
|
200
|
-
"uid": "((block-uid))",
|
|
201
|
-
"string": "Updated block content"
|
|
202
|
-
}
|
|
203
|
-
]
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**3. Replacing `roam_update_multiple_blocks`:**
|
|
209
|
-
|
|
210
|
-
- **Old (Deprecated):**
|
|
211
|
-
```json
|
|
212
|
-
{
|
|
213
|
-
"tool_name": "roam_update_multiple_blocks",
|
|
214
|
-
"arguments": {
|
|
215
|
-
"updates": [
|
|
216
|
-
{
|
|
217
|
-
"block_uid": "((block-uid-1))",
|
|
218
|
-
"content": "Content for block 1"
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
"block_uid": "((block-uid-2))",
|
|
222
|
-
"transform": {
|
|
223
|
-
"find": "old text",
|
|
224
|
-
"replace": "new text"
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
]
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
```
|
|
231
|
-
- **New (Recommended):**
|
|
232
|
-
```json
|
|
233
|
-
{
|
|
234
|
-
"tool_name": "roam_process_batch_actions",
|
|
235
|
-
"arguments": {
|
|
236
|
-
"actions": [
|
|
237
|
-
{
|
|
238
|
-
"action": "update-block",
|
|
239
|
-
"uid": "((block-uid-1))",
|
|
240
|
-
"string": "Content for block 1"
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
"action": "update-block",
|
|
244
|
-
"uid": "((block-uid-2))",
|
|
245
|
-
"string": "((block-content-with-new-text))"
|
|
246
|
-
// Note: Transformations (find/replace) must be handled by the client
|
|
247
|
-
// before sending the 'string' to roam_process_batch_actions.
|
|
248
|
-
}
|
|
249
|
-
]
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
172
|
**Caveat Regarding Heading Formatting:**
|
|
255
173
|
Please note that while the `roam_process_batch_actions` tool can set block headings (H1, H2, H3), directly **removing** an existing heading (i.e., reverting a heading block to a plain text block) through this tool is not currently supported by the Roam API. The `heading` attribute persists its value once set, and attempting to remove it by setting `heading` to `0`, `null`, or omitting the property will not unset the heading.
|
|
256
174
|
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Example Prompts
|
|
178
|
+
|
|
179
|
+
Here are some examples of how to creatively use the Roam tool in an LLM to interact with your Roam graph, particularly leveraging `roam_process_batch_actions` for complex operations.
|
|
180
|
+
|
|
181
|
+
### Example 1: Creating a Project Outline
|
|
182
|
+
|
|
183
|
+
This prompt demonstrates creating a new page and populating it with a structured outline using a single `roam_process_batch_actions` call.
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
"Create a new Roam page titled 'Project Alpha Planning' and add the following outline:
|
|
187
|
+
- Overview
|
|
188
|
+
- Goals
|
|
189
|
+
- Scope
|
|
190
|
+
- Team Members
|
|
191
|
+
- John Doe
|
|
192
|
+
- Jane Smith
|
|
193
|
+
- Tasks
|
|
194
|
+
- Task 1
|
|
195
|
+
- Subtask 1.1
|
|
196
|
+
- Subtask 1.2
|
|
197
|
+
- Task 2
|
|
198
|
+
- Deadlines"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Example 2: Updating Multiple To-Dos and Adding a New One
|
|
202
|
+
|
|
203
|
+
This example shows how to mark existing to-do items as `DONE` and add a new one, all within a single batch.
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
"Mark 'Finish report' and 'Review presentation' as done on today's daily page, and add a new todo 'Prepare for meeting'."
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Example 3: Moving and Updating a Block
|
|
210
|
+
|
|
211
|
+
This demonstrates moving a block from one location to another and simultaneously updating its content.
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
"Move the block 'Important note about client feedback' (from page 'Meeting Notes 2025-06-30') under the 'Action Items' section on the 'Project Alpha Planning' page, and change its content to 'Client feedback reviewed and incorporated'."
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Example 4: Making a Table
|
|
218
|
+
|
|
219
|
+
This demonstrates moving a block from one location to another and simultaneously updating its content.
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
"In Roam, add a new table on the page "Fruity Tables" that compares four types of fruits: apples, oranges, grapes, and dates. Choose randomly four areas to compare."
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
257
227
|
## Setup
|
|
258
228
|
|
|
259
229
|
1. Create a [Roam Research API token](https://x.com/RoamResearch/status/1789358175474327881):
|
|
@@ -303,6 +273,9 @@ Please note that while the `roam_process_batch_actions` tool can set block headi
|
|
|
303
273
|
Note: The server will first try to load from .env file, then fall back to environment variables from MCP settings.
|
|
304
274
|
|
|
305
275
|
3. Build the server (make sure you're in the root directory of the MCP):
|
|
276
|
+
|
|
277
|
+
Note: Customize 'Roam_Markdown_Cheatsheet.md' with any notes and preferences specific to your graph BEFORE building.
|
|
278
|
+
|
|
306
279
|
```bash
|
|
307
280
|
cd roam-research-mcp
|
|
308
281
|
npm install
|
|
@@ -333,6 +306,8 @@ Each error response includes:
|
|
|
333
306
|
- Detailed error message
|
|
334
307
|
- Suggestions for resolution when applicable
|
|
335
308
|
|
|
309
|
+
---
|
|
310
|
+
|
|
336
311
|
## Development
|
|
337
312
|
|
|
338
313
|
### Building
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
!!!! IMPORTANT: Always consult this cheatsheet for correct Roam-flavored markdown syntax BEFORE making any Roam tool calls.
|
|
2
|
+
|
|
3
|
+
# Roam Markdown Cheatsheet
|
|
4
|
+
|
|
5
|
+
⭐️📋 > > > START 📋⭐️
|
|
6
|
+
|
|
7
|
+
## Markdown Styles in Roam:
|
|
8
|
+
|
|
9
|
+
- **Bold Text here**
|
|
10
|
+
- **Italics Text here**
|
|
11
|
+
- External Link: `[Link text](URL)`
|
|
12
|
+
- Image Embed: ``
|
|
13
|
+
- ^^Highlighted Text here^^
|
|
14
|
+
- Bullet points: - or \* followed by a space and the text
|
|
15
|
+
- {{[[TODO]]}} todo text
|
|
16
|
+
- {{[[DONE]]}} todo text
|
|
17
|
+
- LaTeX: `$$E=mc^2$$` or `$$\sum_{i=1}^{n} i = \frac{n(n+1)}{2}$$`
|
|
18
|
+
|
|
19
|
+
## Roam-specific Markdown:
|
|
20
|
+
|
|
21
|
+
- Dates are in ordinal format: `[[January 1st, 2025]]`
|
|
22
|
+
- Block references: `((block-id))` This inserts a reference to the content of a specific block.
|
|
23
|
+
- Page references: `[[Page name]]` This creates a link to another page within your Roam graph.
|
|
24
|
+
- Link to blocks: `[Link Text](<((block-id))>)` This will link to the block.
|
|
25
|
+
- Embed block in a block: `{{[[embed]]: ((block-id))}}`
|
|
26
|
+
- To-do items: `{{[[TODO]]}} todo text` or `{{[[DONE]]}} todo text`
|
|
27
|
+
- Syntax highlighting for fenced code blocks (add language next to backticks before fenced code block - all in one block) - Example:
|
|
28
|
+
```javascript
|
|
29
|
+
const foo(bar) => {
|
|
30
|
+
return bar;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
- Tags:
|
|
34
|
+
- one-word: `#word`
|
|
35
|
+
- multiple words: `#[[two or more words]]`
|
|
36
|
+
- hyphenated words: `#self-esteem`
|
|
37
|
+
|
|
38
|
+
## Roam Tables
|
|
39
|
+
|
|
40
|
+
Roam tables are created by nesting blocks under a `{{[[table]]}}` parent block. The key to correct table rendering is to ensure proper indentation levels for headers and data cells. Each subsequent header or data cell within a row must be nested one level deeper than the previous one.
|
|
41
|
+
|
|
42
|
+
- The `{{[[table]]}}` block acts as the container for the entire table.
|
|
43
|
+
- The first header block should be at level 2 (one level deeper than `{{[[table]]}}`).
|
|
44
|
+
- Subsequent header blocks must increase their level by one.
|
|
45
|
+
- Each row starts at level 2.
|
|
46
|
+
- The first data cell in a row is at level 3 (one level deeper than the row block).
|
|
47
|
+
- Subsequent data cells within the same row must increase their level by one.
|
|
48
|
+
|
|
49
|
+
Example of a 4x4 table structure:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
{{[[table]]}}
|
|
53
|
+
- Header 1
|
|
54
|
+
- Header 2
|
|
55
|
+
- Header 3
|
|
56
|
+
- Header 4
|
|
57
|
+
- Row 1
|
|
58
|
+
- Data 1.1
|
|
59
|
+
- Data 1.2
|
|
60
|
+
- Data 1.3
|
|
61
|
+
- Data 1.4
|
|
62
|
+
- Row 2
|
|
63
|
+
- Data 2.1
|
|
64
|
+
- Data 2.2
|
|
65
|
+
- Data 2.3
|
|
66
|
+
- Data 2.4
|
|
67
|
+
- Row 3
|
|
68
|
+
- Data 3.1
|
|
69
|
+
- Data 3.2
|
|
70
|
+
- Data 3.3
|
|
71
|
+
- Data 3.4
|
|
72
|
+
- Row 4
|
|
73
|
+
- Data 4.1
|
|
74
|
+
- Data 4.2
|
|
75
|
+
- Data 4.3
|
|
76
|
+
- Data 4.4
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Roam Mermaid
|
|
80
|
+
|
|
81
|
+
This markdown structure represents a Roam Research Mermaid diagram. It begins with a `{{[[mermaid]]}}` block, which serves as the primary container for the diagram definition. Nested underneath this block, using bullet points, is the actual Mermaid syntax. Each bullet point corresponds to a line of the Mermaid graph definition, allowing Roam to render a visual diagram based on the provided code. For example, `graph TD` specifies a top-down directed graph, and subsequent bullet points define nodes and their connections.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
- {{[[mermaid]]}}
|
|
85
|
+
- graph TD
|
|
86
|
+
- A[Start] --> B{Decision Point}
|
|
87
|
+
- B -->|Yes| C[Process A]
|
|
88
|
+
- B -->|No| D[Process B]
|
|
89
|
+
- C --> E[Merge Results]
|
|
90
|
+
- D --> E
|
|
91
|
+
- E --> F[End]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Roam Kanban Boards
|
|
95
|
+
|
|
96
|
+
The provided markdown structure represents a Roam Research Kanban board. It starts with a `{{[[kanban]]}}` block, under which nested bullet points define the Kanban cards. Each top-level bullet point directly under `{{[[kanban]]}}` serves as a card title, and any further nested bullet points under a card title act as details or sub-items for that specific card.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
- {{[[kanban]]}}
|
|
100
|
+
- card title 1
|
|
101
|
+
- bullet point 1.1
|
|
102
|
+
- bullet point 1.2
|
|
103
|
+
- card title 2
|
|
104
|
+
- bullet point 2.1
|
|
105
|
+
- bullet point 2.2
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Roam Hiccup
|
|
111
|
+
|
|
112
|
+
This markdown structure allows embedding custom HTML or other content using Hiccup syntax. The `:hiccup` keyword is followed by a Clojure-like vector defining the HTML elements and their attributes in one block. This provides a powerful way to inject dynamic or custom components into your Roam graph. Example: `:hiccup [:iframe {:width "600" :height "400" :src "https://www.example.com"}]`
|
|
113
|
+
|
|
114
|
+
## Specific notes and preferences concerning my Roam Research graph
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
⭐️📋 END (Cheat Sheet LOADED) < < < 📋⭐️
|
|
@@ -41,6 +41,7 @@ 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
|
-
const HTTP_STREAM_PORT = process.env.HTTP_STREAM_PORT || '8088'; // Default to
|
|
44
|
+
const HTTP_STREAM_PORT = process.env.HTTP_STREAM_PORT || '8088'; // Default to 8088
|
|
45
45
|
const SSE_PORT = process.env.SSE_PORT || '8087'; // Default to 8087
|
|
46
|
-
|
|
46
|
+
const CORS_ORIGIN = process.env.CORS_ORIGIN || 'http://localhost:5678';
|
|
47
|
+
export { API_TOKEN, GRAPH_NAME, HTTP_STREAM_PORT, SSE_PORT, CORS_ORIGIN };
|
package/build/markdown-utils.js
CHANGED
|
@@ -21,7 +21,7 @@ function convertTableToRoamFormat(text) {
|
|
|
21
21
|
.replace(/^\||\|$/g, '')
|
|
22
22
|
.split('|')
|
|
23
23
|
.map(cell => cell.trim()));
|
|
24
|
-
let roamTable = '{{table}}\n';
|
|
24
|
+
let roamTable = '{{[[table]]}}\n';
|
|
25
25
|
// First row becomes column headers
|
|
26
26
|
const headers = rows[0];
|
|
27
27
|
for (let i = 0; i < headers.length; i++) {
|
|
@@ -268,8 +268,8 @@ function convertNodesToBlocks(nodes) {
|
|
|
268
268
|
}));
|
|
269
269
|
}
|
|
270
270
|
function convertToRoamActions(nodes, parentUid, order = 'last') {
|
|
271
|
-
// First convert nodes to blocks with UIDs
|
|
272
|
-
const blocks = convertNodesToBlocks(
|
|
271
|
+
// First convert nodes to blocks with UIDs
|
|
272
|
+
const blocks = convertNodesToBlocks(nodes);
|
|
273
273
|
const actions = [];
|
|
274
274
|
// Helper function to recursively create actions
|
|
275
275
|
function createBlockActions(blocks, parentUid, order) {
|
|
@@ -15,9 +15,8 @@ export class TagSearchHandler extends BaseSearchHandler {
|
|
|
15
15
|
targetPageUid = await SearchUtils.findPageByTitleOrUid(this.graph, page_title_uid);
|
|
16
16
|
}
|
|
17
17
|
// Build query to find blocks referencing the page
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
:where
|
|
18
|
+
let queryArgs = [primary_tag];
|
|
19
|
+
let queryWhereClauses = `
|
|
21
20
|
[?ref-page :node/title ?title-match]
|
|
22
21
|
[(clojure.string/lower-case ?title-match) ?lower-title]
|
|
23
22
|
[(clojure.string/lower-case ?title) ?search-title]
|
|
@@ -26,9 +25,19 @@ export class TagSearchHandler extends BaseSearchHandler {
|
|
|
26
25
|
[?b :block/string ?block-str]
|
|
27
26
|
[?b :block/uid ?block-uid]
|
|
28
27
|
[?b :block/page ?p]
|
|
29
|
-
[?p :node/title ?page-title]
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
[?p :node/title ?page-title]`;
|
|
29
|
+
let inClause = `:in $ ?title`;
|
|
30
|
+
if (targetPageUid) {
|
|
31
|
+
inClause += ` ?target-page-uid`;
|
|
32
|
+
queryArgs.push(targetPageUid);
|
|
33
|
+
queryWhereClauses += `
|
|
34
|
+
[?p :block/uid ?target-page-uid]`;
|
|
35
|
+
}
|
|
36
|
+
const queryStr = `[:find ?block-uid ?block-str ?page-title
|
|
37
|
+
${inClause}
|
|
38
|
+
:where
|
|
39
|
+
${queryWhereClauses}]`;
|
|
40
|
+
const rawResults = await q(this.graph, queryStr, queryArgs);
|
|
32
41
|
// Resolve block references in content
|
|
33
42
|
const resolvedResults = await Promise.all(rawResults.map(async ([uid, content, pageTitle]) => {
|
|
34
43
|
const resolvedContent = await resolveRefs(this.graph, content);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
-
import {
|
|
5
|
-
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
6
5
|
import { initializeGraph } from '@roam-research/roam-api-sdk';
|
|
7
|
-
import { API_TOKEN, GRAPH_NAME, HTTP_STREAM_PORT
|
|
6
|
+
import { API_TOKEN, GRAPH_NAME, HTTP_STREAM_PORT } from '../config/environment.js';
|
|
8
7
|
import { toolSchemas } from '../tools/schemas.js';
|
|
9
8
|
import { ToolHandlers } from '../tools/tool-handlers.js';
|
|
10
9
|
import { readFileSync } from 'node:fs';
|
|
@@ -12,6 +11,7 @@ import { join, dirname } from 'node:path';
|
|
|
12
11
|
import { createServer } from 'node:http';
|
|
13
12
|
import { fileURLToPath } from 'node:url';
|
|
14
13
|
import { findAvailablePort } from '../utils/net.js';
|
|
14
|
+
import { CORS_ORIGIN } from '../config/environment.js';
|
|
15
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
16
|
const __dirname = dirname(__filename);
|
|
17
17
|
// Read package.json to get the version
|
|
@@ -20,6 +20,7 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
|
20
20
|
const serverVersion = packageJson.version;
|
|
21
21
|
export class RoamServer {
|
|
22
22
|
constructor() {
|
|
23
|
+
// console.log('RoamServer: Constructor started.');
|
|
23
24
|
try {
|
|
24
25
|
this.graph = initializeGraph({
|
|
25
26
|
token: API_TOKEN,
|
|
@@ -41,6 +42,7 @@ export class RoamServer {
|
|
|
41
42
|
if (Object.keys(toolSchemas).length === 0) {
|
|
42
43
|
throw new McpError(ErrorCode.InternalError, 'No tool schemas defined in src/tools/schemas.ts');
|
|
43
44
|
}
|
|
45
|
+
// console.log('RoamServer: Constructor finished.');
|
|
44
46
|
}
|
|
45
47
|
// Refactored to accept a Server instance
|
|
46
48
|
setupRequestHandlers(mcpServer) {
|
|
@@ -48,10 +50,25 @@ export class RoamServer {
|
|
|
48
50
|
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
49
51
|
tools: Object.values(toolSchemas),
|
|
50
52
|
}));
|
|
53
|
+
// List available resources
|
|
54
|
+
mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
55
|
+
const resources = []; // No resources, as cheatsheet is now a tool
|
|
56
|
+
return { resources };
|
|
57
|
+
});
|
|
58
|
+
// Access resource - no resources handled directly here anymore
|
|
59
|
+
mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
60
|
+
throw new McpError(ErrorCode.InternalError, `Resource not found: ${request.params.uri}`);
|
|
61
|
+
});
|
|
51
62
|
// Handle tool calls
|
|
52
63
|
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
53
64
|
try {
|
|
54
65
|
switch (request.params.name) {
|
|
66
|
+
case 'roam_markdown_cheatsheet': {
|
|
67
|
+
const content = await this.toolHandlers.getRoamMarkdownCheatsheet();
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: 'text', text: content }],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
55
72
|
case 'roam_remember': {
|
|
56
73
|
const { memory, categories } = request.params.arguments;
|
|
57
74
|
const result = await this.toolHandlers.remember(memory, categories);
|
|
@@ -168,6 +185,13 @@ export class RoamServer {
|
|
|
168
185
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
169
186
|
};
|
|
170
187
|
}
|
|
188
|
+
case 'roam_fetch_block_with_children': {
|
|
189
|
+
const { block_uid, depth } = request.params.arguments;
|
|
190
|
+
const result = await this.toolHandlers.fetchBlockWithChildren(block_uid, depth);
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
171
195
|
default:
|
|
172
196
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
173
197
|
}
|
|
@@ -182,41 +206,65 @@ export class RoamServer {
|
|
|
182
206
|
});
|
|
183
207
|
}
|
|
184
208
|
async run() {
|
|
209
|
+
// console.log('RoamServer: run() method started.');
|
|
185
210
|
try {
|
|
211
|
+
// console.log('RoamServer: Attempting to create stdioMcpServer...');
|
|
186
212
|
const stdioMcpServer = new Server({
|
|
187
213
|
name: 'roam-research',
|
|
188
214
|
version: serverVersion,
|
|
189
215
|
}, {
|
|
190
216
|
capabilities: {
|
|
191
217
|
tools: {
|
|
192
|
-
...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName,
|
|
218
|
+
...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName, toolSchemas[toolName].inputSchema])),
|
|
193
219
|
},
|
|
220
|
+
resources: {
|
|
221
|
+
'roam-markdown-cheatsheet.md': {}
|
|
222
|
+
}
|
|
194
223
|
},
|
|
195
224
|
});
|
|
225
|
+
// console.log('RoamServer: stdioMcpServer created. Setting up request handlers...');
|
|
196
226
|
this.setupRequestHandlers(stdioMcpServer);
|
|
227
|
+
// console.log('RoamServer: stdioMcpServer handlers setup complete. Connecting transport...');
|
|
197
228
|
const stdioTransport = new StdioServerTransport();
|
|
198
229
|
await stdioMcpServer.connect(stdioTransport);
|
|
230
|
+
// console.log('RoamServer: stdioTransport connected. Attempting to create httpMcpServer...');
|
|
199
231
|
const httpMcpServer = new Server({
|
|
200
232
|
name: 'roam-research-http', // A distinct name for the HTTP server
|
|
201
233
|
version: serverVersion,
|
|
202
234
|
}, {
|
|
203
235
|
capabilities: {
|
|
204
236
|
tools: {
|
|
205
|
-
...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName,
|
|
237
|
+
...Object.fromEntries(Object.keys(toolSchemas).map((toolName) => [toolName, toolSchemas[toolName].inputSchema])),
|
|
206
238
|
},
|
|
239
|
+
resources: {
|
|
240
|
+
'roam-markdown-cheatsheet.md': {}
|
|
241
|
+
}
|
|
207
242
|
},
|
|
208
243
|
});
|
|
244
|
+
// console.log('RoamServer: httpMcpServer created. Setting up request handlers...');
|
|
209
245
|
this.setupRequestHandlers(httpMcpServer);
|
|
246
|
+
// console.log('RoamServer: httpMcpServer handlers setup complete. Connecting transport...');
|
|
210
247
|
const httpStreamTransport = new StreamableHTTPServerTransport({
|
|
211
248
|
sessionIdGenerator: () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15),
|
|
212
249
|
});
|
|
213
250
|
await httpMcpServer.connect(httpStreamTransport);
|
|
251
|
+
// console.log('RoamServer: httpStreamTransport connected.');
|
|
214
252
|
const httpServer = createServer(async (req, res) => {
|
|
253
|
+
// Set CORS headers
|
|
254
|
+
res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
|
|
255
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
256
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
257
|
+
// Handle preflight OPTIONS requests
|
|
258
|
+
if (req.method === 'OPTIONS') {
|
|
259
|
+
res.writeHead(204); // No Content
|
|
260
|
+
res.end();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
215
263
|
try {
|
|
216
264
|
await httpStreamTransport.handleRequest(req, res);
|
|
217
265
|
}
|
|
218
266
|
catch (error) {
|
|
219
|
-
// console.error('HTTP Stream Server error:', error);
|
|
267
|
+
// // console.error('HTTP Stream Server error:', error);
|
|
220
268
|
if (!res.headersSent) {
|
|
221
269
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
222
270
|
res.end(JSON.stringify({ error: 'Internal Server Error' }));
|
|
@@ -225,70 +273,7 @@ export class RoamServer {
|
|
|
225
273
|
});
|
|
226
274
|
const availableHttpPort = await findAvailablePort(parseInt(HTTP_STREAM_PORT));
|
|
227
275
|
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}`);
|
|
276
|
+
// // console.log(`MCP Roam Research server running HTTP Stream on port ${availableHttpPort}`);
|
|
292
277
|
});
|
|
293
278
|
}
|
|
294
279
|
catch (error) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { q } from '@roam-research/roam-api-sdk';
|
|
2
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
export class BlockRetrievalOperations {
|
|
4
|
+
constructor(graph) {
|
|
5
|
+
this.graph = graph;
|
|
6
|
+
}
|
|
7
|
+
async fetchBlockWithChildren(block_uid_raw, depth = 4) {
|
|
8
|
+
if (!block_uid_raw) {
|
|
9
|
+
throw new McpError(ErrorCode.InvalidRequest, 'block_uid is required.');
|
|
10
|
+
}
|
|
11
|
+
const block_uid = block_uid_raw.replace(/^\(\((.*)\)\)$/, '$1');
|
|
12
|
+
const fetchChildren = async (parentUids, currentDepth) => {
|
|
13
|
+
if (currentDepth >= depth || parentUids.length === 0) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
const childrenQuery = `[:find ?parentUid ?childUid ?childString ?childOrder ?childHeading
|
|
17
|
+
:in $ [?parentUid ...]
|
|
18
|
+
:where [?parent :block/uid ?parentUid]
|
|
19
|
+
[?child :block/parents ?parent]
|
|
20
|
+
[?child :block/uid ?childUid]
|
|
21
|
+
[?child :block/string ?childString]
|
|
22
|
+
[?child :block/order ?childOrder]
|
|
23
|
+
[(get-else $ ?child :block/heading 0) ?childHeading]]`;
|
|
24
|
+
const childrenResults = await q(this.graph, childrenQuery, [parentUids]);
|
|
25
|
+
const childrenByParent = {};
|
|
26
|
+
const allChildUids = [];
|
|
27
|
+
for (const [parentUid, childUid, childString, childOrder, childHeading] of childrenResults) {
|
|
28
|
+
if (!childrenByParent[parentUid]) {
|
|
29
|
+
childrenByParent[parentUid] = [];
|
|
30
|
+
}
|
|
31
|
+
childrenByParent[parentUid].push({
|
|
32
|
+
uid: childUid,
|
|
33
|
+
string: childString,
|
|
34
|
+
order: childOrder,
|
|
35
|
+
heading: childHeading || undefined,
|
|
36
|
+
children: [],
|
|
37
|
+
});
|
|
38
|
+
allChildUids.push(childUid);
|
|
39
|
+
}
|
|
40
|
+
const grandChildren = await fetchChildren(allChildUids, currentDepth + 1);
|
|
41
|
+
for (const parentUid in childrenByParent) {
|
|
42
|
+
for (const child of childrenByParent[parentUid]) {
|
|
43
|
+
child.children = grandChildren[child.uid] || [];
|
|
44
|
+
}
|
|
45
|
+
childrenByParent[parentUid].sort((a, b) => a.order - b.order);
|
|
46
|
+
}
|
|
47
|
+
return childrenByParent;
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
const rootBlockQuery = `[:find ?string ?order ?heading
|
|
51
|
+
:in $ ?blockUid
|
|
52
|
+
:where [?b :block/uid ?blockUid]
|
|
53
|
+
[?b :block/string ?string]
|
|
54
|
+
[?b :block/order ?order]
|
|
55
|
+
[(get-else $ ?b :block/heading 0) ?heading]]`;
|
|
56
|
+
const rootBlockResult = await q(this.graph, rootBlockQuery, [block_uid]);
|
|
57
|
+
if (!rootBlockResult) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const [rootString, rootOrder, rootHeading] = rootBlockResult;
|
|
61
|
+
const childrenMap = await fetchChildren([block_uid], 0);
|
|
62
|
+
return {
|
|
63
|
+
uid: block_uid,
|
|
64
|
+
string: rootString,
|
|
65
|
+
order: rootOrder,
|
|
66
|
+
heading: rootHeading || undefined,
|
|
67
|
+
children: childrenMap[block_uid] || [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new McpError(ErrorCode.InternalError, `Failed to fetch block with children: ${error instanceof Error ? error.message : String(error)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -96,14 +96,6 @@ export class BlockOperations {
|
|
|
96
96
|
// No heading parameter, use original parsing logic
|
|
97
97
|
const convertedContent = convertToRoamMarkdown(content);
|
|
98
98
|
nodes = parseMarkdown(convertedContent);
|
|
99
|
-
// If we have simple newline-separated content (no markdown formatting),
|
|
100
|
-
// and parseMarkdown created nodes at level 0, reverse them to maintain original order
|
|
101
|
-
if (nodes.every(node => node.level === 0 && !node.heading_level)) {
|
|
102
|
-
const lines = content.split('\n').filter(line => line.trim());
|
|
103
|
-
if (lines.length === nodes.length) {
|
|
104
|
-
nodes.reverse();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
99
|
}
|
|
108
100
|
const actions = convertToRoamActions(nodes, targetPageUid, 'last');
|
|
109
101
|
// Execute batch actions to create the nested structure
|
|
@@ -218,6 +218,9 @@ export class OutlineOperations {
|
|
|
218
218
|
let result;
|
|
219
219
|
try {
|
|
220
220
|
// Validate level sequence
|
|
221
|
+
if (validOutline.length > 0 && validOutline[0].level !== 1) {
|
|
222
|
+
throw new McpError(ErrorCode.InvalidRequest, 'Invalid outline structure - the first item must be at level 1');
|
|
223
|
+
}
|
|
221
224
|
let prevLevel = 0;
|
|
222
225
|
for (const item of validOutline) {
|
|
223
226
|
// Level should not increase by more than 1 at a time
|
|
@@ -2,7 +2,7 @@ import { q, createPage as createRoamPage, batchActions } from '@roam-research/ro
|
|
|
2
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { capitalizeWords } from '../helpers/text.js';
|
|
4
4
|
import { resolveRefs } from '../helpers/refs.js';
|
|
5
|
-
import { convertToRoamActions } from '../../markdown-utils.js';
|
|
5
|
+
import { convertToRoamActions, convertToRoamMarkdown } from '../../markdown-utils.js';
|
|
6
6
|
export class PageOperations {
|
|
7
7
|
constructor(graph) {
|
|
8
8
|
this.graph = graph;
|
|
@@ -84,7 +84,7 @@ export class PageOperations {
|
|
|
84
84
|
try {
|
|
85
85
|
// Convert content array to MarkdownNode format expected by convertToRoamActions
|
|
86
86
|
const nodes = content.map(block => ({
|
|
87
|
-
content: block.text,
|
|
87
|
+
content: convertToRoamMarkdown(block.text.replace(/^#+\s*/, '')),
|
|
88
88
|
level: block.level,
|
|
89
89
|
...(block.heading && { heading_level: block.heading }),
|
|
90
90
|
children: []
|
package/build/tools/schemas.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export const toolSchemas = {
|
|
3
3
|
roam_add_todo: {
|
|
4
4
|
name: 'roam_add_todo',
|
|
5
|
-
description: 'Add a list of todo items as individual blocks to today\'s daily page in Roam. Each item becomes its own actionable block with todo status.\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).',
|
|
5
|
+
description: 'Add a list of todo items as individual blocks to today\'s daily page in Roam. Each item becomes its own actionable block with todo status.\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).\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
6
6
|
inputSchema: {
|
|
7
7
|
type: 'object',
|
|
8
8
|
properties: {
|
|
@@ -20,7 +20,7 @@ export const toolSchemas = {
|
|
|
20
20
|
},
|
|
21
21
|
roam_fetch_page_by_title: {
|
|
22
22
|
name: 'roam_fetch_page_by_title',
|
|
23
|
-
description: 'Fetch page by title
|
|
23
|
+
description: 'Fetch page by title. Returns content in the specified format.',
|
|
24
24
|
inputSchema: {
|
|
25
25
|
type: 'object',
|
|
26
26
|
properties: {
|
|
@@ -40,7 +40,7 @@ export const toolSchemas = {
|
|
|
40
40
|
},
|
|
41
41
|
roam_create_page: {
|
|
42
42
|
name: 'roam_create_page',
|
|
43
|
-
description: 'Create new standalone page in Roam with optional content using explicit nesting levels and headings (H1-H3). Best for:\n- Creating foundational concept pages that other pages will link to/from\n- Establishing new topic areas that need their own namespace\n- Setting up reference materials or documentation\n- Making permanent collections of information.',
|
|
43
|
+
description: 'Create new standalone page in Roam with optional content using explicit nesting levels and headings (H1-H3). Best for:\n- Creating foundational concept pages that other pages will link to/from\n- Establishing new topic areas that need their own namespace\n- Setting up reference materials or documentation\n- Making permanent collections of information.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
44
44
|
inputSchema: {
|
|
45
45
|
type: 'object',
|
|
46
46
|
properties: {
|
|
@@ -50,7 +50,7 @@ export const toolSchemas = {
|
|
|
50
50
|
},
|
|
51
51
|
content: {
|
|
52
52
|
type: 'array',
|
|
53
|
-
description: 'Initial content for the page as an array of blocks with explicit nesting levels',
|
|
53
|
+
description: 'Initial content for the page as an array of blocks with explicit nesting levels. Note: While empty blocks (e.g., {"text": "", "level": 1}) can be used for visual spacing, they create empty entities in the database. Please use them sparingly and only for structural purposes, not for simple visual separation.',
|
|
54
54
|
items: {
|
|
55
55
|
type: 'object',
|
|
56
56
|
properties: {
|
|
@@ -80,7 +80,7 @@ export const toolSchemas = {
|
|
|
80
80
|
},
|
|
81
81
|
roam_create_outline: {
|
|
82
82
|
name: 'roam_create_outline',
|
|
83
|
-
description: 'Add a structured outline to an existing page or block (by title text or uid), with customizable nesting levels. Best for:\n- Adding supplementary structured content to existing pages\n- Creating temporary or working outlines (meeting notes, brainstorms)\n- Organizing thoughts or research under a specific topic\n- Breaking down subtopics or components of a larger concept',
|
|
83
|
+
description: 'Add a structured outline to an existing page or block (by title text or uid), with customizable nesting levels. The `outline` parameter defines *new* blocks to be created. To nest content under an *existing* block, provide its UID or exact text in `block_text_uid`, and ensure the `outline` array contains only the child blocks with levels relative to that parent. Including the parent block\'s text in the `outline` array will create a duplicate block. Best for:\n- Adding supplementary structured content to existing pages\n- Creating temporary or working outlines (meeting notes, brainstorms)\n- Organizing thoughts or research under a specific topic\n- Breaking down subtopics or components of a larger concept\nBest for simpler, contiguous hierarchical content. For complex nesting (e.g., tables) or granular control over block placement, consider `roam_process_batch_actions` instead.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
84
84
|
inputSchema: {
|
|
85
85
|
type: 'object',
|
|
86
86
|
properties: {
|
|
@@ -90,11 +90,11 @@ export const toolSchemas = {
|
|
|
90
90
|
},
|
|
91
91
|
block_text_uid: {
|
|
92
92
|
type: 'string',
|
|
93
|
-
description: 'The text content or UID of the block to nest the outline under (UID is preferred for accuracy). If blank, content is nested directly under the page.'
|
|
93
|
+
description: 'The text content or UID of the block to nest the outline under (UID is preferred for accuracy). If blank, content is nested directly under the page (or the default daily page if page_title_uid is also blank).'
|
|
94
94
|
},
|
|
95
95
|
outline: {
|
|
96
96
|
type: 'array',
|
|
97
|
-
description: 'Array of outline items with block text and explicit nesting level',
|
|
97
|
+
description: 'Array of outline items with block text and explicit nesting level. Must be a valid hierarchy: the first item must be level 1, and subsequent levels cannot increase by more than 1 at a time (e.g., a level 3 cannot follow a level 1).',
|
|
98
98
|
items: {
|
|
99
99
|
type: 'object',
|
|
100
100
|
properties: {
|
|
@@ -104,7 +104,7 @@ export const toolSchemas = {
|
|
|
104
104
|
},
|
|
105
105
|
level: {
|
|
106
106
|
type: 'integer',
|
|
107
|
-
description: 'Indentation level (1-10, where 1 is top level)',
|
|
107
|
+
description: 'Indentation level (1-10, where 1 is top level). Levels must be sequential and cannot be skipped (e.g., a level 3 item cannot directly follow a level 1 item).',
|
|
108
108
|
minimum: 1,
|
|
109
109
|
maximum: 10
|
|
110
110
|
},
|
|
@@ -129,7 +129,7 @@ export const toolSchemas = {
|
|
|
129
129
|
},
|
|
130
130
|
roam_import_markdown: {
|
|
131
131
|
name: 'roam_import_markdown',
|
|
132
|
-
description: 'Import nested markdown content into Roam under a specific block. Can locate the parent block by UID (preferred) or by exact string match within a specific page.',
|
|
132
|
+
description: 'Import nested markdown content into Roam under a specific block. Can locate the parent block by UID (preferred) or by exact string match within a specific page.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
133
133
|
inputSchema: {
|
|
134
134
|
type: 'object',
|
|
135
135
|
properties: {
|
|
@@ -155,7 +155,7 @@ export const toolSchemas = {
|
|
|
155
155
|
},
|
|
156
156
|
order: {
|
|
157
157
|
type: 'string',
|
|
158
|
-
description: 'Optional: Where to add the content
|
|
158
|
+
description: 'Optional: Where to add the content undeIs this tr the parent ("first" or "last")',
|
|
159
159
|
enum: ['first', 'last'],
|
|
160
160
|
default: 'first'
|
|
161
161
|
}
|
|
@@ -322,9 +322,18 @@ export const toolSchemas = {
|
|
|
322
322
|
required: ['start_date', 'type', 'scope']
|
|
323
323
|
}
|
|
324
324
|
},
|
|
325
|
+
roam_markdown_cheatsheet: {
|
|
326
|
+
name: 'roam_markdown_cheatsheet',
|
|
327
|
+
description: 'Provides the content of the Roam Markdown Cheatsheet resource, optionally concatenated with custom instructions if CUSTOM_INSTRUCTIONS_PATH is set.',
|
|
328
|
+
inputSchema: {
|
|
329
|
+
type: 'object',
|
|
330
|
+
properties: {},
|
|
331
|
+
required: [],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
325
334
|
roam_remember: {
|
|
326
335
|
name: 'roam_remember',
|
|
327
|
-
description: 'Add a memory or piece of information to remember, stored on the daily page with MEMORIES_TAG tag and optional categories. \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).',
|
|
336
|
+
description: 'Add a memory or piece of information to remember, stored on the daily page with MEMORIES_TAG tag and optional categories. \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).\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
328
337
|
inputSchema: {
|
|
329
338
|
type: 'object',
|
|
330
339
|
properties: {
|
|
@@ -364,7 +373,7 @@ export const toolSchemas = {
|
|
|
364
373
|
},
|
|
365
374
|
roam_datomic_query: {
|
|
366
375
|
name: 'roam_datomic_query',
|
|
367
|
-
description: 'Execute a custom Datomic query on the Roam graph beyond the available search tools. This provides direct access to Roam\'s query engine
|
|
376
|
+
description: 'Execute a custom Datomic query on the Roam graph for advanced data retrieval beyond the available search tools. This provides direct access to Roam\'s query engine. Note: Roam graph is case-sensitive.\nList of some of Roam\'s data model Namespaces and Attributes: ancestor (descendants), attrs (lookup), block (children, heading, open, order, page, parents, props, refs, string, text-align, uid), children (view-type), create (email, time), descendant (ancestors), edit (email, seen-by, time), entity (attrs), log (id), node (title), page (uid, title), refs (text).\nPredicates (clojure.string/includes?, clojure.string/starts-with?, clojure.string/ends-with?, <, >, <=, >=, =, not=, !=).\nAggregates (distinct, count, sum, max, min, avg, limit).\nTips: Use :block/parents for all ancestor levels, :block/children for direct descendants only; combine clojure.string for complex matching, use distinct to deduplicate, leverage Pull patterns for hierarchies, handle case-sensitivity carefully, and chain ancestry rules for multi-level queries.',
|
|
368
377
|
inputSchema: {
|
|
369
378
|
type: 'object',
|
|
370
379
|
properties: {
|
|
@@ -385,7 +394,7 @@ export const toolSchemas = {
|
|
|
385
394
|
},
|
|
386
395
|
roam_process_batch_actions: {
|
|
387
396
|
name: 'roam_process_batch_actions',
|
|
388
|
-
description: 'Executes a sequence of low-level block actions (create, update, move, delete) in a single, non-transactional batch. Actions are executed in the provided order. For creating nested blocks, you can use a temporary client-side UID in a parent block and refer to it in a child block within the same batch. For actions on existing blocks, a valid block UID is required.',
|
|
397
|
+
description: 'Executes a sequence of low-level block actions (create, update, move, delete) in a single, non-transactional batch. Actions are executed in the provided order. For creating nested blocks, you can use a temporary client-side UID in a parent block and refer to it in a child block within the same batch. For actions on existing blocks, a valid block UID is required. Note: Roam-flavored markdown, including block embedding with `((UID))` syntax, is supported within the `string` property for `create-block` and `update-block` actions. For actions on existing blocks or within a specific page context, it is often necessary to first obtain valid page or block UIDs. Tools like `roam_fetch_page_by_title` or other search tools can be used to retrieve these UIDs before executing batch actions. For simpler, sequential outlines, `roam_create_outline` is often more suitable.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.',
|
|
389
398
|
inputSchema: {
|
|
390
399
|
type: 'object',
|
|
391
400
|
properties: {
|
|
@@ -436,8 +445,11 @@ export const toolSchemas = {
|
|
|
436
445
|
description: 'The UID of the parent block or page.'
|
|
437
446
|
},
|
|
438
447
|
"order": {
|
|
439
|
-
|
|
440
|
-
|
|
448
|
+
oneOf: [
|
|
449
|
+
{ type: 'integer', description: 'Zero-indexed position.' },
|
|
450
|
+
{ type: 'string', enum: ['first', 'last'], description: 'Position keyword.' }
|
|
451
|
+
],
|
|
452
|
+
description: 'The position of the block under its parent (e.g., 0, 1, 2) or a keyword ("first", "last").'
|
|
441
453
|
}
|
|
442
454
|
}
|
|
443
455
|
}
|
|
@@ -448,5 +460,25 @@ export const toolSchemas = {
|
|
|
448
460
|
},
|
|
449
461
|
required: ['actions']
|
|
450
462
|
}
|
|
451
|
-
}
|
|
463
|
+
},
|
|
464
|
+
roam_fetch_block_with_children: {
|
|
465
|
+
name: 'roam_fetch_block_with_children',
|
|
466
|
+
description: 'Fetch a block by its UID along with its hierarchical children down to a specified depth.',
|
|
467
|
+
inputSchema: {
|
|
468
|
+
type: 'object',
|
|
469
|
+
properties: {
|
|
470
|
+
block_uid: {
|
|
471
|
+
type: 'string',
|
|
472
|
+
description: 'The UID of the block to fetch.'
|
|
473
|
+
},
|
|
474
|
+
depth: {
|
|
475
|
+
type: 'integer',
|
|
476
|
+
description: 'Optional: The number of levels deep to fetch children. Defaults to 4.',
|
|
477
|
+
minimum: 0,
|
|
478
|
+
maximum: 10
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
required: ['block_uid']
|
|
482
|
+
},
|
|
483
|
+
},
|
|
452
484
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
1
4
|
import { PageOperations } from './operations/pages.js';
|
|
2
5
|
import { BlockOperations } from './operations/blocks.js';
|
|
6
|
+
import { BlockRetrievalOperations } from './operations/block-retrieval.js'; // New import
|
|
3
7
|
import { SearchOperations } from './operations/search/index.js';
|
|
4
8
|
import { MemoryOperations } from './operations/memory.js';
|
|
5
9
|
import { TodoOperations } from './operations/todos.js';
|
|
@@ -11,6 +15,7 @@ export class ToolHandlers {
|
|
|
11
15
|
this.graph = graph;
|
|
12
16
|
this.pageOps = new PageOperations(graph);
|
|
13
17
|
this.blockOps = new BlockOperations(graph);
|
|
18
|
+
this.blockRetrievalOps = new BlockRetrievalOperations(graph); // Initialize new instance
|
|
14
19
|
this.searchOps = new SearchOperations(graph);
|
|
15
20
|
this.memoryOps = new MemoryOperations(graph);
|
|
16
21
|
this.todoOps = new TodoOperations(graph);
|
|
@@ -28,6 +33,9 @@ export class ToolHandlers {
|
|
|
28
33
|
return this.pageOps.fetchPageByTitle(title, format);
|
|
29
34
|
}
|
|
30
35
|
// Block Operations
|
|
36
|
+
async fetchBlockWithChildren(block_uid, depth) {
|
|
37
|
+
return this.blockRetrievalOps.fetchBlockWithChildren(block_uid, depth);
|
|
38
|
+
}
|
|
31
39
|
// Search Operations
|
|
32
40
|
async searchByStatus(status, page_title_uid, include, exclude) {
|
|
33
41
|
return this.searchOps.searchByStatus(status, page_title_uid, include, exclude);
|
|
@@ -74,4 +82,21 @@ export class ToolHandlers {
|
|
|
74
82
|
async processBatch(actions) {
|
|
75
83
|
return this.batchOps.processBatch(actions);
|
|
76
84
|
}
|
|
85
|
+
async getRoamMarkdownCheatsheet() {
|
|
86
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
87
|
+
const __dirname = path.dirname(__filename);
|
|
88
|
+
const cheatsheetPath = path.join(__dirname, '../../Roam_Markdown_Cheatsheet.md');
|
|
89
|
+
let cheatsheetContent = fs.readFileSync(cheatsheetPath, 'utf-8');
|
|
90
|
+
const customInstructionsPath = process.env.CUSTOM_INSTRUCTIONS_PATH;
|
|
91
|
+
if (customInstructionsPath && fs.existsSync(customInstructionsPath)) {
|
|
92
|
+
try {
|
|
93
|
+
const customInstructionsContent = fs.readFileSync(customInstructionsPath, 'utf-8');
|
|
94
|
+
cheatsheetContent += `\n\n${customInstructionsContent}`;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.warn(`Could not read custom instructions file at ${customInstructionsPath}: ${error}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return cheatsheetContent;
|
|
101
|
+
}
|
|
77
102
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roam-research-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "A Model Context Protocol (MCP) server for Roam Research API integration",
|
|
5
5
|
"private": false,
|
|
6
6
|
"repository": {
|
|
@@ -28,13 +28,18 @@
|
|
|
28
28
|
"build"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "tsc && chmod 755 build/index.js",
|
|
31
|
+
"build": "tsc && cp Roam_Markdown_Cheatsheet.md build/Roam_Markdown_Cheatsheet.md && chmod 755 build/index.js",
|
|
32
|
+
"clean": "rm -rf build",
|
|
32
33
|
"watch": "tsc --watch",
|
|
33
34
|
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
|
|
34
|
-
"start": "node build/index.js"
|
|
35
|
+
"start": "node build/index.js",
|
|
36
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
37
|
+
"release:patch": "npm version patch && git push origin v$(node -p \"require('./package.json').version\")",
|
|
38
|
+
"release:minor": "npm version minor && git push origin v$(node -p \"require('./package.json').version\")",
|
|
39
|
+
"release:major": "npm version major && git push origin v$(node -p \"require('./package.json').version\")"
|
|
35
40
|
},
|
|
36
41
|
"dependencies": {
|
|
37
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.13.2",
|
|
38
43
|
"@roam-research/roam-api-sdk": "^0.10.0",
|
|
39
44
|
"dotenv": "^16.4.7"
|
|
40
45
|
},
|