レシピ#6-8: MCP(Model Context Protocol)でClaudeを拡張する

レシピ#6-6で個人用アプリを作り、レシピ#6-7でコラボレーションを通じて進化させました。しかし、アプリが成長するにつれて、外部データや専門的なツールとの連携が必要になってきます。

MCP(Model Context Protocol)は、Claudeを外部システムと安全に連携させるための標準プロトコルです。

このレシピでは、MCPの基礎から、既存MCPサーバーの活用、そして自分専用のMCPサーバーの作成方法まで学びます。


MCPとは何か

従来の課題

【アプリ開発での課題】

ToDoアプリを作った
  ↓
「Googleカレンダーと連携したい」
  ↓
でも...
❌ APIキーの管理が複雑
❌ 毎回認証コードを書く必要
❌ エラーハンドリングが大変
❌ セキュリティリスク

MCPによる解決

【MCPを使うと】

MCP Server(カレンダー連携)
  ↓
Claude が MCP を通じて安全にアクセス
  ↓
✅ 認証・セキュリティは MCP Server が管理
✅ 一度設定すれば使い続けられる
✅ エラーハンドリングも MCP Server が対応
✅ あなたは「カレンダーに予定を追加して」と話すだけ

MCPの3つの重要な概念

1. MCP Server

外部システムとの橋渡し役

【役割】
- 外部APIへの接続
- 認証・セキュリティ管理
- データの取得・操作
- エラーハンドリング

【例】
- Google Calendar MCP: カレンダー操作
- GitHub MCP: リポジトリ・Issue管理
- Database MCP: データベースアクセス
- File System MCP: ローカルファイル操作

2. MCP Client(Claude)

MCPサーバーを利用する側

Claude が:
- MCP Server に接続
- 必要なデータを要求
- 返ってきたデータを理解
- ユーザーに分かりやすく提示

3. Tools & Resources

MCPサーバーが提供する機能

【Tools(ツール)】
実行可能な操作
- add_calendar_event(予定追加)
- search_files(ファイル検索)
- query_database(DB検索)

【Resources(リソース)】
アクセス可能なデータ
- カレンダーの予定一覧
- ファイルの内容
- データベースのレコード

なぜBYOA開発でMCPが重要か

【レシピ#6-6】個人用アプリ
- データはLocalStorageのみ
- 機能は限定的

【レシピ#6-7】特化型機能の追加
- データを活用
- 独自の価値創出

【レシピ#6-8】MCPによる拡張
✅ 外部サービスと連携
✅ 専門的なデータにアクセス
✅ 既存ツールを活用
✅ セキュアな統合

【例】
- 育児アプリ: 地域の保育園情報API連携
- 栽培アプリ: 気象データAPI連携
- 家計簿: 銀行API連携(将来)
- 読書記録: 書籍情報API連携

既存のMCPサーバーを使う

人気のMCPサーバー

1. Filesystem MCP

ローカルファイルへの安全なアクセス

できること:
- ファイルの読み書き
- ディレクトリ一覧取得
- ファイル検索
- テキスト置換

使用例:
「プロジェクト内の全てのTODOコメントを探して」
「settings.jsonを読んで、設定を教えて」

2. GitHub MCP

GitHubリポジトリとの統合

できること:
- Issue の作成・検索
- Pull Request の管理
- ファイル内容の取得
- コミット履歴の確認

使用例:
「このリポジトリの未解決Issueを一覧表示して」
「新しいPRを作成して、レビュワーにAさんを指定」

3. PostgreSQL MCP

データベースへのアクセス

できること:
- SQLクエリ実行
- テーブル構造確認
- データの検索・集計

使用例:
「先月の売上を集計して」
「ユーザー数が多い上位10都市は?」

4. Google Drive MCP

Google Driveファイルの操作

できること:
- ファイル・フォルダ検索
- ファイル内容の読み取り
- 新規ファイル作成
- 共有設定の管理

使用例:
「プロジェクト資料フォルダの最新ファイルを探して」
「この文書をPDFで保存して、チームと共有」

Claude Desktop/WebでのMCP設定

Claude Desktopでの設定

Step 1: 設定ファイルの場所

# macOS
~/Library/Application Support/Claude/claude_desktop_config.json

# Windows
%APPDATA%\Claude\claude_desktop_config.json

# Linux
~/.config/Claude/claude_desktop_config.json

Step 2: 設定ファイルの編集

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/username/Documents"
      ]
    },
    "github": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-github"
      ],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "your_github_token_here"
      }
    }
  }
}

Step 3: Claude Desktop を再起動

