2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

N.Mです。

数年前に自作パズルゲームを自動でプレイするプログラムを作ろうとしたことがありましたが、パズルを解くアルゴリズムで詰まり断念しました。ClaudeのようなAIエージェントが発展する今、AIエージェントがウィンドウを見たり、キーボード操作をできたりするようにすれば、ゲームをプレイさせられるのではと思いました。そこで勉強がてらAIに聞きながら、ウィンドウをキャプチャしたり、キーボード入力をしたりするローカルMCPサーバーを作りました。1

今回の開発の前提

Claude Desktopで使用できるローカルのMCPサーバーを目指しました。Claudeは無料版を使っています。

MCPサーバーの情報のやりとりの方式としてプロセスの標準入出力を使う方式 (STDIO) とHTTP通信を使う方式 (SSE, Streamable HTTP)があります。Claude DesktopでHTTP通信の方式を使用する場合、HTTPSで通信できるようにする必要がありそうで、SSL証明書関連の準備が面倒だったので、標準入出力を使う方式にしました。2

Windows11で動かす想定で、ウィンドウのキャプチャにはWinRTのWindowGraphicsCaptureAPIを使用します。MCPサーバーもそれに合わせてC++で開発しようと思いました。調べたらhkr04/cpp-mcpというリポジトリにC++の簡単なローカルMCPサーバーの実装があり、MITライセンスで公開されていたので、それをベースに実装しました。

この記事で触れること

  • 標準入出力でやり取りするMCPサーバーをC++で作る方法
  • MCPサーバーのデバッグ方法
  • MCPサーバー作成中にあったトラブル

MCPサーバーがどういうものかといったことや、MCPサーバーの仕様の詳細、ウィンドウキャプチャやキーボード操作についてはこの記事では触れません。

作ったもののリポジトリ

WindowControlMCP

ビルドすることで、ローカルMCPサーバーのプロセスを起動するexeファイルが作成されます。

Claude Desktopへの登録方法

Claude Desktopの設定画面にある「開発者」のタグを選ぶと、「設定を編集」というボタンが出てきます。これを押すとclaude_desktop_config.jsonのあるフォルダがエクスプローラーで開きます。claude_desktop_config.jsonにあるmcpServersの設定に以下のように追記することで、Claude Desktopを再起動した際に作ったmcpサーバーが読み込まれるようになります。

  "mcpServers": {
    "WindowControlMCP": {
      "command": "path/to/WindowControlMCP.exe",
      "args": []
    }
  },

実行結果

claudeResult.png

今回作ったMCPサーバーで自作パズルゲームのヘルプ画面を自動で操作させ、その内容を読ませることはできました。

しかし、ゲームプレイについてはAIエージェントからの応答に時間がかかりすぎて、まともにプレイできる感じではありませんでした。3 この部分はいつか応答時間の短いモデルや、ローカルLLMとかでリベンジしたいところです。

C++でのMCPサーバーの実装

MCPサーバーといっても決められたJSONフォーマット(JSON-RPC 2.0)で、AIエージェントからの入力を受け取り、MCPサーバーからの出力を渡すように実装できれば動作するものになります。今回、cpp-mcpがその決められたJSONフォーマットに整形してくれます。そのため、細かいフォーマットを気にせず実装することができました。

標準入出力を受け付けるための改造

ただ、cpp-mcpには標準入出力による方式のサーバーの実装がありませんでした。そのため、cpp-mcpのHTTP通信を使う方式のサーバーの実装をもとに、標準入出力による方式のサーバーを自作しました。

とはいっても、以下のような点でHTTP通信を使う方式よりも簡単に実装できます。4

  • HTTP通信を使う方式ではサーバーに複数のAIエージェント(クライアント)が接続しうるので、セッションでそれらを区別する必要があります。しかし、標準入出力による方式はそれを考える必要がありません。

    • 仕組み上、1つのサーバープロセスに1つのAIエージェントだけが接続するためです。
  • HTTP通信を使う方式では CORS (Cross-Origin Resource Sharing) などの設定が必要です。しかし、標準入出力による方式では不要です。

  • HTTP通信を使う方式ではエンドポイントが複数あり、そのエンドポイントの数だけ処理が書かれているようです。しかし、標準入出力による方式は標準入出力という1つのエンドポイントの処理だけ書けば十分です。

