MCPサーバーの構築チュートリアル:Perplexityで検索できるようにしてみる

このチュートリアルでは、MCPサーバーの基本的な構造、Perplexity検索APIの使用方法、およびそれらを統合する方法を解説します。

1. MCPの概要と基本概念

MCP(Model Context Protocol)は、AIアシスタントと外部のデータソースやツールを接続するためのオープン標準プロトコルです (Introducing the Model Context Protocol \ Anthropic)。Anthropic社によって提唱され、LLM(大規模言語モデル)のアプリケーション(例: Claudeなど)から外部システムに安全に双方向通信できるよう設計されています (Introducing the Model Context Protocol \ Anthropic)。MCPではクライアント‐サーバー方式を採用しており、AI側がMCPクライアント(ホスト)、データソース側がMCPサーバーとして動作します (Understanding the Model Context Protocol (MCP)) (Understanding the Model Context Protocol (MCP))。サーバーは提供する機能(「ツール」や「プロンプト」と呼ばれます)を定義し、クライアントはそれを呼び出して必要な情報を取得します。

通信はJSON-RPC 2.0形式のメッセージで行われ、2つのモードがあります (Understanding the Model Context Protocol (MCP)): 開発環境などでサーバーを直接実行する場合は標準入出力(STDIO)による通信、リモート接続の場合はHTTP (Server-Sent Events, SSE)による通信です (Understanding the Model Context Protocol (MCP))。本チュートリアルではシンプルなSTDIO方式でサーバーを実装し、MCPクライアント(AIモデル)から検索クエリを受け取り、外部のWeb検索サービスであるPerplexity APIを使って結果を取得・返答する流れを解説します。

2. PythonでのMCPサーバーのセットアップ

必要なライブラリと環境構築

まずPython 3系の環境を用意してください。追加のフレームワークは使用せず、標準ライブラリと最小限のパッケージで実装します。外部APIにリクエストを送るためにPythonのHTTPクライアントライブラリであるrequestsを使用します。事前に以下のコマンドでインストールしておきましょう:

pip install requests

また、Perplexity APIを利用するにはAPIキーが必要です (perplexity-mcp/README.md at main · jsonallen/perplexity-mcp · GitHub)。取得したAPIキーはコード中に直接書かず、環境変数に設定することを推奨します。本チュートリアルでは環境変数PERPLEXITY_API_KEYにキーを保存し、Pythonコード内で参照する方針とします。 (perplexity-mcp/README.md at main · jsonallen/perplexity-mcp · GitHub)

基本的なMCPサーバーの作成

純粋なPythonでMCPサーバーを実装するため、標準入出力を用いたループ処理を構築します。MCPクライアント(例えばClaudeデスクトップアプリ)からはJSON-RPC形式のリクエストが標準入力経由で送られてくるため、サーバー側ではそれを読み取って処理し、結果をJSON形式で標準出力に書き出します。リクエストJSONは一般に次のような構造です:

{
  "id": 1,
  "method": "perplexity_search_web",
  "params": {
    "query": "検索したい内容",
    "recency": "week"
  }
}

各フィールドの意味は、idがリクエスト識別子、methodが呼び出し要求されたツール名、paramsにそのツールへの引数(今回の場合は検索クエリやオプション指定)です。サーバーはこの内容を解析し、対応する処理を行います。まずは基本構造として、無限ループでsys.stdinからの入力行を読み取り、JSONデコードしてメソッド名を取り出すコードを用意します。例えば以下のようになります:

import sys, json

for line in sys.stdin:
    data = json.loads(line)
    method = data.get("method")
    if method == "perplexity_search_web":
        # ここで後述の検索処理を行う
        pass
    else:
        # 定義していないメソッドへの対処(必要に応じて)
        continue

この段階では、perplexity_search_webというメソッド(=ツール呼び出し)の場合に後述の検索処理を行うための枠組みを作りました。未知のメソッドは無視または適宜エラーレスポンスを返す実装も考えられますが、基本チュートリアルでは割愛します。

このサーバーは永続的に標準入力を監視し、1行ごとにJSONをパースして処理します。perplexity_search_webは本チュートリアルで扱うWeb検索ツールの名前で、クライアント(AIモデル側)からこの名前で呼び出されることを想定しています。

3. Perplexity検索APIの利用方法

APIキーの取得