Claude Desktop を終了 → 再起動
→ MCPサーバーが自動的に起動
→ Claude のツールパネルに表示される

Claude WebでのMCP設定(将来対応予定)

現在、MCPは主にClaude Desktopで利用可能です。

Web版での対応は、Anthropicが段階的に展開中です。
最新情報は公式ドキュメントを確認してください:
https://modelcontextprotocol.io/

MCP サーバーの動作確認

Claude Desktop で確認:

【あなた】
利用可能なMCPツールを教えて

【Claude】
以下のMCPツールが利用可能です:

Filesystem:
- read_file: ファイルを読む
- write_file: ファイルに書き込む
- list_directory: ディレクトリ一覧
- search_files: ファイル検索

GitHub:
- create_issue: Issue作成
- search_issues: Issue検索
- get_file_contents: ファイル内容取得
- create_pull_request: PR作成

これらのツールを使って、何かお手伝いしましょうか?

カスタムMCPサーバーの作成

なぜカスタムMCPサーバーを作るのか

【既存MCPサーバーの限界】

既存のMCPサーバー:
- 汎用的な機能
- 特定のドメインに特化していない

カスタムMCPサーバー:
✅ 自分の領域に特化した機能
✅ 独自のデータソース
✅ 業務固有のロジック
✅ BYOA開発の真価

【例】
- 育児アプリ: 地域の保育園API → カスタムMCP
- 栽培アプリ: 気象データ + 栽培DB → カスタムMCP
- 家計簿: 複数銀行の統合 → カスタムMCP

最初のカスタムMCPサーバー(TypeScript)

例: タスク管理MCP Server

Step 1: プロジェクト作成

# 新規ディレクトリ作成
mkdir mcp-task-manager
cd mcp-task-manager

# npm初期化
npm init -y

# 必要なパッケージをインストール
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx

# TypeScript設定
npx tsc --init

Step 2: MCP Server 実装

// src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

// タスクの型定義
interface Task {
  id: string;
  title: string;
  completed: boolean;
  dueDate?: string;
  priority: 'high' | 'medium' | 'low';
}

// シンプルなインメモリストレージ
let tasks: Task[] = [];

// MCP Server作成
const server = new Server(
  {
    name: 'task-manager',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// 利用可能なツールのリスト
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'add_task',
        description: '新しいタスクを追加します',
        inputSchema: {
          type: 'object',
          properties: {
            title: {
              type: 'string',
              description: 'タスクのタイトル',
            },
            dueDate: {
              type: 'string',
              description: '期限(YYYY-MM-DD形式)',
            },
            priority: {
              type: 'string',
              enum: ['high', 'medium', 'low'],
              description: '優先度',
            },
          },
          required: ['title'],
        },
      },
      {
        name: 'list_tasks',
        description: 'タスク一覧を取得します',
        inputSchema: {
          type: 'object',
          properties: {
            filter: {
              type: 'string',
              enum: ['all', 'active', 'completed'],
              description: 'フィルター条件',
            },
          },
        },
      },
      {
        name: 'complete_task',
        description: 'タスクを完了にします',
        inputSchema: {
          type: 'object',
          properties: {
            taskId: {
              type: 'string',
              description: 'タスクID',
            },
          },
          required: ['taskId'],
        },
      },
      {
        name: 'delete_task',
        description: 'タスクを削除します',
        inputSchema: {
          type: 'object',
          properties: {
            taskId: {
              type: 'string',
              description: 'タスクID',
            },
          },
          required: ['taskId'],
        },
      },
    ],
  };
});

