#
tokens: 16159/50000 11/11 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | ![nexonco-mcp-banner](https://github.com/user-attachments/assets/c2ec59e8-ff8c-40e1-b66d-17998fe67ecf)
 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 |   [![PyPI](https://img.shields.io/badge/PyPI-nexonco--mcp-000000.svg?style=for-the-badge&logo=pypi&labelColor=000)](https://pypi.org/project/nexonco-mcp) 
11 |   [![NANDA](https://img.shields.io/badge/NANDA-Nexonco-000000.svg?style=for-the-badge&logo=&labelColor=000)](https://github.com/aidecentralized/nanda-servers/tree/main/nexonco-mcp)
12 |   [![License](https://img.shields.io/badge/License-MIT-000000.svg?style=for-the-badge&logo=github&labelColor=000)](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: [![Youtube](https://img.shields.io/badge/YouTube-red)](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 &copy; 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 | 
```