Perplexity(https://www.perplexity.ai)の提供する検索APIを利用するには、有効なAPIキーが必要です (perplexity-mcp/README.md at main · jsonallen/perplexity-mcp · GitHub)。まずPerplexityのアカウントを作成し、APIアクセス用のキーを発行します。Perplexityの設定画面で「API」タブを開き、“Generate API Key”ボタンをクリックすると、新しいAPIキーを生成できます (What is the API? | Perplexity Help Center)。生成されたキーをコピーし、自分の環境変数に保存してください(例: Linux/Macでは export PERPLEXITY_API_KEY="発行されたキー" を~/.bashrcなどに追記)。

💡 メモ: Perplexity APIを初めて利用する場合、キー発行時にクレジットカード情報の登録が必要になることがありま (Initial Setup - Perplexity)】。少額の無料クレジット枠が提供される場合もありますが、利用量に応じて課金される点に注意してください。

APIキーを取得・設定したら、プログラムから環境変数経由で読み込みます。Pythonではos.environを使って次のように取得できます:

import os
API_KEY = os.environ.get("PERPLEXITY_API_KEY")

このAPI_KEYを後ほどPerplexity API呼び出しの認証に利用します。

検索クエリの送信と結果の取得

PerplexityのAPIは、OpenAIのChat Completion APIに似た形式で質問を投げかけると、AIがウェブ検索を行った上で回答を返してくれる仕組みです。エンドポイントは https://api.perplexity.ai/chat/completions で、リクエストはHTTPのPOSTメソッドで行いま (Initial Setup - Perplexity)】。認証のため、先ほど取得したAPIキーをBearerトークンとしてHTTPヘッダーに含めま (Initial Setup - Perplexity)】。具体的には、ヘッダーAuthorizationの値を "Bearer <APIキー>" とします。また、Content-Type: application/jsonヘッダーも指定してJSONペイロードを送ります。

リクエストボディ(JSONペイロード)の構造はOpenAIのAPIと同様で、モデル名メッセージ一覧を含む辞書となります。例えば以下のようなPythonコードでリクエストデータを準備できます:

import requests, json

url = "https://api.perplexity.ai/chat/completions"
headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}
payload = {
    "model": "sonar-pro",  # 利用するモデル(Perplexityの推奨モデルを指定)
    "messages": [
        {"role": "user", "content": "OpenAIの最新ニュースを教えて"}
    ]
}
response = requests.post(url, headers=headers, json=payload)

上記のpayloadでは、modelキーにモデル名(例: "sonar-pro")を指定し、messagesキーには会話の履歴をリストで渡します。Perplexityが提供するモデルにはいくつか種類がありますが、ここでは汎用的なウェブ検索対応モデルとしてドキュメントで紹介されている「sonar-pro」を使用していま (Initial Setup - Perplexity)】。messagesリストには、ロールと内容からなるメッセージを順番に含めます。最初にシステムロールで振る舞いを指示することもできますが(例では省略しています)、基本的にはユーザーロール("role": "user", "content": "<質問文>")に我々の検索クエリ文字列を入れます。

requests.postでAPIを呼び出すと、responseに結果が返ってきます。成功した場合、response.status_code == 200となり、response.json()でJSON形式の応答データを取得できます。Perplexity APIのレスポンスはOpenAIのChat APIと互換性があり、choicesという配列の中にモデルの回答メッセージが含まれています。例えば、以下のようにして回答テキストを取り出せます:

data = response.json()
answer_text = data["choices"][0]["message"]["content"]
print(answer_text)

answer_textには、Perplexityによって生成された回答文が入ります。Perplexityは質問に対する回答を文章として返し、その中に情報源への参照(引用リンク)が含まれることが多いです。例えば「... according to [1]」のような形で文献番号が挿入され、回答の末尾に対応するURLが提示されます。

4. MCPサーバーとPerplexity検索の統合

MCPリクエストを処理し、Perplexity検索を実行

それでは、MCPサーバーの処理ループにPerplexity検索API呼び出しを組み込みましょう。手順としては、前節で準備した検索クエリ送信コードを、MCPリクエストを受け取った箇所に挿入する形になります。具体的には、method == "perplexity_search_web"と判定したブロック内で次の処理を行います:

  1. リクエストのparamsから検索クエリ文字列を取得します(例: query = data["params"]["query"])。

  2. paramsにオプションのrecency(期間指定)が含まれていれば取り出します(例: recency = data["params"].get("recency"))。この値が例えば "week" であれば、「過去一週間で」のように検索期間を限定したいことを意味します。Perplexity API自体には期間フィルタのパラメータはありませんが、クエリ文に付加情報として含めることでモデルに意図を伝えられます。そこで、recencyが指定された場合はクエリ文字列の末尾に " in the past week" のような英語フレーズを追加します(指定値ごとにday -> past 24 hours、month -> past 30 daysなど適宜設定)。

  3. 用意した検索クエリをPerplexity APIに投げます。ここで、前節と同様にrequestsを使い、AuthorizationヘッダーにBearerトークンとしてAPIキーを付与し、modelとmessagesを含むJSONをPOSTします。

  4. レスポンスが返ってきたら、JSONから回答テキストを抽出します。万一リクエストが失敗した場合(例: ネットワーク障害や認証エラー)には例外が発生する可能性があるため、try...exceptで囲み、エラー時は適切なメッセージを設定します。

  5. 最後に、もともとのリクエストのidと取得した回答テキストを含むJSONレスポンスを組み立て、標準出力に書き出します。書き出し後はflush()してバッファを送り出し、クライアント側がすぐ受け取れるようにします。

