PythonでカスタムMCPサーバーを構築する:AIをあなたのシステムに接続する

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

数ヶ月前、同僚がこぼしていた。仕事でClaudeを使っているのに、サーバーのログをチャットにコピペして、分析結果をまたコピペして報告書に貼り付ける作業で半日が潰れてしまうと。「Claudeがうちのサーバーのログを直接読めるようにならないかな?」——その一言がきっかけでMCPについて調べ始めた。

これは彼だけの問題ではない。ほとんどのAIモデルは「ブラックボックス」として動いている——学習データから多くのことを知っているが、あなたの社内システムについては完全に無知だ。社内データベース、ファイルサーバーのディレクトリ、社内専用API——すべて手の届かないところにある。橋渡しが必要だ。MCPサーバーこそがその役割を果たす。

MCPとは何か、なぜ必要なのか?

MCP——Model Context Protocolの略——は、Anthropicが開発したオープンプロトコルで、AIモデルが外部のツールやデータソースと標準化された方法で接続することを可能にする。

イメージとしてはUSBが近い。マウス、キーボード、ハードディスク——すべて同じポートで接続できる統一規格だ。MCPはAIに対してこれと同じ役割を果たす。MCP対応のクライアントであれば、どのMCPサーバーとも接続できる。各ペアごとに個別の統合を実装する必要はない。

MCPアーキテクチャの3つのコンポーネント:

  • MCP Host:AIを実行するアプリケーション——Claude Desktop、Cursor、または自前のアプリ
  • MCP Client:Host内部でMCP通信を処理するコンポーネント
  • MCP Server:あなたのサーバー——ツール、リソース、プロンプトをAIに公開する

サーバーがAIに提供できる3種類のもの:

  • Tools:AIが呼び出せる関数(ファイル読み込み、データベースクエリ、API呼び出しなど)
  • Resources:URI経由でAIが読み取るデータ——URLを読み込むような感覚
  • Prompts:繰り返し作業用の再利用可能なプロンプトテンプレート

PythonでMCPサーバーを初めて構築する

「File Manager」——ディレクトリの一覧表示とファイル内容の読み込みをAIに許可するサーバーを構築する。シンプルだが、MCPがどう動くかを体感するには十分だ。このサーバーは筆者が毎日ログ分析に使っている。何百行ものログをチャットにコピペする代わりに、Claudeが/var/log/から直接読み込んで、その場で分析してくれる。

ステップ1:MCP Python SDKをインストールする

仮想環境を作成してライブラリをインストールする:

python -m venv venv
source venv/bin/activate  # Linux/macOS
# venv\Scripts\activate   # Windows

pip install mcp

ステップ2:基本的なMCPサーバーを作成する

file_manager_server.pyを作成する:

from mcp.server.fastmcp import FastMCP
import os
import json
import logging
from pathlib import Path
import datetime

# ロギングの設定:すべてのツール呼び出しを追跡する
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
log = logging.getLogger(__name__)

# 表示名でサーバーを初期化する
mcp = FastMCP("File Manager")

# アクセスを許可するディレクトリを定義する(セキュリティ上重要)
ALLOWED_DIR = Path("/home/user/documents").resolve()


def _is_safe_path(filepath: str) -> bool:
    """パスが許可されたディレクトリ内にあるかを確認する"""
    try:
        target = Path(filepath).resolve()
        return str(target).startswith(str(ALLOWED_DIR))
    except Exception:
        return False


@mcp.tool()
def list_files(directory: str) -> str:
    """指定されたパスのファイルとディレクトリを一覧表示する。"""
    if not _is_safe_path(directory):
        return "エラー: このディレクトリへのアクセス権限がありません"

    log.info(f"list_files: {directory}")
    try:
        path = Path(directory)
        items = []
        for item in sorted(path.iterdir()):
            items.append({
                "name": item.name,
                "type": "directory" if item.is_dir() else "file",
                "size_kb": round(item.stat().st_size / 1024, 2) if item.is_file() else None
            })
        return json.dumps({"path": directory, "items": items, "count": len(items)}, ensure_ascii=False)
    except Exception as e:
        return f"エラー: {str(e)}"


@mcp.tool()
def read_file(filepath: str, max_lines: int = 100) -> str:
    """テキストファイルの内容を読み込む。コンテキストのオーバーフローを防ぐため、デフォルトは100行。"""
    if not _is_safe_path(filepath):
        return "エラー: このファイルへのアクセス権限がありません"

    log.info(f"read_file: {filepath} (max_lines={max_lines})")
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            lines = f.readlines()

        total_lines = len(lines)
        content = ''.join(lines[:max_lines])

        if total_lines > max_lines:
            content += f"\n... (残り {total_lines - max_lines} 行、必要に応じてmax_linesを増やしてください)"

        return content
    except UnicodeDecodeError:
        return "エラー: テキストファイルではないか、サポートされていないエンコーディングです"
    except Exception as e:
        return f"エラー: {str(e)}"


