at backyard

Color my life with the chaos of trouble.

MCP Serverについて調べてみた備忘録(MCPサーバーの最小限の実装例)

皆が便利だと言っているMCPについて今更ながら入門したので備忘録として残しておく(自分用メモ)

MCPとは?なぜ話題になっている?

MCP(Model Context Protocol)とは、LLM(大規模言語モデル)と外部ツール・データソースをつなぐための統一的なプロトコルを提供する仕組み。

AIエージェントとあらゆる外部システムをつなぐ「USB-Cポート」のようなものをイメージすると良い。

通常、AIの外部連携(ファイル、リポジトリAPIアクセスなど)を行うにはケースごとに専用スクリプトが必要になるが、MCPによって統一インターフェースを介した拡張が可能になる。

これにより、エディタがAIに「実際に外部の何かを実行させる」ことが格段に容易になり、たとえばファイルの読み書き、DBクエリ、API呼び出し、テストの実行、デプロイ、CI/CDとの連携などが、MCP対応のプラグインMCPサーバ)として一元化される。

MCPの基本

MCPと一言で言っても、

というのがある

MCP ClientはCursorやClaude Desktop、Clineのようなものを指す。
また、自前でMCP Clientを書くこともできる

構成的にはざっくり以下のようになっている。構成としてはとてもシンプルで分かりやすい。

MCP Client <---> MCP Server

世間一般的に言われているMCPというのは大抵MCP Serverのことを指しており、ここでCursorなどのMCP Client(AIエージェント)から渡ってきた指示をもとにMCP Server側であらゆることを実行することになる。
(この "あらゆること" に様々な可能性が詰められており、今話題になっているという形)

ちなみにMCP関連については以下を見ておけば最低限の理解は可能そう。

modelcontextprotocol.io

また、Cursorで使う場合はCursor側が出しているドキュメントを参照

docs.cursor.com

Clineで使う場合はClineが出しているドキュメントを参照

docs.cline.bot

MCPサーバーの最小限の実装例

MCP Serverの基本的な挙動を理解するには、まずは「どんなコードが必要なのか」をざっくり見てみるのが早い。以下のサンプルでは、外部ライブラリを使わず、標準入出力(STDIN/STDOUT)を介してMCPクライアント(Cursorなど)と通信する仕組みを、約100行程度で実装している。

JSON-RPC 2.0を扱う」「MCPハンドシェイクを処理する」「ツール呼び出しに応答する」という3点が押さえられれば、ひとまず動くMCPサーバーになるのが分かると思う。

※TypeScriptで書いているので、実際に動かす際はbuildして js にする。node /foo/bar/simple-mcp-server.js という形でCursor側ではプロセスを起動して動かす想定

/**
 * シンプルな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 Serverとして登録して実行したものが以下。
(TestServerという名称で登録している)

指示の文章に誤字があるのは御愛嬌...

コード解説

以上が、「最もシンプルなMCPサーバー実装例」となる。

  1. 標準入出力でJSON-RPCを読み書きする

    • 冒頭で readline を使って process.stdin を行単位で読み込み、process.stdout.write によってレスポンスを返している。
    • MCPクライアント(例えばCursor)がこのプロセスを起動し、標準入出力を通じてメッセージを送受信するイメージ。
  2. JSON-RPCをベースとしたMCPメソッドに分岐

    • initializeプロトコルバージョンやサーバー情報の受け渡し
    • initialized → クライアントからの初期化完了通知
    • tools/list → どんなツールがサーバーにあるかの一覧
    • tools/call → 実際にツールを呼び出して何か処理を行う
    • など、MCPでは一定のメソッドが定義されており、それらを普通のif分岐で処理している。
  3. echoツールのみを実装

    • tools という配列に、echo という名前のツールを1つだけ追加している。
    • tools/list が呼ばれると、このツール情報(名前、説明、inputSchema)が返される。
    • tools/call{ name: "echo", arguments: { message: "Hello" } } のような引数が渡されると、execute 関数が呼ばれて "Echo: Hello" というテキストを返す。
  4. エラー処理

    • JSONがパースできない場合は-32700(Parse error)MCPメソッドの形式が合わない場合は-32600(Invalid Request)、など、JSON-RPC 2.0の標準エラーコードを使っている。
    • MCPでは「知らないメソッド名の場合は-32601(Method not found)を返す」というのが基本ルール。
  5. 最低限の機能

    • リソース一覧(resources/list)やプロンプト一覧(prompts/list)は空を返すだけなので、特に機能としては実装していない。
    • もう少し拡張したいときは同様にif (method===\"resources/list\") { ... } の中で色々書けばOK。

このように、MCPサーバーの最小限の実装は「JSON-RPCメソッドの受け取りと分岐処理」を自力で書くだけでも可能だし、そこに外部ライブラリや外部API呼び出しを加えていけば、いわゆる“MCPプラグインが作れるようになるわけだ。

今回は自身の理解のためにも最低限の実装を動かしてみたが、基本的にMCP Serverを自前で実装するケースはなく、 公式のNode向けライブラリ@modelcontextprotocol/sdk)などを使って作成していくケースが多いかと思われる。

また今回の最小限の実装では標準入出力のみの対応だが、公式ライブラリではSSEなどにも対応していたり、OAuth認証やZodによるスキーマ検証なども入っている。

ただ逆に、JSON-RPC 2.0の仕様に基づいていればライブラリがなくても実装できるため、好きな言語でMCPサーバーを書くことが可能であるとも言える。
こういった、ある程度開発者に自由がある環境というのは、今後MCPが盛り上がっていきそうな要因の一つになるかなと思う。
(どの言語でも実装できるメリットとして他にも、GoやRustなどで実装してシングルバイナリで実行できる形で配布すれば、ユーザー側でNode.jsやPython環境を用意してもらう必要もない)

MCP Marketplaceの存在(Cline)

ClineにはMCP Marketplaceという存在があるらしい。 VSCodeのExtension marketplaceのようなもの。

cline.bot

リリースするにはGitHub経由で提出して審査を受ける必要がある。

その他、MCP Serverがまとめられているサイト

他にもこういうサイトがあるみたい

mcpserver.cc

MCP Serverはやろうと思えば様々なことができてしまうが、外部のMCPを使う場合の安全性の線引がどこらへんにあるのか、私はまだ温度感を掴めていない。

MCPが話題になっている理由が分かった気がした

MCP Clinetが操縦席だとすると、実際の実務を行なうのがMCP Serverとなるだろうか。たしかにこの仕組みを使えばAIに指示を出してやれることが一気に増えるし、可能性も溢れていそう。

MCPが話題になっているのが少しだけ分かった気がした。