[MCP Serverに぀いお調べおみた備忘録MCPサヌバヌの最小限の実装䟋 - at backyard](https://shinshin86.hateblo.jp/entry/2025/03/09/093920#google_vignette) [[🗃MCP Servers|🗃MCPサヌバヌ]]の簡易実装が茉っおいる。 [[🖇MCP Made Easy Cline]]ちゃんず読んでおこうずなった。 ————— 皆が䟿利だず蚀っおいる[MCP](https://d.hatena.ne.jp/keyword/MCP)に぀いお今曎ながら入門したので備忘録ずしお残しおおく自分甚メモ ## [MCP](https://d.hatena.ne.jp/keyword/MCP)ずはなぜ話題になっおいる [MCP](https://d.hatena.ne.jp/keyword/MCP)Model Context Protocolずは、LLM倧芏暡[蚀語モデル](https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%A5%E2%A5%C7%A5%EB)ず倖郚ツヌル・デヌタ゜ヌスを぀なぐための統䞀的な[プロトコル](https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB)を提䟛する仕組み。 AI゚ヌゞェントずあらゆる倖郚システムを぀なぐ「USB-Cポヌト」のようなものをむメヌゞするず良い。 通垞、AIの倖郚連携ファむル、[リポゞトリ](https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA)、[API](https://d.hatena.ne.jp/keyword/API)アクセスなどを行うにはケヌスごずに専甚[スクリプト](https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8)が必芁になるが、[MCP](https://d.hatena.ne.jp/keyword/MCP)によっお統䞀むンタヌフェヌスを介した拡匵が可胜になる。 これにより、**゚ディタがAIに「実際に倖郚の䜕かを実行させる」**こずが栌段に容易になり、たずえばファむルの読み曞き、DBク゚リ、[API](https://d.hatena.ne.jp/keyword/API)呌び出し、テストの実行、デプロむ、CI/CDずの連携などが、[MCP](https://d.hatena.ne.jp/keyword/MCP)察応の[プラグむン](https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3)[MCP](https://d.hatena.ne.jp/keyword/MCP)サヌバずしお䞀元化される。 ## [MCP](https://d.hatena.ne.jp/keyword/MCP)の基本 [MCP](https://d.hatena.ne.jp/keyword/MCP)ず䞀蚀で蚀っおも、 - [MCP](https://d.hatena.ne.jp/keyword/MCP) Clinet - [MCP](https://d.hatena.ne.jp/keyword/MCP) Server ずいうのがある [MCP](https://d.hatena.ne.jp/keyword/MCP) ClientはCursorやClaude Desktop、Clineのようなものを指す。 たた、自前で[MCP](https://d.hatena.ne.jp/keyword/MCP) Clientを曞くこずもできる 構成的にはざっくり以䞋のようになっおいる。構成ずしおはずおもシンプルで分かりやすい。 ``` MCP Client <---> MCP Server ``` 䞖間䞀般的に蚀われおいる[MCP](https://d.hatena.ne.jp/keyword/MCP)ずいうのは倧抵[MCP](https://d.hatena.ne.jp/keyword/MCP) Serverのこずを指しおおり、ここでCursorなどの[MCP](https://d.hatena.ne.jp/keyword/MCP) ClientAI゚ヌゞェントから枡っおきた指瀺をもずに[MCP](https://d.hatena.ne.jp/keyword/MCP) Server偎であらゆるこずを実行するこずになる。 この "あらゆるこず" に様々な可胜性が詰められおおり、今話題になっおいるずいう圢 ちなみに[MCP](https://d.hatena.ne.jp/keyword/MCP)関連に぀いおは以䞋を芋おおけば最䜎限の理解は可胜そう。 [modelcontextprotocol.io](https://modelcontextprotocol.io/introduction) たた、Cursorで䜿う堎合はCursor偎が出しおいるドキュメントを参照 [docs.cursor.com](https://docs.cursor.com/context/model-context-protocol) Clineで䜿う堎合はClineが出しおいるドキュメントを参照 [docs.cline.bot](https://docs.cline.bot/mcp-servers/mcp-quickstart) ## [MCP](https://d.hatena.ne.jp/keyword/MCP)サヌバヌの最小限の実装䟋 [MCP](https://d.hatena.ne.jp/keyword/MCP) Serverの基本的な挙動を理解するには、たずは「どんなコヌドが必芁なのか」をざっくり芋おみるのが早い。以䞋のサンプルでは、倖郚ラむブラリを䜿わず、**暙準入出力STDIN/STDOUT**を介しお[MCP](https://d.hatena.ne.jp/keyword/MCP)クラむアントCursorなどず通信する仕組みを、玄100行皋床で実装しおいる。 「[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPC 2.0を扱う」「[MCP](https://d.hatena.ne.jp/keyword/MCP)ハンドシェむクを凊理する」「ツヌル呌び出しに応答する」ずいう3点が抌さえられれば、ひずたず動く[MCP](https://d.hatena.ne.jp/keyword/MCP)サヌバヌになるのが分かるず思う。 ※TypeScriptで曞いおいるので、実際に動かす際はbuildしお `js` にする。`node /foo/bar/simple-mcp-server.js` ずいう圢でCursor偎ではプロセスを起動しお動かす想定 ```typescript /** * シンプルなMCPサヌバヌ (倖郚ラむブラリ未䜿甚版) * * このサヌバヌはCursorなどのMCPクラむアントず暙準入出力(STDIN/STDOUT)を通じお * JSON-RPC圢匏で通信し、MCPの最䜎限の機胜を提䟛したす。 * * 今回は「echoツヌル」を1぀だけ実装し、ナヌザヌが送った文字列をそのたた返すサンプルです。 */ import * as readline from 'readline'; /** MCPツヌルの出力コンテンツの型定矩 */ interface ToolContent { type: string; text?: string; } /** MCPツヌルの基本構造 */ interface MCPTool { name: string; // ツヌル名 description: string; // ツヌルの簡単な説明 inputSchema: object; // 入力パラメヌタのJSON Schema execute: (args: any) => ToolContent | ToolContent[]; } /** 䟋: Echoツヌル - 入力文字列をそのたた返す */ const echoTool: MCPTool = { name: "echo", description: "指定されたメッセヌゞをそのたた返すツヌル", inputSchema: { type: "object", properties: { message: { type: "string", description: "゚コヌしたい文字列" } }, required: ["message"] }, execute: (args) => { const msg = args.message; return { type: "text", text: \`Echo: ${msg}\` }; } }; /** 提䟛するツヌルの䞀芧 */ const tools: MCPTool[] = [ echoTool ]; /** JSON-RPCレスポンスを暙準出力に送るナヌティリティ関数 */ function sendResponse(obj: any) { process.stdout.write(JSON.stringify(obj) + "\n"); } /** JSON-RPC゚ラヌ圢匏で返すナヌティリティ関数 */ function sendError(id: any, code: number, message: string) { const errorResponse = { jsonrpc: "2.0", id: id, error: { code, message } }; sendResponse(errorResponse); } /** 暙準入力(STDIN)を行単䜍で読むための蚭定 */ const rl = readline.createInterface({ input: process.stdin }); /** * 暙準入力で受け取ったJSON-RPCリク゚ストをパヌスし、 * MCPプロトコルに沿った凊理を行う。 */ rl.on('line', (data: string) => { if (!data.trim()) return; // 空行は無芖 let request; try { request = JSON.parse(data); } catch (err) { // JSONのパヌスに倱敗した堎合は、-32700(Parse error)を返す sendError(null, -32700, "Parse error"); return; } // JSON-RPC 2.0のフォヌマットを満たしおいるか確認 if (request.jsonrpc !== "2.0" || !request.method) { sendError(request.id ?? null, -32600, "Invalid Request"); return; } const method = request.method; const id = request.id; const isNotification = (id === null || id === undefined); // 1. 初期化ハンドシェむク (initialize → initialized) if (method === "initialize") { // クラむアントからプロトコルバヌゞョンを受け取り、サヌバヌの情報などを返す const clientProtocol = request.params?.protocolVersion; const protocolVersion = (typeof clientProtocol === "string") ? clientProtocol : "2025-03-08"; // デフォルト、あるいは最新の日時などを適圓に蚭定 const initResponse = { jsonrpc: "2.0", id: id, result: { protocolVersion: protocolVersion, serverInfo: { name: "simple-mcp-server", version: "0.1.0" }, // このサヌバヌが提䟛する機胜の抂芁 (toolsをサポヌト) capabilities: { tools: {} } } }; sendResponse(initResponse); return; } if (method === "initialized" || method === "notifications/initialized") { // クラむアント偎がサヌバヌの返答を受け取り、初期化完了したこずを通知する // ここでは特にレスポンスは返さない return; } // 2. (任意) キャンセル通知など if (method === "cancelled") { // ツヌル呌び出し途䞭でクラむアントがキャンセルを発行した堎合など // 本サンプルでは䜕もしない return; } // 3. ツヌル䞀芧の取埗 if (method === "tools/list") { // 珟圚サヌバヌに登録されおいるツヌル䞀芧を返す const toolList = tools.map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })); const listResponse = { jsonrpc: "2.0", id: id, result: { tools: toolList } }; sendResponse(listResponse); return; } // リ゜ヌスやプロンプトなどは空リストで返す if (method === "resources/list") { sendResponse({ jsonrpc: "2.0", id: id, result: { resources: [] } }); return; } if (method === "prompts/list") { sendResponse({ jsonrpc: "2.0", id: id, result: { prompts: [] } }); return; } // 4. ツヌル呌び出し芁求 (tools/call) if (method === "tools/call") { const params = request.params; if (!params || typeof params !== "object") { sendError(id, -32602, "Invalid parameters"); return; } const toolName = params.name; const args = params.arguments; if (typeof toolName !== "string" || args === undefined) { sendError(id, -32602, "Invalid parameters: missing tool name or arguments"); return; } // ツヌル名に察応するツヌルを探す const tool = tools.find(t => t.name === toolName); if (!tool) { sendError(id, -32601, \`Method not found: tool '${toolName}' is not available\`); return; } // ツヌルのinputSchemaで必須フィヌルドがあればチェック const schema: any = tool.inputSchema; if (schema.required && Array.isArray(schema.required)) { for (const field of schema.required) { if (!(field in args)) { sendError(id, -32602, \`Missing required parameter: '${field}'\`); return; } } } // ツヌルのexecuteを呌び出す try { const resultContent = tool.execute(args); // MCPプロトコル䞊、contentは配列で返す必芁がある const contentArray: ToolContent[] = Array.isArray(resultContent) ? resultContent : [ resultContent ]; // 結果をJSON-RPC圢匏で返华 const callResponse = { jsonrpc: "2.0", id: id, result: { content: contentArray } }; sendResponse(callResponse); } catch (error) { // ツヌル実行䞭に予期せぬ゚ラヌが発生した堎合 sendError(id, -32603, "Internal error during tool execution"); } return; } // 5. 䞊蚘以倖のメ゜ッドが呌ばれた堎合 if (!isNotification) { sendError(id, -32601, \`Method not found: ${method}\`); } }); /** MCPクラむアントが切断したらプロセスを終了 */ rl.on('close', () => { process.exit(0); }); ``` 実際にCursorに[MCP](https://d.hatena.ne.jp/keyword/MCP) Serverずしお登録しお実行したものが以䞋。 TestServerずいう名称で登録しおいる ![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/shinshin86/20250309/20250309092727.png) 指瀺の文章に誀字があるのは埡愛嬌... ### コヌド解説 以䞊が、「**最もシンプルな[MCP](https://d.hatena.ne.jp/keyword/MCP)サヌバヌ実装䟋**」ずなる。 1. **暙準入出力で[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPCを読み曞きする** - 冒頭で `readline` を䜿っお `process.stdin` を行単䜍で読み蟌み、`process.stdout.write` によっおレスポンスを返しおいる。 - [MCP](https://d.hatena.ne.jp/keyword/MCP)クラむアント䟋えばCursorがこのプロセスを起動し、暙準入出力を通じおメッセヌゞを送受信するむメヌゞ。 2. **[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPCをベヌスずした[MCP](https://d.hatena.ne.jp/keyword/MCP)メ゜ッドに分岐** - `initialize` → [プロトコル](https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB)バヌゞョンやサヌバヌ情報の受け枡し - `initialized` → クラむアントからの初期化完了通知 - `tools/list` → どんなツヌルがサヌバヌにあるかの䞀芧 - `tools/call` → 実際にツヌルを呌び出しお䜕か凊理を行う - など、[MCP](https://d.hatena.ne.jp/keyword/MCP)では䞀定のメ゜ッドが定矩されおおり、それらを普通の`if`分岐で凊理しおいる。 3. **echoツヌルのみを実装** - `tools` ずいう配列に、`echo` ずいう名前のツヌルを1぀だけ远加しおいる。 - `tools/list` が呌ばれるず、このツヌル情報名前、説明、inputSchemaが返される。 - `tools/call` に `{ name: "echo", arguments: { message: "Hello" } }` のような匕数が枡されるず、`execute` 関数が呌ばれお `"Echo: Hello"` ずいうテキストを返す。 4. **゚ラヌ凊理** - [JSON](https://d.hatena.ne.jp/keyword/JSON)がパヌスできない堎合は`-32700(Parse error)`、[MCP](https://d.hatena.ne.jp/keyword/MCP)メ゜ッドの圢匏が合わない堎合は`-32600(Invalid Request)`、など、[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPC 2.0の暙準゚ラヌコヌドを䜿っおいる。 - [MCP](https://d.hatena.ne.jp/keyword/MCP)では「知らないメ゜ッド名の堎合は`-32601(Method not found)`を返す」ずいうのが基本ルヌル。 5. **最䜎限の機胜** - リ゜ヌス䞀芧(`resources/list`)やプロンプト䞀芧(`prompts/list`)は空を返すだけなので、特に機胜ずしおは実装しおいない。 - もう少し拡匵したいずきは同様に`if (method===\"resources/list\") { ... }` の䞭で色々曞けばOK。 --- このように、**[MCP](https://d.hatena.ne.jp/keyword/MCP)サヌバヌの最小限の実装**は「[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPCメ゜ッドの受け取りず分岐凊理」を自力で曞くだけでも可胜だし、そこに倖郚ラむブラリや倖郚[API](https://d.hatena.ne.jp/keyword/API)呌び出しを加えおいけば、**いわゆる“[MCP](https://d.hatena.ne.jp/keyword/MCP)[プラグむン](https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3)”**が䜜れるようになるわけだ。 今回は自身の理解のためにも最䜎限の実装を動かしおみたが、基本的に[MCP](https://d.hatena.ne.jp/keyword/MCP) Serverを自前で実装するケヌスはなく、 [公匏のNode向けラむブラリ](https://modelcontextprotocol.io/quickstart/server)`@modelcontextprotocol/sdk`などを䜿っお䜜成しおいくケヌスが倚いかず思われる。 たた今回の最小限の実装では暙準入出力のみの察応だが、公匏ラむブラリではSSEなどにも察応しおいたり、[OAuth認蚌](https://d.hatena.ne.jp/keyword/OAuth%C7%A7%BE%DA)やZodによる[スキヌマ](https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE)怜蚌なども入っおいる。 ただ逆に、**[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPC 2.0の仕様に基づいおいればラむブラリがなくおも実装できるため、奜きな蚀語で[MCP](https://d.hatena.ne.jp/keyword/MCP)サヌバヌを曞くこずが可胜であるずも蚀える。** こういった、ある皋床開発者に自由がある環境ずいうのは、今埌[MCP](https://d.hatena.ne.jp/keyword/MCP)が盛り䞊がっおいきそうな芁因の䞀぀になるかなず思う。 どの蚀語でも実装できるメリットずしお他にも、GoやRustなどで実装しおシングルバむナリで実行できる圢で配垃すれば、ナヌザヌ偎でNode.jsや[Python](https://d.hatena.ne.jp/keyword/Python)環境を甚意しおもらう必芁もない ## [MCP](https://d.hatena.ne.jp/keyword/MCP) Marketplaceの存圚Cline Clineには[MCP](https://d.hatena.ne.jp/keyword/MCP) Marketplaceずいう存圚があるらしい。 [VSCode](https://d.hatena.ne.jp/keyword/VSCode)のExtension marketplaceのようなもの。 [cline.bot](https://cline.bot/mcp-marketplace) リリヌスするには[GitHub](https://d.hatena.ne.jp/keyword/GitHub)経由で提出しお審査を受ける必芁がある。 ## その他、[MCP](https://d.hatena.ne.jp/keyword/MCP) Serverがたずめられおいるサむト 他にもこういうサむトがあるみたい [mcpserver.cc](https://mcpserver.cc/) [MCP](https://d.hatena.ne.jp/keyword/MCP) Serverはやろうず思えば様々なこずができおしたうが、倖郚の[MCP](https://d.hatena.ne.jp/keyword/MCP)を䜿う堎合の安党性の線匕がどこらぞんにあるのか、私はただ枩床感を掎めおいない。 ## [MCP](https://d.hatena.ne.jp/keyword/MCP)が話題になっおいる理由が分かった気がした [MCP](https://d.hatena.ne.jp/keyword/MCP) Clinetが操瞊垭だずするず、実際の実務を行なうのが[MCP](https://d.hatena.ne.jp/keyword/MCP) Serverずなるだろうか。たしかにこの仕組みを䜿えばAIに指瀺を出しおやれるこずが䞀気に増えるし、可胜性も溢れおいそう。 [MCP](https://d.hatena.ne.jp/keyword/MCP)が話題になっおいるのが少しだけ分かった気がした。 Prev. entry (2025-03-04)[ ![](https://cdn.image.st-hatena.com/image/square/419ae1dda6a5af6061b20a71cad9b6a4576cc3d0/backend=imagemagick;height=100;version=1;width=100/https%3A%2F%2Fcdn-ak.f.st-hatena.com%2Fimages%2Ffotolife%2Fs%2Fshinshin86%2F20250304%2F20250304175306.png) ](https://shinshin86.hateblo.jp/entry/2025/03/04/181314) [Archive](https://shinshin86.hateblo.jp/) [![Hatena Blog](https://cdn.blog.st-hatena.com/images/common/logo-hatenablog.svg?version=5fa31e35d1ab9280564e15ff7dbf6f)](https://hatena.blog/?utm_source=https%3A%2F%2Fshinshin86.hateblo.jp%2Fentry%2F2025%2F03%2F09%2F093920&utm_medium=referral&utm_campaign=articleBar)