CodeCraft Lab

MCPツールを開発する

カスタムMCPツールを自作する流れを、スキーマ設計・実装・エラーハンドリング・テストまで一気通貫で学びます

応用16分で読了
MCPツール開発スキーマ設計テスト

このレッスンで学ぶこと

  • カスタムMCPツールを自作する全体の流れ(設計 → 実装 → テスト → 接続確認)
  • ツール定義のスキーマ設計(入力パラメータ・出力形式の決め方)
  • ツール実行時のエラーハンドリングの設計方針
  • 簡易計算ツールを題材にした、実際に動くサンプル実装
  • ローカルでのテスト・デバッグの進め方

前提条件

  • Lesson 1・2を修了し、既存のMCPサーバーを接続・構築できること
  • Node.js 18以降がインストールされていること
  • JavaScript/TypeScriptの基本的な読み書きができること

ここまでのレッスンでは、既存のMCPサーバーを「接続して使う」側でした。このレッスンでは、自分たちのドメインに合わせたMCPサーバーを自分で作る側に回ります。実装の全工程をゼロから手書きするのではなく、Claude Code自身にスキャフォルドさせながら、設計判断のポイントを押さえていくアプローチで進めます。

自作MCPツール開発の全体の流れ

flowchart LR
    A["1. ユースケースを定義する"]
    B["2. ツールの粒度・入出力を設計する"]
    C["3. 実装する<br/>(スキャフォルド + 手動実装)"]
    D["4. ローカルでテストする"]
    E["5. Claude Codeに接続して確認する"]
 
    A --> B --> C --> D --> E
 
    style A fill:#e8f4fd,stroke:#1a73e8
    style E fill:#fff3e0,stroke:#e65100

この流れのうち、特に時間をかける価値があるのは**ステップ2(設計)**です。実装自体はSDKが面倒を見てくれる部分が多い一方、「どんな粒度のツールにするか」「何を入力させ、何を返すか」の設計判断は人間が行う必要があり、ここの質がツールの使いやすさを左右します。

ゼロから書く前にスキャフォルドを試す

Claude Codeには、公式のmcp-server-devプラグインを使ってMCPサーバーの雛形を生成させる方法があります。/plugin install mcp-server-dev@claude-plugins-officialでインストールし、/mcp-server-dev:build-mcp-serverを実行すると、ユースケースをヒアリングした上でリモートHTTPまたはローカルstdioサーバーの基本構造を生成してくれます。本レッスンでは仕組みの理解を優先するため手動実装で進めますが、実務ではこのスキャフォルドから始めて手を入れていくのが効率的です。

ステップ1〜2: ユースケースとツールの粒度を決める

題材として、社内の見積もり業務でよく使う簡易計算ツール(消費税込み価格の計算、割引後価格の計算)を持つMCPサーバーを作ります。実務での自作MCPサーバーも、突き詰めれば「複数の関連する操作をひとまとまりのサーバーとして提供する」という構造は同じです。

ツールの粒度を決める際は、「人間が依頼する単位」を意識します。

# 良くない例: 内部実装をそのまま公開してしまっている
calculate_multiply(a, b)
calculate_add(a, b)
 
# 良い例: 業務上の意味のあるまとまりで公開している
calculate_price_with_tax(price, taxRate)
calculate_discounted_price(price, discountPercent)

前者は「電卓の関数」をそのまま並べただけで、Claude Codeにとって「いつ使うべきか」が伝わりにくくなります。後者は業務的な意味が名前から読み取れるため、説明文を多少省いてもAIが正しく選択しやすくなります。

ステップ3: スキーマ設計と実装

入力パラメータのスキーマ設計

MCPツールの入力は、JSON Schemaで型・必須項目・説明を定義します。設計時に意識すべき点は3つです。

観点指針
必須項目を絞るデフォルト値で代替できるものはoptionalにし、呼び出し側(AI)の負担を減らす
説明文を具体的に書くpriceよりprice: 税抜価格(円、整数)の方がAIが誤った値を渡しにくい
型を厳密にする数値であるべき箇所に文字列を許容しない。誤った呼び出しを早期に弾く

実装: 簡易計算MCPサーバー

