🕹️

シンプルな MCP サーバを作って動作を理解する

に公開

KeyVisual

はじめに

最近は AI、特に LLM の発展がまさに日進月歩ですね。毎週、下手したら毎日のように新しいニュースが飛び込んできます。

中でも自分が注目しているのは Anthropic 社が提唱している MCP(Model Context Protocol)です。

解説にあたり、事前に簡単な MCP サーバのサンプルを実装しました。実装は GitHub に上がっているのでこれを変更しながら動作を確認してみてください。

今回はこの MCP サーバの実装を元に MCP の動作のさせ方について解説していこうと思います。

https://github.com/edom18/simple-mcp-sample

MCP とは

https://docs.anthropic.com/ja/docs/agents-and-tools/mcp

まずは簡単に MCP について触れておきます。

MCP は Anthropic 社が提唱しているオープンなプロトコル仕様です。ドキュメントから引用すると以下のように説明されています。

MCPは、アプリケーションがLLMにコンテキストを提供する方法を標準化するオープンプロトコルです。MCPはAIアプリケーション用のUSB-Cポートのようなものと考えてください。USB-Cがデバイスをさまざまな周辺機器やアクセサリーに接続するための標準化された方法を提供するように、MCPはAIモデルを異なるデータソースやツールに接続するための標準化された方法を提供します。

重要な概念は「USB-C ポートのようなもの」という部分です。MCP はプロトコルなので具体的な実装があるわけではなく、あくまでやり取りをどういう手順で行うかを規定しているのみです。プログラミング言語で言えばインターフェースなどが近い概念でしょう。

プロトコルのベースは JSON-RPC を採用しており、プロトコルに適合する実装さえ施せば既存のサーバの機能を MCP として提供可能というわけです。

環境

今回の実装は Python で行っています。自分の環境は以下です。

  • macOS Sonoma 14.6.1
  • Python 3.12.9
  • mcp 1.6.0
  • anthropic 0.52.1
  • python-dotenv 1.1.0

必要モジュールをインストール

今回は mcp モジュール(MCP の Python SDK)と anthropic モジュールを利用します。 requirements.txt を用意しているのでこれを利用してセットアップします。

モジュールのインストール
$ pip install -r requirements.txt

リポジトリをクローン

前述のリポジトリをクローンしてください。

リポジトリのクローン
$ git clone git@github.com:edom18/simple-mcp-sample.git

.env ファイルを用意

リポジトリには .env.template ファイルがあるのでこれをコピーして、Anthropic の API Key をご自身のものに置き換えてください。

.env ファイルを用意
$ cp .env.template .env
API Key を設定
ANTHROPIC_API_KEY=<YOUR ANTHROPIC API KEY HERE>

デモを起動

後述するように、今回の MCP サーバは Claude Desktop で動作するように作っていますが、クライアント側の実装イメージも持てるように MCP クライアントの実装もあります。それを試すにはふたつのシェルスクリプトがあるのでそれを利用して起動します。

シングルクライアントの起動
$ ./single-client-launch-demo.sh
マルチクライアントの起動
$ ./multi-client-launch-demo.sh

どちらも起動するとチャットモードに入ります。チャット自体は普通の AI との会話ですが、MCP を設定しているので今回実装した機能を利用するよう促すと AI が自動でそれを判断して使用してくれます。今回用意した MCP サーバの機能はとてもシンプルで、テキストを変換するものとプロフィールデータを提供するものです。(MCP 使わなくてもできるだろというツッコミはなしでw)

MCP の実態は Function Calling なので、AI に対して適当に「次の言葉を逆順にして」とか指示すると適切に MCP サーバを呼び出して実行してくれます。

MCP 機能の利用
Query: 「こんにちは!」を逆順にして

「こんにちは!」というテキストを逆順にするために、reverse-text 関数を使用します。
[Calling tool reverse-text with args {'text': 'こんにちは!'}]
「こんにちは!」を逆順にすると、「!はちにんこ」になります。
正確に文字を一つずつ逆から並べることができましたね。

利用価値はありませんが(w)、サーバ側の実装を知る意味では十分でしょう。

Claude Desktop で利用する

前述の通り、今回実装した MCP サーバは Claude Desktop でも利用できます。以下のように設定を追加してください。

Claude Desktop の MCP 設定
"mcpServers": {
    "simple-mcp-sample": {
        "command": "python",
        "args": [
            "/path/to/simple-mcp-sample/server/simple_server.py"
        ]
    }
}

実際に設定した様子。今回実装したふたつの機能がリストされているのが分かります。

MCP ツールとして認識されている様子

ふたつのツールが認識されている


Claude Desktop の使い方ドキュメントは以下です。

https://modelcontextprotocol.io/quickstart/user

MCP サーバの実装

ここから、実際に MCP サーバの実装を見ながら理解を深めていきます。

まずはインストールした mcp モジュールをインポートします。

モジュールインポート
from mcp.server.models import InitializationOptions
from mcp.server import NotificationOptions, Server
import mcp.types as MCPTypes
import mcp.server.stdio

一番重要なのは mcp.server.Server です。このクラスは MCP のリクエスト・レスポンス部分を提供してくれるクラスで、JSON-RPC による実装を隠蔽してくれます。開発者は実際のロジック部分の実装に集中することができます。

MCP の仕組みとして、クライアントからのリクエストで「なにができるのか」と「それを実行すること」を実装する必要があります。これらは前述のクラスのデコレータで簡単に実現できます。

ツールリストを返す関数を実装する

