Claude 学習
コースガイド

Introduction to Model Context Protocol — 日本語学習ガイド

Anthropic Academy コース「Introduction to Model Context Protocol」の日本語学習ガイド。講師 Stephen Grider による各レッスンの動画字幕(公式トランスクリプト)全文(リポジトリ内 transcripts/combined_transcript.en.md)を主な情報源とし、要約ノート(リポジトリ内 source-notes.en.md)で構成・用語を補いながら、具体例・実演手順・理由づけまで漏らさず盛り込んで再構成した。試験勉強・NotebookLM 動画解説のソースとして使える形にしている。CCA の「ツール設計&MCP統合」ドメインの中核教材であり、building-with-the-claude-api の「04. ツール使用」を土台とする内容になっている。

このコースを通して、講師は「GitHub のデータについて Claude とチャットできる CLI チャットボット」という一つのサンプルアプリを一貫して作り続け、MCP の各要素(tools・resources・prompts)を実際に手を動かしながら学べるように構成している。以下のガイドもそのストーリーラインに沿って整理する。


0. コースの概要(Welcome to the course)

講師の Stephen Grider が、このコースで扱う範囲を最初に説明する。流れは次の通り。

  1. まず MCP の全体像を掴み、MCP がどんな問題を解決しようとしているのかを理解する。
  2. 次に MCP アーキテクチャの基本として、MCP client と MCP server それぞれの責務を見ていく。
  3. そのうえで MCP server を構成する3つの基本要素を順番に学ぶ。
    • Tools — 言語モデルに消費されることを意図した要素
    • Resources — server が client にデータを共有するための要素
    • Prompts — 言語モデルに「型にはまった指示」を与えるための要素

扱う内容は多いため、受講の前提として次の2点が必要になる。


1. MCPの導入(Introducing MCP)

この章では Model Context Protocol(MCP) を扱う。MCP とは、開発者が退屈なコードを大量に書かずに、Claude へ context と tools を供給するための通信レイヤである。

よく見る図とその中身

MCP を学び始めると、client と server という2つの主要な要素を示す図によく出くわす。server の内部にはさらに tools・resources・prompts という名前のコンポーネントが含まれている、という構図である。ここには専門用語が多く出てくるため、講師はこれを理解するために「小さなアプリを実際に作る」という体で説明を進める。

サンプルアプリ: GitHub チャットボット

題材となるサンプルアプリは、ユーザーが自分の GitHub データについて Claude とチャットできる、もう一つのチャットインターフェースである。たとえばユーザーが「自分の全リポジトリにまたがって、開いている pull request は何がある?」と尋ねたとしよう。この場合、Claude は何らかのツールを使って GitHub に問い合わせ、そのユーザーのアカウントにアクセスし、開いている pull request や公開リポジトリなどを確認することになる、というのが期待される挙動である。これは通常、一連のツール群を使って実装することになる。

ここで講師が強調するのは、GitHub には膨大な機能があるという点である。リポジトリ、pull request、issue、プロジェクト、その他にも数え切れないほどの機能がある。完全な GitHub チャットボットを作ろうとすれば、途方もない数のツールを自分たちで書かなければならない。もしこのサンプルアプリを本当に作ろうとしたら、あなたと私(開発者)は、これらすべてのスキーマとすべての関数を書く責任を負うことになる。これは私たち開発者が書き、テストし、保守しなければならないコードであり、非常に大きな労力・負担になる。

開発者に大量の連携コードを保守させることになる、というこの課題こそが、Model Context Protocol が解決しようとしている主要な難点の一つである。

MCP が変えること

MCP は、ツールを定義し実行する責任を、あなたのサーバから「MCP server」と呼ばれる別の何かへと移す。つまり、もはやあなたと私がこのツールを自分で書く必要はなく、代わりにこの MCP server の内部のどこか別の場所で、ツールが書かれ実行されるようになる。

この MCP server は、外部のサービスへのインターフェースだと考えることができる。たとえば GitHub の MCP server であれば、GitHub 固有のデータや機能へのアクセスを提供してくれる。つまり GitHub まわりの膨大な機能を丸ごとラップして、一連のツールという形でこの MCP server の中に詰め込んでおいてくれる、というイメージである。

ここまでで、MCP server について基本的な理解ができたはずだ。MCP server は、何らかの外部サービスに関連する機能を公開する一連の tools へのアクセスを与えてくれるものであり、その利点は、あなたと私がこれらすべてのツールスキーマや関数などを自分で書かなくて済むという点にある。

よくある3つの疑問

MCP server について初めて学ぶとき、多くの人が抱く3つの疑問があるという。

疑問1: これらの MCP server は誰が作るのか? 答えは「誰でも」である。誰でも MCP server の実装を作ることができる。ただし非常によくあるのは、サービス提供者自身が自分たちの公式実装を作るケースである。たとえば AWS が独自の公式 MCP server 実装をリリースし、その中にさまざまなツールを取り揃えている、といった具合である。

疑問2: MCP server を使うのは、サービスの API を直接呼び出すのとどう違うのか? 先ほど見た通り、GitHub のような API を直接呼び出したいなら、自分たちでこのツールを書く必要があり、そうすれば GitHub を直接呼び出せる。では MCP server を挟むことで何が変わったのか。実際に変わったのは、スキーマと関数の実装を自分たちで書かなくてよくなった、という点だけである。つまり MCP server を追加するだけで、私たちは自分の時間をいくらか節約できることになる。

疑問3(というよりよくある批判): MCP と tool use は同じものではないか? これは MCP を正しく理解していない人からよく出てくる批判だという。講師がここまで説明してきた通り、MCP server と tool use は同じものではないが、互いに補完し合う関係にある。 MCP の考え方の核心は、ツールの関数とツールのスキーマを自分で書かなくてよい、という点にある。それは誰か別の人がやってくれることであり、この MCP server の中にラップされている。ある意味では確かに似ている部分もある(どちらもツールの利用について話しているという点で)が、MCP server が本当に語っているのは「実際の作業を誰がやっているか」という点である。もしこの手の批判を見かけたら、それは大抵の場合、MCP が何なのかをよく理解していないことが原因である。