以上を踏まえた統合コード例を示します(ポイントとなる部分以外は簡略化しています):

import os, sys, json, requests

API_KEY = os.environ.get("PERPLEXITY_API_KEY")

for line in sys.stdin:
    req = json.loads(line)
    if req.get("method") != "perplexity_search_web":
        continue  # 他のリクエストは処理しない

    # 1. リクエストからクエリと期間指定を取得
    params = req.get("params", {})
    query = params.get("query", "")
    recency = params.get("recency")

    # 2. recencyに応じてクエリに期間フィルタを付加
    if recency in ("day", "week", "month", "year"):
        if recency == "day":
            query += " in the past 24 hours"
        elif recency == "week":
            query += " in the past week"
        elif recency == "month":
            query += " in the past month"
        elif recency == "year":
            query += " in the past year"

    # 3. Perplexity APIにリクエストを送信
    url = "https://api.perplexity.ai/chat/completions"
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json"
    }
    payload = {
        "model": "sonar-pro",
        "messages": [ {"role": "user", "content": query} ]
    }

    try:
        resp = requests.post(url, headers=headers, json=payload)
        data = resp.json()
        # 4. レスポンスから回答テキストを取得
        answer = data["choices"][0]["message"]["content"]
    except Exception as e:
        # エラー発生時はメッセージに内容を入れる
        answer = f"Error: {e}"

    # 5. 応答JSONを出力(idは元のリクエストに合わせる)
    result = {"id": req.get("id"), "result": answer}
    print(json.dumps(result))
    sys.stdout.flush()

上記コードでは、recencyの値に応じて英語で期間を付加しています。また、例外時には簡易的にエラー内容を文字列としてanswerに入れています(本格的な運用ではエラー内容を隠蔽したりログ出力のみ行う方が安全です)。Perplexity APIから取得した回答テキストは、そのままresultフィールドに格納し、JSON全体を文字列化して出力しています。クライアント(AI側)はこのresult文字列を受け取り、ユーザーへの応答に利用します。

検索結果をフォーマットしてレスポンス

上記実装では、Perplexityから返された回答をそのまま文字列として返しています。この回答文には質問に対する簡潔なまとめと、必要に応じて情報源の引用(ハイパーリンク付きの参照番号)が含まれます。MCPサーバーとしては回答テキストを返すだけで十分ですが、場合によっては検索結果を独自に加工して返すことも可能です。

例えば、回答テキスト中の引用リンク部分だけを抜き出し、上位3件の関連リンクリストを作成して返す、といった応用も考えられます。しかし、Perplexity APIは既に回答文中に情報源を組み込んでいるため、本チュートリアルでは追加のフォーマットは行わず素の回答を返す実装としました。そのままでもユーザーは回答文中のリンク番号をクリックして詳細情報を閲覧できますし、AIクライアント側で必要に応じてフォーマットを調整することもできます。

最後に、JSONレスポンス全体の形式が正しいことを確認しましょう。上記コードでは、元リクエストのidをそのまま返し、結果をresultキーに格納したJSONオブジェクトを出力しています。これはJSON-RPC 2.0のレスポンス形式({"id": <req_id>, "result": <結果>})に沿っています。print(json.dumps(obj))により改行付きで出力されるため、1リクエストにつき1行のJSONレスポンスがクライアントに送られます。flush()を呼んでいるので逐次クライアントに届き、クライアント側で待ち時間なく結果を処理できるでしょう。

5. テストとデバッグ

サーバーの動作確認方法

サーバー実装ができたら、その動作を確認します。以下のような方法でテストできます。

(A) Claudeデスクトップでテストする場合: Claude Desktopアプリをお持ちの場合は、設定ファイルに今回作成したMCPサーバーを登録します。Claude Desktopの設定ファイル(Macなら ~/Library/Application Support/Claude/claude_desktop_config.json)を開き、"mcpServers"セクションに新しいエントリを追加しま (perplexity-mcp/README.md at main · jsonallen/perplexity-mcp · GitHub)】。例えば、本チュートリアルのサーバースクリプトが~/mcp/perplexity_server.pyにある場合、設定は以下のようになります(APIキー部分はご自身のキーに置き換えてください):

