MCP Serverのフルスタック実践:Node.js + Zodを使ってAIファイル読み取りツールをゼロから構築

MCP Serverのフルスタック実践:Node.js + Zodを使ってAIファイル読み取りツールをゼロから構築

前回の記事では、公式の server-filesystem を使用して MCP を設定する方法を学びました。しかし、ツールのロジックをカスタマイズしたい場合はどうすればよいでしょうか?本記事では、@modelcontextprotocol/sdkzod を使用して、ゼロから完全な MCP サーバーを手書きで実装し、AI がローカルファイルを読み取る機能を実現するとともに、MCP プロトコルの通信メカニズムとツール登録プロセスを深く理解します。


はじめに

前回の記事では、.mcp.json を設定することで、公式の server-filesystem を使用しました。しかし、ファイルの読み書き以上の機能が必要な場合はどうでしょうか?例えば、AIにログの読み取り、データベースの照会、内部APIの呼び出しを行わせたい場合――独自のMCPサーバーを自作する必要があります。

本記事では、以下の内容をステップバイステップで解説します:

  1. MCP Serverの通信原理(stdio転送)の理解
  2. @modelcontextprotocol/sdk を使用してServerインスタンスを作成
  3. zod を使用してパラメータスキーマを宣言(型安全性の検証)
  4. ファイルを読み込む完全なMCP Serverの実装
  5. Claude Code PromptからServerでの実行に至るまでの完全な通信経路の理解

一、MCP Serverの動作原理

1.1 通信経路の全体像

(Claude Codeを介して)リクエストを送信すると、データの流れは以下の通りです:

┌──────────────────────────────────────────────────────────────┐
│ MCPの通信フロー全体 │
│ │
│ ユーザーのプロンプト │
│ ↓ │
│ Claude Code(ホスト) │
│ ↓ │
│ LLM推論:ファイルの読み込みが必要 → ファイルシステムクライアントを選択 │
│ ↓ │
│ StdioServerTransport(stdin/stdout) │
│ ↓ stdin(JSON-RPCメッセージ) │
│ MCPサーバー(当社のserver.js) │
│ ↓ read_file ツールの実行 │
│ ローカルファイルの読み込み │
│ ↓ stdout(JSON-RPC 返却結果) │
│ StdioServerTransport │
│ ↓ │
│ Claude Code → LLM → 最終回答の生成 │
└────────────────────────────────────────────── ────────────────┘

重要なポイント:MCP Server は独立した Node.js プロセスであり、標準入出力(stdin/stdout)を介して Host と通信します。Host は Server を子プロセスとして起動し、両者は JSON-RPC メッセージを介してデータを交換します。

1.2 Serverの3つの主要な役割

役割 説明
宣言ツール スキーマを使用してツール名、パラメータ、説明を定義
ロジックの実行 パラメータの受信 → 操作の実行 → 結果の返却
プロトコル通信 stdio/SSE を通じて Host とメッセージを交換

2. プロジェクトのセットアップ:package.json から始める

2.1 プロジェクトの初期化

mkdir simple-read-mcp
cd simple-read-mcp
npm init -y

2.2 依存関係のインストール

npm install @modelcontextprotocol/sdk zod

2つの主要な依存関係

依存関係 バージョン 役割
@modelcontextprotocol/sdk ^1.29.0 MCP 公式 SDK。プロトコル通信の詳細をカプセル化
zod ^4.4.3 TypeScript 優先のランタイムデータ検証ライブラリ

2.3 完全な package.json

{
  「name」: 「simple-read-mcp」,
  「version」: 「1.0.0」,
  「description」: 「カスタム MCP サーバー。AI にファイル読み取り機能を提供」,
  「main」: 「server.js」,
  『type』: 「module」,
  
「dependencies」: {
    「@modelcontextprotocol/sdk」: 「^1.29.0」,
    
「zod」: 「^4.4.3」
  }
}

注意『type』: 「module」 は、ES Modules(.mjs または .js 内で import/export が使用可能)を使用することを宣言しています。