今回の実装は以下です。

ツールリスト
server = Server("SimpleMCPServer")

@server.list_tools()
async def handle_list_tools() -> list[MCPTypes.Tool]:
    """
    List available tools for simple text operations.
    """

    return [
        MCPTypes.Tool(
            name="reverse-text",
            description="Reverse the input text.",
            inputSchema={
                "type": "object",
                "properties": {
                    "text": {
                        "type": "string",
                        "description": "Text to reverse",
                    },
                },
                "required": ["text"],
            },
        ),
        MCPTypes.Tool(
            name="uppercase",
            description="Modify input text to upper case.",
            inputSchema={
                "type": "object",
                "properties": {
                    "text": {
                        "type": "string",
                        "description": "Text to convert to uppercase",
                    },
                },
                "required": ["text"],
            },
        ),
    ]

@server.list_tools() デコレータを利用します。実装部分は提供しているツールを説明する mcp.types.Tool のリストを返すだけで OK です。引数には関数名となにができるのかの説明、そして関数呼び出しに必要な引数のリストを指定しています。引数定義は JSON Schema を使って定義します。

今回定義している関数(ツール)は、受け取ったテキストを逆順にするものとアッパーケースに変更するものの 2 種類です。どちらもテキストを受け取ることが明示されていますね。

ツールの利用部分を実装する

ツールリストを公開したら、次は実際に AI からのリクエストに応える機能を実装します。今回はふたつのツールを提供しているのでそれらを実装していきます。

といっても今回はどちらの機能も 1 行程度で済むので、AI からのリクエストに応える部分をメインに見ていきます。

ツールコール
@server.call_tool()
async def handle_call_tool(
    name: str, arguments: dict | None
) -> list[MCPTypes.TextContent]:
    """
    Handle tool execution requests
    """
    
    if not arguments:
        raise ValueError("Missing arguments")

    text = arguments.get("text")
    if not text:
        return [MCPTypes.TextContent(type="text", text="Error: Missing text parameter")]

    if name == "reverse-text":
        result = text[::-1]
    elif name == "uppercase":
        result = text.upper()
    else:
        raise ValueError(f"Unknown tool {name}")

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

こちらも @server.call_tool() デコレータによって簡単に実装することができます。AI からのリクエストは基本的に文字列で来るため、文字列によって関数を識別して処理します。

具体的には以下の部分ですね。

関数の判別
if name == "reverse-text":
    result = text[::-1]
elif name == "uppercase":
    result = text.upper()
else:
    raise ValueError(f"Unknown tool {name}")

引数で渡ってくる name に関数名が含まれているためこれによって分岐しています。今回の処理はどちらも 1 行で済むのでその場で実行して返していますが、込み入った実装の場合は別関数にしたりクラスを用意するなりして処理すればいいでしょう。場合によってはさらに裏で別のサーバを動かしてそちらに処理を委譲し、その結果を返す窓口的なサーバの実装もありです。

実は MCP サーバの根幹部分は以上です。SDK を利用すると非常に簡単に実装することができますね。

MCP クライアントからの起動を待ち受ける

さて、サーバの根幹部分は完成なのですがもう少しだけ実装しなければならない部分があります。MCP クライアント側の実装を見てもらうと分かるのですが、MCP クライアントはローカルのサーバの場合は別プロセスとして起動し、標準入出力を用いてやり取りを行います。つまり、プロセスとして起動するための処理を実装する必要があります。

といっても、 python コマンドでの起動を想定しておくだけで大丈夫です。実装部分は以下です。

プロセス用処理
async def main():
    print("Launcing an MCP server...")

    # Run the server using stdin/stdout streams
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="SimpleMCPServer",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )

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

別プロセスとして起動された際に main() 関数が起動され、その中で標準入出力のストリームを持つサーバとして起動しているのが分かります。MCP クライアントはこの標準入出力を介してこのサーバとやり取りすることになります。

さいごに

今回は MCP サーバに焦点を当てているので MCP クライアントについては触れません。(時間を見つけて別で書くかも)

MCP サーバを立てて機能を提供する部分はかなり簡単に実装できることが分かってもらえたかと思います。前述のように、窓口としての MCP サーバを実装し、実際の処理は別で実装することも可能なので既存の機能を MCP 化するのは比較的簡単に行えるでしょう。

今回は触れませんでしたが MCP の仕様的にはツールだけでなく、リソース(データ)やプロンプトを提供することもできます。(マルチクライアントの例で使用しているもうひとつの MCP サーバはただデータを提供しているだけなので、実際には resource 提供とするほうが適切かもしれません)

今後、AI の性能は爆発的に上がっていくことは想像に難くありません。つまり AI モデルが単体で行えることがどんどん増えていくことでしょう。そんな中でも、リソース(データ)や既存機能の統合は AI がどんなに高性能になっても不可能な部分です。(似たことを模倣することはできるかもしれませんが、本当の意味で既存機能を使っているわけではない)

つまり AI にエンパワーする意味でも MCP は今後重要な存在になっていくと考えています。今のうちからなにができるのかを模索し、実際に作っておくとのちのちきっと役に立つことでしょう。それに MCP の強みはオープンであることなので、一度作ってしまえば新しい AI が出てきてもすぐに利用することができ、MCP を利用したサービス提供はむしろ AI の性能向上の恩恵を最大に受けられるサービスを作れることを意味しています。

この記事を見て MCP サーバの実装に興味を持ってもらえたら幸いです。

MESONテックブログ

Discussion