@mcp.tool()
def get_file_info(filepath: str) -> str:
    """ファイルのメタデータを取得する:サイズと最終更新時刻。"""
    if not _is_safe_path(filepath):
        return "エラー: アクセス権限がありません"

    try:
        stat = Path(filepath).stat()
        return json.dumps({
            "path": filepath,
            "size_bytes": stat.st_size,
            "size_kb": round(stat.st_size / 1024, 2),
            "modified": datetime.datetime.fromtimestamp(stat.st_mtime).isoformat(),
            "is_file": Path(filepath).is_file()
        }, ensure_ascii=False)
    except Exception as e:
        return f"エラー: {str(e)}"


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

ステップ3:サーバーを試しに動かす

直接実行して構文エラーがないか確認する:

python file_manager_server.py

ターミナルは何も表示しない——これは正しい動作でエラーではない。MCPの通信はJSON-RPC規格のstdin/stdoutを使うため、サーバーは接続を待機しているだけで、通常のWebサーバーのように画面に何かを出力するわけではない。

ステップ4:Claude Desktopに接続する

Claude Desktopの設定ファイルを開く。macOSの場合:

nano ~/Library/Application\ Support/Claude/claude_desktop_config.json

Linuxの場合:

nano ~/.config/claude/claude_desktop_config.json

サーバー設定を追加する:

{
  "mcpServers": {
    "file-manager": {
      "command": "python",
      "args": ["/path/to/file_manager_server.py"]
    }
  }
}

Claude Desktopを再起動する。チャット画面にMCPアイコン(ハンマーのアイコン)が表示されたら成功だ。以降、Claudeが会話の中から直接あなたのツールを呼び出せるようになる。コピペ作業は一切不要。

発展:静的データのためのリソースを公開する

公開できるのはToolsだけではない。Resourcesを使うと、config://app-settingsのようなURIでAIがデータを読み取れるようになる。毎回聞かなくてもAIが常にシステムのコンテキストを持てるようにしたいときに便利だ。例えば、設定ファイルを公開してClaudeが現在のアプリ設定を把握できるようにする:

@mcp.resource("config://app-settings")
def get_app_config() -> str:
    """現在のアプリケーション設定を返す"""
    config_path = ALLOWED_DIR / "config.json"
    if config_path.exists():
        return config_path.read_text(encoding='utf-8')
    return "{}"

AIはconfig://app-settingsをURLを読むような感覚でアクセスできる。ファイルパスを毎回手動でツールに渡すより、はるかにすっきりしている。

実際の運用で注意すべきポイント

自作のMCPサーバーを使ってきた中で、もっと早く知っておけばデバッグの時間をかなり節約できたと感じた問題をいくつか紹介する:

  • 入力バリデーションは必須:AIはツールにどんな文字列でも渡してくる——../../etc/passwdのようなパスも例外ではない。サンプルの_is_safe_path()関数が、AIが許可ディレクトリ外に出るのを防いでいる。これを省くのは過剰な用心ではなく、深刻なセキュリティホールになる。
  • 出力サイズの制限max_lines=100は見た目より重要だ。100MBのログファイルを制限なしで読み込むとコンテキストウィンドウがすぐに溢れ、Claudeがエラーを返してそのセッション全体が無駄になる。
  • エラーは例外ではなく文字列で返す:文字列で返せばAIがエラーメッセージを読んでユーザーに伝えられる。例外を投げるとClaudeは曖昧な通知しか受け取れず、ユーザーには何が起きたかわからなくなる。
  • すべてのツール呼び出しをログに残す:問題が起きたとき、Claudeがどのツールをどんなパラメータで何時に呼び出したかを知る必要がある。ログがなければデバッグはほぼ手探りになる。

まとめ

MCPサーバーの本質は、通常の関数をJSON-RPC規格で公開するPythonスクリプトだ——AIはそれをAPIを呼ぶのと同じように利用する。難しいことは何もない。Python関数が書けて例外処理ができれば、完全なMCPサーバーを構築するには十分だ。

MCP規格の最大の利点:サーバーを一度書けば、あらゆるクライアントから使える。今日のClaude Desktop、来週のCursor、来月の社内アプリ——すべて同じサーバーに接続でき、統合処理を書き直す必要はない。

ファイルシステムに慣れたら、SQLiteやPostgreSQLに接続してClaudeがデータベースを直接クエリできるようにしてみよう。または社内APIをMCPツールとしてラップするのもいい。冒頭の同僚は、デプロイ後にログのコピペ作業から解放された——Claudeが自動で読み込んで分析してくれるので、彼は結果を読むだけでいい。

Share: