Skip to Content
ガイドGuideMCP Server: Notes MCP Server

MCP Server Guide: Notes MCP Serverの構築

このガイドでは、完全なMCP(Model Context Protocol)サーバーをゼロから構築する方法を学びます。このサーバーはマークダウンノートのコレクションを管理し、ノートの作成と読み取りを行うツールを公開し、ノート作成を支援するインテリジェントなプロンプトを提供します。

前提条件

  • Node.jsがインストールされていること
  • TypeScriptの基本的な理解

計画

以下の機能を持つnotesサーバーを構築します:

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

Mastraプロジェクトの初期化

まず、create-mastra CLIを使用して新しいMastraプロジェクトを作成します。

npx create-mastra@latest

プロンプトに従ってください。完了したら、新しいプロジェクトディレクトリに移動します。

cd "<your-project-name>"

デフォルトのスキャフォールドには、エージェント、ツール、ワークフローの例が含まれています。MCPサーバーに焦点を当てているため、プロジェクトをクリーンアップしましょう。

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

また、src/mastra/index.tsのMastraインスタンスに登録されていたこれらのファイルからコンポーネントを削除しましょう。 ファイルは次のようになります:

src/mastra/index.ts
import { Mastra } from '@mastra/core/mastra'; import { PinoLogger } from '@mastra/loggers'; import { LibSQLStore } from '@mastra/libsql'; export const mastra = new Mastra({ storage: new LibSQLStore({ // 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', }), });

ディレクトリ構造の設定

MCPサーバーのロジック用の専用ディレクトリと、ノート用のnotesディレクトリを作成します:

mkdir notes src/mastra/mcp

以下のファイルを作成します:

touch src/mastra/mcp/{server,resources,prompts}.ts
  • server.ts: メインのMCPサーバー設定を含みます。
  • resources.ts: ノートファイルの一覧表示と読み取りを処理します。
  • prompts.ts: スマートプロンプトのロジックを含みます。

結果として得られるディレクトリ構造は以下のようになります:

          • index.ts
            • server.ts
            • resources.ts
            • prompts.ts

      MCP サーバーの作成と登録

      続行する前に、@mastra/mcp パッケージをインストールする必要があります。

      npm install @mastra/mcp

      次に、src/mastra/mcp/server.ts で MCP サーバーインスタンスを定義しましょう:

      src/mastra/mcp/server.ts
      import { MCPServer } from "@mastra/mcp"; export const notes = new MCPServer({ name: "notes", version: "0.1.0", // we will add more configuration here later });

      次に、この MCP サーバーを src/mastra/index.ts の Mastra インスタンスに登録します。キー 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({ // 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, }, })

      リソースハンドラーの実装と登録

      リソースハンドラーを使用すると、クライアントがサーバーが管理するコンテンツを発見して読み取ることができます。 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に登録します:

      src/mastra/mcp/server.ts
      import { MCPServer } from "@mastra/mcp"; import { resourceHandlers } from "./resources"; export const notes = new MCPServer({ name: "notes", version: "0.1.0", resources: resourceHandlers, });

      ツールの実装と登録

      ツールは、サーバーが実行できるアクションです。writeツールを作成しましょう。 まず、src/mastra/tools/write-note.tsでツールを定義します:

      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 ({ context }) => { try { const { title, content } = context; 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でこのツールを登録します:

      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({ name: "notes", version: "0.1.0", resources: resourceHandlers, tools: { write: writeNoteTool, }, });

      プロンプトの実装と登録

      プロンプトハンドラーは、クライアント向けにすぐに使えるプロンプトを提供します。3つのプロンプトを実装しましょう:日次ノート用、ノートの要約用、そしてアイデアのブレインストーミング用です。これにはいくつかのmarkdown解析ライブラリが必要です。

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

      次に、src/mastra/mcp/prompts.tsでプロンプトを実装しましょう:

      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: "新しい日次ノートを作成します。", version: "1.0.0" }, { name: "summarize_note", description: "ノートのTL;DRを提供します。", version: "1.0.0" }, { name: "brainstorm_ideas", description: "ノートに基づいて新しいアイデアをブレインストーミングします。", 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: `${today}」というタイトルで、「## タスク」、「## ミーティング」、「## ノート」のセクションを含む新しいノートを作成してください。` } }]; case "summarize_note": if (!args?.noteContent) throw new Error("コンテンツが提供されていません"); const metaSum = await analyzeMarkdown(args.noteContent as string); return [{ role: "user", content: { type: "text", text: `各セクションを3つ以下の箇条書きで要約してください。\\n\\n### アウトライン\\n${metaSum.headings.map(h => `- ${h} (${metaSum.wordCounts[h] || 0} 語)`).join("\\n")}`.trim() } }]; case "brainstorm_ideas": if (!args?.noteContent) throw new Error("コンテンツが提供されていません"); const metaBrain = await analyzeMarkdown(args.noteContent as string); return [{ role: "user", content: { type: "text", text: `以下の未発達なセクション${args?.topic ? `${args.topic}について` : 'について'}3つのアイデアをブレインストーミングしてください。\\n\\n未発達なセクション:\\n${metaBrain.headings.length ? metaBrain.headings.map(h => `- ${h}`).join("\\n") : "- (なし、任意のものを選択)"}` } }]; default: throw new Error(`プロンプト「${name}」が見つかりません`); } }; export const promptHandlers: MCPServerPrompts = { listPrompts: async () => prompts, getPromptMessages, };

      次に、src/mastra/mcp/server.tsでこれらのプロンプトハンドラーを登録します:

      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 notesServer = new MCPServer({ name: "notes", version: "0.1.0", resources: resourceHandlers, prompts: promptHandlers, tools: { write: writeNoteTool, }, });

      サーバーを実行する

      開発サーバーを実行できるようになりました:

      npm run dev

      Mastraプレイグラウンド(通常はhttp://localhost:4111)に移動します。 「MCP Servers」セクションで、notesサーバーを見つけることができます。 プレイグラウンドを使用して以下のことができます:

      • MCPサーバーのエンドポイントを取得する
      • CursorやWindsurfなどのMCPクライアント用のクライアント設定を取得する
      • MCPサーバーで利用可能なツールのリストを表示し、テストする

      次のステップ

      このMCPサーバーを任意のMCPクライアントで使用できるようになりました。例えば、CursorやWindsurfと組み合わせてノートを管理することができます。