Bio-LLM MCP エージェント — モデルコンテキストプロトコルを用いた自律的な実験ツールオーケストレーションの実用的なワークフロー
このセクションは、フェーズ4パイロットトラックの結論です。前のセクション(01臨床IE、02タンパク質埋め込み、03細胞セグメンテーション、11ボルツ-2構造と親和性)で、各ドメインとパイプラインのハードコアな実装を構築しました。しかし、現実世界のR&Dは、単一のパイプラインで終わるわけではありません。論文の読解、配列のクエリ、構造の検索、実験の設計、結果の再現、および障害の診断が含まれます。このセクションでは、標準化されたプロトコル(MCP)を使用して、これらの複数のツールをClaudeエージェントが自律的にオーケストレーションする、実用的なワークフローを構築します。
📚 推奨される事前学習(強く推奨されます)
このセクションは、AI×Bioの高度なトラックの結論部分です。先に続く前に、以下のDryBenchセクションを確認することを強くお勧めします。
- DryBench ai-native #9 エージェントとツール利用
- DryBench ai-native #10 コンテキストウィンドウの管理
- DryBench ai-native #14 Claudeコードとカーソル
事前資料を確認せずにこのセクションの実用的なコードを理解するのは難しくなります。なぜなら、エージェントループの原理、コンテキストの圧縮/要約戦略、およびClaudeコードにおけるツール利用の実用的なパターンを再説明せずに進むからです。
DryBenchで学んだこと
DryBench ai-native #9では、エージェントはLLM + ツール + ループ + メモリという4つのコンポーネントで定義され、各ツール呼び出しは、関数シグネチャに類似した強制的なスキーマによって管理されることを学びました。#10では、長いセッション中にコンテキストが圧縮およびオフロードされる方法を学びました。#14では、Claudeコードが、これらのすべての原理をCLIで実用的な方法で実装するツールであることを学びました。
しかし、現実世界のバイオR&Dでは、各ツールを個別に定義し、認証、リトライ、タイムアウト、およびレスポンスの解析を各プロジェクトで再実装するのは、非常に非効率的です。Anthropicが2024年11月に発表したモデルコンテキストプロトコル(MCP)は、この繰り返しを標準化するためのオープンプロトコルです[1]。単一のMCPサーバーは、同じセットのツールを複数のクライアント(Claude Desktop、Claude Code、Cursorなど)に公開し、サーバーとクライアントは、ツールを常に発見、呼び出し、およびレスポンスを処理します。このセクションでは、そのプロトコルをバイオドメインに実用的に適用します。
ハードコアな問題を定義する
実用的なR&Dシナリオ
研究者は、特定の疾患に関連する新しい候補遺伝子を特定しました。この遺伝子が本当に興味深いかどうかを判断するために、少なくとも次のタスクを実行する必要があります。
- 遺伝子配列をNCBI BLASTに送信して、相同遺伝子を見つけます。
- 最上位の結果のUniProt情報をクエリして、その機能をチェックします。
- 過去5年間の関連論文をPubMedで検索します。
- 最上位の論文の方法セクションを読み、再現可能かどうかを判断します。
- 再現可能な場合は、GitHubリポジトリをクローンし、環境をセットアップし、自分のデータに適応させ、結果を検証します。
過去には、このプロセスは、人がブラウザ、ターミナル、エディターの間を行き来しながら、各ステップを実行していました。このセクションの目標は、Claudeエージェントが5つのステップすべてを自律的に実行し、構造化されたレポートで結果を返すことです。このプロセスでは、人が重要な瞬間にのみ介入します(例:「この論文の方法を試してみるべきですか?」)。
なぜMCPが必要なのか?
MCPがなくても、上記のワークフローはツール利用のみで実装できます。ただし、MCPを使用すると、次の利点があります。
- 再利用性: NCBI BLASTサーバーを作成すると、Claude Desktop、Claude Code、およびCursorを含むすべてのMCPクライアントが、すぐにそれを使用できます。
- 標準化された認証、エラー、およびペイロード仕様: JSON-RPC 2.0に基づいています。各ドメインで再発明する必要はありません。
- エコシステム: 他の人が作成したMCPサーバー(ファイルシステム、GitHub、Slackなど)を組み合わせて、複雑なエージェントをすぐに構築できます。
- 分離されたプロセス: サーバーはstdin/stdoutまたはSSEを介して通信し、クラッシュをクライアントから分離します。
このセクションの目標メトリック
- 単一のClaudeセッションで5つのステップすべてを自律的に実行する: 最大3回の人間による介入。
- NCBI BLAST、UniProt、およびPubMed MCPサーバー: 既存のオープンコミュニティ実装を使用するか、最小限のカスタム実装を実装します。
- エラーからの回復: レート制限、ネットワーク障害、または解析の失敗の場合に、指数関数的なバックオフで自動的にリトライします。
- 再現性: セッションログ、ツール呼び出し履歴、および最終レポートは、後続の検証のためにJSON形式で保存されます。
ツールスタックとインフラストラクチャ要件
| ツール | 役割 | ライセンス |
|---|---|---|
mcp Python SDK (pip install mcp) | MCPサーバー/クライアントの標準実装 | MIT (Anthropic公式) |
Anthropic Claude API (anthropic) | LLM + ツール使用オーケストレーター | 商用 (使用量に応じた課金) |
| Biopython | NCBI E-utilitiesラッパー | Biopythonライセンス |
requests | UniProt RESTクライアント | Apache 2.0 |
| Docker (オプション) | MCPサーバーのコンテナ化デプロイメント | Apache 2.0 |
| Anthropic Skills (オプション) | ドメイン固有のスキル登録 | Anthropicサービス |
インフラストラクチャ要件:
- GPUなしでも実行可能(LLMはAPIベースであり、ツールはウェブサービスです)。
- ローカルCPUで、2コア、4GB以上のRAMが必要です。
- ネットワーク: NCBI、UniProt、PubMed APIへのアクセスが必要です(レート制限を遵守する必要があります)。
学習者のための概算コスト: Claude APIの価格[2]に基づくと、単一のシナリオを最初から最後まで実行するには、約5ドルから20ドルかかります(ツールの呼び出し回数によって異なります)。個別のパブリックAPI(NCBI 3リクエスト/秒、UniProt制限なし、PubMed基本無料)を遵守すれば、追加のコストは発生しません。
実用的なパイプラインの実装
全体の流れ:
ステップ1:最小限のMCPサーバーの実装 — UniProt RESTの例
MCPサーバーは、JSON-RPC 2.0プロセスであり、stdin / stdoutを介して通信します。 Python SDKは、ほとんどの定型処理を処理します[3]。
"""uniprot_mcp_server.py — UniProt REST APIをMCPツールとして公開します。
MCPサーバーは、複数のツールを公開できます。以下に3つの例を示します。- search_protein:テキストクエリを使用してタンパク質を検索します。- get_protein_details:アクセッション番号に基づいて詳細情報を取得します。- get_sequence:アクセッション番号に基づいてFASTA形式の配列を取得します。"""import asyncioimport jsonfrom typing import Any
import requestsfrom mcp.server import Serverfrom mcp.server.stdio import stdio_serverfrom mcp.types import Tool, TextContent
UNIPROT_BASE = "https://rest.uniprot.org"server = Server("uniprot-mcp")
@server.list_tools()async def list_tools() -> list[Tool]: return [ Tool( name="search_protein", description="UniProtでテキストクエリを使用してタンパク質を検索します。上位K件の結果を返します。", inputSchema={ "type": "object", "properties": { "query": {"type": "string", "description": "検索用語(例:'p53 human')"}, "limit": {"type": "integer", "default": 10}, }, "required": ["query"], }, ), Tool( name="get_protein_details", description="UniProtのアクセッション番号を使用して、タンパク質の詳細情報を取得します。", inputSchema={ "type": "object", "properties": {"accession": {"type": "string"}}, "required": ["accession"], }, ), Tool( name="get_sequence", description="UniProtアクセッション番号のFASTA配列を取得します。", inputSchema={ "type": "object", "properties": {"accession": {"type": "string"}}, "required": ["accession"], }, ), ]
@server.call_tool()async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: """ツール名に基づいて適切な関数にディスパッチします。実用的なシナリオでは、各関数を分離することをお勧めします。""" try: if name == "search_protein": result = _search_protein(arguments["query"], arguments.get("limit", 10)) elif name == "get_protein_details": result = _get_details(arguments["accession"]) elif name == "get_sequence": result = _get_sequence(arguments["accession"]) else: raise ValueError(f"不明なツール:{name}") return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))] except requests.RequestException as e: return [TextContent(type="text", text=json.dumps({"error": f"UniProt APIが失敗しました:{e}"}))] except Exception as e: return [TextContent(type="text", text=json.dumps({"error": str(e)}))]
def _search_protein(query: str, limit: int) -> dict: resp = requests.get( f"{UNIPROT_BASE}/uniprotkb/search", params={ "query": query, "format": "json", "size": limit, "fields": "accession,id,protein_name,organism_name,length", }, timeout=30, ) resp.raise_for_status() results = resp.json().get("results", []) return {"count": len(results), "results": results}
def _get_details(accession: str) -> dict: resp = requests.get( f"{UNIPROT_BASE}/uniprotkb/{accession}.json", timeout=30, ) resp.raise_for_status() data = resp.json() return { "accession": data.get("primaryAccession"), "protein_name": data.get("proteinDescription", {}), "organism": data.get("organism", {}), "length": data.get("sequence", {}).get("length"), "function": [ c["texts"][0]["value"] for c in data.get("comments", []) if c.get("commentType") == "FUNCTION" ], }
def _get_sequence(accession: str) -> dict: resp = requests.get(f"{UNIPROT_BASE}/uniprotkb/{accession}.fasta", timeout=30) resp.raise_for_status() lines = resp.text.strip().split("\n") return { "accession": accession, "header": lines[0], "sequence": "".join(lines[1:]), }
async def main(): async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__": asyncio.run(main())ステップ2:2番目のMCPサーバー — PubMed検索
"""pubmed_mcp_server.py — PubMed E-utilities用のMCPツール。"""import asyncioimport jsonfrom xml.etree import ElementTree as ET
import requestsfrom mcp.server import Serverfrom mcp.server.stdio import stdio_serverfrom mcp.types import Tool, TextContent
EUTILS_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"server = Server("pubmed-mcp")
@server.list_tools()async def list_tools() -> list[Tool]: return [ Tool( name="search_pubmed", description="検索語を使用して、PubMedで論文(PMID)のリストを検索します。", inputSchema={ "type": "object", "properties": { "query": {"type": "string"}, "max_results": {"type": "integer", "default": 20}, "date_range_years": {"type": "integer", "default": 5}, }, "required": ["query"], }, ), Tool( name="fetch_abstracts", description="PMIDのリストの詳細情報(抄録、著者、ジャーナル)を取得します。", inputSchema={ "type": "object", "properties": { "pmids": {"type": "array", "items": {"type": "string"}}, }, "required": ["pmids"], }, ), ]
@server.call_tool()async def call_tool(name: str, arguments: dict) -> list[TextContent]: try: if name == "search_pubmed": result = _search(arguments["query"], arguments.get("max_results", 20), arguments.get("date_range_years", 5)) elif name == "fetch_abstracts": result = _fetch(arguments["pmids"]) else: raise ValueError(f"不明なツール:{name}") return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))] except Exception as e: return [TextContent(type="text", text=json.dumps({"error": str(e)}))]
def _search(query: str, max_results: int, date_range_years: int) -> dict: esearch = requests.get( f"{EUTILS_BASE}/esearch.fcgi", params={ "db": "pubmed", "term": query, "retmax": max_results, "reldate": date_range_years * 365, "datetype": "pdat", "retmode": "json", }, timeout=30, ) esearch.raise_for_status() pmids = esearch.json().get("esearchresult", {}).get("idlist", []) return {"query": query, "count": len(pmids), "pmids": pmids}
def _fetch(pmids: list[str]) -> list[dict]: if not pmids: return [] efetch = requests.get( f"{EUTILS_BASE}/efetch.fcgi", params={"db": "pubmed", "id": ",".join(pmids), "rettype": "abstract", "retmode": "xml"}, timeout=60, ) efetch.raise_for_status() root = ET.fromstring(efetch.content) articles = [] for art in root.findall(".//PubmedArticle"): pmid = art.findtext(".//PMID") title = art.findtext(".//ArticleTitle") or "" abstract = " ".join(t.text or "" for t in art.findall(".//AbstractText")) authors = [ f"{a.findtext('LastName') or ''} {a.findtext('Initials') or ''}".strip() for a in art.findall(".//Author") ] journal = art.findtext(".//Journal/Title") or "" year = art.findtext(".//PubDate/Year") or "" articles.append({ "pmid": pmid, "title": title, "abstract": abstract, "authors": authors[:10], "journal": journal, "year": year, }) return articles
async def main(): async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__": asyncio.run(main())ステップ3:NCBI BLAST MCPサーバー(概要)
BLASTは応答時間が長いため、非同期ポーリングパターンが必要です。
"""blast_mcp_server.py — NCBI BLAST QBlast(概要、コアロジックのみ)。"""import timeimport reimport requests
BLAST_URL = "https://blast.ncbi.nlm.nih.gov/Blast.cgi"
def submit_blast(sequence: str, program: str = "blastp", database: str = "nr") -> str: """QBlast RIDを返します。後でポーリングする必要があります。""" resp = requests.post(BLAST_URL, data={ "CMD": "Put", "PROGRAM": program, "DATABASE": database, "QUERY": sequence, }, timeout=60) resp.raise_for_status() m = re.search(r"RID = (\S+)", resp.text) if not m: raise RuntimeError("QBlast RIDの解析に失敗しました") return m.group(1)
def poll_blast(rid: str, poll_interval: int = 30, max_wait: int = 600) -> str: """BLASTの結果を待ちます。完了したらXMLを返します。""" start = time.time() while time.time() - start < max_wait: resp = requests.get(BLAST_URL, params={"CMD": "Get", "RID": rid, "FORMAT_OBJECT": "SearchInfo"}, timeout=30) if "Status=READY" in resp.text: xml_resp = requests.get(BLAST_URL, params={"CMD": "Get", "RID": rid, "FORMAT_TYPE": "XML"}, timeout=60) xml_resp.raise_for_status() return xml_resp.text elif "Status=FAILED" in resp.text: raise RuntimeError(f"BLASTが失敗しました RID={rid}") time.sleep(poll_interval) raise TimeoutError(f"BLASTタイムアウト RID={rid}")
# MCPツールラッパーは、UniProtとPubMedサーバーと同じパターンに従います。コードは簡潔にするために省略されています。ステップ4:Claude Agentクライアント — MCPオーケストレーション
Claude Agentは、複数のMCPサーバーに同時に接続し、ツールを自律的に呼び出します。
"""bio_agent.py — Claude Agent + 複数のMCPサーバーのオーケストレーション。"""import asynciofrom contextlib import AsyncExitStack
import anthropicfrom mcp import ClientSession, StdioServerParametersfrom mcp.client.stdio import stdio_client
class BioMCPAgent: """複数のMCPサーバーをオーケストレーションするClaude Agent。"""
def __init__(self, model: str = "claude-sonnet-4-5", max_turns: int = 20): self.anthropic = anthropic.Anthropic() self.model = model self.max_turns = max_turns self.sessions: dict[str, ClientSession] = {} self.all_tools: list[dict] = [] self._exit_stack: AsyncExitStack | None = None
async def connect_server(self, name: str, command: str, args: list[str]) -> None: """MCPサーバーに接続します。プロセスを起動し、ツールリストを取得します。""" params = StdioServerParameters(command=command, args=args) stdio_transport = await self._exit_stack.enter_async_context(stdio_client(params)) session = await self._exit_stack.enter_async_context(ClientSession(*stdio_transport)) await session.initialize() tools_result = await session.list_tools() # Claudeのツール使用スキーマに変換します for tool in tools_result.tools: self.all_tools.append({ "name": f"{name}__{tool.name}", # サーバー名のプレフィックスを付けて競合を防ぎます "description": tool.description, "input_schema": tool.inputSchema, }) self.sessions[f"{name}__{tool.name}"] = session
async def __aenter__(self): self._exit_stack = AsyncExitStack() await self._exit_stack.__aenter__() return self
async def __aexit__(self, exc_type, exc_val, exc_tb): await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
async def run(self, user_query: str, system_prompt: str) -> dict: """自律的なループを実行します。Claudeは、stop_reason="end_turn"になるまで繰り返します。""" messages = [{"role": "user", "content": user_query}] tool_log = []
for turn in range(self.max_turns): response = self.anthropic.messages.create( model=self.model, max_tokens=4096, system=system_prompt, tools=self.all_tools, messages=messages, ) messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn": break if response.stop_reason != "tool_use": continue
# tool_useブロックを処理します tool_results = [] for block in response.content: if block.type != "tool_use": continue session = self.sessions.get(block.name) if session is None: tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": f"不明なツール:{block.name}", "is_error": True, }) continue try: # サーバー名のプレフィックスを削除した元の名前を使用してツールを呼び出します original_name = block.name.split("__", 1)[1] result = await session.call_tool(original_name, block.input) content_text = "\n".join( c.text for c in result.content if hasattr(c, "text") ) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": content_text[:10000], # コンテキストを保存します }) tool_log.append({ "tool": block.name, "input": block.input, "output_preview": content_text[:500], }) except Exception as e: tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": f"ツール呼び出しに失敗しました:{e}", "is_error": True, }) messages.append({"role": "user", "content": tool_results})
# 最終的なテキストを抽出します final_text = "" for block in response.content: if hasattr(block, "text"): final_text += block.text return {"final_answer": final_text, "tool_log": tool_log, "turns": turn + 1}
SYSTEM_PROMPT = """あなたはバイオインフォマティクスの研究アシスタントです。ユーザーのリクエストを理解し、必要なMCPツール(uniprot__、pubmed__、blast__)を自律的に呼び出して、証拠に基づいて回答します。
原則:1. 事実を捏造しないでください。すべての主張は、ツール呼び出しの結果によって裏付けられている必要があります。2. 常に、回答にソース(UniProtアクセッション番号、PMIDなど)を含めてください。3. ツールの呼び出しが失敗した場合は、代替方法を試みますが、ユーザーに失敗を通知します。4. 不要なツール呼び出しを最小限に抑えます(コンテキストとコストを節約するため)。"""
async def main(): async with BioMCPAgent() as agent: await agent.connect_server("uniprot", "python", ["uniprot_mcp_server.py"]) await agent.connect_server("pubmed", "python", ["pubmed_mcp_server.py"]) # await agent.connect_server("blast", "python", ["blast_mcp_server.py"])
result = await agent.run( user_query="最近の人間のTP53に関する上位3つの論文を要約し、それらをUniProtのこのタンパク質の機能注釈と比較して、新しい洞察があるかどうかを確認してください。", system_prompt=SYSTEM_PROMPT, ) print("=== 最終的な回答 ===") print(result["final_answer"]) print(f"\n=== ターン数:{result['turns']} ===") print(f"=== ツール呼び出し回数:{len(result['tool_log'])} ===") for entry in result["tool_log"]: print(f" - {entry['tool']}: {entry['input']}")
if __name__ == "__main__": asyncio.run(main())ステップ5:論文の再現ツールの吸収(セクション7-1のP-02を置き換え)
セクション7-1で、P-02(論文再現エージェント)をこのセクションに吸収することが決定されました。再現ツールは、別のMCPサーバーとして提供されます。
"""paper_repro_mcp_server.py — 論文の再現のためのツールのセット(概要)。"""
# ツール1:fetch_paper_pdf — DOIによるPDFの取得(オープンアクセスを優先します)# ツール2:extract_method_section — PDFから方法セクションのテキストを抽出します# ツール3:find_github_repo — 論文本文と補足資料からGitHub URLを検索します# ツール4:clone_repo — Gitクローン(サンドボックスフォルダー内)# ツール5:setup_env — requirements.txtまたはconda環境を検出し、インストールを試みます# ツール6:run_reproduction — リポジトリの標準のエントリポイント(READMEまたはsetup.py)を実行します
# 各ツールは、UniProtとPubMedサーバーと同じMCPパターンに従います。# 安全のため、クローンと実行は、別のコンテナー(Dockerサンドボックス)内で行う必要があります。BioMCPAgentにconnect_server("paper_repro", ...)を使用してこの再現サーバーツールを追加することで、論文の再現を含む自律的な実行ループが完了します。
Anthropic Skills の統合(オプションの拡張機能)
Anthropic Skills は、Claude Sonnet ファミリー内でドメイン固有のツールキットを登録する方法です [4]。上記の MCP サーバーの組み合わせを、単一の「バイオ研究」スキルとしてパッケージ化し、Claude Desktop または Claude Code CLI で簡単にアクティブにすることができます。
# skills/bio-research/skill.yaml
name: bio-research
version: 1.0.0
description: バイオインフォマティクス研究のための自律型エージェント。UniProt、PubMed、BLAST、および論文の再現を統合します。
mcp_servers:
- name: uniprot
command: python
args: [/opt/mcp/uniprot_mcp_server.py]
- name: pubmed
command: python
args: [/opt/mcp/pubmed_mcp_server.py]
- name: blast
command: python
args: [/opt/mcp/blast_mcp_server.py]
- name: paper_repro
command: python
args: [/opt/mcp/paper_repro_mcp_server.py]
system_prompt: |
あなたはバイオインフォマティクス研究のアシスタントです。...パフォーマンス、コスト、および既知の失敗事例
パフォーマンスの参考データ(公開されているデータに基づく)
| アプローチ | シナリオ | 時間 | コスト | ソース |
|---|---|---|---|---|
| 手動(人間、ブラウジング) | 5ステップの遺伝子研究 | 1-3時間 | 0 | 経験的なベースライン |
| Claude エージェント + ツール使用(MCP なし) | 同上 | 5-10 分 | 3-10 米ドル | Anthropic ツール使用ベンチマーク [5] |
| Claude エージェント + MCP(この記事) | 同上 | 3-7 分 | 3-10 米ドル | MCPmed 論文の証拠 [6] |
| GPT-4o + 関数呼び出し | 同上 | 5-10 分 | 5-15 米ドル | OpenAI ドキュメント [7] |
学習者向けの推定再現コスト
- Claude API:シナリオあたり 5〜20 米ドル(ターンの数、ツールの繰り返し回数、および結果のサイズによって異なります)。
- MCP サーバーのインフラストラクチャ:ローカルで実行する場合は無料。小規模な VPS 上にリモートでデプロイする場合は、月額約 5〜10 米ドル。
- NCBI、UniProt、PubMed:無料(レート制限の対象となります)。
3 つの既知の失敗事例(コミュニティと論文から収集)
-
NCBI BLAST のレート制限超過 症状:短い期間に BLAST QBlast を複数回呼び出すと、429 エラーまたは一時的なブロックが発生します。 原因:NCBI のポリシーでは、リクエストは 1 秒あたり 3 回(API キーが登録されている場合は 10 回/秒)に制限されています。 緩和策:(a)少なくとも
time.sleep(1)の間隔を設ける、(b)NCBI API キーを登録し、api_keyパラメーターとして渡す、(c)ピーク時間帯を避けてバッチ検索を実行することをお勧めします(夜間または週末)。 ソース:NCBI E-utilities ドキュメント「使用ガイドライン」[8]。 -
MCP ツール名の衝突 症状:複数の MCP サーバーが同じツール名を公開している場合、クライアントはどちらを呼び出すべきか分かりません。 原因:MCP 仕様では、サーバー内のツール名のユニーク性のみが保証されます。 緩和策:コードに示すように、クライアント側でツール名を
{server_name}__{tool_name}の形式でプレフィックスします。 ソース:MCP GitHub ディスカッション — 「ツール名の衝突」[9]。 -
Claude エージェントがツールを誤って呼び出し、無限ループに陥る 症状:特定のタスクで、エージェントが同じツールを継続的に再呼び出しし、
stop_reason="end_turn"に到達できません。 原因:(a)システムプロンプトで「いつ停止するか」の条件が明確に定義されていない、(b)ツールのレスポンスが常に部分的な回答のみを返す、(c)最大ターンの制限がない。 緩和策:(a)max_turnsにハードリミットを設定する(このコードでは 20)、(b)システムプロンプトで、エージェントが「3 回の失敗後にユーザーに報告する」ことを明示的に記述する、(c)ツールの呼び出し履歴をコンテキストに要約して、繰り返しを検出する。 ソース:Anthropic Cookbook — エージェントのループパターン [10]。
拡張のアイデア
- カスタム MCP サーバーシリーズ: PDB 構造ファイルを取得し、ChEMBL 化合物を検索し、GEO 転写データにアクセスし、Ensembl バリアントにアクセスするなど。パート 15 で続きます。
- マルチエージェントコラボレーション: 研究、可視化、およびレポート作成のエージェントを個別のセッションに分割し、MCP を使用して結果を共有します。
- リモート MCP サーバーのデプロイ: MCP サーバーを Cloudflare Workers、AWS Lambda、または Docker Swarm にホストして、複数のユーザー間で共有します。
- ウェットラボとの統合: 実験の計画を自動化するために、実験機器(例:液体ハンドラーロボット)の API を MCP ツールとして公開します。
次のパート
- パート 15
bio-mcp-server-suite: この記事のサーバーを拡張して、PDB、ChEMBL、および GEO を含む 5〜7 個のカスタム MCP サーバーのシリーズを構築します。 - パート 09
llm-vendor-benchmark: 同じ MCP ツールセットを使用して、Claude、GPT-4o、および Gemini でのツール使用の精度を比較します。 - パート 10
med-llm-reproduction: MCP ベースのベンチマークの再現を自動化します。 - パート 01
clinical-notes-ie-llm: パート 01 の臨床 IE パイプラインを MCP ツールとして公開し、この記事のエージェントがそれを自律的に呼び出せるようにします。
参考文献
- Anthropic. "Model Context Protocol"。公式ドキュメント:
https://modelcontextprotocol.io// アナウンスブログ:https://www.anthropic.com/news/model-context-protocol - Anthropic Claude API の料金:
https://www.anthropic.com/pricing - MCP Python SDK:
https://github.com/modelcontextprotocol/python-sdk - Anthropic Skills のドキュメント:
https://docs.anthropic.com/en/docs/build-with-claude/skills - Anthropic Tool Use のドキュメント:
https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview - Wagner F, Bittrich S. "MCPmed: a call for Model Context Protocol-enabled bioinformatics web services for LLM-driven discovery"。Briefings in Bioinformatics 2026。
https://academic.oup.com/bib/article/27/1/bbag076/8495038 - OpenAI の関数呼び出し:
https://platform.openai.com/docs/guides/function-calling - NCBI E-utilities の使用に関するガイドライン:
https://www.ncbi.nlm.nih.gov/books/NBK25497/ - MCP GitHub Discussions:
https://github.com/modelcontextprotocol/specification/discussions - Anthropic Cookbook(エージェントパターン):
https://github.com/anthropics/anthropic-cookbook - UniProt REST API:
https://www.uniprot.org/help/api - NCBI BLAST QBlast:
https://ncbi.github.io/blast-cloud/dev/api.html - Biopython E-utilities ラッパー:
https://biopython.org/wiki/EUtils - MCP 公式サーバーレジストリ:
https://github.com/modelcontextprotocol/servers - Claude Code(Anthropic 公式):
https://claude.com/product/claude-code