MCP サーバー開発 Tips — エージェントを本番戦力に

x facebook hatena

ARISE analyticsの挺屋です。私は社内の生成AI活用推進を担当し、日々の開発現場でAIツールをどのように安全かつ効果的に使うかを検証・標準化しています。本記事では、その取り組みの中で得られたMCPサーバーのツール作成について、実践的な知見をまとめます。

この記事は、次の方を想定しています。

  • 企業内でMCP サーバーを設計・実装し、提供したいエンジニア

  • MCP実装をしているが、ツールの利用の制御がうまくいっていない方

以下、背景と価値、仕組みのさわりを述べた後、実装と運用に効くTipsを体系的に解説します。

目次


企業でのMCP開発の必要性

企業でAIツールを利用したり、エージェント活用をする際には、MCPを利用することが重要だと思います。MCPで各種外部ツールにAIがアクセスできるようにすることで、真の業務効率化がされると感じています。むしろ、それが無い状態ではいつまでもAIとの対話による問題解決にとどまり、AIが作成した文章やコードをコピペしたり、教えてくれた手順を人が実行するなどAIエージェントにタスクを任せることがしにくくなります。

しかし、企業ではAIの利用に対してある程度ガバナンスを担保して利用を進めることが求められます。ARISE analytics(以下ARISE)でもAIに対する利用ガバナンスの是正がすべからく行われています。ガバナンスを満たしつつ、MCPでの外部接続をした活用を進めるにあたっては、MCP サーバーを自作して、次のような2つの利点を得ることができます。

カスタム機能の実現

  • 企業ごとに独自開発した社内システムや業務プロセスに、AI エージェントを直接接続できます。例えば、自社の勤怠管理システム、社内申請ワークフロー、独自開発のCRM など、既存の公開MCPサーバーではカバーできない社内サービスとの連携が可能になります。

セキュリティとプライバシーの制御

  • 既存の公開MCPサーバーでは、企業のセキュリティポリシーや社内利用基準を満たさないケースがあります。例えば、認証方式、ログ保管要件、データの外部送信制限などが要件を満たさず、利用を許可できないことも少なくありません。自社でMCPサーバーをホストすることで、最小権限の原則、短命トークン、レート制限、スロットリング、出力検証、人手承認(HITL)といった防御層を、企業の要件に合わせて実装できます。プロンプトインジェクションは原理的にゼロにできないため、出力検証と権限制御で"外側"から守る設計が、企業では決定打になります。

MCPの仕組み

次にMCPサーバーを開発するうえで理解するべき基本的な概念を紹介します。

MCP(Model Context Protocol) は、LLMエージェントが外部のツールやシステムを連携するための標準的なプロトコルのことをいいます。MCPは、AIツール(Claude、Cursor、VS Codeなど)と外部システム(データベース、API、ファイルシステムなど)間の複雑な連携を簡素化し、AIアプリケーションの可能性を広げる重要な仕組みとして位置づけられています。

image-20251008-004559

MCPはクライアント・サーバー型アーキテクチャを採用しており、以下の構成要素で成り立っており、MCPクライアント(CursorやClaude Desktopなど)・MCP サーバー間で、JSON-RPC 2.0 に基づくメッセージをやり取りします。やり取りのトランスポートはstdioやStreamable HTTPに対応しており、MCPサーバーはローカル/リモートどちらでも可能です。サーバーが公開する「ツール(Tools)」「リソース(Resources)」「プロンプト(Prompts)」を通じて、外部の機能やデータに安全にアクセスします。MCPサーバー開発する際は主にツールにどのような挙動をさせたいか詳細を実装していくことになります。

サーバーは起動時に能力(capabilities)を提示し、クライアントは tools/list / resources/list で一覧を取得、必要に応じて call_tool でツールを実行します。つまりクライアント側は一覧を取得したテキストを読み取り、どんなMCPツールを利用できるのかを理解する必要があります。

なぜツール設計が重要なのか

上記の仕組みで重要なのが、“LLMにとってのMCPの仕様書はテキスト(説明)とスキーマだけ”という事実です。人間がAPIの仕様を理解する際のように察してはくれないため、名前・説明・入力スキーマ(JSON Schema)を厳格かつ具体的に整えるほど、ツールの誤用が減ります。

通常のAPI開発では関数に対してどのような引数を渡せばよいかが明確であり決定論的システムなので、そのツールの利用方法は明確です。しかし、MCPツールは自然言語の指示で呼び出されるため、どのようにツールを利用するのかが明確でない非決定論的システムとなります。そのため、しばしばLLMがツールを誤用することは起こります。

例えばエージェントに対して「今日は傘を持っていくべき?」という質問をしても、エージェントの挙動は決定論的ではなく、以下のような様々な戦略をとる可能性があり決定論的ではありません。

  • 天気ツールを呼び出して判断する

  • 一般知識から回答する

  • 「どこの天気ですか?」と逆に質問する

  • 場合によっては、ツールの使い方を誤解する

この非決定論性により、ツール設計の考え方を根本から変える必要があります。

