Notes MCP 서버 구축
이 가이드에서는 처음부터 완전한 MCP(Model 컨텍스트 프로토콜) 서버를 구축하는 방법을 배웁니다. 이 서버는 마크다운 메모 모음을 관리하며 다음과 같은 기능을 갖습니다.
- 메모 나열 및 읽기: 클라이언트가 서버에 저장된 마크다운 파일을 찾아보고 볼 수 있도록 허용합니다.
- 메모 작성: 노트 생성 또는 업데이트를 위한 Tool 제공
- 스마트 Prompt 제공: 일일 메모 템플릿 생성, 기존 콘텐츠 요약 등 상황에 맞는 Prompt를 생성합니다.
전제조건전제조건에 대한 직접 링크
- Node.js
v22.13.0이상 설치 - 지원되는 Model Provider의 API 키
- 기존 Mastra 프로젝트(새 프로젝트를 설정하려면 설치 가이드를 따르세요)
필요한 종속성 및 파일 추가필요한 종속성 및 파일 추가에 대한 직접 링크
MCP 서버를 생성하기 전에 먼저 추가 종속성을 설치하고 상용구 폴더 구조를 설정해야 합니다.
프로젝트에
@mastra/mcp를 추가하세요.- npm
- pnpm
- Yarn
- Bun
npm install @mastra/mcp@latestpnpm add @mastra/mcp@latestyarn add @mastra/mcp@latestbun add @mastra/mcp@latest기본 설치 가이드를 따른 후에는 이 가이드와 관련 없는 파일이 프로젝트에 포함됩니다. 이러한 파일은 안전하게 삭제할 수 있습니다.
rm -rf src/mastra/agents src/mastra/workflows src/mastra/tools/weather-tool.ts또한
src/mastra/index.tsfile like so:src/mastra/index.tsimport { 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.dburl: ':memory:',}),logger: new PinoLogger({name: 'Mastra',level: 'info',}),})MCP 서버 로직을 위한 전용 디렉터리와 메모를 저장할
notes디렉터리를 만드세요.mkdir notes src/mastra/mcp다음 파일을 생성합니다:
touch src/mastra/mcp/{server,resources,prompts}.tsserver.ts: 기본 MCP 서버 구성이 포함됩니다.resources.ts: 메모 파일 나열 및 읽기를 처리합니다.prompts.ts: 스마트 Prompt에 대한 논리가 포함됩니다.
결과 디렉터리 구조는 다음과 같아야 합니다.
<your-project-name>/├── notes/└── src/└── mastra/├── index.ts├── mcp/│ ├── server.ts│ ├── resources.ts│ └── prompts.ts└── tools/
MCP 서버 생성MCP 서버 생성에 대한 직접 링크
MCP 서버를 추가해 봅시다!
src/mastra/mcp/server.ts에서 MCP 서버 인스턴스를 정의하세요.src/mastra/mcp/server.tsimport { 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 서버의 공개 식별자입니다.src/mastra/index.tsimport { 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.dburl: ':memory:',}),logger: new PinoLogger({name: 'Mastra',level: 'info',}),mcpServers: {notes,},})리소스 핸들러를 사용하면 클라이언트가 서버에서 관리하는 콘텐츠를 검색하고 읽을 수 있습니다.
notes디렉터리의 Markdown 파일을 처리하는 핸들러를 구현하세요.src/mastra/mcp/resources.ts파일에 추가하세요.src/mastra/mcp/resources.tsimport 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 directoryconst listNoteFiles = async (): Promise<Resource[]> => {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<string | null> => {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:src/mastra/mcp/server.tsimport { 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,})Tool은 서버가 수행할 수 있는 작업입니다.
writeTool을 만들어 보겠습니다. 먼저src/mastra/tools/write-note.ts에 Tool을 정의하세요.src/mastra/tools/write-note.tsimport { 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 } = inputDataconst 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:src/mastra/mcp/server.tsimport { 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,},})Prompt 처리기는 클라이언트에 즉시 사용할 수 있는 Prompt를 제공합니다. 다음 세 가지를 추가합니다.
- 일일 메모
- 메모 요약
- 아이디어 브레인스토밍
이를 위해서는 설치해야 하는 몇 가지 마크다운 구문 분석 라이브러리가 필요합니다.
npm install unified remark-parse gray-matter @types/unistPrompt를 구현하세요.
src/mastra/mcp/prompts.ts:src/mastra/mcp/prompts.tsimport 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.valueif ('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<string, number> = {}let currentHeading = 'untitled'wordCounts[currentHeading] = 0tree.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:src/mastra/mcp/server.tsimport { 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:
npm run dev
브라우저에서 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 파일이 생성됩니다.