@modelcontextprotocol/sdkを使った最小実装です。npm install @modelcontextprotocol/sdk zodで依存関係を準備します。

// server.ts
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: "pricing-tools",
  version: "1.0.0",
});
 
// ツール1: 税込価格を計算する
server.registerTool(
  "calculate_price_with_tax",
  {
    title: "税込価格を計算する",
    description:
      "税抜価格と税率から税込価格を計算します。小数点以下は切り捨てます。",
    inputSchema: {
      price: z.number().int().positive().describe("税抜価格(円、整数)"),
      taxRate: z
        .number()
        .min(0)
        .max(1)
        .default(0.1)
        .describe("税率(例: 10%なら0.1)"),
    },
  },
  async ({ price, taxRate }) => {
    // 入力値の妥当性は schema である程度担保されるが、
    // ビジネスロジック上の異常値(極端な税率など)はここで追加チェックする
    if (taxRate > 0.5) {
      return {
        content: [
          {
            type: "text",
            text: `エラー: 税率が異常です(${taxRate})。0〜0.5の範囲で指定してください。`,
          },
        ],
        isError: true,
      };
    }
 
    const taxedPrice = Math.floor(price * (1 + taxRate));
    return {
      content: [
        {
          type: "text",
          text: `税込価格: ${taxedPrice}円(税抜${price}円、税率${taxRate * 100}%)`,
        },
      ],
    };
  }
);
 
// ツール2: 割引後価格を計算する
server.registerTool(
  "calculate_discounted_price",
  {
    title: "割引後価格を計算する",
    description: "価格と割引率から割引後の価格を計算します。",
    inputSchema: {
      price: z.number().int().positive().describe("割引前価格(円、整数)"),
      discountPercent: z
        .number()
        .min(0)
        .max(100)
        .describe("割引率(%)。例: 20なら20%オフ"),
    },
  },
  async ({ price, discountPercent }) => {
    const discounted = Math.floor(price * (1 - discountPercent / 100));
    return {
      content: [
        {
          type: "text",
          text: `割引後価格: ${discounted}円(元価格${price}円、${discountPercent}%オフ)`,
        },
      ],
    };
  }
);
 
const transport = new StdioServerTransport();
await server.connect(transport);
zodでスキーマと検証を一体化する

TypeScript SDKではzodを使うことで、入力スキーマの定義とランタイムバリデーションを1か所にまとめられます。z.number().min(0).max(1)のように制約を書いておけば、不正な値はSDK側で弾かれ、ハンドラー内では「スキーマを通過した値」だけを安心して扱えます。

エラーハンドリングの設計

MCPツールのエラーには性質の異なる2種類があり、扱い方を分けるのが重要です。

種類原因の例適切な対応
入力エラー不正な型、範囲外の値スキーマレベルで弾く(zod等)。ハンドラーに到達させない
実行時エラー外部API呼び出しの失敗、ビジネスロジック上の異常値isError: trueを返し、Claude Codeに何が起きたかを伝える

実行時エラーを握りつぶしてundefinedを返したり、例外を投げっぱなしにしたりすると、Claude Codeは「なぜ失敗したか」を判断できず、ユーザーへの説明も曖昧になります。エラーも構造化されたレスポンスとして返すのが鉄則です。

async ({ price, taxRate }) => {
  try {
    // 外部APIを呼ぶ処理などをここに書く
    const result = await fetchExternalRate();
    return {
      content: [{ type: "text", text: `結果: ${result}` }],
    };
  } catch (error) {
    // 例外を握りつぶさず、isErrorで明示的に失敗を伝える
    return {
      content: [
        {
          type: "text",
          text: `外部レート取得に失敗しました: ${
            error instanceof Error ? error.message : String(error)
          }`,
        },
      ],
      isError: true,
    };
  }
};
想定外のエラーも構造化して返す

CLAUDE.mdの方針にもあるとおり、想定外のエラーを握りつぶさないことが重要です。MCPツールの場合も、catchしたエラーをそのまま無視せず、原因が伝わるメッセージとともにisError: trueで返すことで、Claude Codeが次の判断(リトライする、ユーザーに尋ねる等)をしやすくなります。

ステップ4: テスト・デバッグの方法