また、ツールを使うのはエージェントでありLLMです。LLMでは一度に扱えるコンテキストの量が有限です。そのためツールを使う・返答を理解するためには所定のコンテキストの範囲内で行う必要があります。たとえコンテキストの範囲内に収まっていても処理に必要なコアの情報が何なのかよくわかりにくいものが返ってくると、ユーザーが本来望んでいるエージェントの挙動とは異なった動きをすることがあります。例えば人間同士の会話でも、得たい回答に対して長ったらしくて何が結論なのかわからない内容で答えられたときに、「で、なにが言いたいのですか?」と心の中で言いたくなると思います。そういったところにも注意を払ってツールを設計・実装していく必要があります。

以降のTipsは、この「LLMという扱えるコンテキストが有限かつ、確率的ユーザーのためのAPI設計」を徹底するための具体的な作法をいくつか紹介します。

ツール設計Tips

本記事ではMCPサーバーを実装するうえで、気を付けておいた方がよいTipsを5つ紹介します。各Tipsの概要は次の通りです。

  1. 一覧取得をやめて検索をレスポンスするようにして、トークンの消費を削減

  2. ツールの説明文を丁寧に記述することで、エージェントが正しくそのツールを使えるようにする

  3. レスポンスの形式を工夫することで、トークン消費を削減

  4. ツールの呼び出し回数が少なくなる設計をして、エージェントの応答時間の効率を良くする

  5. エラーの内容をエージェントが理解しやすいようにすることで、エージェント自身によるタスク継続を促す

各Tipsには実装サンプルを載せていますが、ここでは仮の題材として社内の図書システムとの接続MCPを作成することを挙げています。その図書システムからはARISEにある技術書などの書籍を借りることができます。どのような書籍があり、各書籍がどのようなステータスを持つのかなどMCPサーバー化できる余地があるためここで取り扱うこととします。なお、サンプル実装はあくまで仮でありそのようなMCPサーバーがARISE内で動いているわけではありません。

1 )「一覧」を捨て「検索」を実装し、トークン消費を削減する

初期実装で起こりうる問題

既存のシステムのAPIコールをMCPのツールとして実装しようとしたとき、良く起こりがちなのは既存のAPIエンドポイントをそのまま何も考えずラップしたツールを作成することです。ラップすること自体が悪いわけではありませんが、何かの一覧の取得などAPIレスポンスが多くの文字数を取る場合に問題となります。

▼問題の実装例(例の提示のため、エラーハンドリングや認可など本来あるべき実装は省略します)

# 初期実装 - 素朴なツール定義
@mcp.tool()
def list_all_books():
    """全ての書籍を取得する"""
    response = requests.get("https://library-example.com/api/v1/books/")
    return response.json()
    

ここでは図書館で扱っているすべての書籍を取得するAPIを叩いています。

コンピュータのメモリは豊富ですが、LLMのコンテキストは限られています。

従来のプログラムなら、300冊のリストを効率的にループ処理できます。しかし、LLMエージェントは

  1. トークン単位で全てを読む必要がある - 1冊ずつ、全ての情報を順番に処理

  2. 無関係な情報も消費する - 求めていない本の情報も全て読み込む

  3. コンテキスト枠を圧迫する - 本当に必要な思考や推論のスペースが減る

という挙動をするため、レスポンスが長すぎるとLLMの有限なコンテキストの範囲内では処理しきれないケースが生じます。

ユーザー: 「機械学習に関する本を探して」

エージェント: list_all_books()を呼び出し
→ 300冊全ての書籍データを受信 (約50,000トークン)
→ 全てのデータを読み込んで関連書籍を探そうとする
→ タイムアウトまたはコンテキスト超過
    

推奨アプローチ

Anthropicのドキュメント「Writing effective tools for LLM agents」では、明確にこう述べています。

より多くのツールが常により良い結果をもたらすわけではありません。 一般的な誤りは、既存のソフトウェア機能やAPIエンドポイントを単純にラップしただけのツールを作ることです。エージェントには独自の「アフォーダンス(利用可能な行動の認識)」があり、従来のソフトウェアとは異なります。(和訳)

解決策としては無駄な一覧取得はやめ、なるべく検索のアプローチをとるようにすることです。

list_all_booksの代わりに検索ベースのツールを実装してみます。

# 改善版 - 検索ベースのツール
@mcp.tool()
def search_books(
    query: str,
    category: Optional[str] = None,
    availability: str = "all",  # "all" | "available" | "borrowed"
    max_results: int = 10
) -> dict:
    """
    書籍を検索します。タイトル、著者名、説明文から関連する書籍を検索します。
    
    Args:
        query: 検索キーワード(例:「機械学習」「データ分析」「Python」)
        category: カテゴリでフィルタ(例:「プログラミング」「マーケティング」)
        availability: 貸出状況でフィルタ
            - "all": 全ての本
            - "available": 貸出可能な本のみ
            - "borrowed": 貸出中の本のみ
        max_results: 返却する最大件数(デフォルト: 10)
    
    Returns:
        検索結果のリスト。各書籍には以下の情報が含まれます:
        - title: 書籍タイトル
        - author: 著者名
        - category: カテゴリ
        - published_year: 出版年
        - available_copies: 貸出可能冊数
        - total_copies: 総蔵書数
        - brief_description: 簡潔な説明(最大200文字)
    """
    # 検索ロジック実装
    params = {
        "search": query,
        "category": category,
        "availability": availability,
        "limit": max_results
    }
    response = requests.get(
        "https://library-example.com/api/v1/books/search/",
        params=params
    )
    return response.json()
    

このツールを使えば確実にLLMが扱える範囲内のコンテキストのみを処理することができるようになり、ツールの利用失敗を減らすことができます。

