#
tokens: 47490/50000 42/43 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/2b3pro/roam-research-mcp?lines=false&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

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
node_modules/
build/
*.log
.env*
typescript/*
src/test-queries.ts
test-*
src/.DS_Store
.DS_Store
.clinerules*
tests/test_read.sh
.roam/2b3-custom-instructions.md
.roam/ias-custom-instructions.md
.cline*
```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
# Ignore node_modules, as they will be installed in the container
node_modules/

# Ignore build directory, as it's created inside the container
build/

# Ignore log files
*.log

# Ignore environment files
.env*

# Ignore TypeScript cache
typescript/

# Ignore test files
src/test-queries.ts
test-*
tests/test_read.sh

# Ignore OS-specific files
src/.DS_Store
.DS_Store

# Ignore Git directory
.git

# Ignore Docker related files
Dockerfile
docker-compose.yml
.dockerignore

# Ignore the project's license and documentation
LICENSE
README.md
CHANGELOG.md
Roam Import JSON Schema.md
Roam_Research_Datalog_Cheatsheet.md

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
![](./roam-research-mcp-image.jpeg)

# Roam Research MCP Server

[![npm version](https://badge.fury.io/js/roam-research-mcp.svg)](https://badge.fury.io/js/roam-research-mcp)
[![Project Status: WIP – Initial development is in progress, but there has not yet been a stable, usable release suitable for the public.](https://www.repostatus.org/badges/latest/wip.svg)](https://www.repostatus.org/#wip)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub](https://img.shields.io/github/license/2b3pro/roam-research-mcp)](https://github.com/2b3pro/roam-research-mcp/blob/main/LICENSE)

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)

<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>
<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>

## Installation and Usage

This MCP server supports three primary communication methods:

1.  **Stdio (Standard Input/Output):** Ideal for local inter-process communication, command-line tools, and direct integration with applications running on the same machine. This is the default communication method when running the server directly.
2.  **HTTP Stream:** Provides network-based communication, suitable for web-based clients, remote applications, or scenarios requiring real-time updates over HTTP. The HTTP Stream endpoint runs on port `8088` by default.
3.  **SSE (Server-Sent Events):** A transport for legacy clients that require SSE. The SSE endpoint runs on port `8087` by default. (NOTE: ⚠️ DEPRECATED: The SSE Transport has been deprecated as of MCP specification version 2025-03-26. HTTP Stream Transport preferred.)

### Running with Stdio

You can install the package globally and run it:

```bash
npm install -g roam-research-mcp
roam-research-mcp
```

Or clone the repository and build from source:

```bash
git clone https://github.com/2b3pro/roam-research-mcp.git
cd roam-research-mcp
npm install
npm run build
npm start
```

### Running with HTTP Stream

To run the server with HTTP Stream or SSE support, you can either:

1.  **Use the default ports:** Run `npm start` after building (as shown above). The server will automatically listen on port `8088` for HTTP Stream and `8087` for SSE.
2.  **Specify custom ports:** Set the `HTTP_STREAM_PORT` and/or `SSE_PORT` environment variables before starting the server.

    ```bash
    HTTP_STREAM_PORT=9000 SSE_PORT=9001 npm start
    ```

    Or, if using a `.env` file, add `HTTP_STREAM_PORT=9000` and/or `SSE_PORT=9001` to it.

## Docker

This project can be easily containerized using Docker. A `Dockerfile` is provided at the root of the repository.

### Build the Docker Image

To build the Docker image, navigate to the project root and run:

```bash
docker build -t roam-research-mcp .
```

### Run the Docker Container

To run the Docker container and map the necessary ports, you must also provide the required environment variables. Use the `-e` flag to pass `ROAM_API_TOKEN`, `ROAM_GRAPH_NAME`, and optionally `MEMORIES_TAG`, `HTTP_STREAM_PORT`, and `SSE_PORT`:

```bash
docker run -p 3000:3000 -p 8088:8088 -p 8087:8087 \
  -e ROAM_API_TOKEN="your-api-token" \
  -e ROAM_GRAPH_NAME="your-graph-name" \
  -e MEMORIES_TAG="#[[LLM/Memories]]" \
  -e CUSTOM_INSTRUCTIONS_PATH="/path/to/your/custom_instructions_file.md" \
  -e HTTP_STREAM_PORT="8088" \
  -e SSE_PORT="8087" \
  roam-research-mcp
```

Alternatively, if you have a `.env` file in the project root (which is copied into the Docker image during build), you can use the `--env-file` flag:

```bash
docker run -p 3000:3000 -p 8088:8088 --env-file .env roam-research-mcp
```

## To Test

Run [MCP Inspector](https://github.com/modelcontextprotocol/inspector) after build using the provided npm script:

```bash
npm run inspector
```

## Features

The server provides powerful tools for interacting with Roam Research:

- Environment variable handling with .env support
- Comprehensive input validation
- Case-insensitive page title matching
- Recursive block reference resolution
- Markdown parsing and conversion
- Daily page integration
- Detailed debug logging
- Efficient batch operations
- Hierarchical outline creation
- Enhanced documentation for Roam Tables in `Roam_Markdown_Cheatsheet.md` for clearer guidance on nesting.
  - Custom instruction appended to the cheat sheet about your specific Roam notes.

1. `roam_fetch_page_by_title`: Fetch page content by title. Returns content in the specified format.
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.
3. `roam_create_page`: Create new pages with optional content and headings. Now creates a block on the daily page linking to the newly created page.
4. `roam_import_markdown`: Import nested markdown content under a specific block. (Internally uses `roam_process_batch_actions`.)
5. `roam_add_todo`: Add a list of todo items to today's daily page. (Internally uses `roam_process_batch_actions`.)
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`.)
7. `roam_search_block_refs`: Search for block references within a page or across the entire graph.
8. `roam_search_hierarchy`: Search for parent or child blocks in the block hierarchy.
9. `roam_find_pages_modified_today`: Find pages that have been modified today (since midnight), with pagination and sorting options.
10. `roam_search_by_text`: Search for blocks containing specific text across all pages or within a specific page. This tool supports pagination via the `limit` and `offset` parameters.
11. `roam_search_by_status`: Search for blocks with a specific status (TODO/DONE) across all pages or within a specific page.
12. `roam_search_by_date`: Search for blocks or pages based on creation or modification dates.
13. `roam_search_for_tag`: 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.
14. `roam_remember`: Add a memory or piece of information to remember. (Internally uses `roam_process_batch_actions`.)
15. `roam_recall`: Retrieve all stored memories.
16. `roam_datomic_query`: Execute a custom Datomic query on the Roam graph for advanced data retrieval beyond the available search tools. Now supports client-side regex filtering for enhanced post-query processing. Optimal for complex filtering (including regex), highly complex boolean logic, arbitrary sorting criteria, and proximity search.
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.
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`.)

**Deprecated Tools**:
The following tools have been deprecated as of `v0.36.2` in favor of the more powerful and flexible `roam_process_batch_actions`:

- `roam_create_block`: Use `roam_process_batch_actions` with the `create-block` action.
- `roam_update_block`: Use `roam_process_batch_actions` with the `update-block` action.
- `roam_update_multiple_blocks`: Use `roam_process_batch_actions` with multiple `update-block` actions.

---

### Tool Usage Guidelines and Best Practices

**Pre-computation and Context Loading:**
✅ 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>"

- **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.

**Identifying Pages and Blocks for Manipulation:**
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.

- **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'"
- **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.

**Case-Sensitivity:**
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.

**Iterative Refinement and Verification:**
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.

**Understanding Tool Nuances:**
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.

When making changes to your Roam graph, precision in your requests is crucial for achieving desired outcomes.

**Specificity in Requests:**
Some tools allow for identifying blocks or pages by their text content (e.g., `parent_string`, `title`). While convenient, using **Unique Identifiers (UIDs)** is always preferred for accuracy and reliability. Text-based matching can be prone to errors if there are multiple blocks with similar content or if the content changes. Tools are designed to work best when provided with explicit UIDs where available.

**Example of Specificity:**
Instead of:
`"parent_string": "My project notes"`

Prefer:
`"parent_uid": "((some-unique-uid))"`

**Caveat Regarding Heading Formatting:**
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.

---

## Example Prompts

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.

### Example 1: Creating a Project Outline

This prompt demonstrates creating a new page and populating it with a structured outline using a single `roam_process_batch_actions` call.

```
"Create a new Roam page titled 'Project Alpha Planning' and add the following outline:
- Overview
  - Goals
  - Scope
- Team Members
  - John Doe
  - Jane Smith
- Tasks
  - Task 1
    - Subtask 1.1
    - Subtask 1.2
  - Task 2
- Deadlines"
```

### Example 2: Updating Multiple To-Dos and Adding a New One

This example shows how to mark existing to-do items as `DONE` and add a new one, all within a single batch.

```
"Mark 'Finish report' and 'Review presentation' as done on today's daily page, and add a new todo 'Prepare for meeting'."
```

### Example 3: Moving and Updating a Block

This demonstrates moving a block from one location to another and simultaneously updating its content.

```
"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'."
```

### Example 4: Making a Table

This demonstrates moving a block from one location to another and simultaneously updating its content.

```
"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."
```

---

## Setup

1. Create a [Roam Research API token](https://x.com/RoamResearch/status/1789358175474327881):

   - Go to your graph settings
   - Navigate to the "API tokens" section (Settings > "Graph" tab > "API Tokens" section and click on the "+ New API Token" button)
   - Create a new token

2. Configure the environment variables:
   You have two options for configuring the required environment variables:

   Option 1: Using a .env file (Recommended for development)
   Create a `.env` file in the roam-research directory:

   ```
   ROAM_API_TOKEN=your-api-token
   ROAM_GRAPH_NAME=your-graph-name
   MEMORIES_TAG='#[[LLM/Memories]]'
   CUSTOM_INSTRUCTIONS_PATH='/path/to/your/custom_instructions_file.md'
   HTTP_STREAM_PORT=8088 # Or your desired port for HTTP Stream communication
   SSE_PORT=8087 # Or your desired port for SSE communication
   ```

   Option 2: Using MCP settings (Alternative method)
   Add the configuration to your MCP settings file. Note that you may need to update the `args` to `["/path/to/roam-research-mcp/build/index.js"]` if you are running the server directly.

   - For Cline (`~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`):
   - For Claude desktop app (`~/Library/Application Support/Claude/claude_desktop_config.json`):

   ```json
   {
     "mcpServers": {
       "roam-research": {
         "command": "node",
         "args": ["/path/to/roam-research-mcp/build/index.js"],
         "env": {
           "ROAM_API_TOKEN": "your-api-token",
           "ROAM_GRAPH_NAME": "your-graph-name",
           "MEMORIES_TAG": "#[[LLM/Memories]]",
           "CUSTOM_INSTRUCTIONS_PATH": "/path/to/your/custom_instructions_file.md",
           "HTTP_STREAM_PORT": "8088",
           "SSE_PORT": "8087"
         }
       }
     }
   }
   ```

   Note: The server will first try to load from .env file, then fall back to environment variables from MCP settings.

3. Build the server (make sure you're in the root directory of the MCP):

   Note: Customize 'Roam_Markdown_Cheatsheet.md' with any notes and preferences specific to your graph BEFORE building.

   ```bash
   cd roam-research-mcp
   npm install
   npm run build
   ```

## Error Handling

The server provides comprehensive error handling for common scenarios:

- Configuration errors:
  - Missing API token or graph name
  - Invalid environment variables
- API errors:
  - Authentication failures
  - Invalid requests
  - Failed operations
- Tool-specific errors:
  - Page not found (with case-insensitive search)
  - Block not found by string match
  - Invalid markdown format
  - Missing required parameters
  - Invalid outline structure or content

Each error response includes:

- Standard MCP error code
- Detailed error message
- Suggestions for resolution when applicable

---

## Development

### Building

To build the server:

```bash
npm install
npm run build
```

This will:

1. Install all required dependencies
2. Compile TypeScript to JavaScript
3. Make the output file executable

You can also use `npm run watch` during development to automatically recompile when files change.

### Testing with MCP Inspector

The MCP Inspector is a tool that helps test and debug MCP servers. To test the server:

```bash
# Inspect with npx:
npx @modelcontextprotocol/inspector node build/index.js
```

This will:

1. Start the server in inspector mode
2. Provide an interactive interface to:
   - List available tools and resources
   - Execute tools with custom parameters
   - View tool responses and error handling

## License

MIT License

---

## About the Author

This project is maintained by [Ian Shen](https://github.com/2b3pro).

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import { RoamServer } from './server/roam-server.js';

const server = new RoamServer();
server.run().catch(() => { /* handle error silently */ });

```

--------------------------------------------------------------------------------
/src/search/index.ts:
--------------------------------------------------------------------------------

```typescript
export * from './types.js';
export * from './utils.js';
export * from './tag-search.js';
export * from './status-search.js';
export * from './block-ref-search.js';
export * from './hierarchy-search.js';
export * from './text-search.js';
export * from './datomic-search.js';

```

--------------------------------------------------------------------------------
/src/types/roam.ts:
--------------------------------------------------------------------------------

```typescript
// Interface for Roam block structure
export interface RoamBlock {
  uid: string;
  string: string;
  order: number;
  heading?: number | null;
  children: RoamBlock[];
}

export type RoamBatchAction = {
  action: 'create-block' | 'update-block' | 'move-block' | 'delete-block';
  [key: string]: any;
};

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2021",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": [
    "src/**/*",
    "tests/test-addMarkdownText.ts",
    "tests/test-queries.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
```

--------------------------------------------------------------------------------
/src/utils/helpers.ts:
--------------------------------------------------------------------------------

```typescript
// Helper function to get ordinal suffix for numbers (1st, 2nd, 3rd, etc.)
export function getOrdinalSuffix(n: number): string {
  const j = n % 10;
  const k = n % 100;
  if (j === 1 && k !== 11) return "st";
  if (j === 2 && k !== 12) return "nd";
  if (j === 3 && k !== 13) return "rd";
  return "th";
}

// Format date in Roam's preferred format (e.g., "January 1st, 2024")
export function formatRoamDate(date: Date): string {
  const month = date.toLocaleDateString('en-US', { month: 'long' });
  const day = date.getDate();
  const year = date.getFullYear();
  return `${month} ${day}${getOrdinalSuffix(day)}, ${year}`;
}

```

--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
version: '3.8'

services:
  roam-mcp:
    image: roam-research-mcp
    build: .
    container_name: roam-mcp
    ports:
      - "3010:3000"
      - "8047:8087"
      - "8048:8088"
    env_file:
      - .env
    networks:
      - n8n-net

  # n8n:
  #   image: n8nio/n8n:latest
  #   container_name: n8n
  #   ports:
  #     - "5678:5678"
  #   volumes:
  #     - ~/.n8n:/home/node/.n8n
  #   environment:
  #     - N8N_BASIC_AUTH_ACTIVE=true
  #     - N8N_BASIC_AUTH_USER=admin
  #     - N8N_BASIC_AUTH_PASSWORD=hallelujah
  #   depends_on:
  #     - roam-mcp
  #   networks:
  #     - n8n-net

networks:
  n8n-net:
    driver: bridge
```

--------------------------------------------------------------------------------
/src/tools/types/index.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph } from '@roam-research/roam-api-sdk';
import type { RoamBlock } from '../../types/roam.js';

export interface ToolHandlerDependencies {
  graph: Graph;
}

export interface SearchResult {
  block_uid: string;
  content: string;
  page_title?: string;
}

export interface BlockUpdateResult {
  block_uid: string;
  content: string;
  success: boolean;
  error?: string;
}

export interface BlockUpdate {
  block_uid: string;
  content?: string;
  transform?: {
    find: string;
    replace: string;
    global?: boolean;
  };
}

export interface OutlineItem {
  text: string | undefined;
  level: number;
  heading?: number;
  children_view_type?: 'bullet' | 'document' | 'numbered';
}

export interface NestedBlock {
  uid: string;
  text: string;
  level: number;
  order: number;
  children?: NestedBlock[];
}

export { RoamBlock };

```

--------------------------------------------------------------------------------
/src/search/types.ts:
--------------------------------------------------------------------------------

```typescript
import type { Graph } from '@roam-research/roam-api-sdk';

export interface SearchResult {
  success: boolean;
  matches: Array<{
    block_uid: string;
    content: string;
    page_title?: string;
    [key: string]: any;  // Additional context-specific fields
  }>;
  message: string;
  total_count?: number; // Added for total count of matches
}

export interface SearchHandler {
  execute(): Promise<SearchResult>;
}

// Tag Search Types
export interface TagSearchParams {
  primary_tag: string;
  page_title_uid?: string;
  near_tag?: string;
  exclude_tag?: string;
  case_sensitive?: boolean;
  limit?: number;
  offset?: number;
}

// Text Search Types
export interface TextSearchParams {
  text: string;
  page_title_uid?: string;
  case_sensitive?: boolean;
  limit?: number;
  offset?: number;
}

// Base class for all search handlers
export abstract class BaseSearchHandler implements SearchHandler {
  constructor(protected graph: Graph) { }
  abstract execute(): Promise<SearchResult>;
}

```

--------------------------------------------------------------------------------
/src/tools/operations/search/types.ts:
--------------------------------------------------------------------------------

```typescript
import type { SearchResult } from '../../types/index.js';

// Base search parameters
export interface BaseSearchParams {
  page_title_uid?: string;
}

// Datomic search parameters
export interface DatomicSearchParams {
  query: string;
  inputs?: unknown[];
}

// Tag search parameters
export interface TagSearchParams extends BaseSearchParams {
  primary_tag: string;
  near_tag?: string;
}

// Block reference search parameters
export interface BlockRefSearchParams extends BaseSearchParams {
  block_uid?: string;
}

// Hierarchy search parameters
export interface HierarchySearchParams extends BaseSearchParams {
  parent_uid?: string;
  child_uid?: string;
  max_depth?: number;
}

// Text search parameters
export interface TextSearchParams extends BaseSearchParams {
  text: string;
}

// Status search parameters
export interface StatusSearchParams extends BaseSearchParams {
  status: 'TODO' | 'DONE';
}

// Common search result type
export interface SearchHandlerResult {
  success: boolean;
  matches: SearchResult[];
  message: string;
}

```

--------------------------------------------------------------------------------
/src/utils/net.ts:
--------------------------------------------------------------------------------

```typescript
import { createServer } from 'node:net';

/**
 * Checks if a given port is currently in use.
 * @param port The port to check.
 * @returns A promise that resolves to true if the port is in use, and false otherwise.
 */
export function isPortInUse(port: number): Promise<boolean> {
  return new Promise((resolve) => {
    const server = createServer();

    server.once('error', (err: NodeJS.ErrnoException) => {
      if (err.code === 'EADDRINUSE') {
        resolve(true);
      } else {
        // Handle other errors if necessary, but for this check, we assume other errors mean the port is available.
        resolve(false);
      }
    });

    server.once('listening', () => {
      server.close();
      resolve(false);
    });

    server.listen(port);
  });
}

/**
 * Finds an available port, starting from a given port and incrementing by a specified amount.
 * @param startPort The port to start checking from.
 * @param incrementBy The amount to increment the port by if it's in use. Defaults to 2.
 * @returns A promise that resolves to an available port number.
 */
export async function findAvailablePort(startPort: number, incrementBy = 2): Promise<number> {
  let port = startPort;
  while (await isPortInUse(port)) {
    port += incrementBy;
  }
  return port;
}

```

--------------------------------------------------------------------------------
/src/tools/operations/batch.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph, batchActions as roamBatchActions } from '@roam-research/roam-api-sdk';
import { RoamBatchAction } from '../../types/roam.js';

export class BatchOperations {
  constructor(private graph: Graph) {}

  async processBatch(actions: any[]): Promise<any> {
    const batchActions: RoamBatchAction[] = actions.map(action => {
      const { action: actionType, ...rest } = action;
      const roamAction: any = { action: actionType };

      if (rest.location) {
        roamAction.location = {
          'parent-uid': rest.location['parent-uid'],
          order: rest.location.order,
        };
      }

      const block: any = {};
      if (rest.string) block.string = rest.string;
      if (rest.uid) block.uid = rest.uid;
      if (rest.open !== undefined) block.open = rest.open;
      if (rest.heading !== undefined && rest.heading !== null && rest.heading !== 0) {
        block.heading = rest.heading;
      }
      if (rest['text-align']) block['text-align'] = rest['text-align'];
      if (rest['children-view-type']) block['children-view-type'] = rest['children-view-type'];

      if (Object.keys(block).length > 0) {
        roamAction.block = block;
      }

      return roamAction;
    });

    return await roamBatchActions(this.graph, {actions: batchActions});
  }
}

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Use an official Node.js runtime as a parent image for building
FROM node:lts-alpine AS builder

# Set the working directory in the container
WORKDIR /app

# Copy package.json and package-lock.json to the working directory
COPY package.json ./
COPY package-lock.json ./

# Install development and production dependencies
RUN --mount=type=cache,target=/root/.npm npm install

# Copy source code and TypeScript configuration
COPY src /app/src
COPY tsconfig.json /app/tsconfig.json
COPY Roam_Markdown_Cheatsheet.md /app/Roam_Markdown_Cheatsheet.md

# Build the TypeScript project
RUN npm run build


# Use a minimal Node.js runtime as the base for the release image
FROM node:lts-alpine AS release

# Set environment to production
ENV NODE_ENV=production

# Set the working directory
WORKDIR /app

# Copy only the built application (from /app/build) and production dependencies from the builder stage
COPY --from=builder /app/build /app/build
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/package-lock.json /app/package-lock.json

# Install only production dependencies (based on package-lock.json)
# This keeps the final image small and secure by omitting development dependencies
RUN npm ci --ignore-scripts --omit-dev

# Expose the ports the app runs on (3000 for standard, 8087 for SSE, 8088 for HTTP Stream)
EXPOSE 3000
EXPOSE 8087
EXPOSE 8088

# Run the application
ENTRYPOINT ["node", "build/index.js"]

```

