import { Injectable } from '@nestjs/common';
import { Tool, type Context } from '@rekog/mcp-nest';
import { z } from 'zod';
import { LogseqClient } from '../logseq/logseq.client';
/**
* Search Tool - 검색 기능
*
* User Flow: Knowledge Search
* - 전문 검색
* - Page/Block 탐색
*/
@Injectable()
export class SearchTool {
constructor(private readonly logseq: LogseqClient) {}
@Tool({
name: 'search-query',
description:
'Graph 내에서 키워드로 검색합니다. 특정 주제에 대한 기록을 찾을 때 사용하세요.',
parameters: z.object({
query: z.string().describe('검색 키워드'),
}),
annotations: {
title: '전문 검색',
readOnlyHint: true,
idempotentHint: true,
},
})
async search({ query }: { query: string }, context: Context) {
await context.reportProgress({ progress: 20, total: 100 });
const result = await this.logseq.search(query);
await context.reportProgress({ progress: 100, total: 100 });
// null 안전성 보장
const blocks = Array.isArray(result?.blocks) ? result.blocks : [];
const pages = Array.isArray(result?.pages) ? result.pages : [];
const hasBlocks = blocks.length > 0;
const hasPages = pages.length > 0;
if (!hasBlocks && !hasPages) {
return {
content: [
{
type: 'text',
text: `🔍 "${query}" 검색 결과\n\n결과가 없습니다.`,
},
],
};
}
let text = `🔍 "${query}" 검색 결과\n\n`;
if (hasPages) {
text += `📄 **Pages** (${pages.length})\n`;
text += pages
.slice(0, 10)
.map((p) => `• ${p}`)
.join('\n');
if (pages.length > 10) {
text += `\n... 외 ${pages.length - 10}개`;
}
text += '\n\n';
}
if (hasBlocks) {
text += `📝 **Blocks** (${blocks.length})\n`;
text += blocks
.slice(0, 10)
.map((b) => {
const content =
b.content && b.content.length > 80
? b.content.substring(0, 80) + '...'
: b.content || '';
// page는 EntityID(숫자)이므로 그대로 표시하거나 생략
const pageInfo = b.page ? `[#${b.page}]` : '';
return `• ${pageInfo} ${content}`;
})
.join('\n');
if (blocks.length > 10) {
text += `\n... 외 ${blocks.length - 10}개`;
}
}
return {
content: [{ type: 'text', text }],
};
}
@Tool({
name: 'graph-info',
description:
'현재 열린 Graph의 정보를 조회합니다. 어떤 Graph가 활성화되어 있는지 확인할 때 사용하세요.',
parameters: z.object({}),
annotations: {
title: 'Graph 정보 조회',
readOnlyHint: true,
idempotentHint: true,
},
})
async getGraphInfo(_args: object, context: Context) {
await context.reportProgress({ progress: 30, total: 100 });
const graph = await this.logseq.getCurrentGraph();
await context.reportProgress({ progress: 60, total: 100 });
if (!graph) {
return {
content: [
{
type: 'text',
text: '❌ 현재 열린 Graph가 없습니다. Logseq에서 Graph를 열어주세요.',
},
],
};
}
// 추가 정보 수집
const allPages = await this.logseq.getAllPages();
const pages = allPages.filter((p) => !p['journal?']);
const journals = allPages.filter((p) => p['journal?']);
await context.reportProgress({ progress: 100, total: 100 });
return {
content: [
{
type: 'text',
text: `📊 Graph 정보\n\n이름: ${graph.name}\n경로: ${graph.path}\n\n📄 Pages: ${pages.length}개\n📅 Journals: ${journals.length}개`,
},
],
};
}
}