1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCPサーバを自分で作ってMCPの仕組みを理解する

Posted at

MCPサーバを自分で作ってMCPの仕組みを理解する

はじめに

Claude DesktopなどのAIエージェントには「MCP(Model Context Protocol)」という仕組みがあり、外部サービスとの連携が可能です。しかし、その仕組みを理解することは難しく、私も当初は設定しても正常に動作せず苦労しました。その時勉強したことを以下の記事として作成していました。

Claudeと会話する中で、ふとしたことからMCPを自分で作ってみることとなり、それでMCPの仕組みの理解が進みました。
本記事では、MCPサーバーを自作することで、MCPの仕組みを理解することを目指します。
詳しいことは公式サイトを読み解けばいいのですが、最初の入口としてお役に立てればと思います。

この記事で作るもの

  1. Hello World MCP - 最小構成のMCPサーバーで基本的な仕組みを理解する
  2. 為替換算 MCP - 外部APIと連携するMCPサーバーの例
  3. SQL Server MCP - データベースと連携するMCPサーバーの例

対象読者

  • ClaudeでMCPを使ったことがある方
  • MCPの基本的な仕組みを理解したい方
  • 自分でMCPサーバーを作ってみたい方

本記事の前提環境

  • Node.js v18以上
  • TypeScript
  • Claude Desktop

言語は何でもいいようですが、基本的にはAnthropicとコミュニティがSDKを提供しているTypeScript、JavaScript、Pythonを使うことが多いと思われます。


1. MCPサーバーの基本構造

1.1 MCPとは

MCP(Model Context Protocol)は、AIエージェント(Claude等)が外部サービスにアクセスするための標準プロトコルです。

MCPの役割:AI単体ではできない「外部API呼び出し」「DB接続」などの外部サービスの使用を、MCPサーバーが橋渡しします。

1.2 最小構成のプロジェクト「Hello World」でMCPの仕組みを理解する

外部サービスを使用せず、とりあえず"Hello World"を返すだけのMCPサーバー作成し、MCPの仕組みを理解します。
MCPサーバーを作成するには、以下のファイルが最低限必要です。TypeScriptの使用方法を理解する必要がありますが、本記事での詳細な説明は省略します。

mcp-hello/
├── package.json      # プロジェクト設定・依存関係
├── tsconfig.json     # TypeScript設定
└── src/
    └── index.ts      # MCPサーバー本体

1.3 package.json

{
  "name": "mcp-hello",
  "version": "1.0.0",
  "description": "Hello World MCP Server",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "zod": "^3.23.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0"
  }
}

ポイント
MCPというよりはTypeScriptの設定ですが、以下のSDK、ライブラリを使っているのがポイントです。

  • @modelcontextprotocol/sdk - MCP公式SDK
  • zod - 入力パラメータのバリデーションライブラリ

1.4 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

これもTypeScriptでMCPサーバーを作るための設定で、MCP自体とは直接関係ありません。

1.5 MCPサーバーの雛形(src/index.ts)

以下がMCPの動作や構成を決める大事な本体です。

#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// ========================================
// 1. サーバーの作成
// ========================================
const server = new McpServer({
  name: "mcp-hello",    // サーバー名(識別用)
  version: "1.0.0",     // バージョン
});

// ========================================
// 2. ツールの登録
// ========================================
server.registerTool(
  "greet",                                           // ツール名
  {
    description: "名前を受け取って挨拶を返します",      // 説明
    inputSchema: {                                   // 入力パラメータ定義
      name: z.string().describe("挨拶する相手の名前"),
    },
  },

  async ({ name }) => {                              // 実際の処理
    // 現在時刻に応じた挨拶を生成
    const hour = new Date().getHours();
    let greeting: string;

    if (hour >= 5 && hour < 12) {
      greeting = "おはようございます";
    } else if (hour >= 12 && hour < 18) {
      greeting = "こんにちは";
    } else {
      greeting = "こんばんは";
    }
  }
);

// ========================================
// 3. サーバーの起動
// ========================================
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("mcp-hello server started!");
}

main().catch(console.error);

1.6 各要素の説明

McpServer

const server = new McpServer({
  name: "mcp-hello",
  version: "1.0.0",
});