--------------------------------------------------------------------------------
/src/tools/helpers/refs.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph, q } from '@roam-research/roam-api-sdk';

/**
 * Collects all referenced block UIDs from text
 */
export const collectRefs = (text: string, depth: number = 0, refs: Set<string> = new Set()): Set<string> => {
  if (depth >= 4) return refs; // Max recursion depth
  
  const refRegex = /\(\(([a-zA-Z0-9_-]+)\)\)/g;
  let match;
  
  while ((match = refRegex.exec(text)) !== null) {
    const [_, uid] = match;
    refs.add(uid);
  }
  
  return refs;
};

/**
 * Resolves block references in text by replacing them with their content
 */
export const resolveRefs = async (graph: Graph, text: string, depth: number = 0): Promise<string> => {
  if (depth >= 4) return text; // Max recursion depth
  
  const refs = collectRefs(text, depth);
  if (refs.size === 0) return text;

  // Get referenced block contents
  const refQuery = `[:find ?uid ?string
                    :in $ [?uid ...]
                    :where [?b :block/uid ?uid]
                          [?b :block/string ?string]]`;
  const refResults = await q(graph, refQuery, [Array.from(refs)]) as [string, string][];
  
  // Create lookup map of uid -> string
  const refMap = new Map<string, string>();
  refResults.forEach(([uid, string]) => {
    refMap.set(uid, string);
  });
  
  // Replace references with their content
  let resolvedText = text;
  for (const uid of refs) {
    const refContent = refMap.get(uid);
    if (refContent) {
      // Recursively resolve nested references
      const resolvedContent = await resolveRefs(graph, refContent, depth + 1);
      resolvedText = resolvedText.replace(
        new RegExp(`\\(\\(${uid}\\)\\)`, 'g'),
        resolvedContent
      );
    }
  }
  
  return resolvedText;
};

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "roam-research-mcp",
  "version": "0.36.3",
  "description": "A Model Context Protocol (MCP) server for Roam Research API integration",
  "private": false,
  "repository": {
    "type": "git",
    "url": "git+https://github.com/2b3pro/roam-research-mcp.git"
  },
  "keywords": [
    "mcp",
    "roam-research",
    "api",
    "claude",
    "model-context-protocol"
  ],
  "author": "Ian Shen / 2B3 PRODUCTIONS LLC",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/2b3pro/roam-research-mcp/issues"
  },
  "homepage": "https://github.com/2b3pro/roam-research-mcp#readme",
  "type": "module",
  "bin": {
    "roam-research-mcp": "./build/index.js"
  },
  "files": [
    "build"
  ],
  "scripts": {
    "build": "echo \"Using custom instructions: .roam/${CUSTOM_INSTRUCTIONS_PREFIX}custom-instructions.md\" && tsc && cat Roam_Markdown_Cheatsheet.md .roam/${CUSTOM_INSTRUCTIONS_PREFIX}custom-instructions.md > build/Roam_Markdown_Cheatsheet.md && chmod 755 build/index.js",
    "clean": "rm -rf build",
    "watch": "tsc --watch",
    "inspector": "npx @modelcontextprotocol/inspector build/index.js",
    "start": "node build/index.js",
    "prepublishOnly": "npm run clean && npm run build",
    "release:patch": "npm version patch && git push origin v$(node -p \"require('./package.json').version\")",
    "release:minor": "npm version minor && git push origin v$(node -p \"require('./package.json').version\")",
    "release:major": "npm version major && git push origin v$(node -p \"require('./package.json').version\")"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.13.2",
    "@roam-research/roam-api-sdk": "^0.10.0",
    "dotenv": "^16.4.7"
  },
  "devDependencies": {
    "@types/node": "^20.11.24",
    "ts-node": "^10.9.2",
    "typescript": "^5.3.3"
  }
}
```

--------------------------------------------------------------------------------
/src/tools/operations/todos.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph, q, createBlock, createPage, batchActions } from '@roam-research/roam-api-sdk';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { formatRoamDate } from '../../utils/helpers.js';

export class TodoOperations {
  constructor(private graph: Graph) {}

  async addTodos(todos: string[]): Promise<{ success: boolean }> {
    if (!Array.isArray(todos) || todos.length === 0) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        'todos must be a non-empty array'
      );
    }

    // Get today's date
    const today = new Date();
    const dateStr = formatRoamDate(today);
    
    // Try to find today's page
    const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
    const findResults = await q(this.graph, findQuery, [dateStr]) as [string][];
    
    let targetPageUid: string;
    
    if (findResults && findResults.length > 0) {
      targetPageUid = findResults[0][0];
    } else {
      // Create today's page if it doesn't exist
      try {
        await createPage(this.graph, {
          action: 'create-page',
          page: { title: dateStr }
        });

        // Get the new page's UID
        const results = await q(this.graph, findQuery, [dateStr]) as [string][];
        if (!results || results.length === 0) {
          throw new Error('Could not find created today\'s page');
        }
        targetPageUid = results[0][0];
      } catch (error) {
        throw new Error('Failed to create today\'s page');
      }
    }

    const todo_tag = "{{TODO}}";
    const actions = todos.map((todo, index) => ({
      action: 'create-block',
      location: {
        'parent-uid': targetPageUid,
        order: index
      },
      block: {
        string: `${todo_tag} ${todo}`
      }
    }));

    const result = await batchActions(this.graph, {
      action: 'batch-actions',
      actions
    });

    if (!result) {
      throw new Error('Failed to create todo blocks');
    }
    
    return { success: true };
  }
}

```

--------------------------------------------------------------------------------
/src/config/environment.ts:
--------------------------------------------------------------------------------

```typescript
import * as dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { existsSync } from 'fs';

// Get the project root from the script path
const scriptPath = process.argv[1];  // Full path to the running script
const projectRoot = dirname(dirname(scriptPath));  // Go up two levels from build/index.js

// Try to load .env from project root
const envPath = join(projectRoot, '.env');
if (existsSync(envPath)) {
  dotenv.config({ path: envPath });
}

// Required environment variables
const API_TOKEN = process.env.ROAM_API_TOKEN as string;
const GRAPH_NAME = process.env.ROAM_GRAPH_NAME as string;

// Validate environment variables
if (!API_TOKEN || !GRAPH_NAME) {
  const missingVars = [];
  if (!API_TOKEN) missingVars.push('ROAM_API_TOKEN');
  if (!GRAPH_NAME) missingVars.push('ROAM_GRAPH_NAME');

  throw new Error(
    `Missing required environment variables: ${missingVars.join(', ')}\n\n` +
    'Please configure these variables either:\n' +
    '1. In your MCP settings file:\n' +
    '   - For Cline: ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json\n' +
    '   - For Claude: ~/Library/Application Support/Claude/claude_desktop_config.json\n\n' +
    '   Example configuration:\n' +
    '   {\n' +
    '     "mcpServers": {\n' +
    '       "roam-research": {\n' +
    '         "command": "node",\n' +
    '         "args": ["/path/to/roam-research-mcp/build/index.js"],\n' +
    '         "env": {\n' +
    '           "ROAM_API_TOKEN": "your-api-token",\n' +
    '           "ROAM_GRAPH_NAME": "your-graph-name"\n' +
    '         }\n' +
    '       }\n' +
    '     }\n' +
    '   }\n\n' +
    '2. Or in a .env file in the roam-research directory:\n' +
    '   ROAM_API_TOKEN=your-api-token\n' +
    '   ROAM_GRAPH_NAME=your-graph-name'
  );
}

const HTTP_STREAM_PORT = process.env.HTTP_STREAM_PORT || '8088'; // Default to 8088
const SSE_PORT = process.env.SSE_PORT || '8087'; // Default to 8087
const CORS_ORIGIN = process.env.CORS_ORIGIN || 'http://localhost:5678';

export { API_TOKEN, GRAPH_NAME, HTTP_STREAM_PORT, SSE_PORT, CORS_ORIGIN };

```

--------------------------------------------------------------------------------
/src/search/status-search.ts:
--------------------------------------------------------------------------------

```typescript
import { q } from '@roam-research/roam-api-sdk';
import type { Graph } from '@roam-research/roam-api-sdk';
import { BaseSearchHandler, SearchResult } from './types.js';
import { SearchUtils } from './utils.js';
import { resolveRefs } from '../tools/helpers/refs.js';

export interface StatusSearchParams {
  status: 'TODO' | 'DONE';
  page_title_uid?: string;
}

export class StatusSearchHandler extends BaseSearchHandler {
  constructor(
    graph: Graph,
    private params: StatusSearchParams
  ) {
    super(graph);
  }

  async execute(): Promise<SearchResult> {
    const { status, page_title_uid } = this.params;

    // Get target page UID if provided
    let targetPageUid: string | undefined;
    if (page_title_uid) {
      targetPageUid = await SearchUtils.findPageByTitleOrUid(this.graph, page_title_uid);
    }

    // Build query based on whether we're searching in a specific page
    let queryStr: string;
    let queryParams: any[];

    if (targetPageUid) {
      queryStr = `[:find ?block-uid ?block-str
                  :in $ ?status ?page-uid
                  :where [?p :block/uid ?page-uid]
                         [?b :block/page ?p]
         [?b :block/string ?block-str]
         [?b :block/uid ?block-uid]
         [(clojure.string/includes? ?block-str (str "{{[[" ?status "]]}}"))]]`;
      queryParams = [status, targetPageUid];
    } else {
      queryStr = `[:find ?block-uid ?block-str ?page-title
                  :in $ ?status
                  :where [?b :block/string ?block-str]
                         [?b :block/uid ?block-uid]
                         [?b :block/page ?p]
                         [?p :node/title ?page-title]
                         [(clojure.string/includes? ?block-str (str "{{[[" ?status "]]}}"))]]`;
      queryParams = [status];
    }

    const rawResults = await q(this.graph, queryStr, queryParams) as [string, string, string?][];
    
    // Resolve block references in content
    const resolvedResults = await Promise.all(
      rawResults.map(async ([uid, content, pageTitle]) => {
        const resolvedContent = await resolveRefs(this.graph, content);
        return [uid, resolvedContent, pageTitle] as [string, string, string?];
      })
    );
    
    return SearchUtils.formatSearchResults(resolvedResults, `with status ${status}`, !targetPageUid);
  }
}

```

--------------------------------------------------------------------------------
/src/tools/operations/search/handlers.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph } from '@roam-research/roam-api-sdk';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { TagSearchHandler, BlockRefSearchHandler, HierarchySearchHandler, TextSearchHandler, DatomicSearchHandler, StatusSearchHandler } from '../../../search/index.js';
import type {
  TagSearchParams,
  BlockRefSearchParams,
  HierarchySearchParams,
  TextSearchParams,
  SearchHandlerResult,
  DatomicSearchParams,
  StatusSearchParams
} from './types.js';

// Base class for all search handlers
export abstract class BaseSearchHandler {
  constructor(protected graph: Graph) {}
  abstract execute(): Promise<SearchHandlerResult>;
}

// Tag search handler
export class TagSearchHandlerImpl extends BaseSearchHandler {
  constructor(graph: Graph, private params: TagSearchParams) {
    super(graph);
  }

  async execute() {
    const handler = new TagSearchHandler(this.graph, this.params);
    return handler.execute();
  }
}

// Block reference search handler
export class BlockRefSearchHandlerImpl extends BaseSearchHandler {
  constructor(graph: Graph, private params: BlockRefSearchParams) {
    super(graph);
  }

  async execute() {
    const handler = new BlockRefSearchHandler(this.graph, this.params);
    return handler.execute();
  }
}

// Hierarchy search handler
export class HierarchySearchHandlerImpl extends BaseSearchHandler {
  constructor(graph: Graph, private params: HierarchySearchParams) {
    super(graph);
  }

  async execute() {
    const handler = new HierarchySearchHandler(this.graph, this.params);
    return handler.execute();
  }
}

// Text search handler
export class TextSearchHandlerImpl extends BaseSearchHandler {
  constructor(graph: Graph, private params: TextSearchParams) {
    super(graph);
  }

  async execute() {
    const handler = new TextSearchHandler(this.graph, this.params);
    return handler.execute();
  }
}

// Status search handler
export class StatusSearchHandlerImpl extends BaseSearchHandler {
  constructor(graph: Graph, private params: StatusSearchParams) {
    super(graph);
  }

  async execute() {
    const handler = new StatusSearchHandler(this.graph, this.params);
    return handler.execute();
  }
}

// Datomic query handler
export class DatomicSearchHandlerImpl extends BaseSearchHandler {
  constructor(graph: Graph, private params: DatomicSearchParams) {
    super(graph);
  }

  async execute() {
    const handler = new DatomicSearchHandler(this.graph, this.params);
    return handler.execute();
  }
}

```

--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------

```typescript
declare module '@roam-research/roam-api-sdk' {
  interface Graph {
    token: string;
    graph: string;
  }

  interface RoamBlockLocation {
    'parent-uid': string;
    order: number | string;
  }

  interface RoamBlock {
    string: string;
    uid?: string;
    open?: boolean;
    heading?: number;
    'text-align'?: boolean;
    'children-view-type'?: string;
  }

  interface RoamCreateBlock {
    action?: 'create-block';
    location: RoamBlockLocation;
    block: RoamBlock;
  }

  export function initializeGraph(config: { token: string; graph: string }): Graph;
  
  export function q(
    graph: Graph,
    query: string,
    inputs: any[]
  ): Promise<any[]>;

  interface RoamCreatePage {
    action?: 'create-page';
    page: {
      title: string;
      uid?: string;
      'children-view-type'?: string;
    };
  }

  export function createPage(
    graph: Graph,
    options: RoamCreatePage
  ): Promise<boolean>;

  export function createBlock(
    graph: Graph,
    options: RoamCreateBlock
  ): Promise<boolean>;

  interface RoamUpdateBlock {
    action?: 'update-block';
    block: {
      string?: string;
      uid: string;
      open?: boolean;
      heading?: number;
      'text-align'?: boolean;
      'children-view-type'?: string;
    };
  }

  export function updateBlock(
    graph: Graph,
    options: RoamUpdateBlock
  ): Promise<boolean>;

  export function deleteBlock(
    graph: Graph,
    options: { uid: string }
  ): Promise<void>;

  export function pull(
    graph: Graph,
    pattern: string,
    eid: string
  ): Promise<any>;

  export function pull_many(
    graph: Graph,
    pattern: string,
    eids: string
  ): Promise<any>;

  interface RoamMoveBlock {
    action?: 'move-block';
    location: RoamBlockLocation;
    block: {
      uid: RoamBlock['uid'];
    };
  }

  export function moveBlock(
    graph: Graph,
    options: RoamMoveBlock
  ): Promise<boolean>;

  interface RoamDeletePage {
    action?: 'delete-page';
    page: {
      uid: string;
    };
  }

  export function deletePage(
    graph: Graph,
    options: RoamDeletePage
  ): Promise<boolean>;

  interface RoamDeleteBlock {
    action?: 'delete-block';
    block: {
      uid: string;
    };
  }

  export function deleteBlock(
    graph: Graph,
    options: RoamDeleteBlock
  ): Promise<boolean>;

  interface RoamBatchActions {
    action?: 'batch-actions';
    actions: Array<
      | RoamDeletePage
      | RoamUpdatePage
      | RoamCreatePage
      | RoamDeleteBlock
      | RoamUpdateBlock
      | RoamMoveBlock
      | RoamCreateBlock
    >;
  }

  export function batchActions(
    graph: Graph,
    options: RoamBatchActions
  ): Promise<any>;
}

```

--------------------------------------------------------------------------------
/src/search/datomic-search.ts:
--------------------------------------------------------------------------------

```typescript
import { q } from '@roam-research/roam-api-sdk';
import type { Graph } from '@roam-research/roam-api-sdk';
import { BaseSearchHandler, SearchResult } from './types.js';
// import { resolveRefs } from '../helpers/refs.js';

export interface DatomicSearchParams {
  query: string;
  inputs?: unknown[];
  regexFilter?: string;
  regexFlags?: string;
  regexTargetField?: string[];
}

export class DatomicSearchHandler extends BaseSearchHandler {
  constructor(
    graph: Graph,
    private params: DatomicSearchParams
  ) {
    super(graph);
  }

  async execute(): Promise<SearchResult> {
    try {
      // Execute the datomic query using the Roam API
      let results = await q(this.graph, this.params.query, this.params.inputs || []) as unknown[];

      if (this.params.regexFilter) {
        let regex: RegExp;
        try {
          regex = new RegExp(this.params.regexFilter, this.params.regexFlags);
        } catch (e) {
          return {
            success: false,
            matches: [],
            message: `Invalid regex filter provided: ${e instanceof Error ? e.message : String(e)}`
          };
        }

        results = results.filter(result => {
          if (this.params.regexTargetField && this.params.regexTargetField.length > 0) {
            for (const field of this.params.regexTargetField) {
              // Access nested fields if path is provided (e.g., "prop.nested")
              const fieldPath = field.split('.');
              let value: any = result;
              for (const part of fieldPath) {
                if (typeof value === 'object' && value !== null && part in value) {
                  value = value[part];
                } else {
                  value = undefined; // Field not found
                  break;
                }
              }
              if (typeof value === 'string' && regex.test(value)) {
                return true;
              }
            }
            return false;
          } else {
            // Default to stringifying the whole result if no target field is specified
            return regex.test(JSON.stringify(result));
          }
        });
      }

      return {
        success: true,
        matches: results.map(result => ({
          content: JSON.stringify(result),
          block_uid: '', // Datomic queries may not always return block UIDs
          page_title: '' // Datomic queries may not always return page titles
        })),
        message: `Query executed successfully. Found ${results.length} results.`
      };
    } catch (error) {
      return {
        success: false,
        matches: [],
        message: `Failed to execute query: ${error instanceof Error ? error.message : String(error)}`
      };
    }
  }
}

```

--------------------------------------------------------------------------------
/src/tools/helpers/text.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Capitalizes each word in a string
 */
import { q } from '@roam-research/roam-api-sdk';
import type { Graph } from '@roam-research/roam-api-sdk';

/**
 * Capitalizes each word in a string
 */
export const capitalizeWords = (str: string): string => {
  return str.split(' ').map(word =>
    word.charAt(0).toUpperCase() + word.slice(1)
  ).join(' ');
};

/**
 * Retrieves a block's UID based on its exact text content.
 * This function is intended for internal use by other MCP tools.
 * @param graph The Roam graph instance.
 * @param blockText The exact text content of the block to find.
 * @returns The UID of the block if found, otherwise null.
 */
export const getBlockUidByText = async (graph: Graph, blockText: string): Promise<string | null> => {
  const query = `[:find ?uid .
                  :in $ ?blockString
                  :where [?b :block/string ?blockString]
                         [?b :block/uid ?uid]]`;
  const result = await q(graph, query, [blockText]) as [string][] | null;
  return result && result.length > 0 ? result[0][0] : null;
};

/**
 * Retrieves all UIDs nested under a given block_uid or block_text (exact match).
 * This function is intended for internal use by other MCP tools.
 * @param graph The Roam graph instance.
 * @param rootIdentifier The UID or exact text content of the root block.
 * @returns An array of UIDs of all descendant blocks, including the root block's UID.
 */
export const getNestedUids = async (graph: Graph, rootIdentifier: string): Promise<string[]> => {
  let rootUid: string | null = rootIdentifier;

  // If the rootIdentifier is not a UID (simple check for 9 alphanumeric characters), try to resolve it as block text
  if (!rootIdentifier.match(/^[a-zA-Z0-9]{9}$/)) {
    rootUid = await getBlockUidByText(graph, rootIdentifier);
  }

  if (!rootUid) {
    return []; // No root block found
  }

  const query = `[:find ?child-uid
                  :in $ ?root-uid
                  :where
                    [?root-block :block/uid ?root-uid]
                    [?root-block :block/children ?child-block]
                    [?child-block :block/uid ?child-uid]]`;

  const results = await q(graph, query, [rootUid]) as [string][];
  return results.map(r => r[0]);
};

/**
 * Retrieves all UIDs nested under a given block_text (exact match).
 * This function is intended for internal use by other MCP tools.
 * It strictly requires an exact text match for the root block.
 * @param graph The Roam graph instance.
 * @param blockText The exact text content of the root block.
 * @returns An array of UIDs of all descendant blocks, including the root block's UID.
 */
export const getNestedUidsByText = async (graph: Graph, blockText: string): Promise<string[]> => {
  const rootUid = await getBlockUidByText(graph, blockText);
  if (!rootUid) {
    return []; // No root block found with exact text match
  }
  return getNestedUids(graph, rootUid);
};

```

