Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/troubleshooting/macos.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ You can run `biomcp` commands directly without a full installation using `uvx`.
```
- Test a search command (e.g., trial search):
```bash
uvx --from biomcp-python biomcp trial search --condition NSCLC --size 1 | head -n 5
uvx --from biomcp-python biomcp trial search --condition NSCLC | head -n 5
# Expected Output (NCT ID and Title will vary):
# # Record 1
# Nct Number: NCT0XXXXXXX
Expand Down
82 changes: 82 additions & 0 deletions example_scripts/mcp_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env -S uv --quiet run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "mcp",
# ]
# ///

# Scripts to reproduce this page:
# https://biomcp.org/mcp_integration/

import asyncio

from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
from mcp.types import TextContent


async def check_server():
# Run with pypi package
# server_params = StdioServerParameters(
# command="uvx",
# args=["--from", "biomcp-python", "biomcp", "run"],
# )

# Run with local code
server_params = StdioServerParameters(
command="python",
args=["-m", "biomcp", "run"],
)

async with (
stdio_client(server_params) as (read, write),
ClientSession(read, write) as session,
):
await session.initialize()

# list prompts
prompts = await session.list_prompts()
print("Available prompts:", prompts)

# list resources
resources = await session.list_resources()
print("Available resources:", resources)

# list tools
tool_result = await session.list_tools()
tools = tool_result.tools
print("Available tools:", tools)
assert len(tools) >= 9

# run tool
tool_name = "variant_details"
tool_args = {"variant_id": "rs113488022"}
result = await session.call_tool(tool_name, tool_args)
assert result.isError is False, f"Error: {result.content}"

# --- Assertions ---
# 1. Check the call was successful (not an error)
assert (
result.isError is False
), f"Tool call resulted in error: {result.content}"

# 2. Check there is content
assert result.content is not None
assert len(result.content) >= 1

# 3. Check the type of the first content block
content_block = result.content[0]
assert isinstance(content_block, TextContent)

markdown_output = content_block.text
# print(markdown_output)
assert isinstance(markdown_output, str)
assert "rs113488022" in markdown_output
assert "BRAF" in markdown_output
assert "Pathogenic" in markdown_output
print(f"Successfully called tool '{tool_name}' with args {tool_args}")


if __name__ == "__main__":
asyncio.run(check_server())
108 changes: 108 additions & 0 deletions example_scripts/python_sdk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env -S uv --quiet run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "biomcp-python",
# ]
# ///

# Scripts to reproduce this page:
# https://biomcp.org/python_sdk/

import asyncio
import json

from biomcp.trials.search import (
RecruitingStatus,
TrialPhase,
TrialQuery,
search_trials,
)
from biomcp.variants.getter import get_variant
from biomcp.variants.search import VariantQuery, search_variants


async def find_pathogenic_tp53():
# noinspection PyTypeChecker
query = VariantQuery(gene="TP53", significance="pathogenic", size=5)
# Get results as Markdown (default)
json_output_str = await search_variants(query, output_json=True)
data = json.loads(json_output_str)
assert len(data) == 5
for item in data:
clinvar = item.get("clinvar")
for rcv in clinvar.get("rcv", []):
assert "pathogenic" in rcv["clinical_significance"].lower()


async def get_braf_v600e_details():
variant_id = "chr7:g.140453136A>T" # BRAF V600E variant

# Get results as JSON string
json_output_str = await get_variant(variant_id, output_json=True)
data = json.loads(json_output_str)

# Process the variant data
assert data, "No data returned for BRAF V600E variant"
variant = data[0]
clinvar = variant.get("clinvar", {})
cosmic = variant.get("cosmic", {})
docm = variant.get("docm", {})

# Verify key variant details
assert clinvar.get("gene", {}).get("symbol") == "BRAF"
assert clinvar.get("chrom") == "7"
assert clinvar.get("cytogenic") == "7q34"
assert cosmic.get("cosmic_id") == "COSM476"
assert docm.get("aa_change") == "p.V600E"

