import { Injectable, Scope, Inject } from '@nestjs/common';
import { Resource, ResourceTemplate } from '@rekog/mcp-nest';
import type { ConfigType } from '@nestjs/config';
import * as fs from 'fs';
import * as path from 'path';
import { logseqConfig } from '../config/logseq.config';
/**
* Concept Resource - 프로젝트 개념 설계 문서 리소스
*
* 프로젝트의 개념 설계 문서들을 MCP Resource로 노출하여
* AI Agent가 프로젝트 맥락을 이해하고 개발에 활용할 수 있게 함
*
* 환경변수 DOCS_PATH로 문서 경로 설정 가능
*/
@Injectable({ scope: Scope.REQUEST })
export class ConceptResource {
private readonly docsBasePath: string;
private readonly projectName: string;
constructor(
@Inject(logseqConfig.KEY)
private readonly config: ConfigType<typeof logseqConfig>,
) {
// 환경변수로 설정 가능한 문서 경로
this.docsBasePath = path.resolve(process.cwd(), config.docsPath);
this.projectName = config.projectName;
}
// ==================== Static Resources ====================
@Resource({
name: 'logseq-mcp-concept-design',
description:
'Logseq MCP 통합의 핵심 개념 설계 문서. Graph, Page, Block, Journal, Reference, Agent Command, MCP Tool 등의 개념 정의',
mimeType: 'text/markdown',
uri: 'mcp://concepts/logseq-mcp-concept-design',
})
getConceptDesign({ uri }: { uri: string }) {
const filePath = path.join(
this.docsBasePath,
'integration',
'logseq-mcp-concept-design.md',
);
const content = this.readFileContent(filePath);
return {
contents: [
{
uri,
mimeType: 'text/markdown',
text: content,
},
],
};
}
@Resource({
name: 'logseq-mcp-user-flows',
description:
'Logseq MCP 통합의 사용자 플로우 문서. Daily Recording, Knowledge Search, Task Management 등의 플로우 정의',
mimeType: 'text/markdown',
uri: 'mcp://concepts/logseq-mcp-user-flows',
})
getUserFlows({ uri }: { uri: string }) {
const filePath = path.join(
this.docsBasePath,
'integration',
'logseq-mcp-user-flows.md',
);
const content = this.readFileContent(filePath);
return {
contents: [
{
uri,
mimeType: 'text/markdown',
text: content,
},
],
};
}
@Resource({
name: 'project-architecture',
description:
'프로젝트 아키텍처 개요. 디렉토리 구조, 모듈 관계, 의존성 흐름 등',
mimeType: 'application/json',
uri: 'mcp://project/architecture',
})
getProjectArchitecture({ uri }: { uri: string }) {
const architecture = {
name: 'logseq-mcp',
version: '0.1.0',
framework: 'NestJS',
protocol: 'MCP (Model Context Protocol)',
structure: {
'src/': {
'app.module.ts': '루트 모듈 - 모든 모듈 통합',
'main.ts': '애플리케이션 진입점',
'config/': {
'logseq.config.ts': 'Logseq API 설정',
},
'logseq/': {
'logseq.client.ts': 'Logseq HTTP API 클라이언트',
'logseq.module.ts': 'Logseq 모듈',
'logseq.types.ts': 'Logseq 타입 정의',
},
'tools/': {
'journal.tool.ts': 'Journal MCP 도구',
'page.tool.ts': 'Page MCP 도구',
'block.tool.ts': 'Block MCP 도구',
'search.tool.ts': 'Search MCP 도구',
'dev.tool.ts': '개발 기록 MCP 도구',
'tools.module.ts': 'Tools 모듈',
},
'resources/': {
'concept.resource.ts': '개념 설계 문서 리소스',
'resources.module.ts': 'Resources 모듈',
},
'prompts/': {
'dev.prompt.ts': '개발 관련 프롬프트',
'prompts.module.ts': 'Prompts 모듈',
},
},
},
principles: ['SOLID', 'YAGNI', 'OCP (데코레이터 기반 확장)'],
dependencies: {
core: ['@nestjs/common', '@nestjs/core', '@nestjs/config'],
mcp: ['@rekog/mcp-nest'],
validation: ['zod'],
http: ['axios'],
},
};
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(architecture, null, 2),
},
],
};
}
@Resource({
name: 'mcp-tools-reference',
description: '현재 구현된 모든 MCP 도구들의 참조 문서',
mimeType: 'application/json',
uri: 'mcp://project/tools-reference',
})
getToolsReference({ uri }: { uri: string }) {
const toolsRef = {
journal: [
{
name: 'journal-add-entry',
description: 'Journal에 새로운 내용 기록',
parameters: ['content: string', 'asTask?: boolean'],
},
{
name: 'journal-get-today',
description: '오늘의 Journal 조회',
parameters: [],
},
{
name: 'journal-get-by-date',
description: '특정 날짜의 Journal 조회',
parameters: ['date: string (YYYY-MM-DD)'],
},
],
page: [
{
name: 'page-get',
description: 'Page 조회',
parameters: ['name: string'],
},
{
name: 'page-create',
description: 'Page 생성',
parameters: ['name: string', 'content?: string'],
},
{
name: 'page-get-blocks',
description: 'Page의 Block 목록 조회',
parameters: ['name: string'],
},
{
name: 'page-get-references',
description: 'Page의 역참조 조회',
parameters: ['name: string'],
},
],
block: [
{
name: 'block-get',
description: 'Block 조회',
parameters: ['uuid: string'],
},
{
name: 'block-create',
description: 'Block 생성',
parameters: ['pageOrBlockUuid: string', 'content: string'],
},
{
name: 'block-update',
description: 'Block 수정',
parameters: ['uuid: string', 'content: string'],
},
{
name: 'block-delete',
description: 'Block 삭제',
parameters: ['uuid: string'],
},
{
name: 'block-get-references',
description: 'Block의 역참조 조회',
parameters: ['uuid: string'],
},
],
search: [
{
name: 'search-query',
description: '전문 검색',
parameters: ['query: string', 'limit?: number'],
},
{
name: 'search-get-graph-info',
description: 'Graph 정보 조회',
parameters: [],
},
{
name: 'search-get-all-pages',
description: '모든 Page 목록 조회',
parameters: [],
},
],
dev: [
{
name: 'dev-log-progress',
description: '개발 진행 상황 기록',
parameters: [
'title: string',
'description: string',
'category: feature|refactor|fix|docs|test|chore',
'files?: string[]',
],
},
{
name: 'dev-decision',
description: '기술적 결정 사항(ADR) 기록',
parameters: [
'title: string',
'context: string',
'decision: string',
'consequences: string',
'alternatives?: string[]',
],
},
{
name: 'dev-todo',
description: '개발 TODO 추가',
parameters: [
'content: string',
'priority?: high|medium|low',
'tags?: string[]',
],
},
{
name: 'dev-idea',
description: '개발 아이디어 기록',
parameters: [
'idea: string',
'category?: feature|improvement|experiment|question',
],
},
{
name: 'dev-get-project-logs',
description: '프로젝트 개발 로그 조회',
parameters: ['limit?: number'],
},
],
};
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(toolsRef, null, 2),
},
],
};
}
// ==================== Dynamic Resource Templates ====================
@ResourceTemplate({
name: 'concept-document',
description: '특정 개념 설계 문서 조회. 카테고리와 문서명으로 접근',
mimeType: 'text/markdown',
uriTemplate: 'mcp://concepts/{category}/{document}',
})
getConceptDocument({
uri,
category,
document,
}: {
uri: string;
category: string;
document: string;
}) {
// 보안: 경로 순회 방지
if (category.includes('..') || document.includes('..')) {
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: 'Error: Invalid path',
},
],
};
}
const filePath = path.join(this.docsBasePath, category, `${document}.md`);
const content = this.readFileContent(filePath);
return {
contents: [
{
uri,
mimeType: 'text/markdown',
text: content,
},
],
};
}
// ==================== Helper Methods ====================
private readFileContent(filePath: string): string {
try {
if (!fs.existsSync(filePath)) {
return `# 문서를 찾을 수 없음\n\n경로: ${filePath}\n\n이 문서가 아직 생성되지 않았거나 경로가 올바르지 않습니다.`;
}
return fs.readFileSync(filePath, 'utf-8');
} catch (error) {
return `# 문서 읽기 오류\n\n${error instanceof Error ? error.message : '알 수 없는 오류'}`;
}
}
}