# Directory Structure ``` ├── .dockerignore ├── .gitignore ├── .python-version ├── assets │ └── nexonco-mcp-banner.jpg ├── Dockerfile ├── docs │ ├── claude-desktop-setup.md │ └── nanda-server-setup.md ├── LICENSE ├── pyproject.toml ├── README.md ├── src │ └── nexonco │ ├── api.py │ ├── query.py │ └── server.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 3.11 ``` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` .venv assets .git ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Python-generated files __pycache__/ *.py[oc] build/ dist/ wheels/ *.egg-info # Virtual environments .venv ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown  <div class="title-block" style="text-align: center;" align="center"> <b>Nexonco</b> by <a href="https://www.nexgene.ai">Nexgene Research</a> is an <a href="https://github.com/modelcontextprotocol">MCP</a> server for accessing clinical evidence from the CIViC (Clinical Interpretation of Variants in Cancer) database. It enables fast, flexible search across variants, diseases, drugs, and phenotypes to support precision oncology. </div> <br> <div class="title-block" style="text-align: center;" align="center"> [](https://pypi.org/project/nexonco-mcp) [](https://github.com/aidecentralized/nanda-servers/tree/main/nexonco-mcp) [](https://github.com/Nexgene-Research/nexonco-mcp/blob/main/LICENSE) </div> ## Demo https://github.com/user-attachments/assets/02129685-5ba5-4b90-89e7-9d4a39986210 Watch full video here: [](https://youtu.be/1Mq8Hcb9V7o?si=jCbhqNabupaRiQWq) ## Setup ### Prerequisites - [uv](https://github.com/astral-sh/uv#installation) or Docker - Claude Desktop (for MCP integration) ### Setup Guides For detailed setup instructions, refer to the following documentation: - **NANDA Host Setup** See `docs/nanda-server-setup.md` for backend configuration and local registration of the NANDA Server. - **Claude Desktop Setup** See `docs/claude-desktop-setup.md` for guidance on configuring the local development environment and MCP integration. These guides include all required steps, environment configurations, and usage notes to get up and running. ## Tool List `search_clinical_evidence`: A MCP tool for querying clinical evidence data that returns formatted reports. ### Input Schema The tool accepts the following optional parameters: - **`disease_name` (str)**: Filter by disease (e.g., "Lung Non-small Cell Carcinoma"). - **`therapy_name` (str)**: Filter by therapy or drug (e.g., "Cetuximab"). - **`molecular_profile_name` (str)**: Filter by gene or variant (e.g., "EGFR L858R"). - **`phenotype_name` (str)**: Filter by phenotype (e.g., "Chest Pain"). - **`evidence_type` (str)**: Filter by evidence type (e.g., "PREDICTIVE", "DIAGNOSTIC"). - **`evidence_direction` (str)**: Filter by evidence direction (e.g., "SUPPORTS"). - **`filter_strong_evidence` (bool)**: If `True`, only includes evidence with a rating > 3 (max 5). ### Output The tool returns a formatted string with four sections: 1. **Summary Statistics**: - Total evidence items - Average evidence rating - Top 3 diseases, genes, variants, therapies, and phenotypes (with counts) 2. **Top 10 Evidence Entries**: - Lists the highest-rated evidence items with details like disease, phenotype, gene/variant, therapy, description, type, direction, and rating. 3. **Sources & Citations**: - Citations and URLs for the sources of the top 10 evidence entries. 4. **Disclaimer**: - A note stating the tool is for research purposes only, not medical advice. ## Sample Usage - "Find predictive evidence for colorectal cancer therapies involving KRAS mutations." - "Are there studies on Imatinib for leukemia?" - "What therapies are linked to pancreatic cancer evidence?" ## Acknowledgements - [Model Context Protocol](https://github.com/modelcontextprotocol/python-sdk) - [NANDA: The Internet of AI Agents](https://nanda.media.mit.edu/) - [CIViC - Clinical Interpretation of Variants in Cancer](https://civicdb.org) ## License This project is licensed under the MIT License - see the <a href="https://github.com/Nexgene-Research/nexonco-mcp/blob/main/LICENSE">LICENSE</a> file for details. ## Disclaimer ⚠️ This tool is intended exclusively for research purposes. It is not a substitute for professional medical advice, diagnosis, or treatment. ## Contributors - Obada Qasem (@obadaqasem), [Nexgene AI](https://www.nexgene.ai) - Kutsal Ozkurt (@Goodsea), [Nexgene AI](https://www.nexgene.ai) ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml [project] name = "nexonco-mcp" version = "0.1.17" description = "An advanced MCP Server for accessing and analyzing clinical evidence data, with flexible search options to support precision medicine and oncology research." readme = "README.md" requires-python = ">=3.11" dependencies = [ "mcp[cli]==1.6.0", "pandas>=2.2.3", "requests>=2.32.3", "starlette>=0.46.1", "uvicorn>=0.34.0", ] [project.scripts] nexonco = "nexonco.server:main" ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Use the official uv base image with Python 3.11 FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim # Set working directory inside the container WORKDIR /app # Set environment variables for uv behavior ENV UV_COMPILE_BYTECODE=1 \ UV_LINK_MODE=copy # Copy project files into the container ADD . /app # Create and activate a virtual environment using uv RUN uv venv \ && uv pip install -e . # Expose the port your app listens on EXPOSE 8080 # Run the application via uv script execution CMD ["uv", "run", "nexonco", "--transport", "sse"] ``` -------------------------------------------------------------------------------- /docs/claude-desktop-setup.md: -------------------------------------------------------------------------------- ```markdown # Claude Desktop MCP Configuration Guide ## Overview This guide explains how to configure Claude Desktop to use the Nexonco MCP server. The Model Context Protocol (MCP) allows Claude to interact with external AI models and tools. Claude Desktop MCP server must be launched with STDIO transport. ## Configuration Steps ### Prerequisites - [uv](https://github.com/astral-sh/uv#installation) - Claude Desktop (for MCP integration) ### 1. Locate Configuration File The configuration file location depends on your operating system: - **macOS**: ``` ~/Library/Application Support/Claude/claude_desktop_config.json ``` - **Windows**: ``` %APPDATA%\Claude\claude_desktop_config.json ``` - **Linux**: ``` ~/.config/Claude/claude_desktop_config.json ``` ### 2. Edit Configuration 1. Open the configuration file in a text editor 2. Add or update the mcpServers section: ```json { "mcpServers": { "nexonco": { "command": "uv", "args": ["run", "--with", "nexonco-mcp", "nexonco" ] } } } ``` ### 3. Verify Setup 1. Save the configuration file 2. Restart Claude Desktop completely 3. Test the connection by asking Claude: `Find predictive evidence for colorectal cancer therapies involving KRAS mutations.` ``` -------------------------------------------------------------------------------- /docs/nanda-server-setup.md: -------------------------------------------------------------------------------- ```markdown # NANDA MCP Configuration Guide ## Overview This guide explains how to configure [NANDA, The Internet of Agents](https://nanda.media.mit.edu/) Server to use the Nexonco MCP server. The Model Context Protocol (MCP) allows [NANDA Host](https://host.nanda-registry.com/) to interact with external AI models and tools. NANDA server must be launched with Server-Sent Events (SSE) transport. ## Configuration Steps ### Prerequisites - Docker (for Method 1) - [uv](https://github.com/astral-sh/uv#installation) (for Method 2) ### 1. Download or clone the `nexonco-mcp` GitHub repository ### 2. Build and Run `Nexonco` NANDA server #### <b>Method 1</b>: Run with Docker (Recommended) > Requires: Docker 1. **Build the image:** ```bash docker build -t nexonco-mcp . ``` 2. **Run the container:** ```bash docker run -p 8080:8080 nexonco-mcp ``` #### <b>Method 2</b>: Run with `uv` > Requires: [`uv`](https://github.com/astral-sh/uv#installation) ```bash uv run --with nexonco-mcp nexonco --transport sse ``` ### 3. Register `Nexonco` Server to NANDA-Host - Go to [NANDA Host](https://host.nanda-registry.com/) - Open `Settings > Nanda Servers > Add New Server` - Fill the informations - Server ID: nexonco-local - Server Name: Nexonco - Server URL: 127.0.0.1:8080 - Add Server ### 4. Verify Setup 1. Test the connection by asking NANDA Host: `Find predictive evidence for colorectal cancer therapies involving KRAS mutations.` ``` -------------------------------------------------------------------------------- /src/nexonco/query.py: -------------------------------------------------------------------------------- ```python EVIDENCE_BROWSE_QUERY = """ query EvidenceBrowse($first: Int, $last: Int, $before: String, $after: String, $diseaseName: String, $therapyName: String, $id: Int, $description: String, $evidenceLevel: EvidenceLevel, $evidenceDirection: EvidenceDirection, $significance: EvidenceSignificance, $evidenceType: EvidenceType, $rating: Int, $variantOrigin: VariantOrigin, $variantId: Int, $molecularProfileId: Int, $assertionId: Int, $organizationId: [Int!], $includeSubgroups: Boolean, $userId: Int, $sortBy: EvidenceSort, $phenotypeId: Int, $diseaseId: Int, $therapyId: Int, $sourceId: Int, $clinicalTrialId: Int, $molecularProfileName: String, $status: EvidenceStatusFilter) { evidenceItems( first: $first last: $last before: $before after: $after diseaseName: $diseaseName therapyName: $therapyName id: $id description: $description evidenceLevel: $evidenceLevel evidenceDirection: $evidenceDirection significance: $significance evidenceType: $evidenceType evidenceRating: $rating variantOrigin: $variantOrigin variantId: $variantId molecularProfileId: $molecularProfileId assertionId: $assertionId organization: {ids: $organizationId, includeSubgroups: $includeSubgroups} userId: $userId phenotypeId: $phenotypeId diseaseId: $diseaseId therapyId: $therapyId sourceId: $sourceId clinicalTrialId: $clinicalTrialId molecularProfileName: $molecularProfileName status: $status sortBy: $sortBy ) { totalCount pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { node { ...EvidenceGridFields } } } } fragment EvidenceGridFields on EvidenceItem { id name disease { id name } therapies { id name } molecularProfile { id name parsedName { ...MolecularProfileParsedName } } status description evidenceType evidenceDirection evidenceRating } fragment MolecularProfileParsedName on MolecularProfileSegment { ... on MolecularProfileTextSegment { text } ... on Feature { id name } ... on Variant { id name } } """ BROWSE_PHENOTYPES_QUERY = """ query BrowsePhenotypes($phenotypeName: String) { browsePhenotypes(name: $phenotypeName, sortBy: {direction: DESC, column: EVIDENCE_ITEM_COUNT}) { edges { node { id name evidenceCount } } } } """ EVIDENCE_SUMMARY_QUERY = """ query EvidenceSummary($evidenceId: Int!) { evidenceItem(id: $evidenceId) { source { citation sourceUrl } } } """ ``` -------------------------------------------------------------------------------- /src/nexonco/api.py: -------------------------------------------------------------------------------- ```python from concurrent.futures import ThreadPoolExecutor, as_completed import pandas as pd import requests from .query import ( BROWSE_PHENOTYPES_QUERY, EVIDENCE_BROWSE_QUERY, EVIDENCE_SUMMARY_QUERY, ) class CivicAPIClient: """ Client for interacting with the CIViC (Clinical Interpretation of Variants in Cancer) GraphQL API. Provides methods to browse phenotypes, retrieve evidence, and source details in bulk. """ def __init__(self, cookies=None): """ Initialize the CIViC API client. Args: cookies (dict, optional): Cookies for authenticated requests. """ self.base_url = "https://civicdb.org/api/graphql" self.cookies = cookies or {} self.headers = { "Accept": "application/json, text/plain, */*", "Cache-Control": "no-cache", } def browse_phenotype(self, phenotype_name=None): """ Retrieve phenotype information from the CIViC API. Args: phenotype_name (str, optional): Name of the phenotype to browse. Returns: dict: JSON response containing phenotype data. """ variables = {"phenotypeName": phenotype_name} payload = { "operationName": "BrowsePhenotypes", "variables": variables, "query": BROWSE_PHENOTYPES_QUERY, } result = self._send_request(payload) return result["data"]["browsePhenotypes"]["edges"][0]["node"] def get_sources(self, evidence_id_list): """ Fetch source information for multiple evidence items in parallel. Args: evidence_id_list (list of int): List of evidence IDs. Returns: list of dict: List of source information for each evidence item. """ payloads = [ { "operationName": "EvidenceSummary", "variables": {"evidenceId": eid}, "query": EVIDENCE_SUMMARY_QUERY, } for eid in evidence_id_list ] results = self._send_parallel_requests(payloads) return [res["data"]["evidenceItem"]["source"] for res in results] def search_evidence( self, disease_name=None, therapy_name=None, molecular_profile_name=None, phenotype_name=None, filter_strong_evidence=False, evidence_type=None, evidence_direction=None, ): """ Search for evidence items based on filters like disease, therapy, and molecular profile. Args: disease_name (str, optional): Disease name to filter. therapy_name (str, optional): Therapy name to filter. molecular_profile_name (str, optional): Molecular profile name to filter. phenotype_name (str, optional): Phenotype name to filter. filter_strong_evidence (bool): Whether to include only strong evidence (rating > 3). evidence_type (str, optional): Type of evidence ("PREDICTIVE" or "DIAGNOSTIC" or "PROGNOSTIC" or "PREDISPOSING" or "FUNCTIONAL"). evidence_direction (str, optional): Direction of evidence (SUPPORTS or DOES_NOT_SUPPORT). Returns: pd.DataFrame: DataFrame containing filtered evidence items and source information. """ variables = {"sortBy": {"column": "EVIDENCE_RATING", "direction": "DESC"}} if evidence_type in [ "PREDICTIVE", "DIAGNOSTIC", "PROGNOSTIC", "PREDISPOSING", "FUNCTIONAL", ]: variables["evidenceType"] = evidence_type if evidence_direction in ["SUPPORTS", "DOES_NOT_SUPPORT"]: variables["evidenceDirection"] = evidence_direction variables["status"] = "ACCEPTED" if filter_strong_evidence else "NON_REJECTED" if disease_name: variables["diseaseName"] = disease_name if therapy_name: variables["therapyName"] = therapy_name if molecular_profile_name: variables["molecularProfileName"] = molecular_profile_name phenotype_data = {"id": None, "name": None} if phenotype_name: phenotype_data = self.browse_phenotype(phenotype_name) variables["phenotypeId"] = phenotype_data["id"] payload = { "operationName": "EvidenceBrowse", "variables": variables, "query": EVIDENCE_BROWSE_QUERY, } results = self._send_request(payload) results = results["data"]["evidenceItems"]["edges"] data = [] for entry in results: result = entry["node"] if filter_strong_evidence and entry["evidenceRating"] <= 3: continue evidence = { "id": result["id"], "name": result["name"], "disease_id": result["disease"]["id"], "disease_name": result["disease"]["name"], "therapy_ids": "+".join( [str(therapy["id"]) for therapy in result["therapies"]] ), "therapy_names": "+".join( [therapy["name"] for therapy in result["therapies"]] ), "molecular_profile_id": result["molecularProfile"]["id"], "molecular_profile_name": result["molecularProfile"]["name"], "gene_id": result["molecularProfile"]["parsedName"][0]["id"], "gene_name": result["molecularProfile"]["parsedName"][0]["name"], "variant_id": result["molecularProfile"]["parsedName"][1]["id"], "variant_name": result["molecularProfile"]["parsedName"][1]["name"], "phenotype_id": phenotype_data["id"], "phenotype_name": phenotype_data["name"], "description": result["description"], "evidence_type": result["evidenceType"], "evidence_direction": result["evidenceDirection"], "evidence_rating": result["evidenceRating"], } data.append(evidence) df = pd.DataFrame(data) df = df.dropna(subset=["evidence_rating"]) return df def _send_request(self, payload): """ Internal method to send a single request to the CIViC API. Args: payload (dict): GraphQL query payload. Returns: dict: Parsed JSON response from the API. """ response = requests.post( self.base_url, headers=self.headers, cookies=self.cookies, json=payload ) # Raise exception for HTTP errors response.raise_for_status() return response.json() def _send_parallel_requests(self, payloads, max_workers=12): """ Internal method to send multiple GraphQL requests concurrently. Args: payloads (list): List of GraphQL payloads. max_workers (int): Number of concurrent threads (default 12). Returns: list of dict: List of API responses for each payload. """ results = [] def send(payload): return self._send_request(payload) with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_payload = {executor.submit(send, p): p for p in payloads} for future in as_completed(future_to_payload): try: result = future.result() results.append(result) except Exception as e: results.append( {"error": str(e), "payload": future_to_payload[future]} ) return results def example_usage(): """Example of how to use the CivicAPIClient.""" import json client = CivicAPIClient() results = client.search_evidence( disease_name="cancer", therapy_name="ce", molecular_profile_name="egfr" ) print(results) # print(json.dumps(client.browse_phenotype("pain"), indent=2)) # print(json.dumps(client.get_sources([1572, 1058, 7096]), indent=2)) if __name__ == "__main__": example_usage() ``` -------------------------------------------------------------------------------- /src/nexonco/server.py: -------------------------------------------------------------------------------- ```python from collections import Counter from typing import Optional import pandas as pd import uvicorn from mcp.server import Server from mcp.server.fastmcp import FastMCP from mcp.server.sse import SseServerTransport from pydantic import Field from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse from starlette.routing import Mount, Route from .api import CivicAPIClient API_VERSION = "0.1.17" BUILD_TIMESTAMP = "2025-08-12" mcp = FastMCP( name="nexonco", description="An advanced MCP Server for accessing and analyzing clinical evidence data, with flexible search options to support precision medicine and oncology research.", version=API_VERSION, ) @mcp.tool( name="search_clinical_evidence", description=( "Perform a flexible search for clinical evidence using combinations of filters such as disease, therapy, " "molecular profile, phenotype, evidence type, and direction. This flexible search system allows you to tailor " "your query based on the data needed for research or clinical decision-making. It returns a detailed report that " "includes summary statistics, a top 10 evidence listing, citation sources, and a disclaimer." ), ) def search_clinical_evidence( disease_name: Optional[str] = Field( default="", description="Name of the disease to filter evidence by (e.g., 'Von Hippel-Lindau Disease', 'Lung Non-small Cell Carcinoma', 'Colorectal Cancer', 'Chronic Myeloid Leukemia', 'Glioblastoma'..). Case-insensitive and optional.", ), therapy_name: Optional[str] = Field( default="", description="Therapy or drug name involved in the evidence (e.g., 'Cetuximab', 'Imatinib', 'trastuzumab', 'Lapatinib'..). Optional.", ), molecular_profile_name: Optional[str] = Field( default="", description="Molecular profile or gene name or variant name (e.g., 'EGFR L858R', 'BRAF V600E', 'KRAS', 'PIK3CA'..). Optional.", ), phenotype_name: Optional[str] = Field( default="", description="Name of the phenotype or histological subtype (e.g., 'Hemangioblastoma', 'Renal cell carcinoma', 'Retinal capillary hemangioma', 'Pancreatic cysts', 'Childhood onset'..). Optional.", ), evidence_type: Optional[str] = Field( default="", description="Evidence classification: 'PREDICTIVE', 'DIAGNOSTIC', 'PROGNOSTIC', 'PREDISPOSING', or 'FUNCTIONAL'. Optional.", ), evidence_direction: Optional[str] = Field( default="", description="Direction of the evidence: 'SUPPORTS' or 'DOES_NOT_SUPPORT'. Indicates if the evidence favors the association.", ), filter_strong_evidence: bool = Field( default=False, description="If set to True, only evidence with a rating above 3 will be included, indicating high-confidence evidence. However, the number of returned evidence items may be quite low.", ), ) -> str: """ Query clinical evidence records using flexible combinations of disease, therapy, molecular profile, phenotype, and other evidence characteristics. Returns a formatted report containing a summary of findings, most common genes and therapies, and highlights of top-ranked evidence entries including source URLs and citations. This tool is designed to streamline evidence exploration in precision oncology by adapting to various research or clinical inquiry contexts. Returns: str: A human-readable report summarizing relevant evidence, key statistics, and literature references. """ client = CivicAPIClient() disease_name = None if disease_name == "" else disease_name therapy_name = None if therapy_name == "" else therapy_name molecular_profile_name = ( None if molecular_profile_name == "" else molecular_profile_name ) phenotype_name = None if phenotype_name == "" else phenotype_name evidence_type = None if evidence_type == "" else evidence_type evidence_direction = None if evidence_direction == "" else evidence_direction df: pd.DataFrame = client.search_evidence( disease_name=disease_name, therapy_name=therapy_name, molecular_profile_name=molecular_profile_name, phenotype_name=phenotype_name, evidence_type=evidence_type, evidence_direction=evidence_direction, filter_strong_evidence=filter_strong_evidence, ) if df.empty: return "🔍 No evidence found for the specified filters." # --------------------------------- # 1. Summary Statistics Section # --------------------------------- total_items = len(df) avg_rating = df["evidence_rating"].mean() # Frequency counters for each key attribute disease_counter = Counter(df["disease_name"].dropna()) gene_counter = Counter(df["gene_name"].dropna()) variant_counter = Counter(df["variant_name"].dropna()) therapy_counter = Counter(df["therapy_names"].dropna()) phenotype_counter = Counter(df["phenotype_name"].dropna()) # Prepare top-3 summary for each attribute def format_top(counter: Counter) -> str: return ( ", ".join(f"{item} ({count})" for item, count in counter.most_common(3)) if counter else "N/A" ) top_diseases = format_top(disease_counter) top_genes = format_top(gene_counter) top_variants = format_top(variant_counter) top_therapies = format_top(therapy_counter) top_phenotypes = format_top(phenotype_counter) stats_section = ( f"📊 **Summary Statistics**\n" f"- Total Evidence Items: {total_items}\n" f"- Average Evidence Rating: {avg_rating:.2f}\n" f"- Top Diseases: {top_diseases}\n" f"- Top Genes: {top_genes}\n" f"- Top Variants: {top_variants}\n" f"- Top Therapies: {top_therapies}\n" f"- Top Phenotypes: {top_phenotypes}\n" ) # --------------------------------- # 2. Top 10 Evidence Listing Section # --------------------------------- top_evidences = df.sort_values(by="evidence_rating", ascending=False).head(10) evidence_section = "📌 **Top 10 Evidence Entries**\n" for _, row in top_evidences.iterrows(): evidence_section += ( f"\n🔹 **{row.get('evidence_type', 'N/A')} ({row.get('evidence_direction', 'N/A')})** | Rating: {row.get('evidence_rating', 'N/A')}\n" f"- Disease: {row.get('disease_name', 'N/A')}\n" f"- Phenotype: {row.get('phenotype_name', 'N/A')}\n" f"- Gene/Variant: {row.get('gene_name', 'N/A')} / {row.get('variant_name', 'N/A')}\n" f"- Therapy: {row.get('therapy_names', 'N/A')}\n" f"- Description: {row.get('description', 'N/A')}\n" ) # --------------------------------- # 3. Sources & Citations Section # --------------------------------- citation_section = "🔗 **Sources & Citations**\n" sources = client.get_sources(top_evidences["id"].tolist()) for _, row in pd.DataFrame(sources).iterrows(): citation_section += ( f"• {row.get('citation', 'N/A')} - {row.get('sourceUrl', 'N/A')}\n" ) # --------------------------------- # 4. Disclaimer Section # --------------------------------- disclaimer = "\n⚠️ **Disclaimer:** This tool is intended exclusively for research purposes. It is not a substitute for professional medical advice, diagnosis, or treatment." # --------------------------------- # Combine All Sections into Final Report # --------------------------------- final_report = ( f"{stats_section}\n" f"{evidence_section}\n" f"{citation_section}\n" f"{disclaimer}" ) return final_report async def healthcheck(request: Request) -> JSONResponse: return JSONResponse({"status": "ok", "message": "Server is healthy."}) async def version(request): return JSONResponse({"version": API_VERSION, "build": BUILD_TIMESTAMP}) async def homepage(request: Request) -> HTMLResponse: html_content = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Nexonco MCP Server</title> <style> :root { --primary: #0f172a; --secondary: #0369a1; --accent: #0284c7; --light-blue: #e0f2fe; --light: #ffffff; --dark: #0f172a; --gray: #64748b; --light-gray: #f1f5f9; --border-radius: 4px; --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; color: var(--dark); background-color: #f8fafc; line-height: 1.6; } .container { max-width: 1100px; margin: 0 auto; padding: 0 20px; } header { background-color: #000; color: white; padding: 0; } .banner-container { width: 100%; max-width: 100%; margin: 0 auto; } .banner { width: 100%; max-width: 100%; height: auto; display: block; } .subtitle { text-align: center; padding: 0.75rem 0; background-color: #000; color: white; font-size: 0.95rem; letter-spacing: 0.5px; border-top: 1px solid rgba(255, 255, 255, 0.1); } .main-content { padding: 2.5rem 0; } .status-bar { display: flex; align-items: center; background-color: white; padding: 0.75rem 1.5rem; border-radius: var(--border-radius); margin-bottom: 2rem; box-shadow: var(--shadow); } .status-indicator { display: inline-flex; align-items: center; padding: 0.4rem 0.8rem; background-color: rgba(34, 197, 94, 0.1); border-radius: 2rem; color: #15803d; font-weight: 500; font-size: 0.9rem; margin-right: auto; } .status-indicator::before { content: ""; display: inline-block; width: 8px; height: 8px; background-color: #22c55e; border-radius: 50%; margin-right: 8px; } .modern-layout { display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; } .connection-section { grid-column: 1 / -1; margin-bottom: 1.5rem; } .full-width { grid-column: 1 / -1; } h2 { font-size: 1.25rem; margin: 0 0 1.25rem; color: var(--primary); font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; display: flex; align-items: center; } h2 svg { margin-right: 0.5rem; width: 20px; height: 20px; } p { margin-bottom: 1rem; color: #334155; font-size: 0.95rem; } strong { color: var(--primary); font-weight: 600; } .card { background-color: white; border-radius: var(--border-radius); padding: 1.75rem; margin-bottom: 1.5rem; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); } .connection-card { background-color: var(--light-blue); border-left: 4px solid var(--accent); } .about-card { height: 100%; } .api-card { background-color: #f8fafc; height: 100%; border-left: 4px solid #6366f1; } .query-list { list-style: none; margin-top: 1rem; } .query-item { padding: 0.85rem 1rem; margin-bottom: 0.75rem; background-color: #f1f5f9; border-radius: var(--border-radius); border-left: 3px solid var(--secondary); font-family: monospace; font-size: 0.9rem; color: #334155; } .button { background-color: var(--secondary); color: white; border: none; padding: 0.7rem 1.4rem; margin: 1rem 0.5rem 1rem 0; cursor: pointer; border-radius: var(--border-radius); font-weight: 500; transition: all 0.2s ease; text-transform: uppercase; letter-spacing: 0.5px; font-size: 0.85rem; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } .button:hover { background-color: var(--accent); transform: translateY(-1px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } .button.secondary { background-color: #e2e8f0; color: #334155; } .button.secondary:hover { background-color: #cbd5e1; } .status { border: 1px solid #e2e8f0; padding: 1rem; min-height: 20px; margin-top: 1rem; border-radius: var(--border-radius); color: #64748b; background-color: #f8fafc; font-family: monospace; } .status.connected { border-color: rgba(34, 197, 94, 0.3); color: #15803d; background-color: rgba(34, 197, 94, 0.05); } .status.error { border-color: rgba(239, 68, 68, 0.3); color: #b91c1c; background-color: rgba(239, 68, 68, 0.05); } code { font-family: monospace; background-color: rgba(99, 102, 241, 0.1); padding: 0.2rem 0.4rem; border-radius: 2px; font-size: 0.9rem; color: #4f46e5; } .disclaimer { background-color: rgba(239, 68, 68, 0.05); border-left: 3px solid #ef4444; padding: 1rem; margin: 1.25rem 0 0; border-radius: var(--border-radius); font-size: 0.9rem; color: #7f1d1d; } .api-details { display: flex; align-items: flex-start; margin-top: 0.5rem; } .api-icon { flex-shrink: 0; width: 40px; height: 40px; background-color: rgba(99, 102, 241, 0.1); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 1rem; color: #4f46e5; } .api-text { flex-grow: 1; } .links { display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap; } .links a { color: var(--secondary); text-decoration: none; display: inline-flex; align-items: center; transition: all 0.2s; font-size: 0.9rem; padding: 0.5rem 0.75rem; background-color: #f1f5f9; border-radius: var(--border-radius); border: 1px solid rgba(0, 0, 0, 0.05); } .links a:hover { background-color: #e2e8f0; transform: translateY(-1px); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .icon { width: 16px; height: 16px; margin-right: 8px; } footer { color: #64748b; padding: 1.5rem 0; margin-top: 2rem; text-align: center; font-size: 0.85rem; border-top: 1px solid #e2e8f0; background-color: white; } footer a { color: var(--secondary); text-decoration: none; } footer a:hover { text-decoration: underline; } @media (max-width: 768px) { .modern-layout { grid-template-columns: 1fr; } .links { gap: 0.75rem; } .links a { padding: 0.4rem 0.6rem; font-size: 0.85rem; } } </style> </head> <body> <header> <div class="banner-container"> <img src="https://github.com/user-attachments/assets/c2ec59e8-ff8c-40e1-b66d-17998fe67ecf" alt="Nexonco MCP Banner" class="banner"> </div> <div class="subtitle"> <div class="container"> Clinical Evidence Data Analysis for Precision Oncology </div> <br> <img src="http://nanda-registry.com/api/v1/verification/badge/c6284608-6bce-4417-a170-da6c1a117616" alt="Verified NANDA MCP Server" /> <img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License" /> </div> </header> <main class="main-content"> <div class="container"> <!-- Status Bar --> <div class="status-bar"> <div class="status-indicator">Server is running correctly</div> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect> <rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect> <line x1="6" y1="6" x2="6.01" y2="6"></line> <line x1="6" y1="18" x2="6.01" y2="18"></line> </svg> </div> <!-- Connection Section (Full Width) --> <div class="connection-section"> <div class="card connection-card"> <h2> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M5 12.55a11 11 0 0 1 14.08 0"></path> <path d="M1.42 9a16 16 0 0 1 21.16 0"></path> <path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path> <line x1="12" y1="20" x2="12.01" y2="20"></line> </svg> Server Connection </h2> <p>Test your connection to the MCP server:</p> <button id="connect-button" class="button">Connect to SSE</button> <div id="disconnect-container"></div> <div class="status" id="status">Connection status will appear here...</div> </div> </div> <!-- Modern Asymmetric Layout --> <div class="modern-layout"> <!-- About Card (Left Column - Wider) --> <div class="card about-card"> <h2> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <line x1="12" y1="16" x2="12" y2="12"></line> <line x1="12" y1="8" x2="12.01" y2="8"></line> </svg> About Nexonco </h2> <p><strong>Nexonco</strong> is an advanced MCP Server for accessing and analyzing clinical evidence data, with flexible search across variants, diseases, drugs, and phenotypes to support precision oncology research.</p> <div class="disclaimer"> <strong>⚠️ Disclaimer:</strong> This tool is intended exclusively for research purposes. It is not a substitute for professional medical advice, diagnosis, or treatment. </div> </div> <!-- API Card (Right Column - Narrower) --> <div class="card api-card"> <h2> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <polyline points="16 18 22 12 16 6"></polyline> <polyline points="8 6 2 12 8 18"></polyline> </svg> API Information </h2> <div class="api-details"> <div class="api-icon"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> </div> <div class="api-text"> <p>The server provides the <code>search_clinical_evidence</code> tool for querying clinical evidence data with filters for disease, therapy, molecular profile, phenotype, and evidence type.</p> </div> </div> </div> <!-- Sample Queries Card (Full Width) --> <div class="card full-width"> <h2> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <line x1="22" y1="2" x2="11" y2="13"></line> <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> </svg> Sample Queries </h2> <ul class="query-list"> <li class="query-item">Find predictive evidence for colorectal cancer therapies involving KRAS mutations.</li> <li class="query-item">Are there studies on Imatinib for leukemia?</li> <li class="query-item">What therapies are linked to pancreatic cancer evidence?</li> </ul> </div> <!-- Links Card (Full Width) --> <div class="card full-width"> <h2> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path> </svg> Links </h2> <div class="links"> <a href="https://www.nexgene.ai" target="_blank"> <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <line x1="2" y1="12" x2="22" y2="12"></line> <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> </svg> Nexgene AI </a> <a href="https://www.linkedin.com/company/nexgene" target="_blank"> <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path> <rect x="2" y="9" width="4" height="12"></rect> <circle cx="4" cy="4" r="2"></circle> </svg> LinkedIn </a> <a href="https://github.com/Nexgene-Research/nexonco-mcp" target="_blank"> <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path> </svg> GitHub </a> </div> </div> </div> </div> </main> <footer> <div class="container"> <p>Nexonco MCP Server © 2025 <a href="https://www.nexgene.ai" target="_blank">Nexgene AI</a> | MIT License</p> </div> </footer> <script> // Server connection functionality document.getElementById('connect-button').addEventListener('click', function() { const statusDiv = document.getElementById('status'); const disconnectContainer = document.getElementById('disconnect-container'); try { const eventSource = new EventSource('/sse'); statusDiv.textContent = 'Connecting...'; statusDiv.className = 'status'; eventSource.onopen = function() { statusDiv.textContent = 'Connected to SSE'; statusDiv.className = 'status connected'; }; eventSource.onerror = function() { statusDiv.textContent = 'Error connecting to SSE'; statusDiv.className = 'status error'; eventSource.close(); }; eventSource.onmessage = function(event) { statusDiv.textContent = 'Received: ' + event.data; statusDiv.className = 'status connected'; }; // Add a disconnect option disconnectContainer.innerHTML = ''; const disconnectButton = document.createElement('button'); disconnectButton.textContent = 'Disconnect'; disconnectButton.className = 'button secondary'; disconnectButton.addEventListener('click', function() { eventSource.close(); statusDiv.textContent = 'Disconnected'; statusDiv.className = 'status'; disconnectContainer.innerHTML = ''; }); disconnectContainer.appendChild(disconnectButton); } catch (e) { statusDiv.textContent = 'Error: ' + e.message; statusDiv.className = 'status error'; } }); </script> </body> </html> """ return HTMLResponse(html_content) def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette: """Create a Starlette application that can server the provied mcp server with SSE. This sets up the HTTP routes and SSE connection handling. """ # Create an SSE transport with a path for messages sse = SseServerTransport("/messages/") async def handle_sse(request: Request) -> None: async with sse.connect_sse( request.scope, request.receive, request._send, ) as (read_stream, write_stream): await mcp_server.run( read_stream, write_stream, mcp_server.create_initialization_options(), ) return Starlette( debug=debug, routes=[ Route("/", endpoint=homepage), Route("/health", endpoint=healthcheck), Route("/version", endpoint=version), Route("/sse", endpoint=handle_sse), Mount("/messages/", app=sse.handle_post_message), ], ) def main(): import argparse # Parse command-line arguments parser = argparse.ArgumentParser(description="Run MCP SSE-based server") parser.add_argument( "--transport", type=str, choices=["stdio", "sse"], default="stdio", help="Transport mechanism to use ('stdio' for Claude, 'sse' for NANDA)", ) parser.add_argument("--host", default="0.0.0.0", help="Host to bind to") parser.add_argument("--port", type=int, default=8080, help="Port to listen on") args = parser.parse_args() # Create and run the Starlette application if args.transport == "sse": mcp_server = mcp._mcp_server starlette_app = create_starlette_app(mcp_server, debug=True) uvicorn.run(starlette_app, host=args.host, port=args.port) else: mcp.run(transport="stdio") if __name__ == "__main__": main() ```