Skip to content
Published on

Model Context Protocol(MCP)完全ガイド:AIツール標準化とClaude統合まで

Authors

はじめに

AIが単なるテキスト生成器を超え、実際のツールを使うエージェントへと進化するためには、モデルと外部システム間の標準化された通信手段が必要です。Anthropicが2024年11月に発表した Model Context Protocol(MCP) は、まさにこの課題を解決するオープン標準です。

USB-Cがさまざまな機器を一つの規格で繋ぐように、MCP は LLM がどんなデータソース・ツールとも統一された方法で通信できる プロトコルです。本ガイドでは、MCPのアーキテクチャから本番サーバーの実装、Claude Desktop統合、そしてエージェントシステムの設計まで、完全に解説します。

MCPアーキテクチャ概要

JSON-RPC 2.0 ベースの通信

MCPは JSON-RPC 2.0 プロトコルの上で動作します。クライアントがリクエストを送り、サーバーがレスポンスを返すシンプルな構造ですが、その上に強力な抽象化レイヤーが乗ります。

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "read_file",
    "arguments": {
      "path": "/tmp/example.txt"
    }
  }
}

Hosts / Clients / Servers の三角構造

MCPは3つの役割で構成されています:

  • Host: LLMを動かすアプリケーション — Claude Desktop、VS Code、カスタムAIアプリなど
  • Client: Host内部でMCPサーバーと1対1セッションを管理するコンポーネント
  • Server: ツール・リソース・プロンプトを外部に提供する軽量プロセス
[Host: Claude Desktop]
    └── [Client] ──── stdio/SSE ──── [MCP Server: filesystem]
    └── [Client] ──── stdio/SSE ──── [MCP Server: github]
    └── [Client] ──── stdio/SSE ──── [MCP Server: database]

Hostは複数のMCPサーバーに同時接続し、各サーバーが提供するツールをLLMに公開します。

MCPコアプリミティブ

Resources — 読み取り専用データ

Resourcesはサーバーがクライアントに公開する 読み取り専用データ です。ファイル、DBレコード、APIレスポンスなどを標準化されたURIでアクセスします。

  • URI形式: file:///home/user/docs/report.pdfdb://mydb/users/42
  • 副作用(side effect)なし — 純粋なデータ読み取り
  • テキストおよびバイナリコンテンツに対応

Tools — 関数実行

Toolsは LLMが実行できる関数 です。Resourcesと異なり、副作用を持つことができます。

  • ファイル作成・変更、API呼び出し、データベース書き込みなど
  • JSON Schemaで入力パラメーターを定義
  • 実行結果をテキストまたは画像で返す

Prompts — 再利用可能なテンプレート

Promptsはサーバーが提供する プロンプトテンプレート です。ユーザーが繰り返し使うパターンをサーバー側で標準化します。

@mcp.prompt()
def code_review_prompt(language: str, code: str) -> str:
    return f"{language}のコードをレビューして改善点を教えてください:\n\n{code}"

Sampling — サーバーからのLLM呼び出し

SamplingはMCP独自の機能で、サーバーがクライアントを通じてLLMを逆方向に呼び出す ことができます。サーバーが複雑な処理中にAIの判断が必要な場合に活用します。

MCPサーバーの実装: Python FastMCP

インストールと基本構造

pip install fastmcp

FastMCPはデコレーターベースの簡潔なAPIでMCPサーバーを構築できます。

from fastmcp import FastMCP

mcp = FastMCP("filesystem-server")

@mcp.tool()
def read_file(path: str) -> str:
    """ファイルの内容を読み取って返します。"""
    with open(path, "r", encoding="utf-8") as f:
        return f.read()

@mcp.tool()
def write_file(path: str, content: str) -> str:
    """ファイルに内容を書き込みます。"""
    with open(path, "w", encoding="utf-8") as f:
        f.write(content)
    return f"ファイル保存完了: {path}"

@mcp.resource("file://{path}")
def get_file_resource(path: str) -> str:
    """URIを通じてファイルリソースを提供します。"""
    with open(path, "r", encoding="utf-8") as f:
        return f.read()