# Verify HGVS coding variants
hgvs_coding = clinvar.get("hgvs", {}).get("coding", [])
assert len(hgvs_coding) >= 13
assert "NM_004333.6:c.1799T>A" in hgvs_coding


async def find_melanoma_trials():
query = TrialQuery(
conditions=["Melanoma"],
interventions=["Pembrolizumab"],
recruiting_status=RecruitingStatus.OPEN,
phase=TrialPhase.PHASE3,
)

# Get results as JSON string
json_output_str = await search_trials(query, output_json=True)
data = json.loads(json_output_str)

# Verify we got results
assert data, "No trials found"
assert len(data) >= 2, "Expected at least 2 melanoma trials"

# Verify first trial details (NCT05727904)
trial1 = data[0]
assert trial1["NCT Number"] == "NCT05727904"
assert "lifileucel" in trial1["Study Title"].lower()
assert trial1["Study Status"] == "RECRUITING"
assert trial1["Phases"] == "PHASE3"
assert int(trial1["Enrollment"]) == 670
assert "Melanoma" in trial1["Conditions"]
assert "Pembrolizumab" in trial1["Interventions"]

# Verify second trial details (NCT06697301)
trial2 = data[1]
assert trial2["NCT Number"] == "NCT06697301"
assert "EIK1001" in trial2["Study Title"]
assert trial2["Study Status"] == "RECRUITING"
assert "PHASE3" in trial2["Phases"]
assert int(trial2["Enrollment"]) == 740
assert trial2["Conditions"] == "Advanced Melanoma"


def run():
asyncio.run(find_pathogenic_tp53())
asyncio.run(get_braf_v600e_details())
asyncio.run(find_melanoma_trials())


if __name__ == "__main__":
run()
17 changes: 13 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "biomcp-python"
version = "0.1.0"
version = "0.1.1"
description = "Biomedical Model Context Protocol Server"
authors = [{ name = "Ian Maurer", email = "imaurer@gmail.com" }]
readme = "README.md"
Expand All @@ -16,7 +16,6 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"anyio>=4.8.0",
"certifi>=2025.1.31",
"diskcache>=5.6.3",
"httpx>=0.28.1",
Expand Down Expand Up @@ -61,8 +60,6 @@ build-backend = "setuptools.build_meta"
[tool.setuptools.package-data]
biomcp = ["resources/*.md"]



[project.scripts]
biomcp = "biomcp.__main__:main"

Expand Down Expand Up @@ -164,3 +161,15 @@ omit = [
"src/*/server.py",
"src/*/http_client.py",
]

[tool.deptry]
exclude = [
"example_scripts/python_sdk.py",
"venv",
".venv",
".direnv",
"tests",
".git",
"build",
"dist",
]
24 changes: 6 additions & 18 deletions src/biomcp/cli/server.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import signal
import sys
import asyncio

import anyio
import typer

from .. import logger, mcp_app
Expand All @@ -13,19 +11,9 @@
def run_server():
"""Run the BioMCP server with STDIO transport."""

# Use a simpler approach - just force exit on SIGINT
def handle_sigint(sig, frame):
typer.echo("\nShutting down server...")
sys.exit(0)
tools = asyncio.run(mcp_app.list_tools())
tools_str = ",".join(t.name for t in tools)
logger.info(f"Tools loaded: {tools_str}")

# Register only for SIGINT
signal.signal(signal.SIGINT, handle_sigint)

logger.info("Starting MCP server... (v0.0.1)")
try:
anyio.run(mcp_app.run_stdio_async)
logger.info("MCP server stopped gracefully.")
return 0
except Exception as e:
logger.error(f"Error running MCP server: {e}")
return 1
logger.info("Starting MCP server:")
mcp_app.run()
4 changes: 1 addition & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.