Bio-LLM MCP 에이전트 — Model Context Protocol로 실험 도구를 자율 조율하는 실전
이 편은 Phase 4 파일럿 트랙의 종결자입니다. 앞선 편들(01 임상 IE, 02 단백질 임베딩, 03 세포 분할, 11 Boltz-2 구조·친화도)에서 우리는 각각 하나의 도메인·하나의 파이프라인을 하드코어로 구축했습니다. 그런데 실전 R&D는 하나의 파이프라인만으로 끝나지 않습니다. 논문을 읽고, 서열을 조회하고, 구조를 찾고, 실험을 설계하고, 재현하고, 실패를 진단합니다. 이 편은 이 다중 도구 오케스트레이션을 Claude Agent가 표준화된 프로토콜(MCP)로 자율 조율하는 실전 파이프라인을 구축합니다.
📚 선수 편 권고 (강력 권고)
이 편은 AI×바이오 하드코어 심화 편의 트랙 종결자입니다. 진입 전에 DryBench의 다음 편들을 먼저 듣고·보시길 강력히 권고드립니다.
- DryBench ai-native #9 에이전트와 도구 사용
- DryBench ai-native #10 컨텍스트 윈도우 관리
- DryBench ai-native #14 Claude Code와 Cursor
선수 편 없이 진입할 경우, 이 편에서 다루는 에이전트 loop 원리, 컨텍스트 압축·요약 전략, Claude Code의 tool use 실전 패턴에 대한 재설명 없이 실전 코드부터 진행하므로 따라가기 어렵습니다.
우리 DryBench에서 이거 배웠잖아
DryBench ai-native #9에서 agent가 LLM + Tools + Loop + Memory 4요소로 정의되고 각 tool 호출이 함수 시그니처처럼 강제 스키마로 관리된다는 것을, #10에서 긴 세션에서 컨텍스트가 어떻게 압축·오프로드되는지를, #14에서 Claude Code가 이 모든 원리를 CLI에서 실전으로 구현하는 도구라는 것을 배웠습니다.
그런데 실전 바이오 R&D에서 tool을 하나하나 매번 새로 정의하고, 인증·재시도·타임아웃·응답 파싱을 각 프로젝트마다 다시 짜는 것은 매우 비효율적입니다. Anthropic이 2024년 11월 발표한 Model Context Protocol(MCP)은 이 반복을 표준화하기 위한 오픈 프로토콜입니다 [1]. MCP 서버 하나가 여러 클라이언트(Claude Desktop, Claude Code, Cursor 등)에게 동일한 tool 세트를 노출하고, 서버·클라이언트가 서로를 발견·호출·응답 처리하는 규격이 일관됩니다. 이 편은 그 규격을 바이오 도메인에 실전 적용합니다.
하드코어 문제 정의
실전 R&D 시나리오
한 연구자가 특정 질병 관련 새 유전자 후보를 얻었습니다. 그 유전자가 실제로 흥미로운지 판단하려면 최소 다음 작업을 해야 합니다.
- 유전자 서열을 NCBI BLAST에 던져 상동체를 찾는다.
- 상위 hit의 UniProt 정보를 조회해 기능을 확인한다.
- PubMed에서 관련 논문 최근 5년 치를 검색한다.
- 상위 논문의 method 섹션을 읽고 재현 가능한지 판단한다.
- 재현 가능하면 GitHub 저장소 clone → 환경 셋업 → 자신의 데이터로 adapt → 결과 검증.
기존 방식은 각 단계마다 사람이 브라우저·터미널·에디터를 오가며 처리했습니다. 이 편의 목표는 이 5단계 전체를 Claude Agent가 자율적으로 실행하고 결과를 구조화 리포트로 반환하는 것입니다. 그 과정에서 사람은 결정적 순간(예: "이 논문의 method 재현 시도할까요?")에만 개입.
MCP가 왜 필요한가
MCP 없이 tool use만 사용해도 위 워크플로우는 구현 가능합니다. 하지만 MCP를 쓰면:
- 재사용성: NCBI BLAST 서버를 한번 만들면 Claude Desktop, Claude Code, Cursor 등 모든 MCP 클라이언트가 즉시 사용.
- 표준 인증·에러·페이로드 규격: JSON-RPC 2.0 기반. 도메인마다 재발명 X.
- 에코시스템: 다른 사람이 만든 MCP 서버(예: 파일 시스템, GitHub, Slack)를 조합해 복합 에이전트 즉시 구축.
- 분리된 프로세스: 서버는 stdin/stdout 또는 SSE로 통신, 크래시가 클라이언트에 격리.
이 편의 목표 지표
- 위 5단계 전체를 하나의 Claude 세션에서 자율 실행: 사람 개입 3회 이하.
- NCBI BLAST · UniProt · PubMed MCP 서버: 오픈 커뮤니티 구현 활용 or 커스텀 최소 구현.
- 에러 복구: rate limit · 네트워크 실패 · 파싱 실패에 자동 재시도 + 백오프.
- 재현성: 세션 로그 · tool 호출 이력 · 최종 리포트가 JSON으로 저장되어 후속 검증 가능.
도구 스택과 인프라 요구
| 도구 | 역할 | 라이선스 |
|---|---|---|
mcp Python SDK (pip install mcp) | MCP 서버·클라이언트 표준 구현 | MIT (Anthropic 공식) |
Anthropic Claude API (anthropic) | LLM + tool use 오케스트레이터 | 상용 (사용량 과금) |
| Biopython | NCBI E-utilities 래퍼 | Biopython License |
requests | UniProt REST 클라이언트 | Apache 2.0 |
| Docker (선택) | MCP 서버 컨테이너화 배포 | Apache 2.0 |
| Anthropic Skills (선택) | 도메인 특화 스킬 등록 | Anthropic 서비스 |
인프라 요구:
- GPU 없이 실행 가능 (LLM은 API, tool은 웹 서비스).
- 로컬 CPU 2코어 · RAM 4GB 이상.
- 네트워크: NCBI · UniProt · PubMed API 호출 (rate limit 준수 필수).
학습자 재현 예상 비용: Claude API 요금표 기반 계산 [2]으로 시나리오 하나 완결 실행 약 5~20 USD (tool 반복 호출량에 따라). 각 공개 API(NCBI 3 req/s, UniProt 없음, PubMed 기본 무료) 준수 시 추가 비용 X.
파이프라인 실전 구현
전체 흐름:
Step 1. MCP 서버 최소 구현 — UniProt REST 예시
MCP 서버는 stdin/stdout으로 통신하는 JSON-RPC 2.0 프로세스입니다. Python SDK가 대부분 boilerplate를 처리합니다 [3].
"""uniprot_mcp_server.py — UniProt REST API를 MCP tool로 노출.
MCP 서버 하나가 tool 여러 개를 노출 가능. 여기서는 3개:- search_protein: 텍스트 쿼리로 단백질 검색- get_protein_details: accession으로 상세 정보- get_sequence: accession으로 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 accession으로 단백질 상세 정보 조회.", inputSchema={ "type": "object", "properties": {"accession": {"type": "string"}}, "required": ["accession"], }, ), Tool( name="get_sequence", description="UniProt accession의 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]: """tool 이름 dispatch. 실전에서는 각 함수 분리 권장.""" 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"Unknown tool: {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())Step 2. MCP 서버 두 번째 — PubMed 검색
"""pubmed_mcp_server.py — PubMed E-utilities MCP tool."""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"Unknown tool: {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())Step 3. NCBI BLAST MCP 서버 (요약)
BLAST는 응답 시간이 길어 async 폴링 패턴이 필요합니다.
"""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 timeout RID={rid}")
# MCP tool wrapper는 위 UniProt·PubMed 서버와 동일 패턴. 지면상 생략.Step 4. Claude Agent 클라이언트 — MCP 오케스트레이션
Claude Agent가 여러 MCP 서버를 동시 연결해 tool을 자율 호출합니다.
"""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 서버 하나 연결. 프로세스 spawn + tool list 조회.""" 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 tool use 스키마로 변환 for tool in tools_result.tools: self.all_tools.append({ "name": f"{name}__{tool.name}", # 서버 이름 prefix로 충돌 방지 "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: """자율 loop. 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"Unknown tool: {block.name}", "is_error": True, }) continue try: # MCP 서버 이름 prefix 제거 후 원본 tool 이름으로 호출 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"Tool 호출 실패: {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 tool(uniprot__*, pubmed__*, blast__*)을 자율적으로 호출해증거 기반으로 답변합니다.
원칙:1. 사실을 창작하지 않습니다. 모든 주장은 tool 호출 결과로 뒷받침되어야 합니다.2. 답변에는 반드시 출처(UniProt accession, PMID 등)를 병기합니다.3. tool 호출이 실패하면 대안 경로를 시도하되 사용자에게 실패 사실도 보고합니다.4. 불필요한 tool 호출을 최소화합니다 (컨텍스트·비용 절약)."""
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="Human TP53의 최근 5년 논문 상위 3편의 주요 발견을 요약하고, " "이 단백질의 UniProt 기능 주석과 비교해 새로운 통찰이 있는지 알려주세요.", system_prompt=SYSTEM_PROMPT, ) print("=== Final Answer ===") print(result["final_answer"]) print(f"\n=== Turns: {result['turns']} ===") print(f"=== Tool calls: {len(result['tool_log'])} ===") for entry in result["tool_log"]: print(f" - {entry['tool']}: {entry['input']}")
if __name__ == "__main__": asyncio.run(main())Step 5. 논문 재현 tool 흡수 (P-02 대체)
편 목차 §7-1에서 P-02(논문 재현 에이전트)를 이 편에 흡수한다고 결정했습니다. 재현 tool은 별도 MCP 서버로 제공합니다.
"""paper_repro_mcp_server.py — 논문 재현 tool 세트 (요약)."""
# tool 1: fetch_paper_pdf — DOI로 PDF fetch (오픈 액세스 우선)# tool 2: extract_method_section — PDF에서 method 섹션 텍스트 추출# tool 3: find_github_repo — 논문 본문·supplementary에서 GitHub URL 검색# tool 4: clone_repo — git clone (샌드박스 폴더에)# tool 5: setup_env — requirements.txt · conda env 자동 감지 · 설치 시도# tool 6: run_reproduction — 저장소의 표준 진입점(README·setup.py) 실행
# 각 tool은 위 UniProt·PubMed 서버와 동일 MCP 패턴.# 안전을 위해 clone/실행은 반드시 격리 컨테이너 안 (Docker sandbox).이 재현 서버 tool을 위 BioMCPAgent에 connect_server("paper_repro", ...)로 추가하면 논문 재현까지 자율 실행 loop이 완성됩니다.
Anthropic Skills 통합 (선택적 강화)
Anthropic Skills는 Claude Sonnet 계열에 도메인 특화 툴킷을 등록하는 방식입니다 [4]. 위 MCP 서버 조합을 하나의 "Bio Research" Skill로 패키징해 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 | 경험 baseline |
| Claude Agent + tool use (MCP 없이) | 동일 | 5~10분 | 3~10 USD | Anthropic tool use 벤치 [5] |
| Claude Agent + MCP (본 편) | 동일 | 3~7분 | 3~10 USD | MCPmed 논문 근거 [6] |
| GPT-4o + function calling | 동일 | 5~10분 | 5~15 USD | OpenAI docs [7] |
학습자 재현 예상 비용
- Claude API: 시나리오당 5~20 USD (turn 수 · tool 반복 · 결과 크기에 따라).
- MCP 서버 인프라: 로컬 실행 무료. 원격 배포 시 소형 VPS 월 5~10 USD.
- NCBI · UniProt · PubMed: 공개 무료 (rate limit 준수).
알려진 실패 케이스 3건 (커뮤니티·논문 수집형)
-
NCBI BLAST rate limit 초과
증상: BLAST QBlast을 짧은 시간에 여러 번 호출하면 429 or 임시 차단.
원인: NCBI 정책 초당 3 request 상한 (API key 등록 시 10 req/s).
회피: (a)time.sleep(1)최소 간격, (b) NCBI API key 등록 +api_key파라미터 전달, (c) 배치 검색은 밤·주말에 실행 권장.
출처: NCBI E-utilities documentation "usage guidelines" [8]. -
MCP tool 이름 충돌
증상: 여러 MCP 서버가 같은 tool 이름을 노출하면 클라이언트가 어느 것을 호출해야 할지 모름.
원인: MCP 스펙 상 tool 이름 유일성은 서버 내부에서만 보장.
회피: 위 코드처럼 클라이언트에서{server_name}__{tool_name}prefix로 재래핑.
출처: MCP GitHub Discussions — "tool name collision" [9]. -
Claude Agent가 잘못된 tool 호출 무한 loop
증상: 특정 태스크에서 Agent가 계속 같은 tool을 재호출하며 stop_reason="end_turn"에 도달 못함.
원인: (a) 시스템 프롬프트에 "언제 멈춰야 하는지" 조건이 불명확, (b) tool 응답이 항상 애매하게 부분 답변만 반환, (c) max_turns 상한 없음.
회피: (a)max_turns하드 상한 (본 코드는 20), (b) 시스템 프롬프트에 "3회 시도 후 실패하면 사용자에게 보고" 명시, (c) tool 호출 이력을 컨텍스트에 요약해 반복 감지.
출처: Anthropic Cookbook — agent loop patterns [10].
확장 아이디어
- 커스텀 MCP 서버 시리즈: PDB 구조 파일 fetch · ChEMBL 화합물 검색 · GEO transcriptomics · Ensembl variants 등. 편 15에서 이어짐.
- 다중 에이전트 협업: 리서치 에이전트 + 시각화 에이전트 + 리포트 작성 에이전트를 별도 세션으로 분리하고 MCP로 결과 공유.
- 원격 MCP 서버 배포: Cloudflare Workers · AWS Lambda · Docker Swarm 위에 MCP 서버 상시 가동해 여러 사용자가 공유.
- Wet-lab 통합: 실험 기기 API(예: liquid handler robot)를 MCP tool로 노출해 실험 계획 자동 실행.
다음 편
- 편 15
bio-mcp-server-suite: 이 편의 서버 3개를 확장해 PDB · ChEMBL · GEO 등 5~7개 커스텀 MCP 서버 시리즈로 구축. - 편 09
llm-vendor-benchmark: 같은 MCP tool 세트를 Claude · GPT-4o · Gemini에 물려 tool use 정확도 비교. - 편 10
med-llm-reproduction: MCP 기반 벤치 재현 자동화. - 편 01
clinical-notes-ie-llm: 편 01의 임상 IE 파이프라인을 MCP tool로 노출 → 이 편 에이전트가 자율 호출.
참고 문헌
- Anthropic. "Model Context Protocol." 공식 문서:
https://modelcontextprotocol.io// 발표 블로그:https://www.anthropic.com/news/model-context-protocol - Anthropic Claude API pricing:
https://www.anthropic.com/pricing - MCP Python SDK:
https://github.com/modelcontextprotocol/python-sdk - Anthropic Skills documentation:
https://docs.anthropic.com/en/docs/build-with-claude/skills - Anthropic Tool Use documentation:
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 function calling:
https://platform.openai.com/docs/guides/function-calling - NCBI E-utilities usage guidelines:
https://www.ncbi.nlm.nih.gov/books/NBK25497/ - MCP GitHub Discussions:
https://github.com/modelcontextprotocol/specification/discussions - Anthropic Cookbook (agent patterns):
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 wrapper:
https://biopython.org/wiki/EUtils - MCP official server registry:
https://github.com/modelcontextprotocol/servers - Claude Code (Anthropic official):
https://claude.com/product/claude-code