MCP Server Guide: Notes MCP Serverの構築
このガイドでは、完全なMCP(Model Context Protocol)サーバーをゼロから構築する方法を学びます。このサーバーはマークダウンノートのコレクションを管理し、ノートの作成と読み取りを行うツールを公開し、ノート作成を支援するインテリジェントなプロンプトを提供します。
前提条件
- Node.jsがインストールされていること
- TypeScriptの基本的な理解
計画
以下の機能を持つnotes
サーバーを構築します:
- ノートの一覧表示と読み取り: クライアントがサーバーに保存されたmarkdownファイルを閲覧・表示できるようにします。
- ノートの書き込み: ノートを作成または更新するためのツールを提供します。
- スマートプロンプトの提供: 日次ノートテンプレートの作成や既存コンテンツの要約など、コンテキストに応じたプロンプトを生成します。
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インスタンスに登録されていたこれらのファイルからコンポーネントを削除しましょう。
ファイルは次のようになります:
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 サーバーインスタンスを定義しましょう:
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 サーバーの公開識別子です:
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
にハンドラーを実装しましょう:
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
に登録します:
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
でツールを定義します:
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
でこのツールを登録します:
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
でプロンプトを実装しましょう:
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
でこれらのプロンプトハンドラーを登録します:
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と組み合わせてノートを管理することができます。