--------------------------------------------------------------------------------
/Roam Import JSON Schema.md:
--------------------------------------------------------------------------------

```markdown
- **Description**
    - A schema for JSON import into Roam. A file being uploaded must be an array of pages. Keys not defined in the schema will be ignored.
- **Objects**
    - Page
        - description:: An object representing a page. The only required key is the title
        - keys::
            - title
            - children
            - create-time
            - edit-time
            - edit-user
    - Block
        - description:: An object representing a block. The only required key is the string
        - keys::
            - string
            - uid
            - children
            - create-time
            - edit-time
            - edit-user
            - heading
            - text-align
- **Keys**
    - title
        - description:: The title of a page. The string is unique across a user's database. If importing a title that is already used, it will merge with the already existing content.
        - type:: string
    - string
        - description:: The string of a block
        - type:: string
    - uid
        - description:: The unique identifier of a block. Use this if you want to preserve block references. If the unique identifier conflicts with other uid's in the database or the import, the import will fail. Be careful using this attribute. Standard Roam uid's are 9 characters, but can be any length. Roam uses naniod.js to generate these
        - type:: string
    - children
        - description:: An array of blocks, the order is implicit from the order of the array
        - type:: array of Blocks
    - create-time
        - description:: The time the object was created, measured in ms since unix epoch. If not supplied, the create-time of the object will be filled in by either the edit-time, or now.
        - type:: integer
            - Epoch time in milliseconds (13-digit numbers)
    - edit-time
        - description:: The time the object was last edited, measured in ms since unix epoch. If not supplied, the edit-time of the object will be filled in by either the create-time, or now.
        - type:: integer
    - edit-user
        - description:: The user who last edited the object. 
        - type:: json object of the format `{":user/uid" "ROAM-USER-UID"}`
    - heading
        - description:: Determines what heading tag to wrap the block in, default is no heading (0)
        - type:: integer, 0 | 1 | 2 | 3
            - For level of heading, 0 being no heading (the default) 1 heading h1, etc
    - text-align
        - description:: The text-align style for a block
        - type:: string, "left" | "center" | "right" | "justify"
            - By default is left (as determined by the browser defaults)
- **Example**
    - ```javascript
      [{:title        "December 10th 2018"
        :create-email "[email protected]"
        :create-time  1576025237000
        :children     [{:string   "[[Meeting]] with [[Tim]]"
                        :children [{:string "Meeting went well"}]}
                       {:string "[[Call]] with [[John]]"}]}
       {:title    "December 11th 2018"}]
      ```
    - More (better) examples can be found by exporting roam to json

```

--------------------------------------------------------------------------------
/src/tools/operations/block-retrieval.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph, q } from '@roam-research/roam-api-sdk';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { RoamBlock } from '../../types/roam.js';

export class BlockRetrievalOperations {
  constructor(private graph: Graph) { }

  async fetchBlockWithChildren(block_uid_raw: string, depth: number = 4): Promise<RoamBlock | null> {
    if (!block_uid_raw) {
      throw new McpError(ErrorCode.InvalidRequest, 'block_uid is required.');
    }

    const block_uid = block_uid_raw.replace(/^\(\((.*)\)\)$/, '$1');

    const fetchChildren = async (parentUids: string[], currentDepth: number): Promise<Record<string, RoamBlock[]>> => {
      if (currentDepth >= depth || parentUids.length === 0) {
        return {};
      }

      const childrenQuery = `[:find ?parentUid ?childUid ?childString ?childOrder ?childHeading
                              :in $ [?parentUid ...]
                              :where [?parent :block/uid ?parentUid]
                                     [?parent :block/children ?child]
                                     [?child :block/uid ?childUid]
                                     [?child :block/string ?childString]
                                     [?child :block/order ?childOrder]
                                     [(get-else $ ?child :block/heading 0) ?childHeading]]`;

      const childrenResults = await q(this.graph, childrenQuery, [parentUids]) as [string, string, string, number, number | null][];

      const childrenByParent: Record<string, RoamBlock[]> = {};
      const allChildUids: string[] = [];

      for (const [parentUid, childUid, childString, childOrder, childHeading] of childrenResults) {
        if (!childrenByParent[parentUid]) {
          childrenByParent[parentUid] = [];
        }
        childrenByParent[parentUid].push({
          uid: childUid,
          string: childString,
          order: childOrder,
          heading: childHeading || undefined,
          children: [],
        });
        allChildUids.push(childUid);
      }

      const grandChildren = await fetchChildren(allChildUids, currentDepth + 1);

      for (const parentUid in childrenByParent) {
        for (const child of childrenByParent[parentUid]) {
          child.children = grandChildren[child.uid] || [];
        }
        childrenByParent[parentUid].sort((a, b) => a.order - b.order);
      }

      return childrenByParent;
    };

    try {
      const rootBlockQuery = `[:find ?string ?order ?heading
                               :in $ ?blockUid
                               :where [?b :block/uid ?blockUid]
                                      [?b :block/string ?string]
                                      [?b :block/order ?order]
                                      [(get-else $ ?b :block/heading 0) ?heading]]`;
      const rootBlockResult = await q(this.graph, rootBlockQuery, [block_uid]) as [string, number, number | null] | null;

      if (!rootBlockResult) {
        return null;
      }

      const [rootString, rootOrder, rootHeading] = rootBlockResult;
      const childrenMap = await fetchChildren([block_uid], 0);

      return {
        uid: block_uid,
        string: rootString,
        order: rootOrder,
        heading: rootHeading || undefined,
        children: childrenMap[block_uid] || [],
      };
    } catch (error) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to fetch block with children: ${error instanceof Error ? error.message : String(error)}`
      );
    }
  }
}

```

--------------------------------------------------------------------------------
/src/search/block-ref-search.ts:
--------------------------------------------------------------------------------

```typescript
import { q } from '@roam-research/roam-api-sdk';
import type { Graph } from '@roam-research/roam-api-sdk';
import { BaseSearchHandler, SearchResult } from './types.js';
import { SearchUtils } from './utils.js';
import { resolveRefs } from '../tools/helpers/refs.js';

export interface BlockRefSearchParams {
  block_uid?: string;
  page_title_uid?: string;
}

export class BlockRefSearchHandler extends BaseSearchHandler {
  constructor(
    graph: Graph,
    private params: BlockRefSearchParams
  ) {
    super(graph);
  }

  async execute(): Promise<SearchResult> {
    const { block_uid, page_title_uid } = this.params;

    // Get target page UID if provided
    let targetPageUid: string | undefined;
    if (page_title_uid) {
      targetPageUid = await SearchUtils.findPageByTitleOrUid(this.graph, page_title_uid);
    }

    // Build query based on whether we're searching for references to a specific block
    // or all block references within a page/graph
    let queryStr: string;
    let queryParams: any[];

    if (block_uid) {
      // Search for references to a specific block
      if (targetPageUid) {
        queryStr = `[:find ?block-uid ?block-str
                    :in $ ?ref-uid ?page-uid
                    :where [?p :block/uid ?page-uid]
                           [?b :block/page ?p]
                           [?b :block/string ?block-str]
                           [?b :block/uid ?block-uid]
                           [(clojure.string/includes? ?block-str ?ref-uid)]]`;
        queryParams = [`((${block_uid}))`, targetPageUid];
      } else {
        queryStr = `[:find ?block-uid ?block-str ?page-title
                    :in $ ?ref-uid
                    :where [?b :block/string ?block-str]
                           [?b :block/uid ?block-uid]
                           [?b :block/page ?p]
                           [?p :node/title ?page-title]
                           [(clojure.string/includes? ?block-str ?ref-uid)]]`;
        queryParams = [`((${block_uid}))`];
      }
    } else {
      // Search for any block references
      if (targetPageUid) {
        queryStr = `[:find ?block-uid ?block-str
                    :in $ ?page-uid
                    :where [?p :block/uid ?page-uid]
                           [?b :block/page ?p]
                           [?b :block/string ?block-str]
                           [?b :block/uid ?block-uid]
                           [(re-find #"\\(\\([^)]+\\)\\)" ?block-str)]]`;
        queryParams = [targetPageUid];
      } else {
        queryStr = `[:find ?block-uid ?block-str ?page-title
                    :where [?b :block/string ?block-str]
                           [?b :block/uid ?block-uid]
                           [?b :block/page ?p]
                           [?p :node/title ?page-title]
                           [(re-find #"\\(\\([^)]+\\)\\)" ?block-str)]]`;
        queryParams = [];
      }
    }

    const rawResults = await q(this.graph, queryStr, queryParams) as [string, string, string?][];
    
    // Resolve block references in content
    const resolvedResults = await Promise.all(
      rawResults.map(async ([uid, content, pageTitle]) => {
        const resolvedContent = await resolveRefs(this.graph, content);
        return [uid, resolvedContent, pageTitle] as [string, string, string?];
      })
    );
    
    const searchDescription = block_uid 
      ? `referencing block ((${block_uid}))`
      : 'containing block references';
      
    return SearchUtils.formatSearchResults(resolvedResults, searchDescription, !targetPageUid);
  }
}

```

--------------------------------------------------------------------------------
/src/search/text-search.ts:
--------------------------------------------------------------------------------

```typescript
import { q } from '@roam-research/roam-api-sdk';
import type { Graph } from '@roam-research/roam-api-sdk';
import { BaseSearchHandler, SearchResult, TextSearchParams } from './types.js';
import { SearchUtils } from './utils.js';
import { resolveRefs } from '../tools/helpers/refs.js';

export class TextSearchHandler extends BaseSearchHandler {
  constructor(
    graph: Graph,
    private params: TextSearchParams
  ) {
    super(graph);
  }

  async execute(): Promise<SearchResult> {
    const { text, page_title_uid, case_sensitive = false, limit = -1, offset = 0 } = this.params;

    // Get target page UID if provided for scoped search
    let targetPageUid: string | undefined;
    if (page_title_uid) {
      targetPageUid = await SearchUtils.findPageByTitleOrUid(this.graph, page_title_uid);
    }

    const searchTerms: string[] = [];
    if (case_sensitive) {
      searchTerms.push(text);
    } else {
      searchTerms.push(text);
      // Add capitalized version (e.g., "Hypnosis")
      searchTerms.push(text.charAt(0).toUpperCase() + text.slice(1));
      // Add all caps version (e.g., "HYPNOSIS")
      searchTerms.push(text.toUpperCase());
      // Add all lowercase version (e.g., "hypnosis")
      searchTerms.push(text.toLowerCase());
    }

    const whereClauses = searchTerms.map(term => `[(clojure.string/includes? ?block-str "${term}")]`).join(' ');

    let queryStr: string;
    let queryParams: (string | number)[] = [];
    let queryLimit = limit === -1 ? '' : `:limit ${limit}`;
    let queryOffset = offset === 0 ? '' : `:offset ${offset}`;
    let queryOrder = `:order ?page-edit-time asc ?block-uid asc`; // Sort by page edit time, then block UID


    let baseQueryWhereClauses = `
                    [?b :block/string ?block-str]
                    (or ${whereClauses})
                    [?b :block/uid ?block-uid]
                    [?b :block/page ?p]
                    [?p :node/title ?page-title]
                    [?p :edit/time ?page-edit-time]`; // Fetch page edit time for sorting

    if (targetPageUid) {
      queryStr = `[:find ?block-uid ?block-str ?page-title
                    :in $ ?page-uid ${queryLimit} ${queryOffset} ${queryOrder}
                    :where
                    ${baseQueryWhereClauses}
                    [?p :block/uid ?page-uid]]`;
      queryParams = [targetPageUid];
    } else {
      queryStr = `[:find ?block-uid ?block-str ?page-title
                    :in $ ${queryLimit} ${queryOffset} ${queryOrder}
                    :where
                    ${baseQueryWhereClauses}]`;
    }

    const rawResults = await q(this.graph, queryStr, queryParams) as [string, string, string?][];

    // Query to get total count without limit
    const countQueryStr = `[:find (count ?b)
                            :in $
                            :where
                            ${baseQueryWhereClauses.replace(/\[\?p :edit\/time \?page-edit-time\]/, '')}]`; // Remove edit time for count query

    const totalCountResults = await q(this.graph, countQueryStr, queryParams) as number[][];
    const totalCount = totalCountResults[0] ? totalCountResults[0][0] : 0;

    // Resolve block references in content
    const resolvedResults = await Promise.all(
      rawResults.map(async ([uid, content, pageTitle]) => {
        const resolvedContent = await resolveRefs(this.graph, content);
        return [uid, resolvedContent, pageTitle] as [string, string, string?];
      })
    );

    const searchDescription = `containing "${text}"`;
    const formattedResults = SearchUtils.formatSearchResults(resolvedResults, searchDescription, !targetPageUid);
    formattedResults.total_count = totalCount;
    return formattedResults;
  }
}

```

--------------------------------------------------------------------------------
/src/search/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { q } from '@roam-research/roam-api-sdk';
import type { Graph } from '@roam-research/roam-api-sdk';
import type { SearchResult } from './types.js';

export class SearchUtils {
  /**
   * Find a page by title or UID
   */
  static async findPageByTitleOrUid(graph: Graph, titleOrUid: string): Promise<string> {
    // Try to find page by title
    const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
    const findResults = await q(graph, findQuery, [titleOrUid]) as [string][];
    
    if (findResults && findResults.length > 0) {
      return findResults[0][0];
    }

    // Try as UID
    const uidQuery = `[:find ?uid :where [?e :block/uid "${titleOrUid}"] [?e :block/uid ?uid]]`;
    const uidResults = await q(graph, uidQuery, []) as [string][];
    
    if (!uidResults || uidResults.length === 0) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        `Page with title/UID "${titleOrUid}" not found`
      );
    }

    return uidResults[0][0];
  }

  /**
   * Format search results into a standard structure
   */
  static formatSearchResults(
    results: [string, string, string?][],
    searchDescription: string,
    includePageTitle: boolean = true
  ): SearchResult {
    if (!results || results.length === 0) {
      return {
        success: true,
        matches: [],
        message: `No blocks found ${searchDescription}`
      };
    }

    const matches = results.map(([uid, content, pageTitle]) => ({
      block_uid: uid,
      content,
      ...(includePageTitle && pageTitle && { page_title: pageTitle })
    }));

    return {
      success: true,
      matches,
      message: `Found ${matches.length} block(s) ${searchDescription}`
    };
  }

  /**
   * Format a tag for searching, handling both # and [[]] formats
   * @param tag Tag without prefix
   * @returns Array of possible formats to search for
   */
  static formatTag(tag: string): string[] {
    // Remove any existing prefixes
    const cleanTag = tag.replace(/^#|\[\[|\]\]$/g, '');
    // Return both formats for comprehensive search
    return [`#${cleanTag}`, `[[${cleanTag}]]`];
  }

  /**
   * Parse a date string into a Roam-formatted date
   */
  static parseDate(dateStr: string): string {
    const date = new Date(dateStr);
    const months = [
      'January', 'February', 'March', 'April', 'May', 'June',
      'July', 'August', 'September', 'October', 'November', 'December'
    ];
    // Adjust for timezone to ensure consistent date comparison
    const utcDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000);
    return `${months[utcDate.getMonth()]} ${utcDate.getDate()}${this.getOrdinalSuffix(utcDate.getDate())}, ${utcDate.getFullYear()}`;
  }

  /**
   * Parse a date string into a Roam-formatted date range
   * Returns [startDate, endDate] with endDate being inclusive (end of day)
   */
  static parseDateRange(startStr: string, endStr: string): [string, string] {
    const startDate = new Date(startStr);
    const endDate = new Date(endStr);
    endDate.setHours(23, 59, 59, 999); // Make end date inclusive

    const months = [
      'January', 'February', 'March', 'April', 'May', 'June',
      'July', 'August', 'September', 'October', 'November', 'December'
    ];

    // Adjust for timezone
    const utcStart = new Date(startDate.getTime() + startDate.getTimezoneOffset() * 60000);
    const utcEnd = new Date(endDate.getTime() + endDate.getTimezoneOffset() * 60000);

    return [
      `${months[utcStart.getMonth()]} ${utcStart.getDate()}${this.getOrdinalSuffix(utcStart.getDate())}, ${utcStart.getFullYear()}`,
      `${months[utcEnd.getMonth()]} ${utcEnd.getDate()}${this.getOrdinalSuffix(utcEnd.getDate())}, ${utcEnd.getFullYear()}`
    ];
  }

  private static getOrdinalSuffix(day: number): string {
    if (day > 3 && day < 21) return 'th';
    switch (day % 10) {
      case 1: return 'st';
      case 2: return 'nd';
      case 3: return 'rd';
      default: return 'th';
    }
  }
}

```

--------------------------------------------------------------------------------
/Roam_Research_Datalog_Cheatsheet.md:
--------------------------------------------------------------------------------

```markdown
# Roam Research Datalog Cheatsheet ([Gist](https://gist.github.com/2b3pro/231e4f230ed41e3f52e8a89ebf49848b))

## Basic Structure

- Roam uses Datascript (JavaScript/ClojureScript Datalog implementation)
- Each fact is a datom: `[entity-id attribute value transaction-id]`

## Core Components

### Entity IDs

- Hidden ID: Internal database entity-id
- Public ID: Block reference (e.g., `((GGv3cyL6Y))`) or page title (`[[Page Title]]`)

### Common Block Attributes

```clojure
:block/uid        # Nine-character block reference
:create/email     # Creator's email
:create/time      # Creation timestamp
:edit/email       # Editor's email
:edit/time        # Last edit timestamp
```

### Page-Specific Attributes

```clojure
:node/title       # Page title (pages only)
```

### Block Attributes

```clojure
:block/page      # Reference to page entity-id
:block/order     # Sequence within parent
:block/string    # Block content
:block/parents   # List of ancestor blocks
```

### Optional Block Attributes

```clojure
:children/view-type  # 'bullet', 'document', 'numbered'
:block/heading      # 1, 2, 3 for H1-H3
:block/props        # Image/iframe sizing, slider position
:block/text-align   # 'left', 'center', 'right', 'justify'
```

## Query Examples

### Graph Statistics

#### Count Pages

```clojure
[:find (count ?title)
 :where [_ :node/title ?title]]
```

#### Count Blocks

```clojure
[:find (count ?string)
 :where [_ :block/string ?string]]
```

#### Find Blocks with Most Descendants

```clojure
[:find ?ancestor (count ?block)
 :in $ %
 :where
 [?ancestor :block/string]
 [?block :block/string]
 (ancestor ?block ?ancestor)]
```

### Page Queries

#### List Pages in Namespace

```clojure
[:find ?title:name ?title:uid ?time:date
 :where
 [?page :node/title ?title:name]
 [?page :block/uid ?title:uid]
 [?page :edit/time ?time:date]
 [(clojure.string/starts-with? ?title:name "roam/")]]
```

#### Find Pages Modified Today

```clojure
[:find ?page_title:name ?page_title:uid
 :in $ ?start_of_day %
 :where
 [?page :node/title ?page_title:name]
 [?page :block/uid ?page_title:uid]
 (ancestor ?block ?page)
 [?block :edit/time ?time]
 [(> ?time ?start_of_day)]]
```

### Block Queries

#### Find Direct Children

```clojure
[:find ?block_string
 :where
 [?p :node/title "Page Title"]
 [?p :block/children ?c]
 [?c :block/string ?block_string]]
```

#### Find with Pull Pattern

```clojure
[:find (pull ?e [*{:block/children [*]}])
 :where [?e :node/title "Page Title"]]
```

### Advanced Queries

#### Search with Case-Insensitive Pattern

```javascript
let fragment = "search_term";
let query = `[:find ?title:name ?title:uid ?time:date
              :where [?page :node/title ?title:name]
                    [?page :block/uid ?title:uid]
                    [?page :edit/time ?time:date]]`;

let results = window.roamAlphaAPI
  .q(query)
  .filter((item, index) => item[0].toLowerCase().indexOf(fragment) > 0)
  .sort((a, b) => a[0].localeCompare(b[0]));
```

#### List Namespace Attributes

```clojure
[:find ?namespace ?attribute
 :where [_ ?attribute]
 [(namespace ?attribute) ?namespace]]
```

## Tips

- Use `:block/parents` for ancestors (includes all levels)
- Use `:block/children` for immediate descendants only
- Combine `clojure.string` functions for complex text matching
- Use `distinct` to avoid duplicate results
- Use Pull patterns for hierarchical data retrieval
- Handle case sensitivity in string operations carefully
- Chain ancestry rules for multi-level traversal

## Common Predicates

Available functions:

- clojure.string/includes?
- clojure.string/starts-with?
- clojure.string/ends-with?
- count
- <, >, <=, >=, =, not=, !=

## Aggregates

Available functions:

- sum
- max
- min
- avg
- count
- distinct

# Sources/References:

- [Deep Dive Into Roam's Data Structure - Why Roam is Much More Than a Note Taking App](https://www.zsolt.blog/2021/01/Roam-Data-Structure-Query.html)
- [Query Reference | Datomic](https://docs.datomic.com/query/query-data-reference.html)
- [Datalog Queries for Roam Research | David Bieber](https://davidbieber.com/snippets/2020-12-22-datalog-queries-for-roam-research/)

```

--------------------------------------------------------------------------------
/Roam_Markdown_Cheatsheet.md:
--------------------------------------------------------------------------------

```markdown
!!!! IMPORTANT: Always consult this cheatsheet for correct Roam-flavored markdown syntax BEFORE making any Roam tool calls.

# Roam Markdown Cheatsheet

⭐️📋 > > > START 📋⭐️

## Markdown Styles in Roam:

- **Bold Text here**
- **Italics Text here**
- External Link: `[Link text](URL)`
- Image Embed: `![Alt text](URL)`
- ^^Highlighted Text here^^
- Bullet points: - or \* followed by a space and the text
- {{[[TODO]]}} todo text
- {{[[DONE]]}} todo text
- LaTeX: `$$E=mc^2$$` or `$$\sum_{i=1}^{n} i = \frac{n(n+1)}{2}$$`
- Bullet points use dashes not asterisks.

## Roam-specific Markdown:

- Dates are in ordinal format: `[[January 1st, 2025]]`
- Block references: `((block-id))` This inserts a reference to the content of a specific block.
- Page references: `[[Page name]]` This creates a link to another page within your Roam graph.
- Link to blocks: `[Link Text](<((block-id))>)` This will link to the block.
- Embed block in a block: `{{[[embed]]: ((block-id))}}`
- To-do items: `{{[[TODO]]}} todo text` or `{{[[DONE]]}} todo text`
- Syntax highlighting for fenced code blocks (add language next to backticks before fenced code block - all in one block) - Example:
  ```javascript
      const foo(bar) => {
          return bar;
      }
  ```
- Tags:
  - one-word: `#word`
  - multiple words: `#[[two or more words]]`
  - hyphenated words: `#self-esteem`

## Roam Tables

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.

- The `{{[[table]]}}` block acts as the container for the entire table.
- The first header block should be at level 2 (one level deeper than `{{[[table]]}}`).
- Subsequent header blocks must increase their level by one.
- Each row starts at level 2.
- The first data cell in a row is at level 3 (one level deeper than the row block).
- Subsequent data cells within the same row must increase their level by one.

Example of a 4x4 table structure:

```
{{[[table]]}}
    - Header 1
        - Header 2
            - Header 3
                - Header 4
    - Row 1
        - Data 1.1
            - Data 1.2
                - Data 1.3
                    - Data 1.4
    - Row 2
        - Data 2.1
            - Data 2.2
                - Data 2.3
                    - Data 2.4
    - Row 3
        - Data 3.1
            - Data 3.2
                - Data 3.3
                    - Data 3.4
    - Row 4
        - Data 4.1
            - Data 4.2
                - Data 4.3
                    - Data 4.4
```

## Roam Mermaid

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.

```
- {{[[mermaid]]}}
    - graph TD
        - A[Start] --> B{Decision Point}
        - B -->|Yes| C[Process A]
        - B -->|No| D[Process B]
        - C --> E[Merge Results]
        - D --> E
        - E --> F[End]
```

## Roam Kanban Boards

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.

```
- {{[[kanban]]}}
    - card title 1
        - bullet point 1.1
        - bullet point 1.2
    - card title 2
        - bullet point 2.1
        - bullet point 2.2
```

---

## Roam Hiccup

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"}]`

## Specific notes and preferences concerning my Roam Research graph

```

--------------------------------------------------------------------------------
/src/tools/operations/search/index.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph } from '@roam-research/roam-api-sdk';
import type { SearchResult } from '../../types/index.js';
import type {
  TagSearchParams,
  BlockRefSearchParams,
  HierarchySearchParams,
  TextSearchParams,
  SearchHandlerResult
} from './types.js';
import {
  TagSearchHandlerImpl,
  BlockRefSearchHandlerImpl,
  HierarchySearchHandlerImpl,
  TextSearchHandlerImpl,
  StatusSearchHandlerImpl
} from './handlers.js';

export class SearchOperations {
  constructor(private graph: Graph) {}

  async searchByStatus(
    status: 'TODO' | 'DONE',
    page_title_uid?: string,
    include?: string,
    exclude?: string
  ): Promise<SearchHandlerResult> {
    const handler = new StatusSearchHandlerImpl(this.graph, {
      status,
      page_title_uid,
    });
    const result = await handler.execute();

    // Post-process results with include/exclude filters
    let matches = result.matches;

    if (include) {
      const includeTerms = include.split(',').map(term => term.trim());
      matches = matches.filter((match: SearchResult) => {
        const matchContent = match.content;
        const matchTitle = match.page_title;
        const terms = includeTerms;
        return terms.some(term => 
          matchContent.includes(term) ||
          (matchTitle && matchTitle.includes(term))
        );
      });
    }

    if (exclude) {
      const excludeTerms = exclude.split(',').map(term => term.trim());
      matches = matches.filter((match: SearchResult) => {
        const matchContent = match.content;
        const matchTitle = match.page_title;
        const terms = excludeTerms;
        return !terms.some(term => 
          matchContent.includes(term) ||
          (matchTitle && matchTitle.includes(term))
        );
      });
    }

    return {
      success: true,
      matches,
      message: `Found ${matches.length} block(s) with status ${status}${include ? ` including "${include}"` : ''}${exclude ? ` excluding "${exclude}"` : ''}`
    };
  }

  async searchForTag(
    primary_tag: string,
    page_title_uid?: string,
    near_tag?: string
  ): Promise<SearchHandlerResult> {
    const handler = new TagSearchHandlerImpl(this.graph, {
      primary_tag,
      page_title_uid,
      near_tag,
    });
    return handler.execute();
  }

  async searchBlockRefs(params: BlockRefSearchParams): Promise<SearchHandlerResult> {
    const handler = new BlockRefSearchHandlerImpl(this.graph, params);
    return handler.execute();
  }

  async searchHierarchy(params: HierarchySearchParams): Promise<SearchHandlerResult> {
    const handler = new HierarchySearchHandlerImpl(this.graph, params);
    return handler.execute();
  }

  async searchByText(params: TextSearchParams): Promise<SearchHandlerResult> {
    const handler = new TextSearchHandlerImpl(this.graph, params);
    return handler.execute();
  }

  async searchByDate(params: {
    start_date: string;
    end_date?: string;
    type: 'created' | 'modified' | 'both';
    scope: 'blocks' | 'pages' | 'both';
    include_content: boolean;
  }): Promise<{ 
    success: boolean; 
    matches: Array<{ 
      uid: string; 
      type: string; 
      time: number; 
      content?: string; 
      page_title?: string 
    }>; 
    message: string 
  }> {
    // Convert dates to timestamps
    const startTimestamp = new Date(`${params.start_date}T00:00:00`).getTime();
    const endTimestamp = params.end_date ? new Date(`${params.end_date}T23:59:59`).getTime() : undefined;

    // Use text search handler for content-based filtering
    const handler = new TextSearchHandlerImpl(this.graph, {
      text: '', // Empty text to match all blocks
    });

    const result = await handler.execute();

    // Filter results by date
    const matches = result.matches
      .filter(match => {
        const time = params.type === 'created' ? 
          new Date(match.content || '').getTime() : // Use content date for creation time
          Date.now(); // Use current time for modification time (simplified)
        
        return time >= startTimestamp && (!endTimestamp || time <= endTimestamp);
      })
      .map(match => ({
        uid: match.block_uid,
        type: 'block',
        time: params.type === 'created' ? 
          new Date(match.content || '').getTime() : 
          Date.now(),
        ...(params.include_content && { content: match.content }),
        page_title: match.page_title
      }));

    // Sort by time
    const sortedMatches = matches.sort((a, b) => b.time - a.time);

    return {
      success: true,
      matches: sortedMatches,
      message: `Found ${sortedMatches.length} matches for the given date range and criteria`
    };
  }
}

```

--------------------------------------------------------------------------------
/src/search/hierarchy-search.ts:
--------------------------------------------------------------------------------

```typescript
import { q } from '@roam-research/roam-api-sdk';
import type { Graph } from '@roam-research/roam-api-sdk';
import { BaseSearchHandler, SearchResult } from './types.js';
import { SearchUtils } from './utils.js';
import { resolveRefs } from '../tools/helpers/refs.js';

export interface HierarchySearchParams {
  parent_uid?: string;  // Search for children of this block
  child_uid?: string;   // Search for parents of this block
  page_title_uid?: string;
  max_depth?: number;   // How many levels deep to search (default: 1)
}

export class HierarchySearchHandler extends BaseSearchHandler {
  constructor(
    graph: Graph,
    private params: HierarchySearchParams
  ) {
    super(graph);
  }

  async execute(): Promise<SearchResult> {
    const { parent_uid, child_uid, page_title_uid, max_depth = 1 } = this.params;

    if (!parent_uid && !child_uid) {
      return {
        success: false,
        matches: [],
        message: 'Either parent_uid or child_uid must be provided'
      };
    }

    // Get target page UID if provided
    let targetPageUid: string | undefined;
    if (page_title_uid) {
      targetPageUid = await SearchUtils.findPageByTitleOrUid(this.graph, page_title_uid);
    }

    // Define ancestor rule for recursive traversal
    const ancestorRule = `[
      [ (ancestor ?child ?parent) 
          [?parent :block/children ?child] ]
      [ (ancestor ?child ?a) 
          [?parent :block/children ?child] 
          (ancestor ?parent ?a) ]
    ]`;

    let queryStr: string;
    let queryParams: any[];

    if (parent_uid) {
      // Search for all descendants using ancestor rule
      if (targetPageUid) {
        queryStr = `[:find ?block-uid ?block-str ?depth
                    :in $ % ?parent-uid ?page-uid
                    :where [?p :block/uid ?page-uid]
                           [?parent :block/uid ?parent-uid]
                           (ancestor ?b ?parent)
                           [?b :block/string ?block-str]
                           [?b :block/uid ?block-uid]
                           [?b :block/page ?p]
                           [(get-else $ ?b :block/path-length 1) ?depth]]`;
        queryParams = [ancestorRule, parent_uid, targetPageUid];
      } else {
        queryStr = `[:find ?block-uid ?block-str ?page-title ?depth
                    :in $ % ?parent-uid
                    :where [?parent :block/uid ?parent-uid]
                           (ancestor ?b ?parent)
                           [?b :block/string ?block-str]
                           [?b :block/uid ?block-uid]
                           [?b :block/page ?p]
                           [?p :node/title ?page-title]
                           [(get-else $ ?b :block/path-length 1) ?depth]]`;
        queryParams = [ancestorRule, parent_uid];
      }
    } else {
      // Search for ancestors using the same rule
      if (targetPageUid) {
        queryStr = `[:find ?block-uid ?block-str ?depth
                    :in $ % ?child-uid ?page-uid
                    :where [?p :block/uid ?page-uid]
                           [?child :block/uid ?child-uid]
                           (ancestor ?child ?b)
                           [?b :block/string ?block-str]
                           [?b :block/uid ?block-uid]
                           [?b :block/page ?p]
                           [(get-else $ ?b :block/path-length 1) ?depth]]`;
        queryParams = [ancestorRule, child_uid, targetPageUid];
      } else {
        queryStr = `[:find ?block-uid ?block-str ?page-title ?depth
                    :in $ % ?child-uid
                    :where [?child :block/uid ?child-uid]
                           (ancestor ?child ?b)
                           [?b :block/string ?block-str]
                           [?b :block/uid ?block-uid]
                           [?b :block/page ?p]
                           [?p :node/title ?page-title]
                           [(get-else $ ?b :block/path-length 1) ?depth]]`;
        queryParams = [ancestorRule, child_uid];
      }
    }

    const rawResults = await q(this.graph, queryStr, queryParams) as [string, string, string?, number?][];
    
    // Resolve block references and format results to include depth information
    const matches = await Promise.all(rawResults.map(async ([uid, content, pageTitle, depth]) => {
      const resolvedContent = await resolveRefs(this.graph, content);
      return {
        block_uid: uid,
        content: resolvedContent,
        depth: depth || 1,
        ...(pageTitle && { page_title: pageTitle })
      };
    }));

    const searchDescription = parent_uid
      ? `descendants of block ${parent_uid}`
      : `ancestors of block ${child_uid}`;

    return {
      success: true,
      matches,
      message: `Found ${matches.length} block(s) as ${searchDescription}`
    };
  }
}

```

--------------------------------------------------------------------------------
/src/search/tag-search.ts:
--------------------------------------------------------------------------------

```typescript
import { q } from '@roam-research/roam-api-sdk';
import type { Graph } from '@roam-research/roam-api-sdk';
import { BaseSearchHandler, TagSearchParams, SearchResult } from './types.js';
import { SearchUtils } from './utils.js';
import { resolveRefs } from '../tools/helpers/refs.js';

export class TagSearchHandler extends BaseSearchHandler {
  constructor(
    graph: Graph,
    private params: TagSearchParams
  ) {
    super(graph);
  }

  async execute(): Promise<SearchResult> {
    const { primary_tag, page_title_uid, near_tag, exclude_tag, case_sensitive = false, limit = -1, offset = 0 } = this.params;

    let nearTagUid: string | undefined;
    if (near_tag) {
      nearTagUid = await SearchUtils.findPageByTitleOrUid(this.graph, near_tag);
      if (!nearTagUid) {
        return {
          success: false,
          matches: [],
          message: `Near tag "${near_tag}" not found.`,
          total_count: 0
        };
      }
    }

    let excludeTagUid: string | undefined;
    if (exclude_tag) {
      excludeTagUid = await SearchUtils.findPageByTitleOrUid(this.graph, exclude_tag);
      if (!excludeTagUid) {
        return {
          success: false,
          matches: [],
          message: `Exclude tag "${exclude_tag}" not found.`,
          total_count: 0
        };
      }
    }

    // Get target page UID if provided for scoped search
    let targetPageUid: string | undefined;
    if (page_title_uid) {
      targetPageUid = await SearchUtils.findPageByTitleOrUid(this.graph, page_title_uid);
    }

    const searchTags: string[] = [];
    if (case_sensitive) {
      searchTags.push(primary_tag);
    } else {
      searchTags.push(primary_tag);
      searchTags.push(primary_tag.charAt(0).toUpperCase() + primary_tag.slice(1));
      searchTags.push(primary_tag.toUpperCase());
      searchTags.push(primary_tag.toLowerCase());
    }

    const tagWhereClauses = searchTags.map(tag => {
      // Roam tags can be [[tag name]] or #tag-name or #[[tag name]]
      // The :node/title for a tag page is just the tag name without any # or [[ ]]
      return `[?ref-page :node/title "${tag}"]`;
    }).join(' ');

    let inClause = `:in $`;
    let queryLimit = limit === -1 ? '' : `:limit ${limit}`;
    let queryOffset = offset === 0 ? '' : `:offset ${offset}`;
    let queryOrder = `:order ?page-edit-time asc ?block-uid asc`; // Sort by page edit time, then block UID

    let queryWhereClauses = `
                      (or ${tagWhereClauses})
                      [?b :block/refs ?ref-page]
                      [?b :block/string ?block-str]
                      [?b :block/uid ?block-uid]
                      [?b :block/page ?p]
                      [?p :node/title ?page-title]
                      [?p :edit/time ?page-edit-time]`; // Fetch page edit time for sorting

    if (nearTagUid) {
      queryWhereClauses += `
                      [?b :block/refs ?near-tag-page]
                      [?near-tag-page :block/uid "${nearTagUid}"]`;
    }

    if (excludeTagUid) {
      queryWhereClauses += `
                      (not [?b :block/refs ?exclude-tag-page])
                      [?exclude-tag-page :block/uid "${excludeTagUid}"]`;
    }

    if (targetPageUid) {
      inClause += ` ?target-page-uid`;
      queryWhereClauses += `
                      [?p :block/uid ?target-page-uid]`;
    }

    const queryStr = `[:find ?block-uid ?block-str ?page-title
                      ${inClause} ${queryLimit} ${queryOffset} ${queryOrder}
                      :where 
                      ${queryWhereClauses}]`;

    const queryArgs: (string | number)[] = [];
    if (targetPageUid) {
      queryArgs.push(targetPageUid);
    }

    const rawResults = await q(this.graph, queryStr, queryArgs) as [string, string, string?][];

    // Query to get total count without limit
    const countQueryStr = `[:find (count ?b)
                            ${inClause}
                            :where
                            ${queryWhereClauses.replace(/\[\?p :edit\/time \?page-edit-time\]/, '')}]`; // Remove edit time for count query

    const totalCountResults = await q(this.graph, countQueryStr, queryArgs) as number[][];
    const totalCount = totalCountResults[0] ? totalCountResults[0][0] : 0;

    // Resolve block references in content
    const resolvedResults = await Promise.all(
      rawResults.map(async ([uid, content, pageTitle]) => {
        const resolvedContent = await resolveRefs(this.graph, content);
        return [uid, resolvedContent, pageTitle] as [string, string, string?];
      })
    );

    const searchDescription = `referencing "${primary_tag}"`;
    const formattedResults = SearchUtils.formatSearchResults(resolvedResults, searchDescription, !targetPageUid);
    formattedResults.total_count = totalCount;
    return formattedResults;
  }
}

```

--------------------------------------------------------------------------------
/src/tools/tool-handlers.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph } from '@roam-research/roam-api-sdk';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { PageOperations } from './operations/pages.js';
import { BlockOperations } from './operations/blocks.js';
import { BlockRetrievalOperations } from './operations/block-retrieval.js'; // New import
import { SearchOperations } from './operations/search/index.js';
import { MemoryOperations } from './operations/memory.js';
import { TodoOperations } from './operations/todos.js';
import { OutlineOperations } from './operations/outline.js';
import { BatchOperations } from './operations/batch.js';
import { DatomicSearchHandlerImpl } from './operations/search/handlers.js';

export class ToolHandlers {
  private pageOps: PageOperations;
  private blockOps: BlockOperations;
  private blockRetrievalOps: BlockRetrievalOperations; // New instance
  private searchOps: SearchOperations;
  private memoryOps: MemoryOperations;
  private todoOps: TodoOperations;
  private outlineOps: OutlineOperations;
  private batchOps: BatchOperations;

  constructor(private graph: Graph) {
    this.pageOps = new PageOperations(graph);
    this.blockOps = new BlockOperations(graph);
    this.blockRetrievalOps = new BlockRetrievalOperations(graph); // Initialize new instance
    this.searchOps = new SearchOperations(graph);
    this.memoryOps = new MemoryOperations(graph);
    this.todoOps = new TodoOperations(graph);
    this.outlineOps = new OutlineOperations(graph);
    this.batchOps = new BatchOperations(graph);
  }

  // Page Operations
  async findPagesModifiedToday(limit: number = 50, offset: number = 0, sort_order: 'asc' | 'desc' = 'desc') {
    return this.pageOps.findPagesModifiedToday(limit, offset, sort_order);
  }

  async createPage(title: string, content?: Array<{ text: string; level: number; heading?: number }>) {
    return this.pageOps.createPage(title, content);
  }

  async fetchPageByTitle(title: string, format?: 'markdown' | 'raw') {
    return this.pageOps.fetchPageByTitle(title, format);
  }

  // Block Operations
  async fetchBlockWithChildren(block_uid: string, depth?: number) {
    return this.blockRetrievalOps.fetchBlockWithChildren(block_uid, depth);
  }

  // Search Operations
  async searchByStatus(
    status: 'TODO' | 'DONE',
    page_title_uid?: string,
    include?: string,
    exclude?: string
  ) {
    return this.searchOps.searchByStatus(status, page_title_uid, include, exclude);
  }

  async searchForTag(
    primary_tag: string,
    page_title_uid?: string,
    near_tag?: string
  ) {
    return this.searchOps.searchForTag(primary_tag, page_title_uid, near_tag);
  }

  async searchBlockRefs(params: { block_uid?: string; page_title_uid?: string }) {
    return this.searchOps.searchBlockRefs(params);
  }

  async searchHierarchy(params: {
    parent_uid?: string;
    child_uid?: string;
    page_title_uid?: string;
    max_depth?: number;
  }) {
    return this.searchOps.searchHierarchy(params);
  }

  async searchByText(params: {
    text: string;
    page_title_uid?: string;
  }) {
    return this.searchOps.searchByText(params);
  }

  async searchByDate(params: {
    start_date: string;
    end_date?: string;
    type: 'created' | 'modified' | 'both';
    scope: 'blocks' | 'pages' | 'both';
    include_content: boolean;
  }) {
    return this.searchOps.searchByDate(params);
  }

  // Datomic query
  async executeDatomicQuery(params: { query: string; inputs?: unknown[] }) {
    const handler = new DatomicSearchHandlerImpl(this.graph, params);
    return handler.execute();
  }

  // Memory Operations
  async remember(memory: string, categories?: string[]) {
    return this.memoryOps.remember(memory, categories);
  }

  async recall(sort_by: 'newest' | 'oldest' = 'newest', filter_tag?: string) {
    return this.memoryOps.recall(sort_by, filter_tag);
  }

  // Todo Operations
  async addTodos(todos: string[]) {
    return this.todoOps.addTodos(todos);
  }

  // Outline Operations
  async createOutline(outline: Array<{ text: string | undefined; level: number }>, page_title_uid?: string, block_text_uid?: string) {
    return this.outlineOps.createOutline(outline, page_title_uid, block_text_uid);
  }

  async importMarkdown(
    content: string,
    page_uid?: string,
    page_title?: string,
    parent_uid?: string,
    parent_string?: string,
    order: 'first' | 'last' = 'first'
  ) {
    return this.outlineOps.importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order);
  }

  // Batch Operations
  async processBatch(actions: any[]) {
    return this.batchOps.processBatch(actions);
  }

  async getRoamMarkdownCheatsheet() {
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = path.dirname(__filename);
    const cheatsheetPath = path.join(__dirname, '../../Roam_Markdown_Cheatsheet.md');
    let cheatsheetContent = fs.readFileSync(cheatsheetPath, 'utf-8');

    const customInstructionsPath = process.env.CUSTOM_INSTRUCTIONS_PATH;
    if (customInstructionsPath && fs.existsSync(customInstructionsPath)) {
      try {
        const customInstructionsContent = fs.readFileSync(customInstructionsPath, 'utf-8');
        cheatsheetContent += `\n\n${customInstructionsContent}`;
      } catch (error) {
        console.warn(`Could not read custom instructions file at ${customInstructionsPath}: ${error}`);
      }
    }
    return cheatsheetContent;
  }
}

```

--------------------------------------------------------------------------------
/src/tools/operations/memory.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph, q, createPage, batchActions } from '@roam-research/roam-api-sdk';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { formatRoamDate } from '../../utils/helpers.js';
import { resolveRefs } from '../helpers/refs.js';
import { SearchOperations } from './search/index.js';
import type { SearchResult } from '../types/index.js';

export class MemoryOperations {
  private searchOps: SearchOperations;

  constructor(private graph: Graph) {
    this.searchOps = new SearchOperations(graph);
  }

  async remember(memory: string, categories?: string[]): Promise<{ success: boolean }> {
    // Get today's date
    const today = new Date();
    const dateStr = formatRoamDate(today);
    
    // Try to find today's page
    const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
    const findResults = await q(this.graph, findQuery, [dateStr]) as [string][];
    
    let pageUid: string;
    
    if (findResults && findResults.length > 0) {
      pageUid = findResults[0][0];
    } else {
      // Create today's page if it doesn't exist
      try {
        await createPage(this.graph, {
          action: 'create-page',
          page: { title: dateStr }
        });

        // Get the new page's UID
        const results = await q(this.graph, findQuery, [dateStr]) as [string][];
        if (!results || results.length === 0) {
          throw new McpError(
            ErrorCode.InternalError,
            'Could not find created today\'s page'
          );
        }
        pageUid = results[0][0];
      } catch (error) {
        throw new McpError(
          ErrorCode.InternalError,
          'Failed to create today\'s page'
        );
      }
    }

    // Get memories tag from environment
    const memoriesTag = process.env.MEMORIES_TAG;
    if (!memoriesTag) {
      throw new McpError(
        ErrorCode.InternalError,
        'MEMORIES_TAG environment variable not set'
      );
    }

    // Format categories as Roam tags if provided
    const categoryTags = categories?.map(cat => {
      // Handle multi-word categories
      return cat.includes(' ') ? `#[[${cat}]]` : `#${cat}`;
    }).join(' ') || '';

    // Create block with memory, memories tag, and optional categories
    const blockContent = `${memoriesTag} ${memory} ${categoryTags}`.trim();
    
    const actions = [{
      action: 'create-block',
      location: {
        'parent-uid': pageUid,
        order: 'last'
      },
      block: {
        string: blockContent
      }
    }];

    try {
      const result = await batchActions(this.graph, {
        action: 'batch-actions',
        actions
      });

      if (!result) {
        throw new McpError(
          ErrorCode.InternalError,
          'Failed to create memory block via batch action'
        );
      }
    } catch (error) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to create memory block: ${error instanceof Error ? error.message : String(error)}`
      );
    }

    return { success: true };
  }

  async recall(sort_by: 'newest' | 'oldest' = 'newest', filter_tag?: string): Promise<{ success: boolean; memories: string[] }> {
    // Get memories tag from environment
    var memoriesTag = process.env.MEMORIES_TAG;
    if (!memoriesTag) {
      memoriesTag = "Memories"
    }

    // Extract the tag text, removing any formatting
    const tagText = memoriesTag
      .replace(/^#/, '')  // Remove leading #
      .replace(/^\[\[/, '').replace(/\]\]$/, '');  // Remove [[ and ]]

    try {
      // Get page blocks using query to access actual block content
      const ancestorRule = `[
        [ (ancestor ?b ?a)
          [?a :block/children ?b] ]
        [ (ancestor ?b ?a)
          [?parent :block/children ?b]
          (ancestor ?parent ?a) ]
      ]`;

      // Query to find all blocks on the page
      const pageQuery = `[:find ?string ?time
                         :in $ % ?title
                         :where 
                         [?page :node/title ?title]
                         [?block :block/string ?string]
                         [?block :create/time ?time]
                         (ancestor ?block ?page)]`;
      
      // Execute query
      const pageResults = await q(this.graph, pageQuery, [ancestorRule, tagText]) as [string, number][];

      // Process page blocks with sorting
      let pageMemories = pageResults
        .sort(([_, aTime], [__, bTime]) => 
          sort_by === 'newest' ? bTime - aTime : aTime - bTime
        )
        .map(([content]) => content);

      // Get tagged blocks from across the graph
      const tagResults = await this.searchOps.searchForTag(tagText);
      
      // Process tagged blocks with sorting
      let taggedMemories = tagResults.matches
        .sort((a: SearchResult, b: SearchResult) => {
          const aTime = a.block_uid ? parseInt(a.block_uid.split('-')[0], 16) : 0;
          const bTime = b.block_uid ? parseInt(b.block_uid.split('-')[0], 16) : 0;
          return sort_by === 'newest' ? bTime - aTime : aTime - bTime;
        })
        .map(match => match.content);

      // Resolve any block references in both sets
      const resolvedPageMemories = await Promise.all(
        pageMemories.map(async (content: string) => resolveRefs(this.graph, content))
      );
      const resolvedTaggedMemories = await Promise.all(
        taggedMemories.map(async (content: string) => resolveRefs(this.graph, content))
      );

      // Combine both sets and remove duplicates while preserving order
      let uniqueMemories = [
        ...resolvedPageMemories,
        ...resolvedTaggedMemories
      ].filter((memory, index, self) => 
        self.indexOf(memory) === index
      );

      // Format filter tag with exact Roam tag syntax
      const filterTagFormatted = filter_tag ? 
      (filter_tag.includes(' ') ? `#[[${filter_tag}]]` : `#${filter_tag}`) : null;

      // Filter by exact tag match if provided
      if (filterTagFormatted) {
        uniqueMemories = uniqueMemories.filter(memory => memory.includes(filterTagFormatted));
      }
      
      // Format memories tag for removal and clean up memories tag
      const memoriesTagFormatted = tagText.includes(' ') || tagText.includes('/') ? `#[[${tagText}]]` : `#${tagText}`;
      uniqueMemories = uniqueMemories.map(memory => memory.replace(memoriesTagFormatted, '').trim());

      // return {
      //   success: true,
      //   memories: [
      //     `memoriesTag = ${memoriesTag}`,
      //     `filter_tag = ${filter_tag}`,
      //     `filterTagFormatted = ${filterTagFormatted}`,
      //     `memoriesTagFormatted = ${memoriesTagFormatted}`,
      //   ]
      // }
      return {
        success: true,
        memories: uniqueMemories
      };
    } catch (error: any) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to recall memories: ${error.message}`
      );
    }
  }
}

```

