import { Injectable } from '@nestjs/common';
import { Tool, type Context } from '@rekog/mcp-nest';
import { z } from 'zod';
import { LogseqClient } from '../logseq/logseq.client';
import { LogseqFormatter } from '../logseq/logseq.formatter';
/**
* Block Tool - Block 관리
*
* User Flow: Content Structuring
* - Block 조회, 생성, 수정, 삭제
* - 아이디어 연결 (Reference 추가)
*/
@Injectable()
export class BlockTool {
constructor(
private readonly logseq: LogseqClient,
private readonly formatter: LogseqFormatter,
) {}
@Tool({
name: 'block-get',
description:
'Block의 내용을 조회합니다. UUID로 특정 Block을 확인할 때 사용하세요.',
parameters: z.object({
uuid: z.string().describe('Block의 UUID'),
includeChildren: z.boolean().optional().describe('자식 Block 포함 여부'),
}),
annotations: {
title: 'Block 조회',
readOnlyHint: true,
idempotentHint: true,
},
})
async getBlock(
{ uuid, includeChildren }: { uuid: string; includeChildren?: boolean },
context: Context,
) {
await context.reportProgress({ progress: 30, total: 100 });
const block = await this.logseq.getBlock(uuid, { includeChildren });
await context.reportProgress({ progress: 100, total: 100 });
if (!block) {
return {
content: [
{
type: 'text',
text: `❌ Block을 찾을 수 없습니다: ${uuid}`,
},
],
};
}
let text = `📝 Block\n\n${block.content}`;
if (block.properties && Object.keys(block.properties).length > 0) {
const props = Object.entries(block.properties)
.map(([k, v]) => `${k}:: ${String(v)}`)
.join('\n');
text += `\n\n속성:\n${props}`;
}
if (block.children && block.children.length > 0) {
text += `\n\n자식 Block: ${block.children.length}개`;
}
return {
content: [{ type: 'text', text }],
};
}
@Tool({
name: 'block-add',
description:
'특정 Page에 새로운 Block을 추가합니다. 내용을 기록할 때 사용하세요. 내용은 자동으로 Logseq 아웃라이너 포맷으로 변환됩니다.',
parameters: z.object({
pageName: z.string().describe('Block을 추가할 Page 이름'),
content: z
.string()
.describe(
'Block 내용 (Markdown 또는 Logseq 포맷). 자동으로 Logseq 아웃라이너 포맷으로 변환됩니다.',
),
position: z
.enum(['append', 'prepend'])
.optional()
.default('append')
.describe('추가 위치 (append: 끝에, prepend: 앞에)'),
}),
annotations: {
title: 'Block 추가',
readOnlyHint: false,
idempotentHint: false,
},
})
async addBlock(
{
pageName,
content,
position,
}: { pageName: string; content: string; position?: 'append' | 'prepend' },
context: Context,
) {
await context.reportProgress({ progress: 30, total: 100 });
// 안전한 파싱 사용 - Logseq 호환성 검증 포함
// (하나의 블록에 여러 리스트/헤딩이 있으면 자동 분리)
const parsedBlocks = this.formatter.safeParseToBlocks(content);
let block;
if (position === 'prepend') {
// prepend는 첫 번째 블록만 prepend하고 나머지는 append
// 복잡한 구조는 append 사용 권장
block = await this.logseq.prependBlockInPage(
pageName,
parsedBlocks[0]?.content || content,
);
} else {
block = await this.logseq.appendBlocksInPage(pageName, parsedBlocks);
}
await context.reportProgress({ progress: 100, total: 100 });
if (!block) {
return {
content: [
{
type: 'text',
text: `❌ Block 추가 실패. Page가 존재하는지 확인해주세요: ${pageName}`,
},
],
};
}
return {
content: [
{
type: 'text',
text: `✅ Block 추가 완료\n\n페이지: ${pageName}\n내용: ${content}\nUUID: ${block.uuid}`,
},
],
};
}
@Tool({
name: 'block-insert-child',
description:
'기존 Block 아래에 자식 Block을 추가합니다. 계층적 구조를 만들 때 사용하세요.',
parameters: z.object({
parentUuid: z.string().describe('부모 Block의 UUID'),
content: z.string().describe('자식 Block 내용'),
}),
annotations: {
title: 'Block 자식 추가',
readOnlyHint: false,
idempotentHint: false,
},
})
async insertChildBlock(
{ parentUuid, content }: { parentUuid: string; content: string },
context: Context,
) {
await context.reportProgress({ progress: 30, total: 100 });
const block = await this.logseq.insertBlock(parentUuid, content, {
sibling: false,
});
await context.reportProgress({ progress: 100, total: 100 });
if (!block) {
return {
content: [
{
type: 'text',
text: `❌ Block 추가 실패. 부모 Block UUID를 확인해주세요: ${parentUuid}`,
},
],
};
}
return {
content: [
{
type: 'text',
text: `✅ 자식 Block 추가 완료\n\n내용: ${content}\nUUID: ${block.uuid}`,
},
],
};
}
@Tool({
name: 'block-update',
description: 'Block의 내용을 수정합니다.',
parameters: z.object({
uuid: z.string().describe('수정할 Block의 UUID'),
content: z.string().describe('새로운 내용'),
}),
annotations: {
title: 'Block 수정',
readOnlyHint: false,
idempotentHint: true,
},
})
async updateBlock(
{ uuid, content }: { uuid: string; content: string },
context: Context,
) {
await context.reportProgress({ progress: 30, total: 100 });
// 기존 Block 확인
const existing = await this.logseq.getBlock(uuid);
if (!existing) {
return {
content: [
{
type: 'text',
text: `❌ Block을 찾을 수 없습니다: ${uuid}`,
},
],
};
}
await context.reportProgress({ progress: 60, total: 100 });
await this.logseq.updateBlock(uuid, content);
await context.reportProgress({ progress: 100, total: 100 });
return {
content: [
{
type: 'text',
text: `✅ Block 수정 완료\n\n이전: ${existing.content}\n이후: ${content}`,
},
],
};
}
@Tool({
name: 'block-delete',
description: 'Block을 삭제합니다. ⚠️ 자식 Block도 함께 삭제됩니다.',
parameters: z.object({
uuid: z.string().describe('삭제할 Block의 UUID'),
}),
annotations: {
title: 'Block 삭제',
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
},
})
async deleteBlock({ uuid }: { uuid: string }, context: Context) {
await context.reportProgress({ progress: 30, total: 100 });
// 기존 Block 확인
const existing = await this.logseq.getBlock(uuid, {
includeChildren: true,
});
if (!existing) {
return {
content: [
{
type: 'text',
text: `❌ Block을 찾을 수 없습니다: ${uuid}`,
},
],
};
}
await context.reportProgress({ progress: 60, total: 100 });
await this.logseq.removeBlock(uuid);
await context.reportProgress({ progress: 100, total: 100 });
const childCount = existing.children?.length || 0;
return {
content: [
{
type: 'text',
text: `✅ Block 삭제 완료\n\n내용: ${existing.content}${childCount > 0 ? `\n자식 Block ${childCount}개도 삭제됨` : ''}`,
},
],
};
}
@Tool({
name: 'block-add-reference',
description:
'기존 Block에 다른 Page나 Block에 대한 참조를 추가합니다. 아이디어를 연결할 때 사용하세요.',
parameters: z.object({
uuid: z.string().describe('참조를 추가할 Block의 UUID'),
referenceName: z
.string()
.describe('참조할 Page 이름 (예: [[프로젝트 알파]])'),
}),
annotations: {
title: 'Block에 참조 추가',
readOnlyHint: false,
idempotentHint: false,
},
})
async addReference(
{ uuid, referenceName }: { uuid: string; referenceName: string },
context: Context,
) {
await context.reportProgress({ progress: 30, total: 100 });
const existing = await this.logseq.getBlock(uuid);
if (!existing) {
return {
content: [
{
type: 'text',
text: `❌ Block을 찾을 수 없습니다: ${uuid}`,
},
],
};
}
await context.reportProgress({ progress: 60, total: 100 });
// [[Page Name]] 형식으로 변환
const refText = referenceName.startsWith('[[')
? referenceName
: `[[${referenceName}]]`;
const newContent = `${existing.content} ${refText}`;
await this.logseq.updateBlock(uuid, newContent);
await context.reportProgress({ progress: 100, total: 100 });
return {
content: [
{
type: 'text',
text: `✅ 참조 추가 완료\n\n${existing.content} → ${newContent}`,
},
],
};
}
}