【Streamlit】作って理解するMCP

Streamlit
この記事は約17分で読めます。

こんにちは、JS2IIUです。
最近よく耳にするMCP、すでにさまざまなサービスが提供され始めています。MCPについて理解を深めるため、Streamlitと組み合わせて実装してみたいと思います。今回もよろしくお願いします。

1. はじめに

近年、ChatGPTやClaudeなどの大規模言語モデル(LLM)が驚くべきスピードで進化しています。しかし、どれだけ言語生成が賢くなっても、それだけでは実用的なアプリケーションにはなりません。モデルが社内データにアクセスしたり、タスク管理ツールに書き込んだりと、外部ツールと安全かつ柔軟に接続できる仕組みが必要になります。

そこで登場するのが MCP(Model Context Protocol) です。

本記事では、MCPの基本概念を丁寧に解説しつつ、実際にMCPサーバーを構築してStreamlitでUIから呼び出す体験を通じて、プロトコルの仕組みを深く理解していきます。

MCPについてはこちらの関連記事でも詳しく説明しています。

2. MCPとは何か?仕組みをやさしく解説

2.1 MCPの定義と役割

MCP(Model Context Protocol) とは、LLMと外部ツールやデータソースを安全・汎用的に接続するためのプロトコルです。OpenAIが提唱するこのプロトコルは、従来のFunction Callingやプラグインの複雑さを抽象化し、より構造化された方法で外部機能とやり取りできるように設計されています。

  • LLMは「MCPクライアント」を通じて「MCPサーバー」にリクエストを送信。
  • MCPサーバーは関数やデータを提供する。
  • すべてのやりとりはJSONベースの構造化された通信で行われる。

2.2 MCPが使われるシナリオ例

シナリオ説明
Googleカレンダー操作LLMが予定を取得・登録
Notion連携 LLMがページに追記・検索
自社DB接続顧客情報や販売履歴を照会
ローカルスクリプト呼び出しユーザー環境のスクリプト実行

3. MCPの3つの要素とは?

MCP(Model Context Protocol)は、大規模言語モデル(LLM)が外部リソース(ツール、API、データベースなど)と安全かつ一貫性のある方法で接続できるように設計された通信プロトコルです。

このプロトコルは、次の3つのコンポーネントから構成されており、それぞれが異なる責任範囲を担っています。これらの構成要素の役割と関係性を正しく理解することで、MCPベースのアーキテクチャを効果的に設計・実装できるようになります。

3.1 MCPホスト(Model Host)

  • 役割:LLMアプリケーションの「本体」であり、ユーザーの入力を受け取って解釈し、必要に応じて外部リソースへのアクセスを指示する中枢です。
  • :ChatGPT、Claude、Anthropic Consoleなど
  • 主な機能
    • ユーザーからの自然言語プロンプトを解析し、意図された操作(ツールの呼び出し)を判断
    • MCPクライアントの生成・管理(リクエストごとにクライアントを起動する場合もあれば、長期接続で維持する設計も可能)
    • 出力結果をユーザーに返す

技術的には、ホストはLLMが生成した中間表現(例:ToolCall)をMCPクライアントに転送し、関数の呼び出し処理を委譲します。

3.2 MCPクライアント(Model Client)

  • 役割:MCPホストとMCPサーバーの仲介役となるプロセス
  • 構造:1つのMCPクライアントは、1つのMCPサーバーと1対1で接続し、セッション単位で動作します
  • 主な機能
    • ホストから受け取ったツール呼び出しリクエスト(関数名・引数)をMCPサーバーにHTTPリクエストとして転送
    • MCPサーバーからのレスポンスを受け取り、ホストに返却
    • 接続のライフサイクル(起動・再接続・シャットダウン)管理

MCPクライアントは通常、OpenAIが提供するインフラ上で動作しますが、開発者が自前でクライアントを模倣・テストすることも可能です。HTTPベースの単純なプロトコルであるため、Postmanやcurlなどを使った手動テストもできます。