// ツール実行のハンドラ
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case 'add_task': {
      const task: Task = {
        id: Date.now().toString(),
        title: args.title as string,
        completed: false,
        dueDate: args.dueDate as string | undefined,
        priority: (args.priority as Task['priority']) || 'medium',
      };
      tasks.push(task);

      return {
        content: [
          {
            type: 'text',
            text: `タスクを追加しました: ${task.title} (ID: ${task.id})`,
          },
        ],
      };
    }

    case 'list_tasks': {
      const filter = (args.filter as string) || 'all';
      let filteredTasks = tasks;

      if (filter === 'active') {
        filteredTasks = tasks.filter((t) => !t.completed);
      } else if (filter === 'completed') {
        filteredTasks = tasks.filter((t) => t.completed);
      }

      const taskList = filteredTasks
        .map(
          (t) =>
            `[${t.completed ? 'x' : ' '}] ${t.title} (${t.priority}) ${
              t.dueDate ? `期限: ${t.dueDate}` : ''
            }`
        )
        .join('\n');

      return {
        content: [
          {
            type: 'text',
            text: taskList || 'タスクがありません',
          },
        ],
      };
    }

    case 'complete_task': {
      const taskId = args.taskId as string;
      const task = tasks.find((t) => t.id === taskId);

      if (!task) {
        throw new Error(`タスクが見つかりません: ${taskId}`);
      }

      task.completed = true;

      return {
        content: [
          {
            type: 'text',
            text: `タスクを完了しました: ${task.title}`,
          },
        ],
      };
    }

    case 'delete_task': {
      const taskId = args.taskId as string;
      const index = tasks.findIndex((t) => t.id === taskId);

      if (index === -1) {
        throw new Error(`タスクが見つかりません: ${taskId}`);
      }

      const deleted = tasks.splice(index, 1)[0];

      return {
        content: [
          {
            type: 'text',
            text: `タスクを削除しました: ${deleted.title}`,
          },
        ],
      };
    }

    default:
      throw new Error(`未知のツール: ${name}`);
  }
});

// サーバー起動
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('Task Manager MCP Server running on stdio');
}

main().catch((error) => {
  console.error('Server error:', error);
  process.exit(1);
});

Step 3: package.json 設定

{
  "name": "mcp-task-manager",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "mcp-task-manager": "./build/index.js"
  },
  "scripts": {
    "build": "tsc",
    "prepare": "npm run build"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.3.0",
    "tsx": "^4.7.0"
  }
}

Step 4: TypeScript設定(tsconfig.json)

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

Step 5: ビルド

npm run build

Step 6: Claude Desktop に登録

// claude_desktop_config.json
{
  "mcpServers": {
    "task-manager": {
      "command": "node",
      "args": [
        "/path/to/mcp-task-manager/build/index.js"
      ]
    }
  }
}

Step 7: Claude Desktop で使用

【あなた】
「レシピを書く」というタスクを追加して、優先度は高で

【Claude】
Task Manager MCPを使用してタスクを追加します...

タスクを追加しました: レシピを書く (ID: 1734876543210)

【あなた】
タスク一覧を表示して

【Claude】
[ ] レシピを書く (high)

PythonでのMCPサーバー実装

例: 気象データMCP Server

# weather_mcp_server.py
import asyncio
import json
from typing import Any
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import httpx

# 気象データAPI(Open-Meteo)を使用
WEATHER_API_BASE = "https://api.open-meteo.com/v1"

# MCP Server 作成
app = Server("weather-service")

@app.list_tools()
async def list_tools() -> list[Tool]:
    """利用可能なツールのリスト"""
    return [
        Tool(
            name="get_current_weather",
            description="指定した都市の現在の天気を取得します",
            inputSchema={
                "type": "object",
                "properties": {
                    "latitude": {
                        "type": "number",
                        "description": "緯度"
                    },
                    "longitude": {
                        "type": "number",
                        "description": "経度"
                    }
                },
                "required": ["latitude", "longitude"]
            }
        ),
        Tool(
            name="get_forecast",
            description="7日間の天気予報を取得します",
            inputSchema={
                "type": "object",
                "properties": {
                    "latitude": {
                        "type": "number",
                        "description": "緯度"
                    },
                    "longitude": {
                        "type": "number",
                        "description": "経度"
                    }
                },
                "required": ["latitude", "longitude"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
    """ツール実行"""

    if name == "get_current_weather":
        lat = arguments["latitude"]
        lon = arguments["longitude"]

        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{WEATHER_API_BASE}/forecast",
                params={
                    "latitude": lat,
                    "longitude": lon,
                    "current_weather": "true"
                }
            )
            data = response.json()

        weather = data["current_weather"]
        result = f"""
現在の天気:
気温: {weather['temperature']}°C
風速: {weather['windspeed']} km/h
天気コード: {weather['weathercode']}
        """.strip()

        return [TextContent(type="text", text=result)]

    elif name == "get_forecast":
        lat = arguments["latitude"]
        lon = arguments["longitude"]

        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{WEATHER_API_BASE}/forecast",
                params={
                    "latitude": lat,
                    "longitude": lon,
                    "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum",
                    "timezone": "Asia/Tokyo"
                }
            )
            data = response.json()

        daily = data["daily"]
        forecast_lines = []

        for i in range(7):
            date = daily["time"][i]
            temp_max = daily["temperature_2m_max"][i]
            temp_min = daily["temperature_2m_min"][i]
            precip = daily["precipitation_sum"][i]

            forecast_lines.append(
                f"{date}: 最高{temp_max}°C / 最低{temp_min}°C / 降水量{precip}mm"
            )

        result = "7日間の天気予報:\n" + "\n".join(forecast_lines)

        return [TextContent(type="text", text=result)]

    else:
        raise ValueError(f"Unknown tool: {name}")

async def main():
    """サーバー起動"""
    async with stdio_server() as (read_stream, write_stream):
        await app.run(
            read_stream,
            write_stream,
            app.create_initialization_options()
        )

if __name__ == "__main__":
    asyncio.run(main())

requirements.txt

mcp>=0.9.0
httpx>=0.27.0

インストールと設定

# 依存関係インストール
pip install -r requirements.txt

# Claude Desktop 設定
# claude_desktop_config.json に追加
{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": [
        "/path/to/weather_mcp_server.py"
      ]
    }
  }
}