▼動作例

ユーザー: 「機械学習に関する本で、今借りられるものを探して」

エージェント: search_books(
    query="機械学習",
    availability="available",
    max_results=20
)

結果: 
- データ駆動型回帰分析 : 計量経済学と機械学習の融合- 貸出可能: 1冊
- 機械学習&ディープラーニングのしくみと技術がこれ1冊でしっかりわかる教科書 - 貸出可能: 1冊
- 機械学習デザインパターン : データ準備、モデル構築、MLOpsの実践上の問題と解決 - 貸出可能: 1冊
...(計18冊)

エージェント: 「機械学習に関する貸出可能な本が18冊見つかりました。
『機械学習&ディープラーニングのしくみと技術がこれ1冊でしっかりわかる教科書』(2019年)
から始めることをお勧めします...」
    

2 ) ツール説明を丁寧に書く

情報不足の問題

ツールの説明文が簡潔すぎる場合、人間にとっては十分かもしれませんが、エージェントにとっては不十分です。

def get_book(book_id: str):
    """書籍の詳細を取得する"""
    # ...
    

このようなツールを使った場合以下のようなエージェントの挙動になることが考えられます。

ユーザー: 「『イシューからはじめよ』の詳細を教えて」

エージェント(内部思考):
「book_idが必要だが、タイトルしか与えられていない...
まずsearch_booksで検索してIDを取得すべきか?
それとも、タイトルをIDとして使えるのか?
説明文には何も書かれていない...」

結果: エージェントが混乱し、誤ったパラメータで呼び出す
    

人間の開発者がAPIドキュメントを読む場合、以下のような暗黙知を持っています。

  • IDは通常、検索結果やリストから取得する

  • ISBNコードは書籍の一意な識別子である

  • 存在しないIDを渡すとエラーが返る

しかし、LLMエージェントはこれらの暗黙知を持っていません。そこで、明示的に説明する必要があります。「なぜツール設計が重要なのか」のセクションでも触れた通り、ツールをどのように使ったらよいのかエージェントに対してちゃんと説明するように丁寧に書くことを心掛けなければいけません。

推奨アプローチ

Anthropicのベストプラクティスガイドでは、こう推奨されています

ツールを使用する際に Claude から最高のパフォーマンスを引き出すには、次のガイドラインに従ってください。

  • 非常に詳細な説明を記載してください。これはツールのパフォーマンスにおいて最も重要な要素です。説明には、ツールに関する以下の点を含め、あらゆる詳細を記載する必要があります。

    • ツールの機能

    • いつ使うべきか(そしていつ使うべきでないか)

    • 各パラメータの意味とツールの動作への影響

    • ツール名が不明瞭な場合、ツールが返さない情報など、重要な注意事項や制限事項。ツールに関する詳細な情報をクロードに提供すればするほど、ツールをいつ、どのように使用するかをより正確に判断できるようになります。ツールの説明は1つにつき少なくとも3~4文、複雑なツールの場合はそれ以上の長さにすることを目標にしてください。

  • 具体例よりも説明を優先しましょう。ツールの使い方の例を説明文やプロンプトに含めることは可能ですが、ツールの目的とパラメータを明確かつ包括的に説明することの方が重要です。具体例は、説明文を完全に書き終えてから追加しましょう。(和訳)

▼改善版のツール説明

@mcp.tool()
def get_book_details(
    book_identifier: str,
    identifier_type: str = "title"
) -> dict:
    """
    書籍の詳細情報を取得します。
    
    このツールを使用する前に、通常はsearch_books()で書籍を検索し、
    その結果から正確なタイトルまたはISBNを取得することを推奨します。
    
    Args:
        book_identifier: 書籍を特定する情報
            - identifier_type="title"の場合:完全一致する書籍タイトル
              例:「機械学習を解釈する技術」
              注意:タイトルは正確に一致する必要があります。
              部分一致や曖昧な表現では見つかりません。
              
            - identifier_type="isbn"の場合:13桁のISBNコード
              例:「9784297119485」
              注意:ハイフンなしの数字のみです。
              
        identifier_type: 識別子のタイプ
            - "title": 書籍タイトルで検索(デフォルト)
            - "isbn": ISBNコードで検索
            
    Returns:
        書籍の詳細情報を含む辞書:
        {
            "title": "機械学習を解釈する技術",
            "author": "森下光之助",
            "publisher": "技術評論社",
            "published_date": "2021-08",
            "isbn": "9784297119485",
            "category": "機械学習",
            "pages": 272,
            "description": "あらゆる予測モデルを解釈する4つの手法...",
            "total_copies": 2,
            "available_copies": 2,
            "borrowed_by": [],  # 貸出中の場合、借りている人のリスト
            "related_books": [  # 関連書籍の提案(最大3件)
                {
                    "title": "機械学習デザインパターン",
                    "author": "...",
                    "available": true
                }
            ]
        }
        
    Raises:
        BookNotFoundError: 指定された書籍が見つからない場合
            - よくある原因:タイトルの誤字、部分一致での検索
            - 解決方法:まずsearch_books()で正確なタイトルを確認してください
            
    Examples:
        # タイトルで検索(推奨)
        get_book_details("機械学習を解釈する技術", identifier_type="title")
        
        # ISBNで検索
        get_book_details("9784297119485", identifier_type="isbn")
        
    Note:
        - 大量の書籍の詳細を一度に取得したい場合は、
          代わりにsearch_books()を使用してください。
        - このツールは1冊の詳細情報を取得するためのものです。
    """
    # 実装...
    