MCPサーバーの本体です。このインスタンスにツールを登録していきます。

registerTool

server.registerTool(
  "ツール名",           // Claudeが呼び出す際の識別子
  {
    description: "...", // Claudeがツールを選ぶ判断材料
    inputSchema: {...}, // 入力パラメータの定義(Zod形式)
  },
  async (引数) => {     // 実際の処理
    return {
      content: [{ type: "text", text: "結果" }],
    };
  }
);

AIが使用するツールの定義です。
descriptionは重要です。AIはこの説明を読んで、どのツールを使うべきか判断します。
必要に応じて複数定義できます。
複数登録する場合はserver.registerToolブロックを繰り返し記述します。

Zod によるスキーマ定義


inputSchema: {
  name: z.string().describe("挨拶する相手の名前"),
  age: z.number().describe("年齢"),
  isActive: z.boolean().describe("有効フラグ"),
}

Zodは入力パラメータの説明を定義するライブラリです。不正な入力を自動的にブロックしてくれます。

StdioServerTransport

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("mcp-hello server started!");

AIとの通信方法を定義します。StdioServerTransportは標準入出力(stdin/stdout)でJSON-RPCメッセージをやり取りします。
最後の「Console.error」ですが、公式サイトの説明ではconsole.logなどで標準出力(stdout)には決して書き込まないようにとのことです。JSON-RPCメッセージを破損させMCPサーバーを動作不能にするそうです。
MCPサーバーにおけるログ記録


2. MCPが動作する仕組み

2.1 MCPサーバーのプログラムをコンパイル

コマンドプロンプト(Windowsの場合)で一連のプログラムが置いているルートフォルダ(mcp-hello)に移動して、「npm run build」を実行します。
コンパイルされたMCPサーバーの実行ファイルが作成されます。

2.2 claude_desktop_config.json

Claude Desktopでの例ですが、MCPサーバーの登録は、以下の設定ファイルで行います。

Windows: %APPDATA%\Claude\claude_desktop_config.json
Mac: ~/Library/Application Support/Claude/claude_desktop_config.json

{
  "mcpServers": {
    "mcp-hello": {
      "command": "node",
      "args": ["C:\\path\\to\\mcp-hello\\dist\\index.js"]
    }
  }
}

ポイント

  • command - 実行するコマンド
  • args - コマンドライン引数
  • env - 環境変数(DB接続情報など)

詳しい設定方法は以前別の記事に書きました。

2.1 起動タイミング

Claude Desktopを例にすると、MCPサーバーは、Claude Desktop起動時に自動的に起動されます。

┌──────────────────────────────────────────┐
│ Claude Desktop 起動時の処理               │
│                                          │
│ 1. claude_desktop_config.json を読み込む  │
│                                          │
│ 2. mcpServers に定義された各サーバーを起動  │
│                                          │
│    "mcp-hello" → node dist/index.js      │
│    "別のMCP"   → 別の node dist/index.js  │
│    ・・・                                 │
│                                          │
│ 3. 標準入出力で接続を確立                  │
│ 4. 利用可能なツール一覧を取得              │
└──────────────────────────────────────────┘

2.3 AIエージェントとの連携フロー

ユーザーが「山田さんにあいさつして」と入力した場合:

┌─────────────────────────────────────────────────────────────────┐
│ 1. ユーザー入力                                                  │
│    「山田さんにあいさつして」                                      │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. Claude が判断                                                 |
│    利用可能なツール一覧から description を読んで判断               │
│    「このタスクには greet ツールが適切だ」                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. MCPサーバーにリクエスト送信                                    │
│    {                                                            │
│      "tool": "greet",                                           │
│      "arguments": { "name": "山田" }                            │
│    }                                                            │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. MCPサーバーが処理                                             │
│    - 結果をテキストに整形                                         │
│    - Claudeに返却                                                │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. Claude がユーザーに回答                                       │
│    「こんにちは、山田さん!MCPサーバーからの挨拶です。」            │
└─────────────────────────────────────────────────────────────────┘

いかがでしょうか。
ファイルの構成や決まり文句的に記述方法を守れば、あとは自由に作成することができます。
以下は少し実践的な例を挙げます。

