MCPを肌で実感。俺がMCPサーバーだ

# TL; DR

- MCPサーバー になって、手動でエージェントへレスポンスを返してみた

# はじめに

流行真っただ中のMCP。凄そうなのは伝わった。何やら「[USBポート的なもの](https://modelcontextprotocol.io/introduction)」らしい。
でも、結局のところ何なのかよく分からん。

――じゃあ、**MCPサーバーになってみますか**。

# 構成

こちらが今回構築する「**人間MCPサーバー**」です。
[公式チュートリアル](https://modelcontextprotocol.io/quickstart/server)にならって、天気を取得するツールです。

![構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/67e3b500-83a6-4305-b13b-a66d953b1fc1.png)

VSCodeのGitHub Copilot Agent（MCPホスト）で天気の質問をすると、MCPクライアントからサーバーへリクエストが送られます。
私（MCPサーバー）の仕事は、リクエストを読み、適切なレスポンスをクライアントへ返却することです。

いっしょに試してみたい方は以下のリポジトリをご利用ください :muscle:

https://github.com/Syuparn/humanmcp

## （補足）

人間の手前に別途MCPサーバーがあり気になった方がいるかもしれません。

MCPサーバーは名前こそ「サーバー」ですが、実体は[子プロセスを起動し、標準入出力を使ってやりとりしています](https://modelcontextprotocol.io/docs/concepts/transports#built-in-transport-types)[^http]。

そのままでは人間が子プロセスの標準入出力を扱うことができないため、ファイルを介してやりとりできるように細工をしています（MCPのプロトコルの本質的な要素ではありません）。

- MCPサーバーは標準入力をそのまま`in.txt` に横流しする
- 人間は `in.txt` を読みレスポンスを `out.txt` に書き込む
- MCPサーバーは `out.txt` に書き込まれた内容を標準出力に横流しする

# MCPサーバーのおしごと

MCPのクライアント、サーバー間のやり取りは以下の流れで行われます（[公式サイト](https://modelcontextprotocol.io/docs/concepts/architecture#connection-lifecycle)より引用）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/c6fb39c1-1558-48a2-96f4-25cc6c468c1c.png)

さあ、エージェントからサーバーが起動されました。仕事のはじまりです！

## 初期化依頼に応えよう
### initialize request

クライアントからリクエストが送られてきました。

```json:リクエスト（読みやすいように改行）
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "roots": {
        "listChanged": true
      }
    },
    "clientInfo": {
      "name": "Visual Studio Code",
      "version": "1.99.2"
    }
  }
}
```

MCPの通信では、[JSON-RPC 2.0という形式が使用されています](https://modelcontextprotocol.io/docs/concepts/architecture#transport-layer)。

https://www.jsonrpc.org/specification

`initialize` メソッドなので、初期化の依頼のようです。

### initialize response
初期化依頼にレスポンスを返しましょう。レスポンスもJSON-RPCにする必要があります。

MCPのレスポンスは以下の形式で作成します。

```ts:レスポンスの仕様
{
  jsonrpc: "2.0";
  id: string | number;
  result?: {
    [key: string]: unknown;
  }
  error?: {
    code: number;
    message: string;
    data?: unknown;
  }
}
```

https://modelcontextprotocol.io/specification/2025-03-26/basic/index#responses

`id` には、リクエストと同じ `1` を指定しましょう。
`intilalize` メソッドの結果として返したい情報は `result` の中に詰めます。こちらも仕様で定義されています。

https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle#lifecycle-phases

```json:レスポンス
{
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "tools": {}
    },
    "serverInfo": {
      "name": "humanmcp",
      "version": "1.0.0",
      "capabilities": {
        "resources": {},
        "tools": {}
      }
    }
  },
  "jsonrpc": "2.0",
  "id": 1
}
```

### initialized notification

レスポンスを返すと、またクライアントから何か送られてきました。

```json:notification
{
  "method": "notifications/initialized",
  "jsonrpc": "2.0"
}
```

`notifications/initialized` はクライアントが `initilize` メソッドのレスポンスを受け取り、初期化が完了したことを伝えるものです。

notificationという名前の通り、単なる通知なのでレスポンスは不要です。

## 使えるツールは？
### request
ほっとしたのもつかの間、次のリクエストが飛んできました。

```json:リクエスト
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}
```

`tools/list` という名前の通り、MCPサーバーのツール一覧をリクエストしています。
サーバーがどんなツールを扱っているか知りたいようです。

https://modelcontextprotocol.io/specification/2025-03-26/server/tools#protocol-messages

### response

というわけでツールの一覧を返しましょう。
今回私は天気の質問に答えようと思っているので、以下の形式のツールとして動作することに決めました。
~~人間MCPサーバーは開発工数0~~

- 名前: `get-japan-forecast`

```ts:リクエスト形式
{
  // 地名
  "location": "string"
}
```

```ts:レスポンス形式
{
  // 天気
  "text": "string"
}
```

この情報をクライアントへ伝えましょう[^list]。

```json:レスポンス
{
  "result": {
    "tools": [
      {
        "name": "get-japan-forecast",
        "description": "Get weather forecast",
        "inputSchema": {
          "type": "object",
          "properties": {
            "location": {
              "type": "string",
              "description": "location name"
            }
          },
          "required": [
            "location"
          ],
          "additionalProperties": false,
          "$schema": "http://json-schema.org/draft-07/schema#"
        }
      }
    ]
  },
  "jsonrpc": "2.0",
  "id": 2
}
```

## メインの質問、今日の天気は？
### エージェントがプロンプトに応じてMCPサーバーへリクエスト

しばらくして、エージェント側では何やら天気についてのプロンプトを受け取ったようです。

> 東京の天気を教えてください。

天気の質問ならMCPツール `get-japan-forecast` が使えそうだと判断したようです。
ユーザーに、MCPサーバーへのリクエスト許可を求めます。

![エージェントがMCPサーバーへのリクエスト許可を求めるダイアログ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/4f2419b0-c55a-4ffa-a038-cd1b1a4b1092.png)

ユーザーから許可が出ました。すかさず `get-japan-forecast` のリクエストが送られてきます。
形式は `tools/list` のレスポンスで伝えたとおりになっています。

```json:東京の天気は？
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get-japan-forecast",
    "arguments": {
      "location": "東京"
    }
  }
}
```

### レスポンスを返す

というわけで、東京の天気を答えましょう。

![雲一つない青空](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/b21c7711-84ed-4d1c-be9c-0181bba339a9.png)


情報は `result.content` の中に記載します。

https://modelcontextprotocol.io/specification/2025-03-26/server/tools#calling-tools

```json:晴れ！
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "sunny"
      }
    ]
  },
  "jsonrpc": "2.0",
  "id": 3
}
```

### エージェントがレスポンスをもとにプロンプトに回答

エージェントは受け取った天気の情報をもとに、ユーザーに回答します。

> 東京の天気は晴れです。

![エージェントがプロンプトに返答](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/03e01655-e0a3-4bd3-aefa-99d128cbdbc0.png)

これにて一件落着です。

## エラーを返したい

またしばらくすると、別のリクエストが送られてきました。

```json:大阪の天気は？
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "get-japan-forecast",
    "arguments": {
      "location": "大阪"
    }
  }
}
```

のぞみに飛び乗っても2時間かかります。困った。
こんなときはエラーレスポンスを返しましょう。

https://modelcontextprotocol.io/specification/2025-03-26/server/tools#error-handling

MCPサーバーでは、不正なリクエストや想定外のエラーが発生した際にエラーを返します。
エラーコードでエラーの理由、メッセージでその詳細を伝えます。

```ts:エラーコード
// https://modelcontextprotocol.io/docs/concepts/architecture#error-handling より引用
enum ErrorCode {
  // Standard JSON-RPC error codes
  ParseError = -32700,
  InvalidRequest = -32600,
  MethodNotFound = -32601,
  InvalidParams = -32602,
  InternalError = -32603
}
```

今回は不正なパラメータの指定 `-32602` を使いましょう。

```json:エラー
{
  "error": {
    "code": -32602,
    "message": "遠い。自分で調べて"
  },
  "jsonrpc": "2.0",
  "id": 4
}
```

エージェントの返答でもエラーが出ました。

> 大阪の天気情報を取得できませんでした。別の方法で調べていただけますか？

![エラー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/2aab7466-0e1a-4a35-81ae-6e9b24335dff.png)

### エラーメッセージで原因を伝える

~~往生際の悪いことに~~ユーザーはまた別の場所の天気を訊いたようです。

```json:北海道の天気は？
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "get-japan-forecast",
    "arguments": {
      "location": "北海道"
    }
  }
}
```

エラーメッセージでもっとはっきり書かないとダメみたいですね。

```json
{
  "error": {
    "code": -32602,
    "message": "東京の天気以外知りません。自分でググって"
  },
  "jsonrpc": "2.0",
  "id": 5
}
```

返答にも反映されたようです。

> 北海道の天気を取得しようとしましたが、情報を取得できませんでした。東京の天気以外は対応していないようです。他に知りたいことがあれば教えてください。

![mcphuman3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/f7858269-bf08-4d30-9cc5-edf086b85864.png)

疲れたのでMCPサーバーは閉店！

# おわりに

以上、実際にMCPサーバーを体験してみた紹介記事でした。こうやって見ると、秘術のように感じていたMCPも意外とシンプルな仕組みで動いていることが分かりました。みなさんも、MCPの中身を覗いてみてはいかがでしょうか？

~~自分のプロンプトに自分で答えていると、子供の頃両手に指人形をはめて遊んでいたことを思い出して空しくなりました~~

# 余談

本記事作成に当たり、実際のMCPサーバーの中身をこっそり確認していました。
~~プロトコルの形式を知りたいだけであればこちらの方が何倍も手軽です~~

https://zenn.dev/syuparn/scraps/6ee8adc1afaf94


[^http]: リンク先にあるように、HTTP通信を利用することもできます。
[^list]: レスポンス形式は含めません。おそらく、サーバー側で決められるので伝える必要が無いからだと思います。