# 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: -------------------------------------------------------------------------------- ``` 1 | 3.11 2 | ``` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` 1 | .venv 2 | assets 3 | .git ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 |  2 | 3 | <div class="title-block" style="text-align: center;" align="center"> 4 | <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. 5 | </div> 6 | <br> 7 | 8 | <div class="title-block" style="text-align: center;" align="center"> 9 | 10 | [](https://pypi.org/project/nexonco-mcp) 11 | [](https://github.com/aidecentralized/nanda-servers/tree/main/nexonco-mcp) 12 | [](https://github.com/Nexgene-Research/nexonco-mcp/blob/main/LICENSE) 13 | </div> 14 | 15 | ## Demo 16 | 17 | https://github.com/user-attachments/assets/02129685-5ba5-4b90-89e7-9d4a39986210 18 | 19 | Watch full video here: [](https://youtu.be/1Mq8Hcb9V7o?si=jCbhqNabupaRiQWq) 20 | 21 | ## Setup 22 | 23 | ### Prerequisites 24 | 25 | - [uv](https://github.com/astral-sh/uv#installation) or Docker 26 | - Claude Desktop (for MCP integration) 27 | 28 | ### Setup Guides 29 | 30 | For detailed setup instructions, refer to the following documentation: 31 | 32 | - **NANDA Host Setup** 33 | See `docs/nanda-server-setup.md` for backend configuration and local registration of the NANDA Server. 34 | 35 | - **Claude Desktop Setup** 36 | See `docs/claude-desktop-setup.md` for guidance on configuring the local development environment and MCP integration. 37 | 38 | These guides include all required steps, environment configurations, and usage notes to get up and running. 39 | 40 | ## Tool List 41 | 42 | `search_clinical_evidence`: A MCP tool for querying clinical evidence data that returns formatted reports. 43 | 44 | ### Input Schema 45 | The tool accepts the following optional parameters: 46 | - **`disease_name` (str)**: Filter by disease (e.g., "Lung Non-small Cell Carcinoma"). 47 | - **`therapy_name` (str)**: Filter by therapy or drug (e.g., "Cetuximab"). 48 | - **`molecular_profile_name` (str)**: Filter by gene or variant (e.g., "EGFR L858R"). 49 | - **`phenotype_name` (str)**: Filter by phenotype (e.g., "Chest Pain"). 50 | - **`evidence_type` (str)**: Filter by evidence type (e.g., "PREDICTIVE", "DIAGNOSTIC"). 51 | - **`evidence_direction` (str)**: Filter by evidence direction (e.g., "SUPPORTS"). 52 | - **`filter_strong_evidence` (bool)**: If `True`, only includes evidence with a rating > 3 (max 5). 53 | 54 | ### Output 55 | The tool returns a formatted string with four sections: 56 | 1. **Summary Statistics**: 57 | - Total evidence items 58 | - Average evidence rating 59 | - Top 3 diseases, genes, variants, therapies, and phenotypes (with counts) 60 | 2. **Top 10 Evidence Entries**: 61 | - Lists the highest-rated evidence items with details like disease, phenotype, gene/variant, therapy, description, type, direction, and rating. 62 | 3. **Sources & Citations**: 63 | - Citations and URLs for the sources of the top 10 evidence entries. 64 | 4. **Disclaimer**: 65 | - A note stating the tool is for research purposes only, not medical advice. 66 | 67 | 68 | ## Sample Usage 69 | 70 | - "Find predictive evidence for colorectal cancer therapies involving KRAS mutations." 71 | - "Are there studies on Imatinib for leukemia?" 72 | - "What therapies are linked to pancreatic cancer evidence?" 73 | 74 | ## Acknowledgements 75 | 76 | - [Model Context Protocol](https://github.com/modelcontextprotocol/python-sdk) 77 | - [NANDA: The Internet of AI Agents](https://nanda.media.mit.edu/) 78 | - [CIViC - Clinical Interpretation of Variants in Cancer](https://civicdb.org) 79 | 80 | 81 | ## License 82 | 83 | 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. 84 | 85 | ## Disclaimer 86 | 87 | ⚠️ This tool is intended exclusively for research purposes. It is not a substitute for professional medical advice, diagnosis, or treatment. 88 | 89 | ## Contributors 90 | - Obada Qasem (@obadaqasem), [Nexgene AI](https://www.nexgene.ai) 91 | - Kutsal Ozkurt (@Goodsea), [Nexgene AI](https://www.nexgene.ai) 92 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "nexonco-mcp" 3 | version = "0.1.17" 4 | description = "An advanced MCP Server for accessing and analyzing clinical evidence data, with flexible search options to support precision medicine and oncology research." 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "mcp[cli]==1.6.0", 9 | "pandas>=2.2.3", 10 | "requests>=2.32.3", 11 | "starlette>=0.46.1", 12 | "uvicorn>=0.34.0", 13 | ] 14 | 15 | [project.scripts] 16 | nexonco = "nexonco.server:main" ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Use the official uv base image with Python 3.11 2 | FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim 3 | 4 | # Set working directory inside the container 5 | WORKDIR /app 6 | 7 | # Set environment variables for uv behavior 8 | ENV UV_COMPILE_BYTECODE=1 \ 9 | UV_LINK_MODE=copy 10 | 11 | # Copy project files into the container 12 | ADD . /app 13 | 14 | # Create and activate a virtual environment using uv 15 | RUN uv venv \ 16 | && uv pip install -e . 17 | 18 | # Expose the port your app listens on 19 | EXPOSE 8080 20 | 21 | # Run the application via uv script execution 22 | CMD ["uv", "run", "nexonco", "--transport", "sse"] ``` -------------------------------------------------------------------------------- /docs/claude-desktop-setup.md: -------------------------------------------------------------------------------- ```markdown 1 | # Claude Desktop MCP Configuration Guide 2 | 3 | ## Overview 4 | 5 | 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. 6 | 7 | ## Configuration Steps 8 | 9 | ### Prerequisites 10 | 11 | - [uv](https://github.com/astral-sh/uv#installation) 12 | - Claude Desktop (for MCP integration) 13 | 14 | ### 1. Locate Configuration File 15 | 16 | The configuration file location depends on your operating system: 17 | 18 | - **macOS**: 19 | ``` 20 | ~/Library/Application Support/Claude/claude_desktop_config.json 21 | ``` 22 | 23 | - **Windows**: 24 | ``` 25 | %APPDATA%\Claude\claude_desktop_config.json 26 | ``` 27 | 28 | - **Linux**: 29 | ``` 30 | ~/.config/Claude/claude_desktop_config.json 31 | ``` 32 | 33 | ### 2. Edit Configuration 34 | 35 | 1. Open the configuration file in a text editor 36 | 2. Add or update the mcpServers section: 37 | 38 | ```json 39 | { 40 | "mcpServers": { 41 | "nexonco": { 42 | "command": "uv", 43 | "args": ["run", "--with", "nexonco-mcp", "nexonco" 44 | ] 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | ### 3. Verify Setup 51 | 52 | 1. Save the configuration file 53 | 2. Restart Claude Desktop completely 54 | 3. Test the connection by asking Claude: 55 | `Find predictive evidence for colorectal cancer therapies involving KRAS mutations.` 56 | ``` -------------------------------------------------------------------------------- /docs/nanda-server-setup.md: -------------------------------------------------------------------------------- ```markdown 1 | # NANDA MCP Configuration Guide 2 | 3 | ## Overview 4 | 5 | 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. 6 | 7 | ## Configuration Steps 8 | 9 | ### Prerequisites 10 | 11 | - Docker (for Method 1) 12 | - [uv](https://github.com/astral-sh/uv#installation) (for Method 2) 13 | 14 | ### 1. Download or clone the `nexonco-mcp` GitHub repository 15 | 16 | ### 2. Build and Run `Nexonco` NANDA server 17 | 18 | #### <b>Method 1</b>: Run with Docker (Recommended) 19 | 20 | > Requires: Docker 21 | 22 | 1. **Build the image:** 23 | 24 | ```bash 25 | docker build -t nexonco-mcp . 26 | ``` 27 | 28 | 2. **Run the container:** 29 | 30 | ```bash 31 | docker run -p 8080:8080 nexonco-mcp 32 | ``` 33 | 34 | #### <b>Method 2</b>: Run with `uv` 35 | 36 | > Requires: [`uv`](https://github.com/astral-sh/uv#installation) 37 | 38 | ```bash 39 | uv run --with nexonco-mcp nexonco --transport sse 40 | ``` 41 | 42 | ### 3. Register `Nexonco` Server to NANDA-Host 43 | 44 | - Go to [NANDA Host](https://host.nanda-registry.com/) 45 | - Open `Settings > Nanda Servers > Add New Server` 46 | - Fill the informations 47 | - Server ID: nexonco-local 48 | - Server Name: Nexonco 49 | - Server URL: 127.0.0.1:8080 50 | - Add Server 51 | 52 | ### 4. Verify Setup 53 | 54 | 1. Test the connection by asking NANDA Host: 55 | `Find predictive evidence for colorectal cancer therapies involving KRAS mutations.` 56 | ``` -------------------------------------------------------------------------------- /src/nexonco/query.py: -------------------------------------------------------------------------------- ```python 1 | EVIDENCE_BROWSE_QUERY = """ 2 | 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) { 3 | evidenceItems( 4 | first: $first 5 | last: $last 6 | before: $before 7 | after: $after 8 | diseaseName: $diseaseName 9 | therapyName: $therapyName 10 | id: $id 11 | description: $description 12 | evidenceLevel: $evidenceLevel 13 | evidenceDirection: $evidenceDirection 14 | significance: $significance 15 | evidenceType: $evidenceType 16 | evidenceRating: $rating 17 | variantOrigin: $variantOrigin 18 | variantId: $variantId 19 | molecularProfileId: $molecularProfileId 20 | assertionId: $assertionId 21 | organization: {ids: $organizationId, includeSubgroups: $includeSubgroups} 22 | userId: $userId 23 | phenotypeId: $phenotypeId 24 | diseaseId: $diseaseId 25 | therapyId: $therapyId 26 | sourceId: $sourceId 27 | clinicalTrialId: $clinicalTrialId 28 | molecularProfileName: $molecularProfileName 29 | status: $status 30 | sortBy: $sortBy 31 | ) { 32 | totalCount 33 | pageInfo { 34 | hasNextPage 35 | hasPreviousPage 36 | startCursor 37 | endCursor 38 | } 39 | edges { 40 | node { 41 | ...EvidenceGridFields 42 | } 43 | } 44 | } 45 | } 46 | 47 | fragment EvidenceGridFields on EvidenceItem { 48 | id 49 | name 50 | disease { 51 | id 52 | name 53 | } 54 | therapies { 55 | id 56 | name 57 | } 58 | molecularProfile { 59 | id 60 | name 61 | parsedName { 62 | ...MolecularProfileParsedName 63 | } 64 | } 65 | status 66 | description 67 | evidenceType 68 | evidenceDirection 69 | evidenceRating 70 | } 71 | 72 | fragment MolecularProfileParsedName on MolecularProfileSegment { 73 | ... on MolecularProfileTextSegment { 74 | text 75 | } 76 | ... on Feature { 77 | id 78 | name 79 | } 80 | ... on Variant { 81 | id 82 | name 83 | } 84 | } 85 | """ 86 | 87 | BROWSE_PHENOTYPES_QUERY = """ 88 | query BrowsePhenotypes($phenotypeName: String) { 89 | browsePhenotypes(name: $phenotypeName, sortBy: {direction: DESC, column: EVIDENCE_ITEM_COUNT}) { 90 | edges { 91 | node { 92 | id 93 | name 94 | evidenceCount 95 | } 96 | } 97 | } 98 | } 99 | """ 100 | 101 | EVIDENCE_SUMMARY_QUERY = """ 102 | query EvidenceSummary($evidenceId: Int!) { 103 | evidenceItem(id: $evidenceId) { 104 | source { 105 | citation 106 | sourceUrl 107 | } 108 | 109 | } 110 | } 111 | """ 112 | ``` -------------------------------------------------------------------------------- /src/nexonco/api.py: -------------------------------------------------------------------------------- ```python 1 | from concurrent.futures import ThreadPoolExecutor, as_completed 2 | 3 | import pandas as pd 4 | import requests 5 | 6 | from .query import ( 7 | BROWSE_PHENOTYPES_QUERY, 8 | EVIDENCE_BROWSE_QUERY, 9 | EVIDENCE_SUMMARY_QUERY, 10 | ) 11 | 12 | 13 | class CivicAPIClient: 14 | """ 15 | Client for interacting with the CIViC (Clinical Interpretation of Variants in Cancer) GraphQL API. 16 | Provides methods to browse phenotypes, retrieve evidence, and source details in bulk. 17 | """ 18 | 19 | def __init__(self, cookies=None): 20 | """ 21 | Initialize the CIViC API client. 22 | 23 | Args: 24 | cookies (dict, optional): Cookies for authenticated requests. 25 | """ 26 | self.base_url = "https://civicdb.org/api/graphql" 27 | self.cookies = cookies or {} 28 | self.headers = { 29 | "Accept": "application/json, text/plain, */*", 30 | "Cache-Control": "no-cache", 31 | } 32 | 33 | def browse_phenotype(self, phenotype_name=None): 34 | """ 35 | Retrieve phenotype information from the CIViC API. 36 | 37 | Args: 38 | phenotype_name (str, optional): Name of the phenotype to browse. 39 | 40 | Returns: 41 | dict: JSON response containing phenotype data. 42 | """ 43 | variables = {"phenotypeName": phenotype_name} 44 | payload = { 45 | "operationName": "BrowsePhenotypes", 46 | "variables": variables, 47 | "query": BROWSE_PHENOTYPES_QUERY, 48 | } 49 | result = self._send_request(payload) 50 | return result["data"]["browsePhenotypes"]["edges"][0]["node"] 51 | 52 | def get_sources(self, evidence_id_list): 53 | """ 54 | Fetch source information for multiple evidence items in parallel. 55 | 56 | Args: 57 | evidence_id_list (list of int): List of evidence IDs. 58 | 59 | Returns: 60 | list of dict: List of source information for each evidence item. 61 | """ 62 | payloads = [ 63 | { 64 | "operationName": "EvidenceSummary", 65 | "variables": {"evidenceId": eid}, 66 | "query": EVIDENCE_SUMMARY_QUERY, 67 | } 68 | for eid in evidence_id_list 69 | ] 70 | results = self._send_parallel_requests(payloads) 71 | return [res["data"]["evidenceItem"]["source"] for res in results] 72 | 73 | def search_evidence( 74 | self, 75 | disease_name=None, 76 | therapy_name=None, 77 | molecular_profile_name=None, 78 | phenotype_name=None, 79 | filter_strong_evidence=False, 80 | evidence_type=None, 81 | evidence_direction=None, 82 | ): 83 | """ 84 | Search for evidence items based on filters like disease, therapy, and molecular profile. 85 | 86 | Args: 87 | disease_name (str, optional): Disease name to filter. 88 | therapy_name (str, optional): Therapy name to filter. 89 | molecular_profile_name (str, optional): Molecular profile name to filter. 90 | phenotype_name (str, optional): Phenotype name to filter. 91 | filter_strong_evidence (bool): Whether to include only strong evidence (rating > 3). 92 | evidence_type (str, optional): Type of evidence ("PREDICTIVE" or "DIAGNOSTIC" or "PROGNOSTIC" or "PREDISPOSING" or "FUNCTIONAL"). 93 | evidence_direction (str, optional): Direction of evidence (SUPPORTS or DOES_NOT_SUPPORT). 94 | 95 | Returns: 96 | pd.DataFrame: DataFrame containing filtered evidence items and source information. 97 | """ 98 | variables = {"sortBy": {"column": "EVIDENCE_RATING", "direction": "DESC"}} 99 | 100 | if evidence_type in [ 101 | "PREDICTIVE", 102 | "DIAGNOSTIC", 103 | "PROGNOSTIC", 104 | "PREDISPOSING", 105 | "FUNCTIONAL", 106 | ]: 107 | variables["evidenceType"] = evidence_type 108 | if evidence_direction in ["SUPPORTS", "DOES_NOT_SUPPORT"]: 109 | variables["evidenceDirection"] = evidence_direction 110 | 111 | variables["status"] = "ACCEPTED" if filter_strong_evidence else "NON_REJECTED" 112 | 113 | if disease_name: 114 | variables["diseaseName"] = disease_name 115 | if therapy_name: 116 | variables["therapyName"] = therapy_name 117 | if molecular_profile_name: 118 | variables["molecularProfileName"] = molecular_profile_name 119 | 120 | phenotype_data = {"id": None, "name": None} 121 | if phenotype_name: 122 | phenotype_data = self.browse_phenotype(phenotype_name) 123 | variables["phenotypeId"] = phenotype_data["id"] 124 | 125 | payload = { 126 | "operationName": "EvidenceBrowse", 127 | "variables": variables, 128 | "query": EVIDENCE_BROWSE_QUERY, 129 | } 130 | 131 | results = self._send_request(payload) 132 | results = results["data"]["evidenceItems"]["edges"] 133 | 134 | data = [] 135 | for entry in results: 136 | result = entry["node"] 137 | 138 | if filter_strong_evidence and entry["evidenceRating"] <= 3: 139 | continue 140 | 141 | evidence = { 142 | "id": result["id"], 143 | "name": result["name"], 144 | "disease_id": result["disease"]["id"], 145 | "disease_name": result["disease"]["name"], 146 | "therapy_ids": "+".join( 147 | [str(therapy["id"]) for therapy in result["therapies"]] 148 | ), 149 | "therapy_names": "+".join( 150 | [therapy["name"] for therapy in result["therapies"]] 151 | ), 152 | "molecular_profile_id": result["molecularProfile"]["id"], 153 | "molecular_profile_name": result["molecularProfile"]["name"], 154 | "gene_id": result["molecularProfile"]["parsedName"][0]["id"], 155 | "gene_name": result["molecularProfile"]["parsedName"][0]["name"], 156 | "variant_id": result["molecularProfile"]["parsedName"][1]["id"], 157 | "variant_name": result["molecularProfile"]["parsedName"][1]["name"], 158 | "phenotype_id": phenotype_data["id"], 159 | "phenotype_name": phenotype_data["name"], 160 | "description": result["description"], 161 | "evidence_type": result["evidenceType"], 162 | "evidence_direction": result["evidenceDirection"], 163 | "evidence_rating": result["evidenceRating"], 164 | } 165 | 166 | data.append(evidence) 167 | 168 | df = pd.DataFrame(data) 169 | df = df.dropna(subset=["evidence_rating"]) 170 | 171 | return df 172 | 173 | def _send_request(self, payload): 174 | """ 175 | Internal method to send a single request to the CIViC API. 176 | 177 | Args: 178 | payload (dict): GraphQL query payload. 179 | 180 | Returns: 181 | dict: Parsed JSON response from the API. 182 | """ 183 | response = requests.post( 184 | self.base_url, headers=self.headers, cookies=self.cookies, json=payload 185 | ) 186 | 187 | # Raise exception for HTTP errors 188 | response.raise_for_status() 189 | 190 | return response.json() 191 | 192 | def _send_parallel_requests(self, payloads, max_workers=12): 193 | """ 194 | Internal method to send multiple GraphQL requests concurrently. 195 | 196 | Args: 197 | payloads (list): List of GraphQL payloads. 198 | max_workers (int): Number of concurrent threads (default 12). 199 | 200 | Returns: 201 | list of dict: List of API responses for each payload. 202 | """ 203 | results = [] 204 | 205 | def send(payload): 206 | return self._send_request(payload) 207 | 208 | with ThreadPoolExecutor(max_workers=max_workers) as executor: 209 | future_to_payload = {executor.submit(send, p): p for p in payloads} 210 | for future in as_completed(future_to_payload): 211 | try: 212 | result = future.result() 213 | results.append(result) 214 | except Exception as e: 215 | results.append( 216 | {"error": str(e), "payload": future_to_payload[future]} 217 | ) 218 | 219 | return results 220 | 221 | 222 | def example_usage(): 223 | """Example of how to use the CivicAPIClient.""" 224 | import json 225 | 226 | client = CivicAPIClient() 227 | results = client.search_evidence( 228 | disease_name="cancer", therapy_name="ce", molecular_profile_name="egfr" 229 | ) 230 | print(results) 231 | 232 | # print(json.dumps(client.browse_phenotype("pain"), indent=2)) 233 | # print(json.dumps(client.get_sources([1572, 1058, 7096]), indent=2)) 234 | 235 | 236 | if __name__ == "__main__": 237 | example_usage() 238 | ``` -------------------------------------------------------------------------------- /src/nexonco/server.py: -------------------------------------------------------------------------------- ```python 1 | from collections import Counter 2 | from typing import Optional 3 | 4 | import pandas as pd 5 | import uvicorn 6 | from mcp.server import Server 7 | from mcp.server.fastmcp import FastMCP 8 | from mcp.server.sse import SseServerTransport 9 | from pydantic import Field 10 | from starlette.applications import Starlette 11 | from starlette.requests import Request 12 | from starlette.responses import HTMLResponse, JSONResponse 13 | from starlette.routing import Mount, Route 14 | 15 | from .api import CivicAPIClient 16 | 17 | API_VERSION = "0.1.17" 18 | BUILD_TIMESTAMP = "2025-08-12" 19 | 20 | mcp = FastMCP( 21 | name="nexonco", 22 | description="An advanced MCP Server for accessing and analyzing clinical evidence data, with flexible search options to support precision medicine and oncology research.", 23 | version=API_VERSION, 24 | ) 25 | 26 | 27 | @mcp.tool( 28 | name="search_clinical_evidence", 29 | description=( 30 | "Perform a flexible search for clinical evidence using combinations of filters such as disease, therapy, " 31 | "molecular profile, phenotype, evidence type, and direction. This flexible search system allows you to tailor " 32 | "your query based on the data needed for research or clinical decision-making. It returns a detailed report that " 33 | "includes summary statistics, a top 10 evidence listing, citation sources, and a disclaimer." 34 | ), 35 | ) 36 | def search_clinical_evidence( 37 | disease_name: Optional[str] = Field( 38 | default="", 39 | 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.", 40 | ), 41 | therapy_name: Optional[str] = Field( 42 | default="", 43 | description="Therapy or drug name involved in the evidence (e.g., 'Cetuximab', 'Imatinib', 'trastuzumab', 'Lapatinib'..). Optional.", 44 | ), 45 | molecular_profile_name: Optional[str] = Field( 46 | default="", 47 | description="Molecular profile or gene name or variant name (e.g., 'EGFR L858R', 'BRAF V600E', 'KRAS', 'PIK3CA'..). Optional.", 48 | ), 49 | phenotype_name: Optional[str] = Field( 50 | default="", 51 | description="Name of the phenotype or histological subtype (e.g., 'Hemangioblastoma', 'Renal cell carcinoma', 'Retinal capillary hemangioma', 'Pancreatic cysts', 'Childhood onset'..). Optional.", 52 | ), 53 | evidence_type: Optional[str] = Field( 54 | default="", 55 | description="Evidence classification: 'PREDICTIVE', 'DIAGNOSTIC', 'PROGNOSTIC', 'PREDISPOSING', or 'FUNCTIONAL'. Optional.", 56 | ), 57 | evidence_direction: Optional[str] = Field( 58 | default="", 59 | description="Direction of the evidence: 'SUPPORTS' or 'DOES_NOT_SUPPORT'. Indicates if the evidence favors the association.", 60 | ), 61 | filter_strong_evidence: bool = Field( 62 | default=False, 63 | 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.", 64 | ), 65 | ) -> str: 66 | """ 67 | Query clinical evidence records using flexible combinations of disease, therapy, molecular profile, 68 | phenotype, and other evidence characteristics. Returns a formatted report containing a summary of findings, 69 | most common genes and therapies, and highlights of top-ranked evidence entries including source URLs and citations. 70 | 71 | This tool is designed to streamline evidence exploration in precision oncology by adapting to various research 72 | or clinical inquiry contexts. 73 | 74 | Returns: 75 | str: A human-readable report summarizing relevant evidence, key statistics, and literature references. 76 | """ 77 | 78 | client = CivicAPIClient() 79 | 80 | disease_name = None if disease_name == "" else disease_name 81 | therapy_name = None if therapy_name == "" else therapy_name 82 | molecular_profile_name = ( 83 | None if molecular_profile_name == "" else molecular_profile_name 84 | ) 85 | phenotype_name = None if phenotype_name == "" else phenotype_name 86 | evidence_type = None if evidence_type == "" else evidence_type 87 | evidence_direction = None if evidence_direction == "" else evidence_direction 88 | 89 | df: pd.DataFrame = client.search_evidence( 90 | disease_name=disease_name, 91 | therapy_name=therapy_name, 92 | molecular_profile_name=molecular_profile_name, 93 | phenotype_name=phenotype_name, 94 | evidence_type=evidence_type, 95 | evidence_direction=evidence_direction, 96 | filter_strong_evidence=filter_strong_evidence, 97 | ) 98 | 99 | if df.empty: 100 | return "🔍 No evidence found for the specified filters." 101 | 102 | # --------------------------------- 103 | # 1. Summary Statistics Section 104 | # --------------------------------- 105 | total_items = len(df) 106 | avg_rating = df["evidence_rating"].mean() 107 | 108 | # Frequency counters for each key attribute 109 | disease_counter = Counter(df["disease_name"].dropna()) 110 | gene_counter = Counter(df["gene_name"].dropna()) 111 | variant_counter = Counter(df["variant_name"].dropna()) 112 | therapy_counter = Counter(df["therapy_names"].dropna()) 113 | phenotype_counter = Counter(df["phenotype_name"].dropna()) 114 | 115 | # Prepare top-3 summary for each attribute 116 | def format_top(counter: Counter) -> str: 117 | return ( 118 | ", ".join(f"{item} ({count})" for item, count in counter.most_common(3)) 119 | if counter 120 | else "N/A" 121 | ) 122 | 123 | top_diseases = format_top(disease_counter) 124 | top_genes = format_top(gene_counter) 125 | top_variants = format_top(variant_counter) 126 | top_therapies = format_top(therapy_counter) 127 | top_phenotypes = format_top(phenotype_counter) 128 | 129 | stats_section = ( 130 | f"📊 **Summary Statistics**\n" 131 | f"- Total Evidence Items: {total_items}\n" 132 | f"- Average Evidence Rating: {avg_rating:.2f}\n" 133 | f"- Top Diseases: {top_diseases}\n" 134 | f"- Top Genes: {top_genes}\n" 135 | f"- Top Variants: {top_variants}\n" 136 | f"- Top Therapies: {top_therapies}\n" 137 | f"- Top Phenotypes: {top_phenotypes}\n" 138 | ) 139 | 140 | # --------------------------------- 141 | # 2. Top 10 Evidence Listing Section 142 | # --------------------------------- 143 | top_evidences = df.sort_values(by="evidence_rating", ascending=False).head(10) 144 | evidence_section = "📌 **Top 10 Evidence Entries**\n" 145 | for _, row in top_evidences.iterrows(): 146 | evidence_section += ( 147 | f"\n🔹 **{row.get('evidence_type', 'N/A')} ({row.get('evidence_direction', 'N/A')})** | Rating: {row.get('evidence_rating', 'N/A')}\n" 148 | f"- Disease: {row.get('disease_name', 'N/A')}\n" 149 | f"- Phenotype: {row.get('phenotype_name', 'N/A')}\n" 150 | f"- Gene/Variant: {row.get('gene_name', 'N/A')} / {row.get('variant_name', 'N/A')}\n" 151 | f"- Therapy: {row.get('therapy_names', 'N/A')}\n" 152 | f"- Description: {row.get('description', 'N/A')}\n" 153 | ) 154 | 155 | # --------------------------------- 156 | # 3. Sources & Citations Section 157 | # --------------------------------- 158 | citation_section = "🔗 **Sources & Citations**\n" 159 | sources = client.get_sources(top_evidences["id"].tolist()) 160 | for _, row in pd.DataFrame(sources).iterrows(): 161 | citation_section += ( 162 | f"• {row.get('citation', 'N/A')} - {row.get('sourceUrl', 'N/A')}\n" 163 | ) 164 | 165 | # --------------------------------- 166 | # 4. Disclaimer Section 167 | # --------------------------------- 168 | disclaimer = "\n⚠️ **Disclaimer:** This tool is intended exclusively for research purposes. It is not a substitute for professional medical advice, diagnosis, or treatment." 169 | 170 | # --------------------------------- 171 | # Combine All Sections into Final Report 172 | # --------------------------------- 173 | final_report = ( 174 | f"{stats_section}\n" 175 | f"{evidence_section}\n" 176 | f"{citation_section}\n" 177 | f"{disclaimer}" 178 | ) 179 | 180 | return final_report 181 | 182 | 183 | async def healthcheck(request: Request) -> JSONResponse: 184 | return JSONResponse({"status": "ok", "message": "Server is healthy."}) 185 | 186 | 187 | async def version(request): 188 | return JSONResponse({"version": API_VERSION, "build": BUILD_TIMESTAMP}) 189 | 190 | 191 | async def homepage(request: Request) -> HTMLResponse: 192 | html_content = """ 193 | <!DOCTYPE html> 194 | <html lang="en"> 195 | <head> 196 | <meta charset="UTF-8"> 197 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 198 | <title>Nexonco MCP Server</title> 199 | <style> 200 | :root { 201 | --primary: #0f172a; 202 | --secondary: #0369a1; 203 | --accent: #0284c7; 204 | --light-blue: #e0f2fe; 205 | --light: #ffffff; 206 | --dark: #0f172a; 207 | --gray: #64748b; 208 | --light-gray: #f1f5f9; 209 | --border-radius: 4px; 210 | --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 211 | } 212 | 213 | * { 214 | margin: 0; 215 | padding: 0; 216 | box-sizing: border-box; 217 | } 218 | 219 | body { 220 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 221 | color: var(--dark); 222 | background-color: #f8fafc; 223 | line-height: 1.6; 224 | } 225 | 226 | .container { 227 | max-width: 1100px; 228 | margin: 0 auto; 229 | padding: 0 20px; 230 | } 231 | 232 | header { 233 | background-color: #000; 234 | color: white; 235 | padding: 0; 236 | } 237 | 238 | .banner-container { 239 | width: 100%; 240 | max-width: 100%; 241 | margin: 0 auto; 242 | } 243 | 244 | .banner { 245 | width: 100%; 246 | max-width: 100%; 247 | height: auto; 248 | display: block; 249 | } 250 | 251 | .subtitle { 252 | text-align: center; 253 | padding: 0.75rem 0; 254 | background-color: #000; 255 | color: white; 256 | font-size: 0.95rem; 257 | letter-spacing: 0.5px; 258 | border-top: 1px solid rgba(255, 255, 255, 0.1); 259 | } 260 | 261 | .main-content { 262 | padding: 2.5rem 0; 263 | } 264 | 265 | .status-bar { 266 | display: flex; 267 | align-items: center; 268 | background-color: white; 269 | padding: 0.75rem 1.5rem; 270 | border-radius: var(--border-radius); 271 | margin-bottom: 2rem; 272 | box-shadow: var(--shadow); 273 | } 274 | 275 | .status-indicator { 276 | display: inline-flex; 277 | align-items: center; 278 | padding: 0.4rem 0.8rem; 279 | background-color: rgba(34, 197, 94, 0.1); 280 | border-radius: 2rem; 281 | color: #15803d; 282 | font-weight: 500; 283 | font-size: 0.9rem; 284 | margin-right: auto; 285 | } 286 | 287 | .status-indicator::before { 288 | content: ""; 289 | display: inline-block; 290 | width: 8px; 291 | height: 8px; 292 | background-color: #22c55e; 293 | border-radius: 50%; 294 | margin-right: 8px; 295 | } 296 | 297 | .modern-layout { 298 | display: grid; 299 | grid-template-columns: 2fr 1fr; 300 | gap: 1.5rem; 301 | } 302 | 303 | .connection-section { 304 | grid-column: 1 / -1; 305 | margin-bottom: 1.5rem; 306 | } 307 | 308 | .full-width { 309 | grid-column: 1 / -1; 310 | } 311 | 312 | h2 { 313 | font-size: 1.25rem; 314 | margin: 0 0 1.25rem; 315 | color: var(--primary); 316 | font-weight: 600; 317 | letter-spacing: 0.5px; 318 | text-transform: uppercase; 319 | display: flex; 320 | align-items: center; 321 | } 322 | 323 | h2 svg { 324 | margin-right: 0.5rem; 325 | width: 20px; 326 | height: 20px; 327 | } 328 | 329 | p { 330 | margin-bottom: 1rem; 331 | color: #334155; 332 | font-size: 0.95rem; 333 | } 334 | 335 | strong { 336 | color: var(--primary); 337 | font-weight: 600; 338 | } 339 | 340 | .card { 341 | background-color: white; 342 | border-radius: var(--border-radius); 343 | padding: 1.75rem; 344 | margin-bottom: 1.5rem; 345 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); 346 | } 347 | 348 | .connection-card { 349 | background-color: var(--light-blue); 350 | border-left: 4px solid var(--accent); 351 | } 352 | 353 | .about-card { 354 | height: 100%; 355 | } 356 | 357 | .api-card { 358 | background-color: #f8fafc; 359 | height: 100%; 360 | border-left: 4px solid #6366f1; 361 | } 362 | 363 | .query-list { 364 | list-style: none; 365 | margin-top: 1rem; 366 | } 367 | 368 | .query-item { 369 | padding: 0.85rem 1rem; 370 | margin-bottom: 0.75rem; 371 | background-color: #f1f5f9; 372 | border-radius: var(--border-radius); 373 | border-left: 3px solid var(--secondary); 374 | font-family: monospace; 375 | font-size: 0.9rem; 376 | color: #334155; 377 | } 378 | 379 | .button { 380 | background-color: var(--secondary); 381 | color: white; 382 | border: none; 383 | padding: 0.7rem 1.4rem; 384 | margin: 1rem 0.5rem 1rem 0; 385 | cursor: pointer; 386 | border-radius: var(--border-radius); 387 | font-weight: 500; 388 | transition: all 0.2s ease; 389 | text-transform: uppercase; 390 | letter-spacing: 0.5px; 391 | font-size: 0.85rem; 392 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 393 | } 394 | 395 | .button:hover { 396 | background-color: var(--accent); 397 | transform: translateY(-1px); 398 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 399 | } 400 | 401 | .button.secondary { 402 | background-color: #e2e8f0; 403 | color: #334155; 404 | } 405 | 406 | .button.secondary:hover { 407 | background-color: #cbd5e1; 408 | } 409 | 410 | .status { 411 | border: 1px solid #e2e8f0; 412 | padding: 1rem; 413 | min-height: 20px; 414 | margin-top: 1rem; 415 | border-radius: var(--border-radius); 416 | color: #64748b; 417 | background-color: #f8fafc; 418 | font-family: monospace; 419 | } 420 | 421 | .status.connected { 422 | border-color: rgba(34, 197, 94, 0.3); 423 | color: #15803d; 424 | background-color: rgba(34, 197, 94, 0.05); 425 | } 426 | 427 | .status.error { 428 | border-color: rgba(239, 68, 68, 0.3); 429 | color: #b91c1c; 430 | background-color: rgba(239, 68, 68, 0.05); 431 | } 432 | 433 | code { 434 | font-family: monospace; 435 | background-color: rgba(99, 102, 241, 0.1); 436 | padding: 0.2rem 0.4rem; 437 | border-radius: 2px; 438 | font-size: 0.9rem; 439 | color: #4f46e5; 440 | } 441 | 442 | .disclaimer { 443 | background-color: rgba(239, 68, 68, 0.05); 444 | border-left: 3px solid #ef4444; 445 | padding: 1rem; 446 | margin: 1.25rem 0 0; 447 | border-radius: var(--border-radius); 448 | font-size: 0.9rem; 449 | color: #7f1d1d; 450 | } 451 | 452 | .api-details { 453 | display: flex; 454 | align-items: flex-start; 455 | margin-top: 0.5rem; 456 | } 457 | 458 | .api-icon { 459 | flex-shrink: 0; 460 | width: 40px; 461 | height: 40px; 462 | background-color: rgba(99, 102, 241, 0.1); 463 | border-radius: 50%; 464 | display: flex; 465 | align-items: center; 466 | justify-content: center; 467 | margin-right: 1rem; 468 | color: #4f46e5; 469 | } 470 | 471 | .api-text { 472 | flex-grow: 1; 473 | } 474 | 475 | .links { 476 | display: flex; 477 | gap: 1rem; 478 | margin-top: 1rem; 479 | flex-wrap: wrap; 480 | } 481 | 482 | .links a { 483 | color: var(--secondary); 484 | text-decoration: none; 485 | display: inline-flex; 486 | align-items: center; 487 | transition: all 0.2s; 488 | font-size: 0.9rem; 489 | padding: 0.5rem 0.75rem; 490 | background-color: #f1f5f9; 491 | border-radius: var(--border-radius); 492 | border: 1px solid rgba(0, 0, 0, 0.05); 493 | } 494 | 495 | .links a:hover { 496 | background-color: #e2e8f0; 497 | transform: translateY(-1px); 498 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 499 | } 500 | 501 | .icon { 502 | width: 16px; 503 | height: 16px; 504 | margin-right: 8px; 505 | } 506 | 507 | footer { 508 | color: #64748b; 509 | padding: 1.5rem 0; 510 | margin-top: 2rem; 511 | text-align: center; 512 | font-size: 0.85rem; 513 | border-top: 1px solid #e2e8f0; 514 | background-color: white; 515 | } 516 | 517 | footer a { 518 | color: var(--secondary); 519 | text-decoration: none; 520 | } 521 | 522 | footer a:hover { 523 | text-decoration: underline; 524 | } 525 | 526 | @media (max-width: 768px) { 527 | .modern-layout { 528 | grid-template-columns: 1fr; 529 | } 530 | 531 | .links { 532 | gap: 0.75rem; 533 | } 534 | 535 | .links a { 536 | padding: 0.4rem 0.6rem; 537 | font-size: 0.85rem; 538 | } 539 | } 540 | </style> 541 | </head> 542 | <body> 543 | <header> 544 | <div class="banner-container"> 545 | <img src="https://github.com/user-attachments/assets/c2ec59e8-ff8c-40e1-b66d-17998fe67ecf" alt="Nexonco MCP Banner" class="banner"> 546 | </div> 547 | <div class="subtitle"> 548 | <div class="container"> 549 | Clinical Evidence Data Analysis for Precision Oncology 550 | </div> 551 | <br> 552 | <img src="http://nanda-registry.com/api/v1/verification/badge/c6284608-6bce-4417-a170-da6c1a117616" alt="Verified NANDA MCP Server" /> 553 | <img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License" /> 554 | </div> 555 | </header> 556 | 557 | <main class="main-content"> 558 | <div class="container"> 559 | <!-- Status Bar --> 560 | <div class="status-bar"> 561 | <div class="status-indicator">Server is running correctly</div> 562 | <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"> 563 | <rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect> 564 | <rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect> 565 | <line x1="6" y1="6" x2="6.01" y2="6"></line> 566 | <line x1="6" y1="18" x2="6.01" y2="18"></line> 567 | </svg> 568 | </div> 569 | 570 | <!-- Connection Section (Full Width) --> 571 | <div class="connection-section"> 572 | <div class="card connection-card"> 573 | <h2> 574 | <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"> 575 | <path d="M5 12.55a11 11 0 0 1 14.08 0"></path> 576 | <path d="M1.42 9a16 16 0 0 1 21.16 0"></path> 577 | <path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path> 578 | <line x1="12" y1="20" x2="12.01" y2="20"></line> 579 | </svg> 580 | Server Connection 581 | </h2> 582 | <p>Test your connection to the MCP server:</p> 583 | 584 | <button id="connect-button" class="button">Connect to SSE</button> 585 | <div id="disconnect-container"></div> 586 | 587 | <div class="status" id="status">Connection status will appear here...</div> 588 | </div> 589 | </div> 590 | 591 | <!-- Modern Asymmetric Layout --> 592 | <div class="modern-layout"> 593 | <!-- About Card (Left Column - Wider) --> 594 | <div class="card about-card"> 595 | <h2> 596 | <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"> 597 | <circle cx="12" cy="12" r="10"></circle> 598 | <line x1="12" y1="16" x2="12" y2="12"></line> 599 | <line x1="12" y1="8" x2="12.01" y2="8"></line> 600 | </svg> 601 | About Nexonco 602 | </h2> 603 | <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> 604 | 605 | <div class="disclaimer"> 606 | <strong>⚠️ Disclaimer:</strong> This tool is intended exclusively for research purposes. It is not a substitute for professional medical advice, diagnosis, or treatment. 607 | </div> 608 | </div> 609 | 610 | <!-- API Card (Right Column - Narrower) --> 611 | <div class="card api-card"> 612 | <h2> 613 | <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"> 614 | <polyline points="16 18 22 12 16 6"></polyline> 615 | <polyline points="8 6 2 12 8 18"></polyline> 616 | </svg> 617 | API Information 618 | </h2> 619 | 620 | <div class="api-details"> 621 | <div class="api-icon"> 622 | <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"> 623 | <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> 624 | <polyline points="14 2 14 8 20 8"></polyline> 625 | <line x1="16" y1="13" x2="8" y2="13"></line> 626 | <line x1="16" y1="17" x2="8" y2="17"></line> 627 | <polyline points="10 9 9 9 8 9"></polyline> 628 | </svg> 629 | </div> 630 | <div class="api-text"> 631 | <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> 632 | </div> 633 | </div> 634 | </div> 635 | 636 | <!-- Sample Queries Card (Full Width) --> 637 | <div class="card full-width"> 638 | <h2> 639 | <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"> 640 | <line x1="22" y1="2" x2="11" y2="13"></line> 641 | <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> 642 | </svg> 643 | Sample Queries 644 | </h2> 645 | <ul class="query-list"> 646 | <li class="query-item">Find predictive evidence for colorectal cancer therapies involving KRAS mutations.</li> 647 | <li class="query-item">Are there studies on Imatinib for leukemia?</li> 648 | <li class="query-item">What therapies are linked to pancreatic cancer evidence?</li> 649 | </ul> 650 | </div> 651 | 652 | <!-- Links Card (Full Width) --> 653 | <div class="card full-width"> 654 | <h2> 655 | <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"> 656 | <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> 657 | <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> 658 | </svg> 659 | Links 660 | </h2> 661 | <div class="links"> 662 | <a href="https://www.nexgene.ai" target="_blank"> 663 | <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"> 664 | <circle cx="12" cy="12" r="10"></circle> 665 | <line x1="2" y1="12" x2="22" y2="12"></line> 666 | <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> 667 | </svg> 668 | Nexgene AI 669 | </a> 670 | <a href="https://www.linkedin.com/company/nexgene" target="_blank"> 671 | <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"> 672 | <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> 673 | <rect x="2" y="9" width="4" height="12"></rect> 674 | <circle cx="4" cy="4" r="2"></circle> 675 | </svg> 676 | LinkedIn 677 | </a> 678 | <a href="https://github.com/Nexgene-Research/nexonco-mcp" target="_blank"> 679 | <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"> 680 | <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> 681 | </svg> 682 | GitHub 683 | </a> 684 | </div> 685 | </div> 686 | </div> 687 | </div> 688 | </main> 689 | 690 | <footer> 691 | <div class="container"> 692 | <p>Nexonco MCP Server © 2025 <a href="https://www.nexgene.ai" target="_blank">Nexgene AI</a> | MIT License</p> 693 | </div> 694 | </footer> 695 | 696 | <script> 697 | // Server connection functionality 698 | document.getElementById('connect-button').addEventListener('click', function() { 699 | const statusDiv = document.getElementById('status'); 700 | const disconnectContainer = document.getElementById('disconnect-container'); 701 | 702 | try { 703 | const eventSource = new EventSource('/sse'); 704 | 705 | statusDiv.textContent = 'Connecting...'; 706 | statusDiv.className = 'status'; 707 | 708 | eventSource.onopen = function() { 709 | statusDiv.textContent = 'Connected to SSE'; 710 | statusDiv.className = 'status connected'; 711 | }; 712 | 713 | eventSource.onerror = function() { 714 | statusDiv.textContent = 'Error connecting to SSE'; 715 | statusDiv.className = 'status error'; 716 | eventSource.close(); 717 | }; 718 | 719 | eventSource.onmessage = function(event) { 720 | statusDiv.textContent = 'Received: ' + event.data; 721 | statusDiv.className = 'status connected'; 722 | }; 723 | 724 | // Add a disconnect option 725 | disconnectContainer.innerHTML = ''; 726 | const disconnectButton = document.createElement('button'); 727 | disconnectButton.textContent = 'Disconnect'; 728 | disconnectButton.className = 'button secondary'; 729 | disconnectButton.addEventListener('click', function() { 730 | eventSource.close(); 731 | statusDiv.textContent = 'Disconnected'; 732 | statusDiv.className = 'status'; 733 | disconnectContainer.innerHTML = ''; 734 | }); 735 | 736 | disconnectContainer.appendChild(disconnectButton); 737 | 738 | } catch (e) { 739 | statusDiv.textContent = 'Error: ' + e.message; 740 | statusDiv.className = 'status error'; 741 | } 742 | }); 743 | </script> 744 | </body> 745 | </html> 746 | """ 747 | return HTMLResponse(html_content) 748 | 749 | 750 | def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette: 751 | """Create a Starlette application that can server the provied mcp server with SSE. 752 | 753 | This sets up the HTTP routes and SSE connection handling. 754 | """ 755 | # Create an SSE transport with a path for messages 756 | sse = SseServerTransport("/messages/") 757 | 758 | async def handle_sse(request: Request) -> None: 759 | async with sse.connect_sse( 760 | request.scope, 761 | request.receive, 762 | request._send, 763 | ) as (read_stream, write_stream): 764 | await mcp_server.run( 765 | read_stream, 766 | write_stream, 767 | mcp_server.create_initialization_options(), 768 | ) 769 | 770 | return Starlette( 771 | debug=debug, 772 | routes=[ 773 | Route("/", endpoint=homepage), 774 | Route("/health", endpoint=healthcheck), 775 | Route("/version", endpoint=version), 776 | Route("/sse", endpoint=handle_sse), 777 | Mount("/messages/", app=sse.handle_post_message), 778 | ], 779 | ) 780 | 781 | 782 | def main(): 783 | import argparse 784 | 785 | # Parse command-line arguments 786 | parser = argparse.ArgumentParser(description="Run MCP SSE-based server") 787 | parser.add_argument( 788 | "--transport", 789 | type=str, 790 | choices=["stdio", "sse"], 791 | default="stdio", 792 | help="Transport mechanism to use ('stdio' for Claude, 'sse' for NANDA)", 793 | ) 794 | parser.add_argument("--host", default="0.0.0.0", help="Host to bind to") 795 | parser.add_argument("--port", type=int, default=8080, help="Port to listen on") 796 | args = parser.parse_args() 797 | 798 | # Create and run the Starlette application 799 | if args.transport == "sse": 800 | mcp_server = mcp._mcp_server 801 | starlette_app = create_starlette_app(mcp_server, debug=True) 802 | uvicorn.run(starlette_app, host=args.host, port=args.port) 803 | else: 804 | mcp.run(transport="stdio") 805 | 806 | 807 | if __name__ == "__main__": 808 | main() 809 | ```