if __name__ == "__main__":
    mcp.run()

ディレクトリ探索ツールの追加

import os
from pathlib import Path
from fastmcp import FastMCP

mcp = FastMCP("filesystem-server")

@mcp.tool()
def list_directory(path: str = ".") -> list[str]:
    """ディレクトリのファイル一覧を返します。"""
    p = Path(path)
    if not p.is_dir():
        raise ValueError(f"{path} はディレクトリではありません")
    return [str(item) for item in p.iterdir()]

@mcp.tool()
def search_files(directory: str, pattern: str) -> list[str]:
    """ディレクトリ内でパターンに一致するファイルを検索します。"""
    p = Path(directory)
    return [str(f) for f in p.rglob(pattern)]

if __name__ == "__main__":
    mcp.run(transport="stdio")

MCPサーバーの実装: TypeScript SDK

TypeScriptでデータベースリソースを提供するMCPサーバーを実装します。

import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'

const server = new McpServer({
  name: 'database-server',
  version: '1.0.0',
})

// リソース: DBレコードをURIで公開
server.resource(
  'user',
  new ResourceTemplate('db://users/{id}', { list: undefined }),
  async (uri, { id }) => {
    const user = await fetchUserById(id)
    return {
      contents: [
        {
          uri: uri.href,
          text: JSON.stringify(user, null, 2),
          mimeType: 'application/json',
        },
      ],
    }
  }
)

// ツール: SQLクエリを実行
server.tool('query_db', { sql: z.string().describe('実行するSQLクエリ') }, async ({ sql }) => {
  const results = await executeQuery(sql)
  return {
    content: [
      {
        type: 'text',
        text: JSON.stringify(results, null, 2),
      },
    ],
  }
})

async function main() {
  const transport = new StdioServerTransport()
  await server.connect(transport)
}

main()

stdio vs SSE トランスポート

stdio トランスポート

同一マシン上のプロセス間通信に最適です。Claude Desktopがサーバーを子プロセスとして起動する場合に使用します。

  • メリット: シンプルな設定、ネットワーク不要、セキュア(ローカル専用)
  • デメリット: リモートアクセス不可、シングルクライアントのみ
  • ユースケース: Claude Desktop、ローカル開発環境、CIパイプライン

SSE(Server-Sent Events)トランスポート

HTTPベースのリモート通信に最適です。複数クライアントが1つのサーバーに接続する場合に使用します。

  • メリット: リモートアクセス可能、マルチクライアント対応、Webベース統合
  • デメリット: HTTPサーバー設定が必要、認証実装が必要
  • ユースケース: チーム共有MCPサーバー、クラウドデプロイ、マルチユーザー環境
# SSEサーバーの起動例
if __name__ == "__main__":
    mcp.run(transport="sse", host="0.0.0.0", port=8000)

Claude Desktop統合

claude_desktop_config.json の設定

macOSでは ~/Library/Application Support/Claude/claude_desktop_config.json を編集します。

{
  "mcpServers": {
    "filesystem": {
      "command": "python",
      "args": ["/path/to/filesystem_server.py"],
      "env": {
        "ALLOWED_DIRS": "/home/user/documents,/tmp"
      }
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here"
      }
    },
    "sqlite": {
      "command": "uvx",
      "args": ["mcp-server-sqlite", "--db-path", "/path/to/database.db"]
    }
  }
}

ローカルサーバーの動作確認

Claude Desktopを再起動すると、接続済みサーバーのツールが自動的に利用可能になります。チャット画面下部のツールアイコンから、利用可能なMCPツール一覧を確認できます。

MCPクライアントの実装

