メインコンテンツへ移動

ノート管理 MCP サーバーの構築

このガイドでは、完全な MCP(Model Context Protocol)サーバーを一から構築する方法を説明します。このサーバーは Markdown ノートのコレクションを管理し、次の機能を提供します。

  1. ノートの一覧表示と読み取り:サーバーに保存された Markdown ファイルをクライアントが閲覧できるようにする
  2. ノートの書き込み:ノートを作成または更新する Tool を提供する
  3. スマートな Prompt の提供:日次ノートのテンプレート作成や既存コンテンツの要約など、コンテキストに応じた Prompt を生成する

前提条件
前提条件への直接リンク

  • Node.js v22.13.0 以降がインストールされていること
  • サポートされている Model Provider の API キー
  • 既存の Mastra プロジェクト(新しいプロジェクトをセットアップするには、インストールガイドに従ってください)

必要な依存関係とファイルの追加
必要な依存関係とファイルの追加への直接リンク

MCP サーバーを作成する前に、追加の依存関係をインストールし、基本的なフォルダー構成をセットアップします。

  1. @mastra/mcp をプロジェクトに追加します。

    npm install @mastra/mcp@latest
  2. 標準のインストールガイドに従うと、このガイドでは使用しないファイルもプロジェクトに含まれます。これらは削除して問題ありません。

    rm -rf src/mastra/agents src/mastra/workflows src/mastra/tools/weather-tool.ts

    src/mastra/index.ts ファイルも次のように変更します。

    src/mastra/index.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 ディレクトリを作成します。

    mkdir notes src/mastra/mcp

    次のファイルを作成します。

    touch src/mastra/mcp/{server,resources,prompts}.ts
    • server.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 サーバーを追加します。

  1. src/mastra/mcp/server.ts で MCP サーバーインスタンスを定義します。

    src/mastra/mcp/server.ts
    import { 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.ts
    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. Resource ハンドラーを使うと、サーバーが管理するコンテンツをクライアントが検出して読み取れるようになります。notes ディレクトリの Markdown ファイルを扱うハンドラーを実装します。src/mastra/mcp/resources.ts ファイルに次を追加します。

    src/mastra/mcp/resources.ts
    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<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 にこれらの Resource ハンドラーを登録します。

    src/mastra/mcp/server.ts
    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 を定義します。

    src/mastra/tools/write-note.ts
    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}`
    }
    },
    })

    src/mastra/mcp/server.ts にこの Tool を登録します。

    src/mastra/mcp/server.ts
    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 を提供します。次の 3 つを追加します。

    • 日次ノート
    • ノートの要約
    • アイデアのブレインストーミング

    これには Markdown の解析用ライブラリがいくつか必要です。次のコマンドでインストールします。

    npm install unified remark-parse gray-matter @types/unist

    src/mastra/mcp/prompts.ts に Prompt を実装します。

    src/mastra/mcp/prompts.ts
    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<string, number> = {}
    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,
    }

    src/mastra/mcp/server.ts にこれらの Prompt ハンドラーを登録します。

    src/mastra/mcp/server.ts
    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 を開いて試してみましょう。

npm run dev

ブラウザで http://localhost:4111 を開きます。左のサイドバーで MCP Servers を選択し、notes MCP サーバーを選択します。

IDE に MCP サーバーを追加する方法が表示されます。この MCP サーバーは、任意の MCP Client で使用できます。右側の Available Tools では、write Tool も選択できます。

write Tool 内で、名前に test、Markdown の内容に this is a test を指定して試します。Submit を選択すると、新しい test.md ファイルが notes 内に作成されます。