"mcpServers": {
  "perplexity_search_service": {
    "env": {
      "PERPLEXITY_API_KEY": "YOUR_API_KEY_HERE"
    },
    "command": "python",
    "args": ["/Users/あなたのユーザ名/mcp/perplexity_server.py"]
  }
}

上記のように設定しClaude Desktopを再起動すると、AIが必要に応じてこのMCPサーバー(ここではperplexity_search_serviceという名前)を呼び出せるようになります。試しにClaudeとのチャットで「過去1週間のAnthropicの新着情報を検索して」などとプロンプトを送信してみてください。ClaudeがMCP経由のツール使用を要求するポップアップが表示されたら「このチャットで許可」をクリックしま (perplexity-mcp/README.md at main · jsonallen/perplexity-mcp · GitHub)】。その後、Perplexity経由の検索結果がClaudeから返ってくるはずです。

(B) 手動でサーバーをテストする場合: Claudeがなくても、ターミナルからサーバーにテスト用JSONを送って挙動を確認できます。サーバースクリプトを起動し(例: python perplexity_server.py)、別のターミナルから以下のようにJSON文字列をパイプで送ります:

echo '{"id": 1, "method": "perplexity_search_web", "params": {"query": "OpenAI 最新ニュース", "recency": "week"}}' | python perplexity_server.py

上記では、id=1の検索リクエストをパイプで送り、その結果を標準出力に表示させています。実行すると、一瞬待った後にサーバーからのレスポンスJSONが表示されるでしょう。resultフィールド内にPerplexityから得た回答テキストが含まれていることを確認してください。

よくあるエラーとその対処法

実装やテスト時につまずきやすい点と対処法をいくつか紹介します。

  • APIキーの認証エラー: APIキーが正しく設定されていないと、Perplexity APIへのリクエストが401エラー(Unauthorized)となり失敗します。サーバーログやエラー出力に認証エラーが表示された場合、環境変数PERPLEXITY_API_KEYの値が正しいか確認してください(前後に余計な空白や引用符が入っていないかも要チェックです)。また、Perplexity側でAPIキーに十分なクレジット残高があるかも確認しましょう。利用可能回数が尽きるとキーがブロックされるため、必要に応じてチャージを行いま (Initial Setup - Perplexity)】。

  • ネットワークエラー: インターネット接続が不安定だったりPerplexityのサービスが一時的にダウンしていたりすると、リクエストがタイムアウトや失敗になることがあります。requests.postが例外を投げてキャッチされた場合、エラーメッセージを出力するようにしています。対処としては、ネットワーク接続を確認し、再度リクエストを試みることです。一時的な問題であれば時間をおいて再度実行すると成功する場合もあります。

  • JSONのパースエラー: クライアントから送られてくるJSONが不正な場合、json.loadsで例外が発生してサーバーが停止してしまう可能性があります。Claudeなど正式なMCPクライアントからのリクエストであればこの心配はほぼありませんが、自分で手動入力してテストする際はJSONの構文ミスに注意してください。必要に応じてサーバーコードにtry...exceptを追加し、パースエラー時にcontinueするなどの処理を入れると、サーバーが落ちずに待機を続けられます。

  • 出力がクライアントに届かない: サーバーから結果をprintしているのにクライアントに反映されない場合、出力バッファの問題が考えられます。上記コードでは明示的にsys.stdout.flush()を呼んでいますが、これを忘れると標準出力がバッファリングされたままクライアントに届かないことがあります。逐次flushすることでリアルタイムに結果を送信できます。

  • その他の例外や不具合: 例えば、requestsライブラリがインストールされていないとImportErrorになりますし、モデル名が間違っていると400エラーになります。エラーメッセージを手がかりに原因を特定し、一つ一つ対処してください。開発中はprint文やログを活用して、リクエスト内容やレスポンス内容を確認するとデバッグが容易になります。

以上、PythonによるMCPサーバー実装とPerplexity検索機能統合の基本手順を解説しました。MCPを用いることでAIアシスタントにリアルタイムのウェブ情報を提供できるようになり、より高度な応答や最新情報の参照が可能になります。まずはシンプルな実装から試し、必要に応じて機能拡張やエラーハンドリングの強化を行ってみてください。適切に設定・実行すれば、ClaudeのようなLLMクライアントからウェブ検索ツールがシームレスに呼び出せるようになるでしょう。楽しんで開発を進めてください。🚀

参考資料:

いいなと思ったら応援しよう!