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サーバーの動作確認を容易にするクライアント実装(Stdio / SSE / Streamable HTTP対応)

Last updated at Posted at 2025-07-12

はじめに

MCPサーバーは ファイル操作やコマンド操作といったタスクを処理できるので セキュリティ対策としてDockerでサンドボックス化して起動することが推奨されます。

ただ、自作したMCPサーバーをDockerで実行すると、ツールの中間出力(応答やエラーなど)が見えにくくなり、デバッグが難しくなる場面があります。

また、Transport(Stdio/SSE/Streamable HTTP)の切り替えもAIエディタ経由だとやや面倒です。

本記事では、自作したMCPサーバーをターミナル上でクライアントを使って動作確認(デバック)する方法を紹介します。

EPSS MCPサーバーの紹介

本記事で使用するのは 脆弱性情報を取得する 自作のMCPサーバーです。

できること:

  • 脆弱性情報(説明、CWE、CVSSスコア)を取得
  • EPSSスコアとパーセンタイルを取得(複数のCVE指定も可)
  • EPSS時系列データを取得
  • EPSSの上位N件を取得
    NVD APIEPSS API を使用しています

ベースのコードは EPSS-MCP を参考にしました。

脆弱性情報取得の実行例

以下は Windsurfで SWE-1(free) モデルを使用した際の実行例です。

「あの脆弱性何だっけ?」風に脆弱性情報を取得してみます。

私:  「ADの権限昇格の脆弱性あったよね。なんだっけ?」

応答:

↓ Web検索と知識ベースからの情報提示(LLM単独での応答)」

demo1.png

私: 「一覧で概要を知りたい」

応答:

↓ ツール(get_cve_info) を使用して 各CVEのCVSS,EPSSスコアを表示

demo2.png
demo3.png

私: 「Zerologon の EPSS 履歴を見せて」

応答:

↓ ツール(get_cve_info) を使用して EPSSの履歴を表示

demo4.png
demo5.png

うまく動けば良いのですが、AIエディタ経由では出力が期待通りでない場合、生の応答に問題があるのか、LLMの生成に問題があるのか等、デバッグが難しいと思うことがあります。

MCPクライアントの実装

MCPサーバーの動作検証やデバッグ1 目的で各トランスポート対応(SSE2, Streamable HTTP)のMCPクライアント用意しました。

MCPクライアント の実装は以下のサイトを参考にさせてもらいました。コードはほぼそのまんまです。

コードの簡単な解説

client.py では ユーザーからのクエリを Chat UIが LLM に送信し、MCPクライアント経由でツールリストを取得したり、選択したツールの呼び出しを行います。

ここでは主要な処理 connect_to_server()process_query() のコードの概要を記載します。

使用モデル : gemini-2.0-flash

MODEL_NAME = "gemini-2.0-flash"
MAX_TOKENS = 1000

MCP接続の初期化

async def connect_to_server(self, server_script_path: str):
    stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
    self.stdio, self.write = stdio_transport
    self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
    await self.session.initialize()
    ...

ツール取得

  • MCPクライアント経由でMCPサーバーの利用可能なツール情報を取得します
async def process_query(self, query: str) -> str:
    """Process a query and available tools"""
    ..
    response = await self.session.list_tools()
    available_tools = [
        {
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "parameters": tool.inputSchema,
            },
        }
        for tool in response.tools
    ]

LLM に問い合わせ

  • LLMにユーザーのクエリmessagesと、利用可能なツールの一覧をchat.completions.create()で渡します
  • LLMはどのツールを使うかを判断して返してきます(response)
    ..
    response = self.openai.chat.completions.create(
        model=MODEL_NAME,
        max_tokens=MAX_TOKENS,
        messages=messages,
        tools=available_tools,
    )
    ..

ツール実行

  • LLM が選定した tool_call に従って、MCPクライアント経由で MCPサーバーのツールを実行します
  • 実行結果を再度LLMにchat.completions.create()で渡します
    message = response.choices[0].message
    ..
    for tool_call in message.tool_calls:
        tool_name = tool_call.function.name
        tool_call_id = tool_call.id
        
        tool_args = json.loads(tool_call.function.arguments)
        tool_result = await self.session.call_tool(tool_name, tool_args)
        ..    
        messages.append(
            {
                "role": "tool",
                "tool_call_id": tool_call_id,
                "name": tool_name,
                "content": json.dumps(tool_result_contents),
            }
        )
        ..
        response = self.openai.chat.completions.create(
            model=MODEL_NAME,
            max_tokens=MAX_TOKENS,
            messages=messages,
            tools=available_tools,
        )
       ..

各Transport の実装の違い

client.py(studio), client_sse.py, client_http.py(Streamable HTTP) の3つのクライアントを用意しました。Transportにおける主な違いはパッケージの importconnect_to_server の実装部分のみです。

Studio(client.py)

from mcp.client.stdio import stdio_client

async def connect_to_server(self, server_script_path: str):
..
    stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
    self.stdio, self.write = stdio_transport