3. 例1:外部API連携:為替換算MCPの作成

3.1 概要

外部の為替レートAPIを呼び出して、通貨換算を行うMCPサーバーを作成します。

MCPサーバー → HTTP Request → exchangerate-api.com → JSON Response
 → JSON Response → レートを取得して計算 → 結果をテキストで返す

3.2 使用するAPI

exchangerate-api.com の無料APIを使用します。

使用イメージ:
GET https://api.exchangerate-api.com/v4/latest/USD

Response:
{
  "rates": {
    "JPY": 157.50,
    "EUR": 0.92,
    ...
  }
}

現時点で、最新バージョンではアカウント登録が必須となっていますが、v4がまだ存在していてアカウントも不要でした。当記事でこだわるところではないのでv4を使用しています。

3.3 実装コード

#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "mcp-exchangerate",
  version: "1.0.0",
});

server.registerTool(
  "convert_currency",
  {
    description: "為替レートを取得して通貨を換算します。例: USD→JPY",
    inputSchema: {
      from: z.string().describe("換算元の通貨コード(例: USD, EUR, JPY)"),
      to: z.string().describe("換算先の通貨コード(例: USD, EUR, JPY)"),
      amount: z.number().describe("換算する金額"),
    },
  },
  async ({ from, to, amount }) => {
    try {
      // ========================================
      // 外部APIを呼び出し(これがMCPの真価)
      // ========================================
      const apiUrl = `https://api.exchangerate-api.com/v4/latest/${from.toUpperCase()}`;
      const response = await fetch(apiUrl);

      if (!response.ok) {
        throw new Error(`APIエラー: ${response.status}`);
      }

      const data = await response.json();
      const rate = data.rates[to.toUpperCase()];

      if (!rate) {
        throw new Error(`通貨コード "${to}" が見つかりません`);
      }

      // ========================================
      // 結果をテキストに整形して返す
      // ========================================
      const converted = amount * rate;

      return {
        content: [
          {
            type: "text" as const,
            text: `${amount.toLocaleString()} ${from.toUpperCase()} = ${converted.toLocaleString(undefined, { maximumFractionDigits: 2 })} ${to.toUpperCase()}(レート: ${rate})`,
          },
        ],
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : "不明なエラー";
      return {
        content: [{ type: "text" as const, text: `エラー: ${errorMessage}` }],
        isError: true,
      };
    }
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("mcp-exchangerate server started!");
}

main().catch(console.error);

3.4 ポイント解説

外部API呼び出し

const response = await fetch(apiUrl);
const data = await response.json();

Node.js 18以降では fetch() がネイティブで使えます。これでHTTPリクエストを送信し、JSONレスポンスを取得します。

エラーハンドリング

try {
  // 処理
} catch (error) {
  return {
    content: [{ type: "text", text: `エラー: ${errorMessage}` }],
    isError: true,  // エラーフラグ
  };
}

isError: true を設定すると、Claudeはエラーとして認識し、適切に対応します。

3.5 動作確認

Claude Desktopで以下のように話しかけます:

  • 「500ユーロは何ドル?」
  • 「100ドルを日本円に換算して」

ユーザーが「100ドルを円に換算して」と入力した場合:

┌─────────────────────────────────────────────────────────────────┐
│ 1. ユーザー入力                                                  │
│    「100ドルを円に換算して」                                      │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. Claude が判断                                                 │
│    「このタスクには convert_currency ツールが適切だ」              │
│                                                                 │
│    利用可能なツール一覧から description を読んで判断               │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. MCPサーバーにリクエスト送信                                    │
│    {                                                            │
│      "tool": "convert_currency",                                │
│      "arguments": { "from": "USD", "to": "JPY", "amount": 100 } │
│    }                                                            │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. MCPサーバーが処理                                             │
│    - 外部APIを呼び出し                                           │
│    - 結果をテキストに整形                                         │
│    - Claudeに返却                                                │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. Claude がユーザーに回答                                       │
│    「100ドルは約15,750円です(レート: 157.50)」                  │
└─────────────────────────────────────────────────────────────────┘

4. 例2:データベース連携:SQL Server MCPの作成

4.1 概要

SQL Serverに接続してクエリを実行するMCPサーバーを作成します。
AIにテーブル構造やサンプルデータを理解させるためにコピペする手間が省けます。

MCPサーバー → mssqlライブラリ → SQL Server → 結果セット
 → 結果をテキストテーブル形式に整形 → Claudeに返す

SQL ServerがAPIなどを用意していないので、DBクライアントライブラリを使って直接接続します。
SQLを投げて帰ってきた結果を整形して返すだけです。

4.2 追加の依存関係

npm install mssql
npm install --save-dev @types/mssql

TypeScriptでSQL Serverを扱うためのライブラリと型定義をインストールします。

4.3 実装コード

例ではSELECT文結果、テーブル一覧、テーブル構造取得の3つのツールを実装しています。

#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import sql from "mssql";

// ========================================
// DB接続設定(環境変数から取得)
// ========================================
const dbConfig: sql.config = {
  server: process.env.MSSQL_HOST || "localhost",
  database: process.env.MSSQL_DATABASE || "master",
  user: process.env.MSSQL_USER || "",
  password: process.env.MSSQL_PASSWORD || "",
  options: {
    encrypt: false,
    trustServerCertificate: true,
  },
};

const server = new McpServer({
  name: "mcp-sql",
  version: "1.0.0",
});

// ========================================
// ツール1: execute_query - SQLクエリを実行
// ========================================
server.registerTool(
  "execute_query",
  {
    description: "SQL Serverにクエリを実行して結果を取得します。SELECT文のみ許可。",
    inputSchema: {
      query: z.string().describe("実行するSQLクエリ(SELECT文のみ)"),
    },
  },
  async ({ query }) => {
    // 安全性チェック: SELECT文のみ許可
    const trimmedQuery = query.trim().toUpperCase();
    if (!trimmedQuery.startsWith("SELECT")) {
      return {
        content: [
          {
            type: "text" as const,
            text: "エラー: SELECT文のみ実行可能です。",
          },
        ],
        isError: true,
      };
    }

    try {
      // DB接続
      const pool = await sql.connect(dbConfig);

      // クエリ実行
      const result = await pool.request().query(query);
      const rows = result.recordset;

      await pool.close();

      if (rows.length === 0) {
        return {
          content: [{ type: "text" as const, text: "結果は0件でした。" }],
        };
      }

      // ========================================
      // 結果をテキストテーブル形式に整形
      // ========================================
      const columns = Object.keys(rows[0]);

      let output = "";
      output += columns.join("\t| ") + "\n";
      output += columns.map(() => "---").join("\t| ") + "\n";

      for (const row of rows) {
        const values = columns.map((col) => {
          const val = row[col];
          if (val === null) return "(NULL)";
          if (val instanceof Date) return val.toISOString();
          return String(val);
        });
        output += values.join("\t| ") + "\n";
      }

      output += `\n(${rows.length}件の結果)`;

      return {
        content: [{ type: "text" as const, text: output }],
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : "不明なエラー";
      return {
        content: [{ type: "text" as const, text: `SQLエラー: ${errorMessage}` }],
        isError: true,
      };
    }
  }
);

// ========================================
// ツール2: list_tables - テーブル一覧を取得
// ========================================
server.registerTool(
  "list_tables",
  {
    description: "データベース内のテーブル一覧を取得します",
    inputSchema: {},
  },
  async () => {
    try {
      const pool = await sql.connect(dbConfig);

      const result = await pool.request().query(`
        SELECT TABLE_SCHEMA, TABLE_NAME 
        FROM INFORMATION_SCHEMA.TABLES 
        WHERE TABLE_TYPE = 'BASE TABLE'
        ORDER BY TABLE_SCHEMA, TABLE_NAME
      `);

      await pool.close();

      const rows = result.recordset;
      let output = "テーブル一覧:\n\n";
      for (const row of rows) {
        output += `- ${row.TABLE_SCHEMA}.${row.TABLE_NAME}\n`;
      }
      output += `\n(${rows.length}テーブル)`;

      return {
        content: [{ type: "text" as const, text: output }],
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : "不明なエラー";
      return {
        content: [{ type: "text" as const, text: `エラー: ${errorMessage}` }],
        isError: true,
      };
    }
  }
);

// ========================================
// ツール3: describe_table - テーブル構造を取得
// ========================================
server.registerTool(
  "describe_table",
  {
    description: "指定したテーブルのカラム情報を取得します",
    inputSchema: {
      tableName: z.string().describe("テーブル名(例: dbo.Users)"),
    },
  },
  async ({ tableName }) => {
    try {
      const pool = await sql.connect(dbConfig);

      const parts = tableName.split(".");
      const schema = parts.length > 1 ? parts[0] : "dbo";
      const table = parts.length > 1 ? parts[1] : parts[0];

      const result = await pool
        .request()
        .input("schema", sql.VarChar, schema)
        .input("table", sql.VarChar, table)
        .query(`
          SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
          FROM INFORMATION_SCHEMA.COLUMNS
          WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table
          ORDER BY ORDINAL_POSITION
        `);

      await pool.close();

      const rows = result.recordset;
      let output = `テーブル: ${schema}.${table}\n\n`;
      output += "カラム名\t| 型\t| NULL許可\n";
      output += "---\t| ---\t| ---\n";

      for (const row of rows) {
        output += `${row.COLUMN_NAME}\t| ${row.DATA_TYPE}\t| ${row.IS_NULLABLE}\n`;
      }

      return {
        content: [{ type: "text" as const, text: output }],
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : "不明なエラー";
      return {
        content: [{ type: "text" as const, text: `エラー: ${errorMessage}` }],
        isError: true,
      };
    }
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("mcp-sql server started!");
}

main().catch(console.error);

4.4 ポイント解説

環境変数による設定

const dbConfig: sql.config = {
  server: process.env.MSSQL_HOST || "localhost",
  database: process.env.MSSQL_DATABASE || "master",
  // ...
};

接続情報はコードにハードコードせず、環境変数から取得します。claude_desktop_config.jsonenv セクションで設定します。

安全性チェック

if (!trimmedQuery.startsWith("SELECT")) {
  return { content: [...], isError: true };
}

SELECT文のみ許可することで、意図しないデータ変更を防ぎます。

パラメータ化クエリ

const result = await pool
  .request()
  .input("schema", sql.VarChar, schema)
  .input("table", sql.VarChar, table)
  .query(`... WHERE TABLE_SCHEMA = @schema ...`);

SQLインジェクション対策として、パラメータ化クエリを使用します。

4.5 Claude Desktop への登録

{
  "mcpServers": {
    "mcp-sql": {
      "command": "node",
      "args": ["C:\\path\\to\\mcp-sql\\dist\\index.js"],
      "env": {
        "MSSQL_HOST": "your_server_instance",
        "MSSQL_USER": "your_user",
        "MSSQL_PASSWORD": "your_password",
        "MSSQL_DATABASE": "Your_Database"
      }
    }
  }
}

4.6 動作確認

Claude Desktopで以下のように話しかけます:

  • 「mcp-sqlでテーブル一覧を見せて」
  • 「Usersテーブルの構造を教えて」
  • 「SELECT TOP 10 * FROM Users を実行して」

5. まとめ

5.1 MCPサーバーの共通パターン

// 1. サーバー作成
const server = new McpServer({ name: "xxx", version: "1.0.0" });

// 2. ツール登録(複数可)
server.registerTool("ツール名", { description, inputSchema }, async (引数) => {
  // ここで外部リソースにアクセス
  // 結果をテキストに整形
  return { content: [{ type: "text", text: "結果" }] };
});

// 3. 起動
const transport = new StdioServerTransport();
await server.connect(transport);

5.2 MCPの本質

AIができないこと MCPで橋渡し 外部リソース
HTTPリクエスト fetch() 外部API
DB接続 mssqlライブラリ SQL Server
ファイル操作 fs モジュール ファイルシステム
などなど・・・

結果はすべて「テキスト」に整形してClaudeに返すことが重要です。

5.3 最後に

MCPサーバーの作り方を理解したら、社内システムとの連携や定型レポートの自動生成みたいな応用も可能になるでしょう。
当記事が何かのお役に立てれば幸いです。


参考リンク

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?