ここではツールの概要や、引数、返り値や利用例、注意点などを網羅的に記載しています。これくらい書くことでツールの誤用を防ぐことができます。

実際にAnthropicがウェブ検索機能をリリースした際、Claudeが検索クエリに不要な「2025」という年号を自動的に追加していたという問題が見つかり、それに対してツールの説明文を改善することでこのバグを解消したという例がありました。

# エージェントが実際に行っていた検索例
user_query: "最新のAI技術トレンド"
actual_search_query: "最新のAI技術トレンド 2025"  # 不要な年号を追加
    

このように、ツールの動作を決めるのはコードだけではなく、ツールの説明文やパラメータ定義が、エージェントの振る舞いを大きく左右します。

3 ) レスポンスの情報量を適切な量にする

エージェントへの情報過多レスポンス問題

バックエンドAPIが返す全てのフィールドをそのままエージェントに返している場合、そのうちの多くの情報は不要なもので、無駄にトークン消費することになります。

例として、とある本の詳細項目を知りたいときにget_book_detailsツールを使ったと考えます。

▼すべてのフィールドを返すレスポンス例

{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "title": "機械学習を解釈する技術",
  "title_kana": "キカイガクシュウヲカイシャクスルギジュツ",
  "author": "森下光之助",
  "author_kana": "モリシタミツノスケ",
  "publisher": "技術評論社",
  "publisher_kana": "ギジュツヒョウロンシャ",
  "published_date": "2021-08-25T00:00:00Z",
  "isbn": "9784297119485",
  "isbn10": "4297119485",
  "category_id": "cat_ml_001",
  "category_name": "機械学習",
  "pages": 272,
  "width": 182,
  "height": 235,
  "depth": 18,
  "weight": 480,
  "description": "あらゆる予測モデルを解釈する4つの手法PFI、PD、ICE、SHAP/特徴量の重要度/特徴量と予測値の関係性/インスタンスごとの異質性/予測の理由―そのモデルの振る舞いを説明できますか?",
  "cover_image_url": "https://ndlsearch.ndl.go.jp/thumbnail/9784297119485.jpg",
  "cover_image_url_large": "https://books.google.com/books/content?id=...",
  "total_copies": 2,
  "available_copies": 2,
  "borrowed_copies": 0,
  "reserved_copies": 0,
  "damaged_copies": 0,
  "lost_copies": 0,
  "purchase_date": "2021-09-15T00:00:00Z",
  "purchase_price": 2980,
  "location_shelf": "3F-ML-05-A",
  "location_row": 5,
  "location_column": "A",
  "barcode": "1234567890123",
  "created_at": "2021-09-20T10:30:00Z",
  "updated_at": "2024-10-01T15:45:22Z",
  "created_by": "admin",
  "updated_by": "system",
  ...さらに30個のフィールド
}
    

このレスポンスの問題点として以下のことが挙げられます。

  1. トークンの無駄遣い: 1冊の情報だけで約800トークン消費

  2. 認知負荷が高い: エージェントが必要な情報を探すのに時間がかかる

  3. 技術的詳細の過剰: widthheightbarcodeなどは通常不要

  4. 読みにくい: ISO形式の日時、内部ID、システムメタデータなど

推奨アプローチ①

エージェントにしてほしいタスクに対して必要な情報のみに選別して返すようにしましょう。

処理に対して不必要であるものを返さないのはもちろんですが、自然言語で理解できないものも返さないようにするべきです。

例えば、titleauthorなどは自然言語として理解しやすいものですが、uuidなどの低レベルの識別子は本の詳細情報として返してもどんな特性のものなのか理解できない(そのような情報ではない)ので返すべきではありません。もちろん、このMCPを使用するユースケースとして後続に本の予約をするときにこのuuidを利用するのであれば、返した方が良さそうです。ここではあくまで本の詳細を理解するのにはuuidの情報は意味をなさないということを言っています。

推奨アプローチ②

アプローチ例としてはresponse_formatパラメータを導入することが1つ手として考えられます。

GraphQLのように、エージェントが必要な情報の粒度を選択できるようにします。

▼response_format導入例