2. MCP クライアント(MCP clients)

MCP の次の要素として investigate するのは client である。client の目的は、自分のアプリケーション(=CLI アプリ自身のバックエンド。MCP の公式アーキテクチャではこれを MCP Host と呼ぶ)と MCP server との間の通信手段を提供することにある。この client は、その server が実装しているすべてのツールへのアクセスポイントになる。

トランスポート非依存(transport agnostic)

MCP はトランスポートに依存しない。これは「client と server がさまざまな異なるプロトコルを介して通信できる」ということを述べた、少し格好つけた言い方である。現在もっともよくある構成は、MCP server を MCP client と同じ物理マシン上で動かすというものである。この2つが同じマシン上で動いている場合、標準入出力(stdin/stdout)を介して通信できる。実際にこのコースの後半で構築するのもこの形である。とはいえ、MCP client と MCP server を接続する方法は他にもある。仕様上、公式に標準化されているトランスポートは stdio と Streamable HTTP の2種類のみであり、WebSocket は仕様に定義された標準トランスポートではない(独自にカスタムトランスポートとして実装すれば理論上は可能、という位置づけ)。

メッセージのやり取り

client と server の間に接続が確立されると、両者はメッセージを交換することで通信する。許可される正確なメッセージの内容はすべて MCP の仕様の中で定義されている。講師がこのコースで特に扱うメッセージ種別は次の通り。

具体的な呼び出し例で流れを追う

ここまでで server と client というアイデアが出てきたが、これらが実際にどう協調して動くのかはまだイメージしづらいはずである。そこで講師は、ユーザー・自分たちが組み立てているホスト(=CLI アプリ自身のバックエンド。MCP アーキテクチャ上の正式名称は MCP Host)・MCP client・MCP server・外部プロバイダとしての GitHub・そして Claude、という多くの要素の間で行われるやり取りを、一つの例を通してかなり踏み込んで解説する。

  1. まずユーザーが「自分にはどんなリポジトリがある?」といった質問やクエリをホストに送信する。
  2. このときホストは Claude にリクエストを送る必要があるが、そのリクエストには Claude がアクセスできるすべてのツールを列挙して含めたい。そこでホストは Claude へのリクエストを送る前に、MCP client と server を経由する小さな寄り道をする。
  3. ホストは「Claude に送るためにツールの一覧が必要だ」と気づき、MCP client にツール一覧の取得を依頼する。
  4. MCP client は list tools request を server へ送り、server は list tools result を返す。
  5. MCP client はそのツール一覧をホストに返す。これでホストは、ユーザーからの元のメッセージと、含めるべきツールの一覧という、Claude への最初のリクエストに必要なものがすべて揃った。
  6. ホストはそのクエリとツール一式を添えて Claude にリクエストを送る。
  7. Claude はツール一覧を見て、「ユーザーの元の質問に答えるには、ツールを呼び出したい」と判断する。そこで Claude は tool use というメッセージパートを含めて応答する。
  8. ホストは Claude がツールの実行を望んでいることに気づく。しかしホストはもはやツールの実行を担当していない。代わりにツールは MCP server によって実行される。そこでホストは、Claude が提供した特定の引数を添えて、MCP client にツールの実行を依頼する。
  9. MCP client 自体はツールを実行しない。call tool request を MCP server へ送るだけである。
  10. MCP server はそのリクエストを受け取り、GitHub へのフォローアップリクエストを行う。ここで実際に、そのユーザーが持つリポジトリの一覧を取得することになる。
  11. GitHub はそのリポジトリ一覧で応答する。すると MCP server はそのデータを call tool result の中に包んで MCP client に送り返す。
  12. MCP client はその結果をホストに渡す。これでホストはリポジトリの一覧を手にしたので、ユーザーメッセージの中にツール結果パートを含めて Claude へのフォローアップリクエストを行うことができる。このツール結果には、Claude が求めていたリポジトリの一覧が含まれる。
  13. これで Claude は最終的な応答を組み立てるのに必要な情報をすべて手にした。「あなたのリポジトリは、〜」といったテキストを書き出し、ホストに送り返し、ホストはそれをユーザーに送り返す。

この流れは確かに複雑であるが、講師がこれをあえて示したのは、後で実際に自前の MCP client と MCP server を実装し始めるときに、これらすべての要素が現れることになるからだという。


3. プロジェクトのセットアップ(Project Setup)

このコースでは、CLI ベースのチャットボットを教材として使う。CLI 自体はすでに動く状態になっており、Claude とチャットすることはできるが、それに紐づく MCP server まわりの追加機能はまだ何もない状態からスタートする。

講師は、通常のプロジェクトでは client か server のどちらか一方だけを実装するのが普通であり、このプロジェクトで両方を実装しているのは、パズルの両側を見てもらうための学習目的である、と念を押している。


4. MCP でツールを定義する(Defining tools with MCP)

いよいよ CLI チャットボット用の MCP server を作り始める。CLI 自体はすでに動作し、Claude とチャットできる状態だが、それに紐づく MCP server の機能はまだ何もない。ここでは、まず2つのツールを持つ MCP server を追加する。

server の実装は、プロジェクトルート直下の mcp_server.py ファイルに置く。講師はあらかじめ基本的な MCP server の骨組みを用意しており、メモリ上にだけ存在するドキュメント群のコレクションを定義し、さらにいくつかの to-do 項目(このファイル内で完成させるべきタスク)を用意している。今回扱うのは、そのうち最初の2項目、すなわち2つのツールを書くという部分である。

なぜ Python SDK が楽なのか

