Notes MCP サーバーの構築
このガイドでは、ゼロから完全な MCP(Model Context Protocol)サーバーを構築する方法を学びます。このサーバーは Markdown ノートのコレクションを管理し、次の機能を備えています:
- ノートの一覧と閲覧: クライアントがサーバーに保存された Markdown ファイルを参照・表示できるようにします
- ノートの作成・更新: ノートを新規作成または更新するためのツールを提供します
- スマートプロンプトの提供: 日次ノートのテンプレート作成や既存コンテンツの要約など、文脈に応じたプロンプトを生成します
前提条件
- Node.js
v20.0
以降がインストールされていること - サポート対象のモデルプロバイダーの API キー
- 既存の Mastra プロジェクト(新規プロジェクトのセットアップはインストールガイドをご参照ください)
必要な依存関係とファイルの追加
MCP サーバーを作成する前に、追加の依存関係をインストールし、ボイラープレートとなるフォルダ構成を用意します。
@mastra/mcp
をインストール
@mastra/mcp
をプロジェクトに追加します:
npm install @mastra/mcp
デフォルトのプロジェクトを整理
デフォルトのインストールガイドに従うと、このガイドには不要なファイルがプロジェクトに含まれます。これらは安全に削除できます:
rm -rf src/mastra/agents src/mastra/workflows src/mastra/tools/weather-tool.ts
また、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({
// テレメトリや評価などをメモリストレージに保存します。永続化が必要な場合は 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 サーバーの作成
MCP サーバーを追加してみましょう!
MCPサーバーを作成して登録する
「src/mastra/mcp/server.ts」で、MCP サーバーのインスタンスを定義します:
import { MCPServer } from "@mastra/mcp";
export const notes = new MCPServer({
name: "notes",
version: "0.1.0",
tools: {},
});
src/mastra/index.ts
の Mastra インスタンスにこの MCP サーバーを登録します。キー notes
はこの MCP サーバーの公開識別子です。
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({
// テレメトリや評価などをメモリストレージに保存します。永続化する場合は file:../mastra.db に変更してください
url: ":memory:",
}),
logger: new PinoLogger({
name: "Mastra",
level: "info",
}),
mcpServers: {
notes,
},
});
リソースハンドラを実装して登録する
リソースハンドラーを使うと、クライアントはサーバーが管理するコンテンツを発見して読み取れます。notes
ディレクトリ内の Markdown ファイルを扱うハンドラーを実装し、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"); // 既定の出力ディレクトリからの相対パス
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: `「${title}」に関するメモ`,
mime_type: "text/markdown",
};
});
} catch (error) {
console.error("ノートリソースの一覧取得中にエラーが発生しました:", 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(`リソース ${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
にこれらのリソースハンドラーを登録してください:
import { MCPServer } from "@mastra/mcp";
import { resourceHandlers } from "./resources";
export const notes = new MCPServer({
name: "notes",
version: "0.1.0",
tools: {},
resources: resourceHandlers,
});
ツールを実装して登録する
ツールとは、サーバーが実行できるアクションのことです。write
ツールを作成しましょう。
まず、src/mastra/tools/write-note.ts
でツールを定義します。
import { createMCPTool } from "@mastra/mcp";
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 = createMCPTool({
id: "write",
description: "新規ノートの作成または既存ノートの上書きを行います。",
inputSchema: z.object({
title: z
.string()
.nonempty()
.describe("ノートのタイトル。ファイル名として使用されます。"),
content: z
.string()
.nonempty()
.describe("ノートのMarkdown形式の内容。"),
}),
outputSchema: z.string().nonempty(),
execute: async (params, { elicitation, extra }) => {
try {
const { title, content } = params;
const filePath = path.join(NOTES_DIR, `${title}.md`);
await fs.mkdir(NOTES_DIR, { recursive: true });
await fs.writeFile(filePath, content, "utf-8");
return `ノート「${title}」に正常に書き込みました。`;
} catch (error: any) {
return `ノートの書き込み中にエラーが発生しました: ${error.message}`;
}
},
});
このツールを 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,
},
});
プロンプトの実装と登録
Prompt ハンドラーは、クライアントがすぐに使えるプロンプトを提供します。次の3つを追加します:
- 日次ノート
- ノートを要約
- アイデア出し
これには、いくつかのMarkdown解析ライブラリをインストールする必要があります:
npm install unified remark-parse gray-matter @types/unist
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: "ノートの要点を短く教えてください。",
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 = "無題";
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}」の新しいノートを作成し、セクションは「## Tasks」「## Meetings」「## Notes」としてください。`,
},
},
];
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
で次のプロンプトハンドラを登録します:
import { MCPServer } from "@mastra/mcp";
import { resourceHandlers } from "./resources";
import { writeNoteTool } from "../tools/write-note";
import { promptHandlers } from "./prompts";
export const notes = new MCPServer({
name: "notes",
version: "0.1.0",
resources: resourceHandlers,
prompts: promptHandlers,
tools: {
write: writeNoteTool,
},
});
サーバーを実行する
素晴らしいですね。これで最初の MCP サーバーを作成できました!playground を起動して試してみましょう:
npm run dev
ブラウザで http://localhost:4111
を開きます。左側のサイドバーで MCP Servers を選択し、notes MCP サーバーを選びます。
IDE に MCP サーバーを追加するための手順が表示されます。この MCP サーバーは任意の MCP クライアントで使用できます。右側の Available Tools の下で write ツールも選択できます。
write ツールで、名前に test
、Markdown の内容に this is a test
を入力して試してみてください。Submit をクリックすると、notes
内に新しい test.md
ファイルが作成されます。