@mcp.tool()
def library_search_books(
    query: str,
    category: Optional[str] = None,
    availability: str = "all",
    response_format: str = "concise",
    max_results: int = 10
) -> dict:
    """
    書籍を検索します。response_formatで情報の粒度を制御できます。
    
    Args:
        response_format: レスポンスの詳細度(重要なパラメータです)
            
            "concise" - 必要最小限モード(デフォルト、推奨):
              ├─ フィールド数: 6個
              ├─ トークン消費: 50-100トークン/冊
              ├─ 用途: 一覧表示、簡易検索、在庫確認
              └─ 含まれる情報: タイトル、著者、年、カテゴリ、在庫数
              
            "detailed" - 詳細情報モード:
              ├─ フィールド数: 12個
              ├─ トークン消費: 200-300トークン/冊  
              ├─ 用途: 詳細確認、貸出判断、レポート作成
              └─ 含まれる情報: 上記6個 + 出版社、ISBN、説明文、
                              ページ数、借りている人、関連書籍
                              
    使い分けのガイドライン:
        ✓ ユーザーが「ある?」「何冊?」と聞く → concise
        ✓ ユーザーが「詳しく」「説明して」と聞く → detailed
        ✓ ユーザーが「関連する本も」と聞く → detailed
        ✓ 迷ったら → concise(必要なら後でdetailedで再取得)
        
    Returns (concise):
        {
            "books": [
                {
                    "title": "機械学習を解釈する技術",
                    "author": "森下光之助",
                    "published_year": "2021",
                    "category": "機械学習",
                    "available_copies": 2,
                    "total_copies": 2
                }
            ],
            "total_found": 23,
            "showing": 10
        }
        
    Returns (detailed):
        {
            "books": [
                {
                    "title": "機械学習を解釈する技術",
                    "author": "森下光之助",
                    "publisher": "技術評論社",
                    "published_date": "2021-08",
                    "isbn": "9784297119485",
                    "category": "機械学習",
                    "pages": 272,
                    "description": "あらゆる予測モデルを解釈する...",
                    "total_copies": 2,
                    "available_copies": 2,
                    "borrowed_by": [],
                    "related_books": [...]
                }
            ],
            "total_found": 23,
            "showing": 10
        }
        
    Token Usage Comparison:
        10冊の検索結果:
        - 改善前(全フィールド): 8,000トークン
        - concise: 720トークン(91%削減)
        - detailed: 2,400トークン(70%削減)
        
    Performance Impact:
        - concise使用時の応答時間: 1-2秒
        - detailed使用時の応答時間: 2-3秒
        - 改善前の応答時間: 5-8秒
    """
    # 実装...
    

ここでは簡潔モード(concise)と詳細モード(detailed)を用意し、レスポンスの情報量を切り替えれるようにしています。

conciseレスポンス例

{
  "title": "機械学習を解釈する技術"
  "author": "森下光之助"
  "published_year": "2021"
  "category": "機械学習"
  "available_copies": 2
  "total_copies": 2
}
    

detailedレスポンス例

{
  "title": "機械学習を解釈する技術"
  "author": "森下光之助"
  "publisher": "技術評論社"
  "published_date": "2021-08"
  "isbn": "9784297119485"
  "category": "機械学習"
  "pages": 272
  "description": "あらゆる予測モデルを解釈する4つの手法PFI、PD、ICE、SHAP/特徴量の重要度/特徴量と予測値の関係性/インスタンスごとの異質性/予測の理由",
  "total_copies": 2,
  "available_copies": 2,
  "borrowed_by": [],
  "related_books": [
    {
      "title": "機械学習デザインパターン",
      "author": "Valliappa Lakshmanan",
      "available": true
    },
    {
      "title": "Kaggle Grandmasterに学ぶ機械学習実践アプローチ",
      "author": "門脇大輔",
      "available": true
    }
  ]
}
    

各レスポンス例を見てとれるように、レスポンスの内容を読むのに必要なコンテキストサイズがまるで異なります。本の貸し出しのときには本の詳細を提示しないといけないかな、逆に検索に付随する付加情報の提示であれば簡潔モードでもいいかなといったようにユースケースによって粒度の調整ができると、ユーザビリティが向上してとても良い設計になります。

▼実際の使用例

# シナリオ1: 簡易検索(conciseで十分)
ユーザー: 「Pythonの本ある?」
エージェント: search_books("Python", response_format="concise")
→ 720トークンで10冊の情報取得
→ 「Pythonの本が15冊見つかりました。最新の『退屈なことはPythonにやらせよう』がお勧めです」

# シナリオ2: 詳細確認が必要(detailedを使用)
ユーザー: 「『機械学習を解釈する技術』について詳しく教えて。
         関連する本も知りたい」
エージェント: search_books("機械学習を解釈する技術", 
                           response_format="detailed",
                           max_results=1)
→ 206トークンで詳細情報+関連書籍3件取得
→ 詳細な説明と次に読むべき本の提案

# シナリオ3: 在庫確認のみ(最もconcise)
ユーザー: 「『イシューからはじめよ』は今借りられる?」
エージェント: search_books("イシューからはじめよ", 
                           response_format="concise",
                           max_results=1)
→ 72トークンで在庫状況のみ取得
→ 「はい、2冊とも貸出可能です」
    

4 ) ツールを1つに短縮し、応答時間を短縮する

ツールが過度に細分化される問題

「単一責任の原則」に従い、機能ごとに細かくツールを定義した場合を考えます。ここでは本を貸し出し・までに必要そうなツールを列挙してみます。

# 初期実装 - 7つの個別ツール
1. search_books(query)         # 書籍検索
2. get_book_details(title)     # 詳細取得
3. check_availability(title)   # 在庫確認
4. get_related_books(title)    # 関連書籍
5. borrow_book(title)          # 借りる
6. return_book(title)          # 返却
7. get_borrower_info(title)    # 借りている人の情報
    

一見きれいに分割されて見えます。しかし実際にエージェントが使うとツールの呼び出しが非効率になることが起こり得ます。

▼非効率な呼び出し例: シナリオ:「機械学習の本で今借りられるものを探して、関連書籍も教えて」

【改善前:7回のツール呼び出し】
1. search_books("機械学習") 
   → 20冊のリスト取得

2. check_availability("機械学習を解釈する技術")
   → 貸出可能

3. check_availability("機械学習デザインパターン")
   → 貸出中