詳しくはリポジトリにあるmcp_stdio_server.cppを見ていただければと思いますが、やっていることは以下のループを回しているだけです。

  1. 標準入力からAIエージェントのリクエストを受ける
  2. リクエストを解析(パース)する
  3. リクエストに応じた処理を行い、結果を決められたフォーマットに整形する
  4. 2の結果をAIエージェントに標準出力で送る
  5. 1に戻る
mcp_stdio_server.cpp
bool stdio_server::start() {
    LOG_INFO("Starting MCP server: ", name_, " (", version_, ")");

    while (true) {
        // Step 1
        std::optional<json> msg_json = get_message_from_stdin();
        if (!msg_json.has_value()) {
            break;
        }

        // Step 2
        request req = parse_jsonrpc_message(msg_json.value());
        if (req.method == "exit") {
            break;
        }

        // Step 3
        std::optional<json> res = process_request(req);
        if (!res.has_value()) {
            continue;
        }

        // Step 4
        send_jsonrpc(res.value());
    }

    LOG_ERROR("Stopped MCP server: ", name_);
    return true;
}

その他のツールの登録処理やリクエストのパース処理などはcpp-mcpの実装をそのまま使用しました。

ツールの登録

MCPサーバーではAIエージェントが使用できる機能をツールとして提供します。今回作るサーバーには「キーボードを操作する」、「ウィンドウをキャプチャする」などの機能をツールとして追加します。cpp-mcpではツールの情報を管理するmcp::toolとそれらを作るためのmcp::tool_builderが元々あるので、まずはそれらを用いてツールの情報を設定していきます。

WindowControlMCP.cpp

int main()
{
    // 略

    mcp::tool selectCaptureWindowTool = mcp::tool_builder("select_capture_window")
        .with_description("Selects a window to capture as an image via window handle. "
            "You can change the window to capture by calling it with a different window handle. "
            "You can also stop capturing the window by passing null window handle (0).")
        .with_string_param("window_handle", "Target window handle obtained from get_window_handle.")
        .build();

    // 略

    server.register_tool(selectCaptureWindowTool, SelectCaptureWindowHandler);

    // 略
}
  • mcp::tool_builderのコンストラクタの引数でツール名を設定します。

  • with_descriptionメソッドの引数で、そのツールがどういうものであるかの説明を設定します。一応英語で書きました。

  • with_string_paramメソッドの引数で、そのツールの入力に使用する文字列パラメーターを設定できます。第1引数がパラメーター名で、第2引数がパラメーターの説明です。booleanのパラメーターの場合にはwith_boolean_paramを、数値のパラメーターの場合にはwith_number_paramを使用します。

これらの設定後、buildメソッドを呼び出すことでmcp::toolのオブジェクトができます。サーバーのregister_toolで、このmcp::toolとハンドラ(上記ではSelectCaptureWindowHandler)を設定することで、MCPサーバーにツールを登録できます。

ハンドラはmcp::jsonを引数に取る関数で、この引数はAIエージェントから入力されるパラメーターが入ります。AIエージェントがツールを使用する際、引数のパラメーターをもとにツールの処理を行い、最後結果をJSON形式で返すように記述します。SelectCaptureWindowHandlerの場合は、キャプチャ対象のウィンドウのハンドルを文字列パラメーターで受け取り、対象のウィンドウがキャプチャされる状態にして、AIエージェントに"Success"の文字列を返すようにしています。

引数のパラメーターに必須のものが入っていない場合などで、cpp-mcpに元々あるmcp::mcp_exceptionを投げることで、AIエージェント側にエラーのレスポンスを返すことができます。サーバー側に例外処理があり、mcp::mcp_exceptionの内容をもとにJSON形式のレスポンスを作り、標準出力に送信してくれます。