これまでツールを定義する際には、大きな JSON スキーマを書くなど、そこそこの分量のシンタックスが必要だった。しかしこのプロジェクトでは、公式の MCP Python SDK(パッケージ名 mcp)を利用する。公式ドキュメントでは「わずか15行で動く server が書ける」と紹介されており、サーバーオブジェクトの生成自体は mcp = FastMCP("name") の1行で済むなど、手書きスキーマに比べて驚くほど手間が省ける。

この SDK は、ツールの定義も非常に簡単にしてくれる。ツールを定義するには、次のような記述をするだけでよい。これで add_integers という名前のツールが、指定した description と、必須の2つの引数を伴って作られる。こうしたツール定義を一度書いておけば、裏側で Anthropic がツールの JSON スキーマを自動生成してくれ、それをそのまま Claude に渡すことができる。つまり、ツールの定義のような基本的な作業がぐっと楽になる。

ツール1: ドキュメントを読み取る(read_document)

最初のタスクは、ドキュメントを読み取るツールを実装することである。目的はただ一つ、あるドキュメントの名前を受け取り、その内容を返すことである。すべてのドキュメントは docs というディクショナリにすでに格納されている。キーがドキュメントの ID(実質的には名前)、値がドキュメントの内容である。

実装は非常にシンプルで、この文字列の一つを受け取り、docs ディクショナリの中から該当する値を検索し、それを返すだけである。

具体的には、@mcp.tool() という記法でツールを定義する(括弧を省略した @mcp.tool は実行時エラーになるので注意)。ツール名は read_document(description は「ドキュメントの内容を読み取り、文字列として返す」)とし、実際にツールを走らせる際に呼ばれる関数として read_document という名前の関数を定義する。この関数は doc_id という文字列型の引数を受け取り、pydanticField クラス(from pydantic import Field で先頭にインポートしておく)を使って「読み取るドキュメントの ID」という説明を付与する。

関数本体では、まず Claude が存在しないドキュメントを要求してきたケースを処理する。if doc_id not in docs:、つまり指定されたドキュメント ID がこのディクショナリのキーとして見つからない場合には、f"doc with id {doc_id} not found" という f-string を添えて ValueError を送出する。そのチェックを通過したら、return docs[doc_id] としてドキュメントそのものを返す。

これでツールの定義は完了である。ツールの名前、説明、期待される引数、その型、そしてその引数の説明まで指定した。こうしたデコレータや Field 型などはすべてこの Python MCP SDK にまとめて処理され、JSON スキーマを生成してくれる。

ツール2: ドキュメントを編集する(edit_document)

同じ手順を繰り返す。@mcp.tool()edit_document という名前を与え、description は「文字列を新しい文字列に置き換えることで、ドキュメントの内容を編集する」とする。実装関数も edit_document という名前にする(一貫性を保つため)。

引数は3つ用意する。

つまりこのドキュメント編集は、単純な find-and-replace(検索と置換)である。ここでも、まず Claude が実在するドキュメントを求めているかどうかを確認する。if doc_id not in docs: であれば、先ほどと同様に f"doc with id {doc_id} not found" を添えて ValueError を送出する。該当するドキュメントが見つかった場合は、docs[doc_id] = docs[doc_id].replace(old_string, new_string) として old_stringnew_string に置き換える。

これで2つのツール実装があっという間に出来上がった。講師はここで改めて、この MCP Python SDK を使ってツールを定義するほうが、スキーマ定義を手作業で書くよりもはるかに簡単だということを強調している。


5. サーバインスペクタ(The server inspector)

MCP server の中にいくつか機能を組み込んだが、それが本当に動くかどうかはまだ分からない。そこで役立つのが、この Python SDK を使うことで自動的に手に入るブラウザ内デバッガである。これを使えば、server が期待通りに動作していることを確認できる。

起動方法

