roam-research-mcp
Version:
MCP server and CLI for Roam Research
94 lines (93 loc) • 4.35 kB
JavaScript
import { q } from '@roam-research/roam-api-sdk';
import { BaseSearchHandler } from './types.js';
import { SearchUtils } from './utils.js';
export class TagSearchHandler extends BaseSearchHandler {
constructor(graph, params) {
super(graph);
this.params = params;
}
async execute() {
const { primary_tag, page_title_uid, near_tag, exclude_tag, case_sensitive = false, limit = -1, offset = 0 } = this.params;
let nearTagUid;
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;
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;
if (page_title_uid) {
targetPageUid = await SearchUtils.findPageByTitleOrUid(this.graph, page_title_uid);
}
const tagMatchClause = SearchUtils.buildTagMatchClause(primary_tag, '?ref-page', case_sensitive);
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 = `
${tagMatchClause}
[?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]
[(get-else $ ?b :create/time 0) ?block-create-time]
[(get-else $ ?b :edit/time 0) ?block-edit-time]`; // Fetch page edit time for sorting, block timestamps for sort/group
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 ?block-create-time ?block-edit-time
${inClause} ${queryLimit} ${queryOffset} ${queryOrder}
:where
${queryWhereClauses}]`;
const queryArgs = [];
if (targetPageUid) {
queryArgs.push(targetPageUid);
}
const rawResults = await q(this.graph, queryStr, queryArgs);
// 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);
const totalCount = totalCountResults[0] ? totalCountResults[0][0] : 0;
// Resolve block references in content
const resolvedResults = await this.resolveBlockRefs(rawResults);
const searchDescription = `referencing "${primary_tag}"`;
const formattedResults = SearchUtils.formatSearchResults(resolvedResults, searchDescription, !targetPageUid);
formattedResults.total_count = totalCount;
return formattedResults;
}
}