WindowControlMCP.cpp

mcp::json SelectCaptureWindowHandler(const mcp::json& params) {
    if (!params.contains("window_handle")) {
        throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'window_handle' parameter");
    }

    std::string handleStr = params["window_handle"];
    HWND targetHandle = reinterpret_cast<HWND>(std::stoull(handleStr));
    g_captureObj.SetTargetWindowForCapture(targetHandle);

    return {
        {
            {"type", "text"},
            {"text", "Success"}
        }
    };
}

今回作ったMCPサーバーでは、上記のツール以外にキャプチャされた画像の取得などを追加していますが、同じような方法で登録しています。

MCPサーバーのデバッグ

作ったMCPサーバーをデバッグするのにmodelcontextprotocol/inspectorが便利でした。あらかじめnodejsをPCにインストールしていれば、以下のコマンドですぐに使用できます。

npx @modelcontextprotocol/inspector

コマンドを実行するとブラウザが立ち上がり、以下のような画面が出てきます。

mcpInspector.png

左の部分にある "Transport Type" でSTDIOを、 "Command" で作ったMCPサーバーのexeファイルのパスを指定します。最初は左下に "Connect" ボタンがあるので、押すとコマンドに従ってMCPサーバーのプロセスが起動します。

真ん中で "Tools" タブを選ぶと "List Tools" のボタンがあります。正常にツールがMCPサーバーに登録されていれば、押すことでMCPサーバーのもつツールの一覧が表示されます。この一覧からツールを1つ選ぶと右側にそのツール用のUIが表示され、登録した入力パラメーターが設定できるようになります。 "Run Tool" ボタンを押すとそのツールを実行し、ツールから出力された結果を見ることができます。

開発中にあったトラブル

Claude Desktop起動時の警告エラー文

このMCPサーバーを作り、Claude Desktopに登録したはじめの頃、Claude Desktop起動時に毎回Error from MCP server: ZodErrorと長いエラー詳細の情報が表示されていました。調べるとMCPサーバーが期待されるJSONのフォーマット (JSON-RPC 2.0) に従っていないメッセージを標準出力で送っているとそのようなエラーが出るようです。

自分の場合は、MCPサーバーがnotificationsのリクエストを受け取った際に空のJSON {} を送るようになっていたのが原因でした。AIエージェントからnotificationsのリクエストが来ることがあるますが、MCPサーバーに通知するもので特にMCPサーバーから返すことが無ければ、メッセージを送らないのが正解のようです。

標準入出力で通信するMCPサーバーでは、標準出力にはMCPサーバーの正しいJSONフォーマットに従ったメッセージしか送ってはいけません。ログなどは標準エラー出力に送る必要があります。cpp-mcpに元々あったログ用のマクロは標準エラー出力に出すものだったので、そのまま使用できました。

まとめ、感想

MCPサーバーの実装は決まったJSONのフォーマットに従ってメッセージを送らないといけないというところが大変そうに見えますが、ライブラリなどでフォーマット周りの対応ができていれば、標準入出力でやり取りする仕組みやツールの登録自体はシンプルだと思いました。(ツールの説明文なども英語といった自然言語が使えますし)

2026年7月現在MCPのSDKについて、C++用の公式のものはありませんが、すでに公開されているリポジトリを利用すれば、普通に実装できたという話でした。

  1. ウィンドウをキャプチャするMCPサーバー自体はすでにインターネット上にあるので、使うだけならそれらをダウンロードするのが早いと思います。

  2. 少なくともGUIからMCPサーバーを設定する場合はHTTPSである必要がありそうでした。設定JSONファイルに入力する方法だと違うのかもしれませんが、試していません。

  3. Claude Sonnet 4.6で工数低の設定で実行しましたが、特に自作パズルゲームに1分の制限時間を設けていたこともあって、制限時間以内に操作することができませんでした。

  4. cpp-mcpにあるサーバーのC++コードは約1450行ありましたが、今回のmcp_stdio_server.cppは約500行に収まっています。

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?