← Academy 日本語で学ぶ(一覧へ戻る)

mcp advanced topics

Anthropic Academy コースの日本語ナレッジ(学習用)/ 全 14 項目

Model Context Protocol: Advanced Topics 全文ナレッジ(DA式・日本語)

Anthropic Academy 公式コース「MCP: Advanced Topics」の全レッスンを DA式完全版でナレッジ化。動画ノートは4層(要点整理→実務ポイント→時系列・日本語逐語+画面差し込み→英語逐語)、walkthrough は「ステップ別完全版+実物コード全文+英語原文」。

  • 修了証: https://verify.skilljar.com/c/vz9zss7rf2ud(NAOKI NAKASHIMA / 2026-07-03 / Anthropic Education)
  • 動画10本 計57.5分 + インタラクティブ walkthrough 3本(全17ステップ)+ サーベイ + Assessment 10問全問正答で合格
  • 講師: Stephen Grider / 生成: 署名付き HLS → ffmpeg → mlx_whisper(srt)+ フレーム296枚読解 → 4並列DA式統合 → 2パス検証(ギャップ216枚)+整合性検収(2026-07-03)
  • 演習素材(実物・4プロジェクト): assets/sampling/assets/notifications/assets/roots/assets/transport-http/

目次

Core MCP Features

# ノート 時間 ひとこと
01 Let's get started! 1:28 コース全体像(sampling/notifications/roots → transports)
02 Sampling 5:17 サーバーがクライアント経由で LLM を使う=APIコストをクライアント側に
w1 Sampling walkthrough 6步 create_message → コールバック → CreateMessageResult の実装全景
03 Log and progress notifications 2:41 Context の info/warning/debug/error + report_progress
w2 Notifications walkthrough 4步 logging コールバックは ClientSession へ・progress は call_tool へ
04 Roots 7:55 アクセス許可の意思表示。SDK は強制しない=is_path_allowed を自作
w3 Roots walkthrough 7步 file:// URI・ListRootsResult コールバック・認可パターン

Transports and Communication

# ノート 時間 ひとこと
05 JSON message types 6:45 Request-Result/Notification の2分類と方向(schema.ts 転記)
06 The STDIO transport 9:28 最長回。ターミナル素手で JSONRPC を打つデモ(init 3メッセージ→tools/call→通知→result)
07 The StreamableHTTP transport 6:26 HTTP では server→client が困難=2フラグで progress/logging/sampling が壊れる実演
08 StreamableHTTP in depth 10:28 mcp-session-id・長寿命 GET SSE(回避策の本体)・POST 毎の SSE と自動クローズ
09 State and StreamableHTTP 6:29 水平スケーリング問題 → stateless_http=Truejson_response=True の正体と代償
10 Wrapping up 0:48 次の一歩3つ(GitHub Discussions/modelcontextprotocol.io/自作)

Assessment 10問(完全収録・全選択肢+正答)

Q1. Your MCP server needs to use Claude to summarize data, but you don't want the server to handle API costs. What feature should you use? - Roots / Progress notifications / Logging / ✅ Sampling

Q2. Your MCP tool sends a "Call Tool Request" and expects to get back results. What type of message pattern is this? - Progress message / Notification message / ✅ Request-result message / Logging message

Q3. Your StreamableHTTP server needs to send progress updates to clients, but HTTP doesn't normally allow server-initiated requests. How does StreamableHTTP solve this? - ✅ It creates Server-Sent Events (SSE) connections / It stores messages in a database / It uses WebSockets instead / It switches to stdio transport

Q4. A user asks Claude to "convert video.mp4" but Claude doesn't know where the file is located. What MCP feature helps solve this? - ✅ Roots / Progress notifications / JSON messages / Sampling

Q5. You want simpler HTTP responses without streaming, just getting the final result as plain JSON. Which flag should you enable? - streaming=False / simple_mode=True / stateless_http=True / ✅ json_response=True

Q6. You're developing an MCP server locally and want the simplest way to test communication between client and server on the same machine. Which transport should you use? - HTTP transport / WebSocket transport / StreamableHTTP transport / ✅ Stdio transport

Q7. Which transport method requires both client and server to run on the same machine? - TCP transport / ✅ Stdio transport / WebSocket transport / HTTP transport

Q8. What are roots in MCP? - The main configuration files for MCP servers / Administrative users with full system access / ✅ A system that tells MCP servers what files/folders it can access / The primary communication endpoints between clients and servers

Q9. What is the correct sequence for MCP connection initialization? - ✅ Initialize Request → Initialize Result → Initialized Notification(他3順列は誤り)

Q10. What is sampling in MCP? - A method for collecting data from multiple sources / ✅ A way for servers to access language models through connected MCP clients / A process for validating client credentials / A technique for optimizing server performance

(受験結果: 全問正答で合格 "You have passed!" / 2026-07-03)

逐語データ

  • transcripts/ — ASR 原文 txt+srt(10本)+ walkthrough 全17ステップ原文(walkthroughs.md)
↑ 目次へ戻る

MCP: Advanced Topics | Lesson 01「Let's get started!」(全文ナレッジ)

動画 1分28秒 / 講師: Stephen Grider(Anthropic, Member of Technical Staff)/ 文字起こし: mlx_whisper large-v3-turbo / 2026-07-03

日本語ナレッジ(要点整理)

  • 講師は Anthropic 技術スタッフの Stephen Grider。本コースは Model Context Protocol(MCP)の上級トピックを幅広く扱うイントロダクション回。
  • コースで扱う 6 テーマ(スライド「Course goals」と対応): 1. Sampling — MCP サーバーが、接続中のクライアントを経由して Claude などの言語モデルにテキスト生成を依頼できる仕組み。 2. Log and Progress Notifications(ロギングと進捗通知) — サーバーにアクセスするクライアントへ、長時間処理中のより良いフィードバックを返す。 3. Roots — MCP サーバーがアクセスすべき特定のファイル/フォルダを指し示す仕組み。 4. Messages Format(メッセージフォーマット) — MCP 仕様そのものを深掘りし、サーバー・クライアント間通信で使われる具体的なメッセージ形式を理解する。 5. StandardIO Transport — 上の知識をすぐ応用して、stdio トランスポートの動作を見る。 6. StreamableHTTP Transport — リモートホストされたサーバーを Streamable HTTP トランスポートで扱う詳細な深掘りで締めくくる。
  • 前提条件は 2 つ(スライド「What you need」と対応):
  • Python の基礎(エキスパートである必要はないが、読める必要はある)
  • MCP の基礎知識(クライアント・サーバー・ツールの理解)

実務ポイント(このレッスンから持ち帰るもの)

  • このコースの主戦場は「ツールを作る」の先、サーバー⇄クライアント間の双方向プロトコル機能(sampling/通知/roots)とトランスポート層の内部動作(メッセージ形式・stdio・Streamable HTTP)。
  • 特に Sampling(サーバー→クライアントへの LLM 呼び出し委譲)は、公開 MCP サーバーの API キー・コスト設計に直結する概念として最初に扱われる。次レッスンから順に対応。
  • コード例はすべて Python(MCP Python SDK / FastMCP)。自プロジェクトが TypeScript でも、プロトコル上の概念(create_message、通知、roots、JSON-RPC メッセージ)はそのまま流用できる。

本文(時系列・日本語逐語+画面差し込み)

🖥 [0:00] 画面: タイトルスライド(黒背景)。左上に「ANTHROP\C」ロゴ、中央に大見出し「Model Context Protocol: Advanced Topics」。

[0:00] こんにちは、ようこそ。私の名前は Stephen Grider(スティーブン・グライダー)、Anthropic の技術スタッフの一員です。

[0:05] このコースでは、Model Context Protocol(モデルコンテキストプロトコル)にまつわる幅広い上級トピックを学んでいきます。

[0:10] 本題に入る前に、これから議論するさまざまなトピックについて、簡単な全体像をお見せしたいと思います。

🖥 [0:16] 画面: スライド「Course goals」。右側に 6 つの角丸カードが縦に並ぶ(正確転記): - Sampling — Allows MCP servers to make calls to Claude through a client - Log and Progress Notifications — Give users better feedback during long-running operations - Roots — Direct local MCP servers to specific files/folders - Messages Format — Understand how MCP clients and servers communicate - StandardIO Transport — See how the stdio transport works - StreamableHTTP Transport — Explore how remotely hosted MCP servers work

[0:16] まずは sampling(サンプリング)から始めます。これは、MCP サーバーがクライアントに対して、Claude のような言語モデルを呼び出して一定量のテキストを生成するよう依頼できるようにするものです。

[0:24] 次に、ロギングと進捗通知(logging and progress notifications)を使って、あなたの MCP サーバーにアクセスするクライアントへ、より良いフィードバックを提供する方法を見ていきます。

[0:31] その後は roots(ルーツ)について時間を取って議論します。これは、MCP サーバーがアクセスすべき特定のファイルやフォルダを指し示せるようにする仕組みです。(※ASR は「routes」と誤認識しているが、正しくは「roots」)

[0:39] そこからは、MCP 仕様そのものをより深く見ていき、MCP が使う具体的なメッセージフォーマットを確認することで、サーバー・クライアント間通信の技術的な側面を本当に理解できるようにします。

[0:51] その知識をすぐに応用して、標準入出力(standard IO)トランスポートがどのように動作するかを見ます。

[0:56] そして最後は、Streamable HTTP トランスポートを使ってリモートにホストされたサーバーについての、非常に深い掘り下げで締めくくります。

[1:03] ここには大量のコンテンツがあります。これらすべての異なるトピックを確実にやり切るために、事前に知っておいてほしいことが 2 つあります。

🖥 [1:04] 画面: スライド「What you need」。右側に 2 つの角丸カード(正確転記): - Basic Python fundamentals — Essential syntax, loops, functions - Basic knowledge of MCP — Understand MCP clients, servers, and tools

[1:11] 第一に、私たちは大量の Python コードを見ていきます。Python のエキスパートである必要はありませんが、少なくとも Python を読めることは必要です。

[1:17] 第二に、クライアント、サーバー、ツールを含む、MCP の基本的な理解をすでに持っている必要があります。

[1:24] それでは、最初の技術トピックに飛び込みましょう。

英語逐語(ASR全文)

Hello and welcome. My name is Stephen Greider and I'm a member of technical staff at Anthropic. In this course, you're going to learn a wide variety of advanced topics around model context protocol. Before we dive in, I want to give you a quick overview of some of the different topics we will be discussing. We will begin with sampling, which allows an MCP server to request a client to call a language model like Claude to generate some amount of text. We'll then take a look at providing better feedback to clients accessing your MCP server by using logging and progress notifications. After that, we'll spend some time discussing routes, which allow you to point an MCP server to specific files or folders that it should access. From there, we'll take a deeper look at the MCP spec itself to really help you understand the technical side of server client communication by looking at the specific message format that MCP uses. We will then immediately apply that knowledge to see how the standard IO transport works, and then close out with a very deep dive on remotely hosted servers using the streamable HTTP transport. There is a lot of content here, so to make sure we can get through all these different topics, there are a couple of things that you need to know ahead of time. First, we will be looking at a lot of Python code. Now, you don't need to be a Python expert, but you should at least be able to read Python. Second, you should already have a basic understanding of MCP, including clients, servers, and tools. So, with that, let's dive into our first technical topic.

↑ 目次へ戻る

MCP: Advanced Topics | Lesson 02「Sampling」(全文ナレッジ)

動画 5分17秒 / 講師: Stephen Grider(Anthropic, Member of Technical Staff)/ 文字起こし: mlx_whisper large-v3-turbo / 2026-07-03

日本語ナレッジ(要点整理)

  • Sampling = MCP サーバーが、接続中の MCP クライアント経由で Claude などの言語モデルを利用する仕組み。「Claude を呼ぶ責務をサーバーからクライアントへ移す」ことと考えればよい。
  • デモ題材は Next.js 製チャットアプリ(Wikipedia Research Assistant)。MCP クライアントを内蔵し、research ツールを 1 つ持つ MCP サーバーに接続。ツールは Wikipedia を複数回検索し、その結果を最終レポートに統合(synthesize)する必要がある。
  • 統合(要約)処理の実現方法は 2 択:
  • Option #1: サーバーが直接 Claude を呼ぶ — 動くが複雑さが増える。サーバー側に API 呼び出しコード+レスポンス処理+API キー管理が必要。公開サーバーだと他人の利用分のトークン代を自分が払うことになる。
  • Option #2: Sampling — サーバーがプロンプトを書いてクライアントへ送り「これを Claude に流して」と依頼。クライアントが Claude を呼んで生成結果をサーバーへ返す。複雑さはクライアント側へ移るが、クライアント(Next.js アプリ)はすでに Claude への接続を持っているので追加 1 リクエスト程度の負担で済む。サーバーは API キー不要トークン代の負担なし
  • 実装の要点:
  • サーバー側: ツール関数内で ctx.session.create_message() を呼び、Claude へ渡したいメッセージのリストを渡す。これがリクエストに整形されクライアントへ送られる。
  • クライアント側: sampling callback を実装する。サーバーから送られたメッセージを受け取り、その中で Claude(任意の LLM)を呼んでテキストを生成し、CreateMessageResult を返す。callback は ClientSession に渡して接続する。
  • 公開(publicly accessible)MCP サーバーを作るときは必ず検討すべきテクニック。誰でも使える公開サーバーに「誰でも自由にテキスト生成できる状態」を持たせないため、生成の責務を接続元クライアントへ移す。

実務ポイント(このレッスンから持ち帰るもの)

  • 公開 MCP サーバーに LLM の API キーを埋め込まない。sampling でクライアントに生成を委譲すれば、キー管理もトークン課金もクライアント側の責務になる(設計とコストの両面で効く)。
  • サーバー側 API はシンプル: ツール関数の ctx: Context から await ctx.session.create_message(messages=[SamplingMessage(...)], max_tokens=..., system_prompt=...)。戻り値は result.content.type == "text" を確認してから result.content.text を使う。
  • クライアント側は sampling_callback(context, params) を書き、ClientSession(read, write, sampling_callback=...)渡し忘れないこと(渡さなければサーバーの create_message は機能しない)。
  • callback 内で使う LLM は自由(Claude でなくてもよい)。つまり sampling は「どのモデルで・誰の課金で生成するか」の決定権をクライアントへ渡すプロトコル機能。
  • MCP のメッセージ形式(SamplingMessage)と各社 SDK のメッセージ形式は互換ではないので、callback 側で変換ロジックが要る(詳細は Sampling walkthrough ノート参照)。

本文(時系列・日本語逐語+画面差し込み)

🖥 [0:00] 画面: 黒背景のセクション表紙スライド。「ANTHROP\C」ロゴと大見出し「Sampling」。

[0:00] Sampling(サンプリング)は、サーバーが、接続された MCP クライアントを通じて、Claude のような言語モデルにアクセスできるようにするものです。

[0:06] それが何を意味するのか、sampling とは何なのかを理解してもらうために、簡単なデモアプリケーションをお見せしたいと思います。では、これです。

🖥 [0:12] 画面: スライドからブラウザ(localhost:3000、タブ名「Create Next App」)へ切り替わる。チャットアプリの初期画面(正確転記): - 見出し: Wikipedia Research Assistant - 説明文: Ask me to research any topic on Wikipedia. I'll gather information, analyze content, and create comprehensive reports for you. - Try commands like: - "Research and write a report on geotechnical engineering" - "What are the key discoveries in quantum computing?" - "Explain the history and impact of the Renaissance" - 入力欄: Type a command or ask a question...(Press Enter to send, Shift + Enter for new line.)

[0:12] これは Claude を搭載したチャットアプリケーションです。ですから「1 足す 1 は?」と聞けば、応答が返ってきます。

[0:19] このチャットアプリケーションは、research ツールという 1 つのツールを実装した、単一の MCP サーバーにも接続されています。

[0:25] このツールを使って、Claude に、たとえば考古学(archaeology)についてのレポートを書いてもらうことができます。そうすると、Claude はその research ツールを使うことを決めます。リサーチを行い、

[0:35] すべての結果を統合(synthesize)して、レポートとして私に提示してくれます。

[0:40] さて、この research ツールが動くところを見てもらったので、このアプリケーションの裏側で何が起きているのか、research ツールが実際にどう機能しているのかをお見せしたいと思います。

🖥 [0:47] 画面: 同アプリのチャット実行結果(正確転記): - アシスタント吹き出し: 「1 + 1 = 2 / This is a basic arithmetic operation where adding one unit to another unit results in two units total.」 - ユーザー吹き出し: 「write a report on archaeology」 - ツール実行バッジ: ✅ 🔧 research result / > Show details - 見出し「Archaeology Report」本文: Archaeology is the scientific study of human activity through material remains left by past civilizations, evolving from 18th-century antiquarianism into a sophisticated discipline that combines empirical excavation with theoretical frameworks to understand human history beyond written records. The field encompasses diverse specialized branches including historical archaeology (which integrates material and documentary evidence), queer archaeology (which challenges heteronormative assumptions about past societies), and even software archaeology (which applies archaeological principles to understand legacy computer code), demonstrating its continued relevance and adaptability in revealing insights about human diversity across time and cultures.

[0:50] では、全体像の概要がこちらです。まず、いちばん左上に Next.js アプリケーションがあります。これがブラウザ内でコンテンツを実際にレンダリングしているもので、その中に MCP クライアントが統合されています。

[1:00] ある時点でユーザーが、考古学のようなトピックをリサーチしてほしいと頼みます。私たちはそのクエリを Claude に送り、Claude は、事前に提供されていた research ツールを実行することを決めます。

[1:10] Next.js アプリケーション上の MCP クライアントは、それをツール呼び出しリクエスト(tool call request)に変換して、MCP サーバーへ送ります。(※ASR は「NCP server」と誤認識。正しくは MCP サーバー)

[1:21] そして MCP サーバー上で research ツールが実行されます。Wikipedia に対していくつものリクエストを行い、考古学に関する記事や関連コンテンツなどを探そうとします。

[1:31] research ツールが Wikipedia からある程度のページを取得し終えたら、そのすべてのコンテンツを、ユーザーに表示できる最終レポートへと統合する必要があります。そして、それを実現する方法は 1 つではありません。

[1:40] 私たちが持っている 2 つの異なる選択肢をお見せしましょう。