4. check_availability("Kaggle Grandmasterに学ぶ...")
   → 貸出可能

5. get_related_books("機械学習を解釈する技術")
   → 関連書籍3冊

6. check_availability(関連書籍1)
   → 貸出可能

7. check_availability(関連書籍2)
   → 貸出中

所要時間: 約12秒
トークン消費: 約15,000トークン
    

ここでは、借りられる本を探し当てるのに7回ものツールレスポンスが生じて、そのたびにレスポンスをLLMが理解し次の打ち手を考え、ツールの呼び出しをすることを繰り返します。時間的にも非効率ですし、毎回LLMのトークン消費があるのでコスト的にも非効率となります。

推奨アプローチ

機能を統合し、内部で複数の個別操作(またはAPI呼び出し)を処理するよう、ツールを1つにまとめることができます。

Anthropicのガイドラインでもこの方法が紹介されており、そこでは具体例として次のように説明されています。

  • (会議を設定するとき)list_userslist_eventscreate_eventを実装する代わりに、空き時間を見つけてイベントをスケジュールするschedule_eventを検討してください

  • (ログ情報を返すとき)read_logsを実装する代わりに、関連するログ行とその周辺コンテキストのみを返すsearch_logsを検討してください

  • (顧客の関連情報を返すとき)get_customer_by_idlist_transactionslist_notesを実装する代わりに、顧客の最近の関連情報を一度にまとめるget_customer_contextを実装してください(和訳)

その方法に従って、借りされる本はあるか検索するツールを次のように統合して実装することができます。

@mcp.tool()
def find_available_books(
    query: str,
    category: Optional[str] = None,
    include_related: bool = True,
    max_results: int = 5
) -> dict:
    """
    貸出可能な書籍を検索し、関連書籍も合わせて提案します。
    
    このツールは以下を自動的に行います:
    1. クエリに基づいて書籍を検索
    2. 貸出可能な書籍のみをフィルタリング
    3. 各書籍の関連書籍を自動的に取得(include_related=Trueの場合)
    4. 関連書籍の在庫状況も確認
    
    つまり、このツール1回の呼び出しで、
    「借りられる本」と「次に読むべき本」の両方が分かります。
    
    Args:
        query: 検索キーワード
        category: カテゴリフィルタ
        include_related: 関連書籍も含めるか(デフォルト: True)
        max_results: 返却する最大件数
        
    Returns:
        {
            "available_books": [
                {
                    "title": "機械学習を解釈する技術",
                    "author": "森下光之助",
                    "available_copies": 2,
                    "related_books": [  # include_related=Trueの場合のみ
                        {
                            "title": "機械学習デザインパターン",
                            "available": false,  # 貸出中
                            "return_date": "2025-10-15"  # 返却予定日
                        }
                    ]
                }
            ],
            "total_found": 15,  # 貸出可能な書籍の総数
            "suggestions": [
                "カテゴリ「深層学習」でさらに5冊が貸出可能です",
                "『機械学習デザインパターン』は10月15日に返却予定です"
            ]
        }
    
    Note:
        在庫確認のみが目的なら、search_books(availability="available")
        を使用する方が効率的です。
        このツールは「借りたい本を探す」という具体的なタスク向けです。
    """
    # 内部で複数のAPI呼び出しを統合
    books = search_books_api(query, availability="available")
    
    if include_related:
        for book in books:
            book["related_books"] = get_related_books_api(book["isbn"])
            # 関連書籍の在庫状況も自動チェック
            for related in book["related_books"]:
                related["available"] = check_availability_api(related["isbn"])
    
    return format_response(books)
    

ここではユーザーが検索したい観点での書籍の検索と、借りられるかのチェック、関連書籍の確認を全部一度に行い、逐次的なレスポンスはしないようにしています。そうすることで次のような動作になり効率化を図れます。

ユーザー: 「機械学習の本で今借りられるものを探して。
         次に読むべき本も知りたい」

【改善前】
search_books() → check_availability() × 3 → 
get_related_books() → check_availability() × 2
= 7回のツール呼び出し

【改善後】
find_available_books(query="機械学習", include_related=True)
= 1回のツール呼び出しで完結

エージェント: 「機械学習に関する貸出可能な本が5冊見つかりました。
お勧めは『機械学習を解釈する技術』です。
この本を読んだ後は、『機械学習デザインパターン』が良いでしょう。
ただし、現在貸出中で、10月15日に返却予定です。」
    

5 ) エラーを「ガイド付き代替案」に変える

無意味なエラーメッセージ問題

本を借りるための関数を次のように書いてエラーを吐き出すことを考えてみます。

def borrow_book(title: str):
    book = find_book(title)
    if not book:
        return {"error": "Not found"}
    
    if book.available_copies == 0:
        return {"error": "Not available"}
    
    # 借りる処理...
    

ここでは本が見つからなければNot Found、貸出できなければNot availableがエラーメッセージとして返されます。しかしこの情報でユーザーがエージェントに期待する挙動を十分に満たせるでしょうか。おそらく次のようなユーザーの声が上がると思います。

ユーザー: 「機械学習デザインパターンを借りたい」

エージェント: borrow_book("機械学習デザインパターン")
→ {"error": "Not available"}

エージェント: 「申し訳ございません。その本は現在貸出できません。」

ユーザー(心の声): 「いつ返却されるの?予約できるの?
                  代わりの本は?何も分からない...」
    

