> Discover all available pages from the documentation index: https://mastra.zisheng.pro/ko/llms.txt # Notes MCP 서버 구축 이 가이드에서는 처음부터 완전한 MCP(Model 컨텍스트 프로토콜) 서버를 구축하는 방법을 배웁니다. 이 서버는 마크다운 메모 모음을 관리하며 다음과 같은 기능을 갖습니다. 1. **메모 나열 및 읽기**: 클라이언트가 서버에 저장된 마크다운 파일을 찾아보고 볼 수 있도록 허용합니다. 2. **메모 작성**: 노트 생성 또는 업데이트를 위한 Tool 제공 3. **스마트 Prompt 제공**: 일일 메모 템플릿 생성, 기존 콘텐츠 요약 등 상황에 맞는 Prompt를 생성합니다. ## 전제조건 - Node.js `v22.13.0` 이상 설치 - 지원되는 [Model Provider](https://mastra.zisheng.pro/ko/models)의 API 키 - 기존 Mastra 프로젝트(새 프로젝트를 설정하려면 [설치 가이드](https://mastra.zisheng.pro/ko/guides/getting-started/quickstart)를 따르세요) ## 필요한 종속성 및 파일 추가 MCP 서버를 생성하기 전에 먼저 추가 종속성을 설치하고 상용구 폴더 구조를 설정해야 합니다. 1. 프로젝트에 `@mastra/mcp`를 추가하세요. **npm**: ```bash npm install @mastra/mcp@latest ``` **pnpm**: ```bash pnpm add @mastra/mcp@latest ``` **Yarn**: ```bash yarn add @mastra/mcp@latest ``` **Bun**: ```bash bun add @mastra/mcp@latest ``` 2. 기본 [설치 가이드](https://mastra.zisheng.pro/ko/guides/getting-started/quickstart)를 따른 후에는 이 가이드와 관련 없는 파일이 프로젝트에 포함됩니다. 이러한 파일은 안전하게 삭제할 수 있습니다. ```bash rm -rf src/mastra/agents src/mastra/workflows src/mastra/tools/weather-tool.ts ``` 또한`src/mastra/index.ts` file like so: ```ts import { Mastra } from '@mastra/core' import { PinoLogger } from '@mastra/loggers' import { LibSQLStore } from '@mastra/libsql' export const mastra = new Mastra({ storage: new LibSQLStore({ id: 'mastra-storage', // stores telemetry, evals, ... into memory storage, if it needs to persist, change to file:../mastra.db url: ':memory:', }), logger: new PinoLogger({ name: 'Mastra', level: 'info', }), }) ``` 3. MCP 서버 로직을 위한 전용 디렉터리와 메모를 저장할 `notes` 디렉터리를 만드세요. ```bash mkdir notes src/mastra/mcp ``` 다음 파일을 생성합니다: ```bash touch src/mastra/mcp/{server,resources,prompts}.ts ``` - `server.ts`: 기본 MCP 서버 구성이 포함됩니다. - `resources.ts`: 메모 파일 나열 및 읽기를 처리합니다. - `prompts.ts`: 스마트 Prompt에 대한 논리가 포함됩니다. 결과 디렉터리 구조는 다음과 같아야 합니다. ```text / ├── notes/ └── src/ └── mastra/ ├── index.ts ├── mcp/ │ ├── server.ts │ ├── resources.ts │ └── prompts.ts └── tools/ ``` ## MCP 서버 생성 MCP 서버를 추가해 봅시다! 1. `src/mastra/mcp/server.ts`에서 MCP 서버 인스턴스를 정의하세요. ```typescript import { MCPServer } from '@mastra/mcp' export const notes = new MCPServer({ id: 'notes', name: 'Notes Server', version: '0.1.0', tools: {}, }) ``` 이 MCP 서버를 `src/mastra/index.ts`의 Mastra 인스턴스에 등록하세요. `notes` 키는 MCP 서버의 공개 식별자입니다. ```typescript import { Mastra } from '@mastra/core' import { PinoLogger } from '@mastra/loggers' import { LibSQLStore } from '@mastra/libsql' import { notes } from './mcp/server' export const mastra = new Mastra({ storage: new LibSQLStore({ id: 'mastra-storage', // stores telemetry, evals, ... into memory storage, if it needs to persist, change to file:../mastra.db url: ':memory:', }), logger: new PinoLogger({ name: 'Mastra', level: 'info', }), mcpServers: { notes, }, }) ``` 2. 리소스 핸들러를 사용하면 클라이언트가 서버에서 관리하는 콘텐츠를 검색하고 읽을 수 있습니다. `notes` 디렉터리의 Markdown 파일을 처리하는 핸들러를 구현하세요. `src/mastra/mcp/resources.ts` 파일에 추가하세요. ```typescript import fs from 'fs/promises' import path from 'path' import { fileURLToPath } from 'url' import type { MCPServerResources, Resource } from '@mastra/mcp' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const NOTES_DIR = path.resolve(__dirname, '../../notes') // relative to the default output directory const listNoteFiles = async (): Promise => { try { await fs.mkdir(NOTES_DIR, { recursive: true }) const files = await fs.readdir(NOTES_DIR) return files .filter(file => file.endsWith('.md')) .map(file => { const title = file.replace('.md', '') return { uri: `notes://${title}`, name: title, description: `A note about ${title}`, mime_type: 'text/markdown', } }) } catch (error) { console.error('Error listing note resources:', error) return [] } } const readNoteFile = async (uri: string): Promise => { const title = uri.replace('notes://', '') const notePath = path.join(NOTES_DIR, `${title}.md`) try { return await fs.readFile(notePath, 'utf-8') } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { console.error(`Error reading resource ${uri}:`, error) } return null } } export const resourceHandlers: MCPServerResources = { listResources: listNoteFiles, getResourceContent: async ({ uri }: { uri: string }) => { const content = await readNoteFile(uri) if (content === null) return { text: '' } return { text: content } }, } ``` 다음 리소스 핸들러를 등록하세요.`src/mastra/mcp/server.ts`: ```typescript import { MCPServer } from '@mastra/mcp' import { resourceHandlers } from './resources' export const notes = new MCPServer({ id: 'notes', name: 'Notes Server', version: '0.1.0', tools: {}, resources: resourceHandlers, }) ``` 3. Tool은 서버가 수행할 수 있는 작업입니다. `write` Tool을 만들어 보겠습니다. 먼저 `src/mastra/tools/write-note.ts`에 Tool을 정의하세요. ```typescript import { createTool } from '@mastra/core/tools' import { z } from 'zod' import { fileURLToPath } from 'url' import path from 'node:path' import fs from 'fs/promises' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const NOTES_DIR = path.resolve(__dirname, '../../../notes') export const writeNoteTool = createTool({ id: 'write', description: 'Write a new note or overwrite an existing one.', inputSchema: z.object({ title: z.string().nonempty().describe('The title of the note. This will be the filename.'), content: z.string().nonempty().describe('The markdown content of the note.'), }), outputSchema: z.string().nonempty(), execute: async inputData => { try { const { title, content } = inputData const filePath = path.join(NOTES_DIR, `${title}.md`) await fs.mkdir(NOTES_DIR, { recursive: true }) await fs.writeFile(filePath, content, 'utf-8') return `Successfully wrote to note \"${title}\".` } catch (error: any) { return `Error writing note: ${error.message}` } }, }) ``` 이 Tool을 다음에 등록하세요.`src/mastra/mcp/server.ts`: ```typescript import { MCPServer } from '@mastra/mcp' import { resourceHandlers } from './resources' import { writeNoteTool } from '../tools/write-note' export const notes = new MCPServer({ id: 'notes', name: 'Notes Server', version: '0.1.0', resources: resourceHandlers, tools: { write: writeNoteTool, }, }) ``` 4. Prompt 처리기는 클라이언트에 즉시 사용할 수 있는 Prompt를 제공합니다. 다음 세 가지를 추가합니다. - 일일 메모 - 메모 요약 - 아이디어 브레인스토밍 이를 위해서는 설치해야 하는 몇 가지 마크다운 구문 분석 라이브러리가 필요합니다. ```bash npm install unified remark-parse gray-matter @types/unist ``` Prompt를 구현하세요.`src/mastra/mcp/prompts.ts`: ```typescript import type { MCPServerPrompts } from '@mastra/mcp' import { unified } from 'unified' import remarkParse from 'remark-parse' import matter from 'gray-matter' import type { Node } from 'unist' const prompts = [ { name: 'new_daily_note', description: 'Create a new daily note.', version: '1.0.0', }, { name: 'summarize_note', description: 'Give me a TL;DR of the note.', version: '1.0.0', }, { name: 'brainstorm_ideas', description: 'Brainstorm new ideas based on a note.', version: '1.0.0', }, ] function stringifyNode(node: Node): string { if ('value' in node && typeof node.value === 'string') return node.value if ('children' in node && Array.isArray(node.children)) return node.children.map(stringifyNode).join('') return '' } export async function analyzeMarkdown(md: string) { const { content } = matter(md) const tree = unified().use(remarkParse).parse(content) const headings: string[] = [] const wordCounts: Record = {} let currentHeading = 'untitled' wordCounts[currentHeading] = 0 tree.children.forEach(node => { if (node.type === 'heading' && node.depth === 2) { currentHeading = stringifyNode(node) headings.push(currentHeading) wordCounts[currentHeading] = 0 } else { const textContent = stringifyNode(node) if (textContent.trim()) { wordCounts[currentHeading] = (wordCounts[currentHeading] || 0) + textContent.split(/\\s+/).length } } }) return { headings, wordCounts } } const getPromptMessages: MCPServerPrompts['getPromptMessages'] = async ({ name, args }) => { switch (name) { case 'new_daily_note': const today = new Date().toISOString().split('T')[0] return [ { role: 'user', content: { type: 'text', text: `Create a new note titled \"${today}\" with sections: \"## Tasks\", \"## Meetings\", \"## Notes\".`, }, }, ] case 'summarize_note': if (!args?.noteContent) throw new Error('No content provided') const metaSum = await analyzeMarkdown(args.noteContent as string) return [ { role: 'user', content: { type: 'text', text: `Summarize each section in ≤ 3 bullets.\\n\\n### Outline\\n${metaSum.headings.map(h => `- ${h} (${metaSum.wordCounts[h] || 0} words)`).join('\\n')}`.trim(), }, }, ] case 'brainstorm_ideas': if (!args?.noteContent) throw new Error('No content provided') const metaBrain = await analyzeMarkdown(args.noteContent as string) return [ { role: 'user', content: { type: 'text', text: `Brainstorm 3 ideas for underdeveloped sections below ${args?.topic ? `on ${args.topic}` : '.'}\\n\\nUnderdeveloped sections:\\n${metaBrain.headings.length ? metaBrain.headings.map(h => `- ${h}`).join('\\n') : '- (none, pick any)'}`, }, }, ] default: throw new Error(`Prompt \"${name}\" not found`) } } export const promptHandlers: MCPServerPrompts = { listPrompts: async () => prompts, getPromptMessages, } ``` 다음 Prompt 핸들러를 등록하세요.`src/mastra/mcp/server.ts`: ```typescript import { MCPServer } from '@mastra/mcp' import { resourceHandlers } from './resources' import { writeNoteTool } from '../tools/write-note' import { promptHandlers } from './prompts' export const notes = new MCPServer({ id: 'notes', name: 'Notes Server', version: '0.1.0', resources: resourceHandlers, prompts: promptHandlers, tools: { write: writeNoteTool, }, }) ``` ## 서버 실행 좋습니다. 첫 번째 MCP 서버를 작성했습니다! 이제 Mastra 개발 서버를 시작하고 열어서 시험해 볼 수 있습니다.[Studio](https://mastra.zisheng.pro/ko/docs/studio/overview): ```bash npm run dev ``` 브라우저에서 [`http://localhost:4111`](http://localhost:4111)을 여세요. 왼쪽 사이드바에서 **MCP Servers**를 선택한 다음 **notes** MCP 서버를 선택하세요. 이제 IDE에 MCP 서버를 추가하는 방법이 표시됩니다. 이 MCP 서버는 모든 MCP 클라이언트에서 사용할 수 있습니다. 오른쪽 아래의 **Available Tools**에서 **write** Tool을 선택할 수도 있습니다. **write** Tool 안에서 이름으로 `test`, Markdown 콘텐츠로 `this is a test`를 입력하여 사용해 보세요. **Submit**을 선택하면 `notes` 안에 새 `test.md` 파일이 생성됩니다.