Streamable HTTP(client_http.py)

from mcp.client.streamable_http import streamablehttp_client

async def connect_to_server(self, url: str):
    streamable_transport = await self.exit_stack.enter_async_context(streamablehttp_client(url))
    self.stdio, self.write, _ = streamable_transport

SSE(client_sse.py)

from mcp.client.sse import sse_client

async def connect_to_server(self, url: str):
    sse_transport = await self.exit_stack.enter_async_context(sse_client(url))
    self.stdio, self.write = sse_transport

MCPクライアントを使った動作確認例

ご自身のMCPサーバーで確認することを想定しています。あくまで例としてご参照ください。

MCPクライアントとMCPサーバーの準備

# リポジトリをクローン
git clone https://github.com/kodamap/epss_mcp/
# uv インストール(ない場合)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 仮想環境を準備
uv sync
. .venv/bin/activate

クライアントChatが連携する LLM(gemini-2.0-flash)を使うには、Gemini API Keyが必要です。

export GOOGLE_API_KEY=<your api key>

ターミナル上で動作確認

ターミナル上で各Transportの動作を確認します。

Studio

プロンプト:「CVE-2025-33053 のEPSS履歴教えて」

# client の引数として MCPサーバーのプログラムを指定
$ uv run client/client.py epss-mcp/epss_mcp.py

Connected to server with tools: ['get_cve_info', 'top_epss_cves']

MCP Client Started!
Type your queries or `quit` to exit.

Query: CVE-2025-33053 のEPSS履歴教えて

> Thinking...
> Thinking...

 [Calling tool get_cve_info with args {'time_series': True, 'cve_id': 'CVE-2025-33053'}]
CVE-2025-33053のEPSS履歴は以下の通りです。

| date       | epss      | percentile |
|------------|-----------|------------|
| 2025-07-09 | 0.417630000 | 0.972670000 |
| 2025-07-08 | 0.417630000 | 0.972640000 |
| 2025-07-07 | 0.417630000 | 0.97265000 |
| 2025-07-06 | 0.37155000 | 0.96976000 |

Streamable HTTP

サーバーを起動

# --transport http を付けて起動
$ uv run epss-mcp/epss_mcp.py --transport http
INFO:     Started server process [66074]
INFO:     Waiting for application startup.
[07/10/25 17:30:16] INFO     StreamableHTTP session manager started                    streamable_http_manager.py:112
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

クライアントを起動(サーバーは起動したままターミナルの別窓開く)

プロンプト:「CVE-2025-33053 のEPSS取得して」

$ uv run client/client_http.py http://localhost:8000/mcp

Connected to server with tools: ['get_cve_info', 'top_epss_cves']

MCP SSE Client Started!
Type your queries or `quit` to exit.

Query: CVE-2025-33053 のEPSS取得して

> Thinking...
> Thinking...

 [Calling tool get_cve_info with args {'cve_id': 'CVE-2025-33053'}]
CVE-2025-33053のEPSSスコアは0.41763、パーセンタイルは97.267です。

SSE(Server-Sent Events)

サーバーを起動

# --transport sse を付けて起動
$ uv run epss-mcp/epss_mcp.py --transport sse
INFO:     Started server process [67653]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

クライアントを起動(サーバーは起動したままターミナルの別窓開く)

プロンプト:「EPSSの上位5件を取得して」

$ uv run client/client_sse.py http://localhost:8000/sse

Connected to server with tools: ['get_cve_info', 'top_epss_cves']

MCP SSE Client Started!
Type your queries or `quit` to exit.

Query: EPSSの上位5件を取得して

> Thinking...
> Thinking...

 [Calling tool top_epss_cves with args {'top_n': 5}]
上位5件のCVEは以下の通りです。
CVE-2023-42793, EPSSスコア: 0.94584
CVE-2024-27198, EPSSスコア: 0.94577
CVE-2023-23752, EPSSスコア: 0.94532
CVE-2024-27199, EPSSスコア: 0.94489
CVE-2018-7600, EPSSスコア: 0.94489

まとめ

MCPサーバーをAIエディタ経由で使うだけでは、ツールの呼び出しやレスポンスの中身を細かく検証するのは難しい場面もあります。

クライアントの中で ChatをUIとしてLLMを使用しているので、MCP単体ではなくLLMとも連携して確認できるのが便利かなと思います。

MCPサーバーの動作確認やデバッグのお役に立てば幸いです。

  1. MCPサーバーのデバックには MCP Inspector があるんですね。執筆時点で知らず。。

  2. SSE(Server-Sent Events) はDeprecated になっています。Windsurf では、2025/7/10現在 Streamable HTTP はサポートしていないため、 Stdio か SSE を使う必要があります。「Windsurf supports two transport types for MCP servers: stdio and /sse」 7/19 追記 Streamable HTTP 対応してました。「We can also support streamable HTTP transport and MCP Authentication.」
    https://docs.windsurf.com/windsurf/cascade/mcp

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?