単純なエラーレスポンスにしたばかりに、ユーザーは本を借りたいという要求に対してそれを満たす挙動が無く、現状の確認だけに終わってしまっています。もちろん本の詳細を確認するツールを、別途ユーザーがこの後に呼び出すよう指示すればよいのですが、いちいち挙動をユーザーに任せてしまっていては体験が非常に悪くなります。もしかしたらユーザーはここで離脱するかもしれません。実装したいMCPサーバーが何かしらのECサイトの購入補助だとしたら、購入を阻害する機会損失にもなるかもしれません。

推奨アプローチ

エラーメッセージを単なる失敗の通知だけに終わらせず、次の動作のヒントを提示することが重要です。

  • 診断情報: なぜ失敗したのか

  • 指針: 次回どうすれば成功するか

  • 代替案: 今すぐ取れる別の行動

Anthropicのガイドラインでも次のように示されています。

ツール呼び出しがエラーを起こした場合(例:入力検証時)、不透明なエラーコードやトレースバックではなく、具体的で実行可能な改善を明確に伝えるようにエラーレスポンスをプロンプトエンジニアリングできます。(和訳)

▼エラーメッセージ改良例

@mcp.tool()
def borrow_book(title: str) -> dict:
    """
    書籍を借ります。
    
    エラーハンドリング:
        このツールは、エラーが発生した場合でも、
        次に取るべきアクションを明確に示します。
    """
    # 書籍の存在確認
    book = find_book_by_title(title)
    if not book:
        # 類似タイトルを検索して提案
        similar_books = search_similar_titles(title)
        
        if similar_books:
            return {
                "success": False,
                "error_type": "book_not_found",
                "message": f"'{title}'という書籍が見つかりませんでした。",
                "reason": "タイトルが正確に一致していない可能性があります。",
                "diagnostic": {
                    "searched_for": title,
                    "common_causes": [
                        "副題が省略されている",
                        "記号(:、!など)が異なる",
                        "誤字がある"
                    ]
                },
                "suggested_action": "以下の類似タイトルのいずれかではありませんか?",
                "suggestions": [
                    {
                        "title": book.title,
                        "author": book.author,
                        "match_score": 0.85,
                        "available": book.available_copies > 0
                    }
                    for book in similar_books[:3]
                ],
                "next_step": "正確なタイトルが分かったら、再度borrow_book()を呼び出してください。",
                "how_to_find_exact_title": "search_books()で部分一致検索すると正確なタイトルが分かります"
            }
        else:
            return {
                "success": False,
                "error_type": "book_not_found",
                "message": f"'{title}'という書籍が見つかりませんでした。",
                "reason": "図書館の蔵書にない可能性があります。",
                "suggested_action": "search_books()で利用可能な書籍を探してください。",
                "search_hint": f"search_books('{title.split()[0]}')を試してみてください。",
                "alternative_option": {
                    "description": "この本の購入リクエストを出すこともできます",
                    "tool_to_call": "library_request_purchase(title, reason='業務に必要')"
                }
            }
    
    # 在庫確認
    if book.available_copies == 0:
        # 誰が借りているか、いつ返却予定かを調べる
        borrowers = get_current_borrowers(book.isbn)
        earliest_return = min([b.return_date for b in borrowers])
        
        return {
            "success": False,
            "error_type": "not_available",
            "message": f"'{title}'は現在全て貸出中です。",
            "availability_details": {
                "total_copies": book.total_copies,
                "borrowed_copies": book.total_copies,
                "earliest_return_date": earliest_return.strftime("%Y-%m-%d"),
                "days_until_return": (earliest_return - datetime.now()).days,
                "all_borrowers": [
                    {
                        "name": b.name,
                        "division": b.division,
                        "borrow_date": b.borrow_date.strftime("%Y-%m-%d"),
                        "return_date": b.return_date.strftime("%Y-%m-%d")
                    }
                    for b in borrowers
                ]
            },
            "alternative_actions": [
                {
                    "option": 1,
                    "action": "reserve",
                    "title": "この本を予約する",
                    "description": "返却され次第、自動的にあなたに貸出されます",
                    "tool_to_call": "reserve_book(title)",
                    "estimated_wait": f"{(earliest_return - datetime.now()).days}日",
                    "pros": ["確実に借りられる", "手間なし"],
                    "cons": ["待ち時間が必要"]
                },
                {
                    "option": 2,
                    "action": "find_alternatives",
                    "title": "類似の貸出可能な本を探す",
                    "description": "同じトピックで今すぐ借りられる本",
                    "tool_to_call": f"find_available_books(query='{book.category}', exclude_title='{title}')",
                    "preview": get_related_available_books(book.isbn, limit=2),
                    "pros": ["今すぐ読める", "新しい視点が得られる"],
                    "cons": ["完全に同じ内容ではない"]
                },
                {
                    "option": 3,
                    "action": "contact_borrower",
                    "title": f"{borrowers[0].name}さんに早期返却を相談する",
                    "description": "緊急性が高い場合の最終手段",
                    "contact_method": f"Slack: @{borrowers[0].slack_id}",
                    "template_message": f"『{title}』を緊急で読む必要があります。もし読み終わっていれば、早めに返却いただけないでしょうか?",
                    "pros": ["最短で入手可能"],
                    "cons": ["相手の都合を考慮必要", "緊急時のみ推奨"],
                    "note": "本当に緊急の場合のみ使用してください"
                }
            ],
            "related_available_books": [
                {
                    "title": "信頼性の高い機械学習",
                    "author": "...",
                    "why_similar": "MLOpsと運用に焦点、実践的",
                    "available_copies": 1,
                    "similarity_score": 0.78
                },
                {
                    "title": "Kaggle Grandmasterに学ぶ機械学習実践アプローチ",
                    "author": "...",
                    "why_similar": "実践的なML手法を解説",
                    "available_copies": 1,
                    "similarity_score": 0.72
                }
            ],
            "user_guidance": {
                "recommendation": "オプション2の代替書籍を読むことを推奨します",
                "reasoning": "『信頼性の高い機械学習』は類似度が高く、今すぐ借りられます"
            }
        }
    
    # 貸出処理...
    

