簡単なサンプルで理解する、Model Context Protocol (MCP)

## 概要
昨今MCPが流行っていますが、仕様理解が中々難しい印象です
当該記事では、<ins>MCP Server, Clientを実装したい開発者 (私含め)</ins>が
簡単なサンプルをベースに<ins>MCP Client, Server, LLMの連携イメージ全体像</ins>を掴む事を目指しました

## Model Context Protocol (MCP) とは
#### 概要
MCPは、2024年にAntropic社により[標準化](https://www.anthropic.com/news/model-context-protocol) された`AI <-> 外部データソース連携の為の標準規格`です
MCP以前は、LLMとのツール統合は全てカスタムメイドであり
共通IFという意味で、AI用の「USBポート」を[自称](https://modelcontextprotocol.io/faqs#what-is-mcp%3F)しています
用語はAntropic公式documentが理解しやすいです

https://modelcontextprotocol.io/introduction

#### トレンド
昨今話題のものですと、以下の様なものかと見受けられます
- [Playwright MCP](https://github.com/microsoft/playwright-mcp): LLM経由でのブラウザ自動操作
- [AWS MCP](https://github.com/awslabs/mcp): LLM経由でのAWSリソース分析

これらはMCP Serverです
Cursor, ClineなどがMCP Clientに該当します

#### 利点 (例)
- Modelが持っていない知識をリアルタイムに反映
    - knowledge cutoff対策
- (検索シナリオ等で) コンテキストの節約
    - [As-is] promptに検索対象全件含めるとLLMに与えるコンテキストを浪費していた
    - [to-Be] MCP Server側が指定するschemaに合わせ検索条件を指定、promptに渡す情報を事前にフィルタしコンテキストを節約

#### 機能
MCP Serverは、本来5種類の機能を包括しますが
<ins>一般にMCPとして利用されるものはTools</ins>と考えて差し支えないのが現状かと見受けられます
当該記事では<ins>以降MCP Toolsを中心に解説</ins>をしています

https://modelcontextprotocol.io/clients

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/732bed4f-ccf0-4790-899e-cab5b3a5eca0.png" width="300" /> 

#### IF仕様
MCP Clientから参照するMCP ServerのIF仕様は[公式](https://modelcontextprotocol.io/docs/concepts/architecture)にて予め大方決まっています 
下記(1)にてセッションを確立した後、(2)-(7)ではJSONRPCにて通信を行います
MCP Clientは当該仕様に合わせルーティングを行います

|#|endpoint|プロトコル|機能|
|-|-|-|-|
|1|/session (/mcp)|HTTP|MCP Client - Server間のセッション確立|
|2|[/resources/list](https://modelcontextprotocol.io/docs/concepts/resources#direct-resources)|JSONRPC|resource一覧取得|
|3|[/resources/read](https://modelcontextprotocol.io/docs/concepts/resources#reading-resources)|JSONRPC|resource詳細取得|
|4|[/prompts/list](https://modelcontextprotocol.io/docs/concepts/prompts#discovering-prompts)|JSONRPC|prompt一覧取得|
|5|[/prompts/get](https://modelcontextprotocol.io/docs/concepts/prompts#using-prompts)|JSONRPC|prompt詳細取得|
|6|[/tools/list](https://modelcontextprotocol.io/docs/concepts/tools#overview)|JSONRPC|tool一覧取得|
|7|[/tools/call](https://modelcontextprotocol.io/docs/concepts/tools#overview)|JSONRPC|tool実行|

*注記 = (1)に関してのみ、パスの命名はAntropic仕様に明記されておらずブレがあります

## 作成したもの
理解も兼ねて
- [SUUMO](https://suumo.jp/chintai/kanto/) (賃貸サイト)情報のスクレイピングMCP Server.tool
- 及び簡単なMCP Clientを作成しました

#### 構成
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/49f7e472-0fb9-4bb3-ba0b-2f79ecac5212.png" width="500"/>


例えば、以下の様な、賃貸検索結果画面のURLを指定するとスクレイピングにより賃貸候補リストを取得します
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/119f87df-6b04-451c-814c-9876f4d960b8.png" width="250"/>

```
// prompt例
https://suumo.jp/jj/chintai/ichiran/FR301FC001/
?ar=030&bs=040&ra=013&rn=0220&ek=022007660&cb=0.0&ct=9999999&mb=0
&mt=9999999&et=9999999&cn=9999999&shkr1=03&shkr2=03&shkr3=03
&shkr4=03&sngz=&po1=25&pc=30 から家賃の安い先頭5件を列挙してください
```

#### 成果物
↓ CLIベースにて動作するMCP Clientから、作成したMCP Server.toolを呼び出します
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/02527af1-32b8-4c8f-9229-7365cac18c9e.gif" width="600"/>


#### シーケンス
1. **[User -> MCP Client]** 
「Suumo賃貸検索サイトのURLを含めた指示」をMCP Clientに送信する
2. **[MCP Client <-> LLM (1)]** 
(1)の指示をLLMに転送し、LLMから「MCP Server.tool利用指示」が返却される
3. **[MCP Client <->  MCP Server.tool]**
(2)の指示をMCP Serverに転送
MCP Server.tool内にて、実際に対象のWeb Siteに対しスクレイピングを行い、賃貸情報を取得し返却する
4. **[MCP Client <-> LLM (2)]**
(3)を過去のコンテキストとマージし、再度LLMに送信し、ユーザの欲しい回答を得る
5. **[MCP Client -> User]**
(4)をユーザに返却し、回答とする

#### 注記
<ins>スクレイピングは常識の範囲内でお願い致します</ins>

## Prototype (実装)

https://github.com/tominmin/suumo_mcp_sample

```
L mcp-client (mcp公式ライブラリにてopenaiと接続する簡潔なmcp client)
L mcp-server (fastapi-mcpにて、mcp toolをホスティング)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/1793bdd1-dfc4-458c-bc09-e18f4768343b.png" width="500"/>

1. [start session]
2. [init]: `/tools/list`利用可能なMCP toolsを取得
3. [interaction]: LLMからの返答に`tool_calls`が含まれる場合、`/tools/call`にてMCP Server.toolを利用

#### MCP Client
[modelcontextprotocol](https://github.com/modelcontextprotocol/python-sdk)ライブラリをベースにMCP Clientを作成しています

尚、以下参考とさせていただいております

https://www.ai-shift.co.jp/techblog/5226

https://modelcontextprotocol.io/quickstart/client


#### MCP Server
[FastAPI MCP](https://github.com/tadata-org/fastapi_mcp)は非常に便利です
FastAPIにおける、`app` (server)を`FastApiMCP`でwrapするだけで、MCP Server化します

#### LLM
LLM側は、[OpenAI Chat Competitions API](https://platform.openai.com/docs/api-reference/chat)にて、modelは`gpt-4o`を利用しています

その他、スクレイピング等の内容は、Github Repositoryをご参照ください
(スクレイピングを検証する際は、個人の責任で、常識の範囲内でお願いいたします)


## Prototype (動作)
以下5段階にて、連携を行います
上記Github Repositoryを動作させる際は、README.mdを参照しつつ、以下を実行してください
```
// server
$ cd mcp-server && rye run python src/main.py

// client
$ cd mcp-client && rye run python src/main.py
```


#### 1. User -> MCP Client
MCP Clientに対して、ユーザが指示を入力する

|#|sequence|interaction|
|-|-|-|
|1|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/5102f326-6de0-4e23-acc6-8ad41cacf500.png" width="400"/>|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/4f9c1a38-6ebd-4bb7-b52c-19678480ed07.png)|

#### 2. MCP Client <-> LLM (1)
- ユーザ入力と`/tools/list`にて取得したMCP toolsリストをLLM側に渡します
- LLM側の回答に`tool_calls`が含まれる場合、MCP Clientは指示内容をMCP toolsにルーティングします
    - 尚、指示内容は(3)の通り、`/tools/list`レスポンス内schema定義に沿って<ins>LLM側がjson形式にて指定</ins>
    - `function.name` = tool指定
    - `function.arguments` = 指示を記載

|#|sequence|interaction|
|-|-|-|
|2|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/5e9593b4-9c4f-435a-b7bc-3b3fecd9533f.png" width="400"/>|`available_tools`にMCP toolsリストを含める![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/d959c40b-6c99-4f7b-9ba8-11a28f1254f3.png)|
|3|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/5a856ebf-7206-4a84-9851-6a96f12ca502.png" width="400"/>|response![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/7a56e64a-c944-4d66-8b86-2cc690449ab6.png)|

#### 3. MCP Client <-> MCP Server.tool
- (3)の指示を用いてMCP toolに対し`/tools/call`します
- MCP tool側の回答内、`content`に結果が含まれます

|#|sequence|interaction|
|-|-|-|
|4|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/4a3808c5-414d-4548-9012-9a4cbfe96eeb.png" width="400"/>|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/d6248523-058a-4642-b471-e97ef34ac78e.png)|
|5|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/00db537e-295f-4ae8-b532-9f1660495606.png" width="400"/>|response![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/eebdc9e4-6f9b-41b1-816e-f63e41fe6c87.png)|

#### 4. MCP Client <-> LLM (2)
- (5)の`content`内容を整形し、元のcontextと合わせて再度LLM側に渡します
- MCP tool結果を踏まえたLLM最終結果が返されます
    - 今回は`finish_reason='stop'`。`content`に回答があり、`tool_calls=None`です

|#|sequence|interaction|
|-|-|-|
|6|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/c66947be-d17d-4a11-9779-aa5f365a224a.png" width="400"/>|request context![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/95f5ab08-88f6-4cf1-9540-444da3cc55af.png)↑ `元のcontext`+`tool回答結果` = `messages`![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/6bd7e53e-c8f1-4810-acf8-8e13e215187b.png)|
|7|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/4983db82-2997-4a6f-9a82-afd66826f313.png" width="400"/>|response。`content`に回答あり![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/06e6d6c9-7369-4e8d-a8fd-9de671ddc305.png)|

#### 5. MCP Client -> User
(7)から最終結果を取り出し、ユーザに返却します

|#|sequence|interaction|
|-|-|-|
|8|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/aa6bb533-354e-4dc3-bcd1-b5980ac18ab0.png" width="400"/>|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/a785072d-6d5a-4651-baf0-a2b7a87908e8.png)|

## 総括

非常に進化が早い分野ですので、現時点のご参考までに。といった形です
今回は簡易的な解説でしたが、今後は、JSONRPC周りなど、より詳細な記事を書けたらと思います

<ins>以上</ins>