roam-research-mcp
Version:
MCP server and CLI for Roam Research
151 lines (150 loc) • 6.81 kB
JavaScript
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';
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 { GuidelinesOperations, DEFAULT_GUIDELINES_PAGE } from './operations/guidelines.js';
import { TableOperations } from './operations/table.js';
import { DatomicSearchHandlerImpl } from './operations/search/handlers.js';
import { FullPageViewOperations } from './operations/full-page-view.js';
export class ToolHandlers {
constructor(graph, memoriesTag = 'Memories', guidelinesPage = DEFAULT_GUIDELINES_PAGE) {
this.graph = graph;
this.cachedCheatsheet = null;
this.pageOps = new PageOperations(graph);
this.blockOps = new BlockOperations(graph);
this.blockRetrievalOps = new BlockRetrievalOperations(graph);
this.searchOps = new SearchOperations(graph);
this.memoryOps = new MemoryOperations(graph, memoriesTag);
this.todoOps = new TodoOperations(graph);
this.outlineOps = new OutlineOperations(graph);
this.batchOps = new BatchOperations(graph);
this.tableOps = new TableOperations(graph);
this.fullPageViewOps = new FullPageViewOperations(graph, this.pageOps);
this.guidelinesOps = new GuidelinesOperations(graph, guidelinesPage);
}
// Per-graph agent conventions, read from a page in the graph.
async getGuidelines() {
return this.guidelinesOps.getGuidelines();
}
// Page Operations
async findPagesModifiedToday(limit = 50, offset = 0, sort_order = 'desc') {
return this.pageOps.findPagesModifiedToday(limit, offset, sort_order);
}
async createPage(title, content) {
return this.pageOps.createPage(title, content);
}
async fetchPageByTitle(title, format) {
// The MCP read tool's markdown may be handed straight back to
// roam_update_page_markdown, so it must be round-trippable.
return this.pageOps.fetchPageByTitle(title, format, { escapeNewlines: true });
}
async fetchPageFullView(title, children_depth, max_references) {
return this.fullPageViewOps.fetchPageFullView(title, children_depth, max_references);
}
async getSubPages(prefix, filter_tag, include_content) {
return this.fullPageViewOps.fetchSubPages(prefix, filter_tag, include_content);
}
// Block Operations
async fetchBlock(block_uid, depth, include_ancestors) {
return this.blockRetrievalOps.fetchBlock(block_uid, depth, include_ancestors);
}
async moveBlock(block_uid, parent_uid, order = 'last') {
return this.blockOps.moveBlock(block_uid, parent_uid, order);
}
// Search Operations
async searchByStatus(status, page_title_uid, include, exclude) {
return this.searchOps.searchByStatus(status, page_title_uid, include, exclude);
}
async searchForTag(primary_tag, page_title_uid, near_tag) {
return this.searchOps.searchForTag(primary_tag, page_title_uid, near_tag);
}
async searchBlockRefs(params) {
return this.searchOps.searchBlockRefs(params);
}
async searchHierarchy(params) {
return this.searchOps.searchHierarchy(params);
}
async searchByText(params) {
return this.searchOps.searchByText(params);
}
async searchByDate(params) {
return this.searchOps.searchByDate(params);
}
// Datomic query
async executeDatomicQuery(params) {
const handler = new DatomicSearchHandlerImpl(this.graph, params);
return handler.execute();
}
// Memory Operations
async remember(memory, categories, heading, parent_uid, include_memories_tag) {
return this.memoryOps.remember(memory, categories, heading, parent_uid, include_memories_tag);
}
async recall(sort_by = 'newest', filter_tag) {
return this.memoryOps.recall(sort_by, filter_tag);
}
// Todo Operations
async addTodos(todos) {
return this.todoOps.addTodos(todos);
}
// Outline Operations
async createOutline(outline, page_title_uid, block_text_uid, order) {
return this.outlineOps.createOutline(outline, page_title_uid, block_text_uid, order);
}
async importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order = 'first') {
return this.outlineOps.importMarkdown(content, page_uid, page_title, parent_uid, parent_string, order);
}
// Batch Operations
async processBatch(actions) {
return this.batchOps.processBatch(actions);
}
// Table Operations
async createTable(input) {
return this.tableOps.createTable(input);
}
// Page Update with Diff
async updatePageMarkdown(title, markdown, dryRun = false) {
return this.pageOps.updatePageMarkdown(title, markdown, dryRun);
}
// Page Rename
async renamePage(params) {
return this.pageOps.renamePage(params);
}
async getRoamMarkdownCheatsheet() {
if (this.cachedCheatsheet) {
return this.cachedCheatsheet;
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const cheatsheetPath = path.join(__dirname, '../Roam_Markdown_Cheatsheet.md');
try {
let cheatsheetContent = await fs.promises.readFile(cheatsheetPath, 'utf-8');
const customInstructionsPath = process.env.CUSTOM_INSTRUCTIONS_PATH;
if (customInstructionsPath) {
try {
// Check if file exists asynchronously
await fs.promises.access(customInstructionsPath);
const customInstructionsContent = await fs.promises.readFile(customInstructionsPath, 'utf-8');
cheatsheetContent += `\n\n${customInstructionsContent}`;
}
catch (error) {
// File doesn't exist or is not readable, ignore custom instructions
if (error.code !== 'ENOENT') {
console.warn(`Could not read custom instructions file at ${customInstructionsPath}: ${error}`);
}
}
}
this.cachedCheatsheet = cheatsheetContent;
return cheatsheetContent;
}
catch (error) {
throw new Error(`Failed to read cheatsheet: ${error}`);
}
}
}