This is page 2 of 2. Use http://codebase.md/2b3pro/roam-research-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .dockerignore ├── .gitignore ├── .roam │ └── custom-instructions.md ├── CHANGELOG.md ├── docker-compose.yml ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── Roam Import JSON Schema.md ├── Roam_Markdown_Cheatsheet.md ├── Roam_Research_Datalog_Cheatsheet.md ├── roam-research-mcp-image.jpeg ├── src │ ├── config │ │ └── environment.ts │ ├── index.ts │ ├── markdown-utils.ts │ ├── search │ │ ├── block-ref-search.ts │ │ ├── datomic-search.ts │ │ ├── hierarchy-search.ts │ │ ├── index.ts │ │ ├── status-search.ts │ │ ├── tag-search.ts │ │ ├── text-search.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── server │ │ └── roam-server.ts │ ├── tools │ │ ├── helpers │ │ │ ├── refs.ts │ │ │ └── text.ts │ │ ├── operations │ │ │ ├── batch.ts │ │ │ ├── block-retrieval.ts │ │ │ ├── blocks.ts │ │ │ ├── memory.ts │ │ │ ├── outline.ts │ │ │ ├── pages.ts │ │ │ ├── search │ │ │ │ ├── handlers.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ └── todos.ts │ │ ├── schemas.ts │ │ ├── tool-handlers.ts │ │ └── types │ │ └── index.ts │ ├── types │ │ └── roam.ts │ ├── types.d.ts │ └── utils │ ├── helpers.ts │ └── net.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # Changelog 2 | 3 | v0.36.3 - 2025-08-30 4 | 5 | - FEATURE: Implemented `prompts/list` method for MCP server, returning an empty array of prompts. 6 | - FIXED: Removed `roam-markdown-cheatsheet.md` from advertised resources in MCP server capabilities to align with its tool-only access. 7 | 8 | v0.36.2 - 2025-08-28 9 | 10 | - ENHANCED: `roam_datomic_query` tool 11 | - Added `regexFilter`, `regexFlags`, and `regexTargetField` parameters for client-side regex filtering of results. 12 | - Updated description to reflect enhanced filtering capabilities. 13 | 14 | v0.36.1 - 2025-08-28 15 | 16 | - ENHANCED: `roam_find_pages_modified_today` tool 17 | - Added `limit`, `offset`, and `sort_order` parameters for pagination and sorting. 18 | 19 | v1.36.0 - 2025-08-28 20 | 21 | - ENHANCED: `roam_search_for_tag` and `roam_search_by_text` tools 22 | - Added `offset` parameter for pagination support. 23 | - ENHANCED: `roam_search_for_tag` tool 24 | - Implemented `near_tag` and `exclude_tag` parameters for more precise tag-based filtering. 25 | - ENHANCED: `roam_datomic_query` tool 26 | - Updated description to clarify optimal use cases (Regex, Complex Boolean Logic, Arbitrary Sorting, Proximity Search). 27 | 28 | v.0.35.1 - 2025-08-23 9:33 29 | 30 | - ENHANCED: `roam_create_page` and `roam_create_outline` tool descriptions in `src/tools/schemas.ts` for improved clarity and to guide users toward the most efficient workflow. 31 | 32 | v.0.35.0 - 2025-08-23 33 | 34 | - ENHANCED: `roam_import_markdown` tool 35 | - Now returns a nested object structure for `created_uids`, reflecting the hierarchy of the imported content, including `uid`, `text`, `order`, and `children`. 36 | - If a `parent_string` is provided and the block does not exist, it will be created automatically. 37 | - FIXED: Block ordering issue in `roam_import_markdown` and `roam_create_outline`. Nested outlines are now created in the correct order. 38 | - FIXED: Duplication issue in the response of `roam_fetch_block_with_children`. 39 | 40 | v.0.32.4 41 | 42 | - FIXED: Memory allocation issue (`FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory`) 43 | - Removed `console.log` statements from `src/tools/operations/outline.ts` to adhere to MCP server stdio communication rules. 44 | - Optimized `parseMarkdown` function in `src/markdown-utils.ts` to avoid inefficient `lines.splice()` operations when handling mid-line code blocks, improving memory usage and performance. 45 | - ENHANCED: `roam_create_outline` tool 46 | - Successfully created outlines with nested code blocks, confirming the fix for memory allocation issues. 47 | 48 | v.0.32.1 49 | 50 | - ENHANCED: `roam_create_outline` tool 51 | - The tool now returns a nested structure of UIDs (`NestedBlock[]`) for all created blocks, including children, accurately reflecting the outline hierarchy. 52 | - Implemented a recursive fetching mechanism (`fetchBlockWithChildren` helper) to retrieve all nested block UIDs and their content after creation. 53 | - Fixed an issue where the `created_uids` array was only returning top-level block UIDs. 54 | - Corrected the Datomic query used for fetching children to ensure only direct children are retrieved, resolving previous duplication and incorrect nesting issues. 55 | - Removed `console.log` and `console.warn` statements from `src/tools/operations/outline.ts` to adhere to MCP server stdio communication rules. 56 | - ADDED: `NestedBlock` interface in `src/tools/types/index.ts` to represent the hierarchical structure of created blocks. 57 | 58 | v.0.32.3 59 | 60 | - ENHANCED: `roam_create_page` tool 61 | - Now creates a block on the daily page linking to the newly created page, formatted as `Create [[Page Title]]`. 62 | 63 | v.0.32.2 64 | 65 | - FIXED: `roam_create_outline` now correctly respects the order of top-level blocks. 66 | - Changed the default insertion order for batch actions from 'first' to 'last' in `src/tools/operations/outline.ts` to ensure blocks are added in the intended sequence. 67 | 68 | v.0.30.10 69 | 70 | - ENHANCED: `roam_markdown_cheatsheet` tool 71 | - The tool now reads the `Roam_Markdown_Cheatsheet.md` and concatenates it with custom instructions from the path specified by the `CUSTOM_INSTRUCTIONS_PATH` environment variable, if the file exists. If the custom instructions file is not found, only the cheatsheet content is returned. 72 | - UPDATED: The description of `roam_markdown_cheatsheet` in `src/tools/schemas.ts` to reflect the new functionality. 73 | 74 | v.0.30.9 75 | 76 | - FIXED: `roam_fetch_block_with_children` tool to use a more efficient batched recursive approach, avoiding "Too many requests" and other API errors. 77 | - The tool now fetches all children of a block in a single query per level of depth, significantly reducing the number of API calls. 78 | 79 | v.0.30.8 80 | 81 | - ADDED: `roam_fetch_block_with_children` tool 82 | - Fetches a block by its UID along with its hierarchical children down to a specified depth. 83 | - Automatically handles Roam's `((UID))` formatting, extracting the raw UID for lookup. 84 | - This tool provides a direct and structured way to retrieve specific block content and its nested hierarchy. 85 | 86 | v.0.30.7 87 | 88 | - FIXED: `roam_create_outline` now prevents errors from invalid outline structures by enforcing that outlines must start at level 1 and that subsequent levels cannot increase by more than 1 at a time. 89 | - Updated the tool's schema in `src/tools/schemas.ts` with more explicit instructions to guide the LLM in generating valid hierarchical structures. 90 | - Added stricter validation in `src/tools/operations/outline.ts` to reject outlines that do not start at level 1, providing a clearer error message. 91 | - Optimized page creation 92 | 93 | v.0.30.6 94 | 95 | - FIXED: `roam_create_page` now correctly strips heading markers (`#`) from block content before creation. 96 | - FIXED: Block creation order is now correct. Removed the incorrect `.reverse()` call in `convertToRoamActions` and the corresponding workaround in `createBlock`. 97 | - UPDATED: the cheat sheet for ordinal dates. 98 | 99 | v.0.30.5 100 | 101 | - FIXED: `roam_search_for_tag` now correctly scopes searches to a specific page when `page_title_uid` is provided. 102 | - The Datalog query in `src/search/tag-search.ts` was updated to include the `targetPageUid` in the `where` clause. 103 | 104 | v.0.30.4 105 | 106 | - FIXED: Tools not loading properly in Gemini CLI 107 | - Clarified outline description 108 | - FIXED: `roam_process_batch_actions` `heading` enum type in `schemas.ts` for Gemini CLI compatibility. 109 | 110 | v.0.30.3 111 | 112 | - ADDED: `roam_markdown_cheatsheet` tool 113 | - Provides the content of the Roam Markdown Cheatsheet directly via a tool call. 114 | - The content is now read dynamically from `Roam_Markdown_Cheatsheet.md` on the filesystem. 115 | - **Reason for Tool Creation:** While Cline can access local resources provided by an MCP server, other AI models (suchs as Claude AI) may not have this capability. By exposing the cheatsheet as a tool, it ensures broader accessibility and utility for all connected AI models, allowing them to programmatically request and receive the cheatsheet content when needed. 116 | - REMOVED: Roam Markdown Cheatsheet as a direct resource 117 | - The cheatsheet is no longer exposed as a static resource; it is now accessed programmatically through the new `roam_markdown_cheatsheet` tool. 118 | - ADDED: package.json new utilty scripts 119 | 120 | v.0.30.2 121 | 122 | - ADDED: 4x4 table creation example 123 | - Created a 4x4 table with random data on the "Testing Tables" page, demonstrating proper Roam table structure. 124 | - ENHANCED: `Roam_Markdown_Cheatsheet.md` 125 | - Updated the "Roam Tables" section with a more detailed explanation of table structure, including proper indentation levels for headers and data cells. 126 | - ENHANCED: `src/tools/schemas.ts` 127 | - Clarified the distinction between `roam_create_outline` and `roam_process_batch_actions` in their respective descriptions, providing guidance on their best use cases. 128 | 129 | v.0.30.1 130 | 131 | - ENHANCED: `roam_process_batch_actions` tool description 132 | - Clarified that Roam-flavored markdown, including block embedding with `((UID))` syntax, is supported within the `string` property for `create-block` and `update-block` actions. 133 | - Added a note advising users to obtain valid page or block UIDs using `roam_fetch_page_by_title` or other search tools for actions on existing blocks or within a specific page context. 134 | - Clarified the `block_text_uid` description for `roam_create_outline` to explicitly mention defaulting to the daily page. 135 | - Simplified the top-level description for `roam_datomic_query`. 136 | - Refined the introductory sentence for `roam_datomic_query`. 137 | - ADDED: "Example Prompts" section in `README.md` 138 | - Provided 2-3 examples demonstrating how to prompt an LLM to use the Roam tool, specifically leveraging `roam_process_batch_actions` for creative use cases. 139 | 140 | v.0.30.0 141 | 142 | - DEPRECATED: **Generic Block Manipulation Tools**: 143 | - `roam_create_block`: Deprecated in favor of `roam_process_batch_actions` (action: `create-block`). 144 | - `roam_update_block`: Deprecated in favor of `roam_process_batch_actions` (action: `update-block`). 145 | - `roam_update_multiple_blocks`: Deprecated in favor of `roam_process_batch_actions` for batch updates. 146 | Users are encouraged to use `roam_process_batch_actions` for all direct, generic block manipulations due to its enhanced flexibility and batch processing capabilities. 147 | - REFACTORED: `roam_add_todo` to internally use `roam_process_batch_actions` for all block creations, enhancing efficiency and consistency. 148 | - REFACTORED: `roam_remember` to internally use `roam_process_batch_actions` for all block creations, enhancing efficiency and consistency. 149 | - ENHANCED: `roam_create_outline` 150 | - Refactored to internally use `roam_process_batch_actions` for all block creations, including parent blocks. 151 | - Added support for `children_view_type` in outline items, allowing users to specify the display format (bullet, document, numbered) for nested blocks. 152 | - REFACTORED: `roam_import_markdown` to internally use `roam_process_batch_actions` for all content imports, enhancing efficiency and consistency. 153 | 154 | v.0.29.0 155 | 156 | - ADDED: **Batch Processing Tool**: Introduced `roam_process_batch_actions`, a powerful new tool for executing a sequence of low-level block actions (create, update, move, delete) in a single API call. This enables complex, multi-step workflows, programmatic content reorganization, and high-performance data imports. 157 | - ENHANCED: **Schema Clarity**: Updated the descriptions for multiple tool parameters in `src/tools/schemas.ts` to explicitly state that using a block or page UID is preferred over text-based identifiers for improved accuracy and reliability. 158 | - NOTE: **Heading Removal Limitation**: Discovered that directly removing heading formatting (e.g., setting `heading` to `0` or `null`) via `update-block` action in `roam_process_batch_actions` is not supported by the Roam API. The `heading` attribute persists its value. 159 | 160 | v.0.28.0 161 | 162 | - ADDED: **Configurable HTTP and SSE Ports**: The HTTP and SSE server ports can now be configured via environment variables (`HTTP_STREAM_PORT` and `SSE_PORT`). 163 | - ADDED: **Automatic Port Conflict Resolution**: The server now automatically checks if the desired ports are in use and finds the next available ports, preventing startup errors due to port conflicts. 164 | 165 | v.0.27.0 166 | 167 | - ADDED: SSE (Server-Sent Events) transport support for legacy clients. 168 | - REFACTORED: `src/server/roam-server.ts` to use separate MCP `Server` instances for each transport (Stdio, HTTP Stream, and SSE) to ensure they can run concurrently without conflicts. 169 | - ENHANCED: Each transport now runs on its own isolated `Server` instance, improving stability and preventing cross-transport interference. 170 | - UPDATED: `src/config/environment.ts` to include `SSE_PORT` for configurable SSE endpoint (defaults to `8087`). 171 | 172 | v.0.26.0 173 | 174 | - ENHANCED: Added HTTP Stream Transport support 175 | - Implemented dual transport support for Stdio and HTTP Stream, allowing communication via both local processes and network connections. 176 | - Updated `src/config/environment.ts` to include `HTTP_STREAM_PORT` for configurable HTTP Stream endpoint. 177 | - Modified `src/server/roam-server.ts` to initialize and connect `StreamableHTTPServerTransport` alongside `StdioServerTransport`. 178 | - Configured HTTP server to listen on `HTTP_STREAM_PORT` and handle requests via `StreamableHTTPServerTransport`. 179 | 180 | v.0.25.7 181 | 182 | - FIXED: `roam_fetch_page_by_title` schema definition 183 | - Corrected missing `name` property and proper nesting of `inputSchema` in `src/tools/schemas.ts`. 184 | - ENHANCED: Dynamic tool loading and error reporting 185 | - Implemented dynamic loading of tool capabilities from `toolSchemas` in `src/server/roam-server.ts` to ensure consistency. 186 | - Added robust error handling during server initialization (graph, tool handlers) and connection attempts in `src/server/roam-server.ts` to provide more specific feedback on startup issues. 187 | - CENTRALIZED: Versioning in `src/server/roam-server.ts` 188 | - Modified `src/server/roam-server.ts` to dynamically read the version from `package.json`, ensuring a single source of truth for the project version. 189 | 190 | v.0.25.6 191 | 192 | - ADDED: Docker support 193 | - Created a `Dockerfile` for containerization. 194 | - Added an `npm start` script to `package.json` for running the application within the Docker container. 195 | 196 | v.0.25.5 197 | 198 | - ENHANCED: `roam_create_outline` tool for better heading and nesting support 199 | - Reverted previous change in `src/tools/operations/outline.ts` to preserve original indentation for outline items. 200 | - Refined `parseMarkdown` in `src/markdown-utils.ts` to correctly parse markdown heading syntax (`#`, `##`, `###`) while maintaining the block's hierarchical level based on indentation. 201 | - Updated `block_text_uid` description in `roam_create_outline` schema (`src/tools/schemas.ts`) to clarify its use for specifying a parent block by text or UID. 202 | - Clarified that `roam_create_block` creates blocks directly on a page and does not support nesting under existing blocks. `roam_create_outline` should be used for this purpose. 203 | 204 | v.0.25.4 205 | 206 | - ADDED: `format` parameter to `roam_fetch_page_by_title` tool 207 | - Allows fetching page content as raw JSON data (blocks with UIDs) or markdown. 208 | - Updated `fetchPageByTitle` in `src/tools/operations/pages.ts` to return stringified JSON for raw format. 209 | - Updated `roam_fetch_page_by_title` schema in `src/tools/schemas.ts` to include `format` parameter with 'raw' as default. 210 | - Updated `fetchPageByTitle` handler in `src/tools/tool-handlers.ts` to pass `format` parameter. 211 | - Updated `roam_fetch_page_by_title` case in `src/server/roam-server.ts` to extract and pass `format` parameter. 212 | 213 | v.0.25.3 214 | 215 | - FIXED: roam_create_block multiline content ordering issue 216 | - Root cause: Simple newline-separated content was being created in reverse order 217 | - Solution: Added logic to detect simple newline-separated content and reverse the nodes array to maintain original order 218 | - Fix is specific to simple multiline content without markdown formatting, preserving existing behavior for complex markdown 219 | 220 | v.0.25.2 221 | 222 | - FIXED: roam_create_block heading formatting issue 223 | - Root cause: Missing heading parameter extraction in server request handler 224 | - Solution: Added heading parameter to roam_create_block handler in roam-server.ts 225 | - Also removed problematic default: 0 from heading schema definition 226 | - Heading formatting now works correctly for both single and multi-line blocks 227 | - roam_create_block now properly applies H1, H2, and H3 formatting when heading parameter is provided 228 | 229 | v.0.25.1 230 | 231 | - Investigated heading formatting issue in roam_create_block tool 232 | - Attempted multiple fixes: direct createBlock API → batchActions → convertToRoamActions → direct batch action creation 233 | - Confirmed roam_create_page works correctly for heading formatting 234 | - Identified that heading formatting fails specifically for single block creation via roam_create_block 235 | - Issue remains unresolved despite extensive troubleshooting and multiple implementation approaches 236 | - Current status: roam_create_block does not apply heading formatting, investigation ongoing 237 | 238 | v.0.25.0 239 | 240 | - Updated roam_create_page to use batchActions 241 | 242 | v.0.24.6 243 | 244 | - Updated roam_create_page to use explicit levels 245 | 246 | v.0.24.5 247 | 248 | - Enhanced createOutline to properly handle block_text_uid as either a 9-character UID or string title 249 | - Added proper detection and use of existing blocks when given a valid block UID 250 | - Improved error messages to be more specific about block operations 251 | 252 | v.0.24.4 253 | 254 | - Clarified roam_search_by_date and roam_fetch_page_by_title when it comes to searching for daily pages vs. blocks by date 255 | 256 | v.0.24.3 257 | 258 | - Clarified roam_update_multiple_blocks 259 | - Added a variable to roam_find_pages_modified_today 260 | 261 | v.0.24.2 262 | 263 | - Added sort_by and filter_tag to roam_recall 264 | 265 | v.0.24.1 266 | 267 | - Fixed searchByStatus for TODO checks 268 | - Added resolution of references to various tools 269 | 270 | v.0.23.2 271 | 272 | - Fixed create_page tool as first-level blocks were created in reversed order 273 | 274 | v.0.23.1 275 | 276 | - Fixed roam_outline tool not writing properly 277 | 278 | v.0.23.0 279 | 280 | - Added advanced, more flexible datomic query 281 | 282 | v.0.22.1 283 | 284 | - Important description change in roam_remember 285 | 286 | v0.22.0 287 | 288 | - Restructured search functionality into dedicated directory with proper TypeScript support 289 | - Fixed TypeScript errors and import paths throughout the codebase 290 | - Improved outline creation to maintain exact input array order 291 | - Enhanced recall() method to fetch memories from both tag searches and dedicated memories page 292 | - Maintained backward compatibility while improving code organization 293 | 294 | v0.21.0 295 | 296 | - Added roam_recall tool to recall memories from all tags and the page itself. 297 | 298 | v0.20.0 299 | 300 | - Added roam_remember tool to remember specific memories as created on the daily page. Can be used throughout the graph. Tag set in environmental vars in config. 301 | 302 | v0.19.0 303 | 304 | - Changed default case-sensitivity behavior in search tools to match Roam's native behavior (now defaults to true) 305 | - Updated case-sensitivity handling in findBlockWithRetry, searchByStatus, searchForTag, and searchByDate tools 306 | 307 | v0.18.0 308 | 309 | - Added roam_search_by_date tool to search for blocks and pages based on creation or modification dates 310 | - Added support for date range filtering and content inclusion options 311 | 312 | v0.17.0 313 | 314 | - Enhanced roam_update_block tool with transform pattern support, allowing regex-based content transformations 315 | - Added ability to update blocks with either direct content or pattern-based transformations 316 | 317 | v0.16.0 318 | 319 | - Added roam_search_by_text tool to search for blocks containing specific text, with optional page scope and case sensitivity 320 | - Fixed roam_search_by_tag 321 | 322 | v.0.15.0 323 | 324 | - Added roam_find_pages_modified_today tool to search for pages modified since midnight today 325 | 326 | v.0.14 327 | ``` -------------------------------------------------------------------------------- /src/tools/operations/outline.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Graph, q, createPage, createBlock, batchActions } from '@roam-research/roam-api-sdk'; 2 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; 3 | import { formatRoamDate } from '../../utils/helpers.js'; 4 | import { capitalizeWords, getNestedUids, getNestedUidsByText } from '../helpers/text.js'; 5 | import { 6 | parseMarkdown, 7 | convertToRoamActions, 8 | convertToRoamMarkdown, 9 | hasMarkdownTable, 10 | type BatchAction 11 | } from '../../markdown-utils.js'; 12 | import type { OutlineItem, NestedBlock } from '../types/index.js'; 13 | 14 | export class OutlineOperations { 15 | constructor(private graph: Graph) { } 16 | 17 | /** 18 | * Helper function to find block with improved relationship checks 19 | */ 20 | private async findBlockWithRetry(pageUid: string, blockString: string, maxRetries = 5, initialDelay = 1000): Promise<string> { 21 | // Try multiple query strategies 22 | const queries = [ 23 | // Strategy 1: Direct page and string match 24 | `[:find ?b-uid ?order 25 | :where [?p :block/uid "${pageUid}"] 26 | [?b :block/page ?p] 27 | [?b :block/string "${blockString}"] 28 | [?b :block/order ?order] 29 | [?b :block/uid ?b-uid]]`, 30 | 31 | // Strategy 2: Parent-child relationship 32 | `[:find ?b-uid ?order 33 | :where [?p :block/uid "${pageUid}"] 34 | [?b :block/parents ?p] 35 | [?b :block/string "${blockString}"] 36 | [?b :block/order ?order] 37 | [?b :block/uid ?b-uid]]`, 38 | 39 | // Strategy 3: Broader page relationship 40 | `[:find ?b-uid ?order 41 | :where [?p :block/uid "${pageUid}"] 42 | [?b :block/page ?page] 43 | [?p :block/page ?page] 44 | [?b :block/string "${blockString}"] 45 | [?b :block/order ?order] 46 | [?b :block/uid ?b-uid]]` 47 | ]; 48 | 49 | for (let retry = 0; retry < maxRetries; retry++) { 50 | // Try each query strategy 51 | for (const queryStr of queries) { 52 | const blockResults = await q(this.graph, queryStr, []) as [string, number][]; 53 | if (blockResults && blockResults.length > 0) { 54 | // Use the most recently created block 55 | const sorted = blockResults.sort((a, b) => b[1] - a[1]); 56 | return sorted[0][0]; 57 | } 58 | } 59 | 60 | // Exponential backoff 61 | const delay = initialDelay * Math.pow(2, retry); 62 | await new Promise(resolve => setTimeout(resolve, delay)); 63 | } 64 | 65 | throw new McpError( 66 | ErrorCode.InternalError, 67 | `Failed to find block "${blockString}" under page "${pageUid}" after trying multiple strategies` 68 | ); 69 | }; 70 | 71 | /** 72 | * Helper function to create and verify block with improved error handling 73 | */ 74 | private async createAndVerifyBlock( 75 | content: string, 76 | parentUid: string, 77 | maxRetries = 5, 78 | initialDelay = 1000, 79 | isRetry = false 80 | ): Promise<string> { 81 | try { 82 | // Initial delay before any operations 83 | if (!isRetry) { 84 | await new Promise(resolve => setTimeout(resolve, initialDelay)); 85 | } 86 | 87 | for (let retry = 0; retry < maxRetries; retry++) { 88 | console.log(`Attempt ${retry + 1}/${maxRetries} to create block "${content}" under "${parentUid}"`); 89 | 90 | // Create block using batchActions 91 | const batchResult = await batchActions(this.graph, { 92 | action: 'batch-actions', 93 | actions: [{ 94 | action: 'create-block', 95 | location: { 96 | 'parent-uid': parentUid, 97 | order: 'last' 98 | }, 99 | block: { string: content } 100 | }] 101 | }); 102 | 103 | if (!batchResult) { 104 | throw new McpError( 105 | ErrorCode.InternalError, 106 | `Failed to create block "${content}" via batch action` 107 | ); 108 | } 109 | 110 | // Wait with exponential backoff 111 | const delay = initialDelay * Math.pow(2, retry); 112 | await new Promise(resolve => setTimeout(resolve, delay)); 113 | 114 | try { 115 | // Try to find the block using our improved findBlockWithRetry 116 | return await this.findBlockWithRetry(parentUid, content); 117 | } catch (error: any) { 118 | const errorMessage = error instanceof Error ? error.message : String(error); 119 | // console.log(`Failed to find block on attempt ${retry + 1}: ${errorMessage}`); // Removed console.log 120 | if (retry === maxRetries - 1) throw error; 121 | } 122 | } 123 | 124 | throw new McpError( 125 | ErrorCode.InternalError, 126 | `Failed to create and verify block "${content}" after ${maxRetries} attempts` 127 | ); 128 | } catch (error) { 129 | // If this is already a retry, throw the error 130 | if (isRetry) throw error; 131 | 132 | // Otherwise, try one more time with a clean slate 133 | // console.log(`Retrying block creation for "${content}" with fresh attempt`); // Removed console.log 134 | await new Promise(resolve => setTimeout(resolve, initialDelay * 2)); 135 | return this.createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true); 136 | } 137 | }; 138 | 139 | /** 140 | * Helper function to check if string is a valid Roam UID (9 characters) 141 | */ 142 | private isValidUid = (str: string): boolean => { 143 | return typeof str === 'string' && str.length === 9; 144 | }; 145 | 146 | /** 147 | * Helper function to fetch a block and its children recursively 148 | */ 149 | private async fetchBlockWithChildren(blockUid: string, level: number = 1): Promise<NestedBlock | null> { 150 | const query = ` 151 | [:find ?childUid ?childString ?childOrder 152 | :in $ ?parentUid 153 | :where 154 | [?parentEntity :block/uid ?parentUid] 155 | [?parentEntity :block/children ?childEntity] ; This ensures direct children 156 | [?childEntity :block/uid ?childUid] 157 | [?childEntity :block/string ?childString] 158 | [?childEntity :block/order ?childOrder]] 159 | `; 160 | 161 | const blockQuery = ` 162 | [:find ?string 163 | :in $ ?uid 164 | :where 165 | [?e :block/uid ?uid] 166 | [?e :block/string ?string]] 167 | `; 168 | 169 | try { 170 | const blockStringResult = await q(this.graph, blockQuery, [blockUid]) as [string][]; 171 | if (!blockStringResult || blockStringResult.length === 0) { 172 | return null; 173 | } 174 | const text = blockStringResult[0][0]; 175 | 176 | const childrenResults = await q(this.graph, query, [blockUid]) as [string, string, number][]; 177 | const children: NestedBlock[] = []; 178 | 179 | if (childrenResults && childrenResults.length > 0) { 180 | // Sort children by order 181 | const sortedChildren = childrenResults.sort((a, b) => a[2] - b[2]); 182 | 183 | for (const childResult of sortedChildren) { 184 | const childUid = childResult[0]; 185 | const nestedChild = await this.fetchBlockWithChildren(childUid, level + 1); 186 | if (nestedChild) { 187 | children.push(nestedChild); 188 | } 189 | } 190 | } 191 | 192 | // The order of the root block is not available from this query, so we set it to 0 193 | return { uid: blockUid, text, level, order: 0, children: children.length > 0 ? children : undefined }; 194 | } catch (error: any) { 195 | throw new McpError( 196 | ErrorCode.InternalError, 197 | `Failed to fetch block with children for UID "${blockUid}": ${error.message}` 198 | ); 199 | } 200 | }; 201 | 202 | /** 203 | * Recursively fetches a nested structure of blocks under a given root block UID. 204 | */ 205 | private async fetchNestedStructure(rootUid: string): Promise<NestedBlock[]> { 206 | const query = `[:find ?child-uid ?child-string ?child-order 207 | :in $ ?parent-uid 208 | :where 209 | [?parent :block/uid ?parent-uid] 210 | [?parent :block/children ?child] 211 | [?child :block/uid ?child-uid] 212 | [?child :block/string ?child-string] 213 | [?child :block/order ?child-order]]`; 214 | const directChildrenResult = await q(this.graph, query, [rootUid]) as [string, string, number][]; 215 | 216 | if (directChildrenResult.length === 0) { 217 | return []; 218 | } 219 | 220 | const nestedBlocks: NestedBlock[] = []; 221 | for (const [childUid, childString, childOrder] of directChildrenResult) { 222 | const children = await this.fetchNestedStructure(childUid); 223 | nestedBlocks.push({ 224 | uid: childUid, 225 | text: childString, 226 | level: 0, // Level is not easily determined here, so we set it to 0 227 | children: children, 228 | order: childOrder 229 | }); 230 | } 231 | 232 | return nestedBlocks.sort((a, b) => a.order - b.order); 233 | } 234 | 235 | /** 236 | * Creates an outline structure on a Roam Research page, optionally under a specific block. 237 | * 238 | * @param outline - An array of OutlineItem objects, each containing text and a level. 239 | * Markdown heading syntax (#, ##, ###) in the text will be recognized 240 | * and converted to Roam headings while preserving the outline's hierarchical 241 | * structure based on indentation. 242 | * @param page_title_uid - The title or UID of the page where the outline should be created. 243 | * If not provided, today's daily page will be used. 244 | * @param block_text_uid - Optional. The text content or UID of an existing block under which 245 | * the outline should be inserted. If a text string is provided and 246 | * no matching block is found, a new block with that text will be created 247 | * on the page to serve as the parent. If a UID is provided and the block 248 | * is not found, an error will be thrown. 249 | * @returns An object containing success status, page UID, parent UID, and a nested array of created block UIDs. 250 | */ 251 | async createOutline( 252 | outline: Array<OutlineItem>, 253 | page_title_uid?: string, 254 | block_text_uid?: string 255 | ): Promise<{ success: boolean; page_uid: string; parent_uid: string; created_uids: NestedBlock[] }> { 256 | // Validate input 257 | if (!Array.isArray(outline) || outline.length === 0) { 258 | throw new McpError( 259 | ErrorCode.InvalidRequest, 260 | 'outline must be a non-empty array' 261 | ); 262 | } 263 | 264 | // Filter out items with undefined text 265 | const validOutline = outline.filter(item => item.text !== undefined); 266 | if (validOutline.length === 0) { 267 | throw new McpError( 268 | ErrorCode.InvalidRequest, 269 | 'outline must contain at least one item with text' 270 | ); 271 | } 272 | 273 | // Validate outline structure 274 | const invalidItems = validOutline.filter(item => 275 | typeof item.level !== 'number' || 276 | item.level < 1 || 277 | item.level > 10 || 278 | typeof item.text !== 'string' || 279 | item.text.trim().length === 0 280 | ); 281 | 282 | if (invalidItems.length > 0) { 283 | throw new McpError( 284 | ErrorCode.InvalidRequest, 285 | 'outline contains invalid items - each item must have a level (1-10) and non-empty text' 286 | ); 287 | } 288 | 289 | // Helper function to find or create page with retries 290 | const findOrCreatePage = async (titleOrUid: string, maxRetries = 3, delayMs = 500): Promise<string> => { 291 | // First try to find by title 292 | const titleQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`; 293 | const variations = [ 294 | titleOrUid, // Original 295 | capitalizeWords(titleOrUid), // Each word capitalized 296 | titleOrUid.toLowerCase() // All lowercase 297 | ]; 298 | 299 | for (let retry = 0; retry < maxRetries; retry++) { 300 | // Try each case variation 301 | for (const variation of variations) { 302 | const findResults = await q(this.graph, titleQuery, [variation]) as [string][]; 303 | if (findResults && findResults.length > 0) { 304 | return findResults[0][0]; 305 | } 306 | } 307 | 308 | // If not found as title, try as UID 309 | const uidQuery = `[:find ?uid 310 | :where [?e :block/uid "${titleOrUid}"] 311 | [?e :block/uid ?uid]]`; 312 | const uidResult = await q(this.graph, uidQuery, []); 313 | if (uidResult && uidResult.length > 0) { 314 | return uidResult[0][0]; 315 | } 316 | 317 | // If still not found and this is the first retry, try to create the page 318 | if (retry === 0) { 319 | const success = await createPage(this.graph, { 320 | action: 'create-page', 321 | page: { title: titleOrUid } 322 | }); 323 | 324 | // Even if createPage returns false, the page might still have been created 325 | // Wait a bit and continue to next retry 326 | await new Promise(resolve => setTimeout(resolve, delayMs)); 327 | continue; 328 | } 329 | 330 | if (retry < maxRetries - 1) { 331 | await new Promise(resolve => setTimeout(resolve, delayMs)); 332 | } 333 | } 334 | 335 | throw new McpError( 336 | ErrorCode.InvalidRequest, 337 | `Failed to find or create page "${titleOrUid}" after multiple attempts` 338 | ); 339 | }; 340 | 341 | // Get or create the target page 342 | const targetPageUid = await findOrCreatePage( 343 | page_title_uid || formatRoamDate(new Date()) 344 | ); 345 | 346 | // Get or create the parent block 347 | let targetParentUid: string; 348 | if (!block_text_uid) { 349 | targetParentUid = targetPageUid; 350 | } else { 351 | try { 352 | if (this.isValidUid(block_text_uid)) { 353 | // First try to find block by UID 354 | const uidQuery = `[:find ?uid 355 | :where [?e :block/uid "${block_text_uid}"] 356 | [?e :block/uid ?uid]]`; 357 | const uidResult = await q(this.graph, uidQuery, []) as [string][]; 358 | 359 | if (uidResult && uidResult.length > 0) { 360 | // Use existing block if found 361 | targetParentUid = uidResult[0][0]; 362 | } else { 363 | throw new McpError( 364 | ErrorCode.InvalidRequest, 365 | `Block with UID "${block_text_uid}" not found` 366 | ); 367 | } 368 | } else { 369 | // Create header block and get its UID if not a valid UID 370 | targetParentUid = await this.createAndVerifyBlock(block_text_uid, targetPageUid); 371 | } 372 | } catch (error: any) { 373 | const errorMessage = error instanceof Error ? error.message : String(error); 374 | throw new McpError( 375 | ErrorCode.InternalError, 376 | `Failed to ${this.isValidUid(block_text_uid) ? 'find' : 'create'} block "${block_text_uid}": ${errorMessage}` 377 | ); 378 | } 379 | } 380 | 381 | // Initialize result variable 382 | let result; 383 | 384 | try { 385 | // Validate level sequence 386 | if (validOutline.length > 0 && validOutline[0].level !== 1) { 387 | throw new McpError( 388 | ErrorCode.InvalidRequest, 389 | 'Invalid outline structure - the first item must be at level 1' 390 | ); 391 | } 392 | 393 | let prevLevel = 0; 394 | for (const item of validOutline) { 395 | // Level should not increase by more than 1 at a time 396 | if (item.level > prevLevel + 1) { 397 | throw new McpError( 398 | ErrorCode.InvalidRequest, 399 | `Invalid outline structure - level ${item.level} follows level ${prevLevel}` 400 | ); 401 | } 402 | prevLevel = item.level; 403 | } 404 | 405 | // Convert outline items to markdown-like structure 406 | const markdownContent = validOutline 407 | .map(item => { 408 | const indent = ' '.repeat(item.level - 1); 409 | // If the item text starts with a markdown heading (e.g., #, ##, ###), 410 | // treat it as a direct heading without adding a bullet or outline indentation. 411 | // NEW CHANGE: Handle standalone code blocks - do not prepend bullet 412 | const isCodeBlock = item.text?.startsWith('```') && item.text.endsWith('```') && item.text.includes('\n'); 413 | return isCodeBlock ? `${indent}${item.text?.trim()}` : `${indent}- ${item.text?.trim()}`; 414 | }) 415 | .join('\n'); 416 | 417 | // Convert to Roam markdown format 418 | const convertedContent = convertToRoamMarkdown(markdownContent); 419 | 420 | // Parse markdown into hierarchical structure 421 | // We pass the original OutlineItem properties (heading, children_view_type) 422 | // along with the parsed content to the nodes. 423 | const nodes = parseMarkdown(convertedContent).map((node, index) => { 424 | const outlineItem = validOutline[index]; 425 | return { 426 | ...node, 427 | ...(outlineItem?.heading && { heading_level: outlineItem.heading }), 428 | ...(outlineItem?.children_view_type && { children_view_type: outlineItem.children_view_type }) 429 | }; 430 | }); 431 | 432 | // Convert nodes to batch actions 433 | const actions = convertToRoamActions(nodes, targetParentUid, 'last'); 434 | 435 | if (actions.length === 0) { 436 | throw new McpError( 437 | ErrorCode.InvalidRequest, 438 | 'No valid actions generated from outline' 439 | ); 440 | } 441 | 442 | // Execute batch actions to create the outline 443 | result = await batchActions(this.graph, { 444 | action: 'batch-actions', 445 | actions 446 | }).catch(error => { 447 | throw new McpError( 448 | ErrorCode.InternalError, 449 | `Failed to create outline blocks: ${error.message}` 450 | ); 451 | }); 452 | 453 | if (!result) { 454 | throw new McpError( 455 | ErrorCode.InternalError, 456 | 'Failed to create outline blocks - no result returned' 457 | ); 458 | } 459 | } catch (error: any) { 460 | if (error instanceof McpError) throw error; 461 | throw new McpError( 462 | ErrorCode.InternalError, 463 | `Failed to create outline: ${error.message}` 464 | ); 465 | } 466 | 467 | // Post-creation verification to get actual UIDs for top-level blocks and their children 468 | const createdBlocks: NestedBlock[] = []; 469 | // Only query for top-level blocks (level 1) based on the original outline input 470 | const topLevelOutlineItems = validOutline.filter(item => item.level === 1); 471 | 472 | for (const item of topLevelOutlineItems) { 473 | try { 474 | // Assert item.text is a string as it's filtered earlier to be non-undefined and non-empty 475 | const foundUid = await this.findBlockWithRetry(targetParentUid, item.text!); 476 | if (foundUid) { 477 | const nestedBlock = await this.fetchBlockWithChildren(foundUid); 478 | if (nestedBlock) { 479 | createdBlocks.push(nestedBlock); 480 | } 481 | } 482 | } catch (error: any) { 483 | // This is a warning because even if one block fails to fetch, others might succeed. 484 | // The error will be logged but not re-thrown to allow partial success reporting. 485 | // console.warn(`Could not fetch nested block for "${item.text}": ${error.message}`); 486 | } 487 | } 488 | 489 | return { 490 | success: true, 491 | page_uid: targetPageUid, 492 | parent_uid: targetParentUid, 493 | created_uids: createdBlocks 494 | }; 495 | } 496 | 497 | async importMarkdown( 498 | content: string, 499 | page_uid?: string, 500 | page_title?: string, 501 | parent_uid?: string, 502 | parent_string?: string, 503 | order: 'first' | 'last' = 'last' 504 | ): Promise<{ success: boolean; page_uid: string; parent_uid: string; created_uids: NestedBlock[] }> { 505 | // First get the page UID 506 | let targetPageUid = page_uid; 507 | 508 | if (!targetPageUid && page_title) { 509 | const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`; 510 | const findResults = await q(this.graph, findQuery, [page_title]) as [string][]; 511 | 512 | if (findResults && findResults.length > 0) { 513 | targetPageUid = findResults[0][0]; 514 | } else { 515 | throw new McpError( 516 | ErrorCode.InvalidRequest, 517 | `Page with title "${page_title}" not found` 518 | ); 519 | } 520 | } 521 | 522 | // If no page specified, use today's date page 523 | if (!targetPageUid) { 524 | const today = new Date(); 525 | const dateStr = formatRoamDate(today); 526 | 527 | const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`; 528 | const findResults = await q(this.graph, findQuery, [dateStr]) as [string][]; 529 | 530 | if (findResults && findResults.length > 0) { 531 | targetPageUid = findResults[0][0]; 532 | } else { 533 | // Create today's page 534 | try { 535 | await createPage(this.graph, { 536 | action: 'create-page', 537 | page: { title: dateStr } 538 | }); 539 | 540 | const results = await q(this.graph, findQuery, [dateStr]) as [string][]; 541 | if (!results || results.length === 0) { 542 | throw new McpError( 543 | ErrorCode.InternalError, 544 | 'Could not find created today\'s page' 545 | ); 546 | } 547 | targetPageUid = results[0][0]; 548 | } catch (error) { 549 | throw new McpError( 550 | ErrorCode.InternalError, 551 | `Failed to create today's page: ${error instanceof Error ? error.message : String(error)}` 552 | ); 553 | } 554 | } 555 | } 556 | 557 | // Now get the parent block UID 558 | let targetParentUid = parent_uid; 559 | 560 | if (!targetParentUid && parent_string) { 561 | if (!targetPageUid) { 562 | throw new McpError( 563 | ErrorCode.InvalidRequest, 564 | 'Must provide either page_uid or page_title when using parent_string' 565 | ); 566 | } 567 | 568 | // Find block by exact string match within the page 569 | const findBlockQuery = `[:find ?b-uid 570 | :in $ ?page-uid ?block-string 571 | :where [?p :block/uid ?page-uid] 572 | [?b :block/page ?p] 573 | [?b :block/string ?block-string] 574 | [?b :block/uid ?b-uid]]`; 575 | const blockResults = await q(this.graph, findBlockQuery, [targetPageUid, parent_string]) as [string][]; 576 | 577 | if (blockResults && blockResults.length > 0) { 578 | targetParentUid = blockResults[0][0]; 579 | } else { 580 | // If parent_string block doesn't exist, create it 581 | targetParentUid = await this.createAndVerifyBlock(parent_string, targetPageUid); 582 | } 583 | } 584 | 585 | // If no parent specified, use page as parent 586 | if (!targetParentUid) { 587 | targetParentUid = targetPageUid; 588 | } 589 | 590 | // Always use parseMarkdown for content with multiple lines or any markdown formatting 591 | const isMultilined = content.includes('\n'); 592 | 593 | if (isMultilined) { 594 | // Parse markdown into hierarchical structure 595 | const convertedContent = convertToRoamMarkdown(content); 596 | const nodes = parseMarkdown(convertedContent); 597 | 598 | // Convert markdown nodes to batch actions 599 | const actions = convertToRoamActions(nodes, targetParentUid, order); 600 | 601 | // Execute batch actions to add content 602 | const result = await batchActions(this.graph, { 603 | action: 'batch-actions', 604 | actions 605 | }); 606 | 607 | if (!result) { 608 | throw new McpError( 609 | ErrorCode.InternalError, 610 | 'Failed to import nested markdown content' 611 | ); 612 | } 613 | 614 | // After successful batch action, get all nested UIDs under the parent 615 | const createdUids = await this.fetchNestedStructure(targetParentUid); 616 | 617 | return { 618 | success: true, 619 | page_uid: targetPageUid, 620 | parent_uid: targetParentUid, 621 | created_uids: createdUids 622 | }; 623 | } else { 624 | // Create a simple block for non-nested content using batchActions 625 | const actions = [{ 626 | action: 'create-block', 627 | location: { 628 | "parent-uid": targetParentUid, 629 | "order": order 630 | }, 631 | block: { string: content } 632 | }]; 633 | 634 | try { 635 | await batchActions(this.graph, { 636 | action: 'batch-actions', 637 | actions 638 | }); 639 | } catch (error) { 640 | throw new McpError( 641 | ErrorCode.InternalError, 642 | `Failed to create content block: ${error instanceof Error ? error.message : String(error)}` 643 | ); 644 | } 645 | 646 | // For single-line content, we still need to fetch the UID and construct a NestedBlock 647 | const createdUids: NestedBlock[] = []; 648 | try { 649 | const foundUid = await this.findBlockWithRetry(targetParentUid, content); 650 | if (foundUid) { 651 | createdUids.push({ 652 | uid: foundUid, 653 | text: content, 654 | level: 0, 655 | order: 0, 656 | children: [] 657 | }); 658 | } 659 | } catch (error: any) { 660 | // Log warning but don't re-throw, as the block might be created, just not immediately verifiable 661 | // console.warn(`Could not verify single block creation for "${content}": ${error.message}`); 662 | } 663 | 664 | return { 665 | success: true, 666 | page_uid: targetPageUid, 667 | parent_uid: targetParentUid, 668 | created_uids: createdUids 669 | }; 670 | } 671 | } 672 | } 673 | ``` -------------------------------------------------------------------------------- /src/tools/schemas.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Tool definitions and input schemas for Roam Research MCP server 2 | export const toolSchemas = { 3 | roam_add_todo: { 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).\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.', 6 | inputSchema: { 7 | type: 'object', 8 | properties: { 9 | todos: { 10 | type: 'array', 11 | items: { 12 | type: 'string', 13 | description: 'Todo item text' 14 | }, 15 | description: 'List of todo items to add' 16 | } 17 | }, 18 | required: ['todos'], 19 | }, 20 | }, 21 | roam_fetch_page_by_title: { 22 | name: 'roam_fetch_page_by_title', 23 | description: 'Fetch page by title. Returns content in the specified format.', 24 | inputSchema: { 25 | type: 'object', 26 | properties: { 27 | title: { 28 | type: 'string', 29 | description: 30 | 'Title of the page. For date pages, use ordinal date formats such as January 2nd, 2025' 31 | }, 32 | format: { 33 | type: 'string', 34 | enum: ['markdown', 'raw'], 35 | default: 'raw', 36 | description: 37 | "Format output as markdown or JSON. 'markdown' returns as string; 'raw' returns JSON string of the page's blocks" 38 | } 39 | }, 40 | required: ['title'] 41 | }, 42 | }, 43 | roam_create_page: { 44 | name: 'roam_create_page', 45 | description: 'Create a new standalone page in Roam with optional content, including structured outlines, using explicit nesting levels and headings (H1-H3). This is the preferred method for creating a new page with an outline in a single step. 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.', 46 | inputSchema: { 47 | type: 'object', 48 | properties: { 49 | title: { 50 | type: 'string', 51 | description: 'Title of the new page', 52 | }, 53 | content: { 54 | type: 'array', 55 | 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.', 56 | items: { 57 | type: 'object', 58 | properties: { 59 | text: { 60 | type: 'string', 61 | description: 'Content of the block' 62 | }, 63 | level: { 64 | type: 'integer', 65 | description: 'Indentation level (1-10, where 1 is top level)', 66 | minimum: 1, 67 | maximum: 10 68 | }, 69 | heading: { 70 | type: 'integer', 71 | description: 'Optional: Heading formatting for this block (1-3)', 72 | minimum: 1, 73 | maximum: 3 74 | } 75 | }, 76 | required: ['text', 'level'] 77 | } 78 | }, 79 | }, 80 | required: ['title'], 81 | }, 82 | }, 83 | roam_create_outline: { 84 | name: 'roam_create_outline', 85 | description: 'Add a structured outline to an existing page or block (by title text or uid), with customizable nesting levels. To create a new page with an outline, use the `roam_create_page` tool instead. 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.', 86 | inputSchema: { 87 | type: 'object', 88 | properties: { 89 | page_title_uid: { 90 | type: 'string', 91 | description: 'Title or UID of the page (UID is preferred for accuracy). Leave blank to use the default daily page.' 92 | }, 93 | block_text_uid: { 94 | type: 'string', 95 | 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).' 96 | }, 97 | outline: { 98 | type: 'array', 99 | 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).', 100 | items: { 101 | type: 'object', 102 | properties: { 103 | text: { 104 | type: 'string', 105 | description: 'Content of the block' 106 | }, 107 | level: { 108 | type: 'integer', 109 | 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).', 110 | minimum: 1, 111 | maximum: 10 112 | }, 113 | heading: { 114 | type: 'integer', 115 | description: 'Optional: Heading formatting for this block (1-3)', 116 | minimum: 1, 117 | maximum: 3 118 | }, 119 | children_view_type: { 120 | type: 'string', 121 | description: 'Optional: The view type for children of this block ("bullet", "document", or "numbered")', 122 | enum: ["bullet", "document", "numbered"] 123 | } 124 | }, 125 | required: ['text', 'level'] 126 | } 127 | } 128 | }, 129 | required: ['outline'] 130 | } 131 | }, 132 | roam_import_markdown: { 133 | name: 'roam_import_markdown', 134 | 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. If a `parent_string` is provided and the block does not exist, it will be created. Returns a nested structure of the created blocks.\nIMPORTANT: Before using this tool, ensure that you have loaded into context the \'Roam Markdown Cheatsheet\' resource.', 135 | inputSchema: { 136 | type: 'object', 137 | properties: { 138 | content: { 139 | type: 'string', 140 | description: 'Nested markdown content to import' 141 | }, 142 | page_uid: { 143 | type: 'string', 144 | description: 'Optional: UID of the page containing the parent block (preferred for accuracy).' 145 | }, 146 | page_title: { 147 | type: 'string', 148 | description: 'Optional: Title of the page containing the parent block (used if page_uid is not provided).' 149 | }, 150 | parent_uid: { 151 | type: 'string', 152 | description: 'Optional: UID of the parent block to add content under (preferred for accuracy).' 153 | }, 154 | parent_string: { 155 | type: 'string', 156 | description: 'Optional: Exact string content of an existing parent block to add content under (used if parent_uid is not provided; requires page_uid or page_title). If the block does not exist, it will be created.' 157 | }, 158 | order: { 159 | type: 'string', 160 | description: 'Optional: Where to add the content undeIs this tr the parent ("first" or "last")', 161 | enum: ['first', 'last'], 162 | default: 'first' 163 | } 164 | }, 165 | required: ['content'] 166 | } 167 | }, 168 | roam_search_for_tag: { 169 | name: 'roam_search_for_tag', 170 | description: 'Search for blocks containing a specific tag and optionally filter by blocks that also contain another tag nearby or exclude blocks with a specific tag. This tool supports pagination via the `limit` and `offset` parameters. Use this tool to search for memories tagged with the MEMORIES_TAG.', 171 | inputSchema: { 172 | type: 'object', 173 | properties: { 174 | primary_tag: { 175 | type: 'string', 176 | description: 'The main tag to search for (without the [[ ]] brackets)', 177 | }, 178 | page_title_uid: { 179 | type: 'string', 180 | description: 'Optional: Title or UID of the page to search in (UID is preferred for accuracy). Defaults to today\'s daily page if not provided.', 181 | }, 182 | near_tag: { 183 | type: 'string', 184 | description: 'Optional: Another tag to filter results by - will only return blocks where both tags appear', 185 | }, 186 | case_sensitive: { 187 | type: 'boolean', 188 | description: 'Optional: Whether the search should be case-sensitive. If false, it will search for the provided tag, capitalized versions, and first word capitalized versions.', 189 | default: false 190 | }, 191 | limit: { 192 | type: 'integer', 193 | description: 'Optional: The maximum number of results to return. Defaults to 50. Use -1 for no limit, but be aware that very large results sets can impact performance.', 194 | default: 50 195 | }, 196 | offset: { 197 | type: 'integer', 198 | description: 'Optional: The number of results to skip before returning matches. Useful for pagination. Defaults to 0.', 199 | default: 0 200 | } 201 | }, 202 | required: ['primary_tag'] 203 | } 204 | }, 205 | roam_search_by_status: { 206 | name: 'roam_search_by_status', 207 | description: 'Search for blocks with a specific status (TODO/DONE) across all pages or within a specific page.', 208 | inputSchema: { 209 | type: 'object', 210 | properties: { 211 | status: { 212 | type: 'string', 213 | description: 'Status to search for (TODO or DONE)', 214 | enum: ['TODO', 'DONE'] 215 | }, 216 | page_title_uid: { 217 | type: 'string', 218 | description: 'Optional: Title or UID of the page to search in (UID is preferred for accuracy). If not provided, searches across all pages.' 219 | }, 220 | include: { 221 | type: 'string', 222 | description: 'Optional: Comma-separated list of terms to filter results by inclusion (matches content or page title)' 223 | }, 224 | exclude: { 225 | type: 'string', 226 | description: 'Optional: Comma-separated list of terms to filter results by exclusion (matches content or page title)' 227 | } 228 | }, 229 | required: ['status'] 230 | } 231 | }, 232 | roam_search_block_refs: { 233 | name: 'roam_search_block_refs', 234 | description: 'Search for block references within a page or across the entire graph. Can search for references to a specific block or find all block references.', 235 | inputSchema: { 236 | type: 'object', 237 | properties: { 238 | block_uid: { 239 | type: 'string', 240 | description: 'Optional: UID of the block to find references to' 241 | }, 242 | page_title_uid: { 243 | type: 'string', 244 | description: 'Optional: Title or UID of the page to search in (UID is preferred for accuracy). If not provided, searches across all pages.' 245 | } 246 | } 247 | } 248 | }, 249 | roam_search_hierarchy: { 250 | name: 'roam_search_hierarchy', 251 | description: 'Search for parent or child blocks in the block hierarchy. Can search up or down the hierarchy from a given block.', 252 | inputSchema: { 253 | type: 'object', 254 | properties: { 255 | parent_uid: { 256 | type: 'string', 257 | description: 'Optional: UID of the block to find children of' 258 | }, 259 | child_uid: { 260 | type: 'string', 261 | description: 'Optional: UID of the block to find parents of' 262 | }, 263 | page_title_uid: { 264 | type: 'string', 265 | description: 'Optional: Title or UID of the page to search in (UID is preferred for accuracy).' 266 | }, 267 | max_depth: { 268 | type: 'integer', 269 | description: 'Optional: How many levels deep to search (default: 1)', 270 | minimum: 1, 271 | maximum: 10 272 | } 273 | } 274 | // Note: Validation for either parent_uid or child_uid is handled in the server code 275 | } 276 | }, 277 | roam_find_pages_modified_today: { 278 | name: 'roam_find_pages_modified_today', 279 | description: 'Find pages that have been modified today (since midnight), with pagination and sorting options.', 280 | inputSchema: { 281 | type: 'object', 282 | properties: { 283 | limit: { 284 | type: 'integer', 285 | description: 'The maximum number of pages to retrieve (default: 50). Use -1 for no limit, but be aware that very large result sets can impact performance.', 286 | default: 50 287 | }, 288 | offset: { 289 | type: 'integer', 290 | description: 'The number of pages to skip before returning matches. Useful for pagination. Defaults to 0.', 291 | default: 0 292 | }, 293 | sort_order: { 294 | type: 'string', 295 | description: 'Sort order for pages based on modification date. "desc" for most recent first, "asc" for oldest first.', 296 | enum: ['asc', 'desc'], 297 | default: 'desc' 298 | } 299 | } 300 | } 301 | }, 302 | roam_search_by_text: { 303 | name: 'roam_search_by_text', 304 | description: 'Search for blocks containing specific text across all pages or within a specific page. This tool supports pagination via the `limit` and `offset` parameters.', 305 | inputSchema: { 306 | type: 'object', 307 | properties: { 308 | text: { 309 | type: 'string', 310 | description: 'The text to search for' 311 | }, 312 | page_title_uid: { 313 | type: 'string', 314 | description: 'Optional: Title or UID of the page to search in (UID is preferred for accuracy). If not provided, searches across all pages.' 315 | }, 316 | case_sensitive: { 317 | type: 'boolean', 318 | description: 'Optional: Whether the search should be case-sensitive. If false, it will search for the provided text, capitalized versions, and first word capitalized versions.', 319 | default: false 320 | }, 321 | limit: { 322 | type: 'integer', 323 | description: 'Optional: The maximum number of results to return. Defaults to 50. Use -1 for no limit, but be aware that very large results sets can impact performance.', 324 | default: 50 325 | }, 326 | offset: { 327 | type: 'integer', 328 | description: 'Optional: The number of results to skip before returning matches. Useful for pagination. Defaults to 0.', 329 | default: 0 330 | } 331 | }, 332 | required: ['text'] 333 | } 334 | }, 335 | roam_search_by_date: { 336 | name: 'roam_search_by_date', 337 | description: 'Search for blocks or pages based on creation or modification dates. Not for daily pages with ordinal date titles.', 338 | inputSchema: { 339 | type: 'object', 340 | properties: { 341 | start_date: { 342 | type: 'string', 343 | description: 'Start date in ISO format (YYYY-MM-DD)', 344 | }, 345 | end_date: { 346 | type: 'string', 347 | description: 'Optional: End date in ISO format (YYYY-MM-DD)', 348 | }, 349 | type: { 350 | type: 'string', 351 | enum: ['created', 'modified', 'both'], 352 | description: 'Whether to search by creation date, modification date, or both', 353 | }, 354 | scope: { 355 | type: 'string', 356 | enum: ['blocks', 'pages', 'both'], 357 | description: 'Whether to search blocks, pages', 358 | }, 359 | include_content: { 360 | type: 'boolean', 361 | description: 'Whether to include the content of matching blocks/pages', 362 | default: true, 363 | } 364 | }, 365 | required: ['start_date', 'type', 'scope'] 366 | } 367 | }, 368 | roam_markdown_cheatsheet: { 369 | name: 'roam_markdown_cheatsheet', 370 | description: 'Provides the content of the Roam Markdown Cheatsheet resource, optionally concatenated with custom instructions if CUSTOM_INSTRUCTIONS_PATH is set.', 371 | inputSchema: { 372 | type: 'object', 373 | properties: {}, 374 | required: [], 375 | }, 376 | }, 377 | roam_remember: { 378 | name: 'roam_remember', 379 | 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.', 380 | inputSchema: { 381 | type: 'object', 382 | properties: { 383 | memory: { 384 | type: 'string', 385 | description: 'The memory detail or information to remember' 386 | }, 387 | categories: { 388 | type: 'array', 389 | items: { 390 | type: 'string' 391 | }, 392 | description: 'Optional categories to tag the memory with (will be converted to Roam tags)' 393 | } 394 | }, 395 | required: ['memory'] 396 | } 397 | }, 398 | roam_recall: { 399 | name: 'roam_recall', 400 | description: 'Retrieve all stored memories on page titled MEMORIES_TAG, or tagged block content with the same name. Returns a combined, deduplicated list of memories. Optionally filter blcoks with a specific tag and sort by creation date.', 401 | inputSchema: { 402 | type: 'object', 403 | properties: { 404 | sort_by: { 405 | type: 'string', 406 | description: 'Sort order for memories based on creation date', 407 | enum: ['newest', 'oldest'], 408 | default: 'newest' 409 | }, 410 | filter_tag: { 411 | type: 'string', 412 | description: 'Include only memories with a specific filter tag. For single word tags use format "tag", for multi-word tags use format "tag word" (without brackets)' 413 | } 414 | } 415 | } 416 | }, 417 | roam_datomic_query: { 418 | name: 'roam_datomic_query', 419 | 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.\n\n__Optimal Use Cases for `roam_datomic_query`:__\n- __Advanced Filtering (including Regex):__ Use for scenarios requiring complex filtering, including regex matching on results post-query, which Datalog does not natively support for all data types. It can fetch broader results for client-side post-processing.\n- __Highly Complex Boolean Logic:__ Ideal for intricate combinations of "AND", "OR", and "NOT" conditions across multiple terms or attributes.\n- __Arbitrary Sorting Criteria:__ The go-to for highly customized sorting needs beyond default options.\n- __Proximity Search:__ For advanced search capabilities involving proximity, which are difficult to implement efficiently with simpler tools.\n\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.', 420 | inputSchema: { 421 | type: 'object', 422 | properties: { 423 | query: { 424 | type: 'string', 425 | description: 'The Datomic query to execute (in Datalog syntax). Example: `[:find ?block-string :where [?block :block/string ?block-string] (or [(clojure.string/includes? ?block-string "hypnosis")] [(clojure.string/includes? ?block-string "trance")] [(clojure.string/includes? ?block-string "suggestion")]) :limit 25]`' 426 | }, 427 | inputs: { 428 | type: 'array', 429 | description: 'Optional array of input parameters for the query', 430 | items: { 431 | type: 'string' 432 | } 433 | }, 434 | regexFilter: { 435 | type: 'string', 436 | description: 'Optional: A regex pattern to filter the results client-side after the Datomic query. Applied to JSON.stringify(result) or specific fields if regexTargetField is provided.' 437 | }, 438 | regexFlags: { 439 | type: 'string', 440 | description: 'Optional: Flags for the regex filter (e.g., "i" for case-insensitive, "g" for global).', 441 | }, 442 | regexTargetField: { 443 | type: 'array', 444 | items: { 445 | type: 'string' 446 | }, 447 | description: 'Optional: An array of field paths (e.g., ["block_string", "page_title"]) within each Datomic result object to apply the regex filter to. If not provided, the regex is applied to the stringified full result.' 448 | } 449 | }, 450 | required: ['query'] 451 | } 452 | }, 453 | roam_process_batch_actions: { 454 | name: 'roam_process_batch_actions', 455 | 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.', 456 | inputSchema: { 457 | type: 'object', 458 | properties: { 459 | actions: { 460 | type: 'array', 461 | description: 'An array of action objects to execute in order.', 462 | items: { 463 | type: 'object', 464 | properties: { 465 | "action": { 466 | type: 'string', 467 | description: 'The specific action to perform.', 468 | enum: ['create-block', 'update-block', 'move-block', 'delete-block'] 469 | }, 470 | "uid": { 471 | type: 'string', 472 | description: 'The UID of the block to target for "update-block", "move-block", or "delete-block" actions.' 473 | }, 474 | "string": { 475 | type: 'string', 476 | description: 'The content for the block, used in "create-block" and "update-block" actions.' 477 | }, 478 | "open": { 479 | type: "boolean", 480 | description: "Optional: Sets the open/closed state of a block, used in 'update-block' or 'create-block'. Defaults to true." 481 | }, 482 | "heading": { 483 | type: "integer", 484 | description: "Optional: The heading level (1, 2, or 3) for 'create-block' or 'update-block'.", 485 | enum: [1, 2, 3] 486 | }, 487 | "text-align": { 488 | type: "string", 489 | description: "Optional: The text alignment for 'create-block' or 'update-block'.", 490 | enum: ["left", "center", "right", "justify"] 491 | }, 492 | "children-view-type": { 493 | type: "string", 494 | description: "Optional: The view type for children of the block, for 'create-block' or 'update-block'.", 495 | enum: ["bullet", "document", "numbered"] 496 | }, 497 | "location": { 498 | type: 'object', 499 | description: 'Specifies where to place a block, used in "create-block" and "move-block" actions.', 500 | properties: { 501 | "parent-uid": { 502 | type: 'string', 503 | description: 'The UID of the parent block or page.' 504 | }, 505 | "order": { 506 | type: ['integer', 'string'], 507 | description: 'The position of the block under its parent (e.g., 0, 1, 2) or a keyword ("first", "last").' 508 | } 509 | } 510 | } 511 | }, 512 | required: ['action'] 513 | } 514 | } 515 | }, 516 | required: ['actions'] 517 | } 518 | }, 519 | roam_fetch_block_with_children: { 520 | name: 'roam_fetch_block_with_children', 521 | description: 'Fetch a block by its UID along with its hierarchical children down to a specified depth. Returns a nested object structure containing the block\'s UID, text, order, and an array of its children.', 522 | inputSchema: { 523 | type: 'object', 524 | properties: { 525 | block_uid: { 526 | type: 'string', 527 | description: 'The UID of the block to fetch.' 528 | }, 529 | depth: { 530 | type: 'integer', 531 | description: 'Optional: The number of levels deep to fetch children. Defaults to 4.', 532 | minimum: 0, 533 | maximum: 10 534 | } 535 | }, 536 | required: ['block_uid'] 537 | }, 538 | }, 539 | }; 540 | ```