3. コアコード:40行でMCPサーバーを実装

3.1 完全なコード

server.js

import { McpServer } from 「@modelcontextprotocol/sdk/server/mcp.js」;
import { StdioServerTransport } from 「@modelcontextprotocol/sdk/server/stdio.js」;
import { z } from 「zod」;
import fs from 『fs/promises』;

// 1. MCP Server インスタンスの作成
const server = new McpServer({
  name: 『simple-read-mcp』,
  version: 『1.0.0』
});

// 2. ツールの登録:zod スキーマによる検証を内蔵
server.tool(
  「read_file」, // ツール名
  
「指定されたパスにあるローカルファイルの内容を読み取る」, // ツールの説明(LLMに表示される)
  {
    path: z.string().describe(「ファイルの絶対パスまたは相対パス」) // 引数の検証
  },
  async ({ path }) => { // ツール実行関数
    try {
      const content = await fs.readFile(path, 『utf-8』);
      return {
        content: [{ type: 「text」, text: content }] // 標準の返却形式
      };
    
} catch (err) {
      return {
        isError: true, // エラーレスポンスとしてマーク
        content: [{ type: 「text」, text: `ファイルの読み込みに失敗しました:${err.message}` }]
      };
    }
  }
);

// 3. サービスの起動(stdio 転送モード)
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error(「MCP read_file サービスが起動しました(stdioモード)」);
}

main().catch(console.error);

3.2 コードの行ごとの解説

ステップ1:Serverインスタンスの作成

const server = new McpServer({
  name: 『simple-read-mcp』, // Server名(Hostで識別される)
  version: 『1.0.0』 // バージョン番号
});

McpServer は、MCP SDK が提供する高レベルなラッパーであり、内部で JSON-RPC メッセージの解析とシリアライズを処理するため、開発者はツールの登録とビジネスロジックにのみ注力すればよい。

ステップ2:ツールの登録

server.tool(
  「read_file」, // 引数1:ツール名
  「指定されたパスにあるローカルファイルの内容を読み取る」, // 引数2:ツールの説明(LLMはこの情報に基づいて判断を行う)
  
{ // パラメータ3:パラメータスキーマ
    path: z.string().describe(「ファイルの絶対パスまたは相対パス」)
  },
  async ({ path }) => { ... } // パラメータ4:実行関数
);

server.tool() メソッドは4つのパラメータを受け取ります:

引数 説明 類似例
ツール名 LLMが呼び出す際の識別子 関数名
説明 LLMがこれを確認して呼び出すかどうかを決定 関数のドキュメント
スキーマ 引数の型検証 関数引数の型
関数の実行 検証済みの引数を受け取り、操作を実行する 関数本体

ステップ3:標準の戻り値形式

return {
  content: [{ type: 「text」, text: content }]
};

MCPプロトコルでは、ツールが返す値は content の配列でなければならないと規定されており、各要素には typetext(または image)が含まれます。

ステップ4:エラー処理

return {
  isError: true, // LLMに「これはエラー結果です」と通知
  content: [{ type: 「text」, text: `ファイルの読み込みに失敗しました:${err.message}` }]
};

isError: true これにより、LLM に操作が失敗したことを通知し、再試行または別の方法を検討できるようになります。

ステップ5:stdio 転送の起動

const transport = new StdioServerTransport();
await server.connect(transport);

StdioServerTransport は、stdin を通じてホストからのリクエストを受け取り、stdout を通じて結果を返します。stdout はプロトコルによって占有されているため、ログ出力には console.log ではなく console.error を使用することに注意してください。


4. Zod:パラメータ検証のセーフティネット

4.1 なぜ Zod が必要なのか?

MCP Server は LLM から送信されたパラメータを受け取りますが、LLM の出力は確率的なものであり、誤った型のパラメータが渡される可能性があります。Zod は実行時にパラメータの検証を行い、予期せぬエラーを防ぎます。

// z.string() は、パラメータが文字列であることを保証します
// .describe() は説明を提供します(MCP SDK はこれを JSON Schema に変換して LLM に送信します)
{
  path: z.string().describe(「ファイルの絶対パスまたは相対パス」)
}

4.2 Zod の一般的な使い方

import { z } from 「zod」;

// 文字列
z.string()
z.string().min(1) // 空でない
z.string().describe(「説明」)

// 数値
z.number()
z.number().int().positive() // 正の整数

// ブール値
z.boolean()

// 列挙型
z.enum([「read」, 「write」, 『delete』])

// オブジェクト
z.object({
  path: z.string(),
  encoding: z.enum([「utf-8」, 「gbk」]).optional(),
  lineCount: z.number().int().optional()
})

// 配列
z.array(z.string())

4.3 Zod 対 手書きの JSON Schema

旧版 MCP SDK(手書きの JSON Schema):
{
  type: 「object」,
  properties: {
    path: { type: 『string』, description: 「ファイルパス」 }
  },
  
required: [「path」]
}

新版 MCP SDK + Zod:
{
  path: z.string().describe(「ファイルパス」)
}

新版 SDK では、内部で Zod スキーマが自動的に JSON スキーマに変換されるため、コードがより簡潔になり、型安全性も向上します。


5. Claude Code の導入:.mcp.json の設定

5.1 設定ファイル

プロジェクトのルートディレクトリに .mcp.json を作成します:

{
  「mcpServers」: {
    「simple-read」: {
      「type」: 「stdio」,
      「command」: 「node」,
      『args』: [「/path/to/simple-read-mcp/server.js」]
    }
  }
}

主な違い :以前は npx @modelcontextprotocol/server-filesystem(公式サーバー)を使用していましたが、現在は node server.js独自に作成したサーバー)を使用しています。

5.2 パラメータの比較

設定項目 公式サーバー カスタムサーバー
command npx node
args [「@modelcontextprotocol/server-filesystem」, 「/path」] [「/path/to/server.js」]
カスタマイズ性 固定機能 完全な自由度

5.3 Claude Code での使用方法

⟩ simple-read ツールを使用して server.js の内容を読み込む

Claude Code は以下を行います:

  1. リクエストを解析 → simple-read Client
  2. を選択

  3. 呼び出しパラメータ {「path」: 「server.js」}
  4. を生成

  5. stdin 経由で JSON-RPC メッセージをサーバーに送信
  6. サーバーがファイルを読み込み → stdout 経由で内容を返す
  7. LLM がファイルの内容に基づいて回答を生成

6. プロジェクト構造の全体像

simple-read-mcp/
├── package.json # プロジェクト設定 + 依存関係宣言
├── server.js # MCP Server のコアコード(40行)
├── .mcp.json # Claude Code 連携設定
└── node_modules/
    ├── @modelcontextprotocol/sdk/ # MCP プロトコル SDK
    └── zod/ # パラメータ検証ライブラリ

コード行数の統計:MCP Server 全体でわずか 40 行のコード------MCP SDK の抽象化は非常に洗練されています。


7. 基礎から応用へ:MCP Serverの拡張

7.1 ツールの追加

// ツール1:ファイルの読み込み
server.tool(「read_file」, 『ファイルの読み込み』, { path: z.string() }, async ({ path }) => {
  const content = await fs.readFile(path, 『utf-8』);
  return { content: [{ type: 「text」, text: content }] };
});

// ツール2:ディレクトリの一覧表示
server.tool(「list_dir」, 『ディレクトリ内容の一覧表示』, { path: z.string() }, async ({ path }) => {
  const files = await fs.readdir(path);
  return { content: [{ type: 「text」, text: files.join(『\\n』) }] };
});

// ツール3:ファイル情報の取得
server.tool(「file_info」, 「ファイルのメタ情報を取得」,
  { path: z.string() },
  async ({ path }) => {
    const stat = await fs.stat(path);
    return {
      
content: [{ type: 「text」, text: JSON.stringify({
        size: stat.size,
        created: stat.birthtime,
        modified: stat.mtime,
        isFile: stat.isFile(),
        isDir: stat.isDirectory()
      }, null, 2) }]
    };
  }
);

7.2 リソース(Resources)の追加

MCPはツール(Tool)だけでなく、リソース(Resource)もサポートしています------LLMにコンテキストデータを提供します:

server.resource(
  「config」, // リソース名
  「file:///config.json」, // URI
  
async (uri) => {
    const config = await fs.readFile(『./config.json』, 『utf-8』);
    return {
      contents: [{ uri, mimeType: 「application/json」, text: config }]
    };  });

7.3 応用の方向性

方向性 説明
データベースツール pg / mysql2 を使用したクエリツール
API プロキシ 内部API呼び出しをカプセル化し、AIがプライベートサービスにアクセスできるようにする
ログ分析 アプリケーションログを読み取り・分析し、診断レポートを生成する
複数ツールの組み合わせ 1つのサーバーに複数のツールを登録し、包括的な機能を提供する

ナレッジツリー

MCP Serverのフルスタック実践(手書き)├── 動作原理│ ├── 完全な通信経路(Host → stdin → Server → stdout → Host)│ └── Serverの3大役割(ツールの宣言 / ロジックの実行 / プロトコル通信)├── プロジェクトのセットアップ│ ├── package.json(ESM + SDK + Zod)│ └── npm install @modelcontextprotocol/sdk zod├── コアコード(40行)│ ├── McpServer のインスタンス化│ ├── server.tool() によるツールの登録│ │ ├── ツール名 + 説明│ │ ├── Zodスキーマによるパラメータ検証│ │ └── 非同期実行関数 + 標準の戻り値形式│ ├── StdioServerTransportの起動│ └── console.error(console.logは使用しない)├── Zodによるパラメータ検証│ ├── string / number / boolean / enum│ ├── object / array│ └── Zod 対 手書きの JSON Schema├── Claude Code の連携│ ├── .mcp.json 設定(command: node)│ └── 自然言語呼び出しツール├── セキュリティ境界│ └── パスホワイトリスト(境界越えアクセスの防止)└── さらなる発展    ├── 複数ツールの登録    ├── リソース     ├── データベース / API ツール    └── ログ分析

まとめ

MCP Server を手書きで作成するには、たった 40 行のコードで十分です----- -これは、@modelcontextprotocol/sdkによる高レベルなカプセル化と、zodの簡潔な構文のおかげです。

MCP Serverの本質を理解すれば、そのアーキテクチャが非常に洗練されていることがわかるでしょう:

  1. McpServerはプロトコル通信を担当します(JSON-RPCの詳細を知る必要はありません)
  2. server.tool() はツールの登録を担当します(名前 + 説明 + スキーマ + 実行関数)
  3. StdioServerTransport は転送(stdin/stdout)を担当します
  4. zod はセキュリティ(実行時のパラメータ検証)を担当します

公式のサーバーの利用から、独自のサーバーを手作りするまで、あなたは「消費者」から「創造者」へと飛躍を遂げました。これで、データベースの読み取り、内部APIの呼び出し、ログの分析、さらにはデバイスの遠隔制御に至るまで、AIのためにあらゆる機能を構築できるようになりました。

MCPは、すべての開発者をAI機能の構築者へと変えます。わずか40行のコードで、無限の可能性の世界が開かれます。


参考資料および関連文献:

  • @modelcontextprotocol/sdk 公式ドキュメント
  • zod v4 公式ドキュメント
  • MCP プロトコル仕様 (Model Context Protocol Specification)
  • 『MCPプロトコルの徹底解説』------ AI界のUSB-C実践ガイド
  • 『Harness Engineeringの徹底解説』------ MCPはHarnessツール層の中核

この記事が、MCP Serverを手動で実装する全プロセスの理解に役立った場合は、ぜひ「いいね」と「ブックマーク」をお願いします。MCP Serverの機能について拡張したい点があれば、コメント欄でぜひ議論しましょう ?

#MCP #Node.js #Zod #ClaudeCode #AIエンジニアリング #掘金技術コミュニティ