使用例

【あなた】
東京(緯度35.6762、経度139.6503)の現在の天気を教えて

【Claude】
Weather MCPを使用して天気を取得します...

現在の天気:
気温: 15.2°C
風速: 12.5 km/h
天気コード: 1 (晴れ)

【あなた】
7日間の予報も見せて

【Claude】
7日間の天気予報:
2025-12-22: 最高16°C / 最低8°C / 降水量0mm
2025-12-23: 最高14°C / 最低7°C / 降水量0mm
2025-12-24: 最高15°C / 最低9°C / 降水量2mm
...

BYOAアプリとMCPの連携

パターン1: WebアプリからMCP経由でデータ取得

栽培アプリの例

<!-- cultivation-app.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>栽培管理アプリ</title>
</head>
<body>
  <h1>栽培管理</h1>

  <div id="weather-info">
    <h2>今日の天気</h2>
    <button onclick="updateWeather()">天気を更新</button>
    <div id="weather-display"></div>
  </div>

  <div id="cultivation-log">
    <h2>栽培記録</h2>
    <textarea id="work-note" placeholder="今日の作業"></textarea>
    <button onclick="saveLog()">記録</button>
  </div>

  <script>
    // Claude との連携を想定したコード
    // 実際には Claude Projects で実行

    async function updateWeather() {
      // Claude に依頼
      const prompt = `
気象MCPを使って、東京の現在の天気と
今週の予報を取得して、栽培に適した条件か判断してください。
      `;

      // Claude が MCP を使って天気を取得
      // 結果を表示
      document.getElementById('weather-display').innerHTML = `
        <p>気温: 15°C(栽培適温)</p>
        <p>降水: なし</p>
        <p><strong>本日の栽培作業: 適しています</strong></p>
      `;
    }

    function saveLog() {
      const note = document.getElementById('work-note').value;
      const log = {
        date: new Date().toISOString(),
        work: note,
        weather: '晴れ、15°C' // MCP経由で取得
      };

      // LocalStorageに保存
      const logs = JSON.parse(localStorage.getItem('logs') || '[]');
      logs.push(log);
      localStorage.setItem('logs', JSON.stringify(logs));

      alert('記録を保存しました');
      document.getElementById('work-note').value = '';
    }
  </script>
</body>
</html>

パターン2: Claude Projects + MCP で統合開発

育児アプリの例

# Claude Project: 育児記録アプリ

## カスタム指示

このプロジェクトは育児記録アプリの開発です。

利用可能なMCP:
- filesystem: アプリのコード管理
- childcare-info: 地域の保育園情報(カスタムMCP)

## 使い方

【あなた】
アプリに「近くの保育園検索」機能を追加したい。
現在地から3km以内の保育園を表示する機能。

【Claude】
Childcare Info MCPを使って、保育園情報を取得できます。
以下のコードを追加しましょう:

```javascript
// 保育園検索機能
async function searchNearbyNurseries(latitude, longitude) {
  // この部分で Claude が MCP を呼び出す想定
  const nurseries = await getNurseriesWithinRadius(
    latitude,
    longitude,
    3000 // 3km
  );

  return nurseries.map(n => ({
    name: n.name,
    address: n.address,
    distance: n.distance,
    capacity: n.capacity,
    phone: n.phone
  }));
}

Filesystem MCPを使って、このコードを src/nursery-search.js に保存しました。


---

### パターン3: 複数MCPの組み合わせ

**プロジェクト管理アプリの例**

利用MCP: - GitHub MCP: Issue管理 - Task Manager MCP: 個人タスク - Weather MCP: 作業環境判断

【シナリオ】 「今週のスプリント計画を立てて」

【Claude の動作】 1. GitHub MCPでマイルストーンを確認 2. Task Manager MCPで個人タスクを取得 3. Weather MCPで今週の天気を確認 → 雨の日は屋内作業を優先 4. 総合的なスプリント計画を提案

【出力例】 今週のスプリント計画:

月曜(晴れ): - GitHub Issue #45: 外部API統合テスト - 個人タスク: ドキュメント更新

火曜(雨): - GitHub Issue #47: リファクタリング - 個人タスク: コードレビュー

...


---

## MCPのセキュリティとベストプラクティス

### セキュリティ考慮事項

【重要】

❌ 避けるべきこと: - APIキーをコードにハードコーディング - 機密情報をログに出力 - 認証なしで外部公開

✅ 推奨: - 環境変数でAPIキー管理 - 適切なエラーハンドリング - ローカルでの実行を基本とする


**環境変数の使用例**

```json
// claude_desktop_config.json
{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
      }
    }
  }
}
# .env ファイル(gitignoreに追加)
GITHUB_TOKEN=ghp_your_token_here

ベストプラクティス

1. エラーハンドリング

// 良い例
try {
  const result = await fetchData();
  return { content: [{ type: 'text', text: result }] };
} catch (error) {
  return {
    content: [{
      type: 'text',
      text: `エラーが発生しました: ${error.message}`
    }],
    isError: true
  };
}

2. 入力バリデーション

// 入力値の検証
if (!args.taskId || typeof args.taskId !== 'string') {
  throw new Error('taskIdは必須です(文字列)');
}

if (args.priority && !['high', 'medium', 'low'].includes(args.priority)) {
  throw new Error('priorityは high, medium, low のいずれかです');
}

3. ログ出力

// stderr にログ(Claude には表示されない)
console.error('[MCP] Task added:', task.id);
console.error('[MCP] Current tasks count:', tasks.length);

// エラーログ
console.error('[MCP ERROR]', error);

4. リソース管理

// データベース接続は適切にクローズ
const db = await openDatabase();
try {
  const result = await db.query('SELECT * FROM tasks');
  return formatResult(result);
} finally {
  await db.close();
}

トラブルシューティング

よくある問題と解決方法

1. MCPサーバーが起動しない

# ログ確認
# macOS/Linux
tail -f ~/Library/Logs/Claude/mcp*.log

# 設定ファイルの文法チェック
cat ~/Library/Application\ Support/Claude/claude_desktop_config.json | jq .

解決方法: - JSONの文法エラーを確認 - コマンドパスが正しいか確認 - 依存関係がインストールされているか確認

2. ツールが表示されない

【確認事項】
✓ server.setRequestHandler(ListToolsRequestSchema) が実装されているか
✓ tools 配列が正しく返されているか
✓ Claude Desktop を再起動したか

3. ツール実行時のエラー

// デバッグログを追加
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  console.error('[DEBUG] Tool called:', request.params.name);
  console.error('[DEBUG] Arguments:', request.params.arguments);

  // ... ツール実行
});

次のステップ

学んだこと

✅ MCPの基本概念(Server, Client, Tools, Resources)
✅ 既存MCPサーバーの活用
✅ Claude Desktop での設定方法
✅ TypeScript/Python でのカスタムMCP作成
✅ BYOAアプリとの連携パターン
✅ セキュリティとベストプラクティス

レシピ#6-9で学ぶこと

実践的なMCP活用編

次のレシピでは:

✅ 育児アプリ × MCP の具体的実装
✅ 栽培アプリ × 気象データ連携
✅ 家計簿アプリ × データ分析MCP
✅ 読書アプリ × 書籍情報API連携
✅ 特化型機能とMCPの組み合わせ
✅ 実用的なユースケース集

→ レシピ#6-7の特化型機能に、MCPを統合して
  さらに強力なBYOAアプリを構築します!

まとめ

MCPがBYOA開発にもたらす価値

【レシピ#6-6】個人用アプリ
- LocalStorageのみ
- 閉じた環境

【レシピ#6-7】コラボで進化
- 特化型機能
- データ活用

【レシピ#6-8】MCPで拡張
✅ 外部システムと安全に連携
✅ 専門的なデータへアクセス
✅ 既存APIの活用
✅ セキュアな統合

【レシピ#6-9】実践活用(次回)
✅ 具体的なユースケース
✅ 特化型機能との統合
✅ 実用的なパターン

MCPは、あなたのBYOAアプリを外の世界とつなぐ架け橋です。

セキュアに、柔軟に、自分だけの統合を実現できます。

次のレシピで、MCPを使った実践的な開発を学びましょう!