🖥 [1:44] 画面: シーケンス図スライド(Option #1)。上部に 4 レーン: ClaudeNext JS + MCP ClientMCP ServerWikipedia(正確転記): - Client → Claude: Research a topic - Claude → Client: Run the 'research' tool → Client → Server: Tool Call Request → Server → Wikipedia: Research… ×3 - Server 内ノート: Research complete! / I need to use Claude to summarize the results! - Server → Claude(太矢印): Please summarize this… - 下部ボックス: Option #1 — Give the MCP server access to Claude, have the research tool generate the summary itself

[1:44] 選択肢その 1 では、MCP サーバーが直接 Claude(あるいは類似の LLM)に接続し、Wikipedia 検索で見つかったすべての結果を要約するよう依頼する、という形が取れます。

[1:53] つまりこの場合、MCP サーバーに言語モデルへの直接アクセスを与えることになります。これは間違いなく動きます。確実に実現可能なことです。

[2:01] ただし、これは MCP サーバーにかなりの複雑さを加えます。MCP サーバーが Claude へリクエストを送り、レスポンスを受け取り、生成されたテキストを抽出する……といったコードを書き込まなければなりません。

[2:16] さらに、MCP サーバーが Claude(あるいは使っている他の言語モデル)にアクセスするための API キーを持っていることも保証しなければなりません。

[2:22] 繰り返しますが、選択肢その 1 は動きます。ただ、それなりの複雑さが加わります。では、選択肢その 2 へ。

🖥 [2:26] 画面: シーケンス図スライド(Option #2)。同じ 4 レーン(正確転記): - Client → Claude: Research a topic / Claude → Client: Run the 'research' tool / Client → Server: Tool Call Request / Server → Wikipedia: Research… ×3 - Server 内ノート: I need to use Claude to summarize the results! - Server → Client(太矢印): Could you call Claude for me? - Client → Claude(太矢印): Call to Claude on behalf of the server - Claude → Client(太矢印): Claude responds to the client / Client → Server(太矢印): Here's the results of the call you asked for - 下部ボックス: Option #2 — Server generates a prompt, then asks the client to send it to Claude and report back

[2:27] 選択肢その 2 では、MCP サーバーに sampling(サンプリング)と呼ばれるテクニックを使わせます。

[2:33] sampling では、MCP サーバーが、自分の代わりにプロンプトを実行してくれるようクライアントに依頼します。

[2:39] 要するに、MCP サーバーがプロンプトを書き上げて MCP クライアントに送り、「ねえ、これを私の代わりに Claude に流してくれない?」と言うわけです。

[2:47] 私たちが組み上げた MCP クライアントは、そのプロンプトを受け取って Claude へ送ります。

[2:51] レスポンスを受け取り、そのテキスト生成の結果を MCP サーバーへ送り返します。サーバーはその生成されたテキストを、好きなように使うことができます。

[3:01] このアプローチは、複雑さを MCP クライアント側へ移します。

[3:05] そして今回のケースでは、それはまあ問題ありません。ご覧いただいたとおり、私たちの MCP クライアント、つまりあの Next.js アプリケーションは、すでに Claude への既存の接続を持っているからです。すでに Claude へリクエストを送っているのです。

[3:15] ですから、MCP サーバーの代わりに追加のリクエストを 1 つ実行することは、大した追加負担ではありません。

[3:21] もう 1 つの明白なメリットは、MCP サーバーが Claude にアクセスするための API キーをまったく必要としないことです。

[3:27] そして、これが一般公開された MCP サーバーである場合、サーバーを利用する誰かのために生成されたトークンの代金を支払う心配も、もう不要になります。

[3:36] というわけで、このテクニックは sampling(サンプリング)と呼ばれます。MCP サーバーがクライアントに対し、Claude なり他の任意の言語モデルなりを実行して、一定量のテキストを生成するよう依頼できるようにするものです。

🖥 [3:37] 画面: スライド「Sampling」。左に箇条書き、右に黒いコードパネル(サーバー側コード)(正確転記): - Allows the server to request the client to run Claude (or any LLM) - Server can request a LLM call by using the 'create_message()' function on the context - Reduces the amount of configuration we need to provide to the server - Shifts the cost and config burden to the client

```python @mcp.tool() async def summarize(text_to_summarize: str, ctx: Context): prompt = f""" Please summarize the following text: {text_to_summarize} """

result = await ctx.session.create_message(
    messages=[
        SamplingMessage(
            role="user",
            content=TextContent(
                type="text",
                text=prompt
            )
        )
    ],
    max_tokens=4000,
    system_prompt="You are a helpful research assistant",
)

if result.content.type == "text":
    return result.content.text
else:
    raise ValueError("Sampling failed")

```

[3:46] sampling は本当のところ、「Claude を呼び出す責務をサーバーからクライアントへ移すこと」だと考えることができます。

[3:52] sampling が最も役に立つのは、一般公開でアクセス可能な MCP サーバーを組み立てるときです。誰でも使える公開 MCP サーバーが、来た人誰にでも自由にテキストを生成させてしまう、という状態にはしたくないはずです。

[4:04] 公開サーバーでは sampling を活用して、テキスト生成の責務をサーバーから、接続してくるクライアント側へ移すのが望ましいのです。

[4:14] sampling を使うには、サーバーとクライアントの両方で少しだけセットアップが必要です。

[4:19] まずサーバー側では、ツール関数の内部で create message 関数を呼び出し、最終的に Claude へ渡してほしいメッセージのリストをそこに渡します。

[4:27] このメッセージのリストがリクエストに整形されて、クライアントへ送り届けられます。

[4:33] 次にクライアント側では、もう少しコードを書く必要があります。具体的には sampling callback(サンプリングコールバック)と呼ばれるものです。右側のスニペットの上部に、サンプリングコールバックの例があります。

🖥 [4:36] 画面: クライアント側コードのスライド。右に黒いコードパネル、左に 4 つの注釈吹き出し(矢印でコードの該当行を指す)(正確転記): - 注釈: 「Called when the server sends a sampling request」→ sampling_callback 定義行 - 注釈: 「Call a LLM of your choice」→ text = await chat(params.messages) 行 - 注釈: 「Return the generated text in a CreateMessageResult」→ return CreateMessageResult( 行 - 注釈: 「Pass the sampling callback to the ClientSession」→ sampling_callback=sampling_callback

```python async def sampling_callback( context: RequestContext, params: CreateMessageRequestParams ): # Call Claude using the Anthropic SDK text = await chat(params.messages)

return CreateMessageResult(
    role="assistant",
    model=model,
    content=TextContent(type="text", text=text),
)

async def run(): async with stdio_client(server_params) as (read, write): async with ClientSession( read, write, sampling_callback=sampling_callback ) as session: await session.initialize()

        result = await session.call_tool(
            name="summarize",
            arguments={"text_to_summarize": "lots of text"},
        )
        print(result.content)

```

[4:43] サンプリングコールバックは、サーバーから送られてきたメッセージを受け取ります。そしてこのコールバックの中で、Claude なり、あなたが使っている他の言語モデルなりを呼び出してテキストを生成し、create message result を返すのは、あなた次第(あなたの責務)です。

[4:58] はい、以上が sampling です。ご覧のとおり、それほど複雑なものではありません。

🖥 [5:00] 画面: まとめとして再び「Sampling」スライド(箇条書き 4 点+サーバー側 summarize ツールのコードパネル。[3:37] と同一内容)。

[5:03] これは実際のところ、「一定量のテキストを生成するという負担を、サーバーからクライアントへ移し替える」というコンセプト、あるいはアイデアにすぎません。

[5:08] そして繰り返しになりますが、これは、一般公開でアクセス可能な MCP サーバーを組み立てるときにはいつでも、間違いなく検討したくなるテクニックです。

英語逐語(ASR全文)

Sampling allows a server to access a language model like Claude through a connected MCP client. To help you understand what that means and what sampling is all about, I want to show you a quick demo application. So here we go. This is a chat application that is powered by Claude, so I can ask it, what's 1 plus 1? And I'll get a response back. This chat application is also connected to a single MCP server that implements one tool called the research tool. I can use this tool to ask Claude to maybe write a report on archaeology. When I do so, Claude is going to decide to use that research tool. It's going to go and do some research, synthesize all the results, and then present a report back to me. Now that you have seen this research tool run, I'd like to show you what is going on behind the scenes in this application and how the research tool actually works. All right, so here's a high-level overview of what's going on. First, at the very top left, I've got a Next.js application. That's what's actually rendering some content inside the browser, and that's got a integrated MCP client inside of it. A user at some point in time is going to ask us to research a topic like archaeology. We're going to send that query off to Claude, and Claude is going to decide to run the research tool that it has been provided ahead of time. Our MCP client on the Next.js application is going to translate that into a tool call request and send it over to the NCP server. Then on the NCP server, the research tool is going to run. It's going to make a number of requests off to Wikipedia and try to find some articles around archaeology and then some related content and so on. Once the research tool has fetched some number of pages from Wikipedia, we need to take all that content and synthesize it into a final report that can be displayed to the user. And there's more than one way that we could do that. Let me show you two different options we have. In option number one, we could have the MCP server reach out directly to Claude or some similar LLM and ask it to summarize all the results that found from the Wikipedia searches. So in this case, we would be giving our MCP server direct access to a language model. This would absolutely work. It is definitely something we could do. However, this adds a lot of complexity to our MCP server. We would have to write in some amount of code to allow the MCP server to make a request off to Claude, get the response back, extract the generated text, and so on. We would also have to make sure that the MCP server has a API key to access Claude or whatever other language model we were using. So again, option number one would work, but it adds a decent amount of complexity. So on to option two. In option two, we're going to have our MCP server make use of a technique referred to as sampling. With sampling, we're going to have our MCP server ask the client to run a prompt on its behalf. So in short, the MCP server is going to write off a prompt, send it off to the MCP client, and say, hey, could you feed this into Claude for me? The MCP client that we've put together is then going to take that prompt and send it off to Claude. It will get a response back and then send the result of that text generation back to the MCP server, where it can then do whatever it wants to do with that generated text. This approach moves the complexity over to the MCP client. And in this case, it's kind of okay, because as you saw, our MCP client or that Next.js application already has an existing connection to Claude. It is already making requests off to Claude. So it's not that much more to just run one additional request on behalf of the MCP server. The other obvious benefit here is that the MCP server does not need an API key to access Claude at all. And if this is a publicly accessible MCP server, now it doesn't need to worry about paying for tokens generated on behalf of someone else making use of the server. So this technique is referred to as sampling. It allows a MCP server to ask the client to run Claude or any other language model to generate some amount of text. You can really think of sampling as moving the responsibility of calling Claude from a server off to the client. Sampling is most useful anytime you are putting together a publicly accessible MCP server. You don't want to have a MCP server that is publicly available for anyone to use that is just going to allow anyone to come and generate some text. For your publicly accessible server, you would want to make use of sampling and move the responsibility of generating text from your server off to whatever client is connecting to it. Making use of sampling requires a little bit of setup on both the server and the client. First off, on the server, inside of a tool function, you will call the create message function. and you're going to pass into that a list of messages that you want to be eventually handed off to Claude. This list of messages is going to be formulated into a request that gets sent down to the client. Then on the client, you have to write out a little bit more code, specifically something called a sampling callback. I've got an example sampling callback at the top of this snippet on the right hand side. Your sampling callback is going to receive the messages that were sent from the server. Then inside this callback, it is up to you to call Claude or whatever other language model you are making use of, generate some text, and then return a create message result. All right, so that is sampling. As you can see, it's not terribly complicated. It's really just this concept or this idea of shifting the burden of generating some text from the server back over to the client. And again, this is a technique you're definitely going to want to look at anytime you are putting together a publicly accessible MCP server.

↑ 目次へ戻る

MCP: Advanced Topics | Lesson 03「Log and progress notifications」(全文ナレッジ)

動画 2分41秒 / 講師: Stephen Grider(コース講師)/ 文字起こし: mlx_whisper large-v3-turbo / 2026-07-03 対応 walkthrough: w2「Notifications walkthrough」(実物コード = assets/notifications/ の server.py・client.py)

日本語ナレッジ(要点整理)

なぜログ・進捗通知が必要か

  • 長時間かかるツール(デモでは Wikipedia リサーチアプリの research ツール)は、実行中ユーザーに何のフィードバックも無い。ユーザーからは「失敗して停止状態にはまっているのか、動いているのか」区別がつかない。
  • logging / progress notifications はセットアップが非常に簡単で、MCP サーバーの UX を大きく改善する。
  • デモで表示される進捗メーターとログは偽データではない。research ツールの実行中にツール内部から emit された実データがそのままクライアントに届いている。

サーバー側(Python MCP SDK / FastMCP)

  • ツール関数は context 引数を受け取る。これはツール関数の最後の引数として自動的に注入される。
  • context オブジェクトには、情報をログする・進捗を報告するための各種メソッドが載っている:
  • info() などのログメソッド(walkthrough では warning / debug / error も列挙)
  • report_progress()(進捗の報告。例:report_progress(20, 100)
  • これらを呼ぶたびに、メッセージが自動的にクライアントへ送信される。

クライアント側

  • サンプリング(前レッスン)と似た構成でコールバックを2つ書く:
  • logging コールバック:サーバーからログ文を受け取るたびに呼ばれる → ClientSession に渡す
  • progress コールバック:進捗の更新を受け取る → call_tool() に渡す
  • コールバック内でどう表示するかは実装者の自由
  • CLI アプリならターミナルに print するだけでよい
  • Web アプリなら「サーバー→クライアント→ブラウザ」へ情報を届ける追加の仕組みが必要
  • 表示しなくてもよい(含めない・見せないも自由)。あくまで「裏で何が起きているか」をユーザーに伝える純粋な UX 強化

実務ポイント(このレッスンから持ち帰るもの)

  1. 長時間ツールには ctx.info + ctx.report_progress を必ず仕込む。クライアントが対応していれば拾えるし、非対応でも害はない。「停まってる?」というユーザー不安(=体感品質の低下)を先回りで潰せる。
  2. 渡し先を取り違えないlogging_callbackClientSession のコンストラクタprogress_callbackcall_tool() の引数。逆に渡すと通知は届かない(walkthrough ステップ4と同じ注意)。
  3. progress は (progress, total) の組で届く。total が None のケースの分岐をクライアント側で書いておく(実物 client.py がその形)。
  4. 進捗メーター表示は「fake ではなくツール内 emit の実データ」——つまり進捗の粒度・タイミング設計はサーバー実装者の責任。処理の節目ごとに info + report_progress を対で打つ(デモの research ツールは 20/100 → 70/100 のように段階報告)。
  5. Web アプリでは中継設計が別途必要(コールバックはあくまで MCP クライアント側で発火する。ブラウザへは SSE/WebSocket 等で流す工夫が要る)。
  6. 通知はプロトコル上の必須要素ではなく UX エンハンス。まず動くツールを作り、その後この2メソッドを足すだけで体験が変わる、という導入順で良い。

本文(時系列・日本語逐語+画面差し込み)

🖥 [0:00] 画面: 黒背景のタイトルスライド「Logging and progress notifications」(左上に ANTHROP\C ロゴ)

[0:00] 次はログと進捗の通知(logging and progress notifications)です。

[0:03] これらの通知はセットアップが本当に簡単で、あなたの MCP サーバーまわりのユーザー体験を大きく改善します。

[0:09] では、もう一度手早いデモをお見せしましょう。

🖥 [0:12] 画面: スライドが横にスライドし、ブラウザ(タブ名 Create Next App/URL localhost:3000)へ切り替わる。画面中央に「Wikipedia Research Assistant」——「Ask me to research any topic on Wikipedia. I'll gather information, analyze content, and create comprehensive reports for you.」 「Try commands like:」として3例:"Research and write a report on geotechnical engineering""What are the key discoveries in quantum computing?""Explain the history and impact of the Renaissance" 下部に入力欄「Type a command or ask a question...」と「Press Enter to send, Shift + Enter for new line.」

[0:12] 少し前にお見せしたアプリを覚えているでしょう。このリサーチアプリケーションです。

[0:15] そして Claude に考古学(archaeology)についてのレポート生成を頼むと、先ほど話したあの MCP サーバーを通じてツール呼び出しが行われます。

[0:23] 具体的には research 関数を呼び出します。

[0:26] さて、この research 関数は完了までにしばらく時間がかかります。

[0:28] そして実行されている間、ユーザーは実質何のフィードバックも得られません。

[0:32] つまりユーザーの知る限りでは、このツール呼び出しは実は失敗していて、単に停止状態か何かにはまり込んでいるだけかもしれないのです。

[0:39] ですから、裏側で何が起きているのかについて、ユーザーにもう少しだけ内側を見せてあげられたら本当に良いですよね。

🖥 [0:44] 画面: (通知オフの初回実行の完了画面)右上にユーザーメッセージ「Write a report about archaeology」。AI 応答「I'll research archaeology for you and write a report based on the findings.」の下に、緑のチェック付きボックス「✅ 🔧 research result > Show details」。続いて本文:「Based on my research, here is a report on archaeology:」+考古学レポート本文(Archaeology is the scientific study of human activity through the examination of material remains left behind by past civilizations, ... queer archaeology ... more inclusive understandings of the past. まで1段落)——進捗もログも一切表示されない

[0:45] そのために、ログと通知(logging and notifications)を利用できます。

[0:48] ではログと通知をオンにした場合に、まったく同じクエリがどう見えるかをお見せします。有効化は、たった今ページをリフレッシュしたときに済ませてあります。

[0:56] 同じクエリをもう一度尋ねます。今度も同じ research ツール呼び出しが行われますが、今回はそれに付随して進捗メーターと、いくつかのログ文が表示されます。

🖥 [1:01] 画面: 2回目の実行中。「🔄 🔧 research call」ボックス内に: Progress メーター「80/100」(バーが8割まで進行) 黒いターミナル風ボックスに直近ログ3行: >_ Total content size: 6940 characters >_ 3. Generating LLM summary ... >_ Estimated tokens being sent to LLM: 1152 下に「> Show details」。入力欄右端には赤い停止ボタン

[1:05] この進捗とログのメーターは、偽データではありません。

[1:08] これらは research ツールの中——ツール呼び出しの内部——で emit されているログ文と進捗の文なのです。

[1:14] では少しコードをお見せして、この仕組みの理解を手伝いましょう。

🖥 [1:18] 画面: スライド「Logging and Progress Notifications」。箇条書き3点: ・Notifications emitted from the server to help the client track the status of long-running tasks ・In the Python MCP SDK, logging and progress are done through the Context argument provided to tools ・It's up to the client to decide how (if at all) to present these to the user 右にサーバー側コード: ```python from mcp.server.fastmcp import Context

@mcp.tool( name="research", description="Research a given topic" ) async def research( topic: str = Field(description="Topic to research"), *, context: Context ): await context.info("About to do research...") await context.report_progress(20, 100) sources = await do_research(topic)

await context.info("Writing report...")
await context.report_progress(70, 100)
results = await generate_report(sources)

return results

```

[1:18] サーバー上のツール関数の中でログと進捗通知を使い始めるには、context 引数を受け取ります。これはツール関数の最後の引数として自動的に含まれるものです。

[1:27] その context オブジェクトの上には、情報をログしたり、このツール実行の進捗をクライアントへ報告し返したりできる、さまざまなメソッドが揃っています。

[1:38] つまり info や report progress のようなメソッドが手に入ります。これらの関数を呼ぶといつでも、メッセージが自動的にクライアントへ送り返されます。

🖥 [1:45] 画面: クライアント側コードのスライド。4つの注釈(薄茶のラベル)が矢印でコードを指す: ・「Called whenever the server emits a log statement」→ logging_callback ・「Called whenever the server emits a progress update」→ print_progress_callback ・「Provide the logging callback to the client session」→ logging_callback=logging_callback ・「Provide the progress callback to the tool call」→ progress_callback=print_progress_callback コード全文: ```python async def logging_callback(params: LoggingMessageNotificationParams): print(params.data)

async def print_progress_callback( progress: float, total: float | None, message: str | None ): if total is not None: percentage = (progress / total) * 100 print(f"Progress: {progress}/{total} ({percentage:.1f}%)") else: print(f"Progress: {progress}")

async def run(): async with stdio_client(server_params) as (read, write): async with ClientSession( read, write, logging_callback=logging_callback ) as session: await session.initialize()

        await session.call_tool(
            name="add",
            arguments={"a": 1, "b": 3},
            progress_callback=print_progress_callback,
        )

```

[1:46] クライアント側でこれらのログ文と進捗更新を利用するには、少し前にサンプリングのところで見たものとよく似たコードを書きます。

[1:52] サーバーからログ文を受け取るたびに呼ばれるコールバック関数を組み立てます。

[1:56] さらに、サーバーからの進捗の更新を受け取る、別のコールバックも作ります。

[2:01] そして、ロギングのコールバックはクライアントセッションに渡し、進捗のコールバックは call tool 関数へ渡します。

[2:11] これらのコールバックの中で、受け取ったログ文と進捗をユーザーにどう報告するかは、あなた次第です。

[2:16] CLI アプリケーションを作っているなら、ターミナルにそのまま print するだけでも構いません。

[2:21] Web アプリを作っているなら、その情報をブラウザまで届けるために、もう少し気の利いた仕組みを考える必要があるでしょう。

[2:27] もちろん、これらのログ文や進捗文を実際には一切含めない、というのでも構いません。

[2:32] 望まないなら、ユーザーにまったく見せなくてもよいのです。

[2:35] これらは、繰り返しになりますが、何が起きているのかをユーザーによりよく把握してもらうための、ユーザー体験の強化にすぎません。

英語逐語(ASR全文)

Next up is logging and progress notifications. These notifications are really easy to set up and greatly improve the user experience around your MCP server. So let me show you once again a quick demonstration. You might recall the app I showed you just a moment ago, this research application. And if I ask Claude to generate a report on archaeology, it is going to make a tool call through that MCP server that we discussed a moment ago. It's going to call specifically the research function. Now it takes a while for this research function to complete. And as it is running, a user is not really getting any feedback. So as far as the user knows, this tool call might've actually failed and it might just be in a stalled state or something like that. So it would be really nice if we could give users a little bit more insight into what is going on behind the scenes.

To do so, we can make use of logging and notifications. So let me show you what that exact same query would look like if I turn on logging and notifications, which I did when I just refreshed the page right now. So I'm going to ask the same query again, and now I'm going to get the same research tool call, but now I'm going to get a progress meter and some log statements to go along with it. The progress and the log meter is not fake data. These are log and progress statements that are being emitted inside of the tool call inside of the research tool. So let me show you a little bit of code and help you understand how this works.

To get started with logging and progress notifications inside of a tool function on our server, we'll receive the context argument, which is automatically included as the last argument to our tool function. On that context object are a variety of different methods that allow us to either log information or report the progress of this tool run back over to the client. So we get methods like info and report progress. Anytime you call these functions, a message will be automatically sent back to the client.

To make use of the log statements and the progress updates on the client, we'll write out a little bit of code similar to what we saw just a moment ago around sampling. So we'll put together a callback function that'll be called whenever we receive a logging statement from the server. We'll also make a separate callback that will receive updates on progress from the server. We'll then take the logging callback and pass it off to our client session and the progress callback and that gets passed off to the call tool function. Inside of these callbacks it is up to you on how you're going to report these log statements and the progress to your user. You can just print them out at the terminal if you're making a CLI application. If you're making a web app, you'll have to come up with a little bit more clever solution to get that information down to the browser.

You, of course, also do not have to actually include any of these log statements or progress statements. You don't have to show them to the user at all if you do not want to. These are just user experience enhancements, once again, to give your user a better idea of what is going on.

↑ 目次へ戻る

MCP: Advanced Topics | Lesson 04「Roots」(全文ナレッジ)

動画 7分55秒 / 講師: Stephen Grider(コース講師)/ 文字起こし: mlx_whisper large-v3-turbo / 2026-07-03 表記注: ASR は "roots" をしばしば "routes"、"biking.mp4" を "Viking.mp4" と聞き取っているが、画面上の実表記は roots / list_roots / biking.mp4(日本語逐語では画面準拠に正す。英語逐語は ASR のまま)

日本語ナレッジ(要点整理)

Roots とは

  • ユーザーが MCP サーバーに対して、特定のファイル/フォルダ群へのアクセスを許可する仕組み。「ねえ MCP サーバー、これらのファイルにはアクセスしていいよ」を成文化したもの。
  • ただし roots の働きは許可付与だけではない(後述の「2つの役割」)。

Roots が無い世界の問題(convert_video 単体サーバー)

  • ツールは convert_video 1つだけ:ローカルの動画ファイルパスを受け取り、別形式へ変換(例 MP4→MOV)。
  • ユーザーが「biking.mp4 を MOV に変換して」と頼む → Claude はユーザーの言葉どおり {"path": "biking.mp4"} で呼ぶ → 「そんなファイルは無い」エラー
  • 原因:ユーザーのファイルシステムは複雑。本人は「Movies ディレクトリの中」と知っていても、Claude にはファイルシステムを探索して所在を突き止める能力が無い
  • 回避策としてフルパス必須(~/Movies/biking.mp4)にすれば動くが、毎回フルパスを打たせるのは UX として最悪。ユーザーは「この動画を変換して」+ファイル名程度で済ませたい。

Roots による解決(デモサーバーの3ツール構成)

ツール 役割
convert_video 変更なし(動画変換)
read_dir 指定ディレクトリのファイル/フォルダ一覧を返す
list_roots roots の一覧を返す。root=ユーザーが事前に読み取りを許可したファイル/フォルダ
- roots の与え方:サーバー起動時にコマンドライン引数で許可パスを渡す(デモ:uv run main.py ~/Desktop)。
- さらに read_dir と convert_video にチェックコードを追加し、「roots のいずれかに含まれるファイル/フォルダにしかアクセスできない」ようにする。

デモの挙動(2ケース)

  1. root 内のファイル(Desktop/biking.mp4):「biking.mp4 を MOV にして」→ Claude が list_roots →(Desktop だけと判明)→ read_dir → biking.mp4 発見 → フルパスで convert_video と自律的に解決し成功。ユーザーはフルパス不要。
  2. root 外のファイル(Documents/swimming.mp4・フルパス指定):フルパスを与えたので Claude は直接 convert_video を呼ぶ → 即エラー(ファイルが見つからない) → Claude が list_roots / read_dir で状況確認 → 「アクセスできるのは Desktop だけ。swimming は変換できない。アクセス権を付与するかファイルを移動して」と返答。

Roots の2つの役割

  1. アクセス許可:ユーザーが特定のファイル/フォルダへのアクセスを与える(=それ以外を制限する)
  2. 探索範囲のフォーカス:Claude をファイルシステムの特定領域だけに集中させられる。フルパスを打たせなくても、Claude が自律的に roots を見て、その中を探して目的ファイルを解決できる

最重要の注意(講師が強調)

  • roots の概念は「緩い」——MCP SDK には roots 周りの実装がほとんど無く、ファイル/フォルダアクセスを自動制限してくれる仕組みはどの SDK にも存在しない
  • 制限の実装はサーバー実装者の責任。ツールがファイル/フォルダに触るたびに「そのパスが roots のいずれかに含まれるか」を確認するコード(例:is_path_allowed)を自分で書く。
  • is_path_allowed:要求パスを受け取る → クライアントから roots 一覧を取得 → パスがいずれかの root に含まれるか判定 → 含まれれば許可、含まれなければ拒否。
  • list_roots のツール化は必須ではない。roots 一覧をプロンプトに直接注入する方式でもよい。ツール化は「Claude が必要と判断したタイミングで roots を見に行ける」ようにする、講師が有用と感じた一パターン。

実務ポイント(このレッスンから持ち帰るもの)

  1. セキュリティ境界は自前実装——「roots を渡したから安全」は錯覚。SDK は何も強制しない。is_path_allowed 相当をヘルパー化し、ファイルに触る全ツールの冒頭で必ず呼ぶ(デモの convert_video は if not await is_path_allowed(input_path, ctx): raise ValueError(...))。
  2. パス包含判定は Path.relative_to + ValueError で(スライドのコードが手本)。文字列 startswith 判定は /Users/foo/Users/foobar の混同や ../ 遡り事故の温床。ファイルなら parent に正規化し、存在しないパスは即 False。
  3. roots の取得はサーバー→クライアントの往復ctx.session.list_roots() を呼ぶとクライアント側の roots コールバックが実行されて返ってくる(=roots の正本はクライアント側にある。詳細は w3 Roots walkthrough)。
  4. 「探索用ツールとのセット設計」が UX の肝:roots 単体では場所解決ができない。list_roots(どこを見てよいか)+ read_dir(そこに何があるか) を揃えて初めて「ファイル名だけで頼める」体験になる。
  5. エラーメッセージは Claude が読む前提で書く:デモの Claude はツールエラーを受けて自律的に list_roots → read_dir → 状況説明(アクセス権を要求)まで進んだ。リカバリ手順を想起させるエラー文(何が無い・どこは見える)がエージェントの復帰品質を決める。
  6. list_roots ツール vs プロンプト注入の使い分け:roots が少なく固定ならプロンプト注入で1往復節約。roots が動的・多数ならツール化して必要時のみ参照(コンテキスト節約)。
  7. ユーザー向け UI では「どのフォルダを許可するか」を起動引数/設定で渡させる設計(デモは CLI 引数)。許可外に触ろうとした時の文言(付与を促す・移動を促す)まで含めて体験設計する。

本文(時系列・日本語逐語+画面差し込み)

🖥 [0:00] 画面: 黒背景のタイトルスライド「Roots」(左上に ANTHROP\C ロゴ)

[0:00] 次のトピックは、ルート(roots)の仕組みです。roots は、ユーザーがサーバーに対して、ある特定のファイルとフォルダの集合へのアクセスを許可できるようにするものです。roots とは「ねえ MCP サーバー、君はこれらのファイルにアクセスしていいよ」という言い方を成文化したようなものだ、と考えて構いません。しかし roots は、単に許可を与える以上のことをします。どう動くのか理解してもらうために、簡単な例をひととおり歩いて見ていきましょう。

🖥 [0:21] 画面: スライド。上部に赤枠で「If roots didn't exist...」。左に「MCP Server」枠、その中に Tools として「convert_video」ボックス。右に注釈:「Tool that will convert a .mp4 video file to .mov / Requires a path to a video file on the local machine」

[0:21] この例ではまず、roots が存在しなかった場合に私たちがぶつかるであろう、よくある問題をお見せします。

[0:26] 問題を理解したら、次に roots がそれをどう解決するかを見ます。

[0:30] この例では、Convert Video というたった1つのツールを持つ、ごくシンプルな MCP サーバーを考えます。

[0:36] このツールは、ユーザーのローカルマシン上にある動画ファイルへのパスを受け取り、

[0:41] その動画ファイルを別のファイル形式へ変換します。たとえば MP4 から MOV へ、といった具合です。

[0:48] では、ユーザーが実際これをどう使うか想像してみましょう。

[0:51] ユーザーは、何らかの CLI アプリケーションか何かに入力して、biking.mp4 という動画ファイルを MOV 形式に変換するよう Claude に頼むかもしれません。

🖥 [0:52] 画面: チャットアプリのスライド。左にチャット UI——「You: Convert biking.mp4 to mov format」「AI: I'll call convert_video」。AI の吹き出しから矢印が黒いコードボックス { "path": "biking.mp4" } を経由して、右の「MCP Server」枠内 Tools の「convert_video」へ

[1:01] Claude は、この MCP サーバーが提供するツールの一覧を眺め、convert video ツールが利用できると知ります。

[1:08] そこで Claude はおそらくこう判断するでしょう。

[1:10] 「最高だ。あのツールを呼ぼう。そしてパスには biking.mp4 を渡そう。だってユーザーがまさにそう頼んだのだから」

[1:18] ユーザーは biking.mp4 という名前の動画ファイルの変換を頼みました。

[1:23] そして、そのツールからの応答として私がおそらく目にするであろうものが、これです。

[1:27] おそらく「そのような biking.mp4 ファイルはありません」といったエラーが返ってくるはずです。

🖥 [1:32] 画面: 同じチャットスライドに赤いエラーボックスが出現(正確転記): 「Error / No such file biking.mp4」。convert_video ツールから黒い矢印がこのエラーボックスを経由して AI 吹き出し(I'll call convert_video)へ戻る=ツール呼び出しがエラーで返る図

[1:34] では、なぜそのエラーが返ってくると予想するのでしょうか?

[1:37] 思い出してください。ユーザーのファイルシステムは、実に複雑かもしれないのです。

🖥 [1:38] 画面: スライド右側に黒いパネル「Your Local Filesystem」——ディレクトリツリー: Movies/Sports/skateboarding_tricks.mp4)・biking.mp4(オレンジ枠で強調)・hiking_adventures.mov Documents/Invoices/invoice_2024_Q4.pdf)・project_proposal.docxmeeting_notes.txt Photos/Vacation/beach_sunset.jpg)・family_portrait.pngbirthday_party.heic 左のチャット内「biking.mp4」からツリー内の biking.mp4 へオレンジの矢印

[1:41] 多種多様なファイルやフォルダ、大量のドキュメントが、そこら中に散らばっているかもしれません。ユーザー自身は biking.mp4 がこの Movies ディレクトリの中にあると知っているかもしれませんが、Claude がそれを理解する現実的な方法はありません。

[1:54] つまり、ユーザーが単に「biking.mp4」と頼んだだけでは、Claude にはユーザーのファイルシステムを探索して、その文書が正確にどこにあるのかを突き止める能力が、実質的に無いのです。

[2:04] これを解決する一つの方法は、ユーザーに動画ファイルへのフルパスを常に渡すよう義務付けてしまうことです。

🖥 [2:06] 画面: 同じチャットスライドの改訂版。「You: Convert ~/Movies/biking.mp4 to mov format」→ コードボックス { "path": "~/Movies/biking.mp4" } → 「MCP Server」の convert_video へ

[2:09] つまり「このツールを使いたければ、movies/biking.mp4 のような完全修飾パスを渡さなければならない」とするわけです。

[2:20] これでおそらく動きはします。しかし、まったく便利ではありません。ユーザーは、アクセスしようとするすべてのファイルにフルパスを打ち込むために座っていたくなどないのです。本当はただ「この動画を変換して」と、名前を入れて別の形式へ、という程度で済ませたいのです。

[2:32] さて、問題が分かったので、roots という考え方を使ってこれをどう解決できるかをお見せしましょう。

🖥 [2:39] 画面: スライド「MCP Server」——3ツールの表: | Tool Name | Purpose | |---|---| | convert_video | Unchanged | | read_dir | Return a list of files/folders in a specified directory. | | list_roots | Return a list of roots: files/folders that the user has granted permission to be read. / The read_dir and convert_video tools are only allowed to work on files/folders contained in one of these roots |

[2:39] この問題は、MCP サーバーにいくつかツールを追加することで解決します。同じ convert video ツールは残しつつ、read directory というツールも追加します。何をするかは想像がつくでしょう。特定のディレクトリの中のファイルとフォルダを列挙するだけのものです。

[2:52] そして3つ目のツールは list roots と呼ぶことにします。これは roots のリストを返します。root とは、ユーザーが事前に読み取りの許可を与えたファイルまたはフォルダのことです。

[3:08] 言い換えると、ユーザーがこの MCP サーバーを最初に起動するとき、サーバーが読んでよいファイルとフォルダのリストを、コマンドライン引数として渡せる、ということです。

[3:13] その上で、他のツールにもちょっとした決まりを入れます。read directory ツールと convert video ツールにコードを追加して、それらがディレクトリや動画ファイルへアクセスしようとするときはいつでも、これらの roots のいずれかの中に含まれているファイルとフォルダにしかアクセスできないようにするのです。

[3:33] この3つのツールがどう組み合わさるのかを理解してもらうために、ごく小さなサンプルアプリケーションを用意しました。さっそく手短にデモをお見せします。

🖥 [3:42] 画面: 左に黒いターミナル(タイトル ..mcp/dev/roots (-zsh))——プロンプトに roots uv run main.py と入力中。右に Finder 風の「Desktop」ウィンドウ:中身は biking.mp4 1つ

[3:43] このプログラムを動かすには、uv run main.py を実行します。そしてこのプログラムは、追加のコマンドライン引数のリストを受け取るように作ってあります。

[3:49] 望むなら、こんなふうに desktop を入れられます。それがこの MCP サーバーの中で root として設定されます。

[3:55] つまり私は実質、「サーバーにはデスクトップフォルダと、その中のすべてのファイルへアクセスできるようになってほしい」と言っているわけです。

🖥 [4:05] 画面: ターミナル(タイトル uv (Python))——1行目 roots uv run main.py ~/Desktop、2行目のプロンプトに > turn と入力し始めている。右の「Desktop」ウィンドウには biking.mp4

[4:05] そして右側のこちらには、私のデスクトップのディレクトリが見えます。その中に biking.mp4 というファイルがあります。

[4:09] では、これを実行して、Claude に biking.mp4 ファイルを MOV ファイルへ変換するよう頼みます。

[4:15] さあ実行すると、いくつかのツール呼び出しが出力されていくのが見えます。

[4:22] まず Claude は、list roots ツールを呼ぶことで、自分がアクセスできるさまざまなファイルとフォルダを確認します。

[4:27] すると、アクセスできるディレクトリはただ一つ、デスクトップフォルダだけだと分かります。このプログラムで私がアクセスを許可したのは、それだけだからです。

[4:36] 次にそのディレクトリを読み取ろうとして、その中に biking.mp4 ファイルを発見します。

[4:42] そしてその時点で、convert video ツールの実行を実際に成功させるために必要なものが、すべて揃いました。

🖥 [4:45] 画面: ターミナル出力——直前のツール呼び出しボックスに "path": "/Users/sgrider/Desktop"。続いて: 「Great! I found the biking.mp4 file on the Desktop. Now I'll convert it to MOV format:」 「Tool Call 🔧 convert_video / Arguments: { "input_path": "/Users/sgrider/Desktop/biking.mp4", "format": "mov" }」 「Perfect! I've successfully converted your biking.mp4 file to MOV format. The converted file has been saved as biking.mov in the same location (/Users/sgrider/Desktop/). The conversion is now complete and ready for use.」 右の「Desktop」ウィンドウには biking.mp4 と biking.mov の2つが並ぶ

[4:46] そこで、私のデスクトップフォルダの biking.mp4 への完全修飾パスを付けて、そのツールを実行できるのです。

[4:52] さて思い出してください。roots は、MCP サーバーがアクセスできるファイルとフォルダを制限することを意図したものです。

[4:59] そこで、あのデスクトップフォルダに加えて、ここには Documents フォルダもあります。そのディレクトリの中には swimming.mp4 というファイルが入っています。

[5:04] そこで Claude に、swimming.mp4 ファイルも同じく MOV へ変換するよう頼みます。そして今回は、その mp4 ファイルへのフルパスを与えることにします。

🖥 [5:10] 画面: ターミナルに > Convert the ~/Documents/swimming.mp4 file to mov と入力中。右には2つの Finder 風ウィンドウ——「Desktop」(biking.mp4・biking.mov)と「Documents」(swimming.mp4

[5:16] 理屈の上では、Claude はもう roots の一覧を見る必要がありません。変換したい対象ファイルへの完全修飾パスをすでに持っているのですから、convert video ツールを直接呼べばよいはずです。

[5:26] では実行して、何が起きるか見てみましょう。

[5:30] すぐに convert video を呼ぼうとしますが、早々に戻ってくることになります。Claude は「すみません、そのツールの実行でエラーが出ました。指定のファイルを見つけられませんでした」と言うわけです。

🖥 [5:40] 画面: ターミナル出力—— 「It looks like the file wasn't found at that path. Let me check what directories are available and help you locate the correct path for your swimming.mp4 file.」 「Tool Call 🔧 list_roots / Arguments:(なし)」 「Tool Call 🔧 read_dir / Arguments: { "path": "/Users/sgrider/Desktop" }」 「I can see that the accessible directory is /Users/sgrider/Desktop, and I found a biking.mp4 file there, but not the …(スクロールで続く)」

[5:40] Claude は次に、利用できる roots を確認しようとし、唯一利用できる root——この場合はデスクトップ——を読み取ります。

[5:50] そして Claude は気づくのです。「どうやら、そのファイルにはそもそもアクセスできないようだ。私がアクセスできる唯一のディレクトリはデスクトップだ。swimming ファイルを変換してほしいのなら——残念ながらアクセスできない。だから、アクセス権を与えるか、ファイルを移動するか、何か他の方法を取ってほしい」

🖥 [6:04] 画面: ターミナル出力の続き([5:40] で切れていた Claude の最終応答の全文・正確転記)。直前のツール呼び出しボックスに { "path": "/Users/sgrider/Desktop" }。続いて: 「I can see that the accessible directory is /Users/sgrider/Desktop, and I found a biking.mp4 file there, but not the swimming.mp4 file you mentioned. The file ~/Documents/swimming.mp4 doesn't appear to be accessible from this server. Could you please: 1. Check if the swimming.mp4 file is actually located in a different directory 2. Move the file to /Users/sgrider/Desktop if needed 3. Or provide the correct path where the swimming.mp4 file is located Once the file is in an accessible location, I'll be happy to convert it to MOV format for you.」 右の Finder 風ウィンドウは「Desktop」(biking.mp4・biking.mov)と「Documents」(swimming.mp4)の2つ

[6:05] さて、いまのデモに基づくと、roots には実は2つの異なる目的があることに気づくでしょう。

[6:10] 一方では、ユーザーが特定のファイルとフォルダへのアクセスを許可できるようにします。

🖥 [6:12] 画面: まとめスライド「Roots」。箇条書き4点: ・Allows users to specify specific files/folders that should be accessible ・Helps point Claude to specific files folders; avoids forcing Claude to search the whole filesystem ・You can provide roots to Claude through a tool, or just inject them into a prompt ・Your server has to ensure that files/folders that other tools try to access are contained within a root(太字強調) 右に実装コード: ```python

Helper function to see if a given path is contained within a root

async def is_path_allowed(requested_path: Path, ctx: Context) -> bool: roots_result = await ctx.session.list_roots() client_roots = roots_result.roots

if not requested_path.exists():
    return False

if requested_path.is_file():
    requested_path = requested_path.parent

for root in client_roots:
    root_path = file_url_to_path(root.uri)
    try:
        requested_path.relative_to(root_path)
        return True
    except ValueError:
        continue

return False

@mcp.tool() async def convert_video( input_path: str = Field(description="Path to the input MP4 file"), *, ctx: Context ): """Convert an MP4 video file to another format using ffmpeg"""

# Ensure the input file is contained in a root
if not await is_path_allowed(input_path, ctx):
    raise ValueError(f"Access to path is not allowed: {input_path}")

return await VideoConverter.convert(input_path, format)

```

[6:15] しかし他方で、もう一つの実にありがたい利点は、Claude をあなたのファイルシステムの特定の領域だけに集中させられることです。

[6:20] 見てきたとおり、Claude にアクセスさせたい特定のディレクトリやファイルに対して、完全修飾パスを打ち込む必要はありません。その代わりに、Claude が自律的に、利用可能な roots を見に行き、その roots の中を探索して特定のファイルを見つける、という判断ができるのです。

[6:36] ここでまだきちんと述べていないものの、理解しておくことが非常に重要な点が一つあります。roots という考え方はいくぶん「緩く」、その周辺には大した実装が存在しないのです。

[6:46] 言い換えると、どの MCP SDK の中にも、特定のファイルやフォルダへのアクセスを実際に自動で制限してくれるものは、何もありません。

[6:52] その代わり、ツールがファイルやフォルダにアクセスしようとするときにはいつでも、それがこれらの roots のいずれかに列挙・包含されていることを確認するのは、あなたの MCP サーバーの中で、あなたがやるべきことなのです。

[7:04] それは、この右上に見えるような関数——is path allowed のようなもの——を実装することで行えるでしょう。

[7:14] この関数は、要求されたパスを受け取ります。次にクライアントから roots のリストを取得し、ツールが使おうとしているパスがそれらの roots のいずれかの中に含まれているかを確認します。

[7:24] 含まれていれば——素晴らしい、アクセスを許可します。そうでなければ——ノー、アクセスは一切許可しません。

[7:28] 最後に触れておきたいのは、私は list roots のようなツールを実装して、Claude が任意のタイミングでそのツールを呼べるようにする形をお見せした、という点です。これは厳密には必須ではありません。

[7:39] roots のリストをそのまま全部、手動でプロンプトに放り込んでしまっても構いません。

[7:44] つまり、あのツールを作る必要はないのです。あれは、Claude が「自分がどのファイルとフォルダにアクセスできるのかを把握する必要がある」と判断したときに、いつでも roots の一覧を見られるようにしておく——私が便利だと感じている一つのパターンにすぎません。

英語逐語(ASR全文)

Our next topic is the root system. Roots allow users to grant a server access to some particular set of files and folders. You can really think of roots as being like a codified way of saying, hey, MCP server, you can access these files. But roots do more than just grant permission. Let me walk you through a quick example to help you understand how they work. In this example, I'm going to first show you a common problem that we would run into if roots did not exist. Once we understand the issue, we'll then see how roots solve that problem.

So in this example, we're going to have a very simple MCP server with one single tool called Convert Video. This tool is going to take in a path to a video file on the user's local machine, and it's going to convert that video file into some other file type, so maybe a MP4 to a MOV. Now, let's imagine how a user would actually make use of this. A user might type into some CLI application or something like that, asking Claude to convert a Viking.mp4 video file into MOV format. Claude would then take a look at the list of tools that are provided by this MCP server and see that the convert video tool is available. So Claude would probably decide, fantastic, I'm going to call that tool and I'm going to pass in a path of Viking.mp4 because that's exactly what the user asked for. The user asked for a conversion of a video file named Viking.mp4. And here's what I would probably expect to see in response from that tool. I would probably expect to see an error come back saying something like, there's no such Viking.mp4 file available.

So why would I expect to see that error exactly? Well, remember, the user's file system might be really complex. They might have a wide variety of different files and folders, tons of different documents all over the place. So a user might know that the biking.mp4 file is inside this movies directory, but there's no real way that Claude would understand that. So if a user just asked for biking.mp4, Claude doesn't really have the ability to search over the user's file system and figure out exactly where that particular document is. Now one way that we could solve this would be to just require a user to always pass in a full path to some video file. So we might say, you know what, if you want to use this tool, you must pass in a fully qualified path like movies slash biking mp4 and now probably this would work but it's not super convenient users don't really want to sit there and have to type out full paths to every file they ever try to access they really probably just want to put in something like convert this video and then just put in the name to it to some other format.

So now that we understand the issue let me show you how we could solve it by using the idea of roots so we're going to solve this problem by adding in some additional tools to our MCP server. We're still going to have the same convert video tool, but we're also going to add in a tool of read directory. I bet you could guess what that does. It's just going to list out all the files and folders in some specific directory. And then the third tool that we're going to have is going to be called list routes. This is going to return back a list of routes. A route is a file or a folder that the user has granted permission to be read ahead of time. So in other words, when they first start up this MCP server, they might pass in as command line arguments, a list of files and folders that the server is allowed to read. On top of that, we're also going to put in a little requirement in our other tools. We're going to add in some code to our read directory tool and the convert video tool. And we're going to make sure that whenever they try to access some directory or some video file, they only get to access files and folders that are contained inside of one of these different routes.

To help you understand how these three different tools come together, I put together a very small sample application. So let me give you a quick demo right away. To run this program, I execute uvrun main.py. And then the program is set up to accept a list of additional command line arguments. So if I want to, I could put in desktop like so. And that's going to be set up as a root inside of the MCP server. So I'm essentially saying that I want the server to be able to access the desktop folder and all the files inside of it. Then on the right hand inside over here, you can see my desktop directory inside. There is a file called Viking dot MP four. So now I'm going to run this and I'm going to ask Claude to turn the Viking dot MP four file into a MOV file. Now I'm going to run that and we'll see a couple of tool calls printed out. First, Claude is going to take a look at all the different files and folders it can access by calling the list roots tool. It's then going to see that there's only one directory you can access the desktop folder because that's the only one that I granted access to in this program. It's then going to try to read that directory and it's going to discover the viking.mp4 file inside there. And so at that point in time it now has everything it needs to actually successfully run the convert video tool. So it can run that tool with a path of the fully qualified path to my desktop folder viking.mp4.

Now remember roots are intended to limit what files and folders the MCP server can access. So in addition to that desktop folder, I also have a documents folder inside of here. Inside of that directory is a swimming.mp4 file. So I'm going to ask Claude to convert the swimming.mp4 file into a MOV as well. And this time around, I'm going to provide the full path to the mp4 file. So now in theory, Claude doesn't need to take a look at the list of routes. It can just directly call the convert video tool because it's already got a fully qualified path to the particular file it wants to convert. So I'm going to run this and we'll see what happens. So it's going to immediately try to call convert video, but it's going to return early because Claude is going to say, sorry, but I got an error around running that tool. It was not able to find the particular file. Claude is then going to try to take a look at the routes that are available, read the only route that is available, in this case, the desktop. And then Claude is going to realize, hey, it looks like I actually can't access that file at all. The only directory I can access is desktop. And if you want me to convert the swimming file, well, I just can't access it. So try to give me access or move the file or whatever else.

Now, based upon that demonstration, you might notice that roots really have two distinct purposes. On the one hand, they allow users to grant access to particular files and folders. But on the other hand, the other really nice benefit to them is that they allow Claude to focus on just particular areas of your file system. So as we saw, we don't have to put in fully qualified paths to some particular directory or file that we might want Claude to access. Instead, Claude can autonomously decide to take a look at the available routes and then search through those routes to find some particular file.

The one thing that I haven't quite mentioned here, but is very critical for you to understand, the idea of routes is kind of loose, and there's not a tremendous amount of implementation around them. So in other words, there's nothing inside of any MCP SDK that will actually automatically limit access to particular files and folders. Instead, it is up to you in your MCP server to make sure that whenever a tool tries to access a file or a folder, it is listed or contained inside of one of these different routes. And you might do that by implementing a function like you see on the top right hand side here, something like is path allowed. Inside that function, it takes in some requested path. It's then going to get a list of roots from the client and then see if the path that the tool wants to use is contained within one of those roots. And if it is, fantastic, we'll allow access. Otherwise, nope, we're not going to allow access at all. Now, the last thing I want to mention is that I showed implementing a tool of something like list roots and allowing Claude to call that tool at any point in time. That is not strictly required. You can also just take the list of roots and toss them all into a prompt manually. So you don't have to make that tool, it's just a pattern that I found was kind of helpful for allowing Claude to look at the list of roots whenever it decides that it needs to figure out what files and folders it can access.

↑ 目次へ戻る

MCP: Advanced Topics | Lesson 05「JSON message types」(全文ナレッジ)

動画 6分45秒 / 講師: Stephen Grider(コース講師) / 文字起こし: mlx_whisper large-v3-turbo / 2026-07-03

日本語ナレッジ(要点整理)

  • なぜこの回をやるか:本コースの大目標は streamable HTTP トランスポート(リモートホストの MCP サーバー接続)の理解。HTTP トランスポートには「サーバーができることの大きな制約」があり、その制約は MCP メッセージと標準入出力(stdio)トランスポートの土台がないと理解しづらい。だから先にこの2トピックをやる。
  • メッセージ=クライアントとサーバーが交換する JSON スニペット。MCP 仕様には目的別に多数のメッセージ型が定義されている。
  • 例:LLM がツールを呼ぶと決めた → クライアントが CallToolRequest を送る → サーバーがツールを実行し結果を CallToolResult に入れて返す。
  • メッセージ型の正本は仕様リポジトリgithub.com/modelcontextprotocol/modelcontextprotocol)の schema.ts
  • 仕様リポジトリは Python/TypeScript SDK のリポジトリとは別物
  • schema.ts一度も実行されない・どの SDK にも含まれない。TypeScript が型データの記述に便利だから TS で書かれているだけ。
  • CallToolRequestmethod: "tools/call"params: { name, arguments }。図にあった jsonrpcid は別の型 JSONRPCRequestjsonrpc: "2.0"id: string | number)側で定義されている。
  • ファイル最下部が最重要/* Client messages */(ClientRequest / ClientNotification / ClientResult)と /* Server messages */(ServerRequest / ServerNotification / ServerResult)のコメント区分。どちらの側が送るメッセージかを示している。
  • メッセージは大きく 2〜3カテゴリに分けられる: 1. Request–Result メッセージ:必ずペア(〜Request ↔ 〜Result)。送ったら応答を期待する(CallTool、Initialize、ListPrompts、ReadResource…)。 2. Notification メッセージ:イベント通知。応答不要(Progress、LoggingMessage、ToolListChanged…)。
  • この動画の最大の持ち帰りサーバー→クライアント方向に送られるメッセージ群が存在する(ServerRequest=Ping / CreateMessage / ListRoots / Elicit、ServerNotification=Cancelled / Progress / LoggingMessage / ResourceUpdated / ResourceListChanged / ToolListChanged / PromptListChanged)。いまは無関係に見えても、streamable HTTP を見るときに決定的に重要になる

実務ポイント(このレッスンから持ち帰るもの)

  1. メッセージ型の暗記法:「〜Request には必ず対の 〜Result がある。Notification には応答がない」。この2分類+「どちら側が送るか」の掛け算で全メッセージ型が整理できる。
  2. 仕様の正本は schema.ts。メッセージの正確な形(必須フィールド・method 名)を確かめるときは SDK のコードではなく modelcontextprotocol/modelcontextprotocol リポジトリの schema/…/schema.ts を検索する(動画では blob/main/schema/draft/schema.ts)。
  3. jsonrpc/id はレイヤーが別:ドメイン型(CallToolRequest 等)は method/params だけを定義し、JSON-RPC 封筒(jsonrpc: "2.0", id)は JSONRPCRequest/JSONRPCNotification が付与する。自前実装やデバッグ時のメッセージ検証はこの2層で見る。
  4. ServerRequest の4種(Ping / CreateMessage / ListRoots / Elicit)を覚える:sampling(CreateMessage)も roots(ListRoots)も「サーバーがクライアントに要求する」型。トランスポートがサーバー→クライアントの起点リクエストを運べないと、これらの機能が使えなくなる——次回以降の streamable HTTP の制約はまさにここに効く。
  5. 通知の代表例も方向で整理:クライアント発=Initialized / Cancelled / Progress / RootsListChanged、サーバー発=Progress / LoggingMessage / ResourceUpdated / ResourceListChanged / ToolListChanged / PromptListChanged / Cancelled。

本文(時系列・日本語逐語+画面差し込み)

🖥 [0:00] 画面: スライド「Our Plan」。左の下向き矢印に沿って3枚の角丸カードが縦に並ぶ(正確転記): - Messages Format — Examine communication between MCP servers and clients - Stdio Transport — See how the stdio transport receives and responds to messages - StreamableHTTP Transport — Understand critical limitations of the HTTP transport

(このスライドが [0:00]〜[0:56] の予告トーク全体の背景として表示され続ける)

[0:00] ここまでいくつかのトピックを終えました。先に進む前に、ちょっとした予告をさせてください。

[0:05] 実は、次の2〜3本の動画は MCP メッセージと標準入出力(standard IO)トランスポートに焦点を当てます。

[0:11] これらの動画は少し無味乾燥で退屈に見えるかもしれませんが、これらのトピックを扱うのには非常に重要な理由があります。

[0:19] このコースの大きなゴールの1つは、streamable HTTP トランスポートを理解してもらうことです。

[0:24] これは、リモートにホストされた MCP サーバーへクライアントが接続できるようにするものです。

[0:28] 問題は、HTTP トランスポートを使っているとき、MCP サーバーができることに大きな制約が生じるケースがあることです。

[0:37] その制約を理解するのはなかなか難しく、MCP メッセージと標準入出力トランスポートの確かな足場があれば、ずっと理解しやすくなります。

[0:47] だから、最初のこの2つのトピックに時間を使うのです。

[0:50] それでは、この前提を頭に置いた上で、MCP が通信に使うフォーマット、具体的にはメッセージのフォーマットを見ていきましょう。

[0:57] クライアントとサーバーは JSON を使って通信します。

🖥 [0:57] 画面: スライド「MCP Messages」——MCP Client ⇅ JSON Message ⇅ MCP Server の図+Example Message

text MCP Messages ・There are many types of messages, each designed to achieve a particular goal (list tools, call tools, get a resource, etc) ・The MCP specification defines a full list of all the different message types

json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "add", "arguments": {"a": 5, "b": 3} } }

[1:00] この JSON のスニペットをメッセージと呼びます。

[1:03] MCP 仕様の中には多くの異なるメッセージ型が定義されていて、それぞれが固有の目的のために設計されています。

[1:09] すぐに例をお見せしましょう。

[1:11] 例として、MCP クライアントに接続された言語モデルが、MCP サーバーの提供するツールを呼び出すと決めたら、クライアントはサーバーへメッセージを送ります。

🖥 [1:11〜1:38] 画面: シーケンス図「MCP Client ⇄ MCP Server」——左吹き出し「I need to call a tool!」、右吹き出し「I'll call that tool and respond with the result」

```json Call Tool Request { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "add", "arguments": {"a": 5, "b": 3} } }

Call Tool Result { "jsonrpc": "2.0", "id": 1, "result": { "content": [{"type": "text", "text": "8"}], "isError": false } } ```

[1:21] このタイプのメッセージは call tool request という名前です。

[1:24] この図の上側に call tool request の例があります。MCP サーバーはツールを実行し、その実行結果を call tool result というもう1つのメッセージ型に入れます。その例も図の下側にあります。

[1:38] さて、言ったとおり、MCP 仕様はすべての異なるメッセージ型の完全なリストを定義しています。完全なリストは GitHub にホストされている MCP 仕様リポジトリの中にあります。すぐ見に行きます。

[1:49] でもまず、これから見るこのリポジトリについてはっきりさせておくと、

[1:53] この仕様(specification)リポジトリは、Python や TypeScript の MCP SDK のような各種 SDK リポジトリとは別物です。

[2:01] この仕様リポジトリには、MCP 仕様がどう機能するかを記述したいくつかのドキュメントがあります。

[2:07] このリポジトリの中で、メッセージ型の完全なリストは TypeScript ファイルの中に書かれています。

[2:12] 念のためはっきりさせておくと、この TypeScript ファイルは何かに実行されることは一切ありません

[2:17] どの SDK にも含まれていません。

[2:19] 型が TypeScript で書かれているのは、TypeScript が型データを記述するのに本当に便利な言語だからにすぎません。

[2:26] このスキーマファイルには本当に面白い洞察が引き出せるので、すべてのメッセージ型を列挙したこのファイルをお見せしたいのです。

🖥 [2:26] 画面: スライド「MCP Specification」

```text MCP Specification github.com/modelcontextprotocol/modelcontextprotocol

・Defines how MCP clients and servers should behave ・Defines all the different valid message types Written in Typescript for convenience ```

[2:34] では、画面上部に見えるアドレスに移動します。

[2:37] このリポジトリの中の、あるパスに schema.ts というファイルがあります。

[2:42] schema.ts ファイルには、私たちが利用できるすべての異なるメッセージ型が入っています。

[2:47] このファイルの中で「call tool request」を検索してみます。

[2:51] はい、ありました。

[2:52] これが call tool request がどんな見た目かの例です。

🖥 [2:52〜3:03] 画面: GitHub modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts・検索「calltoolrequest」1/2件目(709〜725行目)

```ts /* * Used by the client to invoke a tool provided by the server. / export interface CallToolRequest extends Request { method: "tools/call"; params: { name: string; arguments?: { [key: string]: unknown }; }; }

/* * An optional notification from the server to the client, informing it that the list of tools it offers has changed… / export interface ToolListChangedNotification extends Notification { method: "notifications/tools/list_changed"; } `` (同ファイル6行目にexport type JSONRPCMessage =` も見えている)

[2:55] call tool request は tools/call という method を持たなければならず、それから、呼びたいツールの名前と渡す引数を持つ params オブジェクトが必要です。

[3:03] さっきお見せした図をよく見ると、そこには JSON RPC と ID も載っていることに気づくかもしれません。

🖥 [3:03] 画面: 先ほどの Call Tool Request / Result のシーケンス図に戻る("jsonrpc": "2.0""id": 1 を確認)

[3:10] それらは実際には、スキーマファイル内の JSON RPC request という別のオブジェクトで定義されています。

[3:16] それを調べてみると——はい、これが JSONRPCRequest です。ID も必須であることがわかります。

🖥 [3:16〜3:22] 画面: schema.ts・検索「jsonrpcrequest」2/2件目(58〜75行目)

```ts * A uniquely identifying ID for a request in JSON-RPC. */ export type RequestId = string | number;

/* * A request that expects a response. / export interface JSONRPCRequest extends Request { jsonrpc: typeof JSONRPC_VERSION; id: RequestId; }

/* * A notification which does not expect a response. / export interface JSONRPCNotification extends Notification { jsonrpc: typeof JSONRPC_VERSION; } ```

[3:23] さて、このファイル全体の中で本当に注目してほしいのは、一番下に定義されているいくつかの型です。

[3:28] ファイルの一番下の、そのまた一番下までスクロールします。

[3:31] そこから少しだけ上にスクロールすると、server messages というコメントと client messages というコメントが見つかります。

[3:38] この2つのセクションの中には、似たような名前の型があることに気づくでしょう。

🖥 [3:38〜3:52] 画面: schema.ts 1335〜1354行目(シンボルパネルに type ClientResult 定義 1338行目)

```ts | InitializedNotification | RootsListChangedNotification;

export type ClientResult = EmptyResult | CreateMessageResult | ListRo…

/ Server messages / export type ServerRequest = | PingRequest | CreateMessageRequest | ListRootsRequest | ElicitRequest;

export type ServerNotification = | CancelledNotification | ProgressNotification | LoggingMessageNotification | ResourceUpdatedNotification | ResourceListChangedNotification | ToolListChangedNotification | PromptListChangedNotification; ```

[3:42] client messages の中には、client request、client notification、client result があります。

[3:48] そして server セクションの中には、server request、server notification、server result があります。

[3:53] では、このファイル内のこれらの型——特に client と server のセクション——が本当は何を伝えているのかを理解するための図をお見せします。

[4:01] はい、これです。

[4:03] あの型ファイルの中では、これらすべてのメッセージ型を2〜3個のカテゴリに分けることができます。

🖥 [4:03〜5:09] 画面: スライド「Request - Result Messages / Notification Messages」

```text Request - Result Messages Notification Messages Message types where we make a Message types where we are informing request and expect to get a the client or server about some event, response back but don't need a response

Call Tool Request ⟷ Call Tool Result Progress Notification List Prompts Request ⟷ List Prompts Result Logging Message Notification Read Resource Request ⟷ Read Resource Result Tool List Changed Notification Initialize Request ⟷ Initialize Result Resource Updated Notification ```

[4:10] まず、この図の左側にあるのが request–result メッセージです。

[4:15] これは必ずペアで登場するメッセージ型です。

[4:18] 名前は必ず「なんとか Request」で、それと対になる「なんとか Result」というメッセージ型があります。

[4:25] 例として、call tool request は call tool result とペアになります。

[4:30] initialize request は initialize result とペアになります。

[4:34] これらのメッセージ型が何をしてくれるかは、きっと想像がつくでしょう。

[4:37] クライアントまたはサーバーへ送り出すメッセージと、その応答として返ってくると期待するメッセージの型を記述しています。

[4:45] つまり、call tool request を送ったなら、call tool result が返ってくると期待します。

[4:51] もう1つの扱うメッセージ型は notification(通知)メッセージです。

[4:55] これらはむしろイベントのようなものです。

[4:57] 直前に何かが起きたことをクライアントまたはサーバーに伝えますが、応答は特に必要ありません

[5:02] progress notification、logging message notification、tool list changed notification などの型があります。

[5:09] さて、さっき見ていた型ファイルに戻ると、server messagesclient messages という見出しがあったのに気づいたでしょう。

🖥 [5:09〜5:45] 画面: schema.ts 1323〜1342行目(ClientRequest のメンバー一覧〜ClientNotification 選択中)

```ts | ListPromptsRequest | ListResourcesRequest | ListResourceTemplatesRequest | ReadResourceRequest | SubscribeRequest | UnsubscribeRequest | CallToolRequest | ListToolsRequest;

export type ClientNotification = | CancelledNotification | ProgressNotification | InitializedNotification | RootsListChangedNotification;

export type ClientResult = EmptyResult | CreateMessageResult | ListRo…

/ Server messages / export type ServerRequest = | PingRequest ```

[5:17] これらのコメントと、それぞれの中にある型は、そのタイプのメッセージがクライアントとサーバーのどちらから送られることを意図しているかを示しています。

[5:24] たとえば、この client request 型は、クライアントから送られると期待されるすべてのリクエストの型を記述しています。

[5:31] 同様に、こちらの下は、サーバーから送られると期待されるすべてのリクエスト。

[5:36] server notifications も同じで、これらはすべてサーバーが発行する通知。

[5:40] そしてこれらは、クライアントが発行する通知です。

[5:45] というわけで、全体をこんな図にまとめられます。これはすべてのメッセージ型を示しているわけではなく、

🖥 [5:45〜6:44] 画面: まとめ図「MCP Client ⇄ MCP Server」(方向つき・全型網羅ではない)

```text ── クライアント起点のペア ── Call Tool Request ⟷ Call Tool Result List Prompts Request ⟷ List Prompts Result Read Resource Request ⟷ Read Resource Result Initialize Request ⟷ Initialize Result

── サーバー起点のペア ── Create Message Result ⟷ Create Message Request(サーバー→) List Roots Result ⟷ List Roots Request(サーバー→)

── 通知(応答なし)── クライアント発: Initialized Notification / Cancelled Notification サーバー発: Progress Notification / Logging Notification 「Notifications don't require a response」 ```

[5:50] MCP クライアントから来ることが意図されたリクエストがあり(その例がここ)、サーバーから来ることが意図されたリザルトがあり、

[5:59] クライアントから送られることが意図されたリザルトがあり、サーバーから送られることが意図されたリクエストがある、ということを示すためのものです。

[6:08] さあ、ここが理解してほしい肝心なところ、streamable HTTP トランスポートの話を始めるときに本当に重要になる、この動画の最大の持ち帰りです。

[6:18] 決定的に重要なのは、サーバーからクライアントへ送られることが意図された一群のメッセージが存在するということです。

[6:24] ここにある server request 型と server notification 型——これらはすべて、サーバーからクライアントへ送られるメッセージです。

🖥 [6:25〜6:41] 画面: 再び GitHub の schema.ts(1338〜1357行)。1341行 export type ServerRequest = がハイライトされ、右に「All Symbols」パネル——type ServerRequest / タブ「Definition / Search」/ In this file「1341 export type ServerRequest =」/ 「Search for this symbol」。本文には ServerRequest(PingRequest / CreateMessageRequest / ListRootsRequest / ElicitRequest)・ServerNotification(7種)に続いて、これまで映っていなかった 1356行 export type ServerResult = / 1357行 | EmptyResult(次行に InitializeResult) も見える

[6:34] 今はまったく無関係に思えるでしょう。「それを知って何が重要なの?」と。

[6:38] 繰り返しますが、これは streamable HTTP トランスポートを見始めると本当に重要になります。

[6:43] だから、しばらく頭の片隅に置いておいてください。

英語逐語(ASR全文)

We have gotten through a couple of topics, and before we move on, I just want to give you a little heads up. You see, the next two or three videos are going to be focused on MCP messages and the standard IO transport. These videos might seem a little bit dry and a little bit boring, but there's a very important reason that we're going to cover these topics. One of the big goals of this course is to help you understand the streamable HTTP transport, which allows clients to connect to remotely hosted MCP servers. The issue is that in some cases, there are some big limitations on what a MCP server can do when it is using the HTTP transport. Understanding those limitations can be rather challenging, and it's a lot easier to understand if you have a solid footing in MCP messages and a standard IO transport. So that's why we're going to spend some time on these first two topics. So with all this in mind, let's take a look at the format that MCP uses to communicate, specifically the messages format. Clients and servers communicate using JSON. We refer to these snippets of JSON as messages. There are many different types of messages defined inside the MCP spec, each of which are designed for a distinct purpose. So let me show you an example right away. As an example, if a language model connected to a MCP client decides to call a tool provided by MCP server, the client will send a message over to the server. This type of message is named a call tool request. and I've got an example of a call tool request at the top of this diagram. The MCP server will then run the tool and then put the result of the tool run into another message type named a call tool result. And I've got an example of that on the bottom of the diagram as well. Now, as I mentioned, the MCP spec defines a full list of all the different message types. You can find the full list inside the MCP spec repository hosted on GitHub. And we're going to take a look at that in just a moment. But first, to be clear about this repository that we're going to take a look at, This specification repo is separate from all the different SDK repositories, like the Python or the TypeScript MCP SDKs. This specification repo has some different documents that describe how the MCP spec works. So in this repository, the full list of message types is written inside of a TypeScript file. Again, to be clear here, this TypeScript file isn't ever executed by anything. It's not included in any of the different SDKs. The types are just written in TypeScript because TypeScript is a really convenient language for describing type data. So I'd like to show you this schema file that lists out all the different message types, just because there are some really interesting insights to be pulled out of it. So I'm going to navigate to the address you see up here at the top of the screen, and then inside this repository, you will find at a certain path a file called schema.ts. The schema.ts file contains all the different message types that are available to us. So inside this file, I can do a search for call tool request. And here we go. Here's an example of what a call tool request looks like. So a call tool request must have a method of tool slash call, and then a params object that has the name of the tool we want to call and some arguments to pass into it. You might notice that in the diagram I showed you a moment ago back over here, there's also this JSON RPC and an ID on there as well. Those are actually defined on a separate object back inside the schema file called a JSON RPC request. So if I look that up, I'll see, okay, here is JSON RPC request, and it also must have an ID as well. Now, out of this entire file, what I really want you to focus on are some different types that are defined at the very bottom. So I'm going to scroll down to the very bottom, very, very bottom of the file. I'll then scroll up just a little bit, and I'm going to find a comment that says server messages and a comment that says client messages. You'll notice that inside these two sections, there are some kind of similarly named types. So inside of client messages, there are client request, client notification, and client result. And then down inside the server section, there's server request, server notification, and server result. So let me show you a diagram that can help you understand what these different types inside this file and specifically the client and server sections are really telling us. Okay, here we go. So inside that type file, we can kind of divide all these different types of messages into two or three different categories. First, on the left-hand side of this diagram, there are request result messages. These are message types that always come in pairs. They're always going to have a name of something something request, and then a paired message type of something something result. So as an example, we have call tool request that gets paired up with a call tool result. We have a initialize request that gets paired up with a initialize result. All these different message types, I bet you could guess what they do for us. They describe messages that we send off either to a client or a server, and then the type of message that we expect to get back in response. So if I ever send off a call to request, I would expect to get back a call tool result. The other type of messages that we're going to work with are notification messages. These are more like events. They tell the client or the server about something that just occurred, but they don't really need a response. So we have types like progress notification, logging message notification, tool is change notification, and so on. Now back inside of the type file that we were looking at a moment ago, you may have noticed that we have the headers here of server messages and client messages. These comments and the types inside of each one are meant to indicate whether or not these types of messages are meant to be sent by the client or the server. So for example, this client request type describes all of the different types of requests that are expected to be sent from the client. and likewise down here these are all requests that are expected to be sent from the server and the same thing for server notifications these are all notifications issued by the server and these are all notifications that are issued by the client so we can kind of summarize everything in a diagram a little bit like this now this doesn't show every type of message i just mean to indicate that there are some requests that are meant to come from the mcp client and i've got examples of those right here there are results that are meant to come from the server there are results that are meant to be sent from the client, the requests that are meant to be sent from the server, and so on. All right, so here's the critical thing that I want you to understand, the big takeaway from this video that's going to become really important as we start to talk about the streamable HTTP transport. The big critical thing to understand here is that there are a bunch of different messages that are meant to be sent from the server over to the client. So the server request types right here and the server notification types, these are all messages that are going to be sent from the server over to the client. And I know right now that might seem like just completely irrelevant. Just why is that important to know at all? Again, this is going to become really important as we start to look at the streamable HTTP transport. So just keep that in mind for a little bit.

(注:ASR の「call to request」=call tool request、「tool is change notification」=画面上は ToolListChangedNotification。「a certain path」の実体は schema/draft/schema.ts。)

↑ 目次へ戻る

MCP: Advanced Topics | Lesson 06「The STDIO transport」(全文ナレッジ)

動画 9分28秒(本コース最長級) / 講師: Stephen Grider(コース講師) / 文字起こし: mlx_whisper large-v3-turbo / 2026-07-03

日本語ナレッジ(要点整理)

  • トランスポート=JSON メッセージを実際にクライアント⇄サーバー間で動かす仕組み。JSON 自体は HTTP でも WebSocket でも(極端な話ハガキでも)運べる。MCP 仕様で「何で運ぶか」を指すのが transport。
  • stdio トランスポートの仕組み
  • クライアントが MCP サーバーをサブプロセスとして起動する(=プロセスのハンドルを持つ)
  • クライアント→サーバー:サーバーの stdin に書き込む
  • サーバー→クライアント:サーバーが stdout に書き込み、クライアントがそれを監視する
  • 決定的な長所:クライアントもサーバーも、いつでもどちらからでも通信を開始できる
  • 唯一の大きな欠点:クライアントとサーバーが同一マシン上でしか使えない
  • デモFastMCP(name="Demo Server")add ツール1本だけの極小サーバーを uv run server.py で起動し、別クライアントを作らずターミナルから直接 JSON を貼り付けて対話する(ターミナル入力=そのプロセスの stdin、画面出力=stdout)。
  • 接続初期化は必ず3メッセージ(MCP 仕様の規定): 1. initialize request(クライアント→サーバー・最初は必ずこれ) 2. initialize result(サーバー→クライアント・即返る) 3. initialized notification(クライアント→サーバー・通知なので応答なし)
  • この3通の後で初めて call tool request や prompt listing request が実行できる。
  • デモの tools/call(add, a=5, b=3, _meta.progressToken 付き)には応答が3連で返った:①notifications/message(ログ・info "Preparing to add...")②notifications/progress(80.0/100.0)③call tool result(content text "8")。①②はサーバー→クライアントの通知。
  • トランスポートが処理すべき4シナリオ: 1. クライアント→サーバーの起点リクエスト → stdio では「stdin に書く」 2. サーバー→クライアントの応答 → 「stdout に書く」 3. サーバー→クライアントの起点リクエスト(例:sampling の create message request) → 「stdout に書く」 4. クライアント→サーバーの応答 → 「stdin に書く」
  • 結論:stdio は4シナリオ全部を自明に満たす=どちら側からでも任意時点で通信を開始できるのが素晴らしい点。streamable HTTP ではこれが成り立たない場面がある(サーバーが起点リクエストをクライアントに送れないケースがある)——次回以降の核心。

実務ポイント(このレッスンから持ち帰るもの)

  1. stdio サーバーはターミナルから素手でデバッグできるuv run server.py → JSON-RPC を1行ずつ貼るだけ。クライアント実装なしでサーバー単体の挙動(初期化・ツール呼び出し・通知)を確認できる。
  2. 初期化3点セットの JSON はそのままコピペで使える(画面転記・下記本文 [2:36] 参照):initialize(id:1)→ 返答確認 → notifications/initialized → 以後 tools/call 等。自作サーバーのスモークテスト定型。
  3. _meta: {"progressToken": ...} を付けると progress notification が返る。ログは ctx.info()notifications/message、進捗は ctx.report_progress(80, 100)notifications/progress。通知には id が無く、応答も不要。
  4. stdout はプロトコル専用チャネルという理解が本質(メッセージ=stdout に書かれた JSON)。このデモの print() 群は「人間がクライアント」だから成立している。実クライアント相手の stdio サーバーで stdout に非 JSON を print すると壊れるので、ログは stderr / ctx.info() に出す。
  5. 「4シナリオ」チェックリストはあらゆるトランスポート評価のレンズ:特にシナリオ3(サーバー起点リクエスト=sampling / roots / elicitation が依存)を運べるかが streamable HTTP 理解の焦点になる。

本文(時系列・日本語逐語+画面差し込み)

[0:00] 少し前に話したとおり、クライアントとサーバーは、メッセージと呼ぶ JSON オブジェクトを交換して通信します。

[0:07] さて、JSON というもの自体は、クライアントとサーバーの間で実にさまざまな方法で送ることができます。

[0:13] たとえば HTTP リクエストでも、WebSocket でも。

[0:18] 極端な話、JSON をハガキに書いて誰かに送り、その人に手でサーバーへ打ち込んでもらうことだってできます。

[0:25] つまり、JSON を送受信する方法は本当にたくさんあるのです。

[0:29] MCP 仕様では、クライアントとサーバーの間で実際に JSON を動かすために使うものをトランスポートと呼びます。

[0:37] MCP サーバーやクライアントを開発し始めの頃に非常によく使われるのが、標準入出力(standard IO)トランスポートです。

🖥 [0:37〜1:29] 画面: スライド「'Stdio' transport」

text 'Stdio' transport ・Client launches the MCP server as a subprocess ・Client sends messages to the MCP Server using the server's 'stdin' ・Server responds by writing to 'stdout' ・Critical: either the server or the client can send a message at any time! ・Only appropriate when the client and server are running on the same machine 右図:MCP Client → [stdin](Client sends message to server)/[stdout]→ MCP Client(Server sends message to client)、下に MCP Server

[0:43] このトランスポートの考え方は、クライアントがサーバーを別プロセスとして起動するというものです。

[0:48] そうすることで、クライアントはそのプロセスのハンドルを持ちます。

[0:52] そしてクライアントは、サーバーの標準入力(stdin)チャネルにメッセージを書き込むことでサーバーへ送信でき、

[0:57] サーバーの標準出力(stdout)チャネルを監視することでメッセージを受信できます。

[1:02] 標準入出力トランスポートの本当に良いところは、クライアント側からでもサーバー側からでも、通信を非常に簡単に開始できることです。

[1:11] 言い換えると、任意の時点で、クライアントは stdin に書くことでサーバーへメッセージを送れますし、

[1:16] サーバーは stdout に書くことでクライアントへ返答できます。

[1:20] しかし標準入出力トランスポートには1つ大きな欠点があります。それは、クライアントとサーバーが同じ物理マシンで動いているときにしか使えないことです。

[1:29] 標準入出力トランスポートをよりよく理解してもらうために、簡単なデモをお見せします。私はとても小さな MCP サーバーを用意しました。

[1:34] 上のほうに、状況を理解しやすくするための print 文がいくつかありますが、それを除けば、このサーバーには大したものはありません。

🖥 [1:34〜2:03] 画面: VS Code server.py(transport-stdio リポジトリ)——サーバーの本体はこれだけ(左のターミナルには uv run server.py を入力済み)

```python from mcp.server.fastmcp import FastMCP, Context import asyncio

mcp = FastMCP(name="Demo Server", log_level="ERROR")

@mcp.tool() async def add(a: int, b: int, ctx: Context) -> int: await ctx.info("Preparing to add...") await asyncio.sleep(2) await ctx.report_progress(80, 100)

return a + b

if name == "main": mcp.run(transport="stdio") ```

「上のほうの print 文」は後続フレームで確認できる(7〜17行目に挿入されている版):

```python print("=== MCP STDIO Transport Demo ===")

print("Example messages:") print( '{"jsonrpc": "2.0", "method": "initialize", "params": {"pr…' ) print('{"jsonrpc": "2.0", "method":"notifications/initialized"…') print( '{"jsonrpc": "2.0", "method": "tools/call", "params": {"_m…' ) print("\n" + "=" * 50 + "\n") ```

[1:42] サーバーを作り、ツールを1つだけ定義し、標準入出力トランスポートでサーバーを起動しているだけです。

[1:47] さて、標準入出力トランスポートを使ってこのサーバーに接続するクライアントを作ることもできますが、

[1:52] 別個のクライアントを一切作らずに、ターミナルから直接サーバーに接続することもできます。どういう意味か、お見せしましょう。

[2:03] ここで uv run server.py を実行すると、あのサーバーが起動します。

[2:07] これでサーバーは stdin を待ち受け、送信メッセージがあれば stdout に書き出す状態になりました。

[2:12] ところで、ご存じないかもしれませんが、ターミナルでは、プログラムを実行中に下の部分に何かを打ち込むと、その実行中プログラムの stdin に書き込んでいることになります。

🖥 [2:12〜2:29] 画面: ターミナル——「下に打ち込めば実行中プログラムの stdin に入る」ことの実演として、サーバー出力の ===== 区切り線の下にランダムな文字列(lkasjdflkasjfdlkajsfdl)をそのまま打鍵してみせている(この直後 [2:32] にサーバーを再起動してから JSON 貼り付けデモに入る)

[2:22] つまり、実際にこの下に JSON メッセージを貼り付けて実行すれば、それはサーバーへの入力として受け取られるのです。

[2:29] それでは簡単なデモをやってみます。

[2:32] まずサーバーをサッと再起動します。

[2:36] それから、上にあるこの最初の例のメッセージをコピーして、ここに貼り付けます。

🖥 [2:36] 画面: ターミナル——サーバーが起動時に print した「Example messages:」(コピペ用サンプル3通)

```text Example messages: {"jsonrpc": "2.0", "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "demo-client", "version": "1.0.0"}}, "id": 1}

{"jsonrpc": "2.0", "method":"notifications/initialized"}

{"jsonrpc": "2.0", "method": "tools/call", "params": {"_meta": {"progressToken": "abc123"}, "name": "add", "arguments": {"a": 5, "b": 3}}, "id": 3}

================================================== ```

[2:42] Enter を押すと、ほぼ瞬時に応答が返ってきます。

🖥 [2:42〜2:54] 画面: ターミナル——1通目(initialize request)を貼り付け → 即座に initialize result が返る

text {"jsonrpc": "2.0", "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "demo-client", "version": "1.0.0"}}, "id": 1} {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"Demo Server","version":"1.9.3"}}}

[2:47] ここで私はメッセージを stdin に書き込み、stdout 上の出力メッセージがそこに見えている、というわけです。

[2:54] 次に、もう1通メッセージを送ります。

🖥 [2:54〜3:02] 画面: ターミナル——2通目 {"jsonrpc": "2.0", "method":"notifications/initialized"} を貼り付け(ハイライト表示)→ 応答なし

[2:58] はい、送りました。

[2:59] 今回は何の応答も表示されません

[3:02] そして最後に、もう1通メッセージを実行します。

🖥 [3:02〜3:17] 画面: ターミナル——3通目(tools/call)を貼り付け → 応答3連

text {"jsonrpc": "2.0", "method": "tools/call", "params": {"_meta": {"progressToken": "abc123"}, "name": "add", "arguments": {"a": 5, "b": 3}}, "id": 3} {"method":"notifications/message","params":{"level":"info","data":"Preparing to add..."},"jsonrpc":"2.0"} {"method":"notifications/progress","params":{"progressToken":"abc123","progress":80.0,"total":100.0},"jsonrpc":"2.0"} {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"8"}],"isError":false}}

[3:08] これを実行すると、すぐに応答が1つ、その後もう1つ、さらにもう1つと返ってきます。

[3:13] 合計で3つのオブジェクトです。

[3:14] 1つ目、2つ目、3つ目。

[3:18] さて、これらのメッセージを貼り付けながら私が実際に何をしていたのかを理解するために、いくつか図をお見せします。

[3:23] はい、こちらに戻ってきました。

[3:25] 左側に「MCP クライアント」とラベルを付けたものがありますが、実際にはこれはターミナルでメッセージを貼り付けている私自身です。

[3:31] メッセージを貼り付けることで、私は MCP サーバーの stdin に書き込んでいます。

[3:35] サーバーは与えられたメッセージを処理し、場合によっては結果を stdout に print するだけの形で送り返します。それが私のターミナルに直接表示されるのです。

[3:44] 私が最初に送り込んだメッセージは、initialize request と呼ばれるものでした。

🖥 [3:44〜4:40] 画面: シーケンス図「MCP Client ⇄ MCP Server」

text Initialize Request ──────────────▶ ◀──────────── Initialize Result Initialized Notification ─────────▶ Notification, no result comes back

[3:48] ここで少し背景を説明させてください。

[3:50] クライアントが最初にサーバーへ接続するとき、MCP 仕様は「3種類のメッセージを順番にやり取りしなければならない」と定めています。

[3:58] 一番最初のメッセージは、必ず initialize request でなければなりません。

[4:02] 繰り返しますが、それが最初に私がここに貼り付けたものです。

[4:05] あれが initialize request でした。

[4:08] リクエスト型のメッセージを送ったのだから、リザルトが返ってくると期待します。

[4:12] まさにそのとおりのことが起きました。

[4:14] initialize result と呼ばれるものが返ってきました。あのテキストがそれです。

[4:20] さらに MCP 仕様は、これらの最初のメッセージを交換した後、クライアントからサーバーへ initialized notification も送らなければならない、と定めています。

[4:27] 思い出してください、通知(notification)は応答を必要としません

[4:33] 私はあそこで initialized notification を送りました。そして見てのとおり、

[4:39] 即座の応答は何も返ってきませんでした。繰り返しますが、それは単に通知が応答を必要としないからです。

[4:43] この3つのメッセージを交換し終えると、MCP サーバーへの接続は初期化済み(initialized)と見なされます

[4:49] そしてその時点から、call tool request でも prompt listing request でも、やりたいことを何でも実行できるようになります。

🖥 [4:49〜5:20] 画面: フルセッションのシーケンス図(スライド版・実行結果とは通知の内訳が異なる)

text Initialize Request ──▶ / ◀── Initialize Result Initialized Notification ──▶(Notification, no result comes back) Tool Call Request ──▶ ◀── Logging Message Notification ◀── Logging Message Notification ◀── Tool Call Result

[5:02] 私の場合は、見てのとおり call tool request を送りました。一番下のあれです。

[5:09] 引数 5 と 3 で add ツールを呼び出そうとしました。

[5:15] さて、私が add ツールを作ったときの作りとして、いくつかのログを送り返してから、最終的に call tool result を返すようにしてあります。

[5:21] ログを見てみると——実際には、あの図に出ているものから少し変えてあります。まず message notification、つまりログ出力が返ってきました。あそこから来ています。私のログ文です。

🖥 [5:21〜5:48] 画面: ターミナル——応答3連の精読(notifications/message の行をハイライト)

text {"method":"notifications/message","params":{"level":"info","data":"Preparing to add..."},"jsonrpc":"2.0"} ← ①ログ通知 {"method":"notifications/progress","params":{"progressToken":"abc123","progress":80.0,"total":100.0},"jsonrpc":"2.0"} ← ②進捗通知 {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"8"}],"isError":false}} ← ③call tool result(8)

[5:31] 次に progress(進捗)が返ってきました。あそこの progress の更新表示です。

[5:37] これらは両方とも、サーバーからクライアントへ送られている通知です。

[5:42] そして最後に、call tool response があそこに返ってきました。

[5:44] その中には 3 足す 5 の計算結果が入っています。8 が見えますね。

[5:48] さて、標準入出力トランスポートの実例を見たところで、このトランスポートまわりで指摘しておきたい本当に重要な考え方がいくつかあります。streamable HTTP トランスポートを見始めるときに、非常に大事になってくるものです。

[6:03] まず、少し前にこの図をお見せしました。この図を見せたのは、クライアントからサーバーへ送られることを意図したメッセージ型と、サーバーからクライアントへ送られることを意図した型があると理解してもらうためでした。

🖥 [6:03〜6:15] 画面: Lesson 05 のまとめ図を再掲(クライアント起点ペア4種/サーバー起点ペア Create Message・List Roots/通知 Initialized・Cancelled・Progress・Logging、「Notifications don't require a response」)

[6:15] この図から本当に理解してほしかったもう1つのことは、特に左上のこれらのメッセージには、クライアントがサーバーとの通信を開始しているとイメージできる場面がある、ということです。

[6:24] つまりクライアントが「私はリクエストを送る。応答が返ってくることを期待する」と言っている。

[6:34] 同様に、MCP サーバーの側が最初のリクエストを送り出して、応答が返ることを期待する場面もあります。

[6:39] それが例えば create message request——sampling を開始するのに使われるものです。

[6:45] つまりサーバーはこのリクエストをクライアントへ送り出し、何らかのリザルトが返ってくることを期待する必要があります。

[6:51] これをもう少し明確な言い方にすると、こういうことです。標準入出力トランスポートのような各種トランスポートには、処理できなければならないシナリオが実は4つあります。

🖥 [6:51〜7:26] 画面: スライド「How can we implement each of these with stdio?」——4シナリオ

text How can we implement each of these with stdio? ┌ Initial request from Client → Server ┐ ┌ Response from Server → Client ┐ ┌ Initial request from Server → Client ┐ ┌ Response from Client → Server ┐ 右図:MCP Client/stdin・stdout/MCP Server(stdio 図の再掲)

[6:57] ①クライアントからサーバーへの開始(起点リクエスト)を処理できること。②サーバーからクライアントへ応答を送れること。

[7:11] ③サーバーがクライアントへ起点リクエストを送れること。④同様に、クライアントが応答を返せること。

[7:22] では、この4つのシナリオをすべて順に見て、標準入出力トランスポートでどう実装するかを考えましょう。

[7:26] まず、クライアントからサーバーへの起点リクエスト

🖥 [7:26〜7:45] 画面: シナリオ①をハイライト——クライアント吹き出し「I need to send a message to the server!」+「Write to stdin」ラベル

[7:31] このシナリオは、クライアントが何かをサーバーへ送りたいとき——たとえば call tool request のようなもの——ならいつでも該当します。

[7:36] これを標準入出力トランスポートで実装するには、stdin に書くだけでいい。MCP サーバーはそのメッセージを受け取り、処理して、うまくいけば応答を組み立てます。

[7:46] 応答ができたら、stdout にメッセージを書くだけで応答します。(=シナリオ②)

[7:52] 次に3つ目のシナリオ、サーバーからクライアントへ送られる起点リクエストです。

🖥 [7:52〜8:12] 画面: シナリオ③をハイライト——サーバー吹き出し「I need to send a message to the client」+「Write to stdout」ラベル

[7:58] これは、たとえばサーバーが sampling をやりたい場面です。それにはサーバーがクライアントへ何らかの起点メッセージを送る必要があります。

[8:06] サーバーがクライアントへ起点リクエストを送る必要があるときは、stdout に書くだけでいい。

[8:12] そして同様に、応答するには、クライアントは stdin に書くだけでいいのです。(=シナリオ④)

🖥 [8:12〜8:23] 画面: シナリオ④をハイライト——クライアント吹き出し「I need to respond to the message I just got」+「Write to stdin」ラベル

[8:18] さて、この数分間の話と、いま見せた一連の図は、おそらく少し混乱させたと思います。

[8:27] 「起点リクエストだの応答だの、この話は一体何なんだ?」と思っているでしょう。

[8:33] ポイントはこうです。この話全体の目的は、標準入出力トランスポートが本当に素晴らしいのは、任意の時点でクライアントからでもサーバーからでも通信を開始できるからだ、と理解してもらうことです。

[8:44] どちらの側も、いつでもリクエストを送って、応答が返ることを期待できる。

[8:49] そして肝心なのはここです。streamable HTTP トランスポートでは、これが成り立たなくなります。

🖥 [8:49〜9:11] 画面: 4シナリオのスライドで「Initial request from Server → Client」(シナリオ③)を再びハイライト——streamable HTTP で問題になるのはまさにこの行

[8:54] streamable HTTP トランスポートがこの状況を許さないシナリオがあるのです。

[9:02] サーバーがクライアントへ起点リクエストを送れないケースがある。

[9:06] それが streamable HTTP トランスポートについて理解しにくい、厄介なところです。

[9:12] それでは、ここで一旦区切りにします。戻ってきたら HTTP トランスポートを見始めます。

[9:17] そしてこの特定のシナリオを目にして、「なるほど、確かにここには少し厄介なものがあるな」と、自分の MCP サーバーを開発するときに必ず理解しておくべきことを確認していきます。

英語逐語(ASR全文)

As we discussed a moment ago, clients and servers communicate by exchanging JSON objects that we refer to as messages. Now, JSON in general can be transmitted between a client and a server in a wide variety of different ways. For example, we can make a HTTP request. We could use WebSockets. We could even write out a postcard with some JSON and then send it to someone else and have them manually type that JSON into a server. So there really are a tremendous number of ways to send and receive JSON. In the MCP spec, the thing that we use to actually move JSON between a client and a server is referred to as the transport. When you are initially developing an MCP server or a client, a very commonly used transport is the standard IO transport. The idea with this transport is that our client is going to launch a server as a separate process. By doing so, the client has a handle on that process. And the client can send messages into the server by writing them into the server's standard end channel. and it can receive messages by watching the server's standard out channel. Something that is really nice about the standard IO transport is that communication between the client or the server can be very easily initiated on either side. So in other words, at any point in time, the client can communicate or send a message off to the server by writing to standard in, and the server can communicate back to the client by writing to standard out. But the standard IO transport also has one big downside, and that is that we can only make use this transport when the client and the server are running on the same physical machine. To help you better understand the standard IO transport, let me give you a quick demonstration. I put together a very small MCP server. I've got some print statements up here at the top just to help you understand what's going on, but besides those print statements, the server doesn't really have a whole lot to it. I'm creating a server, I'm defining one single tool, and then starting the server up with a standard IO transport. Now I could create a client to connect to the server using the standard IO transport, but I can also just connect to the server directly from my terminal without creating any distinct separate client. Let me show you what I mean by that. If I run uvrun server.py right here, it's going to start up that server. And now that server is going to be listening to standard in and then writing any outgoing messages to standard out. Well, in my terminal, in case you don't know, whenever I run a program, if I type anything down here, I am writing to standard in of this running program. So I can actually paste in JSON messages down here at the bottom, execute them, and they will be received as input to the server. So let me give you a quick demonstration of that. I'm going to restart the server really quickly. And then I'm going to copy this first example message up here. I'm going to paste it right here. And it's going to occur almost instantly when I hit enter, I'm going to immediately get a response back. So I've written to standard in right here with a message, and I am seeing a output message on standard out right there. I'm then going to send another message. Here we go. And this time around, I'm not going to see any response. And then finally, I'm going to run one more message down here. And when I run this one, I'll immediately get back one response and then another response and another response after that. So three objects in total. There's one, there's two, and there's three. So let me show you a couple diagrams to help you understand what I'm really doing as I paste these different messages in. Okay, so back over here. On the left-hand side, I've got what I've labeled my MCP client, but really it's just me at my terminal pasting messages in. By pasting those messages in, I'm writing to standard in of the MCP server. The server is then going to process these given messages and then possibly send a result back by just printing it to standard out, which I see directly printed at my terminal. The first message that I sent in was something called an initialized request. Let me just give you a little bit of backstory here. Whenever a client first connects to the server, the MCP spec says that we must send a sequence of three different messages back and forth. The very first message must always be an initialized request. And again, that is what I initially pasted in right here. That was initialized request. Because I sent in a request type message, I would expect to get back a result. And that's exactly what happened. I got back something referred to as a initialized result. So that was that text right there. Then the NCP spec also says that after exchanging these initial messages, we must then also send a initialized notification from the client to the server. As a reminder, notifications do not require a response. So I did send in that initialized notification right there. And as we noticed, we didn't get any immediate response back. Again, that's just because notifications do not require a response. Once we have exchanged these three messages, our connection to the MCP server is considered to be initialized. And at that point on, we can then run call tool requests or run prompt listing requests, whatever else we want to do. So in my case, as you saw, I sent in a call tool request. That was the last one down here. I attempted to call the add tool with arguments of five and three. Now the way that I put together the add tool, it is set up to send back a couple of different logs and then eventually a call tool result. So if I look through the logs, I actually changed it from what you see in that diagram. First, I got a message notification, so a login statement that's coming from right there. There's my login statement. I then got a progress statement. So there's the progress update right there. Those are both notifications being sent from the server to the client. And then finally, I got my call tool response right there. And inside of it is the calculated result of adding three plus five. I see eight. So now that we have seen an example of the standard IO transport, there are some really critical ideas that I want to point out around this transport in particular that are going to be very relevant or very important to understand as we start to take a look at the streamable HTTP transport. First, I showed you this diagram a little bit ago, and I showed you this diagram to help you understand that there are some message types that are intended to be sent from the client to the server and some types that are meant to be sent from the server to the client. The other thing that I really wanted you to understand from this diagram is that there are some instances, specifically with these messages on the top left-hand side, where we can kind of imagine that the client is initiating communication with the server. So in other words, the client is saying, I am making a request and I expect to get back a response. Likewise, there are some situations where the MCP server is sending the initial request off and expects to get back a response. So that would be like the create message request, which is what is used to initiate sampling. So the server needs to send off this request to the client and expects to get back some kind of result. Now to put that in slightly more clear terms, here's what I really mean to indicate with that. There are really four different scenarios with these different transports, like the standard IO transport, that we need to be able to handle. We need to be able to handle the initiation or kind of a initial request from the client to the server, and the transport needs to be able to handle sending a response from the server to the client. Also, the server needs to be able to send an initial request to the client, and then likewise, the client needs to be able to respond. So I want to walk through all these four different scenarios and think about how we would implement them with a standard IO transport. So first is an initial request from the client to the server. So the scenario here would be anytime the client wants to send something off to the server, like maybe a call to a request or something like that. To implement this with a standard IO transport, all we have to do is write to standard in, and the NCP server will receive that message, process it, and then hopefully formulate a response. Once it has a response, it would respond by just writing a message to standard out. Now the third scenario would be where we have an initial request to be sent from the client to the server. So this would be where maybe the server wants to do some sampling, and that's going to require the server to send some kind of initial message off to the client. Whenever the server needs to send an initial request to the client, all it has to do is write to standard out. And then likewise, to respond, all the client has to do is write to standard in. All right, now I know that the last couple of minutes in this video, and the sequence of diagrams I just went over, are probably a little bit confusing. And you're probably thinking, what is this about initial requests and responses and whatnot? Well, here's the point. The whole point of all this is to help you understand that the standard IO transport is really fantastic because at any point in time, the client or the server can initiate communication. Either one can send a request at any point in time and expect to get back a response. Here's the key thing. That is not going to be the case with a streamable HTTP transport. There are some scenarios where the streamable HTTP transport does not allow for this situation. There are some cases where the server cannot send some initial request off to the client. And that's the tricky thing to understand about the streamable HTTP transport. So now we're going to take a pause right here, come back, we're going to start to take a look at the HTTP transport, and we're going to see this particular scenario and understand that, yeah, there's something a little bit tricky about this that you definitely need to understand when you are developing your own MCP servers.

(注:ASR の「initialized request / initialized result」=正しくは initialize request / initialize result(画面図は Initialize Request / Initialize Result)。「NCP spec / NCP server」=MCP。「uvrun server.py」=uv run server.py。「login statement」=logging statement。「Now the third scenario would be where we have an initial request to be sent from the client to the server」は言い間違い/誤認識で、文脈・画面ともサーバー→クライアントの起点リクエストが正。「adding three plus five. I see eight」——引数は a=5, b=3。)

↑ 目次へ戻る

MCP: Advanced Topics | Lesson 07「The StreamableHTTP transport」(全文ナレッジ)

動画 6分26秒 / 講師: Stephen Grider(Anthropic・Member of Technical Staff) / 文字起こし: mlx_whisper / 2026-07-03

日本語ナレッジ(要点整理)

  • StreamableHTTP トランスポート=HTTP 接続越しにクライアント/サーバー間でメッセージを送る仕組み。stdio がクライアントとサーバーを同一マシンで動かす前提だったのに対し、リモートホスト(例: https://mcp-server.com/mcp)の MCP サーバーを実現し、誰でも接続できるパブリックサーバーへの道を開く。
  • ただし大きな注意点:このトランスポートには、適用が必要になりうる設定(stateless_httpjson_response)があり、サーバー→クライアント方向に送れるメッセージの種類を制限する。「ローカルの stdio では全部動いたのに、デプロイして HTTP にしたら動かない」——そのときに戻ってくるべき回。
  • 故障デモ(Wikipedia Research Assistant):research ツールを実装した MCP サーバー(StreamableHTTP)に接続した Next.js アプリで「Write a report on archaeology」を実行。
  • 通常時(両フラグ false)=call tool リクエスト表示 → プログレスバー → ステータス更新(ログ)→ 完全なレポート回答。
  • stateless_http=True=プログレスバーが消える。放置するとテキストを一切生成せずリクエスト自体が失敗
  • さらに json_response=True=プログレスバーもログ文も出ず、やはり失敗。
  • 原因の土台は HTTP の性質:クライアント→サーバーのリクエスト(例: POST)と、その応答は簡単。しかしサーバー→クライアントを起点とするリクエストは困難——サーバーはクライアントのアドレスを知らないし、クライアントは公開アクセス可能とも限らない。
  • HTTP で実装しづらい MCP メッセージ型=サーバー起点のもの:sampling(CreateMessageRequest)/roots 一覧(ListRootsRequest)/Progress Notification/Logging Notification(+図に載っていない他のいくつか)。
  • デモで壊れたのはまさにこれら:progress・logging・sampling。最終レポートの執筆は sampling で行われていたので、フラグを立てると最終回答まで消えた。
  • 次回への引き:StreamableHTTP にはこの問題への巧妙な解決策(workaround)がある。ただし caveats(注意点)付き——それが Lesson 08 の主題。

実務ポイント(このレッスンから持ち帰るもの)

  1. 「stdio で動く=HTTP でも動く」ではない。サーバー起点機能(sampling/roots/progress/logging/subscription)を使う MCP サーバーは、HTTP 化した瞬間に挙動が変わりうる。デプロイ前チェックリストの筆頭に入れる。
  2. フラグの場所は FastMCP コンストラクタFastMCP("mcp-server", stateless_http=True, json_response=True)。デフォルトはどちらも false。コメントアウト解除ひとつで挙動が激変するので、環境差分(dev/prod)をこの2引数で管理するなら要注意。
  3. 故障はエラーではなく「静かな機能欠落」として現れる:プログレスバーが出ない → ログが出ない → 最後にタイムアウト的に失敗。監視・デバッグ時は「何が欠けたか」(progress? logging? 最終応答=sampling?)から逆算すると、どのフラグ/経路が原因か特定できる。
  4. メンタルモデル=4つの通信パターン:①client→server リクエスト ✔/②その応答 ✔/③server→client リクエスト ✗/④その応答 ?——素の HTTP で自然に成立するのは①②だけ。この4分類は Lesson 08 の workaround 理解の土台になる。
  5. HTTP の制約は MCP 固有ではない(講師が繰り返し強調)。「サーバーはクライアントの URL を知らない」という Web 一般の制約が、MCP のメッセージ型一覧の上に投影されているだけ——と捉えると忘れない。

本文(時系列・日本語逐語+画面差し込み)

[0:00] streamable HTTP トランスポートについて議論するときが来ました。このトランスポートは、HTTP 接続越しにクライアントとサーバーの間でメッセージを送れるようにしてくれます。

🖥 [0:00] 画面(スライド): 左枠「Your Computer」内に「MCP Client」、右枠「Remote Machine」内に「MCP Server」。両者を結ぶ双方向矢印にラベル「Streamable HTTP Transport」。

[0:05] このトランスポートの素晴らしい点は、リモートホストされた MCP サーバーを本当に実現してくれることです。standard IO トランスポートでは、クライアントとサーバーを常に同じマシンで動かさなければなりませんでしたが、streamable HTTP トランスポートなら、サーバーをリモートにホストできます。リモートサーバーは、たとえば mcpserver.com のような場所にホストされるかもしれません。

[0:26] これは MCP サーバーをめぐる可能性の幅を本当に大きく広げます。誰でも接続できるパブリックなサーバーを作れるからです。

🖥 [0:30] 画面(スライド「Streamable HTTP transport」): 箇条書き—— ・Allows a client to access a remotely hosted MCP server ・Some configuration settings can apply limitations to the MCP server's functionality because implementing all four communication patterns is challenging with HTTP! 右図: Your Computer →「MCP Client」— 縦の双方向矢印(注記「HTTP Connection」、線上に「JSON Message」ボックス)—「MCP Server」— 下に「Hosted at https://mcp-server.com/mcp」。

[0:37] しかし、直近の数本の動画でほのめかしてきたとおり、このトランスポートには適用が必要になるかもしれない設定がいくつかあり、それが MCP サーバーの機能を制限します。具体的には、サーバーからクライアントへ送り返せるメッセージの種類を制限するのです。

[0:51] それこそが、皆さんに意識しておいてほしい大きな問題です。この特定のトランスポートを使っているとき、サーバーの機能を制限することになる設定が、いくつか存在します。

[1:01] ですから、もしローカルマシンでアプリケーションを開発していて、standard IO トランスポートでは万事うまく動いていたのに、デプロイして HTTP トランスポートを使い始めたら、どうもうまく動かない——そうなったときに、何が起きているのかを理解するために戻ってくるべき動画が、これです。

[1:16] 始めるにあたって、このコースの中で以前お見せしたアプリのクイックデモをお見せしたいと思います。この Wikipedia Research Assistant を覚えているかもしれませんね。research ツールを実装した MCP サーバーに接続された、Next.js アプリケーションです。

🖥 [1:17] 画面(ブラウザ+VS Code の分割画面): 左=ブラウザ localhost:3001。見出し「Wikipedia Research Assistant」、説明文「Ask me to research any topic on Wikipedia. I'll gather information, analyze content, and create comprehensive reports for you.」、例—— ・"Research and write a report on geotechnical engineering" ・"What are the key discoveries in quantum computing?" ・"Explain the history and impact of the Renaissance" 入力欄に「Write a report on archaeology」入力済み。 右=VS Code server.py — research: ```python from mcp.server.fastmcp import FastMCP from mcp_server.tools.research import research

mcp = FastMCP( "mcp-server", # stateless_http=True, # json_response=True )

mcp.add_tool(research)

def run(): mcp.run(transport="streamable-http") `` ——stateless_httpjson_response` の2行はコメントアウト状態

[1:31] このサーバーは streamable HTTP トランスポートを使っています。ですから、このアプリケーションを使って、streamable HTTP トランスポートが期待どおりには動かないシナリオを、いくつかお見せできるわけです。

[1:43] まず、ここでリクエストを送信します。このアプリケーションがどんな見た目だったかを思い出してもらうために、考古学(archaeology)についてのレポートを頼むだけのものです。普段こういうクエリを送り込むと、ここに call tool リクエストが表示され、素敵なプログレスバーが出ます。ステータス更新も受け取り、少し待てば、最終的に完全な応答が得られます。

🖥 [2:00] 画面(ブラウザ:通常時(両フラグ false)の実行結果): 「I'll research archaeology for you and write a report based on the findings.」→ ツールカード ✅ 🔧 research resultProgress 100/100(プログレスバー満タン表示)→ 黒いログ枠: text Total content size: 8417 characters 3. Generating LLM summary ... Estimated tokens being sent to LLM: 1583 → 「Based on my research, here is a report on archaeology:」に続き完全なレポート本文(考古学の定義、Enlightenment 期の Stonehenge/Pompeii/Herculaneum の初期発掘、LIDAR や GIS といった現代手法まで言及する長文)。

[2:03] さて、こちらの MCP サーバーのソースコードの中を見ると、私がコメントアウトしてある2つの設定——stateless HTTP と JSON response——があることに気づくでしょう。デフォルトでは、これらの設定は false になっています。しかし、これらをどうしても有効化する、つまり true に設定しなければならないシナリオがいくつか出てきます。そのシナリオが何なのかは、もう少し後で詳しく見ていきます。

[2:22] これらの設定を true に変えることは、MCP サーバーの機能に影響を与えます。場合によっては、クライアントの動き方すら壊すかもしれません。では、その例をさっそくお見せしましょう。

🖥 [2:30] 画面(ブラウザ+VS Code:stateless_http=True での2回目実行・進行中): VS Code 側は6行目のコメントが外れて—— python mcp = FastMCP( "mcp-server", stateless_http=True, # json_response=True ) ブラウザ側はツールカード 🔄 🔧 research call(スピナー=実行中)とログ—— text Researching: archaeology 1. Searching for articles ... Found 10 potential articles プログレスバーは表示されていない

[2:32] stateless HTTP を true に変えます。ファイルを保存します。それからページをリロードして、まったく同じクエリをもう一度実行します。

🖥 [2:39] 画面(ブラウザ+VS Code): ページ再読み込み中(リロードボタンが停止アイコン)。チャットには research call カードが残り、左下に Next.js の赤いエラーバッジ「1 Issue ✕」=失敗の痕跡。

[2:41] 大きな違いは見えないでしょうが、すぐに気づくことが1つあります。もうプログレスバーが出ないのです。ログ文の上に、プログレスバーがまったくありません。加えて、このまましばらく放置しておくと——動画を早送りしますが——最終的にリクエストは、テキストを一切生成せずに完全に失敗します。

🖥 [2:41〜3:02] 画面(ブラウザ+VS Code・stateless_http=True 実行の続き): VS Code は stateless_http=True, のみ有効(# json_response=True はコメントのまま)。research call カード(スピナー継続・「> Show details」トグル付き)のログはさらに進み—— text Total content size: 8518 characters 3. Generating LLM summary ... Estimated tokens being sent to LLM: 1587 ——通常時 [2:00] と同じ「LLM 要約生成」段階までログ文自体は流れ続けるものの、プログレスバーだけは最後まで現れない。この後、最終レポート(sampling)が返らないままリクエストが失敗する。

[3:02] 今度はページをリフレッシュします。JSON response を true に切り替え、ファイルを保存して、まったく同じクエリをもう一度実行します。両方の設定を true にすると、さらに驚きの結果が出ます。今度はプログレスバーも見えなければ、ログ文も一切出ません。そして、私の言葉を信じてほしいのですが、このまま放置しておけば、リクエストはまた失敗します。

🖥 [3:02〜3:20] 画面(ブラウザ+VS Code・両フラグ true での再実行): VS Code は7行目 json_response=True もコメント解除(stateless_http=True,json_response=True の2フラグとも有効)。チャットは「I'll research archaeology for you and write a report based on the findings.」に続く research call カードに「> Show details」だけが表示され、黒いログ枠もプログレスバーも一切出ない

[3:20] というわけで、この2つの設定は一見無害そうに見えて、私たちの MCP サーバーに大きな影響を与えることがすぐに分かります。この動画の残りはすべて、これらの設定が何の話をしているのか、なぜ true に設定することがあるのか、そして正確には何が起きているのかを理解してもらうためのものです。

🖥 [3:30] 画面(スライド「How can we implement each of these with stdio?」): 左に4つのボックス—— 1. Initial request from Client → Server 2. Response from Server → Client 3. Initial request from Server → Client 4. Response from Client → Server 右図: 「MCP Client」→(Client sends message to server)→ サーバー上部の「stdin」タブ、「stdout」タブ →(Server sends message to client)→ クライアント。下に「MCP Server」。

[3:36] まず、前回の動画でお見せした図について、ごく手短に思い出してもらいます。私は、standard IO トランスポートには、クライアントからサーバーへリクエストを開始して応答を受け取る能力があること、そして同様に、サーバーもいつでもクライアントへリクエストを開始して応答を受け取れることを、大きく強調しました。

[3:55] それをほんの少し頭に置いたまま、HTTP 通信のクイックレビューをしましょう。これは HTTP の基礎に立ち返る話で、MCP 関連に限らない、HTTP 通信全般の話です。

🖥 [3:59] 画面(スライド「HTTP Communication」): 左ボックス—— ・HTTP clients can easily initiate requests to servers / The server is hosted at a known URL ・The server can easily respond to these requests 右のシーケンス図: HTTP ClientHTTP Server。Client→Server 矢印「POST https://my-server.com/api」、Server→Client の戻り矢印「Response from server」。右下の茶色注記: 「Note: This applies to all HTTP requests, not just with MCP」。

[4:06] 復習すると、クライアントとサーバーがあれば、クライアントはいつでも、とても簡単にサーバーへリクエストを送れます。たとえば右側では、クライアントがサーバーへ POST リクエストを送り、何らかの応答が返ってくることを期待できます。この構図には、まったく何の問題もありません。

[4:23] これを MCP の世界に翻訳すると、クライアントからサーバーへ最初のリクエストを送りたい場合——問題なし、動きます。そしてサーバーからクライアントへ応答を返したい場合——まったく問題なし、ちゃんと動きます。

🖥 [4:26] 画面(スライド・同図+判定追加): 左下に緑ハイライトのボックス2つ+緑チェック—— ・Initial request from Client → Server ✔ ・Response from Server → Client ✔

🖥 [4:30] 画面(スライド): HTTP Client/HTTP Server のシーケンス図で、Server → Client 方向の矢印に「???」「I don't know the address of the client!」のラベル。右のグレーボックス: 「HTTP servers cannot easily initiate communication with a client / Clients don't have a known URL!」。茶色注記は同前。

[4:35] ところが、逆のシナリオを考えると——サーバーがクライアントへ向かってリクエストを開始したい場合、HTTP リクエストではそう簡単にはいきません。ごく基本的なレベルの話として、サーバーはクライアントのアドレスを知りませんし、クライアントは公開アクセス可能ですらないかもしれない。だから伝統的な HTTP では、サーバーからクライアントへリクエストを開始するのは非常に難しいのです。

[4:57] そしてそれは私たちの MCP の世界では、サーバーがあの最初のリクエストを送り下ろすのが本当に難しい、ということを意味します。さらに言えば、クライアントからサーバーへ応答が返ってくることをどうやって期待すればいいのかすら、なかなか想像しがたい、ということでもあります。

🖥 [5:00〜5:13] 画面(スライド:MCP メッセージ型の全体図): MCP ClientMCP Server の2列(破線ライフライン)を双方向矢印で対応付け—— Call Tool Request ↔ Call Tool Result / List Prompts Request ↔ List Prompts Result / Read Resource Request ↔ Read Resource Result / Initialize Request ↔ Initialize Result / Create Message ResultCreate Message Request / List Roots ResultList Roots Request / Initialized Notification・Cancelled Notification(クライアント側)/ Progress NotificationLogging Notification(サーバー側) 下部キャプション: Notifications don't require a response

🖥 [5:03] 画面(スライド・同図+判定追加): 右側に—— ・赤ボックス+赤✗: Initial request from Server → Client ・黄土色ボックス+?: Response from Client → Server

[5:12] そこで、少し前に見たこの図——サーバーからクライアントへ発行されるリクエストがいくつか存在する、と私がお話しした図——を考えると、この HTTP の世界では、普通の HTTP リクエストではどうにも実装しづらいメッセージ型がいくつかある、ということになります。

🖥 [5:24] 画面(スライド・同図+赤注釈): サーバー側の Create Message Request/List Roots Request/Progress Notification/Logging Notification を波括弧でまとめ、赤いボックスで—— 「Server initiated requestsTough to implement with plain HTTP!

[5:29] 具体的には、sampling リクエスト、roots の一覧取得、progress 通知、そして logging 通知——この図には載せていない他のいくつかのメッセージ型も合わせて——が、HTTP の世界では実装がより難しいのです。

[5:38] そして案の定、それがまさに、さっきのデモのところで見たものです。私がこれらのプロパティを false から true に切り替え始めた途端、アプリケーションのあちこちが壊れ始めて、期待どおりに動かなくなるのを見ましたよね。

🖥 [5:44] 画面(VS Code へのトランジション): server.py2フラグとも有効化された状態が選択ハイライトされている—— python mcp = FastMCP( "mcp-server", stateless_http=True, json_response=True ) 左端にはブラウザの断片(「Write a report on archaeology」ピル、research call カード、赤い「1 Issue ✕」バッジの一部)、右にはメッセージ型図が並ぶ。

[5:52] では、正確には何が壊れたのでしょうか? そう、そのとおり。progress 通知が壊れ、logging が壊れ、そして sampling——実質的には create message リクエスト——も壊れました。sampling は、最終的な研究レポートの執筆に使われていたのです。

🖥 [6:00] 画面(スライド・まとめ): 左テキスト「StreamableHTTP Transport has a clever solution to this, but there are caveats」。右に4ボックス—— 1. 緑✔ Initial request from Client → Server 2. 緑✔ Response from Server → Client 3. 赤✗ Initial request from Server → Client 4. ? Response from Client → Server

[6:07] さて、良い知らせもあります。純粋な HTTP の世界で、サーバーからクライアントへリクエストを行うのは難題であるにもかかわらず、streamable HTTP トランスポートには巧妙な解決策があるのです。ただし、知っておくべき注意点(caveats)がいくつかあります。このトランスポートが実際にどう動くのか、そしてその注意点が正確には何なのか——すぐ後で見ていきましょう。

英語逐語(ASR全文)

Time to discuss the streamable HTTP transport. This transport allows us to send messages between a client and a server over an HTTP connection. The nice thing about this transport is that it really enables remotely hosted MCP servers. So whereas with the standard IO transport, we always had to run the client and the server on the same machine, with the streamable HTTP transport, we can remotely host our server. So our remote server might be hosted at mcpserver.com or something like that. This really opens the door to a much wider range of possibilities around MCP servers because you can make a public server that anyone can connect to. However, as I've hinted at over the last couple of videos, there are some settings that you might need to apply to this transport that will limit the functionality of your MCP server, specifically limit the types of messages that you can send from the server back over to the client. So that is the big issue that I want you to be aware of. There are some settings that are going to restrict the functionality of the server when you are using this particular transport. So if you are ever developing an application on your local machine and everything is working just fine with the standard IO transport, and then you deploy and you start to use the HTTP transport and things aren't working quite so well, this is the video you want to come back to to understand what is going on. To get started, I would like to give you a quick demonstration of an app that I showed you previously inside of this course. You might recall this Wikipedia Research Assistant. It makes use of a Next.js application that is connected to an MCP server that implements a research tool. This server is using the streamable HTTP transport, so I will be able to use this application to show you a couple scenarios where the streamable HTTP transport doesn't quite work as expected. First, I'm going to send off a request here. It's going to ask for a report on archaeology just to remind you what this application looks like. So usually when I send in a query like this, I'm going to see a call tool request here, and I get a nice progress bar. I get status updates, and eventually if I wait just a little bit, I will get a full response. Now over here inside the source code for the NCP server, you'll notice that there are two settings that I have commented out, stateless HTTP and JSON response. By default, these settings are set to false, but there are going to be some scenarios where you very much have to enable them or set them to true. And we'll go into more details on what those scenarios are a little bit later on. Changing these settings to true is going to impact the functionality of your MCP server. And in some cases, it might even break how your client works. So let me show you an example of this right away. I'm going to change stateless HTTP to be true. I'm going to save the file. Then I'm going to reload the page and run the exact same query again. And now you're not going to see a big difference here, but there is something you'll notice right away. You'll notice that now I don't get a progress bar anymore. So no progress bar whatsoever above the log statements. In addition, if I let this just sit here for a while and I'm going to speed the video up, we will see that eventually the request fails entirely without generating any text. Now I'm going to refresh the page. I'm going to turn on JSON response to be true, save the file and then run the exact same query again. Now we'll see that with both these settings set to true we get some even more surprising results. Now I don't see any progress bar I don't get any log statements and take my word for it if we let this just sit here the request is going to fail again. So right away we can see that these two settings although seemingly innocuous have a big impact on our MCP server. So the rest of this video is all about helping you understand what these settings are talking about why we might set them to true and exactly what's going on here. First, I want to give you a very quick reminder of a diagram I showed you in the last video. I made a big deal about how the standard IO transport had the ability to initiate a request from the client to the server and then get a response back. And likewise, the server at any point in time could initiate a request off to the client and get a response back. So keep that in mind for just a moment as I give you a quick review on HTTP communication. So this is going back to some of the basics around HTTP, and this is all HTTP communication not just MCP related stuff. So as a reminder, if we have a client and a server at any point in time, a client can very easily make a request off to the server. For example, on the right hand side, the client can make a post request off to the server and expect to get back some kind of response. No issue with this setup whatsoever. So translating this over to the MCP world, that means that if we want to make an initial request from the client to the server, no problem, it's going to work. And if we want to get a response from the server back with the client, no problem whatsoever. It's going to work just fine. However, if we consider the reverse scenario, if the server wants to initiate a request down to the client, that's not quite so easy with HTTP requests. At a very basic level, the server doesn't know the address of the client, and the client might not even be publicly accessible. So it's very challenging in traditional HTTP for the server to initiate a request down to the client. And that means that in our MCP world, it's really hard for the server to send that initial request down and it also means that it's kind of hard to imagine how you'd ever expect to get a response from the client back to the server. So if we consider this diagram that we looked at a moment ago where I had told you that there are some requests that are being issued from the server down to the client that means that in this HTTP world there are some message types that are just plain tough to implement with normal HTTP HTTP requests. Specifically, sampling requests, listing routes, progress notifications, and logging notifications, all in with some other message types that I'm just not showing on this diagram, are tougher to implement in an HTTP world. And sure enough, that's kind of what you saw back over here with this demo. As soon as I started to flip some of these properties from false to true, well, you saw some different parts of the application start to break and just not work as expected. And what exactly broke? Well, that's right. The progress notification broke, the logging broke, and sampling, or essentially the create message request, broke as well. Sampling was being used to author the final research report. Now, I do have some good news. Even though making requests from a server down to a client is challenging in a pure HTTP world, the streamable HTTP transport does have a clever solution. But there are some caveats that you need to be aware of. Let's take a look at how the transport actually works and exactly what those caveats are in just a moment.

↑ 目次へ戻る

MCP: Advanced Topics | Lesson 08「StreamableHTTP in depth」(全文ナレッジ)

動画 10分28秒(本コース最長) / 講師: Stephen Grider(Anthropic・Member of Technical Staff) / 文字起こし: mlx_whisper / 2026-07-03

日本語ナレッジ(要点整理)

  • 前提の整理(recap):①sampling・notifications・logging など一部の MCP 機能は「サーバー→クライアントのリクエスト」に依存する。②しかし HTTP ではサーバー→クライアントのリクエストは難しい。この2点は正面衝突する。③StreamableHTTP にはこれを解く workaround がある。④ただし stateless_httpjson_response を true にすると workaround が壊れる——この4段構造が、StreamableHTTP が分かりにくい理由のすべて。
  • 初期化フローと mcp-session-id:MCP スペックどおり「InitializeRequest → InitializeResult → initialized notification」の3手。HTTP トランスポートでは InitializeResult の HTTP レスポンスヘッダに mcp-session-id(ランダムな英数文字列=接続の識別子)が付き、以後の全リクエストにこのヘッダを含めることが必須になる。セッション ID は StreamableHTTP 専用の概念で、stdio では一切使われない
  • workaround の本体=長寿命 GET SSE:初期化後、クライアントは任意で(必須ではない)セッション ID 付きの GET リクエストをサーバーへ送れる。返ってくるのは SSE(Server-Sent Events)レスポンス=任意の長さ開いたままにできる特別なレスポンス。これが確立されると、サーバーはいつでも個々のメッセージをクライアントへストリームで流せる=実質「server→client リクエスト」チャネルになる。これがトリック。
  • tools/call 時は SSE が2本になる:GET SSE が開いたまま、クライアントがセッション ID 付き POST で call tool リクエストを送ると、サーバーは2本目の SSE レスポンスを開く。役割分担——
  • 1本目(GET 側)=サーバー→クライアントのリクエスト用。開きっぱなし。
  • 2本目(POST 側)その call tool リクエストに関係するメッセージ専用CallToolResult を送った瞬間に自動クローズ
  • SDK 実装の癖(重要):ログ通知も progress 通知も理屈上は call tool リクエストに紐づくので「両方 POST 側に流れる」と思いきや、多くの SDK(Python SDK 含む)では progress 通知だけは別扱いで1本目(GET SSE)へ流れる。logging メッセージ+CallToolResult は POST 側に流れる。
  • ライブデモ(MCP Protocol Demo・localhost:8000)での実測: 1. Initialize 送信 → レスポンスヘッダに mcp-session-id: eaa24154e8214a64b59c4245de2d2672(「eaa」始まり)。以後のリクエストへ自動適用。 2. initialized notification 送信 → HTTP 202 Accepted — Notification acknowledgedcontent-length: 0)。 3. 「Start GET SSE」クリック → パネルのドットが赤→緑、GET SSE connection established。 4. 「Call Add Tool」(add 5+3)→ 2秒待ち後、POST 側ストリームnotifications/message("Preparing to add...")→ result("8")SSE connection closedGET SSE パネル側notifications/progress(progress 80.0/total 100.0)。
  • 次回への引き:ではなぜフラグを true にしてまでこのフローを壊すのか? 何が壊れるのか?——Lesson 09 へ。

実務ポイント(このレッスンから持ち帰るもの)

  1. 自作クライアントは mcp-session-id の伝搬が生命線。InitializeResult のレスポンスヘッダから取り出し、以後の全 POST/GET に付ける。忘れるとサーバーはクライアントを識別できない(Lesson 09 の stateless の議論はこの裏返し)。
  2. GET SSE は「任意」。sampling・server→client リクエスト・進捗購読を使わないクライアントは張らなくてよい。逆に、進捗 UI やサンプリング対応をうたうクライアントは必ず張る。
  3. 「進捗が来ない」誤診に注意:progress 通知は tools/call への応答ストリームではなく GET SSE 側に届く(SDK の癖)。クライアントは2本のストリームを両方 listen して初めて全メッセージを拾える。
  4. POST 側 SSE は使い捨て:CallToolResult で自動クローズされる。1リクエスト=1ストリームで、再利用を前提に設計しない。
  5. 手元で再現できる:配布 assets(transport-http)で uv syncuv run main.py → ブラウザで localhost:8000。カードのボタンを順に押すだけで、この回の図の全メッセージが生 HTTP ヘッダ・生 SSE イベントとして見られる。プロトコル学習の最短ルート。
  6. 診断の目印:初期化応答も tools/call 応答も content-type: text/event-streamtransfer-encoding: chunked で返る(デフォルト時)。notification への応答は 202 Acceptedcontent-length: 0。curl で叩くときは Accept: application/json, text/event-stream を忘れない(デモクライアントも全リクエストに付けている)。

本文(時系列・日本語逐語+画面差し込み)

[0:00] 直近の数本の動画のクイック復習をして、認識が揃っていることを確認しましょう。まず私たちが理解したのは、MCP の一部の機能——具体的には sampling や notifications、logging のようなもの——は、サーバーがクライアントへリクエストを行うことに依存している、ということでした。

🖥 [0:00] 画面(スライド「StreamableHTTP Issues」): 右側にボックス4つ—— 1. Some MCP functionality relies on a server making a request to a client 2. With HTTP, it's challenging to allow a server to make a request to a client 3. StreamableHTTP has a workaround to fix all of this! / You can get full MCP functionality with StreamableHTTP! 4. In some scenarios, you'll want to set stateless_http and json_response to True, which breaks the workaround

[0:15] その少し後に、HTTP 全般を使う場合、サーバーがクライアントへリクエストを行えるようにするのはなかなか難しい、と学びました。つまり、この最初の2点は正面衝突しています。相反しているのです。完全な機能を得るためにはサーバーがクライアントへリクエストできる必要がある。しかし同時に、HTTP を使うとそれは難しい。

[0:34] さて、お話ししたとおり、Streamable HTTP にはこれを修正する workaround があります。この動画では、その workaround が正確に何なのか、そしてこのトランスポートが全体としてどう動くのかを、きっちりお見せします。ようやく、何が起きているのかクリアに分かるところまで行きますよ。

[0:47] 最後に、頭に入れておいてください。シナリオによっては、あの2つのフラグを true に設定することになります。なぜ true にするのかも議論します。そして true にすると、この workaround が壊れるのです。streamable HTTP というトランスポートが少し頭で捉えにくいのは、これが理由でもあります。HTTP をめぐる制約があり、workaround を手に入れた、しかし場合によってはその workaround をあえて使いたくない——だからトリッキーな主題なのです。

[1:11] さて、前置きはこのくらいにして、取りかかりましょう。このトランスポートが実際どう動くのかを理解しましょう。まず、裏側のプロセスを理解してもらうために、何枚かの図をお見せします。それから、これらのリクエストが実際に行われ、システム全体がひとつに組み上がるところを、とても小さなデモアプリケーションで見てもらう短いデモをお見せします。

🖥 [1:17] 画面(シーケンス図:初期化フロー・ヘッダなし版): 「MCP Client」|「MCP Server」の破線ライフライン。 ① Client → Server「Initialize Request」 ② Server → Client「Initialize Result」 ③ Client → Server「Initialized Notification」+注記 Notification, no result comes back

[1:29] さあ行きましょう。最初に思い出してほしいのは、クライアントが最初にサーバーへ接続するときに何が起きるか、です。MCP スペックによれば、クライアントはサーバーへ initialized リクエストを送らなければならず、サーバーは result で応答し、それからクライアントは initialized notification という後続リクエストを行わなければなりません。その時点で、サーバーはクライアントが接続されたと見なし、万事準備完了です。

[1:57] さて、HTTP トランスポートを使い始めた途端、このフローがごくわずかに変わります。このトランスポートを使い始めると、サーバーから送り返される initialized result の HTTP レスポンスの中に、あるヘッダが入るようになるのです。このヘッダが MCP セッション ID です。これは私たちのクライアントに割り当てられる、数字と文字のランダムな文字列です。実質的に、サーバーへの私たちの接続の識別子です。

🖥 [2:00] 画面(シーケンス図:初期化フロー+セッション ID 版): 同じ3本の矢印に加えて—— ・「Initialize Result」ボックス内に Response Headers の表: mcp-session-id7089d4160bbb4d ・「Initialized Notification」ボックス内に Request Headers の表: mcp-session-id7089d4160bbb4d

[2:13] そのヘッダを受け取ったら、以後サーバーへ行うすべてのフォローアップリクエストに、それを含めることが必須になります。これによって、サーバーは私たちのクライアントを識別できるのです。

[2:28] さて、この初期化を済ませて、あのセッション ID をもらったら——ここからが魔法のパートの始まりです。これが、サーバーからクライアントへのリクエストを可能にする、例の大きな workaround です。初期化を終えたらすぐ、クライアントは——任意です、やらなくてもいいのですが——MCP サーバーへ GET リクエストを送り、その中にあのセッション ID を含めることができます。

🖥 [2:30] 画面(シーケンス図): Client → Server の矢印1本。「GET mcp-server.com/mcp/」+ Request Headers: mcp-session-id7089d4160bbb4d

[2:50] 返ってくるレスポンスは、非常に特別です。SSE レスポンスです。SSE は server sent events の略。これは、任意の長さの時間、開いたままにしておける種類のレスポンスです。このレスポンスがクライアントとの間で確立されると、サーバーはそこから、小さな情報の断片——実質的には個々のメッセージ——をクライアントへストリームで流し下ろせます。

🖥 [3:00] 画面(シーケンス図): ① Client → Server「GET mcp-server.com/mcp/」+mcp-session-id。② Server → Client「SSE Response」+注記「Can be held open arbitrarily long」。右注記 Creates a Server Sent Events (SSE) connection。下部の茶色強調ボックス—— 「This SSE response is what allows the server to send requests to the client!

[3:12] この接続が張られた今——ここが本当に肝心なところです——これでサーバーは、任意の時点でクライアントへメッセージを送り下ろせるようになりました。そしてこの接続は、実質的に、サーバーがクライアントへリクエストを行うために使えるのです。これがトリックです。これが HTTP トランスポートの使う workaround です。この長寿命の SSE レスポンスを利用して、クライアントへ送りたいメッセージをストリームで流し下ろすのです。

[3:41] では、もう少し先へ進んで、クライアントがツールを呼びたくなったときに何が起きるかを一通り見ましょう。複雑さはまだ終わっていないからです。理解してほしい重要なパートが、もう1つあります。いま、この図の上部には、さっき作ったあの SSE レスポンスがあります。そして覚えておいてください。あのレスポンス——あの開いた接続——にはセッション ID が紐づいています。だから MCP サーバーは、あの接続がどのクライアントのものなのか、正確に知っています。

🖥 [3:47] 画面(シーケンス図): 上部に Server → Client「SSE Response for session 7089..」(Can be held open arbitrarily long)。下に Client → Server「Call Tool Request」+「POST mcp-server.com/mcp/」+mcp-session-id7089d4160bbb4d

[4:06] それから、後のある時点で、そのレスポンスがまだ走っている間に、私たちのクライアントがサーバーへ call tool リクエストを行うと決めるかもしれません。このリクエストを行うとき、クライアントは自分のセッション ID をヘッダとして含めます。すると MCP サーバーは、2本目の SSE レスポンスを開きます。

[4:18] さあ、この時点で、2本の別々の SSE レスポンスが存在します。上の1本目は、サーバーからクライアントへ向かっていくリクエストのために使われることを意図したもの。2本目の真新しい SSE レスポンスは、この call tool リクエストに関係するメッセージのために使われることを意図したものです。そして重要なことに——これが肝心な部分ですが——この2本目の SSE レスポンスは、call tool result メッセージが送られると同時に自動的にクローズされます。つまり下のレスポンスは、結果が得られたらすぐ、ごく短時間で自動的に閉じる。一方、上のレスポンスは、任意の長さの時間、開いたままにされ続けることを意図しています。

🖥 [4:30] 画面(シーケンス図+茶色注釈2つ): 3本の矢印(上 SSE/POST/下 SSE)。 ・1本目の SSE を指す注釈——「Intended to be used for requests from the server to the client」 ・2本目の SSE を指す注釈——「Used for messages that are related to the CallToolRequest.Closes once the CallToolResult is sent

[4:55] それから思い出してください。ここでの最終目的は、実際にツールを呼ぶことでした。たとえば、何度も見てきた add ツールを呼ぶとしましょう。何回かお見せしたあの実装には、中にログメッセージがいくつかと progress 通知があり、最終的に結果を返します。それが私たちの call tool result になるわけです。

🖥 [5:00] 画面(シーケンス図+右側リスト): 図の右に見出し Messages produced during the tool call とボックス3つ—— ・Logging Message Notification ・Progress Notification ・Call Tool Result

[5:14] さて、ここにもう1つ、ちょっとしたトリッキーな部分があります。技術的には、ログメッセージも progress 通知も、実のところ call tool リクエストに紐づいています。だから、「この最初の2つのメッセージはどちらも下のレスポンスで送り返されるはずだ」と考えたくなるでしょう。どちらも本当にこの call tool リクエストに紐づいているのですから。

[5:29] ところが、いまの多くの SDK は、そういう動きをしないのです。progress 通知は、入ってきた call tool リクエストとは別物として扱われてしまいます。そのため progress 通知は、実際には1本目の SSE レスポンス——つまり、サーバーがクライアントへリクエストを送り下ろせるように長時間開いたままにしておくことを意図した、あのレスポンス——の中で送られます。そして、ログメッセージと実際の call tool result は、行われた POST リクエスト(call tool リクエスト)への応答の中で送り返されます。それが、フローの全体です。

🖥 [5:30] 画面(シーケンス図:メッセージの振り分け・この回の核心図): ・上の矢印(Server → Client): 矢印上に「Progress Notification」、下に「SSE Response for session 7089..」(Can be held open arbitrarily long)=progress は GET 側 SSE へ ・中央(Client → Server): 「Call Tool Request」/「POST mcp-server.com/mcp/」/mcp-session-id7089d4160bbb4d ・下の矢印(Server → Client): 矢印上に「Logging Message Notification」「Call Tool Result」、下に「SSE Response for session 7089..」=ログと結果は POST 側 SSE へ

[5:59] さて、これらすべてが完全にクリアになるように、これらのさまざまなリクエストが行われるところを、とても視覚的な形で見られる、ごく短いデモを一緒に歩いてみたいと思います。

🖥 [6:00〜6:15] 画面(ブラウザ+VS Code): 右=VS Code main.py — transport-http: ```python from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP( "mcp-server", # stateless_http=True, # json_response=True )

@mcp.tool() async def add(a: int, b: int, ctx: Context) -> int: await ctx.info("Preparing to add...") await asyncio.sleep(2) await ctx.report_progress(80, 100)

return a + b

Load the demo HTML page

@mcp.custom_route("/", methods=["GET"]) async def get(request: Request) -> Response: with open("index.html", "r") as f: html_content = f.read() return Response(content=html_content, media_type="text/html") `` (ctx.info("Preparing to add...")の行が選択ハイライト。2フラグはコメントアウト=デフォルト動作) 左=ブラウザlocalhost:8000`「MCP Protocol Demo」のカード「1 Initialize Request」(POST・initialize の JSON ボディ)。

📄 実物コード(assets/transport-http/main.py 1-31行)——このデモサーバーの完全版はコース配布 assets にそのまま入っている(配布版は2フラグ有効化済み・Lesson 09 ノート参照)。デモページ本体は同ディレクトリの index.html(カード1〜3の JSON-RPC ボディと「Server-Initiated Events (GET SSE)」パネル、startSSE()stopSSE() 等の実装入り・全1195行)。起動は README.md どおり uv syncuv run main.py

[6:06] というわけで、小さなデモを組んでみました。まず、シンプルな MCP サーバーを用意してあります。今回もまた、シンプルな add ツール付きです。この add ツールは、情報をログに書き、2秒待ち、progress 報告を行い、それから結果を返します。

[6:20] この MCP サーバーには、ブラウザの中で動いているこのクライアントで接続してあります。このクライアントを使うと、サーバーへひと握りの種類のリクエストを行い、返ってくる正確なレスポンスを表示できます。

🖥 [6:23] 画面(ブラウザ:デモページ全景・送信前): 紫グラデーションのバナー「MCP Protocol Demo / Model Context Protocol - Streamable HTTP Transport」。カード「1 Initialize Request」——POST Request: json { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": { "roots": { "listChanged": true } }, "clientInfo": { "name": "MCP Demo Client", "version": "1.0.0" } } } ヘッダ: Content-Type: application/jsonAccept: application/json, text/event-stream。Response 欄「No response yet. Click the button to send request.」。右下フロートパネル「Server-Initiated Events (GET SSE)」(赤ドット・「No GET SSE connection active. Click "Start GET SSE" to begin monitoring server-initiated events.」・ボタン Start GET SSEStop SSEClear)。

[6:30] このクライアントは全体として、画面に見えているフローをたどっていきます。情報量が多いのは分かっていますが、要するに、まず上のほうにある一式で初期化プロセス全体を通します。それから、サーバーがクライアントへリクエストを送れるようにする GET SSE 接続を形成します。そして call tool リクエストを行います。それが2本目の SSE 接続を形成し、この特定の call tool リクエストに関係するメッセージの処理を受け持ちます。では、実行して何が起きるか見てみましょう。

🖥 [6:45] 画面(シーケンス図:全体まとめ・デモの合間に繰り返し表示される): Initialize Request → Initialize Resultmcp-session-id7089d4160bbb4d)→ Initialized Notification(同ヘッダ)→ GET mcp-server.com/mcp/(同ヘッダ)→「SSE Response for server to client requests」→ Call Tool Request(同ヘッダ)→「SSE Response for messages related to this request

[6:58] initialized リクエストを行います。送信します。これで私のセッション ID が得られます。以後のあらゆるリクエストにおいて、私のクライアントをサーバーに対して一意に識別するものです。覚えておいてください。セッション ID が提供されるのは、streamable HTTP トランスポートを使っているときだけです。standard IO トランスポートではまったく使われません。EAA で始まっているのが分かりますね。

🖥 [7:00] 画面(ブラウザ:カード1送信後): Response ✓ Success。レスポンスヘッダ—— cache-control: no-cache, no-transformconnection: keep-alivecontent-type: text/event-streammcp-session-id: eaa24154e8214a64b59c4245de2d2672(枠付きハイライト)/server: uvicorntransfer-encoding: chunkedx-accel-buffering: no ストリーム欄—— text stream: SSE connection established event: message data: {"jsonrpc":"2.0","id":1,"resul…(略)…05","capabilities":{"experimental":{} …{"listChanged":false},"resources":{"subscribe":false,"listChanged":fals… …{"listChanged":false}},"serverInfo":{…server","version":"1.9.3"}}} stream: SSE connection closed

[7:16] このヘッダは自動的に取得され、以後のすべてのリクエストに適用されます。ですから、次のリクエストまでスクロールダウンすると、MCP セッション ID のところに、すぐ上のリクエストで得た正しいセッション ID が自動的に適用されているのが分かります。

[7:28] さて、initialized リクエスト——あれです——を送ったので、私たちの2番目のリクエストは initialized notification です。これを送信します。

[7:38] よし。これで、ここにある最初の3つの箱は片付きました。次は、サーバーへの GET リクエストを行います。この GET リクエストは、長時間開いたままにされます。これが、サーバーが望む任意の時点で、私たちのクライアントへリクエストを送り下ろせるようにするものです。sampling をはじめ、あらゆる種類のサーバー起点リクエストに使われることになります。

🖥 [7:39] 画面(ブラウザ:カード2送信後): 「2 Initialized Notification」——POST Request: json { "jsonrpc": "2.0", "method": "notifications/initialized", "params": {} } Request ヘッダに mcp-session-id: eaa24154e8214a64b59c4245de2d2672(自動適用済み)。Response ✓ Success—— content-length: 0content-type: application/jsonmcp-session-id: eaa24154e8214a64b59c4245de2d2672server: uvicorn 下段ボックス: 「HTTP 202 Accepted — Notification acknowledged」。右下 GET SSE パネルはまだ未接続(赤ドット)

[8:00] そのリクエストを行うために、右下にあるこの小さなボックスを使います。start get sse をクリックします。このリクエストにも、あのセッション ID が含まれます。そして——これで接続が形成されました。これで理屈のうえでは、サーバーはいつでも、sampling や logging や progress 通知などのために、私のクライアントへメッセージを送り下ろせます。それらを受信したら、このボックスの中に表示されるはずです。

🖥 [8:00] 画面(ブラウザ): GET SSE パネルのドットが赤→緑に変化し、本文が「GET SSE connection established」に。下には次カード「3 Tool Call - Add Function」の頭が見えている。

🖥 [8:00〜8:43] 画面(ブラウザ:カード3・実行前): 「3 Tool Call - Add Function」——POST Request: json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "add", "arguments": { "a": 5, "b": 3 } } } ヘッダ: Content-Type: application/jsonAccept: application/json, text/event-streammcp-session-id: eaa24154e8214a64b59c4245de2d2672。Response 欄「No response yet. Initialize first, then call tool!」。ボタン「Call Add Tool」。GET SSE パネルは緑ドット・established。

[8:21] さて、最後のステップです。add 関数を呼びます。ここにもまた、正しいセッション ID が入っています。念のため思い出してもらうと、これをクリックしたとき、理屈のうえでは、ログ文・progress 文・最終的な call tool result のすべてが、レスポンスのその部分を表すこちらのボックスに現れてほしいところです。しかし実際には、Python 用 MCP SDK の作りの都合で、progress 通知は上のこのレスポンス(GET SSE)の一部として送られることになります。だから、こちらにはログ文と call tool result が、右下のあのボックスには progress 文が出るはずです。では、実行して何が起きるか見ましょう。

🖥 [9:00〜9:30] 画面(ブラウザ:カード3実行後・この回のクライマックス): POST 側レスポンス——ヘッダ: content-type: text/event-streamtransfer-encoding: chunkedmcp-session-id: eaa24154e8214a64b59c4245de2d2672 ほか。ストリーム: text stream: SSE connection established event: message data: {"method":"notifications/message","params": {"level":"info","data":"Preparing to add..."},"jsonrpc":"2.0"} event: message data: {"jsonrpc":"2.0","id":3,"result":{"content": [{"type":"text","text":"8"}],"isError…(末尾はパネルに隠れ) stream: SSE connection closed GET SSE パネル側—— text Event Type: message Server Notification: {"method":"notifications/progress","params": {"progressToken":"abc","progress":80.0,"total":100.0},"jsonrp…ログ+結果は POST ストリーム、progress(80.0/100.0)だけ GET SSE 側。[5:30] の図の説明どおりの実測結果。

[9:06] よし、実行します。それから少し遅延があって——中に2秒のポーズがあるのを思い出してください——結果が得られます。案の定、右下のここに notifications progress が来ています。あれが progress 文です。80/100% の完了地点です。あれは、あちらのレスポンスチャネルで送られてきたわけです。

[9:27] そして、私の call tool リクエストへの実際の応答には、notification/message という形のログ文が入っています。そしてその後に、call tool リクエストの実際の結果があります。これが私の call tool result です。2つの数を足し合わせた答えは 8 です。

[9:43] そして思い出してください。あの call tool result が返ってきた瞬間に、この接続は自動的にクローズされます。ちょうどそこに見えていますね。はい、接続は閉じられました。この SSE レスポンスに沿ってメッセージを受け取ることは、もうできません。

[9:56] さて、以上です。これがフローの全体です。これで、streamable HTTP トランスポートの裏側で何が起きているのか、理解できましたね。

🖥 [10:00] 画面(ブラウザ+VS Code): VS Code main.pypython # stateless_http=True, # json_response=True の2行が選択ハイライト——冒頭スライド4項目目「これらを True にすると workaround が壊れる」への呼応で、次レッスンへの引き。

[10:06] しかし、まだ完全には終わっていません。思い出してください。何度も言ったとおり、これらのフラグを true に設定したくなるシナリオがあり、それはこのフローの一部を壊すことになります。それが、私たちが理解すべき最後のことです。なぜ true に設定することなどあるのか?——おそらく皆さんも、どこかの時点でやりたくなることだからです。そして、正確には何が壊れるのか? それが、私たちが本当に解明すべき最後のことです。

英語逐語(ASR全文)

Let's do a quick recap of the last couple of videos just to make sure that we are on the same page. The first thing that we understood was that some MCP functionality, namely things like sampling and notifications and logging, relies upon the server making a request to the client. Shortly after that, we learned that when we are making use of HTTP in general, it's kind of hard to allow a server to make a request to a client. So these first two points are kind of butting heads. They are at odds. We need a server to be able to make a request to a client to get full functionality, but at the same time, that is hard to do when using HTTP. Now, as I mentioned, Streamable HTTP has a workaround to fix this, and I'm going to show you exactly what that workaround is and how the transport works in general in this video. So we're going to finally get going to get clarity on exactly what's going on. Lastly, keep in mind that in some scenarios, we are going to set those two flags to true, and we'll discuss why we'd ever set them to true. and when you do so that breaks the workaround and that's kind of why streamable http as a transport is a little bit tough to wrap your head around because we've got this limitation around http we got the workaround but in some cases we want to not use that workaround per se that's why it's a tricky subject well without further ado let's get to it let's understand how this transport actually works i'm going to begin by showing you a couple of different diagrams to help you understand the behind the scenes process. And I'm going to show you a short demonstration where you're going to see all these requests actually made and the entire system come together in a very small demo application. So here we go. First thing I want you to recall is what happens when a client initially connects to a server. According to the NCP spec, the client must send a initialized request to the server, server replies with a result, and then the client has to make a follow-up request with the initialized notification. At that point in time, the server then considers the client to be connected and everything is good to go. Now, as soon as we start to use the HTTP transport, this flow changes ever so slightly. So as soon as you start using this transport, the initialized result that gets sent back from the server is going to have a header inside the HTTP response. This header is the MCP session ID. This is a random string of numbers and letters that gets assigned to our client. It's essentially an identifier for our connection to the server. As soon as we receive that header, we are required to include it in all follow-up requests we make to the server. This allows the server to identify our client. Now, after we have done this initialization and gotten that session ID, here's where the magic part begins. So this is kind of the big workaround that allows the server to make requests to the client. As soon as we finish that initialization, the client optionally, it doesn't have to do this, optionally, it can make a get request to the MCP server and include that session ID inside the request. The response we get back is very special. It is a SSE response. SSE is short for server sent events. This is a kind of response that can be held open for an arbitrary amount of time. Once this response has been established with our client, the server can then stream back little bits of information, essentially individual messages down to our client. So once this connection has been made, now at this point in time, this is the real critical part. This now allows the server to send down messages to the client at any point in time. And this connection can be used essentially to allow the server to make a request to the client. So this is the trick. This is the workaround that the HTTP transport uses. It makes use of this long lived SSE response and it's going to stream down messages that it wants to send to the client. So now let's go a little bit further and walk through what happens now whenever the client might want to call a tool because the complexity is not quite done just yet. There's one other critical part for you to understand. So now at the top of this diagram, I've got that SSE response that we just created. And remember that response, that open connection, it has a session ID tied to it. So the MCP server knows exactly what that connection or which client that connection belongs to. So then later on at some future point in time, while that response is still running, our client might decide to make a call to a request to the server. When it makes this request, it's going to include its session ID as a header. Then the MCP server is going to open up a second SSE response. So now at this point in time, we have two separate SSE responses. The first one at the top, it is intended to be used for requests that were going from the server down to the client. The second brand new SSE response is intended to be used for messages that are related to this call tool request. And importantly, this is the critical part, this second SSE response, it is closed automatically as soon as the call tool result message is sent. So the bottom response closed automatically very shortly as soon as we get a result. And the upper response is meant to be held open for an arbitrary amount of time. Then remember, our ultimate goal here was to actually call a tool. So maybe we call the add tool that we've seen many times. And the implementation of that that I've shown you a couple times, it has some logging messages inside of it and some progress notifications, and eventually it returns a result, which would be our call tool result. Now here's another kind of little tricky part here. Technically, both the logging message and the progress notification are really tied to the call tool request. So you would kind of think that both these first messages would be sent back in the bottom response because they're both really tied to this call to a request. Well, that's not quite how many SDKs work right now. The progress notification ends up being considered to be separate from the incoming call to a request. So the progress notification actually gets sent in the first SSE response. So that's the one that is meant to be held open for a long time to allow the server to make a request down to the client. And then the login message and the actual call to a result is sent back in the response to the post request or the get tool request that was made. So that is the entire flow. Now to make sure that all this is crystal clear, I want to walk you through a very quick demonstration where you can see all these different requests being made in a very visual way. So I put together a little demonstration. First, I've got a simple MCP server put together. Once again, with a simple add tool, the add tool is going to log some information, wait for two seconds, make a progress report, and then return a result. I've connected to this MCP server with this client that is running inside the browser. This client will allow us to make a handful of different requests to the server and then show the exact response that we get. Now in total, this client is going to go through the flow that you see on the screen. So I know there's a lot here, but in essence, we're going to first go through the entire initialization process with the stuff up here at the top. We're then going to form the get SSE connection, which allows the server to send a request to the client. And then we'll do a call tool request. That's going to form a second SSE connection. That's going to be responsible for handling messages related to this particular call to a request. So let's run this and see what happens. I'm going to make the initialized request. So I'll send that off. That's going to get me my session ID, which uniquely identifies my client to the server on any future request. Remember, the session ID is only provided when you are using the streamable HTTP transport. It is not used with the standard IO transport at all. You'll notice it begins with EAA. This header is automatically taken and applied to all future requests. So if I scroll down to the next request, you'll notice the MCP session ID has automatically been applied with the correct session ID that I got from the request right above. So now that we have sent the initialized request, that one right there, our second request is initialized notification. So I'm going to send that off. Okay, so now at this point in time, we've taken care of the first three boxes on here. So now we're going to make our get request to the server. This get request is going to be held open for a long time. This is what allows the server to send a request down to our client at any point in time that it wishes. it's going to be used for things like sampling or any kind of server initiated request to make that request i'm going to use this little box down here on the bottom right hand side so i'm going to click on start get sse this request is also going to include that session id as well and now i have formed that connection so now in theory at any point in time the server can send down a message to my client for things like sampling or logging or progress notifications and so on and if i receive any of those, they should appear inside of this box. So now here's the last step. I'm going to call the add function. I've once again got the correct session ID inside there. Now to remind you, when I click on this, in theory, we would kind of want to see the log statement, progress statement, and the final call to a result all to appear in this box right here, which represents that part of the response. But in reality, just because the way that the MCP SDK for Python is set up, the progress notification is actually going to be sent as a part of this response up here. So we should see a log statement and a call to a result right here. And we should see the progress statement down here on the bottom right in that box. So let's run it and see what happens. Okay, so I'm going to run it. And then after a little delay, because remember, there's the two second pause inside there, we're then going to get our result. So sure enough, down here on the bottom right hand side, I've got a notifications progress. So that is the progress statement. I'm at 80 out of 100% completion. So that was sent in that response channel right there. And then the actual response to my call to request has a login statement in the form of notification slash message. And then after that, I've got the actual result of the call tool request. So this is my call to result. And the answer to adding those two numbers together is eight. And then remember, as soon as we get back that call to result, this connection is automatically closed. So I see right there, yes, the connection was closed, and I can no longer receive any messages along this SSE response we got. All right, so that's it. That's the entire flow. So now you understand what's going on behind the scenes with the streamable HTTP transport. But we're not quite done just yet. Remember, I said many times that there are scenarios where you're going to want to set these flags to be true, and it's going to break portions of this flow. So that's the last thing we need to understand. Why would we ever set those to true? Because it is something that you probably are going to want to do at some point. And exactly what does it break? So that's the last thing we really need to figure out.

↑ 目次へ戻る

MCP: Advanced Topics | Lesson 09「State and the StreamableHTTP transport」(全文ナレッジ)

動画 6分29秒 / 講師: Stephen Grider(Anthropic・Member of Technical Staff) / 文字起こし: mlx_whisper / 2026-07-03

日本語ナレッジ(要点整理)

  • 主題は stateless_http / json_response の2フラグの正体。何を無効にするのか、いつ true にすべきなのかを、水平スケーリングのシナリオから逆算して理解する回。
  • フラグが必要になるシナリオ=水平スケーリング:公開 MCP サーバーが人気になり、単一インスタンスでは捌けなくなる → サーバーのコピーを複数立ててロードバランサーで振り分ける。ところが StreamableHTTP は「長寿命 GET SSE(server→client 用)」と「POST ごとの SSE レスポンス」の2本の接続を前提とするため、GET SSE を張ったサーバーと tools/call の POST を受けるサーバーが別マシンに分裂しうる。
  • 分裂すると何が困るか:サーバー2上のツールが sampling(CreateMessageRequest)を使いたくても、sampling は GET SSE 経路を通す必要があり、その接続はサーバー1が保有。リクエストをサーバー1へ回送 → GET SSE でクライアントへ → クライアントが Claude で生成 → 結果をサーバー2へ戻す……という調整(コーディネーション)が必要になり、「できなくはないが、多大な困難と追加インフラ」。
  • stateless_http=True の効果:クライアントにセッション ID を発行しない → サーバーはクライアントを追跡できない → GET SSE 経路が使えない → server→client リクエスト全般、すなわち sampling/progress 報告/logging/subscription(リソース更新等)が全部無効になる。講師の比喩=「口座 ID のない銀行」(金を受け取っても誰のものか分からず、誰に返すべきかも分からない)。
  • stateless_http=True の利点:initialize リクエスト+initialized notification の初期化2往復が不要になり、サーバーが受けるトラフィックが確実に減る。LB 配下ではどのサーバーに当たっても処理できる(リクエスト単発で完結)。
  • json_response=True の効果:POST リクエストへのレスポンスのストリーミングを無効化。中間の notification(ログ等)は届かず、最終結果だけの単一プレーン JSON が返る。
  • デモでの実測差(MCP Protocol Demo アプリ・add ツール):
  • 通常時=content-type: text/event-streamtransfer-encoding: chunkedevent: message が複数(notifications/message のログ → 最終 result)流れ、result 到達で SSE 接続が閉じる。
  • json_response=True 時=content-type: application/jsoncontent-length: 90 の単一ボディ。中間メッセージゼロ、結果「8」のみ。
  • 締めの教訓:ローカルでは stdio、本番では StreamableHTTP……とトランスポートを変えると挙動が変わる。開発時から本番で使う予定のトランスポートで開発せよ

実務ポイント(このレッスンから持ち帰るもの)

  1. 公開・水平スケール前提の MCP サーバーは stateless_http=True(多くは json_response=True も)が定石。FastMCP ならコンストラクタ引数2つで済む(本レッスン配布コード main.py 参照)。逆にセッション調整インフラ(sticky session や pub/sub でのリクエスト回送)を組むくらいなら、stateless に倒すほうが現実的——というのが講師の整理。
  2. stateless にした瞬間に失うもの一覧を設計へ織り込む:sampling・progress・logging・subscription が全滅する。ツールは「クライアントに頼らず自己完結」に再設計する(例:LLM が必要ならサンプリングではなくサーバー側で自前の API キーで呼ぶ)。
  3. 2フラグは独立。デモでは json_response=True のみ有効化した実行があり、その場合もセッション ID は発行されていた(=ストリーミングだけが消える)。「stateless=json_response」ではない。
  4. 挙動差はレスポンスヘッダで即診断できる:ストリーミング時は text/event-streamchunked、無効時は application/jsoncontent-length。デプロイ後に「進捗が出ない」「ログが来ない」ときはまずここを見る。
  5. Lesson 07 の故障デモの答え合わせ:Wikipedia Research Assistant で進捗バー・ログ・最終レポート(sampling)が壊れたのは、この2フラグが GET SSE 経路と POST ストリーミングを切ったから。
  6. 開発と本番のトランスポートを一致させる。stdio で全部動いたのに HTTP 化で壊れる——を避ける最短の習慣。

本文(時系列・日本語逐語+画面差し込み)

[0:00] 最後にやるべきことは、stateless と JSON response のフラグが一体何なのかを理解することです。これらは私たちのサーバーにどう影響し、そもそもいつ true に設定するのでしょうか?

🖥 [0:00] 画面(VS Code:main.py — transport-http): ```python import asyncio from starlette.requests import Request from starlette.responses import Response from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP( "mcp-server", # stateless_http=True, # json_response=True, )

@mcp.tool() async def add(a: int, b: int, ctx: Context) -> int: await ctx.info("Preparing to add...") await asyncio.sleep(2) await ctx.report_progress(80, 100)

return a + b

Load the demo HTML page

@mcp.custom_route("/", methods=["GET"]) async def get(request: Request) -> Response: `` ——この時点では **2フラグとも先頭#` でコメントアウト**(=デフォルトの false)。

📄 実物コード(assets/transport-http/main.py 全文)——配布版では2フラグが有効化済みの状態で入っている: ```python import asyncio from starlette.requests import Request from starlette.responses import Response from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP( "mcp-server", stateless_http=True, json_response=True, )

@mcp.tool() async def add(a: int, b: int, ctx: Context) -> int: await ctx.info("Preparing to add...") await asyncio.sleep(2) await ctx.report_progress(80, 100)

return a + b

Load the demo HTML page

@mcp.custom_route("/", methods=["GET"]) async def get(request: Request) -> Response: with open("index.html", "r") as f: html_content = f.read() return Response(content=html_content, media_type="text/html")

mcp.run(transport="streamable-http") `` 起動方法(assets/transport-http/README.md):uv syncuv run main.py`

[0:08] まず、これらを true に設定したくなるシナリオを一通り歩いてみたいと思います。そうすると、なぜこのフラグが存在し、サーバーにどう影響するのかの枠組みが本当によく見えるからです。

[0:15] あなたと私で MCP サーバーを作り、誰でも接続できる形でどこかにオンライン公開でデプロイしたと想像してみましょう。私たち自身のクライアントで接続し、他の人たちも自分のクライアントで接続してくるかもしれません。

🖥 [0:15] 画面(スライド): 上段に「MCP Client」の箱が3つ(ラベル: YouSomeone elseSomeone else)、右に「+ 99999 other people...」。全員の矢印が下の1点、「MCP Server」(枠外ラベル: Remote Machine)へ収束。

[0:27] 時間が経つにつれて、私たちのサーバーは本当に大人気になるかもしれません。最初に接続した3つのクライアントに加えて、他にもたくさんの人が接続してくる。ある時点で、1台のマシン上でサーバーの単一インスタンスを走らせるだけでは、流入するトラフィック量に対しておそらくスケールしなくなります。

[0:41] これを解決する一つの方法が、ちょっとした水平スケーリングです。水平スケーリングでは、サーバーのコピーを複数走らせ、その手前にロードバランサーを置いてアクセスを制御します。入ってくるリクエストは、これらの異なるサーバーのどれかにランダムにルーティングされます。こうすれば、より高いトラフィック需要に応えられるようスケールアップできます。

[0:58] さて、ここにロードバランサーを置いたら何が起きるかを考えましょう。特に念頭に置きたいのは、任意のクライアントから MCP サーバーへ、実質2本の別々の接続を持ちたいという事実です。第一に、あの GET リクエストを常時走らせておきたい。サーバーからクライアントへのリクエストを受け取れるようにするためです。さらにクライアントは POST リクエストも送り、その中にいくつかのメッセージを含む SSE レスポンスを受け取る必要があります。

🖥 [1:00] 画面(スライド): 上段「MCP Client」×3(You/Someone else/Someone else)→ 中央の横長ボックス「Load Balancer」→ 下段「MCP Server」×3 へ分岐。矢印はすべて クライアント → LB → サーバー の下向き。

[1:22] では、1つのクライアントが、たとえば2台の異なるサーバーに接続する場合だけを考えます。このクライアントはまず最初のリクエストを行い、最初の GET SSE レスポンスのパイプラインをセットアップします。このレスポンスは保留状態、つまりクライアントへ向かって走りっぱなしになっているとイメージしてください。繰り返しますが、これはすべて、サーバーがクライアントへリクエストを送れるようにするための仕組みです。

🖥 [1:30] 画面(スライド): 上「MCP Client」・中央「Load Balancer」・下「MCP Server」×2。左の MCP Server からクライアントへ、ロードバランサーを迂回する橙色の曲線矢印が上向きに伸び、ラベル「GET SSE Response」。右のサーバーには接続なし。

[1:42] それから、将来のある時点で、クライアントがツールを実行しようと決めるかもしれません。POST リクエストを使って call tool リクエストを送る。そして、このリクエストが2台目の MCP サーバーへルーティングされるかもしれない。2台目のサーバーは、自分自身の別の SSE レスポンスをセットアップして応答します。

[1:59] ここからは、図を分かりやすくするためにロードバランサーは表示しません。ツールがそのサーバーの中で走るとき、Claude を使う必要があると想像してみましょう。create message リクエストを作ることで Claude を使おうとするかもしれない。あれは sampling を使う試みを表すのでしたね。

🖥 [2:00] 画面(スライド): 上「MCP Client」。左下「MCP Server」からクライアントへ橙色矢印「GET SSE Response」。右下「MCP Server」の枠内に薄橙の内箱2つ——「Create Message Request」「Call Tool Result」——があり、このサーバーからクライアントへ橙色矢印「POST SSE Response」。GET SSE の持ち主(サーバー1)と、sampling を発したいサーバー2が別、という分裂の図。

[2:14] さて、sampling リクエストは必ず GET SSE レスポンスを通す必要があります。しかしその接続は、完全に別のサーバーによって形成されたものです。だから私たちは、この(サーバー2の)リクエストをあちらのサーバー(サーバー1)へ届ける方法をどうにか考え出し、サーバー1にこの GET SSE レスポンス経由でリクエストをクライアントへ送らせ、MCP クライアントに Claude を走らせてテキストを生成させ、それをサーバー1へ送り返し、さらにその生成テキストをどうにかしてサーバー2へ戻す——ということをやらないといけません。

[2:43] すぐお分かりのとおり、これらすべてを調整するのは本当に、本当に大変です。絶対にできないわけではありません。しかし、多大な困難と、多くの追加設定・追加インフラを意味します。

[2:52] だから、水平スケールさせる想定の MCP サーバーを作っていて、こうした余計な調整やインフラ構築を経たくないのであれば、あの stateless HTTP フラグを true に設定する、という決断をするかもしれません。

🖥 [3:00] 画面(スライド・このレッスンの核心図): 左上ボックス—— 「Clients don't get a session ID - server can't keep track of clients. Disables: ・Server to client requests ・Sampling ・Progress reports ・Subscriptions (resource updates, etc)」 左下ボックス——「POST request responses aren't streamed」 右の黒いコードパネル: python mcp = FastMCP( "mcp-server", stateless_http=True, json_response=True, ) stateless_http=True,json_response=True, の行がそれぞれ橙色の枠で囲まれ、上のボックス→stateless_http、下のボックス→json_response へ矢印が結ばれている。

[3:02] このフラグを true にすると、即座に生じる、しかし非常に重要な効果が1つあります。クライアントがセッション ID をもらえなくなるのです。つまり、サーバーはクライアントを一切追跡(トラッキング)できなくなります。それだけで、いくつもの大きな波及効果が生まれます。セッション ID がなければ、MCP サーバーはもう GET SSE レスポンス経路を使ってクライアントへリクエストを送り下ろすことができません。

🖥 [3:20] 画面(スライド「No session id?」): 箇条書き—— ・GET SSE response can't be used anymore - the server can't figure out how to pair that response pathway with any incoming request ・Without the GET SSE response, we can't use sampling, progress logging, subscriptions ・In stateless mode, client initialization is no longer required 右図: 上「MCP Client」・下「MCP Server」。左側の「GET SSE Response」矢印は灰色破線にグレーアウト(無効化)、右側の「POST SSE Response」は黒い実線のまま。

[3:25] なぜあのレスポンス経路がもう使えないのかを理解するために、口座 ID が一切ない銀行を考えてみることをおすすめします。銀行はお金を受け取るかもしれませんが、誰がそのお金をくれたのか本当のところ分からないし、誰にお金を渡すべきかも分からない。誰に何が帰属するのか分からないからです。識別トークンがないというのは、ここでも同じ状況なのです。

[3:45] そして、あの GET SSE レスポンス経路がないということは、サーバーがクライアントへ一切リクエストを送れないので、sampling や progress、logging、リソース変更などまわりの subscription といった機能も使えない、ということです。

[4:01] ただし、これにはもう一つの利点もあります。stateless モードでは、クライアントの初期化をもう経る必要がないのです。つまり、こちらのアプリケーションに戻ると——思い出してください、サーバーに接続するときは毎回、最初の initialized リクエストを送り、その後に initialized notification を送らないといけませんでした。stateless モードのときは、この2つのリクエストを送る必要がありません。これはサーバーが受け取るトラフィック量を確実に削減してくれます。ですから、ここにはなかなか良いトレードオフがあるわけです。

🖥 [4:07] 画面(ブラウザ:localhost:8000「MCP Protocol Demo」ページ最上部): バナー「Model Context Protocol - Streamable HTTP Transport」。カード「1 Initialize Request」——POST Request: json { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": { "roots": { "listChanged": true } }, "clientInfo": { "name": "MCP Demo Client", "version": "1.0.0" } } } ヘッダ: Content-Type: application/jsonAccept: application/json, text/event-stream。Response 側は「No response yet. Click the button to send request.」、ボタン「Send Initialize Request」。その下にカード「2 Initialized Notification」("method": "notifications/initialized"mcp-session-id: Not yet available)とカード見出し「3 Tool Call - Add Function」。右下にフロートパネル「Server-Initiated Events (GET SSE)」(ボタン: Start GET SSE/Stop SSE/Clear)。

[4:24] さて、私たちが議論してきたもう一つのフラグは JSON response でした。JSON response を true に設定すると、それは単に、(クライアントへ送り返す)POST リクエストのレスポンスでストリーミングが有効にならない、という意味になります。これはとても単純で簡単なデモをお見せできます。

[4:43] こちらで手短に、このアプリケーションが普段どう動くかを思い出してもらいましょう。initialized リクエストを送り、initialized notification を送り、そして add ツール関数を呼ぶと、あの SSE 接続が開いて、まず message を受け取り、その後に call tool result を受け取ります。

🖥 [5:00] 画面(ブラウザ:Initialize 実行後の Response): ヘッダ server: uvicorn。ボディ: json { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "experimental": {}, "prompts": { "listChanged": false }, "resources": { "subscribe": false, "listChanged": false }, "tools": { "listChanged": false } }, "serverInfo": { "name": "mcp-server", "version": "1.9.3" } } }

🖥 [5:00] 画面(VS Code:切り替え直前の main.py): # stateless_http=True,# json_response=True, と、この実行までは両フラグともコメントアウトのまま(カーソルは # json_response=True, 行末)。

[5:03] では、ここで素早く JSON response フラグを true に切り替えます。繰り返しますが、これで私が受け取るレスポンスはすべて、最終結果だけのプレーンな JSON になります。ストリーミングは一切ありません。

🖥 [5:10] 画面(ブラウザ:通常時(ストリーミング有効)の Tool Call 実行結果): 左の POST Request: json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "add", "arguments": { "a": 5, "b": 3 } } } Request ヘッダに mcp-session-id: 23c4c69f325d47c4937f421ebcf83b42(=セッション ID あり)。Response ヘッダ: cache-control: no-cache, no-transformconnection: keep-alivecontent-type: text/event-streammcp-session-id: 23c4c69f325d47c4937f421ebcf83b42server: uvicorntransfer-encoding: chunkedx-accel-buffering: no ストリーム表示: text stream: SSE connection established event: message data: {"method":"notifications/message","params": {"level":"info","data":"Preparing to add..."},"jsonrpc":"2.0"} event: message data: {"jsonrpc":"2.0","id":3,"result":{"content": [{"type":"text","text":"8"}],"isError"…(選択ハイライトで末尾一部隠れ) stream: SSE connection closed ——1本のレスポンスの中に「中間ログ notification → 最終 result」が別イベントとして流れ、result 後に接続が閉じる

📄 実物コード(assets/transport-http/index.html 556-568・701-730行あたり)——この画面のカード3の Request ボディと GET SSE パネルは、配布 HTML にそのまま定義されている: html <pre id="tool-request-body"> { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "add", "arguments": { "a": 5, "b": 3 } } }</pre> html <!-- SSE Panel for GET requests --> <div class="sse-panel"> <h3> Server-Initiated Events (GET SSE) <span class="sse-indicator" id="sse-indicator"></span> </h3> ... <button class="sse-button" onclick="startSSE()" id="sse-start-button" disabled> Start GET SSE </button> <button class="sse-button" onclick="stopSSE()" id="sse-stop-button" disabled> Stop SSE </button> <button class="sse-button" onclick="clearSSE()">Clear</button> </div>

[5:12] 同じプロセスをもう一度やっていきます。initialize を実行し、notification を実行します。そして今度は add 関数を呼んでも、ログのような中間メッセージは受け取りません。その代わり、最終的な tool call result を受け取り、それ以外は何もなし、です。

[5:29] さあ実行してみると、ストリーミングのレスポンスは返ってきません。その代わり、ツール呼び出しが完了するまで待って、結果だけを受け取り、それ以外は絶対に何も受け取りません。ログ文は一切なしです。

[5:39] というわけで、この2つのフラグがサーバーの振る舞いにどれほど甚大な影響を与えるか、すぐにお分かりいただけます。しかし、アプリケーションをどう開発し、どうデプロイしようとしているかによっては、これらのフラグを true に設定することが完全に適切な場合もあるのです。

🖥 [5:42] 画面(ブラウザ:json_response=True 時の Tool Call 実行結果): 左の POST Request は同じ tools/call(a=5, b=3)。Request ヘッダ mcp-session-id: d33e9b2f3a7e49a999f3d0d05c235fb1(ページ再読込で新セッション・この実行でも session ID は発行されている=stateless_http は無効のまま)。Response に緑バッジ「✓ Success」。Response ヘッダ: content-length: 90content-type: application/jsonmcp-session-id: d33e9b2f3a7e49a999f3d0d05c235fb1server: uvicorn ボディ(単一 JSON・stream:/event: 行なし・transfer-encoding: chunked なし): json { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "8" } ], "isError": false } }

[5:50] さて、多くの忍耐と、たくさんの異なる図を経て、streamable HTTP トランスポートがどう動くのか、ようやく妥当な理解にたどり着けたと思います。この2つのフラグと、それがサーバーに正確にどう影響するかを覚えておいてください。

[6:06] もしローカルマシン上で standard IO をトランスポートとして開発していて、本番へデプロイするときに streamable HTTP トランスポートを使うつもりなら、サーバーの挙動が少し変わりうることを頭に入れておいてください。ですから、サーバーを開発するときは、本番で使う予定のトランスポートを使って開発することをおすすめします。そうすれば、後々の多くのトラブルを避けられますから。

英語逐語(ASR全文)

The last thing we need to do is understand what the stateless and JSON response flags are all about. How do they affect our server and when would we ever set them to true? I first want to walk you through a scenario where we might want to set these things to true because it really frames why these flags exist and how they affect our server. Let's imagine that you and I build an MCP server and we deploy it somewhere publicly online where anyone can connect to it. So maybe we connect to it with our own client and then some other people connect to it with their own clients as well. over time our server might become really really popular and in addition to the three initial clients we had connected maybe many many other people connect to it as well at a certain point running a single instance of our server on a single machine would probably not scale to the amount of traffic that is incoming so one way we might solve this is through a little bit of horizontal scaling with horizontal scaling we might run multiple copies of our server and then gate access to them with a load balancer so any incoming request would be randomly routed to one of these different servers. This would allow us to scale up to meet higher demands of traffic. Now let's think about what would happen if we put a load balancer here, particularly keeping in mind the fact that we want to have really two separate connections to our MCP server from any given client. First, we want to have that running git request running at all times so we can receive requests from the server to the client. And our client also needs to make post requests and then receive a SSE response with some number of messages inside it as well. So let's consider just one client connecting to maybe two different servers. This client would then make an initial request, setting up that initial get SSE response pipeline. So we then imagine that we've got this response pending or kind of running continuously down to the client. Again, remember, this is all about allowing a server to send a request down to the client. Then maybe at some future point in time, the client decides to run a tool. So it might make a call tool request using a post request and maybe this request gets routed to the second MCP server. The second server would then respond by setting up its own separate SSE response. Now at this point in time I'm not going to show the load balancer just for clarity. As the tool runs inside that server let's imagine for a second that it needs to make use of Claude and it might try to make use of Claude by creating a create message request. Remember that represents a attempt to use sampling. Now sampling requests always need to go through the git sse response, but that connection has been formed by a completely separate server. So we would need to figure out some way to get this request right here over to this other server and have this server send the request down through this git sse response, have the mcp client run clod generate some text, send it back to this server, and then somehow get that generated text back over to this server. As you can tell right away, coordinating all this would be really really challenging. Now it absolutely could be done but it represents a lot of difficulty and a lot of added configuration and infrastructure. So if we are building an NCP server that we expect to scale horizontally and we do not want to go through all this extra coordination and infrastructure setup we might decide to set that stateless HTTP flag to true. Setting this flag to true has one immediate but very important effect. It means that clients do not get a session ID, and that means that a server can never keep track of a client. Right away, that has some really big follow-up effects. Without a session ID, the MCP server can no longer make use of the getSSE response pathway to send requests down to the client. To understand why that response pathway can't be used anymore, I'd encourage you to think of a bank where you don't have any account IDs. So a bank might receive money, but it wouldn't really know who gave them the money and it wouldn't really know who to give money to because it doesn't know who is owed what. That's the same situation here without any identifying token. Now without that get SSE response pathway that means that because our server can't send any requests on the client we also can't use features like sampling or progress logging or subscriptions around things like changing resources and so on. There's another upside to this however. In stateless mode, you do not need to go through client initialization anymore. So that means that back over here inside of this application, remember whenever we connect to the server, we had to make that initial initialized request and then the follow-up initialized notification. When you're in stateless mode, you do not have to make those two requests, which definitely cuts down on the amount of traffic that your server is receiving. So there is a pretty good trade-off here. Now the other flag that we've been discussing has been JSON response. Whenever you set JSON responds to true, that just means that the post requests that you send down to the client are not going to have streaming enabled. And I can give you a very simple and easy demonstration of what that looks like. So over here, very quickly, let me give you a reminder of how this application usually works. I'll send the initialized request, initialized notification, and then when I call the add tool function, I'm going to open up that SSE connection, and I'll first get back a message and then the call tool result. Now I'm going to very quickly flip the JSON response flag to true. And again, that means that all the responses that I get back are going to be just the final result as plain JSON. There's no streaming at all. So I'm going to go through the same process again. I'm going to run the initialize. I'm going to run the notification. And now this time when I call the add function, I'm not going to get those intermediate messages about some logging or anything like that. Instead, instead, I'm going to get the final tool call result and nothing else. So now if I run this, we'll see that I'm not streaming response back. Instead, I'm waiting until the tool call is complete and I get just the result and absolutely nothing else. No log statements at all. So you can see right away how these two flags have a tremendous impact on how your server behaves. But depending upon how you are trying to develop and deploy your application, setting these flags to true might be entirely appropriate all right through a lot of patience and many different diagrams i think we finally got a reasonable idea of how the streamable http transport works so keep in mind these two flags and exactly how they affect your server if you are developing your server in development on your local machine using standard io as your transport when you deploy to production if you are intending to use the streamable http transport just keep in mind that your server might behave a little bit differently. So as you are developing your server, I would encourage you to use the transport that you plan to use in production, because it's going to save you a lot of trouble down the line.

↑ 目次へ戻る

MCP: Advanced Topics | Lesson 10「Wrapping up」(全文ナレッジ)

動画 0分48秒 / 講師: Stephen Grider(Anthropic・Member of Technical Staff) / 文字起こし: mlx_whisper / 2026-07-03

日本語ナレッジ(要点整理)

  • コースの締め。全10レッスン(sampling → notifications/logging → roots → メッセージフォーマット → stdio トランスポート → StreamableHTTP 深掘り)を終えての「次の一歩」が3つ提示される。
  • 次の一歩①:MCP コミュニティに参加する。GitHub にホストされたディスカッションボード(github.com/orgs/modelcontextprotocol/discussions)には、MCP の今後の変更やアイデアに関する膨大な議論がある。他の人が考えている MCP のユースケースを覗くだけでも価値がある。
  • 次の一歩②:MCP 公式ホームページ(modelcontextprotocol.io/)をウォッチする。スペック(仕様)の変更や最新ニュースはここに出る。
  • 次の一歩③:自分の MCP サーバーを作る。「自分で作ることに勝るものはない」。このコースで配布されたコードを流用して、学んだトピック(sampling/notifications/roots/StreamableHTTP など)を実際に実装して練習することを強く推奨。

実務ポイント(このレッスンから持ち帰るもの)

  1. スペック追従の一次情報源は2つ:仕様変更・新機能の議論は GitHub Discussions(github.com/orgs/modelcontextprotocol/discussions)、確定情報は公式サイト(modelcontextprotocol.io/)。MCP は仕様の動きが速いので、この2つの定点観測をワークフローに入れる。
  2. 配布コードは練習台として再利用してよい、と講師が明言。本コースの assets(sampling/notifications/roots/transport-http の各プロジェクト)が、そのまま自作サーバーの雛形になる。
  3. 学習の完成は「作ること」。特に Lesson 07〜09 で学んだ StreamableHTTP の2フラグ(stateless_httpjson_response)の挙動差は、自分でサーバーを立てて確かめるのが最短の定着法。

本文(時系列・日本語逐語+画面差し込み)

[0:00] さて、締めくくりの時間です。ただ、お別れの前に、ここから先の次のステップについて、いくつかアイデアをお伝えしたいと思います。

🖥 [0:00] 画面(スライド・動画全編を通して表示): タイトル「Wrap up and next steps」。右側にカード3枚—— 1. 「Engage in the community」 github.com/orgs/modelcontextprotocol/discussions 2. 「Watch the MCP homepage」 modelcontextprotocol.io/ 3. 「Build your own MCP server」 Practice the skills you've learned in this course

左下に ANTHROP\C ロゴ。

[0:06] まず第一に、MCP コミュニティへの参加を強くおすすめします。

[0:10] GitHub 上にホストされているディスカッションボードがあり、MCP の今後の変更やアイデアをめぐる膨大な量の議論が交わされています。

[0:18] 他の人たちが考え出している MCP のユースケースに関するさまざまなアイデアを、ぜひ覗いてみてください。

[0:23] 第二に、MCP のホームページをウォッチすることを強くおすすめします。今後のニュースやスペックの変更が出てきます。

[0:29] 最後に、自分自身の MCP サーバーを作ることに勝るものはありません。

[0:33] ですから、このコースを通じて私が渡してきたコードをぜひ持ち帰って、学んできたさまざまなトピックを練習したり、実装したりしてみてください。

[0:42] さて、今回はここまでです。このコースを楽しんでいただけたなら幸いです。作っていて、間違いなく楽しいコースでしたから。

英語逐語(ASR全文)

Well, it is time to wrap things up, but before I let you go, I want to give you some ideas on next steps from here. First off, I highly recommend you engage in the MCP community. There is a discussion board hosted on GitHub that has a tremendous amount of discussion around upcoming changes or ideas in MCP. Take a look at some of the different ideas that other people are coming up with on use cases for MCP. Second, I highly recommend you watch the MCP homepage for upcoming news and changes to the spec. Finally, nothing replaces building your own MCP server. So I highly recommend you take some of the code that I've given you throughout this course and use it to practice some of the different topics or implement the different topics that you have learned about. All right, that is it for now. I hope you've enjoyed this course because it definitely has been a pleasure to put together.

↑ 目次へ戻る

MCP: Advanced Topics | Walkthrough「Sampling walkthrough」(全文ナレッジ)

インタラクティブ教材(全6ステップ)+実物コード / 2026-07-03

Lesson 02「Sampling」の実装ウォークスルー。教材と同一プロジェクトの実物コード(assets/sampling/server.pyclient.py)を各ステップに対応させて引用する。

日本語ナレッジ(ステップ別・完全版)

Step 1 — Initiating sampling(サンプリングの開始)

原文の日本語訳: サーバー側で、ツール呼び出しの最中に create_message() メソッドを実行し、言語モデルへ送りたいメッセージを渡します。

該当する実物コード(server.pysummarize ツール内):

@mcp.tool()
async def summarize(text_to_summarize: str, ctx: Context):
    prompt = f"""
        Please summarize the following text:
        {text_to_summarize}
    """

    result = await ctx.session.create_message(
        messages=[
            SamplingMessage(
                role="user", content=TextContent(type="text", text=prompt)
            )
        ],
        max_tokens=4000,
        system_prompt="You are a helpful research assistant.",
    )

ポイント: ツール関数は最後の引数として ctx: Context を受け取り、ctx.session.create_message() がクライアントへの sampling リクエストになる。max_tokenssystem_prompt も一緒に指定できる。

Step 2 — Sampling callbacks(サンプリングコールバック)

原文の日本語訳: クライアント側では、sampling callback を実装しなければなりません。このコールバックは、サーバーから提供されたメッセージのリストを受け取ります。

該当する実物コード(client.py):

async def sampling_callback(
    context: RequestContext, params: CreateMessageRequestParams
):
    # Call Claude using the Anthropic SDK
    text = await chat(params.messages)

ポイント: コールバックは RequestContextCreateMessageRequestParams を受け取り、サーバーが送ったメッセージ群は params.messages に入っている。

Step 3 — Message formats(メッセージフォーマット)

原文の日本語訳: サーバーから提供されるメッセージのリストは、MCP での通信用にフォーマットされています。個々のメッセージが、あなたの使っている LLM SDK とそのまま互換である保証はありません。たとえば Anthropic SDK を使っているなら、MCP のメッセージを Anthropic SDK 互換の形式へ変換する、ちょっとした変換ロジックを書く必要があります。

該当する実物コード(client.pychat() 内の変換ロジック):

async def chat(input_messages: list[SamplingMessage], max_tokens=4000):
    messages = []
    for msg in input_messages:
        if msg.role == "user" and msg.content.type == "text":
            content = (
                msg.content.text
                if hasattr(msg.content, "text")
                else str(msg.content)
            )
            messages.append({"role": "user", "content": content})
        elif msg.role == "assistant" and msg.content.type == "text":
            content = (
                msg.content.text
                if hasattr(msg.content, "text")
                else str(msg.content)
            )
            messages.append({"role": "assistant", "content": content})

    response = await anthropic_client.messages.create(
        model=model,
        messages=messages,
        max_tokens=max_tokens,
    )

    text = "".join([p.text for p in response.content if p.type == "text"])
    return text

ポイント: MCP の SamplingMessagerolecontent.type/text)を、Anthropic SDK の {"role": ..., "content": ...} の dict 形式へ詰め替えてから anthropic_client.messages.create() を呼ぶ。レスポンス側も text タイプのブロックだけを連結して素のテキストに戻している。

Step 4 — Returning generated text(生成テキストの返却)

原文の日本語訳: LLM でテキストを生成したあとは、生成されたテキストを含む CreateMessageResult を返します。

該当する実物コード(client.pysampling_callback の戻り値):

    return CreateMessageResult(
        role="assistant",
        model=model,
        content=TextContent(type="text", text=text),
    )

ポイント: role="assistant"、使用モデル名(この実物コードでは model = "claude-sonnet-4-0")、TextContent に包んだ生成テキスト、の 3 点を返す。

Step 5 — Connecting the callback(コールバックの接続)

原文の日本語訳: 忘れないでください。クライアント側のコールバックは、ClientSession の呼び出しに渡す必要があります。

該当する実物コード(client.pyrun() 内):

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(
            read, write, sampling_callback=sampling_callback
        ) as session:
            await session.initialize()

ポイント: sampling_callback=ClientSession に渡し忘れると、サーバー側の create_message() に応答できない。ここが接続の要。

Step 6 — Getting the result(結果の受け取り)

原文の日本語訳: クライアントがテキストを生成して返したあと、それはサーバーへ送られます。このテキストは何にでも使えます: - ツール内のワークフローの一部として使う - さらにもう一度 sampling 呼び出しを行うと決める - 生成されたテキストをそのまま返す

該当する実物コード(server.pycreate_message の結果処理):

    if result.content.type == "text":
        return result.content.text
    else:
        raise ValueError("Sampling failed")

(クライアント側でツール結果を受け取る箇所、client.py):

            result = await session.call_tool(
                name="summarize",
                arguments={"text_to_summarize": "lots of text"},
            )
            print(result.content)

ポイント: この実物コードは 3 択のうち「生成されたテキストをそのまま返す」パターン。result.content.type を確認してから text を取り出すのが定石。

実物コード全文

server.py

from mcp.server.fastmcp import FastMCP, Context
from mcp.types import SamplingMessage, TextContent

mcp = FastMCP(name="Demo Server")


@mcp.tool()
async def summarize(text_to_summarize: str, ctx: Context):
    prompt = f"""
        Please summarize the following text:
        {text_to_summarize}
    """

    result = await ctx.session.create_message(
        messages=[
            SamplingMessage(
                role="user", content=TextContent(type="text", text=prompt)
            )
        ],
        max_tokens=4000,
        system_prompt="You are a helpful research assistant.",
    )

    if result.content.type == "text":
        return result.content.text
    else:
        raise ValueError("Sampling failed")


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

client.py

import asyncio
from anthropic import AsyncAnthropic
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.session import RequestContext
from mcp.types import (
    CreateMessageRequestParams,
    CreateMessageResult,
    TextContent,
    SamplingMessage,
)

anthropic_client = AsyncAnthropic()
model = "claude-sonnet-4-0"

server_params = StdioServerParameters(
    command="uv",
    args=["run", "server.py"],
)


async def chat(input_messages: list[SamplingMessage], max_tokens=4000):
    messages = []
    for msg in input_messages:
        if msg.role == "user" and msg.content.type == "text":
            content = (
                msg.content.text
                if hasattr(msg.content, "text")
                else str(msg.content)
            )
            messages.append({"role": "user", "content": content})
        elif msg.role == "assistant" and msg.content.type == "text":
            content = (
                msg.content.text
                if hasattr(msg.content, "text")
                else str(msg.content)
            )
            messages.append({"role": "assistant", "content": content})

    response = await anthropic_client.messages.create(
        model=model,
        messages=messages,
        max_tokens=max_tokens,
    )

    text = "".join([p.text for p in response.content if p.type == "text"])
    return text


async def sampling_callback(
    context: RequestContext, params: CreateMessageRequestParams
):
    # Call Claude using the Anthropic SDK
    text = await chat(params.messages)

    return CreateMessageResult(
        role="assistant",
        model=model,
        content=TextContent(type="text", text=text),
    )


async def run():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(
            read, write, sampling_callback=sampling_callback
        ) as session:
            await session.initialize()

            result = await session.call_tool(
                name="summarize",
                arguments={"text_to_summarize": "lots of text"},
            )
            print(result.content)


if __name__ == "__main__":
    import asyncio

    asyncio.run(run())

英語原文(全ステップ)

  1. Initiating sampling — On the server, during a tool call, run the create_message() method, passing in some messages that you wish to send to a language model.
  2. Sampling callbacks — On the client, you must implement a sampling callback. It will receive a list of messages provided by the server.
  3. Message formats — The list of messages provided by the server are formatted for communication in MCP. The individual messages aren't guaranteed to be compatible with whatever LLM SDK you are using. For example, if you're using the Anthropic SDK, you'll have to write a little bit of conversion logic to turn the MCP messages into a format compatible with Anthropic's SDK.
  4. Returning generated text — After generating text with the LLM, you'll return a CreateMessageResult, which contains the generated text.
  5. Connecting the callback — Don't forget: the callback on the client needs to be passed into the ClientSession call.
  6. Getting the result — After the client has generated and returned some text, it will be sent to the server. You can do anything with this text: Use it as part of a workflow in your tool / Decide to make another sampling call / Return the generated text
↑ 目次へ戻る

MCP: Advanced Topics | Walkthrough「Notifications walkthrough」(全文ナレッジ)

テキスト教材(Skilljar インタラクティブ・ステップ送り式/教材ID 291036/全4ステップ)/ 2026-07-03 実物コード: assets/notifications/(server.py・client.py・pyproject.toml・uv.lock・README.md)——Lesson 03「Log and progress notifications」の実装対応版

日本語ナレッジ(ステップ別・完全版)

ステップ1|Tool function receives Context argument(ツール関数は Context 引数を受け取る)

: ツール関数は、最後の引数として自動的に 'Context' を受け取る。このオブジェクトには、クライアントに対してログを送る/進捗を報告するためのメソッドが備わっている。

実物コード断片(server.py):

from mcp.server.fastmcp import FastMCP, Context

@mcp.tool()
async def add(a: int, b: int, ctx: Context) -> int:

補足: FastMCP は引数の 型アノテーション Context を見て自動注入する。ツールのスキーマ(クライアントに公開される引数一覧)には ctx は現れない——a, b だけが公開引数になる。

ステップ2|Create logs and progress with context(context でログと進捗を作る)

: ツール関数の中で info()warning()debug()error() メソッドを呼び、種類の異なるメッセージをクライアント向けにログする。また report_progress() メソッドを呼び、そのツール呼び出しの残り作業量の見積もりを報告する。

実物コード断片(server.py・add ツール本体):

    await ctx.info("Preparing to add...")
    await ctx.report_progress(20, 100)

    await asyncio.sleep(2)

    await ctx.info("OK, adding...")
    await ctx.report_progress(80, 100)

    return a + b

補足: ログメソッドは4段階(info / warning / debug / error)。report_progress(progress, total) は「20/100 → 80/100」のように処理の節目で段階的に打つ。すべて await が必要(メッセージ送信は非同期)。デモの asyncio.sleep(2) は長時間処理の擬似——実務ではここが実処理になる。

ステップ3|Define callbacks on the client(クライアント側でコールバックを定義する)

: クライアントは logging コールバックprogress コールバックを定義する必要がある。サーバーがログ/進捗メッセージを emit するたびに、これらは自動的に呼び出される。コールバックは、受け取ったログ・進捗データをユーザーに表示するよう努めるべきである。

実物コード断片(client.py):

from mcp.types import LoggingMessageNotificationParams

async def logging_callback(params: LoggingMessageNotificationParams):
    print(params.data)


async def print_progress_callback(
    progress: float, total: float | None, message: str | None
):
    if total is not None:
        percentage = (progress / total) * 100
        print(f"Progress: {progress}/{total} ({percentage:.1f}%)")
    else:
        print(f"Progress: {progress}")

補足: - logging コールバックの引数は LoggingMessageNotificationParams 型。実データは params.data に入る。 - progress コールバックのシグネチャは (progress: float, total: float | None, message: str | None)total は None があり得るので分岐必須(この実物コードがそのまま雛形になる)。 - 表示方法は自由(CLI なら print、Web ならブラウザへの中継を別途設計)。表示しない選択も可。

ステップ4|Pass callbacks to appropriate functions(コールバックをそれぞれ正しい関数へ渡す)

: logging コールバックは ClientSession にprogress コールバックは call_tool() 関数に、それぞれ確実に渡すこと。

実物コード断片(client.py・run()):

async def run():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(
            read, write, logging_callback=logging_callback
        ) as session:
            await session.initialize()

            await session.call_tool(
                name="add",
                arguments={"a": 1, "b": 3},
                progress_callback=print_progress_callback,
            )

補足: 渡し先が非対称なのが最大の引っかけどころ—— - logging_callbackClientSession コンストラクタ(セッション全体で1つ。どのツール呼び出しのログもここに届く) - progress_callbackcall_tool() の引数(ツール呼び出し単位。呼び出しごとに別のコールバックを渡せる)

逆に渡すと通知は一切届かない。

実物コード全文

server.py

from mcp.server.fastmcp import FastMCP, Context
import asyncio

mcp = FastMCP(name="Demo Server")


@mcp.tool()
async def add(a: int, b: int, ctx: Context) -> int:
    await ctx.info("Preparing to add...")
    await ctx.report_progress(20, 100)

    await asyncio.sleep(2)

    await ctx.info("OK, adding...")
    await ctx.report_progress(80, 100)

    return a + b


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

client.py

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.types import LoggingMessageNotificationParams

server_params = StdioServerParameters(
    command="uv",
    args=["run", "server.py"],
)


async def logging_callback(params: LoggingMessageNotificationParams):
    print(params.data)


async def print_progress_callback(
    progress: float, total: float | None, message: str | None
):
    if total is not None:
        percentage = (progress / total) * 100
        print(f"Progress: {progress}/{total} ({percentage:.1f}%)")
    else:
        print(f"Progress: {progress}")


async def run():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(
            read, write, logging_callback=logging_callback
        ) as session:
            await session.initialize()

            await session.call_tool(
                name="add",
                arguments={"a": 1, "b": 3},
                progress_callback=print_progress_callback,
            )


if __name__ == "__main__":
    import asyncio

    asyncio.run(run())

実行方法(README.md より)

uv sync           # 依存インストール
uv run client.py  # クライアント起動(client が stdio 経由で server.py を子プロセス起動する)
  • トランスポートは STDIOStdioServerParameters(command="uv", args=["run", "server.py"]) により、クライアント実行だけでサーバーも立ち上がる。
  • 期待される出力:Preparing to add...Progress: 20.0/100.0 (20.0%) →(2秒待ち)→ OK, adding...Progress: 80.0/100.0 (80.0%)

英語原文(全ステップ)

  1. Tool function receives Context argument — Tool functions automatically receive 'Context' as their last argument. This object has methods for logging and reporting progress to the client.

  2. Create logs and progress with context — Throughout your tool function, call the info(), warning(), debug(), or error() methods to log different types of messages for the client. Also call the report_progress() method to estimate the amount of remaining work for the tool call.

  3. Define callbacks on the client — The client needs to define logging and progress callbacks, which will automatically be called whenever the server emits log or progress messages. These callbacks should try to display the provided logging and progress data to the user.

  4. Pass callbacks to appropriate functions — Make sure you provide the logging callback to the ClientSession and the progress callback to the call_tool() function.

↑ 目次へ戻る

MCP: Advanced Topics | Walkthrough「Roots walkthrough」(全文ナレッジ)

Skilljar ステップ送り式インタラクティブ教材(ID 295839・全7ステップ)/ 原文: transcripts/walkthroughs.md / 実物コード: assets/roots/(main.py・mcp_client.py・mcp_server.py・core/)/ 2026-07-03

題材プロジェクトの全体像(VidsMCP)

CLI チャットアプリ(Claude API 接続)+ 動画変換 MCP サーバー「VidsMCP」。ユーザーが CLI 引数で許可ディレクトリ(roots)を指定し、MCP サーバーはその roots 配下のファイル/フォルダにしかアクセスできない、という roots によるファイルシステム認可の実装例。

  • 起動:uv run main.py <root1> [root2] ...(例:uv run main.py /path/to/videos /another/path
  • サーバーのツール:convert_video(ffmpeg で MP4→mov/avi/webm/mkv/gif 変換)/list_roots(アクセス可能ディレクトリ一覧)/read_dir(root 内のディレクトリ読み取り)
  • roots の流れ:CLI 引数 → MCPClient が Root オブジェクト化 → サーバーが必要時に list_roots リクエスト → クライアントのコールバックが返答 → サーバー側 is_path_allowed で認可判定

日本語ナレッジ(ステップ別・完全版=訳+実物コード断片)

ステップ1: Defining roots(roots を定義する)

:理想的には、MCP サーバーがどのファイル/フォルダにアクセスできるかはユーザーが指示する。このプログラムは CLI 引数のリストを受け取るように作られており、それらは「ユーザーがアクセスを許可したいパス」として解釈される。そのパスのリストは、42行目あたりで MCPClient に渡される。

📄 実物コード(main.py 29〜44行目)——sys.argv[1:] が roots の供給源。引数なしなら usage を出して終了:

    # Get root directories from command line arguments
    root_paths = sys.argv[1:]
    if not root_paths:
        print("Usage: uv run main.py <root1> [root2] ...")
        print("Example: uv run main.py /path/to/videos /another/path")
        sys.exit(1)

    clients = {}

    async with AsyncExitStack() as stack:
        # Create the MCP client with the provided root directories
        doc_client = await stack.enter_async_context(
            MCPClient(
                command="uv", args=["run", "mcp_server.py"], roots=root_paths
            )
        )

ポイント:roots の決定権はサーバーでも Claude でもなくユーザー(クライアント側)にある。これが roots 機能の設計思想(ユーザーが許可した範囲だけをサーバーに開示する)。

ステップ2: Creating root objects(Root オブジェクトを作る)

:MCP 仕様によると、すべての root は file:// で始まる URI を持たなければならない。この関数は、ユーザーが指定したパスのリストを受け取り、Root オブジェクトに変換する。

📄 実物コード(mcp_client.py 29〜36行目)——Path.resolve() で絶対パス化 → FileUrlRoot(uri=..., name=...)

    def _create_roots(self, root_paths: list[str]) -> list[Root]:
        """Convert path strings to Root objects."""
        roots = []
        for path in root_paths:
            p = Path(path).resolve()
            file_url = FileUrl(f"file://{p}")
            roots.append(Root(uri=file_url, name=p.name or "Root"))
        return roots

ポイントRootFileUrlmcp.types / pydantic から(from mcp.types import Root, ListRootsResult, ErrorDatafrom pydantic import FileUrl)。p.name or "Root"/ のようにディレクトリ名が空になるケースのフォールバック。

ステップ3: Roots callback(roots コールバック)

:クライアントは roots のリストをすぐにはサーバーに渡さない。代わりに、サーバーが将来の任意の時点でクライアントにリクエストを送れる。サーバーが roots を要求したときに実行されるコールバックを作る。コールバックは roots のリストを ListRootsResult オブジェクトに入れて返す必要がある。このコールバックは 58行目で ClientSession に渡される。

📄 実物コード(mcp_client.py 38〜42行目=コールバック本体、44〜63行目=ClientSession への接続):

    async def _handle_list_roots(
        self, context: RequestContext["ClientSession", None]
    ) -> ListRootsResult | ErrorData:
        """Callback for when server requests roots."""
        return ListRootsResult(roots=self._roots)
    async def connect(self):
        server_params = StdioServerParameters(
            command=self._command,
            args=self._args,
            env=self._env,
        )
        stdio_transport = await self._exit_stack.enter_async_context(
            stdio_client(server_params)
        )
        _stdio, _write = stdio_transport
        self._session = await self._exit_stack.enter_async_context(
            ClientSession(
                _stdio,
                _write,
                list_roots_callback=self._handle_list_roots
                if self._roots
                else None,
            )
        )
        await self._session.initialize()

ポイント:roots は push ではなく pull(サーバー起点の List Roots Request)。これは STDIO/HTTP トランスポート回(Lesson 05・06)で出てくる「サーバー→クライアント方向のリクエスト(ServerRequest)」の代表例そのもの。roots 未指定時は list_roots_callback=None(capability を宣言しない)。

ステップ4: Using the roots(roots を使う)

:ここからサーバー側。サーバーが roots を使う場面は2つ: 1. ツールがファイル/フォルダにアクセスしようとするとき 2. LLM(Claude など)がファイル/フォルダをフルパスに解決する必要があるとき。ユーザーが「todos.txt を読んで」と言った場面を考えると、Claude はそのテキストファイルがどこにあるかを把握する必要があり、roots のリストを見てそれを行うかもしれない。

2つ目のケースに対応するには、roots を列挙するツールを定義するか、プロンプトに直接注入するかのどちらかでよい。

📄 実物コード(mcp_server.py 49〜58行目)——このリポジトリは「roots を列挙するツール」方式を採用:

@mcp.tool()
async def list_roots(ctx: Context):
    """
    List all directories that are accessible to this server.
    These are the root directories where files can be read from or written to.
    """
    roots_result = await ctx.session.list_roots()
    client_roots = roots_result.roots

    return [file_url_to_path(root.uri) for root in client_roots]

ステップ5: Accessing the roots(roots へアクセスする)

:roots へは ctx.session.list_roots() を呼んでアクセスする。これはクライアントへメッセージを送り返し、クライアント側で root 列挙コールバックが実行される。

ポイント:ツール関数の ctx: Context 引数(FastMCP が自動注入)経由で ctx.session.list_roots() → 裏側では List Roots Request がサーバー→クライアントに飛び、ステップ3のコールバックが ListRootsResult を返す。roots はサーバー側にキャッシュされず、毎回クライアントに問い合わせるのがこの実装の形。

ステップ6: Authorizing access(アクセスを認可する)

:忘れないこと——MCP SDK は、ツールが読もうとするファイル/フォルダを一切制限しようとしない! そのチェックは自分で実装しなければならないis_path_allowed のような関数の実装を検討すること。これは、要求されたパスを roots のリストと比較して、アクセス可能かどうかを判定する。

📄 実物コード(mcp_server.py 11〜29行目)——本ウォークスルーの核心・認可パターン全文:

async def is_path_allowed(requested_path: Path, ctx: Context) -> bool:
    roots_result = await ctx.session.list_roots()
    client_roots = roots_result.roots

    if not requested_path.exists():
        return False

    if requested_path.is_file():
        requested_path = requested_path.parent

    for root in client_roots:
        root_path = file_url_to_path(root.uri)
        try:
            requested_path.relative_to(root_path)
            return True
        except ValueError:
            continue

    return False

パターン分解: 1. ctx.session.list_roots()その都度最新の roots を取得 2. 存在しないパスは即 False(存在チェックが認可の前提) 3. ファイルなら親ディレクトリに置き換えて判定 4. 各 root に対して Path.relative_to(root_path) を試す——文字列 prefix 比較ではなく pathlib の包含判定を使う(/root-evil/root にマッチする類の prefix バグを回避)。ValueError = その root の配下ではない → 次の root へ 5. どの root にも含まれなければ False(デフォルト拒否)

file_url_to_path(core/utils.py・12行)は file:// URI → Path 変換(urlparseunquote、Windows のドライブレター /C: 先頭スラッシュ除去も処理):

def file_url_to_path(file_url) -> Path:
    """Convert a file:// URL to a Path object."""
    url_str = str(file_url)
    parsed = urlparse(url_str)
    path = unquote(parsed.path)
    if len(path) > 2 and path[0] == "/" and path[2] == ":":
        path = path[1:]

    return Path(path)

⚠️ 実務メモ(ウォークスルー本文外・コード精読からの注意点):この教材実装では read_dirPath(path).resolve() してから判定するが、convert_videoVideoConverter.validate_input() が返す 未 resolve の Path をそのまま判定に渡す。symlink や相対パスの正規化を認可前に必ず行う(resolve() してから is_path_allowed)のが、実務ではより堅い形。教材の主眼は「SDK は守ってくれない・自分で認可関数を書く」という原則。

ステップ7: Authorizing access (2)(認可関数をツール全体に敷く)

is_path_allowed のような認可関数を組み上げたら、すべてのツールでそれを使い、要求されたパスがアクセス可能であることを確認すること。

📄 実物コード(mcp_server.py 32〜46行目・61〜73行目)——ファイルを触る2ツール両方の入口でガード:

@mcp.tool()
async def convert_video(
    input_path: str = Field(description="Path to the input MP4 file"),
    format: str = Field(description="Output format (e.g. 'mov')"),
    *,
    ctx: Context,
):
    """Convert an MP4 video file to another format using ffmpeg"""
    input_file = VideoConverter.validate_input(input_path)

    # Ensure the input file is contained in a root
    if not await is_path_allowed(input_file, ctx):
        raise ValueError(f"Access to path is not allowed: {input_path}")

    return await VideoConverter.convert(input_path, format)
@mcp.tool()
async def read_dir(
    path: str = Field(description="Path to a directory to read"),
    *,
    ctx: Context,
):
    """Read directory contents. Path must be within one of the client's roots."""
    requested_path = Path(path).resolve()

    if not await is_path_allowed(requested_path, ctx):
        raise ValueError("Error: can only read directories within a root")

    return [entry.name for entry in requested_path.iterdir()]

ポイント:拒否は ValueError 送出(MCP がツールエラーとして Claude に返し、Claude が自己修正できる)。「認可 → 実処理」の順序を全ツールで統一するのがこのステップの教え。

実物コード全文(要所+ファイル構成)

ファイル構成(assets/roots/)

roots/
├── main.py            # エントリポイント:CLI引数→roots→MCPClient→CliChat 起動(62行)
├── mcp_client.py      # MCPClient:stdio接続・roots生成・list_rootsコールバック(109行)
├── mcp_server.py      # VidsMCP サーバー:is_path_allowed+3ツール(78行)
├── core/
│   ├── chat.py            # チャットループ基盤(57行)
│   ├── claude.py          # Anthropic API ラッパ(105行)
│   ├── cli_chat.py        # CLI用チャット(84行)
│   ├── cli.py             # CLIアプリ(113行)
│   ├── tools.py           # ツール実行仲介(108行)
│   ├── utils.py           # file_url_to_path(12行・全文は上掲)
│   └── video_converter.py # ffmpeg 変換ロジック(96行・要所は下記)
├── pyproject.toml     # deps: anthropic>=0.51.0, mcp[cli]>=1.8.0, prompt-toolkit, pyboxen, python-dotenv
├── .env.example       # CLAUDE_MODEL="claude-sonnet-4-0" / ANTHROPIC_API_KEY=""
└── README.md          # セットアップ(uv sync)・FFmpeg 必須・roots 指定起動

mcp_server.py(全文・78行)

from pathlib import Path
from mcp.server.fastmcp import FastMCP
from pydantic import Field
from mcp.server.fastmcp import Context
from core.video_converter import VideoConverter
from core.utils import file_url_to_path

mcp = FastMCP("VidsMCP", log_level="ERROR")


async def is_path_allowed(requested_path: Path, ctx: Context) -> bool:
    roots_result = await ctx.session.list_roots()
    client_roots = roots_result.roots

    if not requested_path.exists():
        return False

    if requested_path.is_file():
        requested_path = requested_path.parent

    for root in client_roots:
        root_path = file_url_to_path(root.uri)
        try:
            requested_path.relative_to(root_path)
            return True
        except ValueError:
            continue

    return False


@mcp.tool()
async def convert_video(
    input_path: str = Field(description="Path to the input MP4 file"),
    format: str = Field(description="Output format (e.g. 'mov')"),
    *,
    ctx: Context,
):
    """Convert an MP4 video file to another format using ffmpeg"""
    input_file = VideoConverter.validate_input(input_path)

    # Ensure the input file is contained in a root
    if not await is_path_allowed(input_file, ctx):
        raise ValueError(f"Access to path is not allowed: {input_path}")

    return await VideoConverter.convert(input_path, format)


@mcp.tool()
async def list_roots(ctx: Context):
    """
    List all directories that are accessible to this server.
    These are the root directories where files can be read from or written to.
    """
    roots_result = await ctx.session.list_roots()
    client_roots = roots_result.roots

    return [file_url_to_path(root.uri) for root in client_roots]


@mcp.tool()
async def read_dir(
    path: str = Field(description="Path to a directory to read"),
    *,
    ctx: Context,
):
    """Read directory contents. Path must be within one of the client's roots."""
    requested_path = Path(path).resolve()

    if not await is_path_allowed(requested_path, ctx):
        raise ValueError("Error: can only read directories within a root")

    return [entry.name for entry in requested_path.iterdir()]


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

mcp_client.py(全文・109行)

from typing import Optional, Any
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client
from mcp.types import Root, ListRootsResult, ErrorData
from mcp.shared.context import RequestContext
from pathlib import Path
from pydantic import FileUrl

import json
from pydantic import AnyUrl


class MCPClient:
    def __init__(
        self,
        command: str,
        args: list[str],
        env: Optional[dict] = None,
        roots: Optional[list[str]] = None,
    ):
        self._command = command
        self._args = args
        self._env = env
        self._roots = self._create_roots(roots) if roots else []
        self._session: Optional[ClientSession] = None
        self._exit_stack: AsyncExitStack = AsyncExitStack()

    def _create_roots(self, root_paths: list[str]) -> list[Root]:
        """Convert path strings to Root objects."""
        roots = []
        for path in root_paths:
            p = Path(path).resolve()
            file_url = FileUrl(f"file://{p}")
            roots.append(Root(uri=file_url, name=p.name or "Root"))
        return roots

    async def _handle_list_roots(
        self, context: RequestContext["ClientSession", None]
    ) -> ListRootsResult | ErrorData:
        """Callback for when server requests roots."""
        return ListRootsResult(roots=self._roots)

    async def connect(self):
        server_params = StdioServerParameters(
            command=self._command,
            args=self._args,
            env=self._env,
        )
        stdio_transport = await self._exit_stack.enter_async_context(
            stdio_client(server_params)
        )
        _stdio, _write = stdio_transport
        self._session = await self._exit_stack.enter_async_context(
            ClientSession(
                _stdio,
                _write,
                list_roots_callback=self._handle_list_roots
                if self._roots
                else None,
            )
        )
        await self._session.initialize()

    def session(self) -> ClientSession:
        if self._session is None:
            raise ConnectionError(
                "Client session not initialized or cache not populated. Call connect_to_server first."
            )
        return self._session

    async def list_tools(self) -> list[types.Tool]:
        result = await self.session().list_tools()
        return result.tools

    async def call_tool(
        self, tool_name: str, tool_input
    ) -> types.CallToolResult | None:
        return await self.session().call_tool(tool_name, tool_input)

    async def list_prompts(self) -> list[types.Prompt]:
        result = await self.session().list_prompts()
        return result.prompts

    async def get_prompt(self, prompt_name, args: dict[str, str]):
        result = await self.session().get_prompt(prompt_name, args)
        return result.messages

    async def read_resource(self, uri: str) -> Any:
        result = await self.session().read_resource(AnyUrl(uri))
        resource = result.contents[0]

        if isinstance(resource, types.TextResourceContents):
            if resource.mimeType == "application/json":
                return json.loads(resource.text)

            return resource.text

    async def cleanup(self):
        await self._exit_stack.aclose()
        self._session = None

    async def __aenter__(self):
        await self.connect()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.cleanup()

main.py(全文・62行)

import asyncio
import sys
import os
from dotenv import load_dotenv
from contextlib import AsyncExitStack

from mcp_client import MCPClient
from core.claude import Claude

from core.cli_chat import CliChat
from core.cli import CliApp

load_dotenv()

# Anthropic Config
claude_model = os.getenv("CLAUDE_MODEL", "claude-sonnet-4-0")
anthropic_api_key = os.getenv("ANTHROPIC_API_KEY", "")


assert claude_model, "Error: CLAUDE_MODEL cannot be empty. Update .env"
assert anthropic_api_key, (
    "Error: ANTHROPIC_API_KEY cannot be empty. Update .env"
)


async def main():
    claude_service = Claude(model=claude_model)

    # Get root directories from command line arguments
    root_paths = sys.argv[1:]
    if not root_paths:
        print("Usage: uv run main.py <root1> [root2] ...")
        print("Example: uv run main.py /path/to/videos /another/path")
        sys.exit(1)

    clients = {}

    async with AsyncExitStack() as stack:
        # Create the MCP client with the provided root directories
        doc_client = await stack.enter_async_context(
            MCPClient(
                command="uv", args=["run", "mcp_server.py"], roots=root_paths
            )
        )
        clients["doc_client"] = doc_client

        chat = CliChat(
            doc_client=doc_client,
            clients=clients,
            claude_service=claude_service,
        )

        cli = CliApp(chat)
        await cli.initialize()
        await cli.run()


if __name__ == "__main__":
    if sys.platform == "win32":
        asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
    asyncio.run(main())

core/video_converter.py(要所のみ)

  • SUPPORTED_FORMATS = ["webm", "mkv", "avi", "mov", "gif"]、品質プリセット(low/medium/high の crf/preset、変換は medium 固定)
  • validate_input(input_path): 存在チェック+.mp4 拡張子チェック → Path を返す(不正は ValueError
  • generate_output_path: 拡張子置換で出力パス生成
  • build_ffmpeg_command: GIF は fps=15,scale=480:-1:flags=lanczos、動画は libx264 + aac 128k
  • convert: asyncio.create_subprocess_exec で ffmpeg を非同期実行、returncode != 0RuntimeError、ffmpeg 不在は FileNotFoundErrorRuntimeError("FFmpeg not found...")

英語原文(全ステップ)

  1. Defining roots — Ideally, a user will dictate which files/folders can be accessed by the MCP server. This program is set up to accept a list of CLI arguments, which are interpretted as paths that the user wants to allow access to. That list of paths is provided to the MCPClient down on lines 42.
  2. Creating root objects — According to the MCP spec, all roots should have a URI that begins with file://. This function takes the list of paths of that the user provided and turns them into Root objects.
  3. Roots callback — The client doesn't immediately provide the list of roots to the server. Instead, the server can make a request to the client at some future point in time. We make a callback that will be executed when the server requests the roots. The callback needs to return the list of roots inside of a ListRootsResult object. This callback is passed into the ClientSession down on line 58.
  4. Using the roots — On to the server. The server will use the roots in two scenarios: Whenever a tool attempts to access a file or folder / When a LLM (like Claude) needs to resolve a file or folder to a full path. Think of when a user says 'read the todos.txt file' - Claude needs to figure out where the text file is, and might do so by looking at the list of roots. To handle the second case, we can either define a tool that lists out the roots or inject them directly in a prompt.
  5. Accessing the roots — Roots are accessed by calling ctx.session.list_roots(). This sends a message back to the client, which causes it to run the root-listing callback.
  6. Authorizing access — Remember: the MCP SDK does not attempt to limit what files or folders your tools attempt to read! You must implement that check yourself. Consider implementing a function like is_path_allowed, which will decide whether a path is accessible by comparing it to the list of roots.
  7. Authorizing access (2) — Once you've put an authorization function together - like is_path_allowed - use it throughout your tools to ensure the requested path is accessible.

(注:原文ステップ1の「lines 42」、ステップ3の「line 58」は実物コードの行番号と一致——main.py 42行目=command="uv", args=["run", "mcp_server.py"], roots=root_paths、mcp_client.py 58行目=list_roots_callback=self._handle_list_roots。原文の「interpretted」「the list of paths of that the user provided」は原文ママ。)

↑ 目次へ戻る