--------------------------------------------------------------------------------
/src/markdown-utils.ts:
--------------------------------------------------------------------------------

```typescript
import type {
  RoamCreateBlock,
  RoamCreatePage,
  RoamUpdateBlock,
  RoamDeleteBlock,
  RoamDeletePage,
  RoamMoveBlock
} from '@roam-research/roam-api-sdk';

export type BatchAction =
  | RoamCreateBlock
  | RoamCreatePage
  | RoamUpdateBlock
  | RoamDeleteBlock
  | RoamDeletePage
  | RoamMoveBlock;

interface MarkdownNode {
  content: string;
  level: number;
  heading_level?: number;  // Optional heading level (1-3) for heading nodes
  children_view_type?: 'bullet' | 'document' | 'numbered'; // Optional view type for children
  children: MarkdownNode[];
}

/**
 * Check if text has a traditional markdown table
 */
function hasMarkdownTable(text: string): boolean {
  return /^\|([^|]+\|)+\s*$\n\|(\s*:?-+:?\s*\|)+\s*$\n(\|([^|]+\|)+\s*$\n*)+$/.test(text);
}

/**
 * Converts a markdown table to Roam format
 */
function convertTableToRoamFormat(text: string) {
  const lines = text.split('\n')
    .map(line => line.trim())
    .filter(line => line.length > 0);

  const tableRegex = /^\|([^|]+\|)+\s*$\n\|(\s*:?-+:?\s*\|)+\s*$\n(\|([^|]+\|)+\s*$\n*)+/m;

  if (!tableRegex.test(text)) {
    return text;
  }

  const rows = lines
    .filter((_, index) => index !== 1)
    .map(line =>
      line.trim()
        .replace(/^\||\|$/g, '')
        .split('|')
        .map(cell => cell.trim())
    );

  let roamTable = '{{[[table]]}}\n';

  // First row becomes column headers
  const headers = rows[0];
  for (let i = 0; i < headers.length; i++) {
    roamTable += `${'  '.repeat(i + 1)}- ${headers[i]}\n`;
  }

  // Remaining rows become nested under each column
  for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) {
    const row = rows[rowIndex];
    for (let colIndex = 0; colIndex < row.length; colIndex++) {
      roamTable += `${'  '.repeat(colIndex + 1)}- ${row[colIndex]}\n`;
    }
  }

  return roamTable.trim();
}

function convertAllTables(text: string) {
  return text.replaceAll(
    /(^\|([^|]+\|)+\s*$\n\|(\s*:?-+:?\s*\|)+\s*$\n(\|([^|]+\|)+\s*$\n*)+)/gm,
    (match) => {
      return '\n' + convertTableToRoamFormat(match) + '\n';
    }
  );
}

/**
 * Parse markdown heading syntax (e.g. "### Heading") and return the heading level (1-3) and content.
 * Heading level is determined by the number of # characters (e.g. # = h1, ## = h2, ### = h3).
 * Returns heading_level: 0 for non-heading content.
 */
function parseMarkdownHeadingLevel(text: string): { heading_level: number; content: string } {
  const match = text.match(/^(#{1,3})\s+(.+)$/);
  if (match) {
    return {
      heading_level: match[1].length,  // Number of # characters determines heading level
      content: match[2].trim()
    };
  }
  return {
    heading_level: 0,  // Not a heading
    content: text.trim()
  };
}

function convertToRoamMarkdown(text: string): string {
  // Handle double asterisks/underscores (bold)
  text = text.replace(/\*\*(.+?)\*\*/g, '**$1**');  // Preserve double asterisks

  // Handle single asterisks/underscores (italic)
  text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '__$1__');  // Single asterisk to double underscore
  text = text.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, '__$1__');        // Single underscore to double underscore

  // Handle highlights
  text = text.replace(/==(.+?)==/g, '^^$1^^');

  // Convert tasks
  text = text.replace(/- \[ \]/g, '- {{[[TODO]]}}');
  text = text.replace(/- \[x\]/g, '- {{[[DONE]]}}');

  // Convert tables
  text = convertAllTables(text);

  return text;
}

function parseMarkdown(markdown: string): MarkdownNode[] {
  markdown = convertToRoamMarkdown(markdown);

  const originalLines = markdown.split('\n');
  const processedLines: string[] = [];

  // Pre-process lines to handle mid-line code blocks without splice
  for (const line of originalLines) {
    const trimmedLine = line.trimEnd();
    const codeStartIndex = trimmedLine.indexOf('```');

    if (codeStartIndex > 0) {
      const indentationWhitespace = line.match(/^\s*/)?.[0] ?? '';
      processedLines.push(indentationWhitespace + trimmedLine.substring(0, codeStartIndex));
      processedLines.push(indentationWhitespace + trimmedLine.substring(codeStartIndex));
    } else {
      processedLines.push(line);
    }
  }

  const rootNodes: MarkdownNode[] = [];
  const stack: MarkdownNode[] = [];
  let inCodeBlock = false;
  let codeBlockContent = '';
  let codeBlockIndentation = 0;
  let codeBlockParentLevel = 0;

  for (let i = 0; i < processedLines.length; i++) {
    const line = processedLines[i];
    const trimmedLine = line.trimEnd();

    if (trimmedLine.match(/^(\s*)```/)) {
      if (!inCodeBlock) {
        inCodeBlock = true;
        codeBlockContent = trimmedLine.trimStart() + '\n';
        codeBlockIndentation = line.match(/^\s*/)?.[0].length ?? 0;
        codeBlockParentLevel = stack.length;
      } else {
        inCodeBlock = false;
        codeBlockContent += trimmedLine.trimStart();

        const linesInCodeBlock = codeBlockContent.split('\n');

        let baseIndentation = '';
        for (let j = 1; j < linesInCodeBlock.length - 1; j++) {
          const codeLine = linesInCodeBlock[j];
          if (codeLine.trim().length > 0) {
            const indentMatch = codeLine.match(/^[\t ]*/);
            if (indentMatch) {
              baseIndentation = indentMatch[0];
              break;
            }
          }
        }

        const processedCodeLines = linesInCodeBlock.map((codeLine, index) => {
          if (index === 0 || index === linesInCodeBlock.length - 1) return codeLine.trimStart();

          if (codeLine.trim().length === 0) return '';

          if (codeLine.startsWith(baseIndentation)) {
            return codeLine.slice(baseIndentation.length);
          }
          return codeLine.trimStart();
        });

        const level = Math.floor(codeBlockIndentation / 2);
        const node: MarkdownNode = {
          content: processedCodeLines.join('\n'),
          level,
          children: []
        };

        while (stack.length > codeBlockParentLevel) {
          stack.pop();
        }
        if (level === 0) {
          rootNodes.push(node);
          stack[0] = node;
        } else {
          while (stack.length > level) {
            stack.pop();
          }
          if (stack[level - 1]) {
            stack[level - 1].children.push(node);
          } else {
            rootNodes.push(node);
          }
          stack[level] = node;
        }

        codeBlockContent = '';
      }
      continue;
    }

    if (inCodeBlock) {
      codeBlockContent += line + '\n';
      continue;
    }

    if (trimmedLine === '') {
      continue;
    }

    const indentation = line.match(/^\s*/)?.[0].length ?? 0;
    let level = Math.floor(indentation / 2);

    let contentToParse: string;
    const bulletMatch = trimmedLine.match(/^(\s*)[-*+]\s+/);
    if (bulletMatch) {
      level = Math.floor(bulletMatch[1].length / 2);
      contentToParse = trimmedLine.substring(bulletMatch[0].length);
    } else {
      contentToParse = trimmedLine;
    }

    const { heading_level, content: finalContent } = parseMarkdownHeadingLevel(contentToParse);

    const node: MarkdownNode = {
      content: finalContent,
      level,
      ...(heading_level > 0 && { heading_level }),
      children: []
    };

    while (stack.length > level) {
      stack.pop();
    }

    if (level === 0 || !stack[level - 1]) {
      rootNodes.push(node);
      stack[0] = node;
    } else {
      stack[level - 1].children.push(node);
    }
    stack[level] = node;
  }

  return rootNodes;
}

function parseTableRows(lines: string[]): MarkdownNode[] {
  const tableNodes: MarkdownNode[] = [];
  let currentLevel = -1;

  for (const line of lines) {
    const trimmedLine = line.trimEnd();
    if (!trimmedLine) continue;

    // Calculate indentation level
    const indentation = line.match(/^\s*/)?.[0].length ?? 0;
    const level = Math.floor(indentation / 2);

    // Extract content after bullet point
    const content = trimmedLine.replace(/^\s*[-*+]\s*/, '');

    // Create node for this cell
    const node: MarkdownNode = {
      content,
      level,
      children: []
    };

    // Track the first level we see to maintain relative nesting
    if (currentLevel === -1) {
      currentLevel = level;
    }

    // Add node to appropriate parent based on level
    if (level === currentLevel) {
      tableNodes.push(node);
    } else {
      // Find parent by walking back through nodes
      let parent = tableNodes[tableNodes.length - 1];
      while (parent && parent.level < level - 1) {
        parent = parent.children[parent.children.length - 1];
      }
      if (parent) {
        parent.children.push(node);
      }
    }
  }

  return tableNodes;
}

function generateBlockUid(): string {
  // Generate a random string of 9 characters (Roam's format)
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_';
  let uid = '';
  for (let i = 0; i < 9; i++) {
    uid += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return uid;
}

interface BlockInfo {
  uid: string;
  content: string;
  heading_level?: number;  // Optional heading level (1-3) for heading nodes
  children_view_type?: 'bullet' | 'document' | 'numbered'; // Optional view type for children
  children: BlockInfo[];
}

function convertNodesToBlocks(nodes: MarkdownNode[]): BlockInfo[] {
  return nodes.map(node => ({
    uid: generateBlockUid(),
    content: node.content,
    ...(node.heading_level && { heading_level: node.heading_level }),  // Preserve heading level if present
    children: convertNodesToBlocks(node.children)
  }));
}

function convertToRoamActions(
  nodes: MarkdownNode[],
  parentUid: string,
  order: 'first' | 'last' | number = 'last'
): BatchAction[] {
  // First convert nodes to blocks with UIDs
  const blocks = convertNodesToBlocks(nodes);
  const actions: BatchAction[] = [];

  // Helper function to recursively create actions
  function createBlockActions(blocks: BlockInfo[], parentUid: string, order: 'first' | 'last' | number): void {
    for (let i = 0; i < blocks.length; i++) {
      const block = blocks[i];
      // Create the current block
      const action: RoamCreateBlock = {
        action: 'create-block',
        location: {
          'parent-uid': parentUid,
          order: typeof order === 'number' ? order + i : i
        },
        block: {
          uid: block.uid,
          string: block.content,
          ...(block.heading_level && { heading: block.heading_level }),
          ...(block.children_view_type && { 'children-view-type': block.children_view_type })
        }
      };

      actions.push(action);

      // Create child blocks if any
      if (block.children.length > 0) {
        createBlockActions(block.children, block.uid, 'last');
      }
    }
  }

  // Create all block actions
  createBlockActions(blocks, parentUid, order);

  return actions;
}

// Export public functions and types
export {
  parseMarkdown,
  convertToRoamActions,
  hasMarkdownTable,
  convertAllTables,
  convertToRoamMarkdown,
  parseMarkdownHeadingLevel
};

```

--------------------------------------------------------------------------------
/src/tools/operations/blocks.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph, q, createBlock as createRoamBlock, updateBlock as updateRoamBlock, batchActions, createPage } from '@roam-research/roam-api-sdk';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { formatRoamDate } from '../../utils/helpers.js';
import { 
  parseMarkdown, 
  convertToRoamActions,
  convertToRoamMarkdown,
  hasMarkdownTable,
  type BatchAction 
} from '../../markdown-utils.js';
import type { BlockUpdate, BlockUpdateResult } from '../types/index.js';

export class BlockOperations {
  constructor(private graph: Graph) {}

  async createBlock(content: string, page_uid?: string, title?: string, heading?: number): Promise<{ success: boolean; block_uid?: string; parent_uid: string }> {
    // If page_uid provided, use it directly
    let targetPageUid = page_uid;
    
    // If no page_uid but title provided, search for page by title
    if (!targetPageUid && title) {
      const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
      const findResults = await q(this.graph, findQuery, [title]) as [string][];
      
      if (findResults && findResults.length > 0) {
        targetPageUid = findResults[0][0];
      } else {
        // Create page with provided title if it doesn't exist
        try {
          await createPage(this.graph, {
            action: 'create-page',
            page: { title }
          });

          // Get the new page's UID
          const results = await q(this.graph, findQuery, [title]) as [string][];
          if (!results || results.length === 0) {
            throw new Error('Could not find created page');
          }
          targetPageUid = results[0][0];
        } catch (error) {
          throw new McpError(
            ErrorCode.InternalError,
            `Failed to create page: ${error instanceof Error ? error.message : String(error)}`
          );
        }
      }
    }
    
    // If neither page_uid nor title provided, use today's date page
    if (!targetPageUid) {
      const today = new Date();
      const dateStr = formatRoamDate(today);
      
      // Try to find today's page
      const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
      const findResults = await q(this.graph, findQuery, [dateStr]) as [string][];
      
      if (findResults && findResults.length > 0) {
        targetPageUid = findResults[0][0];
      } else {
        // Create today's page if it doesn't exist
        try {
          await createPage(this.graph, {
            action: 'create-page',
            page: { title: dateStr }
          });

          // Get the new page's UID
          const results = await q(this.graph, findQuery, [dateStr]) as [string][];
          if (!results || results.length === 0) {
            throw new Error('Could not find created today\'s page');
          }
          targetPageUid = results[0][0];
        } catch (error) {
          throw new McpError(
            ErrorCode.InternalError,
            `Failed to create today's page: ${error instanceof Error ? error.message : String(error)}`
          );
        }
      }
    }

    try {
      // If the content has multiple lines or is a table, use nested import
      if (content.includes('\n')) {
        let nodes;
        
        // If heading parameter is provided, manually construct nodes to preserve heading
        if (heading) {
          const lines = content.split('\n');
          const firstLine = lines[0].trim();
          const remainingLines = lines.slice(1);
          
          // Create the first node with heading formatting
          const firstNode = {
            content: firstLine,
            level: 0,
            heading_level: heading,
            children: []
          };
          
          // If there are remaining lines, parse them as children or siblings
          if (remainingLines.length > 0 && remainingLines.some(line => line.trim())) {
            const remainingContent = remainingLines.join('\n');
            const convertedRemainingContent = convertToRoamMarkdown(remainingContent);
            const remainingNodes = parseMarkdown(convertedRemainingContent);
            
            // Add remaining nodes as siblings to the first node
            nodes = [firstNode, ...remainingNodes];
          } else {
            nodes = [firstNode];
          }
        } else {
          // No heading parameter, use original parsing logic
          const convertedContent = convertToRoamMarkdown(content);
          nodes = parseMarkdown(convertedContent);
        }
        
        const actions = convertToRoamActions(nodes, targetPageUid, 'last');
        
        // Execute batch actions to create the nested structure
        const result = await batchActions(this.graph, {
          action: 'batch-actions',
          actions
        });

        if (!result) {
          throw new Error('Failed to create nested blocks');
        }

        const blockUid = result.created_uids?.[0];
        return { 
          success: true,
          block_uid: blockUid,
          parent_uid: targetPageUid!
        };
      } else {
        // For single block content, use the same convertToRoamActions approach that works in roam_create_page
        const nodes = [{
          content: content,
          level: 0,
          ...(heading && typeof heading === 'number' && heading > 0 && { heading_level: heading }),
          children: []
        }];
        
        if (!targetPageUid) {
          throw new McpError(ErrorCode.InternalError, 'targetPageUid is undefined');
        }
        
        const actions = convertToRoamActions(nodes, targetPageUid, 'last');
        
        // Execute batch actions to create the block
        const result = await batchActions(this.graph, {
          action: 'batch-actions',
          actions
        });

        if (!result) {
          throw new Error('Failed to create block');
        }

        const blockUid = result.created_uids?.[0];
        return { 
          success: true,
          block_uid: blockUid,
          parent_uid: targetPageUid!
        };
      }
    } catch (error) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to create block: ${error instanceof Error ? error.message : String(error)}`
      );
    }
  }

  async updateBlock(block_uid: string, content?: string, transform?: (currentContent: string) => string): Promise<{ success: boolean; content: string }> {
    if (!block_uid) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        'block_uid is required'
      );
    }

    // Get current block content
    const blockQuery = `[:find ?string .
                        :where [?b :block/uid "${block_uid}"]
                               [?b :block/string ?string]]`;
    const result = await q(this.graph, blockQuery, []);
    if (result === null || result === undefined) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        `Block with UID "${block_uid}" not found`
      );
    }
    const currentContent = String(result);
    
    if (currentContent === null || currentContent === undefined) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        `Block with UID "${block_uid}" not found`
      );
    }

    // Determine new content
    let newContent: string;
    if (content) {
      newContent = content;
    } else if (transform) {
      newContent = transform(currentContent);
    } else {
      throw new McpError(
        ErrorCode.InvalidRequest,
        'Either content or transform function must be provided'
      );
    }

    try {
      await updateRoamBlock(this.graph, {
        action: 'update-block',
        block: {
          uid: block_uid,
          string: newContent
        }
      });

      return { 
        success: true,
        content: newContent
      };
    } catch (error: any) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to update block: ${error.message}`
      );
    }
  }

  async updateBlocks(updates: BlockUpdate[]): Promise<{ success: boolean; results: BlockUpdateResult[] }> {
    if (!Array.isArray(updates) || updates.length === 0) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        'updates must be a non-empty array'
      );
    }

    // Validate each update has required fields
    updates.forEach((update, index) => {
      if (!update.block_uid) {
        throw new McpError(
          ErrorCode.InvalidRequest,
          `Update at index ${index} missing block_uid`
        );
      }
      if (!update.content && !update.transform) {
        throw new McpError(
          ErrorCode.InvalidRequest,
          `Update at index ${index} must have either content or transform`
        );
      }
    });

    // Get current content for all blocks
    const blockUids = updates.map(u => u.block_uid);
    const blockQuery = `[:find ?uid ?string
                        :in $ [?uid ...]
                        :where [?b :block/uid ?uid]
                               [?b :block/string ?string]]`;
    const blockResults = await q(this.graph, blockQuery, [blockUids]) as [string, string][];
    
    // Create map of uid -> current content
    const contentMap = new Map<string, string>();
    blockResults.forEach(([uid, string]) => {
      contentMap.set(uid, string);
    });

    // Prepare batch actions
    const actions: BatchAction[] = [];
    const results: BlockUpdateResult[] = [];

    for (const update of updates) {
      try {
        const currentContent = contentMap.get(update.block_uid);
        if (!currentContent) {
          results.push({
            block_uid: update.block_uid,
            content: '',
            success: false,
            error: `Block with UID "${update.block_uid}" not found`
          });
          continue;
        }

        // Determine new content
        let newContent: string;
        if (update.content) {
          newContent = update.content;
        } else if (update.transform) {
          const regex = new RegExp(update.transform.find, update.transform.global ? 'g' : '');
          newContent = currentContent.replace(regex, update.transform.replace);
        } else {
          // This shouldn't happen due to earlier validation
          throw new Error('Invalid update configuration');
        }

        // Add to batch actions
        actions.push({
          action: 'update-block',
          block: {
            uid: update.block_uid,
            string: newContent
          }
        });

        results.push({
          block_uid: update.block_uid,
          content: newContent,
          success: true
        });
      } catch (error: any) {
        results.push({
          block_uid: update.block_uid,
          content: contentMap.get(update.block_uid) || '',
          success: false,
          error: error.message
        });
      }
    }

    // Execute batch update if we have any valid actions
    if (actions.length > 0) {
      try {
        const batchResult = await batchActions(this.graph, {
          action: 'batch-actions',
          actions
        });

        if (!batchResult) {
          throw new Error('Batch update failed');
        }
      } catch (error: any) {
        // Mark all previously successful results as failed
        results.forEach(result => {
          if (result.success) {
            result.success = false;
            result.error = `Batch update failed: ${error.message}`;
          }
        });
      }
    }

    return {
      success: results.every(r => r.success),
      results
    };
  }
}

```

--------------------------------------------------------------------------------
/src/tools/operations/pages.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph, q, createPage as createRoamPage, batchActions, createBlock } from '@roam-research/roam-api-sdk';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { capitalizeWords } from '../helpers/text.js';
import { resolveRefs } from '../helpers/refs.js';
import type { RoamBlock } from '../types/index.js';
import {
  parseMarkdown,
  convertToRoamActions,
  convertToRoamMarkdown,
  hasMarkdownTable
} from '../../markdown-utils.js';

// Helper to get ordinal suffix for dates
function getOrdinalSuffix(day: number): string {
  if (day > 3 && day < 21) return 'th'; // Handles 11th, 12th, 13th
  switch (day % 10) {
    case 1: return 'st';
    case 2: return 'nd';
    case 3: return 'rd';
    default: return 'th';
  }
}

export class PageOperations {
  constructor(private graph: Graph) { }

  async findPagesModifiedToday(limit: number = 50, offset: number = 0, sort_order: 'asc' | 'desc' = 'desc') {
    // Define ancestor rule for traversing block hierarchy
    const ancestorRule = `[
      [ (ancestor ?b ?a)
        [?a :block/children ?b] ]
      [ (ancestor ?b ?a)
        [?parent :block/children ?b]
        (ancestor ?parent ?a) ]
    ]`;

    // Get start of today
    const startOfDay = new Date();
    startOfDay.setHours(0, 0, 0, 0);

    try {
      // Query for pages modified today, including modification time for sorting
      let query = `[:find ?title ?time
          :in $ ?start_of_day %
          :where
          [?page :node/title ?title]
          (ancestor ?block ?page)
          [?block :edit/time ?time]
          [(> ?time ?start_of_day)]]`;

      if (limit !== -1) {
        query += ` :limit ${limit}`;
      }
      if (offset > 0) {
        query += ` :offset ${offset}`;
      }

      const results = await q(
        this.graph,
        query,
        [startOfDay.getTime(), ancestorRule]
      ) as [string, number][];

      if (!results || results.length === 0) {
        return {
          success: true,
          pages: [],
          message: 'No pages have been modified today'
        };
      }

      // Sort results by modification time
      results.sort((a, b) => {
        if (sort_order === 'desc') {
          return b[1] - a[1]; // Newest first
        } else {
          return a[1] - b[1]; // Oldest first
        }
      });

      // Extract unique page titles from sorted results
      const uniquePages = Array.from(new Set(results.map(([title]) => title)));

      return {
        success: true,
        pages: uniquePages,
        message: `Found ${uniquePages.length} page(s) modified today`
      };
    } catch (error: any) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to find modified pages: ${error.message}`
      );
    }
  }

  async createPage(title: string, content?: Array<{ text: string; level: number; heading?: number }>): Promise<{ success: boolean; uid: string }> {
    // Ensure title is properly formatted
    const pageTitle = String(title).trim();

    // First try to find if the page exists
    const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
    type FindResult = [string];
    const findResults = await q(this.graph, findQuery, [pageTitle]) as FindResult[];

    let pageUid: string | undefined;

    if (findResults && findResults.length > 0) {
      // Page exists, use its UID
      pageUid = findResults[0][0];
    } else {
      // Create new page
      try {
        await createRoamPage(this.graph, {
          action: 'create-page',
          page: {
            title: pageTitle
          }
        });

        // Get the new page's UID
        const results = await q(this.graph, findQuery, [pageTitle]) as FindResult[];
        if (!results || results.length === 0) {
          throw new Error('Could not find created page');
        }
        pageUid = results[0][0];
      } catch (error) {
        throw new McpError(
          ErrorCode.InternalError,
          `Failed to create page: ${error instanceof Error ? error.message : String(error)}`
        );
      }
    }

    // If content is provided, create blocks using batch operations
    if (content && content.length > 0) {
      try {
        // Convert content array to MarkdownNode format expected by convertToRoamActions
        const nodes = content.map(block => ({
          content: convertToRoamMarkdown(block.text.replace(/^#+\s*/, '')),
          level: block.level,
          ...(block.heading && { heading_level: block.heading }),
          children: []
        }));

        // Create hierarchical structure based on levels
        const rootNodes: any[] = [];
        const levelMap: { [level: number]: any } = {};

        for (const node of nodes) {
          if (node.level === 1) {
            rootNodes.push(node);
            levelMap[1] = node;
          } else {
            const parentLevel = node.level - 1;
            const parent = levelMap[parentLevel];

            if (!parent) {
              throw new Error(`Invalid block hierarchy: level ${node.level} block has no parent`);
            }

            parent.children.push(node);
            levelMap[node.level] = node;
          }
        }

        // Generate batch actions for all blocks
        const actions = convertToRoamActions(rootNodes, pageUid, 'last');

        // Execute batch operation
        if (actions.length > 0) {
          const batchResult = await batchActions(this.graph, {
            action: 'batch-actions',
            actions
          });

          if (!batchResult) {
            throw new Error('Failed to create blocks');
          }
        }
      } catch (error) {
        throw new McpError(
          ErrorCode.InternalError,
          `Failed to add content to page: ${error instanceof Error ? error.message : String(error)}`
        );
      }
    }

    // Add a link to the created page on today's daily page
    try {
      const today = new Date();
      const day = today.getDate();
      const month = today.toLocaleString('en-US', { month: 'long' });
      const year = today.getFullYear();
      const formattedTodayTitle = `${month} ${day}${getOrdinalSuffix(day)}, ${year}`;

      const dailyPageQuery = `[:find ?uid .
                              :where [?e :node/title "${formattedTodayTitle}"]
                                     [?e :block/uid ?uid]]`;
      const dailyPageResult = await q(this.graph, dailyPageQuery, []);
      const dailyPageUid = dailyPageResult ? String(dailyPageResult) : null;

      if (dailyPageUid) {
        await createBlock(this.graph, {
          action: 'create-block',
          block: {
            string: `Created page: [[${pageTitle}]]`
          },
          location: {
            'parent-uid': dailyPageUid,
            order: 'last'
          }
        });
      } else {
        console.warn(`Could not find daily page with title: ${formattedTodayTitle}. Link to created page not added.`);
      }
    } catch (error) {
      console.error(`Failed to add link to daily page: ${error instanceof Error ? error.message : String(error)}`);
    }

    return { success: true, uid: pageUid };
  }

  async fetchPageByTitle(
    title: string,
    format: 'markdown' | 'raw' = 'raw'
  ): Promise<string | RoamBlock[]> {
    if (!title) {
      throw new McpError(ErrorCode.InvalidRequest, 'title is required');
    }

    // Try different case variations
    const variations = [
      title, // Original
      capitalizeWords(title), // Each word capitalized
      title.toLowerCase() // All lowercase
    ];

    let uid: string | null = null;
    for (const variation of variations) {
      const searchQuery = `[:find ?uid .
                          :where [?e :node/title "${variation}"]
                                 [?e :block/uid ?uid]]`;
      const result = await q(this.graph, searchQuery, []);
      uid = (result === null || result === undefined) ? null : String(result);
      if (uid) break;
    }

    if (!uid) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        `Page with title "${title}" not found (tried original, capitalized words, and lowercase)`
      );
    }

    // Define ancestor rule for traversing block hierarchy
    const ancestorRule = `[
      [ (ancestor ?b ?a)
        [?a :block/children ?b] ]
      [ (ancestor ?b ?a)
        [?parent :block/children ?b]
        (ancestor ?parent ?a) ]
    ]`;

    // Get all blocks under this page using ancestor rule
    const blocksQuery = `[:find ?block-uid ?block-str ?order ?parent-uid
                        :in $ % ?page-title
                        :where [?page :node/title ?page-title]
                               [?block :block/string ?block-str]
                               [?block :block/uid ?block-uid]
                               [?block :block/order ?order]
                               (ancestor ?block ?page)
                               [?parent :block/children ?block]
                               [?parent :block/uid ?parent-uid]]`;
    const blocks = await q(this.graph, blocksQuery, [ancestorRule, title]);

    if (!blocks || blocks.length === 0) {
      if (format === 'raw') {
        return [];
      }
      return `${title} (no content found)`;
    }

    // Get heading information for blocks that have it
    const headingsQuery = `[:find ?block-uid ?heading
                          :in $ % ?page-title
                          :where [?page :node/title ?page-title]
                                 [?block :block/uid ?block-uid]
                                 [?block :block/heading ?heading]
                                 (ancestor ?block ?page)]`;
    const headings = await q(this.graph, headingsQuery, [ancestorRule, title]);

    // Create a map of block UIDs to heading levels
    const headingMap = new Map<string, number>();
    if (headings) {
      for (const [blockUid, heading] of headings) {
        headingMap.set(blockUid, heading as number);
      }
    }

    // Create a map of all blocks
    const blockMap = new Map<string, RoamBlock>();
    const rootBlocks: RoamBlock[] = [];

    // First pass: Create all block objects
    for (const [blockUid, blockStr, order, parentUid] of blocks) {
      const resolvedString = await resolveRefs(this.graph, blockStr);
      const block = {
        uid: blockUid,
        string: resolvedString,
        order: order as number,
        heading: headingMap.get(blockUid) || null,
        children: []
      };
      blockMap.set(blockUid, block);

      // If no parent or parent is the page itself, it's a root block
      if (!parentUid || parentUid === uid) {
        rootBlocks.push(block);
      }
    }

    // Second pass: Build parent-child relationships
    for (const [blockUid, _, __, parentUid] of blocks) {
      if (parentUid && parentUid !== uid) {
        const child = blockMap.get(blockUid);
        const parent = blockMap.get(parentUid);
        if (child && parent && !parent.children.includes(child)) {
          parent.children.push(child);
        }
      }
    }

    // Sort blocks recursively
    const sortBlocks = (blocks: RoamBlock[]) => {
      blocks.sort((a, b) => a.order - b.order);
      blocks.forEach(block => {
        if (block.children.length > 0) {
          sortBlocks(block.children);
        }
      });
    };
    sortBlocks(rootBlocks);

    if (format === 'raw') {
      return JSON.stringify(rootBlocks);
    }

    // Convert to markdown with proper nesting
    const toMarkdown = (blocks: RoamBlock[], level: number = 0): string => {
      return blocks
        .map(block => {
          const indent = '  '.repeat(level);
          let md: string;

          // Check block heading level and format accordingly
          if (block.heading && block.heading > 0) {
            // Format as heading with appropriate number of hashtags
            const hashtags = '#'.repeat(block.heading);
            md = `${indent}${hashtags} ${block.string}`;
          } else {
            // No heading, use bullet point (current behavior)
            md = `${indent}- ${block.string}`;
          }

          if (block.children.length > 0) {
            md += '\n' + toMarkdown(block.children, level + 1);
          }
          return md;
        })
        .join('\n');
    };

    return `# ${title}\n\n${toMarkdown(rootBlocks)}`;
  }
}