サーバーだけでなく、独自のMCPクライアントを作成してサーバーに接続することもできます。

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    server_params = StdioServerParameters(
        command="python",
        args=["filesystem_server.py"],
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # 初期化
            await session.initialize()

            # 利用可能なツール一覧
            tools = await session.list_tools()
            print("ツール一覧:", [t.name for t in tools.tools])

            # ツールの呼び出し
            result = await session.call_tool(
                "read_file",
                arguments={"path": "/tmp/test.txt"}
            )
            print("結果:", result.content[0].text)

asyncio.run(main())

エージェントシステム: LangGraph + MCP

LangGraphとMCPを連携させることで、LangGraphエージェントがMCPサーバーのツールを直接使用できます。

from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from langchain_anthropic import ChatAnthropic
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import asyncio

async def create_mcp_agent():
    model = ChatAnthropic(model="claude-3-5-sonnet-20241022")

    server_params = StdioServerParameters(
        command="python",
        args=["filesystem_server.py"],
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # MCPツールをLangChainツールに変換
            tools = await load_mcp_tools(session)

            # ReActエージェントを作成
            agent = create_react_agent(model, tools)

            result = await agent.ainvoke({
                "messages": [{"role": "user", "content": "/tmp ディレクトリのファイル一覧を表示してください"}]
            })
            return result

asyncio.run(create_mcp_agent())

危険なツールへのユーザー承認フロー

機密性の高い操作にはユーザー確認を求めるパターンを実装します。

from fastmcp import FastMCP
from fastmcp.exceptions import ToolError

mcp = FastMCP("safe-server")

DANGEROUS_PATHS = ["/etc", "/sys", "/proc", "/root"]

@mcp.tool()
def delete_file(path: str, confirmed: bool = False) -> str:
    """
    ファイルを削除します。削除前に必ず confirmed=True を明示してください。

    Args:
        path: 削除するファイルのパス
        confirmed: 削除意思の明示的な確認(デフォルト: False)
    """
    import os
    from pathlib import Path

    abs_path = str(Path(path).resolve())

    # 危険パスのブロック
    for danger in DANGEROUS_PATHS:
        if abs_path.startswith(danger):
            raise ToolError(f"{danger} 配下のパスは削除できません")

    # 確認なしで呼び出された場合、ユーザーに確認を要求
    if not confirmed:
        return (
            f"警告: {abs_path} を完全に削除しようとしています。"
            f"confirmed=True を設定して再度呼び出すと削除されます。"
        )

    os.remove(abs_path)
    return f"削除完了: {abs_path}"

セキュリティの考慮事項

権限モデル

MCPサーバーは最小権限の原則に従う必要があります。

  • 必要なディレクトリ・DBにのみアクセスを許可
  • 環境変数で許可パスリストを管理
  • パストラバーサル攻撃への対策
import os
from pathlib import Path

ALLOWED_DIRS = os.getenv("ALLOWED_DIRS", "/tmp").split(",")

def is_path_allowed(path: str) -> bool:
    abs_path = Path(path).resolve()
    return any(
        str(abs_path).startswith(allowed.strip())
        for allowed in ALLOWED_DIRS
    )

機密データの取り扱い

  • APIキー・パスワードは環境変数で注入し、コードへのハードコーディングを禁止
  • ログへの機密レスポンス内容の記録を禁止
  • TLSなしのリモートMCPサーバー運用を禁止

サンドボックス化

本番環境ではMCPサーバーをコンテナや仮想環境で隔離します。

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
# rootではなく専用ユーザーで実行
RUN useradd -m mcpuser
USER mcpuser
CMD ["python", "server.py"]

ツール呼び出しの検証

LLMが生成したtool callパラメーターはサーバー側で必ず再検証する必要があります。LLMへのプロンプトインジェクション攻撃により悪意あるパラメーターが注入される可能性があります。

from pydantic import BaseModel, validator

class FileReadParams(BaseModel):
    path: str

    @validator("path")
    def validate_path(cls, v):
        p = Path(v).resolve()
        if ".." in str(p):
            raise ValueError("パストラバーサルの試みを検出しました")
        if not is_path_allowed(str(p)):
            raise ValueError("許可されていないパスです")
        return str(p)

MCP vs 従来のfunction calling

項目OpenAI Function CallingMCP
標準化ベンダー依存オープン標準
再利用性モデルごとに再実装が必要1回の実装で全互換モデルに対応
データアクセスなし(ツールのみ)Resourcesでデータを直接公開
逆方向LLM呼び出しなしSamplingでサーバーがLLMを呼び出し可能
トランスポートHTTPのみstdio、SSEに対応

クイズ

Q1. MCPにおけるResourcesとToolsの核心的な違いは何ですか?

答え: Resourcesは副作用のない読み取り専用データアクセスであり、Toolsはシステム状態を変更できる関数実行です。

解説: Resourcesは file://db:// などのURIを通じてデータを読み取るだけで、システムの状態を変えません。Toolsはファイル書き込み、API呼び出し、DB書き込みなど状態を変更する処理を実行します。LLMがデータを読む必要があるときはResource、何かを実行・変更するときはToolを使います。

Q2. MCPがOpenAI function callingよりツール標準化に有利な理由は?

答え: MCPはベンダー中立のオープン標準であり、一度実装したMCPサーバーをClaude、GPT、オープンソースモデルなど全ての互換モデルで再利用できます。

解説: OpenAI function callingはOpenAI API形式に依存しており、別のモデルに切り替えるとツールを全部作り直す必要があります。MCPサーバーはプロトコルレベルで標準化された独立サービスであり、特定のモデルから完全に分離されています。また、Resourcesによるデータ公開、Promptsによるテンプレート共有、Samplingによる逆方向LLM呼び出しなど、function callingにない機能も備えています。

Q3. stdioとSSEトランスポートはそれぞれどのような場面に適していますか?

答え: stdioはローカルのシングルクライアント環境(Claude Desktop、個人開発)に、SSEはリモート・マルチクライアント環境(チーム共有サーバー、クラウドデプロイ)に適しています。

解説: stdioは親プロセスが子プロセスを直接起動するため、ネットワーク設定が不要でセキュリティがシンプルです。SSEはHTTPサーバーを立て、複数のクライアントが同時接続できるため、クラウドデプロイやチームでの共有に向いています。ただし、SSEを使う場合は認証やTLSの設定が別途必要です。

Q4. MCP Samplingプリミティブの動作方式とセキュリティ上の考慮事項は?

答え: SamplingはMCPサーバーがクライアント経由でLLMを逆方向に呼び出せる機能で、ユーザーのレビューと承認を経てのみ実行されるべきです。

解説: 通常のフローではLLMがサーバーのツールを呼び出しますが、Samplingはその逆で、サーバーがLLMに「このテキストを分析してください」とリクエストします。Hostはこのリクエストを中間でレビューし、ユーザーの承認後にのみLLMへ転送する必要があります。悪意あるサーバーがSamplingを通じて無制限にLLMを呼び出したり、精巧に作られたSamplingリクエストで機密情報を抽出したりするのを防がなければなりません。

Q5. MCPサーバーで危険なツールへのユーザー承認ワークフローはどのように実装しますか?

答え: confirmed パラメーターパターンを使い、最初の呼び出しで警告を返し、confirmed=True を明示的に設定した2回目の呼び出しでのみ実際の処理を実行します。

解説: LLMはToolの説明(description)を読んでどう呼び出すかを決めます。confirmed=False の最初の呼び出しで「これを実行しようとしています。confirmed=True で再呼び出しすると実行されます」と明確な警告を返すと、LLMはユーザーに確認を求めます。ユーザーが承認すれば、LLMが confirmed=True で再呼び出しし、実際の処理が実行されます。このパターンは危険パスのブロックや権限チェックと組み合わせて使う必要があります。

まとめ

MCPはAIエージェントエコシステムの USB-C です。一度標準を定義すれば、どのモデルとも、どのクライアントとも接続できます。AnthropicがオープンスタンダードとしてMCPを公開したことで、すでにコミュニティから数百のMCPサーバーが開発されており、VS Code、Cursor、JetBrainsなど主要な開発ツールもMCPのサポートを開始しています。

Python FastMCPで5分で最初のサーバーを作り、Claude Desktopに接続してみてください。AIが皆さんのファイルシステム、データベース、APIを直接操作する体験がいかに強力かをすぐに感じることができます。