0
0

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サーバーを自作する — Model Context Protocolの最小実装

0
Last updated at Posted at 2026-08-30

MCPサーバーを自作する — Model Context Protocolの最小実装

MCP(Model Context Protocol)は、LLMが外部ツールやリソースにアクセスするためのオープンプロトコルです。Anthropicが提唱し、OpenAIやGoogleも対応を進めており、AIエージェントの機能拡張における事実上の標準になりつつあります。

本記事では、MCPサーバーをゼロから実装する最小手順を解説します。Python SDKを使って「計算機ツールを提供するサーバー」を作り、デバッグツールのMCP Inspectorで動作確認までを行います。

MCPサーバーの最小構成要素

MCPサーバーを動かすために必要な要素は4つだけです。

  1. サーバーインスタンス — プロトコルメッセージを処理する本体
  2. トランスポート — クライアントとの通信手段(デフォルトはstdio)
  3. ツール定義 — サーバーが提供する機能
  4. エントリポイント — サーバーを起動する関数

プロトコルの複雑さ(JSON-RPCメッセージの解析、ハンドシェイク、ライフサイクル管理)はSDKが内部で処理するため、開発者は「どのようなツールを提供するか」だけに集中できます。

開発環境の準備

MCPの公式Python SDKはmcpパッケージとしてPyPIで配布されています。Python 3.10以上が必要です。

mkdir mcp-server-project && cd mcp-server-project
python3 -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]"

[cli]エクストラを指定すると、デバッグツールのMCP Inspectorも一緒にインストールされます。インストール後、python3 -c "import mcp; print(mcp.__version__)"で確認できます。

最小限のMCPサーバーを実装する

足し算・引き算・掛け算・割り算ができる計算機ツールを提供するサーバーを作ります。src/server.pyを作成し、以下のコードを記述します。

#!/usr/bin/env python3
""" 最小限のMCPサーバー — 計算機ツールを提供する """

from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types
import asyncio
import logging
import sys

# ログはstderrに出力(stdoutはMCP通信に使うため絶対禁止)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    stream=sys.stderr,
)

server = Server("calculator-server")


@server.list_tools()
async def list_tools() -> list[types.Tool]:
    """ サーバーが提供するツールの一覧を返す """
    return [
        types.Tool(
            name="add",
            description="2つの数値を足し算する",
            inputSchema={
                "type": "object",
                "properties": {
                    "a": {"type": "number", "description": "足される数"},
                    "b": {"type": "number", "description": "足す数"},
                },
                "required": ["a", "b"],
            },
        ),
        types.Tool(
            name="divide",
            description="2つの数値の割り算を行う(a / b)",
            inputSchema={
                "type": "object",
                "properties": {
                    "a": {"type": "number", "description": "割られる数"},
                    "b": {"type": "number", "description": "割る数(0以外)"},
                },
                "required": ["a", "b"],
            },
        ),
    ]


@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    """ ツール呼び出しを処理して結果を返す """
    a = arguments.get("a")
    b = arguments.get("b")

    if a is None or b is None:
        raise ValueError("引数 a と b は必須です")

    if name == "add":
        result = a + b
    elif name == "divide":
        if b == 0:
            raise ValueError("ゼロで割ることはできません")
        result = a / b
    else:
        raise ValueError(f"不明なツール: {name}")

    return [
        types.TextContent(
            type="text",
            text=f"計算結果: {a} {name} {b} = {result}",
        )
    ]


async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            server.create_initialization_options(),
        )


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

コードの解説

ツール一覧の定義

@server.list_tools()デコレータを付けた関数は、クライアントがtools/listリクエストを送ったときに呼び出されます。各ツールにはnamedescriptioninputSchemaを定義します。

inputSchemaはJSON Schema形式で記述します。LLMはこのスキーマを読んで「どのような引数を渡せばよいか」を判断するため、各パラメータのdescriptionを明確に記述することが重要です。曗昧な説明だとLLMが間違った引数を渡す原因になります。

ツール呼び出しの処理

@server.call_tool()デコレータを付けた関数は、クライアント(LLM)がtools/callリクエストを送ったときに呼び出されます。nameにはツール名、argumentsにはLLMが構築した引数オブジェクトが渡されます。

戻り値はコンテンツのリストで、テキスト結果を返す場合はtypes.TextContentを使います。画像を返す場合はtypes.ImageContent、リソースを埋め込む場合はtypes.EmbeddedResourceを使用します。

トランスポートの設定

stdio_server()はstdioトランスポートを提供するコンテキストマネージャです。サーバーは標準入力からMCPメッセージを読み取り、標準出力にレスポンスを書き込みます。これがMCPのデフォルト通信方式です。

stdoutへのログ出力は絶対禁止

MCPサーバーでは絶対に標準出力(stdout)にログを出力してはいけません。stdioトランスポートはstdoutをプロトコルメッセージの通信路として使うため、ログ文字列がstdoutに混入するとJSON-RPCメッセージの解析エラーを引き起こし、サーバーがクラッシュします。ログは必ずsys.stderrに出力してください。

# 絶対にやってはいけない
print("サーバー起動しました")

# loggingモジュールでstream=sys.stderrを指定
logging.basicConfig(stream=sys.stderr, level=logging.INFO)

MCP Inspectorで動作確認

サーバーをClaude Desktopに接続する前に、MCP Inspectorで動作確認を行います。

mcp dev src/server.py

実行するとブラウザ上でWeb UIが起動します。以下の手順で確認します。

  1. 接続状態の確認 — 画面左上にcalculator-serverと表示され、Connectedになっていれば成功
  2. ツール一覧の確認 — Toolsタブで定義したツールが表示されるか確認
  3. ツールの実行テストaddツールを選び、aに3、bに5を入力してRun Tool。計算結果が返れば正常
  4. エラーケースの確認divideツールでbに0を入力して実行。エラーメッセージが返されることを確認

Inspectorでテストが成功すれば、サーバーの実装は完了です。あとはClaude Desktopの設定ファイルにサーバーを登録すれば、LLMからツールを呼び出せます。

よくある実装ミス

初めてMCPサーバーを作る際によく遭遇する問題を3つ紹介します。

非同期関数の同期呼び出し: MCP SDKのハンドラはすべてasync defで定義する必要があります。内部で非同期処理を行う場合はawaitを使いましょう。

inputSchemaの形式誤り: トップレベルの"type": "object"を省略すると、LLMが正しく引数を構築できない場合があります。requiredフィールドで必須パラメータを明示することも重要です。

パッケージバージョンの古さ: pip install --upgrade "mcp[cli]"で最新版に保ちましょう。SDKはまだ活発に開発されており、APIの変更が入ることがあります。

まとめ

MCPサーバーの最小構成要素は「サーバーインスタンス・トランスポート・ツール定義・エントリポイント」の4つです。Python SDKのデコレータベースAPIを使えば、数十行で動作するサーバーを作れます。

ツールのinputSchemaに明確なdescriptionを書くこと、ログをstderrに出力すること、MCP Inspectorで事前にテストすること — この3点を守れば、つまづかずに実装できます。

MCPはAIエージェントの機能拡張における標準プロトコルになりつつあります。自作サーバーで独自ツールを提供できるようになれば、AIアプリケーションの可能性が大きく広がります。


本記事は著者 葉山悠希 の書籍『MCP実践入門』の内容をベースに、Qiita単体で完結するよう再構成した技術記事です。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?