```

--------------------------------------------------------------------------------
/src/server/roam-server.ts:
--------------------------------------------------------------------------------

```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import {
  CallToolRequestSchema,
  ErrorCode,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  McpError,
  Resource,
  ListToolsRequestSchema,
  ListPromptsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { initializeGraph, type Graph } from '@roam-research/roam-api-sdk';
import { API_TOKEN, GRAPH_NAME, HTTP_STREAM_PORT, SSE_PORT } from '../config/environment.js';
import { toolSchemas } from '../tools/schemas.js';
import { ToolHandlers } from '../tools/tool-handlers.js';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { createServer, IncomingMessage, ServerResponse } from 'node:http';
import { fileURLToPath } from 'node:url';
import { findAvailablePort } from '../utils/net.js';
import { CORS_ORIGIN } from '../config/environment.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Read package.json to get the version
const packageJsonPath = join(__dirname, '../../package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
const serverVersion = packageJson.version;

export class RoamServer {
  private toolHandlers: ToolHandlers;
  private graph: Graph;

  constructor() {
    // console.log('RoamServer: Constructor started.');
    try {
      this.graph = initializeGraph({
        token: API_TOKEN,
        graph: GRAPH_NAME,
      });
    } catch (error: unknown) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      throw new McpError(ErrorCode.InternalError, `Failed to initialize Roam graph: ${errorMessage}`);
    }

    try {
      this.toolHandlers = new ToolHandlers(this.graph);
    } catch (error: unknown) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      throw new McpError(ErrorCode.InternalError, `Failed to initialize tool handlers: ${errorMessage}`);
    }

    // Ensure toolSchemas is not empty before proceeding
    if (Object.keys(toolSchemas).length === 0) {
      throw new McpError(ErrorCode.InternalError, 'No tool schemas defined in src/tools/schemas.ts');
    }
    // console.log('RoamServer: Constructor finished.');
  }

  // Refactored to accept a Server instance
  private setupRequestHandlers(mcpServer: Server) {
    // List available tools
    mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: Object.values(toolSchemas),
    }));

    // List available resources
    mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => {
      const resources: Resource[] = []; // No resources, as cheatsheet is now a tool
      return { resources };
    });

    // Access resource - no resources handled directly here anymore
    mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
      throw new McpError(ErrorCode.InternalError, `Resource not found: ${request.params.uri}`);
    });

    // List available prompts
    mcpServer.setRequestHandler(ListPromptsRequestSchema, async () => {
      return { prompts: [] };
    });

    // Handle tool calls
    mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
      try {
        switch (request.params.name) {
          case 'roam_markdown_cheatsheet': {
            const content = await this.toolHandlers.getRoamMarkdownCheatsheet();
            return {
              content: [{ type: 'text', text: content }],
            };
          }
          case 'roam_remember': {
            const { memory, categories } = request.params.arguments as {
              memory: string;
              categories?: string[];
            };
            const result = await this.toolHandlers.remember(memory, categories);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_fetch_page_by_title': {
            const { title, format } = request.params.arguments as {
              title: string;
              format?: 'markdown' | 'raw';
            };
            const content = await this.toolHandlers.fetchPageByTitle(title, format);
            return {
              content: [{ type: 'text', text: content }],
            };
          }

          case 'roam_create_page': {
            const { title, content } = request.params.arguments as {
              title: string;
              content?: Array<{
                text: string;
                level: number;
              }>;
            };
            const result = await this.toolHandlers.createPage(title, content);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }


          case 'roam_import_markdown': {
            const {
              content,
              page_uid,
              page_title,
              parent_uid,
              parent_string,
              order = 'first'
            } = request.params.arguments as {
              content: string;
              page_uid?: string;
              page_title?: string;
              parent_uid?: string;
              parent_string?: string;
              order?: 'first' | 'last';
            };
            const result = await this.toolHandlers.importMarkdown(
              content,
              page_uid,
              page_title,
              parent_uid,
              parent_string,
              order
            );
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_add_todo': {
            const { todos } = request.params.arguments as { todos: string[] };
            const result = await this.toolHandlers.addTodos(todos);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_create_outline': {
            const { outline, page_title_uid, block_text_uid } = request.params.arguments as {
              outline: Array<{ text: string | undefined; level: number }>;
              page_title_uid?: string;
              block_text_uid?: string;
            };
            const result = await this.toolHandlers.createOutline(
              outline,
              page_title_uid,
              block_text_uid
            );
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_search_for_tag': {
            const { primary_tag, page_title_uid, near_tag } = request.params.arguments as {
              primary_tag: string;
              page_title_uid?: string;
              near_tag?: string;
            };
            const result = await this.toolHandlers.searchForTag(primary_tag, page_title_uid, near_tag);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_search_by_status': {
            const { status, page_title_uid, include, exclude } = request.params.arguments as {
              status: 'TODO' | 'DONE';
              page_title_uid?: string;
              include?: string;
              exclude?: string;
            };
            const result = await this.toolHandlers.searchByStatus(status, page_title_uid, include, exclude);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_search_block_refs': {
            const params = request.params.arguments as {
              block_uid?: string;
              page_title_uid?: string;
            };
            const result = await this.toolHandlers.searchBlockRefs(params);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_search_hierarchy': {
            const params = request.params.arguments as {
              parent_uid?: string;
              child_uid?: string;
              page_title_uid?: string;
              max_depth?: number;
            };

            // Validate that either parent_uid or child_uid is provided, but not both
            if ((!params.parent_uid && !params.child_uid) || (params.parent_uid && params.child_uid)) {
              throw new McpError(
                ErrorCode.InvalidRequest,
                'Either parent_uid or child_uid must be provided, but not both'
              );
            }

            const result = await this.toolHandlers.searchHierarchy(params);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_find_pages_modified_today': {
            const { max_num_pages } = request.params.arguments as {
              max_num_pages?: number;
            };
            const result = await this.toolHandlers.findPagesModifiedToday(max_num_pages || 50);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_search_by_text': {
            const params = request.params.arguments as {
              text: string;
              page_title_uid?: string;
            };
            const result = await this.toolHandlers.searchByText(params);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_search_by_date': {
            const params = request.params.arguments as {
              start_date: string;
              end_date?: string;
              type: 'created' | 'modified' | 'both';
              scope: 'blocks' | 'pages' | 'both';
              include_content: boolean;
            };
            const result = await this.toolHandlers.searchByDate(params);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }


          case 'roam_recall': {
            const { sort_by = 'newest', filter_tag } = request.params.arguments as {
              sort_by?: 'newest' | 'oldest';
              filter_tag?: string;
            };
            const result = await this.toolHandlers.recall(sort_by, filter_tag);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }


          case 'roam_datomic_query': {
            const { query, inputs } = request.params.arguments as {
              query: string;
              inputs?: unknown[];
            };
            const result = await this.toolHandlers.executeDatomicQuery({ query, inputs });
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_process_batch_actions': {
            const { actions } = request.params.arguments as {
              actions: any[];
            };
            const result = await this.toolHandlers.processBatch(actions);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          case 'roam_fetch_block_with_children': {
            const { block_uid, depth } = request.params.arguments as {
              block_uid: string;
              depth?: number;
            };
            const result = await this.toolHandlers.fetchBlockWithChildren(block_uid, depth);
            return {
              content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
            };
          }

          default:
            throw new McpError(
              ErrorCode.MethodNotFound,
              `Unknown tool: ${request.params.name}`
            );
        }
      } catch (error: unknown) {
        if (error instanceof McpError) {
          throw error;
        }
        const errorMessage = error instanceof Error ? error.message : String(error);
        throw new McpError(
          ErrorCode.InternalError,
          `Roam API error: ${errorMessage}`
        );
      }
    });
  }

  async run() {
    // console.log('RoamServer: run() method started.');
    try {
      // console.log('RoamServer: Attempting to create stdioMcpServer...');
      const stdioMcpServer = new Server(
        {
          name: 'roam-research',
          version: serverVersion,
        },
        {
          capabilities: {
            tools: {
              ...Object.fromEntries(
                (Object.keys(toolSchemas) as Array<keyof typeof toolSchemas>).map((toolName) => [toolName, toolSchemas[toolName].inputSchema])
              ),
            },
            resources: {}, // No resources exposed via capabilities
            prompts: {}, // No prompts exposed via capabilities
          },
        }
      );
      // console.log('RoamServer: stdioMcpServer created. Setting up request handlers...');
      this.setupRequestHandlers(stdioMcpServer);
      // console.log('RoamServer: stdioMcpServer handlers setup complete. Connecting transport...');

      const stdioTransport = new StdioServerTransport();
      await stdioMcpServer.connect(stdioTransport);
      // console.log('RoamServer: stdioTransport connected. Attempting to create httpMcpServer...');

      const httpMcpServer = new Server(
        {
          name: 'roam-research-http', // A distinct name for the HTTP server
          version: serverVersion,
        },
        {
          capabilities: {
            tools: {
              ...Object.fromEntries(
                (Object.keys(toolSchemas) as Array<keyof typeof toolSchemas>).map((toolName) => [toolName, toolSchemas[toolName].inputSchema])
              ),
            },
            resources: { // No resources exposed via capabilities
            },
            prompts: {}, // No prompts exposed via capabilities
          },
        }
      );
      // console.log('RoamServer: httpMcpServer created. Setting up request handlers...');
      this.setupRequestHandlers(httpMcpServer);
      // console.log('RoamServer: httpMcpServer handlers setup complete. Connecting transport...');

      const httpStreamTransport = new StreamableHTTPServerTransport({
        sessionIdGenerator: () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15),
      });
      await httpMcpServer.connect(httpStreamTransport);
      // console.log('RoamServer: httpStreamTransport connected.');

      const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
        // Set CORS headers
        res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
        res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
        res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

        // Handle preflight OPTIONS requests
        if (req.method === 'OPTIONS') {
          res.writeHead(204); // No Content
          res.end();
          return;
        }

        try {
          await httpStreamTransport.handleRequest(req, res);
        } catch (error) {
          // // console.error('HTTP Stream Server error:', error);
          if (!res.headersSent) {
            res.writeHead(500, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ error: 'Internal Server Error' }));
          }
        }
      });

      const availableHttpPort = await findAvailablePort(parseInt(HTTP_STREAM_PORT));
      httpServer.listen(availableHttpPort, () => {
        // // console.log(`MCP Roam Research server running HTTP Stream on port ${availableHttpPort}`);
      });

    } catch (error: unknown) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      throw new McpError(ErrorCode.InternalError, `Failed to connect MCP server: ${errorMessage}`);
    }
  }
}

```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# Changelog

v0.36.3 - 2025-08-30

- FEATURE: Implemented `prompts/list` method for MCP server, returning an empty array of prompts.
- FIXED: Removed `roam-markdown-cheatsheet.md` from advertised resources in MCP server capabilities to align with its tool-only access.

v0.36.2 - 2025-08-28

- ENHANCED: `roam_datomic_query` tool
  - Added `regexFilter`, `regexFlags`, and `regexTargetField` parameters for client-side regex filtering of results.
  - Updated description to reflect enhanced filtering capabilities.

v0.36.1 - 2025-08-28

- ENHANCED: `roam_find_pages_modified_today` tool
  - Added `limit`, `offset`, and `sort_order` parameters for pagination and sorting.

v1.36.0 - 2025-08-28

- ENHANCED: `roam_search_for_tag` and `roam_search_by_text` tools
  - Added `offset` parameter for pagination support.
- ENHANCED: `roam_search_for_tag` tool
  - Implemented `near_tag` and `exclude_tag` parameters for more precise tag-based filtering.
- ENHANCED: `roam_datomic_query` tool
  - Updated description to clarify optimal use cases (Regex, Complex Boolean Logic, Arbitrary Sorting, Proximity Search).

v.0.35.1 - 2025-08-23 9:33

- 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.

v.0.35.0 - 2025-08-23 

- ENHANCED: `roam_import_markdown` tool
  - Now returns a nested object structure for `created_uids`, reflecting the hierarchy of the imported content, including `uid`, `text`, `order`, and `children`.
  - If a `parent_string` is provided and the block does not exist, it will be created automatically.
- FIXED: Block ordering issue in `roam_import_markdown` and `roam_create_outline`. Nested outlines are now created in the correct order.
- FIXED: Duplication issue in the response of `roam_fetch_block_with_children`.

v.0.32.4

- FIXED: Memory allocation issue (`FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory`)
  - Removed `console.log` statements from `src/tools/operations/outline.ts` to adhere to MCP server stdio communication rules.
  - 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.
- ENHANCED: `roam_create_outline` tool
  - Successfully created outlines with nested code blocks, confirming the fix for memory allocation issues.

v.0.32.1

- ENHANCED: `roam_create_outline` tool
  - The tool now returns a nested structure of UIDs (`NestedBlock[]`) for all created blocks, including children, accurately reflecting the outline hierarchy.
  - Implemented a recursive fetching mechanism (`fetchBlockWithChildren` helper) to retrieve all nested block UIDs and their content after creation.
  - Fixed an issue where the `created_uids` array was only returning top-level block UIDs.
  - Corrected the Datomic query used for fetching children to ensure only direct children are retrieved, resolving previous duplication and incorrect nesting issues.
  - Removed `console.log` and `console.warn` statements from `src/tools/operations/outline.ts` to adhere to MCP server stdio communication rules.
- ADDED: `NestedBlock` interface in `src/tools/types/index.ts` to represent the hierarchical structure of created blocks.

v.0.32.3

- ENHANCED: `roam_create_page` tool
  - Now creates a block on the daily page linking to the newly created page, formatted as `Create [[Page Title]]`.

v.0.32.2

- FIXED: `roam_create_outline` now correctly respects the order of top-level blocks.
  - 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.

v.0.30.10

- ENHANCED: `roam_markdown_cheatsheet` tool
  - 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.
- UPDATED: The description of `roam_markdown_cheatsheet` in `src/tools/schemas.ts` to reflect the new functionality.

v.0.30.9

- FIXED: `roam_fetch_block_with_children` tool to use a more efficient batched recursive approach, avoiding "Too many requests" and other API errors.
- The tool now fetches all children of a block in a single query per level of depth, significantly reducing the number of API calls.

v.0.30.8

- ADDED: `roam_fetch_block_with_children` tool
  - Fetches a block by its UID along with its hierarchical children down to a specified depth.
  - Automatically handles Roam's `((UID))` formatting, extracting the raw UID for lookup.
  - This tool provides a direct and structured way to retrieve specific block content and its nested hierarchy.

v.0.30.7

- 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.
  - Updated the tool's schema in `src/tools/schemas.ts` with more explicit instructions to guide the LLM in generating valid hierarchical structures.
  - Added stricter validation in `src/tools/operations/outline.ts` to reject outlines that do not start at level 1, providing a clearer error message.
  - Optimized page creation

v.0.30.6

- FIXED: `roam_create_page` now correctly strips heading markers (`#`) from block content before creation.
- FIXED: Block creation order is now correct. Removed the incorrect `.reverse()` call in `convertToRoamActions` and the corresponding workaround in `createBlock`.
- UPDATED: the cheat sheet for ordinal dates.

v.0.30.5

- FIXED: `roam_search_for_tag` now correctly scopes searches to a specific page when `page_title_uid` is provided.
  - The Datalog query in `src/search/tag-search.ts` was updated to include the `targetPageUid` in the `where` clause.

v.0.30.4

- FIXED: Tools not loading properly in Gemini CLI
- Clarified outline description
- FIXED: `roam_process_batch_actions` `heading` enum type in `schemas.ts` for Gemini CLI compatibility.

v.0.30.3

- ADDED: `roam_markdown_cheatsheet` tool
  - Provides the content of the Roam Markdown Cheatsheet directly via a tool call.
  - The content is now read dynamically from `Roam_Markdown_Cheatsheet.md` on the filesystem.
  - **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.
- REMOVED: Roam Markdown Cheatsheet as a direct resource
  - The cheatsheet is no longer exposed as a static resource; it is now accessed programmatically through the new `roam_markdown_cheatsheet` tool.
- ADDED: package.json new utilty scripts

v.0.30.2

- ADDED: 4x4 table creation example
  - Created a 4x4 table with random data on the "Testing Tables" page, demonstrating proper Roam table structure.
- ENHANCED: `Roam_Markdown_Cheatsheet.md`
  - Updated the "Roam Tables" section with a more detailed explanation of table structure, including proper indentation levels for headers and data cells.
- ENHANCED: `src/tools/schemas.ts`
  - Clarified the distinction between `roam_create_outline` and `roam_process_batch_actions` in their respective descriptions, providing guidance on their best use cases.

v.0.30.1

- ENHANCED: `roam_process_batch_actions` tool description
  - Clarified that Roam-flavored markdown, including block embedding with `((UID))` syntax, is supported within the `string` property for `create-block` and `update-block` actions.
  - 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.
  - Clarified the `block_text_uid` description for `roam_create_outline` to explicitly mention defaulting to the daily page.
  - Simplified the top-level description for `roam_datomic_query`.
  - Refined the introductory sentence for `roam_datomic_query`.
- ADDED: "Example Prompts" section in `README.md`
  - 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.

v.0.30.0

- DEPRECATED: **Generic Block Manipulation Tools**:
  - `roam_create_block`: Deprecated in favor of `roam_process_batch_actions` (action: `create-block`).
  - `roam_update_block`: Deprecated in favor of `roam_process_batch_actions` (action: `update-block`).
  - `roam_update_multiple_blocks`: Deprecated in favor of `roam_process_batch_actions` for batch updates.
    Users are encouraged to use `roam_process_batch_actions` for all direct, generic block manipulations due to its enhanced flexibility and batch processing capabilities.
- REFACTORED: `roam_add_todo` to internally use `roam_process_batch_actions` for all block creations, enhancing efficiency and consistency.
- REFACTORED: `roam_remember` to internally use `roam_process_batch_actions` for all block creations, enhancing efficiency and consistency.
- ENHANCED: `roam_create_outline`
  - Refactored to internally use `roam_process_batch_actions` for all block creations, including parent blocks.
  - Added support for `children_view_type` in outline items, allowing users to specify the display format (bullet, document, numbered) for nested blocks.
- REFACTORED: `roam_import_markdown` to internally use `roam_process_batch_actions` for all content imports, enhancing efficiency and consistency.

v.0.29.0

- 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.
- 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.
- 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.

v.0.28.0

- 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`).
- 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.

v.0.27.0

- ADDED: SSE (Server-Sent Events) transport support for legacy clients.
- 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.
- ENHANCED: Each transport now runs on its own isolated `Server` instance, improving stability and preventing cross-transport interference.
- UPDATED: `src/config/environment.ts` to include `SSE_PORT` for configurable SSE endpoint (defaults to `8087`).

v.0.26.0

- ENHANCED: Added HTTP Stream Transport support
- Implemented dual transport support for Stdio and HTTP Stream, allowing communication via both local processes and network connections.
- Updated `src/config/environment.ts` to include `HTTP_STREAM_PORT` for configurable HTTP Stream endpoint.
- Modified `src/server/roam-server.ts` to initialize and connect `StreamableHTTPServerTransport` alongside `StdioServerTransport`.
- Configured HTTP server to listen on `HTTP_STREAM_PORT` and handle requests via `StreamableHTTPServerTransport`.

v.0.25.7

- FIXED: `roam_fetch_page_by_title` schema definition
- Corrected missing `name` property and proper nesting of `inputSchema` in `src/tools/schemas.ts`.
- ENHANCED: Dynamic tool loading and error reporting
- Implemented dynamic loading of tool capabilities from `toolSchemas` in `src/server/roam-server.ts` to ensure consistency.
- 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.
- CENTRALIZED: Versioning in `src/server/roam-server.ts`
- Modified `src/server/roam-server.ts` to dynamically read the version from `package.json`, ensuring a single source of truth for the project version.

v.0.25.6

- ADDED: Docker support
- Created a `Dockerfile` for containerization.
- Added an `npm start` script to `package.json` for running the application within the Docker container.

v.0.25.5

- ENHANCED: `roam_create_outline` tool for better heading and nesting support
- Reverted previous change in `src/tools/operations/outline.ts` to preserve original indentation for outline items.
- Refined `parseMarkdown` in `src/markdown-utils.ts` to correctly parse markdown heading syntax (`#`, `##`, `###`) while maintaining the block's hierarchical level based on indentation.
- 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.
- 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.

v.0.25.4

- ADDED: `format` parameter to `roam_fetch_page_by_title` tool
- Allows fetching page content as raw JSON data (blocks with UIDs) or markdown.
- Updated `fetchPageByTitle` in `src/tools/operations/pages.ts` to return stringified JSON for raw format.
- Updated `roam_fetch_page_by_title` schema in `src/tools/schemas.ts` to include `format` parameter with 'raw' as default.
- Updated `fetchPageByTitle` handler in `src/tools/tool-handlers.ts` to pass `format` parameter.
- Updated `roam_fetch_page_by_title` case in `src/server/roam-server.ts` to extract and pass `format` parameter.

v.0.25.3

- FIXED: roam_create_block multiline content ordering issue
- Root cause: Simple newline-separated content was being created in reverse order
- Solution: Added logic to detect simple newline-separated content and reverse the nodes array to maintain original order
- Fix is specific to simple multiline content without markdown formatting, preserving existing behavior for complex markdown

v.0.25.2

- FIXED: roam_create_block heading formatting issue
- Root cause: Missing heading parameter extraction in server request handler
- Solution: Added heading parameter to roam_create_block handler in roam-server.ts
- Also removed problematic default: 0 from heading schema definition
- Heading formatting now works correctly for both single and multi-line blocks
- roam_create_block now properly applies H1, H2, and H3 formatting when heading parameter is provided

v.0.25.1

- Investigated heading formatting issue in roam_create_block tool
- Attempted multiple fixes: direct createBlock API → batchActions → convertToRoamActions → direct batch action creation
- Confirmed roam_create_page works correctly for heading formatting
- Identified that heading formatting fails specifically for single block creation via roam_create_block
- Issue remains unresolved despite extensive troubleshooting and multiple implementation approaches
- Current status: roam_create_block does not apply heading formatting, investigation ongoing

v.0.25.0

- Updated roam_create_page to use batchActions

v.0.24.6

- Updated roam_create_page to use explicit levels

v.0.24.5

- Enhanced createOutline to properly handle block_text_uid as either a 9-character UID or string title
- Added proper detection and use of existing blocks when given a valid block UID
- Improved error messages to be more specific about block operations

v.0.24.4

- Clarified roam_search_by_date and roam_fetch_page_by_title when it comes to searching for daily pages vs. blocks by date

v.0.24.3

- Clarified roam_update_multiple_blocks
- Added a variable to roam_find_pages_modified_today

v.0.24.2

- Added sort_by and filter_tag to roam_recall

v.0.24.1

- Fixed searchByStatus for TODO checks
- Added resolution of references to various tools

v.0.23.2

- Fixed create_page tool as first-level blocks were created in reversed order

v.0.23.1

- Fixed roam_outline tool not writing properly

v.0.23.0

- Added advanced, more flexible datomic query

v.0.22.1

- Important description change in roam_remember

v0.22.0

- Restructured search functionality into dedicated directory with proper TypeScript support
- Fixed TypeScript errors and import paths throughout the codebase
- Improved outline creation to maintain exact input array order
- Enhanced recall() method to fetch memories from both tag searches and dedicated memories page
- Maintained backward compatibility while improving code organization

v0.21.0

- Added roam_recall tool to recall memories from all tags and the page itself.

v0.20.0

- 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.

v0.19.0

- Changed default case-sensitivity behavior in search tools to match Roam's native behavior (now defaults to true)
- Updated case-sensitivity handling in findBlockWithRetry, searchByStatus, searchForTag, and searchByDate tools

v0.18.0

- Added roam_search_by_date tool to search for blocks and pages based on creation or modification dates
- Added support for date range filtering and content inclusion options

v0.17.0

- Enhanced roam_update_block tool with transform pattern support, allowing regex-based content transformations
- Added ability to update blocks with either direct content or pattern-based transformations

v0.16.0

- Added roam_search_by_text tool to search for blocks containing specific text, with optional page scope and case sensitivity
- Fixed roam_search_by_tag

v.0.15.0

- Added roam_find_pages_modified_today tool to search for pages modified since midnight today

v.0.14

```

--------------------------------------------------------------------------------
/src/tools/operations/outline.ts:
--------------------------------------------------------------------------------

```typescript
import { Graph, q, createPage, createBlock, batchActions } from '@roam-research/roam-api-sdk';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { formatRoamDate } from '../../utils/helpers.js';
import { capitalizeWords, getNestedUids, getNestedUidsByText } from '../helpers/text.js';
import {
  parseMarkdown,
  convertToRoamActions,
  convertToRoamMarkdown,
  hasMarkdownTable,
  type BatchAction
} from '../../markdown-utils.js';
import type { OutlineItem, NestedBlock } from '../types/index.js';

export class OutlineOperations {
  constructor(private graph: Graph) { }

  /**
   * Helper function to find block with improved relationship checks
   */
  private async findBlockWithRetry(pageUid: string, blockString: string, maxRetries = 5, initialDelay = 1000): Promise<string> {
    // Try multiple query strategies
    const queries = [
      // Strategy 1: Direct page and string match
      `[:find ?b-uid ?order
          :where [?p :block/uid "${pageUid}"]
                 [?b :block/page ?p]
                 [?b :block/string "${blockString}"]
                 [?b :block/order ?order]
                 [?b :block/uid ?b-uid]]`,

      // Strategy 2: Parent-child relationship
      `[:find ?b-uid ?order
          :where [?p :block/uid "${pageUid}"]
                 [?b :block/parents ?p]
                 [?b :block/string "${blockString}"]
                 [?b :block/order ?order]
                 [?b :block/uid ?b-uid]]`,

      // Strategy 3: Broader page relationship
      `[:find ?b-uid ?order
          :where [?p :block/uid "${pageUid}"]
                 [?b :block/page ?page]
                 [?p :block/page ?page]
                 [?b :block/string "${blockString}"]
                 [?b :block/order ?order]
                 [?b :block/uid ?b-uid]]`
    ];

    for (let retry = 0; retry < maxRetries; retry++) {
      // Try each query strategy
      for (const queryStr of queries) {
        const blockResults = await q(this.graph, queryStr, []) as [string, number][];
        if (blockResults && blockResults.length > 0) {
          // Use the most recently created block
          const sorted = blockResults.sort((a, b) => b[1] - a[1]);
          return sorted[0][0];
        }
      }

      // Exponential backoff
      const delay = initialDelay * Math.pow(2, retry);
      await new Promise(resolve => setTimeout(resolve, delay));
    }

    throw new McpError(
      ErrorCode.InternalError,
      `Failed to find block "${blockString}" under page "${pageUid}" after trying multiple strategies`
    );
  };

  /**
   * Helper function to create and verify block with improved error handling
   */
  private async createAndVerifyBlock(
    content: string,
    parentUid: string,
    maxRetries = 5,
    initialDelay = 1000,
    isRetry = false
  ): Promise<string> {
    try {
      // Initial delay before any operations
      if (!isRetry) {
        await new Promise(resolve => setTimeout(resolve, initialDelay));
      }

      for (let retry = 0; retry < maxRetries; retry++) {
        console.log(`Attempt ${retry + 1}/${maxRetries} to create block "${content}" under "${parentUid}"`);

        // Create block using batchActions
        const batchResult = await batchActions(this.graph, {
          action: 'batch-actions',
          actions: [{
            action: 'create-block',
            location: {
              'parent-uid': parentUid,
              order: 'last'
            },
            block: { string: content }
          }]
        });

        if (!batchResult) {
          throw new McpError(
            ErrorCode.InternalError,
            `Failed to create block "${content}" via batch action`
          );
        }

        // Wait with exponential backoff
        const delay = initialDelay * Math.pow(2, retry);
        await new Promise(resolve => setTimeout(resolve, delay));

        try {
          // Try to find the block using our improved findBlockWithRetry
          return await this.findBlockWithRetry(parentUid, content);
        } catch (error: any) {
          const errorMessage = error instanceof Error ? error.message : String(error);
          // console.log(`Failed to find block on attempt ${retry + 1}: ${errorMessage}`); // Removed console.log
          if (retry === maxRetries - 1) throw error;
        }
      }

      throw new McpError(
        ErrorCode.InternalError,
        `Failed to create and verify block "${content}" after ${maxRetries} attempts`
      );
    } catch (error) {
      // If this is already a retry, throw the error
      if (isRetry) throw error;

      // Otherwise, try one more time with a clean slate
      // console.log(`Retrying block creation for "${content}" with fresh attempt`); // Removed console.log
      await new Promise(resolve => setTimeout(resolve, initialDelay * 2));
      return this.createAndVerifyBlock(content, parentUid, maxRetries, initialDelay, true);
    }
  };

  /**
   * Helper function to check if string is a valid Roam UID (9 characters)
   */
  private isValidUid = (str: string): boolean => {
    return typeof str === 'string' && str.length === 9;
  };

  /**
   * Helper function to fetch a block and its children recursively
   */
  private async fetchBlockWithChildren(blockUid: string, level: number = 1): Promise<NestedBlock | null> {
    const query = `
        [:find ?childUid ?childString ?childOrder
         :in $ ?parentUid
         :where
         [?parentEntity :block/uid ?parentUid]
         [?parentEntity :block/children ?childEntity] ; This ensures direct children
         [?childEntity :block/uid ?childUid]
         [?childEntity :block/string ?childString]
         [?childEntity :block/order ?childOrder]]
      `;

    const blockQuery = `
        [:find ?string
         :in $ ?uid
         :where
         [?e :block/uid ?uid]
         [?e :block/string ?string]]
      `;

    try {
      const blockStringResult = await q(this.graph, blockQuery, [blockUid]) as [string][];
      if (!blockStringResult || blockStringResult.length === 0) {
        return null;
      }
      const text = blockStringResult[0][0];

      const childrenResults = await q(this.graph, query, [blockUid]) as [string, string, number][];
      const children: NestedBlock[] = [];

      if (childrenResults && childrenResults.length > 0) {
        // Sort children by order
        const sortedChildren = childrenResults.sort((a, b) => a[2] - b[2]);

        for (const childResult of sortedChildren) {
          const childUid = childResult[0];
          const nestedChild = await this.fetchBlockWithChildren(childUid, level + 1);
          if (nestedChild) {
            children.push(nestedChild);
          }
        }
      }

      // The order of the root block is not available from this query, so we set it to 0
      return { uid: blockUid, text, level, order: 0, children: children.length > 0 ? children : undefined };
    } catch (error: any) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to fetch block with children for UID "${blockUid}": ${error.message}`
      );
    }
  };

  /**
   * Recursively fetches a nested structure of blocks under a given root block UID.
   */
  private async fetchNestedStructure(rootUid: string): Promise<NestedBlock[]> {
    const query = `[:find ?child-uid ?child-string ?child-order
                    :in $ ?parent-uid
                    :where
                      [?parent :block/uid ?parent-uid]
                      [?parent :block/children ?child]
                      [?child :block/uid ?child-uid]
                      [?child :block/string ?child-string]
                      [?child :block/order ?child-order]]`;
    const directChildrenResult = await q(this.graph, query, [rootUid]) as [string, string, number][];

    if (directChildrenResult.length === 0) {
      return [];
    }

    const nestedBlocks: NestedBlock[] = [];
    for (const [childUid, childString, childOrder] of directChildrenResult) {
      const children = await this.fetchNestedStructure(childUid);
      nestedBlocks.push({
        uid: childUid,
        text: childString,
        level: 0, // Level is not easily determined here, so we set it to 0
        children: children,
        order: childOrder
      });
    }

    return nestedBlocks.sort((a, b) => a.order - b.order);
  }

  /**
   * Creates an outline structure on a Roam Research page, optionally under a specific block.
   *
   * @param outline - An array of OutlineItem objects, each containing text and a level.
   *                  Markdown heading syntax (#, ##, ###) in the text will be recognized
   *                  and converted to Roam headings while preserving the outline's hierarchical
   *                  structure based on indentation.
   * @param page_title_uid - The title or UID of the page where the outline should be created.
   *                         If not provided, today's daily page will be used.
   * @param block_text_uid - Optional. The text content or UID of an existing block under which
   *                         the outline should be inserted. If a text string is provided and
   *                         no matching block is found, a new block with that text will be created
   *                         on the page to serve as the parent. If a UID is provided and the block
   *                         is not found, an error will be thrown.
   * @returns An object containing success status, page UID, parent UID, and a nested array of created block UIDs.
   */
  async createOutline(
    outline: Array<OutlineItem>,
    page_title_uid?: string,
    block_text_uid?: string
  ): Promise<{ success: boolean; page_uid: string; parent_uid: string; created_uids: NestedBlock[] }> {
    // Validate input
    if (!Array.isArray(outline) || outline.length === 0) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        'outline must be a non-empty array'
      );
    }

    // Filter out items with undefined text
    const validOutline = outline.filter(item => item.text !== undefined);
    if (validOutline.length === 0) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        'outline must contain at least one item with text'
      );
    }

    // Validate outline structure
    const invalidItems = validOutline.filter(item =>
      typeof item.level !== 'number' ||
      item.level < 1 ||
      item.level > 10 ||
      typeof item.text !== 'string' ||
      item.text.trim().length === 0
    );

    if (invalidItems.length > 0) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        'outline contains invalid items - each item must have a level (1-10) and non-empty text'
      );
    }

    // Helper function to find or create page with retries
    const findOrCreatePage = async (titleOrUid: string, maxRetries = 3, delayMs = 500): Promise<string> => {
      // First try to find by title
      const titleQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
      const variations = [
        titleOrUid, // Original
        capitalizeWords(titleOrUid), // Each word capitalized
        titleOrUid.toLowerCase() // All lowercase
      ];

      for (let retry = 0; retry < maxRetries; retry++) {
        // Try each case variation
        for (const variation of variations) {
          const findResults = await q(this.graph, titleQuery, [variation]) as [string][];
          if (findResults && findResults.length > 0) {
            return findResults[0][0];
          }
        }

        // If not found as title, try as UID
        const uidQuery = `[:find ?uid
                          :where [?e :block/uid "${titleOrUid}"]
                                 [?e :block/uid ?uid]]`;
        const uidResult = await q(this.graph, uidQuery, []);
        if (uidResult && uidResult.length > 0) {
          return uidResult[0][0];
        }

        // If still not found and this is the first retry, try to create the page
        if (retry === 0) {
          const success = await createPage(this.graph, {
            action: 'create-page',
            page: { title: titleOrUid }
          });

          // Even if createPage returns false, the page might still have been created
          // Wait a bit and continue to next retry
          await new Promise(resolve => setTimeout(resolve, delayMs));
          continue;
        }

        if (retry < maxRetries - 1) {
          await new Promise(resolve => setTimeout(resolve, delayMs));
        }
      }

      throw new McpError(
        ErrorCode.InvalidRequest,
        `Failed to find or create page "${titleOrUid}" after multiple attempts`
      );
    };

    // Get or create the target page
    const targetPageUid = await findOrCreatePage(
      page_title_uid || formatRoamDate(new Date())
    );

    // Get or create the parent block
    let targetParentUid: string;
    if (!block_text_uid) {
      targetParentUid = targetPageUid;
    } else {
      try {
        if (this.isValidUid(block_text_uid)) {
          // First try to find block by UID
          const uidQuery = `[:find ?uid
                           :where [?e :block/uid "${block_text_uid}"]
                                  [?e :block/uid ?uid]]`;
          const uidResult = await q(this.graph, uidQuery, []) as [string][];

          if (uidResult && uidResult.length > 0) {
            // Use existing block if found
            targetParentUid = uidResult[0][0];
          } else {
            throw new McpError(
              ErrorCode.InvalidRequest,
              `Block with UID "${block_text_uid}" not found`
            );
          }
        } else {
          // Create header block and get its UID if not a valid UID
          targetParentUid = await this.createAndVerifyBlock(block_text_uid, targetPageUid);
        }
      } catch (error: any) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        throw new McpError(
          ErrorCode.InternalError,
          `Failed to ${this.isValidUid(block_text_uid) ? 'find' : 'create'} block "${block_text_uid}": ${errorMessage}`
        );
      }
    }

    // Initialize result variable
    let result;

    try {
      // Validate level sequence
      if (validOutline.length > 0 && validOutline[0].level !== 1) {
        throw new McpError(
          ErrorCode.InvalidRequest,
          'Invalid outline structure - the first item must be at level 1'
        );
      }

      let prevLevel = 0;
      for (const item of validOutline) {
        // Level should not increase by more than 1 at a time
        if (item.level > prevLevel + 1) {
          throw new McpError(
            ErrorCode.InvalidRequest,
            `Invalid outline structure - level ${item.level} follows level ${prevLevel}`
          );
        }
        prevLevel = item.level;
      }

      // Convert outline items to markdown-like structure
      const markdownContent = validOutline
        .map(item => {
          const indent = '  '.repeat(item.level - 1);
          // If the item text starts with a markdown heading (e.g., #, ##, ###),
          // treat it as a direct heading without adding a bullet or outline indentation.
          // NEW CHANGE: Handle standalone code blocks - do not prepend bullet
          const isCodeBlock = item.text?.startsWith('```') && item.text.endsWith('```') && item.text.includes('\n');
          return isCodeBlock ? `${indent}${item.text?.trim()}` : `${indent}- ${item.text?.trim()}`;
        })
        .join('\n');

      // Convert to Roam markdown format
      const convertedContent = convertToRoamMarkdown(markdownContent);

      // Parse markdown into hierarchical structure
      // We pass the original OutlineItem properties (heading, children_view_type)
      // along with the parsed content to the nodes.
      const nodes = parseMarkdown(convertedContent).map((node, index) => {
        const outlineItem = validOutline[index];
        return {
          ...node,
          ...(outlineItem?.heading && { heading_level: outlineItem.heading }),
          ...(outlineItem?.children_view_type && { children_view_type: outlineItem.children_view_type })
        };
      });

      // Convert nodes to batch actions
      const actions = convertToRoamActions(nodes, targetParentUid, 'last');

      if (actions.length === 0) {
        throw new McpError(
          ErrorCode.InvalidRequest,
          'No valid actions generated from outline'
        );
      }

      // Execute batch actions to create the outline
      result = await batchActions(this.graph, {
        action: 'batch-actions',
        actions
      }).catch(error => {
        throw new McpError(
          ErrorCode.InternalError,
          `Failed to create outline blocks: ${error.message}`
        );
      });

      if (!result) {
        throw new McpError(
          ErrorCode.InternalError,
          'Failed to create outline blocks - no result returned'
        );
      }
    } catch (error: any) {
      if (error instanceof McpError) throw error;
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to create outline: ${error.message}`
      );
    }

    // Post-creation verification to get actual UIDs for top-level blocks and their children
    const createdBlocks: NestedBlock[] = [];
    // Only query for top-level blocks (level 1) based on the original outline input
    const topLevelOutlineItems = validOutline.filter(item => item.level === 1);

    for (const item of topLevelOutlineItems) {
      try {
        // Assert item.text is a string as it's filtered earlier to be non-undefined and non-empty
        const foundUid = await this.findBlockWithRetry(targetParentUid, item.text!);
        if (foundUid) {
          const nestedBlock = await this.fetchBlockWithChildren(foundUid);
          if (nestedBlock) {
            createdBlocks.push(nestedBlock);
          }
        }
      } catch (error: any) {
        // This is a warning because even if one block fails to fetch, others might succeed.
        // The error will be logged but not re-thrown to allow partial success reporting.
        // console.warn(`Could not fetch nested block for "${item.text}": ${error.message}`);
      }
    }

    return {
      success: true,
      page_uid: targetPageUid,
      parent_uid: targetParentUid,
      created_uids: createdBlocks
    };
  }

  async importMarkdown(
    content: string,
    page_uid?: string,
    page_title?: string,
    parent_uid?: string,
    parent_string?: string,
    order: 'first' | 'last' = 'last'
  ): Promise<{ success: boolean; page_uid: string; parent_uid: string; created_uids: NestedBlock[] }> {
    // First get the page UID
    let targetPageUid = page_uid;

    if (!targetPageUid && page_title) {
      const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
      const findResults = await q(this.graph, findQuery, [page_title]) as [string][];

      if (findResults && findResults.length > 0) {
        targetPageUid = findResults[0][0];
      } else {
        throw new McpError(
          ErrorCode.InvalidRequest,
          `Page with title "${page_title}" not found`
        );
      }
    }

    // If no page specified, use today's date page
    if (!targetPageUid) {
      const today = new Date();
      const dateStr = formatRoamDate(today);

      const findQuery = `[:find ?uid :in $ ?title :where [?e :node/title ?title] [?e :block/uid ?uid]]`;
      const findResults = await q(this.graph, findQuery, [dateStr]) as [string][];

      if (findResults && findResults.length > 0) {
        targetPageUid = findResults[0][0];
      } else {
        // Create today's page
        try {
          await createPage(this.graph, {
            action: 'create-page',
            page: { title: dateStr }
          });

          const results = await q(this.graph, findQuery, [dateStr]) as [string][];
          if (!results || results.length === 0) {
            throw new McpError(
              ErrorCode.InternalError,
              'Could not find created today\'s page'
            );
          }
          targetPageUid = results[0][0];
        } catch (error) {
          throw new McpError(
            ErrorCode.InternalError,
            `Failed to create today's page: ${error instanceof Error ? error.message : String(error)}`
          );
        }
      }
    }

    // Now get the parent block UID
    let targetParentUid = parent_uid;

    if (!targetParentUid && parent_string) {
      if (!targetPageUid) {
        throw new McpError(
          ErrorCode.InvalidRequest,
          'Must provide either page_uid or page_title when using parent_string'
        );
      }

      // Find block by exact string match within the page
      const findBlockQuery = `[:find ?b-uid
                             :in $ ?page-uid ?block-string
                             :where [?p :block/uid ?page-uid]
                                    [?b :block/page ?p]
                                    [?b :block/string ?block-string]
                                    [?b :block/uid ?b-uid]]`;
      const blockResults = await q(this.graph, findBlockQuery, [targetPageUid, parent_string]) as [string][];

      if (blockResults && blockResults.length > 0) {
        targetParentUid = blockResults[0][0];
      } else {
        // If parent_string block doesn't exist, create it
        targetParentUid = await this.createAndVerifyBlock(parent_string, targetPageUid);
      }
    }

    // If no parent specified, use page as parent
    if (!targetParentUid) {
      targetParentUid = targetPageUid;
    }

    // Always use parseMarkdown for content with multiple lines or any markdown formatting
    const isMultilined = content.includes('\n');

    if (isMultilined) {
      // Parse markdown into hierarchical structure
      const convertedContent = convertToRoamMarkdown(content);
      const nodes = parseMarkdown(convertedContent);

      // Convert markdown nodes to batch actions
      const actions = convertToRoamActions(nodes, targetParentUid, order);

      // Execute batch actions to add content
      const result = await batchActions(this.graph, {
        action: 'batch-actions',
        actions
      });

      if (!result) {
        throw new McpError(
          ErrorCode.InternalError,
          'Failed to import nested markdown content'
        );
      }

      // After successful batch action, get all nested UIDs under the parent
      const createdUids = await this.fetchNestedStructure(targetParentUid);

      return {
        success: true,
        page_uid: targetPageUid,
        parent_uid: targetParentUid,
        created_uids: createdUids
      };
    } else {
      // Create a simple block for non-nested content using batchActions
      const actions = [{
        action: 'create-block',
        location: {
          "parent-uid": targetParentUid,
          "order": order
        },
        block: { string: content }
      }];

      try {
        await batchActions(this.graph, {
          action: 'batch-actions',
          actions
        });
      } catch (error) {
        throw new McpError(
          ErrorCode.InternalError,
          `Failed to create content block: ${error instanceof Error ? error.message : String(error)}`
        );
      }

      // For single-line content, we still need to fetch the UID and construct a NestedBlock
      const createdUids: NestedBlock[] = [];
      try {
        const foundUid = await this.findBlockWithRetry(targetParentUid, content);
        if (foundUid) {
          createdUids.push({
            uid: foundUid,
            text: content,
            level: 0,
            order: 0,
            children: []
          });
        }
      } catch (error: any) {
        // Log warning but don't re-throw, as the block might be created, just not immediately verifiable
        // console.warn(`Could not verify single block creation for "${content}": ${error.message}`);
      }

      return {
        success: true,
        page_uid: targetPageUid,
        parent_uid: targetParentUid,
        created_uids: createdUids
      };
    }
  }
}

```
Page 1/2FirstPrevNextLast