3.3 MCPサーバー(Tool Server)

  • 役割実際の処理ロジックを実装するエンドポイント群で、外部の業務ロジック、ツール、データソースへのアクセスを提供します
  • 実装形態:RESTfulなWebサービス(FastAPI, Flask, Express.jsなどで実装可能)
  • 主な機能
    • ユーザーから指定された関数名と入力引数を受け取り、適切な処理を実行
    • 実行結果をMCPクライアントに返却(JSON形式)
    • 処理単位でログ記録やセキュリティ制御を実装可能

MCP仕様上、サーバーは以下の2つのエンドポイントを必須で実装しなければなりません:

エンドポイント概要
GET /healthサーバーが正常に動作しているかを返すステータス確認用API
POST /call呼び出したい関数名と引数を含むリクエストを受け取り、処理結果を返す主エンドポイント

技術的には、/call は関数名(例:greet, search_customer, add_task)と引数(inputs)を含むJSONを受け取り、同様にJSON形式でoutputsを返す必要があります。MCPホストはこの形式に従ってのみツールとの通信を行うため、仕様に準拠して実装することが重要です。

図で理解する:MCP全体の構成イメージ

このように、MCPはLLMと外部ツールを安全・構造的に連携させるための明確な分離構造を提供します。どこにどの処理責任があるのかを明確にすることで、セキュリティ、スケーラビリティ、保守性が高まります。

次章では、これらの構成のうち「MCPサーバー」を実際に実装し、その動作を確認してみましょう。

4. MCPサーバーを作ってみよう(FastAPI + Streamlit)

ここからは、MCPサーバーの実装を実際に体験しながら学んでいきましょう。MCPサーバーは、MCPクライアントからのリクエストを受け取り、関数を呼び出してその結果を返すREST APIです。

今回は、Python製の高速なWebフレームワークである FastAPI を使ってMCPサーバーを実装し、Streamlitを使ってUIからAPIを操作できるようにします。

4.1 開発環境の準備

MCPサーバーの構築には、以下のPythonライブラリを使用します。

ライブラリ用途
fastapiWeb API の実装
uvicornASGIサーバー。FastAPIを起動するために必要
pydanticリクエスト/レスポンスのデータバリデーション
streamlitユーザーインターフェース用(後半で使用)

仮想環境を作成(推奨)

Pythonの仮想環境を作って、プロジェクトごとに依存関係を分離しましょう。

Bash
python -m venv .venv
source .venv/bin/activate  # Windows の場合: .venv\Scripts\activate

ライブラリをインストール

Bash
pip install fastapi uvicorn streamlit pydantic

4.2 MCPサーバーの基本実装

FastAPIでMCPサーバーを作るには、最低限2つのエンドポイントを実装する必要があります。

  • GET /health:MCPホストがサーバーの正常性を確認するためのチェック用
  • POST /call:ツール(関数)を呼び出すためのメイン処理

4.2.1 /health エンドポイントの実装

このエンドポイントは、MCPホストがサーバーと接続できるかどうかを確認するために利用されます。とてもシンプルな構造で、サーバーが稼働中であることを返します。

Python
# app.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health_check():
    return {"status": "ok"}
解説
  • FastAPI():FastAPIアプリケーションのインスタンスを作成します。
  • @app.get("/health")GET /health に対応するエンドポイントを定義。
  • 関数 health_check は辞書型(JSON)で "status": "ok" を返します。

実運用では、ヘルスチェックにCPU使用率やDB接続状態を加えることもありますが、MCP準拠のサーバーではこの簡単な形式で十分です。

4.2.2 /call エンドポイントの実装

このエンドポイントは、MCPクライアントから送信される「関数名」と「引数」を受け取り、指定された処理を実行し、出力結果をJSON形式で返す役割を担います。

Python
from pydantic import BaseModel
from typing import Dict

class CallRequest(BaseModel):
    function: str
    inputs: Dict[str, str]

class CallResponse(BaseModel):
    outputs: Dict[str, str]

@app.post("/call", response_model=CallResponse)
def call(req: CallRequest):
    if req.function == "greet":
        name = req.inputs.get("name", "world")
        return {"outputs": {"message": f"Hello, {name}!"}}
    else:
        return {"outputs": {"message": "Unknown function"}}