ターミナルに戻り、Python 環境がアクティベートされていることを確認する(README にアクティベートの正確なコマンドが書かれている)。アクティベートを確認したら、mcp dev に続けて server が定義されているファイル名(ここでは mcp_server.py)を指定して実行する。実行すると、裏側で動く MCP Proxy server がポート 6277 で待ち受けている旨が表示されるとともに、ブラウザで開くべき MCP Inspector 本体のアドレス(デフォルトはポート 6274)が提示される。そのアドレス(http://127.0.0.1:6274 など)をブラウザで開くと、MCP Inspector の画面が表示される(6277 はあくまでバックエンドのプロキシサーバー用ポートであり、これをブラウザで開いても Inspector の画面にはならない)。

ここで講師が念を押しているのは、この Inspector は活発に開発が続いているツールだということである。したがって、この動画を見ている時点での画面は、講師が見せているものとかなり違っている可能性がある。とはいえ、機能面ではおおむね似たようなものになっているはずだという。

画面構成と操作の流れ

画面左手には Connect ボタンがある。これをクリックすると、たった今編集したファイルに対応する MCP server が起動する。Connect をクリックすると、すぐに画面上にいくつかの要素が現れる。まず画面上部のメニューバーに注目する。ここには Resources・Prompts・Tools などのセクションが並んでいる(この画面も動画視聴時点では変わっているかもしれないので、このメニューバーが見当たらない場合は、単に「Tools セクション」を探せばよい)。

Tools をクリックし、続けて List Tools をクリックすると、先ほど作ったツールの名前が表示される。一つをクリックすると、右側のパネルが切り替わる。このパネルを使って、ツールを手動で実行し、期待通りに動作しているかを確認できる。つまり、実際のアプリケーションに配線しなくても、MCP server 単体でライブ開発ができるということである。

実演: read_document ツールのテスト

read_document ツールを使うには、ドキュメント ID を入力するだけでよい。エディタに戻り、docs ディクショナリの中からドキュメント ID の一つ(deposition.md)をコピーし、doc ID として入力して Run Tool をクリックする。すると "Run Tool" の成功と、そのドキュメントの内容が表示されるはずである。実際にエディタ内の該当文字列と見比べれば、まったく同じ文字列であることが確認できる。

実演: edit_document ツールのテスト

同じ手法で、もう一方のツールもテストできる。edit document ツールに切り替え、ドキュメント ID を入力する。old string には置き換えたい単語(講師は打ちやすい単語を選んでいる。大文字小文字は区別されることに注意)を入力し、new string には「report」を入力する。ツールを実行すると成功が返る。ただし、このツールはドキュメントの内容そのものを返さない点に注意が必要である。単にドキュメントを編集するだけである。そこで編集が正しく行われたことを確認するには、再び read document contents ツールに戻り、同じドキュメント ID で再実行すればよい。すると "a report" のように置換後の文字列が反映されているのが確認できる。

このように MCP Inspector を使えば、実際のアプリケーションに server を配線しなくても、実装中の MCP server を非常に簡単にデバッグできる。自分で MCP server を作り始めれば、この Inspector ツールをかなり頻繁に使うことになるはずであり、このコースの中でもこの後もう少し使うことになる。それは、server の開発が順調に進んでいることを確認するためである。


6. クライアントの実装(Implementing a client)

server がある程度形になったところで、今度は少しギアを切り替えて MCP client に取り組む。client は、プロジェクトルート直下の mcp_client.py ファイルに実装する。

作業に入る前に、講師は一つ念押しをしている。典型的なプロジェクトでは、client を使うか server を実装するかのどちらか一方だけを行うのが普通である。今回取り組んでいるこの特定のプロジェクトだけが、両方を行っているに過ぎない(パズルの両側を見てもらうためである)。

なぜこのファイルは大きいのか

このファイル内の MCP client は、単一のクラスから構成されている。中にはかなりの量のコードがあり、先ほど server 側で書いたコードほど見た目がきれいではない。これには理由がある。

このファイルの中では MCP client クラスを作っている。このクラスは client session と呼ばれるものをラップしている。client session とは、MCP server への実際の接続そのものであり、MCP Python SDK の一部である。つまりこの session こそが、外部の server へのこの接続を私たちに与えてくれるものである。

この session には、多少のリソースクリーンアップが必要になる。つまり、プログラムをシャットダウンするときや、もう server が不要になったと判断したときには、多少のクリーンアップ処理を通さなければならない。講師はすでに、そのクリーンアップコードの多くを MCP client クラスの中に書き込んである。このクラスが存在する理由は、まさにそのクリーンアップを少し楽にするためである。そのクリーンアップコードの一部は、Connect 関数の中や、少し下にある cleanup、__aenter____aexit__ 関数の中に見られる。つまり client session をそのまま直接使うのではなく、こうしたリソース管理をまとめて面倒を見てくれる、より大きなクラスでラップしておくのが非常に一般的な実践だということである。

client は何をしてくれているのか

次に明らかにしたいのは、この client がそもそも何のために存在するのか、つまり client は私たちのために実際に何をしてくれているのか、という点である。思い出してほしいのは、先ほど見たあのフローである。あの中で、私たちはある時点で、Claude に送るためのツール一覧が必要だった。そしてその後、Claude から要求されたツールを実行する必要もあった。MCP server に問い合わせてこのツール一覧を取得したり、ツールを実行したりする際に使われるのが、この MCP client である。つまりこの client は、server に属する何らかの機能を、コードベースの残りの部分に公開しているとイメージできる。

このプロジェクトの core ディレクトリの中には、すでに講師が用意した多くのコードがあり、この MCP client クラスを利用している。つまり別の場所にあるコードが、list_toolscall_toollist_promptsget_prompt といった、このクラス内のさまざまな関数を呼び出すことになる。

このレッスンでは、そのうち list_toolscall_tool の2つの関数の実装に絞って取り組む。先ほどの図で見た通り、この2つの関数は、コードベースの別々の箇所で、Claude に提供するツール一覧を取得したり、Claude がツールの呼び出しを要求してきたときに実際にそのツールを呼び出したりするために使われる。

list_tools と call_tool の実装

この2つの関数の実装は、とてもシンプルで簡潔である。

list_tools から始める。to-do を削除し、result = await self.session.list_tools() のように、session に対して組み込み関数を呼び出す。そして return result.tools とする。これは、MCP server への実際の接続である session にアクセスし、その server が実装しているすべての異なるツールの定義や一覧を取得する組み込み関数を呼び出している、ということである。結果を result として受け取り、その中の tools だけを返す。

同様のやり方で call_tool も実装できる。return await self.session.call_tool(tool_name, tool_input) とする。ここでも session(server への接続)にアクセスし、渡された名前のツールを、Claude から提供された入力パラメータ(引数)とともに呼び出そうとする。

動作確認

この2つの関数を早速テストするために、ファイル下部に用意されているテスト用のハーネスを使う。この mcp_client.py を直接実行できるようにするテストブロックがすでに用意されており、これを実行すると MCP server への接続が形成され、いくつかコマンドを実行してその結果を見ることができる。なお、Uvicorn を使っていない場合はコマンドと引数を変更する必要がある旨のコメントがコード中にあるので、該当する場合はそのコメントを確認する必要がある。

このテストブロックの中に、result = await _client.list_tools() として結果を出力するだけのテストコードを追加する。これにより、MCP server のコピーが起動し、そこに定義されているすべてのツールの一覧を取得しようとし、その結果を出力するはずである。

ターミナルに戻り uv run mcp_client.py(Uvicorn を使わない場合は python mcp_client.py)を実行すると、ツール定義の一覧が表示される。ここには、先ほど作った read_document ツールと edit_document ツールがそれぞれ、description と入力スキーマとともに表示される。これが Claude に最終的に渡されることになるツール定義である。

CLI から実際に Claude にツールを使わせる

ここまでで、ツール一覧を取得して Claude に渡す機能と、MCP server が実装しているツールを呼び出してその結果を Claude に渡す機能を実装した。この機能がすでに実装されているので(list_tools と call_tool を呼び出す部分のコードはプロジェクトの他の場所にすでに用意されている)、今度は改めて CLI を実行し、Claude にこれらのツールを使わせてみることができる。つまり、あるドキュメントの内容を調べさせたり、ドキュメントを編集させたりできるようになる。

MCP server の中には ID が report.pdf のドキュメントがあり、その中身は「20メートルのコンデンサータワー」といった趣旨のテキストになっている、という点を思い出してほしい。ターミナルに戻り uv run main.py でプロジェクトを実行し、Claude に「report.pdf ドキュメントの中身は何?」と尋ねる(ここでは正確に report.pdf と入力する必要がある)。これを実行すると、リクエストと一緒にツール一覧が送られ、Claude は read_document ツールを使うことを選択し、そのドキュメントの内容を取得する。その結果、Claude はそのドキュメントの内容を取得できたことが分かり、「レポートは20メートルのコンデンサータワーについての何かだ」という趣旨の回答が返ってくる。

ここまでで client にいくつか機能を追加した。client は、MCP server の中に実装された機能へのアクセスを提供してくれるものであり、この時点で、server が作成したツールの一覧を取得し、server が実装しているツールを実行できるようになった。


7. リソースの定義(Defining resources)

続いて、MCP server の次の主要機能である Resources に移る。これを理解するために、講師はプロジェクトにさらに別の機能を実装していく。

追加したい機能

ユーザーがメッセージの中に @ 記号と、続けてドキュメント名を入力することで、ドキュメントに「メンション」できるようにしたい。ユーザーがそうすると、自動的にそのドキュメントの内容を取得し、Claude に送るプロンプトの中に挿入したい。つまりこの機能には大きく2つの側面がある。

  1. ユーザーがメッセージの中に @ 記号を入力したら、自動的にメンションできるすべてのドキュメントの一覧を、小さなオートコンプリートのウィンドウに表示する。
  2. ユーザーがメンションを含むメッセージを送信したら、自動的にそのドキュメントの内容を取得し、Claude に送るプロンプトの中に挿入する。

たとえばユーザーが「@report.pdf ファイルには何が書いてある?」と言った場合、ユーザーからのクエリと、「ユーザーが何らかのドキュメントを参照した可能性があり、そのドキュメントの内容はこれである」という情報を含んだプロンプトを組み立て、Claude に送りたい。ここでの考え方は、report.pdf ファイルの中身を知るために Claude に何らかのツールを使わせるのではなく、ユーザーが先回りしてファイルに言及することで、あらかじめコンテキストを自動的に挿入しておくというものである。

2つの機能、2つのリソース

このデータを MCP server から取得するために使うのが Resources である。Resources は、MCP server が client に何らかのデータを公開するための仕組みである。通常、読み取り操作ごとに1つの resource を定義する。この例では、ドキュメントの一覧を取得する処理と、単一ドキュメントの内容を読み取る処理の2つが必要になるため、結果として2つの別々の resource を作ることになる。一つはオートコンプリートに入れるためのドキュメント名一覧だけを返す resource、もう一つはドキュメント ID に基づいて単一ドキュメントの内容を公開する resource である。

これらの resource は、MCP client を通じてアクセスされることになる。全体のフローとしては、ユーザーが「@ の後に何か書いた」と入力し始めた瞬間、オートコンプリートに表示するためのドキュメント名一覧を表示する必要がある。そこでコードは MCP client に問い合わせ、client は read resource request を MCP server に送る。この read resource request の中には URI と呼ばれるものを含める。これは実質的に、読み取りたい resource のアドレスのようなものである。この URI は、その resource を最初に組み立てる際に定義される。

この read resource request を送ると、MCP server はここに含まれる正確な URI を確認し、対応する関数を実行する。その結果を受け取り、read resource result メッセージの中に入れて返す。私たちはその中のデータを取り出し、オートコンプリートに表示したり、必要な処理を行ったりできる。

2種類の resource: direct と templated

resource には direct(静的、static とも呼ばれる)と templated(テンプレート化)の2種類がある。

想像がつく通り、templated resource は、要求する内容にもう少し選択肢・バリエーション・カスタマイズ性を持たせたい場合に使うことになる。

実装

エディタに戻り、mcp_server.py ファイルの中で「すべてのドキュメント ID を返す resource を書く」というコメントと「特定ドキュメントの内容を返す resource を書く」というコメントを見つける。

1つ目: ドキュメント ID の一覧。ここでのドキュメント ID は、実質的にはドキュメントの名前である。つまり、これらの ID を返すことがそのまま名前を返すことになるので、そのままオートコンプリート要素に入れられる。to-do を削除し、@mcp.resource を追加する。第一引数は、この要素にアクセスするための URI(ルートハンドラのようなもの)である。ここでは docs://documents を使い、さらに MIME タイプとして application/json を指定する。

resource はどんな種類のデータでも返せる(プレーンテキスト、JSON、バイナリデータなど何でもよい)。どの種類のデータを返しているのかを client にヒントとして伝えるのが私たちの役目であり、そのために MIME タイプを定義する。application/json という MIME タイプは、この resource を要求することになる client に対して、「構造化された JSON データを含む文字列を返す」ということを伝えるヒントになる。したがって、そのデータを deserialize(デシリアライズ)して、使えるデータ構造に変換するのは client 側の仕事になる。

デコレータの下には list_docs 関数を書き、文字列のリストを返す。中では list(docs.keys())、つまりディクショナリからキーだけを取り出してリストにしたものを返す。ここで返しているのは明示的な JSON 文字列ではない点に注意する。Python MCP SDK が、返した値を自動的に文字列へ変換してくれる。

2つ目: 単一ドキュメントの内容。コメントを削除し、@mcp.resource("docs://documents/{doc_id}") のように書く。今回はワイルドカードを含む templated resource にする。MIME タイプは今回は少し趣向を変えて text/plain にする。これは単にドキュメントの内容そのものであり、何の構造にも包まない値だからである。

講師はここで、実際のアプリケーションであれば、ドキュメントを読み取るような処理では、ID・内容・著者名・著者 ID などを含んだドキュメントレコード全体(何らかのディクショナリ)を返すのが普通だろう、と補足している。ただしあくまで例として分かりやすくするため、ここではプレーンテキストの返し方を示すために、ドキュメントのテキストだけを返すことにする。

MIME タイプは text/plain にし、関数は fetch_doc とする。doc_id(文字列)を受け取り、文字列を返す。ここでも、URI に書いた文字列がそのままキーワード引数として関数内に現れる。もし doc_type のような追加のパラメータを URI に加えていれば、それも同様に追加のキーワード引数として現れることになる。

関数の中身では、まず要求された ID が実際に存在するかを確認する。if doc_id not in docs: であれば f"doc with id {doc_id} not found" という f-string を添えて ValueError を送出する。そのチェックを通過したら return docs[doc_id] とする。これで完成である。

MCP Inspector での動作確認

再び MCP Inspector で試してみる。ターミナルで先ほどと同じ mcp dev mcp_server.py を実行すると、デフォルトのポート 6274 で MCP Inspector の Web サーバが起動する(裏側のプロキシサーバーはポート 6277)。ブラウザでそのアドレスを開き、Connect をクリックし、Resources を見つける。すると利用可能なすべての resource が一覧表示される。ここで一覧表示されるのは具体的には静的(direct)resource だけであり、docs://documents だけが見えるはずである。resource template のほうは別途一覧表示でき、fetch_doc という resource template が1つあるのが見える。

まず /documents のほうを実行してみて、結果を確認する。これは MCP server から実際に返されるメッセージそのものの構造であり、text というプロパティがあり、その中に、返しているすべてのデータが JSON 文字列としてシリアライズされて入っている。したがって、この text を JSON 文字列からデシリアライズして使える文字列のリストに変換するのは、CLI アプリケーション側の仕事になる。

続けて fetch_doc もテストできる。それをクリックし、doc ID を入力する(講師は report.pdf を読みたいとして入力している)。read resource を実行すると、そのドキュメントの内容が表示されるはずである。ここでも text/plain という MIME タイプが返ってきており、これは「このデータは JSON としてデシリアライズすべきではない」というヒントになっている。


8. リソースへのアクセス(Accessing resources)

MCP server の中に2つの resource を定義したので、次は client 側にこれらの resource を要求する能力を持たせる。そのために、MCP client の中に単一の関数を追加する。この MCP client には、プロジェクトの他の部分から使われることになる機能をこれまでも実装してきた。今回もその一環で、プロジェクトの別の場所にあるコードが、これから追加するこの関数を利用しようとする、という位置づけである。

read_resource の実装

mcp_client.py を再び開き、read_resource を見つける。ここでの目標は、MCP server に対してリクエストを行うことで特定の resource を読み取り、返ってきた内容をその MIME タイプに応じてパースし、得られたデータを返すことである。引数として渡されるのは URI であり、これは server から取得したい resource の URI にあたる。

型をきちんと扱うために、ファイル冒頭に2つの import を追加する。json モジュールと、pydantic から AnyUrl をインポートする。read_resource 関数に戻り、コメントと return 文をクリアする。result = await self.session.read_resource(AnyUrl(uri)) として結果を取得する(AnyUrl を使っているのは、あくまで型をきちんと成立させるためである)。

ここで、なぜこの部分を追加しているのかをはっきりさせておきたい。先ほど Inspector の中で見た応答が、まさにこの result 変数にあたる。result には contents というリストがあり、その中に要素のリストが入っている。私たちが本当に関心を持っているのは、その最初の要素だけである。そこで最初のディクショナリ(result.contents[0])を取得し、その type プロパティと mimeType(キャメルケース)を確認する。特に MIME タイプが欲しいのは、それによって返ってきたデータの種類を把握できるからである。

もしそれが JSON であれば、テキストを JSON としてパースし、その結果を返す必要がある。具体的には、isinstance(resource, types.TextResourceContents) を確認し、その中で resource.mimeType == "application/json"(属性名はキャメルケースの mimeType。スネークケースの mime_type では AttributeError になる)であれば、json.loads(resource.text) を返す。逆に、この if 文に入らずに早期リターンしなかった場合には、単に resource.text を返す。この場合はテキストを何もパースせず、プレーンテキストのまま返すことになる。これはまさに、単一ドキュメントの内容を取得するケースに該当する。

これで read_resource の実装は完了である。ここで書いたコードは、コードベースの他の複数の場所から使われることになる(ドキュメント名の一覧を取得したり、最終的にプロンプトに挿入するためのドキュメントの内容を取得したりするために呼び出される)。この時点で、残りの部分の実装はすでに済んでいるので、基本的にすべてがうまく動くはずである。

動作確認: @メンション機能

ターミナルに戻り、uv run main.py で CLI アプリケーションを再度実行し、このメンション機能が動作するかを確認する。「@ の後に何か」と入力し始めると、resource の一覧が表示され、矢印キーでスクロールできる。気に入った resource のところで space キーを押すと、それが挿入される。「report.pdf ドキュメントには何が書いてある?」という具合である。

これを送信すると、すぐに応答が返り、report.pdf の中身について教えてくれる。今回、Claude はドキュメントの内容を読み取るためにツールを使う必要がなかった、という点が重要である。ドキュメントの内容がプロンプトの中にすでに含まれていて、Claude に送られていたからである。

これで resources について一通り見てきた。resource は、MCP server から何らかの情報を公開するために使うものである。


9. プロンプトの定義(Defining prompts)

MCP server における最後の主要トピックは Prompts である。resources のときと同じように、プロジェクトに小さな機能を実装しながら、prompts が何なのかを理解していく。

追加したい機能: スラッシュコマンド

追加するのはスラッシュコマンドのサポートである。たとえば /format というコマンドを用意したい。ユーザーがスラッシュ(/)だけを入力すると、アプリケーションがサポートするコマンドの一覧が小さなオートコンプリートとして表示される。今回は format というコマンドが一つだけある状態にする。/ だけを入力すると、小さなオートコンプリートが現れ、選択肢は format だけになるはずである。それを選択すると、その後にドキュメント ID(report.pdf などのドキュメント名の一つ)を入力するよう促される。

このコマンドを実行すると、目的は Claude にそのドキュメントを Markdown 記法で再フォーマットさせることである。つまり、現在ドキュメントの中にある、特別な書式が何もついていないただの文字列を Claude に読み込ませ、Markdown 記法で書き直させたい。実行結果としては、「ドキュメントの再フォーマットをお手伝いします」といった出力に続き、Claude がツールを使ってドキュメントの内容を読み取り、最終的な応答の中でそのドキュメントの内容が Markdown 記法で書き直されて表示される、という流れを期待している。

このコマンドの本質: 実は「開発」しなくても動く

ここで講師が指摘する興味深い点がある。この機能の本当の核心(本当のゴール)は、ユーザーがドキュメントを Markdown 記法に再フォーマットできるようにすることであり、これは実は私たち開発者がコードを書かなくても実現できる操作だという。

どういうことかというと、ユーザーはすでに CLI を起動して「report.pdf ファイルを Markdown 記法で再フォーマットして」と言うことができる。これはすでに問題なくできることであり、Claude はそれなりに良い仕事をする。ドキュメントの内容を取得し、Markdown へと再フォーマットしてくれる。実際、講師が試すと完璧に動作している。

では、この機能はいったい何をやろうとしているのか。考え方はこうである。もしこれをユーザーに完全に任せて「これを Markdown に変換して」と各自に手打ちさせた場合、まずまずの結果は得られるかもしれないが、もし私たち MCP server の作者が、この特定のシナリオ(ドキュメントを Markdown に変換する)に特化した、本当に強力で、しっかりテスト・評価されたプロンプトを用意しておけば、ユーザーはもっとずっと良い結果を得られるかもしれない。つまり、ユーザーはこのワークフロー全体を自分でも実行できるが、代わりにこの作り込まれたプロンプトを使えば、その方がずっと良い結果になるだろう、というのが prompts 機能の本当の狙いである。

考え方としては、あらかじめ、自分の server が本当に得意とする分野に合わせてカスタムに調整された一連のプロンプトを、server の中に定義しておくことができる。今回の例では、server はドキュメントの管理・読み取り・編集などを扱っているので、非常に質の高い、評価・テスト済みで、さまざまなシナリオでうまく機能することが分かっているプロンプトを一式追加しておくことになるかもしれない。そうしたプロンプトを、今組み立てている CLI アプリのような、あらゆる client アプリケーションから使えるように公開できる。

ここで一つ補足しておきたいのは、このプロンプトを CLI のコードベースに直接組み込んでしまうことも当然可能だという点である。それも十分にあり得る。しかし、ここでの考え方は、特定のタスクに特化した MCP server が、あらかじめ開発しておく手間をユーザーにかけさせずに使える一連のプロンプトを公開できる、というところにある。

実装

MCP server の中で prompt を定義するには、すでに書いてきた tools や resources とよく似た構文を使う。prompt デコレータを使い、prompt に名前を付け、任意で説明も付与する。そして client がこの prompt を要求してきたら、実際のユーザーメッセージとアシスタントメッセージからなるメッセージのリストを返す。このメッセージをそのまま Claude に送ることができる。

server に戻り、「ドキュメントを Markdown 形式に書き直す」というコメントのところまでスクロールする。to-do を削除し、@mcp.prompt を追加して、名前を format、説明を「ドキュメントの内容を Markdown 形式に書き直す」とする。実装関数として format_document を定義し、引数として doc_id を受け取る(ツールのときと同様、Field を使って「フォーマット対象のドキュメントの ID」という説明を任意で付けられる。型注釈として文字列であることも明示する)。

この関数からはメッセージのリストを返す。そのために、ファイル冒頭、既存の mcp.server.fastmcp の import のすぐ下に from mcp.server.fastmcp.prompts import base を追加する。関数の中では、あらかじめしっかりテスト・評価済みのプロンプトを定義する。このプロンプトは、Claude にドキュメント ID を受け取らせ、暗黙的に read_document ツールを使ってそのドキュメント ID の内容を取得させ、取得したドキュメントを Markdown 記法で書き直させ、最後にその更新内容を server 内に保存するため、edit_document を使って編集させる、という内容になっている。

プロンプトを定義したら、メッセージのリストを返す。具体的には [base.UserMessage(prompt)] のように、先ほど書いたプロンプトを渡した UserMessage を1件だけ含むリストを返す。

動作確認

ファイルを保存し、再び MCP Development Inspector を起動してこの prompt をテストする。ターミナルで同じコマンドを実行し、ブラウザでそのアドレスを開き、server に Connect する。今度は Prompts セクションを見つけ、利用可能なプロンプトを一覧表示する。この時点では format という一つのプロンプトしかない。それをクリックすると、ドキュメント ID を入力するよう求められる。ここでは試しに outlook.pdf を入力し、Get Prompt を実行する。すると、あらかじめ組み立てておいたメッセージの一覧が表示される。中身は1つのメッセージパート(テキストパート)で、先ほど書いた完全なプロンプトがそのまま入っており、ドキュメント ID がその中に補間されているのが確認できる。このメッセージ一式をそのまま Claude に送れば、期待通りの応答が返ってくるはずである。

改めて繰り返すと、MCP server 内に実装するこうした prompts の核心は、十分にテストされ、評価され、特定のユースケースに強く特化したものにする、という点にある。


10. クライアントでのプロンプト利用(Prompts in the client)

最後の主要タスクとして、MCP client 側に、MCP server 内に定義されているすべてのプロンプトを一覧表示する機能と、変数が補間された特定のプロンプトを取得する機能を実装する。

list_prompts と get_prompt の実装

まず list_prompts を実装する。コメントを削除し、result = await self.session.list_prompts() として、return result.prompts とする。ほぼこれだけである。

次に get_prompt である。個別のプロンプトを取得する際には、いくつかの引数を渡すことになる。これらの引数は、最終的にプロンプト関数の中に渡されることになる。たとえば format_document では、ドキュメント ID を受け取ることを想定している。この args ディクショナリの中には document_id(あるいは doc_id)というキーが存在することが期待され、それが対応する関数へ渡され、プロンプトそのものにその値が補間される、という流れである。

get_prompt 関数の中では、self.session.get_prompt(...) から結果を取得する。取得したいプロンプトの名前と、引数を渡す。そして return result.messages とする。これが返ってくるメッセージであり、Claude にそのまま投入したい何らかの会話を形成している。これで client 側の実装は完了である。

動作確認

CLI 自体でこれをテストできる。プロジェクトを再度実行し、/ を入力すると format コマンドにアクセスできることが分かる。format はまさに、これから呼び出そうとしているプロンプトの名前そのものである。それを選択して space キーを押すと、いずれかのドキュメントを選ぶよう求められるので、plan.md を選ぶ。Enter を押すと、そのプロンプト全体(実質的には単一のユーザーメッセージ)がそのまま Claude に投入される。

これで Claude は、ドキュメントを Markdown 記法に再フォーマットするための指示と、再フォーマット対象のドキュメント ID の両方を与えられたことになる。Claude がまず行う必要があるのは、そのドキュメントの内容を取得することであり、read_document ツールを使ってそれを行う。そして最終的に、Claude はこのドキュメントの Markdown 版で応答する。つまり、大量の Markdown 記法が組み込まれたドキュメントが出力される。

prompts のおさらい

うまく動作したところで、prompts について何を理解すべきだったかを簡単に振り返っておく。

  1. まず、自分たちの MCP server の目的に関連性のあるプロンプトを書き、評価することから始める。今回の例ではドキュメントを扱う server を作っていたので、ドキュメントを別のスタイルで書き直すという機能はそれなりに理にかなっている。
  2. プロンプトを組み立てたら、MCP server の中にそのプロンプトを定義する。
  3. すると client はいつでもそのプロンプトを要求できるようになる。プロンプトを要求する際には、いくつかの引数を渡すことができ、それがこのプロンプト関数にキーワード引数として渡される。関数はそのキーワード引数をプロンプト自体の中で利用できる。

11. MCP のレビュー(MCP review)

プロジェクトはすべて完了したが、次に進む前に、これまで学んできた 3つの server プリミティブ(tools・resources・prompts)についてざっと復習しておきたい。特に強調したいのは、それぞれの実行をアプリケーションのどの部分が実際に担っているか、つまり典型的なアプリケーションにおいて、これらをそれぞれ実際に動かしているのは誰であり、その恩恵を受けるのは誰なのか、という点である。

それぞれを制御しているものが何かを強調するのは、その目的をイメージしてもらうためである。もし Claude に何らかの能力を追加したいのであれば、おそらく MCP server の中に tools を実装するか、何らかの server の tools を自分の MCP client を通じて消費することを検討することになる。もし UI にコンテンツを表示するなどの目的でアプリにデータを取り込みたいのであれば、おそらく resource を使いたくなるはずである。そして、何らかの定型化されたワークフローを実装したいのであれば、おそらく prompts に目を向けることになる。

Claude.ai での実例

これらの考え方の実例は、公式の Claude インターフェース(claude.ai)の中にも見ることができる。講師は実際の画面を見せながら、次の3つの例を挙げている。

  1. チャット入力欄の下にあるボタン群。この一つをクリックし、続けて例の一つをクリックすると、すぐにチャットに入っていく。これはユーザー制御のアクションである。ユーザーである自分が、この特定のワークフローを開始することを決めており、あらかじめ書かれ、おそらく何らかの形で最適化されているであろう prompt を利用している。この一連のボタンを実装しようとするなら、おそらく MCP server の中に一連の異なる prompts を組み立てることになるだろう。
  2. 「+」ボタンから「Add from Google Drive」を選ぶ操作。(講師は自分の内部ドキュメントが表示されてしまうため実際にはクリックしていないが)このボタンをクリックすると、コンテキストとしてチャットに追加できるドキュメントの一覧が表示される。ここでどのドキュメントを実際に一覧表示するかを把握し、いずれかをクリックしたときにその内容を自動的にチャットのコンテキストへ挿入する、という処理はすべてアプリケーション関連のコードである。つまり、ここで一覧表示すべきドキュメントの一覧を把握しているのは、もっぱらアプリケーション側であり、まさに UI 関連の要素である。Google Drive からのこうしたドキュメント一覧表示を実装しようとするなら、おそらく MCP server の中に resource を実装することを検討するだろう。
  3. チャットに「What is the square root of 3, use JavaScript to calculate the value」のようなメッセージを送る操作。この場合、明らかに Claude が何らかの JavaScript コードを実行することを期待しており、それはおそらく tool の利用を通じて行われることになる。この場合、tool を使うという判断は 100% モデル制御である。何らかの JavaScript 実行ツールを使うと判断したのはモデル自身である。これを MCP server の中に実装しようとするなら、当然ながら tool を提供することになるだろう。

まとめると、これが3つの異なる server プリミティブであり、それぞれがアプリケーション全体の異なる部分によって利用されることを意図している。tools は主にモデルに仕え、resources は主にアプリに仕え、prompts は主にユーザーに仕えるものである。改めて言えば、これらはあくまで大まかな指針であり、講師がこれらに言及するのは、何かを組み立てようとするときに、それぞれのプリミティブをいつ使うべきかの感覚をつかんでもらうためである。


まとめ(暗記用)

Claude 学習サイト — 完全ローカル静的サイト(build.py で生成)。進捗はこのブラウザの localStorage に保存されます。