建立筆記 MCP 伺服器
本指南會教你由零開始建立一個完整的 MCP(Model Context Protocol)伺服器。這個伺服器會管理一組 markdown 筆記,並提供以下功能:
- 列出及讀取筆記:讓客戶端瀏覽及查看儲存在伺服器上的 markdown 檔案
- 寫入筆記:提供建立或更新筆記的 Tool
- 提供智能提示詞:產生符合情境的提示詞,例如建立每日筆記範本或總結現有內容
前置條件前置條件 的直接連結
- 已安裝 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.ts檔案: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:包含智能提示詞的邏輯
完成後的目錄結構應如下:
<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: {},})在
src/mastra/index.ts的 Mastra 實例中註冊這個 MCP 伺服器。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}`}},})在
src/mastra/mcp/server.ts中註冊這個 Tool: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,},})提示詞處理器為客戶端提供可立即使用的提示詞。你將加入以下三個:
- 每日筆記
- 總結筆記
- 腦力激盪構思
這項功能需要使用數個 markdown 解析程式庫,請先安裝它們:
npm install unified remark-parse gray-matter @types/unist在
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,}在
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 伺服器。
畫面現在會顯示如何將 MCP 伺服器加入 IDE 的說明。你可以在任何 MCP Client 中使用這個 MCP 伺服器,亦可在右側的 Available Tools 下選擇 write Tool。
在 write Tool 中,以 test 作為名稱、this is a test 作為 markdown 內容來試用。選擇 Submit 後,便會有一個新的 test.md 檔案存放在 notes 內。