VS-Code の GitHub Copilot の Agent モードで単純なMCPサーバーを試す

## はじめに
MCPサーバ初心者です。まだ何も分からないです。

公開レポジトリから、GitHub Copilot が無料で使えるので、感覚をつかむ意味で簡単なサンプルを作りました。これから MCPサーバを試そうと考えている人向けの記事です。

コードはここで公開しています。 cloneすれば、この記事の「実行例」にある手順からすぐに試せると思います。

* [mcp-gh-copilot-starter-py](https://github.com/unmyr/mcp-gh-copilot-starter-py)

## 構成

* 言語: Python
   * FastMCP
       * mcp >= 1.9.2
* MCP
   * type: stdio 

## インストール

1. `mcp` パッケージを導入します

    ```shell
    python -m pip install -r requirements.txt --user
    ```

    * requirements.txt

        ```
        mcp >= 1.9.2
        ```

## MCPサーバの作成
今回はこんな感じのコードです。Copilotで生成したコードです。
`stdio` なのでJSON-RPCの形式で要求を送ると、結果をJSONで返してくれます。

```python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("simple-mcp-server")


@mcp.tool(name="add", description="Add two numbers")
async def add(a: int, b: int) -> int:
    return a + b


@mcp.tool(name="subtract", description="Subtract two numbers")
async def subtract(a: int, b: int) -> int:
    return a - b


if __name__ == "__main__":
    mcp.run(transport="stdio")
```

見た目、APIサーバみたいなコードで、実際そうです。通常のAPIサーバと異なるのは、 `description` の部分が重要な意味をもつことですね。Agentモードで言語モデルが description の内容をもとにどのツールのどのメソッドが問題解決に役立つか、ここの文章で判断します。

### GitHub Copilot側の設定

`.vscode/mcp.json` に設定を入れると VS-Code から読み取ってくれます。
`${workspaceFolder}`の変数を使うことで、絶対パスをコードに埋め込まないで済みます。

```json
{
  "inputs": [],
  "servers": {
    "simple-mcp-server": {
      "command": "python",
      "type": "stdio",
      "args": ["${workspaceFolder}/src/server.py"],
      "env": {}
    }
  }
}
```


### 実行例

1. Copilotを開いて、 `Agent` にモードを切り替えます  
![chat-01-set-mode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326797/0b37c2ed-8b97-4b8a-b3c0-94a60ef06628.png)

2. Select Tools... を見るとツールが読み込めているかどうかが分かります
  ![chat-02-show-my-mcp-server.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326797/7c196ea5-24ac-41e4-8564-973063a3ad0c.png)

3. 言語モデルがコード中のメソッド名を連想しやすいように `add 2 3` と適当に質問を入れると、このツールでよいかと聞いてくれました  
![chat-03-ask-add-2-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326797/cbab5f25-9518-4d65-a57b-c45cc0a6fc7c.png)

4. 実行するとこう  
![chat-04-result-add-2-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326797/c0142d4f-22d7-47ec-b83d-ec53630e0744.png)

簡単でしょ？

### おまけ: 単体テスト
単体テストをやりたくなって、知識がない状態から試行錯誤で試しました。

最初は、こんな風にできるのかな？と思ったらできなかった。

```shell
echo '{"method": "add", "params": {"a": 2, "b": 4}, "id": 1}' | python src/server.py
```

まず、JSON-RPCの作法で入力を流し込む必要がある。そして、まず初期化のリクエストが完了している必要がある。

それに、同期して応答が返る訳ではないようで、リクエストを送って、標準入力に何もないので echo や cat で入力を流し込んでも、テストプログラムは応答を受け取る前に終了してしまう。

……というわけで、 最終的には `expect` を使って動かしたコードがこんな感じです。
多分、こんなマニアックなことをせずにテストできるツールはあるでしょうけど、どういう仕組みかが気になって仕方なかったので、これで満足です。

```shell
#!/usr/bin/expect -f
set timeout 10

set t0 [clock seconds]
spawn python src/server.py
send -- "{\"jsonrpc\": \"2.0\", \"id\": 0, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2024-11-05\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"whatever\", \"version\": \"0.0.0\"}}}\r"
expect \"result\"

send --  "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\",\"params\":{}}\r"
set t1 [clock seconds]

set elapsed_time [expr {$t1 - $t0}]
puts "Elapsed time for Initialize: $elapsed_time seconds"

send -- "{\"jsonrpc\": \"2.0\", \"id\": 0, \"method\": \"tools/list\", \"params\":{}}\r"
expect \"result\"

send -- "{\"jsonrpc\": \"2.0\", \"id\": 0, \"method\": \"tools/call\", \"params\":{\"name\": \"add\", \"arguments\": {\"a\": 1, \"b\": 3}}}\r"
expect \"result\"
set t2 [clock seconds]

set elapsed_time_initialize [expr {$t2 - $t1}]
puts "Elapsed time for call: $elapsed_time seconds"

exit
```