書籍の存在確認では、見つからなかった事象に対してタイトルが違うかもしれないので再度検索しなおした方がよいという指針、実はこのタイトルなのではないかという代替案を提示することで、次にエージェントはどのような条件でリトライ処理をすればよいか明確になります。

▼存在確認の改善例

【改善前】
ユーザー: 「イシューからはじめよを借りたい」
エージェント: borrow_book("イシューからはじめよ")
→ {"error": "Not found"}
エージェント: 「その本は見つかりませんでした。」
ユーザー: 「正確なタイトルは?」(追加質問)
→ さらに2-3回のやり取りが必要

【改善後】
ユーザー: 「イシューからはじめよを借りたい」
エージェント: borrow_book("イシューからはじめよ")
→ 類似タイトル3件を含む詳細エラー
エージェント: 「『イシューからはじめよ』という書籍は見つかりませんでしたが、
以下のタイトルと一致する可能性があります:
1. 『イシューからはじめよ:知的生産の「シンプルな本質」』(一致度: 95%)
   - 著者:安宅和人
   - 貸出可能:2冊

こちらの本でしょうか?」
→ 1回のやり取りで解決
    

在庫の確認では、在庫が無かった時に対して関連のあるこれらの本なら借りられるよという代替案を示すことで、ユーザーの離脱なく借りたいという要求に対してダイレクトに答えることができます。

▼在庫確認の改善例

【改善前】
ユーザー: 「機械学習デザインパターンを借りたい」
エージェント: borrow_book("機械学習デザインパターン")
→ {"error": "Not available"}
エージェント: 「その本は貸出できません。」
ユーザー: 「いつ借りられるの?」(追加質問が必要)
エージェント: get_return_date("機械学習デザインパターン")
→ さらに1ツール呼び出し
ユーザー: 「代わりになる本は?」(さらに追加質問)
→ さらに1ツール呼び出し

【改善後】
ユーザー: 「機械学習デザインパターンを借りたい」
エージェント: borrow_book("機械学習デザインパターン")
→ 詳細なエラー情報を受信
エージェント: 「その本は現在貸出中で、10月15日(3日後)に返却予定です。
以下の選択肢があります:

【オプション1】この本を予約する
  - 返却され次第、自動的に貸出されます
  - 予想待ち時間:3日
  
【オプション2】類似の本を今すぐ借りる
  - 『信頼性の高い機械学習』(貸出可能)
    同じMLOpsトピック、実践的な内容
  - 『Kaggle Grandmasterに学ぶ機械学習実践アプローチ』(貸出可能)
    
【オプション3】借りている人に相談する
  - 山田花子さん(Data Science部門)
  - Slack: @yamada.hanako
  - 注:緊急の場合のみ推奨

どうされますか?」

→ 1回のツール呼び出しで、全ての情報と選択肢を提示
    

実際に設計していくにあたってこのような表を作成して整理していくとよいでしょう。

 

エラータイプ

必須情報

推奨代替案

追加提供すべき情報

book_not_found

類似タイトル3件

search_books()の使い方

購入リクエスト方法

not_available

返却予定日、借主情報

予約、代替書籍、借主連絡

類似書籍2-3件

invalid_category

有効なカテゴリ一覧

最も近いカテゴリ名

カテゴリ取得ツールの案内

invalid_parameter

パラメータの説明

正しい形式の例

よくある間違いリスト

permission_denied

必要な権限

権限リクエスト方法

代替手段(代理依頼など)

already_borrowed

現在の貸出情報

延長方法

返却期限リマインダー

 

まとめ

MCPのツールを実装するにあたって、トークン削減やより確度の高い実行のための注意点を説明しました。

MCPサーバー開発によって会社での生成AI活用をより一層高度なものにすることができると思います。その際にユーザビリティを意識して、使いやすいものを展開する必要がありますが、そのために何をすればよいか上記を参考にして作っていただけたら幸いです。

ここでは設計にフォーカスをしており、実装の過程での注意点はあげていません。実装するにあたって、細かなテストサイクルを用意し、そのテスト観点の整備・テストの実装・修正プランニングもLLMに任せてみて、AIのためのツール開発をAIにさせてみるのも良いでしょう。その際には実装観点のコンテキストに本記事の内容を入れてみると良いかもしれません。

ARISEでは生成AI活用を積極的に行っています。これからも生成AIについての記事を更新していきますので是非ご注目ください。

参考

 

 

 

ご質問・お問い合わせは
こちらよりお送りください
採用
ARISE analyticsとは

PAGE TOP