解説
データモデルの定義(Pydantic)
Python
class CallRequest(BaseModel):
    function: str
    inputs: Dict[str, str]
  • function:呼び出したい関数名(文字列)
  • inputs:関数に渡される引数を格納した辞書(key-value)
Python
class CallResponse(BaseModel):
    outputs: Dict[str, str]
  • outputs:関数実行後に返す結果(辞書形式)

MCP仕様では、リクエストは必ず functioninputs を持ち、レスポンスは必ず outputs を持つ必要があります。

エンドポイントのロジック
Python
@app.post("/call", response_model=CallResponse)
def call(req: CallRequest):
  • @app.post("/call"):HTTPのPOSTリクエストを /call に送るとこの関数が実行されます。
  • response_model=CallResponse:出力JSONをPydanticモデルでバリデーション・整形します。
Python
if req.function == "greet":
    name = req.inputs.get("name", "world")
    return {"outputs": {"message": f"Hello, {name}!"}}
  • function == "greet" のとき、inputs から name を取得し、挨拶メッセージを返します。
  • inputs.get("name", "world"):指定がなければデフォルトで “world” を使います。
Python
else:
    return {"outputs": {"message": "Unknown function"}}
  • 未定義の関数名が来た場合は、”Unknown function” というメッセージで応答します。

動作確認例(curl)

以下のコマンドを使って、/call エンドポイントをテストできます。

Bash
curl -X POST http://localhost:8000/call \
  -H "Content-Type: application/json" \
  -d '{"function": "greet", "inputs": {"name": "Makoto"}}'

期待されるレスポンス:

Plaintext
{
  "outputs": {
    "message": "Hello, Makoto!"
  }
}

このように、FastAPIを使えば、MCP準拠のサーバーを非常に少ないコード量で実装することができます。次のセクションでは、このMCPサーバーにStreamlitから接続して、UIを使って動かしてみます。

5. StreamlitからMCPサーバーを呼び出してみよう

MCPクライアントのような役割を果たすUIをStreamlitで作成し、サーバーにデータを送ってみましょう。

5.1 Streamlitアプリのコード

Python
# client.py

import streamlit as st
import requests

st.title("MCP クライアント シミュレーター")

name = st.text_input("名前を入力してください")
if st.button("送信"):
    res = requests.post("http://localhost:8000/call", json={
        "function": "greet",
        "inputs": {"name": name}
    })
    if res.status_code == 200:
        st.success(res.json()["outputs"]["message"])
    else:
        st.error("エラーが発生しました")

5.2 サーバーの起動コマンド

Bash
uvicorn app:app --reload

別ターミナルで以下を実行してUIを表示:

Bash
streamlit run client.py

6. 発展:複数の関数をMCPサーバーに追加してみよう

MCPは「複数の関数」に対応できます。たとえば「add」関数を追加してみましょう。

6.1 足し算関数の追加

Python
def add(x: int, y: int) -> int:
    return x + y

@app.post("/call", response_model=CallResponse)
def call(req: CallRequest):
    if req.function == "greet":
        name = req.inputs.get("name", "world")
        return {"outputs": {"message": f"Hello, {name}!"}}
    elif req.function == "add":
        try:
            x = int(req.inputs.get("x", "0"))
            y = int(req.inputs.get("y", "0"))
            return {"outputs": {"result": str(add(x, y))}}
        except Exception as e:
            return {"outputs": {"error": str(e)}}
    else:
        return {"outputs": {"message": "Unknown function"}}

Streamlit 側も以下のように修正することで、複数関数を選択可能にできます。

Python
function = st.selectbox("呼び出す関数", ["greet", "add"])

if function == "add":
    x = st.number_input("x", value=0)
    y = st.number_input("y", value=0)

inputs = {"name": name} if function == "greet" else {"x": str(x), "y": str(y)}

if st.button("送信"):
    res = requests.post("http://localhost:8000/call", json={
        "function": function,
        "inputs": inputs
    })
    ...

7. 参考リンク・ドキュメント

最後まで読んでいただきありがとうございます。
ご意見ご感想はコメント欄にお願い致します。

コメント

タイトルとURLをコピーしました