MCP Inspectorでの単体確認

実装したサーバーをClaude Codeに繋ぐ前に、公式のMCP Inspectorでツール単体の動作を確認すると、問題の切り分けが速くなります。

npx @modelcontextprotocol/inspector node server.ts

ブラウザでInspectorのUIが開き、登録したツールの一覧、入力スキーマ、実際の呼び出し結果をGUIで確認できます。Claude Codeを介さずに「ツール自体が正しく動くか」を検証できるため、最初のデバッグはここで行うのが効率的です。

ユニットテストを書く

ツールのハンドラー関数はただの非同期関数なので、通常のテストフレームワークでテストできます。CLAUDE.mdの方針どおり、新規実装には必ずテストを書きます。

// pricing.test.ts
import { describe, it, expect } from "vitest";
import { calculatePriceWithTax } from "./pricing";
 
describe("calculatePriceWithTax", () => {
  it("税率10%のとき、税抜1000円が税込1100円になる", () => {
    expect(calculatePriceWithTax(1000, 0.1)).toBe(1100);
  });
 
  it("小数点以下が出る場合、切り捨てられる", () => {
    expect(calculatePriceWithTax(999, 0.1)).toBe(1098); // 1098.9 → 1098
  });
 
  it("税率が0.5を超える異常値の場合、エラーになる", () => {
    expect(() => calculatePriceWithTax(1000, 0.8)).toThrow();
  });
});

Claude Codeに接続して統合確認する

ローカルサーバーとして登録する

claude mcp add pricing-tools -- node /path/to/server.ts

接続状態を確認する

claude mcp list

✓ Connectedになっていること、/mcpでツールが2つ表示されることを確認します。

自然言語でツールを呼び出してみる

税抜3000円の商品に消費税10%をかけた価格を計算してください

Claude Codeが自動的にcalculate_price_with_taxを選択して呼び出すことを確認します。意図したツールが選ばれない場合は、ツールの説明文(description)を見直しましょう。

異常系も試す

税抜1000円に税率80%をかけた場合の価格を教えてください

エラーメッセージが適切にユーザーへ伝わるか確認します。ここで「内部エラーが発生しました」のような曖昧な表示になっている場合、ハンドラー側のエラーメッセージ設計を見直す必要があります。

デバッグ時はサーバーログを別ターミナルで確認する

stdioサーバーは標準入出力をプロトコル通信に使っているため、console.logでのデバッグ出力はプロトコルを破壊してしまいます。デバッグ出力はconsole.error(標準エラー出力)に書くか、ファイルへのログ出力に切り替えましょう。

よくある質問

Q. ツールの数が増えてきたら、サーバーを分割すべきですか?

明確な答えはありませんが、目安として「関連性の薄い操作が同じサーバーに混在し始めたら分割を検討する」とよいでしょう。価格計算と在庫管理のように業務ドメインが異なる機能は、別々のMCPサーバーに分けた方が説明文の精度も上がり、保守もしやすくなります。

Q. PythonでもMCPサーバーは作れますか?

作れます。公式のmcp Python SDK(pip install mcp)でも同様のツール登録ができ、デコレータベースのAPIが提供されています。本レッスンではTypeScript SDKを例にしましたが、考え方(スキーマ設計、エラーハンドリングの方針)は言語によらず共通です。

まとめ

  • 自作MCPツールの開発は「ユースケース定義 → 設計 → 実装 → テスト → 接続確認」という流れで進める
  • ツールの粒度は「人間が依頼する単位」に合わせ、内部実装をそのまま公開しない
  • 入力スキーマはzod等で型・制約・説明文を明確にし、不正な値をハンドラーに到達させない
  • エラーは握りつぶさず、isError: trueと具体的なメッセージで構造化して返す
  • MCP Inspectorでツール単体を検証してから、Claude Codeに接続して統合確認する

次のステップ

MCP関連の3レッスンはここまでです。次のLesson 4では、MCPとは異なる仕組み——Hooksを扱います。Hooksは「ツール実行の前後で自動的に処理を差し込む」仕組みであり、今回作ったような自作ツールと組み合わせることで、より高度な自動化ワークフローを構築できるようになります。