Upload 17 files
Browse files- .gitignore +52 -0
- Dockerfile +32 -32
- LICENSE +27 -27
- README.md +252 -151
- app.py +182 -242
- data/1of6_ligand.sdf +31 -0
- data/1of6_ligands.sdf +256 -0
- data/1of6_protein_one_lig_removed.pdb +0 -0
- main.py +170 -0
- mcp_context.json +99 -63
- pytest.ini +4 -0
- requirements.txt +15 -6
- schema.py +128 -0
- tests/__pycache__/test_mcp.cpython-312-pytest-8.3.5.pyc +0 -0
- tests/conftest.py +6 -0
- tests/test_mcp.py +101 -0
- utils.py +112 -0
.gitignore
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
env/
|
| 8 |
+
build/
|
| 9 |
+
develop-eggs/
|
| 10 |
+
dist/
|
| 11 |
+
downloads/
|
| 12 |
+
eggs/
|
| 13 |
+
.eggs/
|
| 14 |
+
lib/
|
| 15 |
+
lib64/
|
| 16 |
+
parts/
|
| 17 |
+
sdist/
|
| 18 |
+
var/
|
| 19 |
+
wheels/
|
| 20 |
+
*.egg-info/
|
| 21 |
+
.installed.cfg
|
| 22 |
+
*.egg
|
| 23 |
+
|
| 24 |
+
# Unit test / coverage reports
|
| 25 |
+
htmlcov/
|
| 26 |
+
.tox/
|
| 27 |
+
.coverage
|
| 28 |
+
.coverage.*
|
| 29 |
+
.cache
|
| 30 |
+
nosetests.xml
|
| 31 |
+
coverage.xml
|
| 32 |
+
*.cover
|
| 33 |
+
.hypothesis/
|
| 34 |
+
.pytest_cache/
|
| 35 |
+
|
| 36 |
+
# Jupyter Notebook
|
| 37 |
+
.ipynb_checkpoints
|
| 38 |
+
|
| 39 |
+
# VS Code
|
| 40 |
+
.vscode/
|
| 41 |
+
|
| 42 |
+
# Environment
|
| 43 |
+
.env
|
| 44 |
+
.venv
|
| 45 |
+
venv/
|
| 46 |
+
ENV/
|
| 47 |
+
|
| 48 |
+
# mypy
|
| 49 |
+
.mypy_cache/
|
| 50 |
+
|
| 51 |
+
# Logs
|
| 52 |
+
*.log
|
Dockerfile
CHANGED
|
@@ -1,32 +1,32 @@
|
|
| 1 |
-
FROM python:3.10-slim
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
CMD ["python", "app.py"]
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
libmagic1 \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Copy requirements first for better caching
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
RUN pip install -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy application files
|
| 15 |
+
COPY main.py .
|
| 16 |
+
COPY app.py .
|
| 17 |
+
COPY utils.py .
|
| 18 |
+
COPY schema.py .
|
| 19 |
+
COPY mcp_context.json .
|
| 20 |
+
|
| 21 |
+
# Copy test files
|
| 22 |
+
COPY tests/ tests/
|
| 23 |
+
|
| 24 |
+
# Create a test script
|
| 25 |
+
RUN echo '#!/bin/bash\npytest -v "$@"' > /usr/local/bin/run-tests && \
|
| 26 |
+
chmod +x /usr/local/bin/run-tests
|
| 27 |
+
|
| 28 |
+
# Expose port
|
| 29 |
+
EXPOSE 7860
|
| 30 |
+
|
| 31 |
+
# Run the integrated server
|
| 32 |
+
CMD ["python", "app.py"]
|
LICENSE
CHANGED
|
@@ -1,27 +1,27 @@
|
|
| 1 |
-
BSD 3-Clause License
|
| 2 |
-
|
| 3 |
-
Copyright (c) 2023, Martin Buttenschoen
|
| 4 |
-
All rights reserved.
|
| 5 |
-
|
| 6 |
-
Redistribution and use in source and binary forms, with or without
|
| 7 |
-
modification, are permitted provided that the following conditions are met:
|
| 8 |
-
|
| 9 |
-
1. Redistributions of source code must retain the above copyright notice, this
|
| 10 |
-
list of conditions and the following disclaimer.
|
| 11 |
-
2. Redistributions in binary form must reproduce the above copyright notice,
|
| 12 |
-
this list of conditions and the following disclaimer in the documentation
|
| 13 |
-
and/or other materials provided with the distribution.
|
| 14 |
-
3. Neither the name of the copyright holder nor the names of its
|
| 15 |
-
contributors may be used to endorse or promote products derived from
|
| 16 |
-
this software without specific prior written permission.
|
| 17 |
-
|
| 18 |
-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
| 19 |
-
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
| 20 |
-
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
| 21 |
-
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
| 22 |
-
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
| 23 |
-
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
| 24 |
-
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
| 25 |
-
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
| 26 |
-
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| 27 |
-
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
| 1 |
+
BSD 3-Clause License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2023, Martin Buttenschoen
|
| 4 |
+
All rights reserved.
|
| 5 |
+
|
| 6 |
+
Redistribution and use in source and binary forms, with or without
|
| 7 |
+
modification, are permitted provided that the following conditions are met:
|
| 8 |
+
|
| 9 |
+
1. Redistributions of source code must retain the above copyright notice, this
|
| 10 |
+
list of conditions and the following disclaimer.
|
| 11 |
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
| 12 |
+
this list of conditions and the following disclaimer in the documentation
|
| 13 |
+
and/or other materials provided with the distribution.
|
| 14 |
+
3. Neither the name of the copyright holder nor the names of its
|
| 15 |
+
contributors may be used to endorse or promote products derived from
|
| 16 |
+
this software without specific prior written permission.
|
| 17 |
+
|
| 18 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
| 19 |
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
| 20 |
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
| 21 |
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
| 22 |
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
| 23 |
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
| 24 |
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
| 25 |
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
| 26 |
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| 27 |
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
README.md
CHANGED
|
@@ -1,151 +1,252 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Posebusters Wrapper MCP
|
| 3 |
-
emoji: 😻
|
| 4 |
-
colorFrom: gray
|
| 5 |
-
colorTo: pink
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 5.36.2
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
license: bsd-3-clause
|
| 11 |
-
short_description: '
|
| 12 |
-
---
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
>
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
-
|
| 40 |
-
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
-
|
| 46 |
-
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
- ✅
|
| 54 |
-
- ✅
|
| 55 |
-
- ✅
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
- 🔹
|
| 63 |
-
- 🔹
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
-
|
| 69 |
-
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
-
|
| 79 |
-
-
|
| 80 |
-
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
curl -X GET http://localhost:7860/mcp/context
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
```
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
-F
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Posebusters Wrapper MCP
|
| 3 |
+
emoji: 😻
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.36.2
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: bsd-3-clause
|
| 11 |
+
short_description: 'MCP wrapper for PoseBusters: validates ligand–protein struct'
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# 🧪 PoseBusters MCP Wrapper
|
| 18 |
+
|
| 19 |
+
This Hugging Face Space provides an **MCP-compatible API wrapper** around [PoseBusters](https://github.com/maabuu/posebusters), a command-line tool for validating the **physical and chemical plausibility** of molecular docking poses.
|
| 20 |
+
|
| 21 |
+
> **⚠️ Disclaimer**
|
| 22 |
+
> This project is unofficial and not affiliated with or endorsed by the original author of PoseBusters.
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## ✅ Features
|
| 27 |
+
|
| 28 |
+
✅ **Supports molecular file uploads**
|
| 29 |
+
Accepts ligand files (`.sdf`) and protein structures (`.pdb`) via either:
|
| 30 |
+
- a simple web interface (Gradio tab UI), or
|
| 31 |
+
- HTTP POST requests using `multipart/form-data`.
|
| 32 |
+
|
| 33 |
+
🔁 **Redocking validation (optional)**
|
| 34 |
+
If a crystal ligand (`.sdf`) is provided, the API performs redocking validation by comparing it to the predicted ligand pose.
|
| 35 |
+
|
| 36 |
+
⚙️ **Leverages the `bust` CLI from PoseBusters**
|
| 37 |
+
Internally, this wrapper uses the PoseBusters command-line tool to evaluate:
|
| 38 |
+
- pose plausibility
|
| 39 |
+
- chemical validity
|
| 40 |
+
- geometry checks
|
| 41 |
+
|
| 42 |
+
📊 **Structured JSON responses**
|
| 43 |
+
The output follows the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), making it easy to use results in:
|
| 44 |
+
- UI panels
|
| 45 |
+
- workflows
|
| 46 |
+
- logic pipelines
|
| 47 |
+
|
| 48 |
+
🤖 **MCP-Compatible API (for use in AI workflows)**
|
| 49 |
+
|
| 50 |
+
This project is fully **MCP-compliant**, meaning it follows the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), the standard for exposing tools in AI-driven workflows and UIs.
|
| 51 |
+
|
| 52 |
+
- ✅ Exposes a valid `GET /mcp/context` for tool discovery and UI generation.
|
| 53 |
+
- ✅ Accepts `POST /mcp/predict` with `multipart/form-data` for structured tool execution.
|
| 54 |
+
- ✅ Returns results in structured JSON, ready for use in agents, chatbots, or pipelines.
|
| 55 |
+
- ✅ When deployed on your own **Hugging Face Spaces**, it works as an **MCP server** that can be added to your toolset from the MCP badge.
|
| 56 |
+
|
| 57 |
+
*🧠 What does this mean for you?*
|
| 58 |
+
|
| 59 |
+
If you're using **VSCode with Hugging Face MCP**, **Claude**, or **any other MCP-compatible client**, you can:
|
| 60 |
+
|
| 61 |
+
- 🔹 Add this tool directly from its Space card using the MCP badge.
|
| 62 |
+
- 🔹 Interact with it using standard UI panels or programmatic workflows.
|
| 63 |
+
- 🔹 Submit files like `.sdf` and `.pdb` and receive validated pose results.
|
| 64 |
+
|
| 65 |
+
*Notes*
|
| 66 |
+
|
| 67 |
+
- The app runs perfectly inside a **Gradio Space** or a Docker container, using `FastAPI` as its backend.
|
| 68 |
+
- The app is **fully MCP-compatible** and **discoverable** once deployed.
|
| 69 |
+
- You can host it on your own infrastructure, or push it to Spaces for instant integration into MCP-enabled environments.
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
## 🤗 How to Use (in Hugging Face Space)
|
| 74 |
+
|
| 75 |
+
👉 **Space UI**: [https://huggingface.co/spaces/lepanto1571/posebusters-wrapper-MCP](https://huggingface.co/spaces/lepanto1571/posebusters-wrapper-MCP)
|
| 76 |
+
|
| 77 |
+
- Upload your `.sdf` ligand and `.pdb` protein files
|
| 78 |
+
- (Optional) Add a `.sdf` with the ''true'' crystal ligand
|
| 79 |
+
- Click on **Submit**
|
| 80 |
+
- Results will appear in the interactive table
|
| 81 |
+
|
| 82 |
+
---
|
| 83 |
+
|
| 84 |
+
## 🐳 Run Locally with Docker
|
| 85 |
+
### 1. Clone the repository
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
git clone https://github.com/lepanto1571/posebusters-wrapper-MCP.git
|
| 89 |
+
cd posebusters-wrapper-MCP
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
### 2. Build the Docker image
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
docker buildx build --load -t posebusters-wrapper-mcp .
|
| 96 |
+
```
|
| 97 |
+
### 3. Run the container
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
docker run -p 7860:7860 posebusters-wrapper-mcp
|
| 101 |
+
```
|
| 102 |
+
The server will start on http://localhost:7860.
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
## ⚙️ How to Use the API (MCP-compatible)
|
| 107 |
+
|
| 108 |
+
### 🔎 1. Discover API via MCP Context
|
| 109 |
+
|
| 110 |
+
```bash
|
| 111 |
+
# Using curl
|
| 112 |
+
curl -X GET http://localhost:7860/mcp/context
|
| 113 |
+
|
| 114 |
+
# Using Python
|
| 115 |
+
import requests
|
| 116 |
+
response = requests.get("http://localhost:7860/mcp/context")
|
| 117 |
+
context = response.json()
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
### 2. Run validation (ligand + protein)
|
| 121 |
+
|
| 122 |
+
```bash
|
| 123 |
+
# Using curl
|
| 124 |
+
curl -X POST http://localhost:7860/mcp/predict \
|
| 125 |
+
-F action=validate_pose \
|
| 126 |
+
-F ligand_input=@ligand.sdf \
|
| 127 |
+
-F protein_input=@protein.pdb
|
| 128 |
+
|
| 129 |
+
# Using Python
|
| 130 |
+
import requests
|
| 131 |
+
|
| 132 |
+
files = {
|
| 133 |
+
'ligand_input': ('ligand.sdf', open('ligand.sdf', 'rb')),
|
| 134 |
+
'protein_input': ('protein.pdb', open('protein.pdb', 'rb'))
|
| 135 |
+
}
|
| 136 |
+
data = {'action': 'validate_pose'}
|
| 137 |
+
|
| 138 |
+
response = requests.post(
|
| 139 |
+
"http://localhost:7860/mcp/predict",
|
| 140 |
+
files=files,
|
| 141 |
+
data=data
|
| 142 |
+
)
|
| 143 |
+
results = response.json()
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
### 3. Run redocking validation (ligand + crystal + protein)
|
| 147 |
+
|
| 148 |
+
```bash
|
| 149 |
+
# Using curl
|
| 150 |
+
curl -X POST http://localhost:7860/mcp/predict \
|
| 151 |
+
-F action=redocking_validation \
|
| 152 |
+
-F ligand_input=@ligand.sdf \
|
| 153 |
+
-F protein_input=@protein.pdb \
|
| 154 |
+
-F crystal_input=@crystal.sdf
|
| 155 |
+
|
| 156 |
+
# Using Python
|
| 157 |
+
import requests
|
| 158 |
+
|
| 159 |
+
files = {
|
| 160 |
+
'ligand_input': ('ligand.sdf', open('ligand.sdf', 'rb')),
|
| 161 |
+
'protein_input': ('protein.pdb', open('protein.pdb', 'rb')),
|
| 162 |
+
'crystal_input': ('crystal.sdf', open('crystal.sdf', 'rb'))
|
| 163 |
+
}
|
| 164 |
+
data = {'action': 'redocking_validation'}
|
| 165 |
+
|
| 166 |
+
response = requests.post(
|
| 167 |
+
"http://localhost:7860/mcp/predict",
|
| 168 |
+
files=files,
|
| 169 |
+
data=data
|
| 170 |
+
)
|
| 171 |
+
results = response.json()
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
### Response Format
|
| 175 |
+
|
| 176 |
+
All responses follow the MCP standard format:
|
| 177 |
+
|
| 178 |
+
```python
|
| 179 |
+
{
|
| 180 |
+
"object_id": "validation_results",
|
| 181 |
+
"data": {
|
| 182 |
+
"columns": ["ligand_id", "status", "passed/total", "details"],
|
| 183 |
+
"rows": [
|
| 184 |
+
["mol1", "✅", "8/8", "All tests passed"],
|
| 185 |
+
# ... more results
|
| 186 |
+
]
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
### Error Handling
|
| 192 |
+
|
| 193 |
+
The API uses standard HTTP status codes:
|
| 194 |
+
- 200: Success
|
| 195 |
+
- 400: Invalid request (wrong file type, missing required files)
|
| 196 |
+
- 500: Server error (validation failed, internal error)
|
| 197 |
+
|
| 198 |
+
Error responses include detailed messages:
|
| 199 |
+
```python
|
| 200 |
+
{
|
| 201 |
+
"object_id": "validation_results",
|
| 202 |
+
"data": {
|
| 203 |
+
"columns": ["ligand_id", "status", "passed/total", "details"],
|
| 204 |
+
"rows": [
|
| 205 |
+
["unknown", "❌", "0/0", "Detailed error message"]
|
| 206 |
+
]
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
## 🧪 Development and Testing
|
| 212 |
+
|
| 213 |
+
### Running Tests
|
| 214 |
+
|
| 215 |
+
Tests can be run directly using Docker:
|
| 216 |
+
|
| 217 |
+
```bash
|
| 218 |
+
# Run tests with verbose output
|
| 219 |
+
docker run posebusters-wrapper-mcp pytest -v
|
| 220 |
+
|
| 221 |
+
# Run tests with coverage report
|
| 222 |
+
docker run posebusters-wrapper-mcp pytest --cov=. --cov-report=term-missing
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
### Validation
|
| 226 |
+
|
| 227 |
+
The API uses JSON Schema validation for:
|
| 228 |
+
- MCP Context (`/mcp/context`)
|
| 229 |
+
- Prediction Responses (`/mcp/predict`)
|
| 230 |
+
- File Types (MIME validation)
|
| 231 |
+
|
| 232 |
+
Schema definitions are in `schema.py`.
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
## 📚 Documentation & Citation
|
| 237 |
+
### 📖 PoseBusters documentation:
|
| 238 |
+
Full usage and command-line reference available at
|
| 239 |
+
https://posebusters.readthedocs.io/en/latest
|
| 240 |
+
|
| 241 |
+
### 🧾 Scientific paper:
|
| 242 |
+
Martin Buttenschoen, Andreas Bender (2023). "PoseBusters: a consistency check for 3D protein–ligand binding poses".
|
| 243 |
+
Read it on arXiv: https://arxiv.org/abs/2308.05777
|
| 244 |
+
|
| 245 |
+
### 💡 If you use this wrapper or PoseBusters in your work, consider citing the original paper.
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
+
## 📄 License and Credits
|
| 250 |
+
This project uses [PoseBusters](https://github.com/maabuu/posebusters) by Martin Buttenschoen (© 2023) under the BSD 3-Clause License.
|
| 251 |
+
This service is **not affiliated with or endorsed by** the original author.
|
| 252 |
+
|
app.py
CHANGED
|
@@ -1,242 +1,182 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import
|
| 5 |
-
import
|
| 6 |
-
import
|
| 7 |
-
import
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
import
|
| 11 |
-
import
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
):
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
"name": "validate_pose",
|
| 184 |
-
"description": "Validate ligand–protein structure",
|
| 185 |
-
"parameters": {
|
| 186 |
-
"ligand_path": "ligand_input",
|
| 187 |
-
"protein_path": "protein_input"
|
| 188 |
-
}
|
| 189 |
-
},
|
| 190 |
-
{
|
| 191 |
-
"name": "redocking_validation",
|
| 192 |
-
"description": "Validate redocking using a reference ligand",
|
| 193 |
-
"parameters": {
|
| 194 |
-
"ligand_path": "ligand_input",
|
| 195 |
-
"crystal_path": "crystal_input",
|
| 196 |
-
"protein_path": "protein_input"
|
| 197 |
-
}
|
| 198 |
-
}
|
| 199 |
-
]
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
import gradio.themes.base
|
| 203 |
-
|
| 204 |
-
theme = gradio.themes.Base()
|
| 205 |
-
|
| 206 |
-
iface = gr.Interface(
|
| 207 |
-
fn=gradio_ui,
|
| 208 |
-
inputs=[
|
| 209 |
-
gr.File(label="Ligand (.sdf)", type="filepath"),
|
| 210 |
-
gr.File(label="Protein (.pdb)", type="filepath"),
|
| 211 |
-
gr.File(label="Crystal Ligand (.sdf, optional)", type="filepath")
|
| 212 |
-
],
|
| 213 |
-
outputs="json",
|
| 214 |
-
title="PoseBusters Wrapper MCP",
|
| 215 |
-
description="Free MCP-compatible API for validating ligand–protein structures using PoseBusters.",
|
| 216 |
-
theme=theme
|
| 217 |
-
)
|
| 218 |
-
|
| 219 |
-
context_button = gr.Interface(
|
| 220 |
-
fn=get_mcp_context,
|
| 221 |
-
inputs=[],
|
| 222 |
-
outputs="json",
|
| 223 |
-
title="MCP Context",
|
| 224 |
-
description="Returns the MCP context metadata for the app.",
|
| 225 |
-
theme=theme
|
| 226 |
-
)
|
| 227 |
-
|
| 228 |
-
demo = gr.TabbedInterface(
|
| 229 |
-
interface_list=[iface, context_button],
|
| 230 |
-
tab_names=["Validation", "MCP Context"],
|
| 231 |
-
theme=theme
|
| 232 |
-
)
|
| 233 |
-
|
| 234 |
-
def main():
|
| 235 |
-
if os.environ.get("MCP_LOCAL") == "2":
|
| 236 |
-
print("Running MCP server on 0.0.0.0:7860")
|
| 237 |
-
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 238 |
-
else:
|
| 239 |
-
demo.launch(server_name="0.0.0.0")
|
| 240 |
-
|
| 241 |
-
if __name__ == "__main__":
|
| 242 |
-
main()
|
|
|
|
| 1 |
+
# Standard library imports
|
| 2 |
+
import os
|
| 3 |
+
import io
|
| 4 |
+
import asyncio
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Dict, Optional
|
| 7 |
+
from contextlib import contextmanager
|
| 8 |
+
|
| 9 |
+
# Third-party imports
|
| 10 |
+
import gradio as gr
|
| 11 |
+
from fastapi import FastAPI, File, Form, UploadFile
|
| 12 |
+
import uvicorn
|
| 13 |
+
import json
|
| 14 |
+
|
| 15 |
+
# Local imports
|
| 16 |
+
from utils import validate_mime_type, format_results_to_mcp
|
| 17 |
+
from main import predict, app as fastapi_app
|
| 18 |
+
|
| 19 |
+
# Check if we're running in Hugging Face Spaces
|
| 20 |
+
IS_HF_SPACE = os.getenv("SPACE_ID") is not None
|
| 21 |
+
|
| 22 |
+
# Store the original working directory
|
| 23 |
+
ORIGINAL_CWD = os.getcwd()
|
| 24 |
+
|
| 25 |
+
@contextmanager
|
| 26 |
+
def stable_cwd():
|
| 27 |
+
"""Context manager to maintain a stable working directory."""
|
| 28 |
+
current_dir = os.getcwd()
|
| 29 |
+
try:
|
| 30 |
+
os.chdir(ORIGINAL_CWD)
|
| 31 |
+
yield
|
| 32 |
+
finally:
|
| 33 |
+
os.chdir(current_dir)
|
| 34 |
+
|
| 35 |
+
# Get MCP context with proper path handling
|
| 36 |
+
def get_mcp_context_wrapper() -> Dict[str, Any]:
|
| 37 |
+
"""Wrapper around get_mcp_context that handles Spaces paths."""
|
| 38 |
+
try:
|
| 39 |
+
with stable_cwd():
|
| 40 |
+
# In Spaces, we know the file is in the root directory
|
| 41 |
+
if IS_HF_SPACE:
|
| 42 |
+
context_path = Path(ORIGINAL_CWD) / 'mcp_context.json'
|
| 43 |
+
if not context_path.exists():
|
| 44 |
+
raise FileNotFoundError(f"MCP context file not found at {context_path}")
|
| 45 |
+
with open(context_path, 'r') as f:
|
| 46 |
+
return json.load(f)
|
| 47 |
+
# Otherwise use the utility function that searches multiple locations
|
| 48 |
+
from utils import get_mcp_context
|
| 49 |
+
return get_mcp_context()
|
| 50 |
+
except Exception as e:
|
| 51 |
+
return {
|
| 52 |
+
"error": f"Failed to load MCP context: {str(e)}",
|
| 53 |
+
"app": {
|
| 54 |
+
"id": "lepanto1571.wrapper.posebusters",
|
| 55 |
+
"name": "PoseBusters MCP wrapper",
|
| 56 |
+
"version": "0.5"
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
async def process_files(ligand_file, protein_file, crystal_file=None):
|
| 61 |
+
"""Process uploaded files using the robust predict function from main.py."""
|
| 62 |
+
try:
|
| 63 |
+
with stable_cwd():
|
| 64 |
+
# Handle file uploads in a way that works in both environments
|
| 65 |
+
async def create_upload_file(file_obj) -> Optional[UploadFile]:
|
| 66 |
+
if file_obj is None:
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
# Get the filename, handling both string paths and file-like objects
|
| 71 |
+
if hasattr(file_obj, 'orig_name'):
|
| 72 |
+
filename = file_obj.orig_name
|
| 73 |
+
elif hasattr(file_obj, 'name'):
|
| 74 |
+
filename = file_obj.name
|
| 75 |
+
else:
|
| 76 |
+
filename = str(file_obj)
|
| 77 |
+
|
| 78 |
+
# Create a UploadFile object that works in both environments
|
| 79 |
+
if hasattr(file_obj, 'read'):
|
| 80 |
+
# File-like object (e.g. in Spaces or already open file)
|
| 81 |
+
content = await file_obj.read() if asyncio.iscoroutinefunction(file_obj.read) else file_obj.read()
|
| 82 |
+
if hasattr(file_obj, 'seek'):
|
| 83 |
+
file_obj.seek(0) # Reset file pointer for future reads
|
| 84 |
+
file_like = io.BytesIO(content if isinstance(content, bytes) else content.encode('utf-8'))
|
| 85 |
+
return UploadFile(
|
| 86 |
+
file=file_like,
|
| 87 |
+
filename=Path(filename).name
|
| 88 |
+
)
|
| 89 |
+
elif hasattr(file_obj, 'name') and os.path.exists(file_obj.name):
|
| 90 |
+
# Local file path that exists
|
| 91 |
+
return UploadFile(
|
| 92 |
+
file=open(file_obj.name, "rb"),
|
| 93 |
+
filename=Path(filename).name
|
| 94 |
+
)
|
| 95 |
+
else:
|
| 96 |
+
raise ValueError(f"Unable to read file: {filename}")
|
| 97 |
+
|
| 98 |
+
except Exception as e:
|
| 99 |
+
raise ValueError(f"Failed to process file {getattr(file_obj, 'name', str(file_obj))}: {str(e)}")
|
| 100 |
+
|
| 101 |
+
# Convert files to UploadFile objects
|
| 102 |
+
ligand_input = await create_upload_file(ligand_file)
|
| 103 |
+
protein_input = await create_upload_file(protein_file)
|
| 104 |
+
crystal_input = await create_upload_file(crystal_file)
|
| 105 |
+
|
| 106 |
+
if not ligand_input or not protein_input:
|
| 107 |
+
raise ValueError("Both ligand and protein files are required")
|
| 108 |
+
|
| 109 |
+
# Use the action based on whether crystal file is provided
|
| 110 |
+
action = "redocking_validation" if crystal_input else "validate_pose"
|
| 111 |
+
|
| 112 |
+
# Call the robust predict function
|
| 113 |
+
return await predict(
|
| 114 |
+
action=action,
|
| 115 |
+
ligand_input=ligand_input,
|
| 116 |
+
protein_input=protein_input,
|
| 117 |
+
crystal_input=crystal_input
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
except Exception as e:
|
| 121 |
+
return {
|
| 122 |
+
"object_id": "validation_results",
|
| 123 |
+
"data": {
|
| 124 |
+
"columns": ["ligand_id", "status", "passed/total", "details"],
|
| 125 |
+
"rows": [[
|
| 126 |
+
Path(getattr(ligand_file, 'orig_name',
|
| 127 |
+
getattr(ligand_file, 'name', 'unknown'))).stem,
|
| 128 |
+
"❌",
|
| 129 |
+
"0/0",
|
| 130 |
+
str(e)
|
| 131 |
+
]]
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
theme = gr.themes.Base()
|
| 136 |
+
|
| 137 |
+
# Main validation interface
|
| 138 |
+
validation_ui = gr.Interface(
|
| 139 |
+
fn=process_files,
|
| 140 |
+
inputs=[
|
| 141 |
+
gr.File(label="Ligand (.sdf)", type="filepath", file_count="single"),
|
| 142 |
+
gr.File(label="Protein (.pdb)", type="filepath", file_count="single"),
|
| 143 |
+
gr.File(label="Crystal Ligand (.sdf, optional)", type="filepath", file_count="single")
|
| 144 |
+
],
|
| 145 |
+
outputs="json",
|
| 146 |
+
title="PoseBusters Validation",
|
| 147 |
+
description="Upload files to validate ligand–protein structures using PoseBusters.",
|
| 148 |
+
theme=theme,
|
| 149 |
+
cache_examples=False # Disable caching to prevent file handle issues
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
# MCP Context viewer
|
| 153 |
+
context_ui = gr.Interface(
|
| 154 |
+
fn=get_mcp_context_wrapper,
|
| 155 |
+
inputs=None,
|
| 156 |
+
outputs=gr.JSON(),
|
| 157 |
+
title="MCP Context",
|
| 158 |
+
description="View the MCP context metadata that defines this tool's capabilities.",
|
| 159 |
+
theme=theme,
|
| 160 |
+
cache_examples=False # Disable caching to prevent file handle issues
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
# Combined interface
|
| 164 |
+
demo = gr.TabbedInterface(
|
| 165 |
+
interface_list=[validation_ui, context_ui],
|
| 166 |
+
tab_names=["💊 Validation", "🔍 MCP Context"],
|
| 167 |
+
title="PoseBusters MCP Wrapper",
|
| 168 |
+
theme=theme
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
def main():
|
| 172 |
+
"""Run the server based on environment."""
|
| 173 |
+
if IS_HF_SPACE:
|
| 174 |
+
# In Hugging Face Spaces, just run the Gradio app
|
| 175 |
+
demo.launch(server_name="0.0.0.0")
|
| 176 |
+
else:
|
| 177 |
+
# For local/Docker, mount Gradio into FastAPI and run the server
|
| 178 |
+
app = gr.mount_gradio_app(fastapi_app, demo, path="/")
|
| 179 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 180 |
+
|
| 181 |
+
if __name__ == "__main__":
|
| 182 |
+
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data/1of6_ligand.sdf
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DTY
|
| 2 |
+
RDKit 3D
|
| 3 |
+
|
| 4 |
+
13 13 0 0 1 0 0 0 0 0999 V2000
|
| 5 |
+
30.6770 -3.3700 50.5160 N 0 0 0 0 0 0 0 0 0 0 0 0
|
| 6 |
+
31.3780 -3.1310 49.2550 C 0 0 2 0 0 0 0 0 0 0 0 0
|
| 7 |
+
31.9670 -4.4050 48.6940 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 8 |
+
32.0170 -5.4920 49.3380 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 9 |
+
32.4890 -2.1160 49.5260 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 10 |
+
32.0300 -0.7150 49.8830 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 11 |
+
32.2930 -0.2240 51.1250 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 12 |
+
31.3170 0.0570 48.9730 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 13 |
+
31.8570 1.0320 51.4460 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 14 |
+
30.9070 1.3420 49.2730 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 15 |
+
31.2050 1.8270 50.5410 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 16 |
+
30.8150 2.9790 50.9180 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 17 |
+
32.3720 -4.3160 47.5370 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 18 |
+
2 1 1 1
|
| 19 |
+
2 3 1 0
|
| 20 |
+
2 5 1 0
|
| 21 |
+
3 4 2 0
|
| 22 |
+
3 13 1 0
|
| 23 |
+
5 6 1 0
|
| 24 |
+
6 7 2 0
|
| 25 |
+
6 8 1 0
|
| 26 |
+
7 9 1 0
|
| 27 |
+
8 10 2 0
|
| 28 |
+
9 11 2 0
|
| 29 |
+
10 11 1 0
|
| 30 |
+
11 12 1 0
|
| 31 |
+
M END
|
data/1of6_ligands.sdf
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DTY
|
| 2 |
+
RDKit 3D
|
| 3 |
+
|
| 4 |
+
13 13 0 0 1 0 0 0 0 0999 V2000
|
| 5 |
+
30.2540 -2.5680 74.6470 N 0 0 0 0 0 0 0 0 0 0 0 0
|
| 6 |
+
29.6440 -2.9290 75.9150 C 0 0 2 0 0 0 0 0 0 0 0 0
|
| 7 |
+
30.4450 -3.9540 76.6840 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 8 |
+
31.3690 -4.4990 76.1010 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 9 |
+
28.2660 -3.5000 75.5090 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 10 |
+
27.2580 -2.4780 75.0340 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 11 |
+
26.8210 -1.4240 75.8630 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 12 |
+
26.7940 -2.4670 73.7230 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 13 |
+
25.8850 -0.4270 75.4190 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 14 |
+
25.9000 -1.4590 73.2560 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 15 |
+
25.4270 -0.3980 74.0850 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 16 |
+
24.6220 0.5320 73.6460 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 17 |
+
30.1990 -4.2930 77.8630 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 18 |
+
2 1 1 6
|
| 19 |
+
2 3 1 0
|
| 20 |
+
2 5 1 0
|
| 21 |
+
3 4 2 0
|
| 22 |
+
3 13 1 0
|
| 23 |
+
5 6 1 0
|
| 24 |
+
6 7 2 0
|
| 25 |
+
6 8 1 0
|
| 26 |
+
7 9 1 0
|
| 27 |
+
8 10 2 0
|
| 28 |
+
9 11 2 0
|
| 29 |
+
10 11 1 0
|
| 30 |
+
11 12 1 0
|
| 31 |
+
M END
|
| 32 |
+
$$$$
|
| 33 |
+
DTY
|
| 34 |
+
RDKit 3D
|
| 35 |
+
|
| 36 |
+
13 13 0 0 1 0 0 0 0 0999 V2000
|
| 37 |
+
9.9250 13.9750 69.7540 N 0 0 0 0 0 0 0 0 0 0 0 0
|
| 38 |
+
9.6350 13.8330 71.1740 C 0 0 2 0 0 0 0 0 0 0 0 0
|
| 39 |
+
8.4120 14.6560 71.5400 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 40 |
+
7.9590 15.4550 70.7310 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 41 |
+
10.8440 14.2980 71.9980 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 42 |
+
11.9810 13.3300 72.0410 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 43 |
+
13.2260 13.7020 71.5180 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 44 |
+
11.8380 12.0740 72.5910 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 45 |
+
14.2940 12.8090 71.5560 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 46 |
+
12.8680 11.1570 72.5970 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 47 |
+
14.1380 11.5440 72.1180 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 48 |
+
15.1170 10.6980 72.1020 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 49 |
+
7.8390 14.5290 72.6020 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 50 |
+
2 1 1 6
|
| 51 |
+
2 3 1 0
|
| 52 |
+
2 5 1 0
|
| 53 |
+
3 4 2 0
|
| 54 |
+
3 13 1 0
|
| 55 |
+
5 6 1 0
|
| 56 |
+
6 7 2 0
|
| 57 |
+
6 8 1 0
|
| 58 |
+
7 9 1 0
|
| 59 |
+
8 10 2 0
|
| 60 |
+
9 11 2 0
|
| 61 |
+
10 11 1 0
|
| 62 |
+
11 12 1 0
|
| 63 |
+
M END
|
| 64 |
+
$$$$
|
| 65 |
+
DTY
|
| 66 |
+
RDKit 3D
|
| 67 |
+
|
| 68 |
+
13 13 0 0 1 0 0 0 0 0999 V2000
|
| 69 |
+
26.1300 22.5850 52.9470 N 0 0 0 0 0 0 0 0 0 0 0 0
|
| 70 |
+
26.5090 22.8290 51.5510 C 0 0 2 0 0 0 0 0 0 0 0 0
|
| 71 |
+
26.3970 24.3040 51.1390 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 72 |
+
25.9330 25.2450 51.8910 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 73 |
+
25.5510 21.9320 50.7760 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 74 |
+
25.7750 20.4360 50.9080 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 75 |
+
27.0050 19.8350 50.4840 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 76 |
+
24.8040 19.6220 51.4360 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 77 |
+
27.2260 18.4680 50.6160 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 78 |
+
25.0330 18.2430 51.5690 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 79 |
+
26.1980 17.6590 51.1350 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 80 |
+
26.3460 16.4460 51.3140 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 81 |
+
26.8520 24.5980 50.0070 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 82 |
+
2 1 1 1
|
| 83 |
+
2 3 1 0
|
| 84 |
+
2 5 1 0
|
| 85 |
+
3 4 2 0
|
| 86 |
+
3 13 1 0
|
| 87 |
+
5 6 1 0
|
| 88 |
+
6 7 2 0
|
| 89 |
+
6 8 1 0
|
| 90 |
+
7 9 1 0
|
| 91 |
+
8 10 2 0
|
| 92 |
+
9 11 2 0
|
| 93 |
+
10 11 1 0
|
| 94 |
+
11 12 1 0
|
| 95 |
+
M END
|
| 96 |
+
$$$$
|
| 97 |
+
DTY
|
| 98 |
+
RDKit 3D
|
| 99 |
+
|
| 100 |
+
13 13 0 0 1 0 0 0 0 0999 V2000
|
| 101 |
+
84.1470 43.7570 49.3420 N 0 0 0 0 0 0 0 0 0 0 0 0
|
| 102 |
+
84.7810 44.0620 48.0210 C 0 0 2 0 0 0 0 0 0 0 0 0
|
| 103 |
+
85.3370 42.8890 47.2840 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 104 |
+
85.8700 43.0440 46.2130 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 105 |
+
85.9370 45.0260 48.2620 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 106 |
+
85.3930 46.3460 48.7450 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 107 |
+
85.6440 46.7700 50.0290 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 108 |
+
84.6580 47.1680 47.9060 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 109 |
+
85.1490 47.9950 50.4910 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 110 |
+
84.1750 48.3890 48.3590 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 111 |
+
84.4030 48.8190 49.6570 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 112 |
+
83.9390 49.9130 50.0990 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 113 |
+
85.2190 41.7490 47.7280 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 114 |
+
2 1 1 1
|
| 115 |
+
2 3 1 0
|
| 116 |
+
2 5 1 0
|
| 117 |
+
3 4 2 0
|
| 118 |
+
3 13 1 0
|
| 119 |
+
5 6 1 0
|
| 120 |
+
6 7 2 0
|
| 121 |
+
6 8 1 0
|
| 122 |
+
7 9 1 0
|
| 123 |
+
8 10 2 0
|
| 124 |
+
9 11 2 0
|
| 125 |
+
10 11 1 0
|
| 126 |
+
11 12 1 0
|
| 127 |
+
M END
|
| 128 |
+
$$$$
|
| 129 |
+
DTY
|
| 130 |
+
RDKit 3D
|
| 131 |
+
|
| 132 |
+
13 13 0 0 1 0 0 0 0 0999 V2000
|
| 133 |
+
64.2120 59.0260 71.4630 N 0 0 0 0 0 0 0 0 0 0 0 0
|
| 134 |
+
63.9150 58.6880 72.8890 C 0 0 2 0 0 0 0 0 0 0 0 0
|
| 135 |
+
62.7790 59.4940 73.4060 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 136 |
+
62.2610 59.3030 74.5120 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 137 |
+
65.1260 59.0470 73.7540 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 138 |
+
66.3630 58.2190 73.5400 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 139 |
+
67.4950 58.7590 72.8930 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 140 |
+
66.3690 56.9090 74.0520 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 141 |
+
68.6130 57.9270 72.7050 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 142 |
+
67.5080 56.1000 73.8880 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 143 |
+
68.6070 56.5980 73.2120 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 144 |
+
69.6140 55.8620 73.0710 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 145 |
+
62.3520 60.3180 72.6160 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 146 |
+
2 1 1 6
|
| 147 |
+
2 3 1 0
|
| 148 |
+
2 5 1 0
|
| 149 |
+
3 4 2 0
|
| 150 |
+
3 13 1 0
|
| 151 |
+
5 6 1 0
|
| 152 |
+
6 7 2 0
|
| 153 |
+
6 8 1 0
|
| 154 |
+
7 9 1 0
|
| 155 |
+
8 10 2 0
|
| 156 |
+
9 11 2 0
|
| 157 |
+
10 11 1 0
|
| 158 |
+
11 12 1 0
|
| 159 |
+
M END
|
| 160 |
+
$$$$
|
| 161 |
+
DTY
|
| 162 |
+
RDKit 3D
|
| 163 |
+
|
| 164 |
+
13 13 0 0 1 0 0 0 0 0999 V2000
|
| 165 |
+
85.3020 43.0320 73.7720 N 0 0 0 0 0 0 0 0 0 0 0 0
|
| 166 |
+
84.8700 42.5910 75.0950 C 0 0 2 0 0 0 0 0 0 0 0 0
|
| 167 |
+
85.7370 41.5470 75.7070 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 168 |
+
86.6680 41.0290 75.0590 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 169 |
+
83.5050 42.0000 74.8990 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 170 |
+
82.4380 43.0170 74.5710 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 171 |
+
81.9960 43.9160 75.4990 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 172 |
+
81.8350 43.0290 73.3200 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 173 |
+
81.0170 44.8750 75.1840 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 174 |
+
80.8710 43.9700 72.9940 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 175 |
+
80.4220 44.9020 73.9230 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 176 |
+
79.4820 45.7400 73.6110 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 177 |
+
85.4920 41.1990 76.8520 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 178 |
+
2 1 1 6
|
| 179 |
+
2 3 1 0
|
| 180 |
+
2 5 1 0
|
| 181 |
+
3 4 2 0
|
| 182 |
+
3 13 1 0
|
| 183 |
+
5 6 1 0
|
| 184 |
+
6 7 2 0
|
| 185 |
+
6 8 1 0
|
| 186 |
+
7 9 1 0
|
| 187 |
+
8 10 2 0
|
| 188 |
+
9 11 2 0
|
| 189 |
+
10 11 1 0
|
| 190 |
+
11 12 1 0
|
| 191 |
+
M END
|
| 192 |
+
$$$$
|
| 193 |
+
DTY
|
| 194 |
+
RDKit 3D
|
| 195 |
+
|
| 196 |
+
13 13 0 0 1 0 0 0 0 0999 V2000
|
| 197 |
+
30.6770 -3.3700 50.5160 N 0 0 0 0 0 0 0 0 0 0 0 0
|
| 198 |
+
31.3780 -3.1310 49.2550 C 0 0 2 0 0 0 0 0 0 0 0 0
|
| 199 |
+
31.9670 -4.4050 48.6940 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 200 |
+
32.0170 -5.4920 49.3380 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 201 |
+
32.4890 -2.1160 49.5260 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 202 |
+
32.0300 -0.7150 49.8830 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 203 |
+
32.2930 -0.2240 51.1250 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 204 |
+
31.3170 0.0570 48.9730 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 205 |
+
31.8570 1.0320 51.4460 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 206 |
+
30.9070 1.3420 49.2730 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 207 |
+
31.2050 1.8270 50.5410 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 208 |
+
30.8150 2.9790 50.9180 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 209 |
+
32.3720 -4.3160 47.5370 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 210 |
+
2 1 1 1
|
| 211 |
+
2 3 1 0
|
| 212 |
+
2 5 1 0
|
| 213 |
+
3 4 2 0
|
| 214 |
+
3 13 1 0
|
| 215 |
+
5 6 1 0
|
| 216 |
+
6 7 2 0
|
| 217 |
+
6 8 1 0
|
| 218 |
+
7 9 1 0
|
| 219 |
+
8 10 2 0
|
| 220 |
+
9 11 2 0
|
| 221 |
+
10 11 1 0
|
| 222 |
+
11 12 1 0
|
| 223 |
+
M END
|
| 224 |
+
$$$$
|
| 225 |
+
DTY
|
| 226 |
+
RDKit 3D
|
| 227 |
+
|
| 228 |
+
13 13 0 0 1 0 0 0 0 0999 V2000
|
| 229 |
+
78.1870 69.1110 54.6720 N 0 0 0 0 0 0 0 0 0 0 0 0
|
| 230 |
+
78.4910 69.2750 53.2300 C 0 0 2 0 0 0 0 0 0 0 0 0
|
| 231 |
+
78.2650 70.7210 52.9630 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 232 |
+
77.6840 71.4160 53.7710 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 233 |
+
77.6090 68.4400 52.2900 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 234 |
+
77.8710 66.9670 52.3560 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 235 |
+
79.0090 66.4720 51.7490 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 236 |
+
77.0040 66.0680 53.0050 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 237 |
+
79.3270 65.1250 51.8140 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 238 |
+
77.3050 64.7110 53.0120 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 239 |
+
78.4750 64.2220 52.4400 C 0 0 0 0 0 0 0 0 0 0 0 0
|
| 240 |
+
78.7940 62.9710 52.5270 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 241 |
+
78.5830 71.2520 51.9460 O 0 0 0 0 0 0 0 0 0 0 0 0
|
| 242 |
+
2 1 1 1
|
| 243 |
+
2 3 1 0
|
| 244 |
+
2 5 1 0
|
| 245 |
+
3 4 2 0
|
| 246 |
+
3 13 1 0
|
| 247 |
+
5 6 1 0
|
| 248 |
+
6 7 2 0
|
| 249 |
+
6 8 1 0
|
| 250 |
+
7 9 1 0
|
| 251 |
+
8 10 2 0
|
| 252 |
+
9 11 2 0
|
| 253 |
+
10 11 1 0
|
| 254 |
+
11 12 1 0
|
| 255 |
+
M END
|
| 256 |
+
$$$$
|
data/1of6_protein_one_lig_removed.pdb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
main.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Standard library imports
|
| 2 |
+
import os
|
| 3 |
+
import subprocess
|
| 4 |
+
import tempfile
|
| 5 |
+
import traceback
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Dict, Any, Optional
|
| 8 |
+
|
| 9 |
+
# Third-party imports
|
| 10 |
+
import gradio as gr
|
| 11 |
+
from fastapi import FastAPI, File, Form, UploadFile, HTTPException
|
| 12 |
+
from fastapi.responses import JSONResponse
|
| 13 |
+
from jsonschema import validate, ValidationError
|
| 14 |
+
from schema import MCP_CONTEXT_SCHEMA, MCP_PREDICT_RESPONSE_SCHEMA
|
| 15 |
+
|
| 16 |
+
# Local imports
|
| 17 |
+
from utils import validate_mime_type, format_results_to_mcp, get_mcp_context, ALLOWED_MIME_TYPES
|
| 18 |
+
|
| 19 |
+
app = FastAPI(title="PoseBusters MCP API")
|
| 20 |
+
|
| 21 |
+
@app.post("/mcp/predict")
|
| 22 |
+
async def predict(
|
| 23 |
+
action: str = Form(...),
|
| 24 |
+
ligand_input: UploadFile = File(...),
|
| 25 |
+
protein_input: UploadFile = File(...),
|
| 26 |
+
crystal_input: Optional[UploadFile] = File(None),
|
| 27 |
+
) -> Dict[str, Any]:
|
| 28 |
+
"""
|
| 29 |
+
MCP-compliant prediction endpoint.
|
| 30 |
+
Validates file types and runs PoseBusters validation.
|
| 31 |
+
Response is validated against MCP_PREDICT_RESPONSE_SCHEMA.
|
| 32 |
+
"""
|
| 33 |
+
try:
|
| 34 |
+
# Validate MIME types
|
| 35 |
+
validate_mime_type(ligand_input, ".sdf")
|
| 36 |
+
validate_mime_type(protein_input, ".pdb")
|
| 37 |
+
if crystal_input:
|
| 38 |
+
validate_mime_type(crystal_input, ".sdf")
|
| 39 |
+
|
| 40 |
+
async def save_upload(upload: UploadFile, suffix: str) -> str:
|
| 41 |
+
"""Safely save uploaded file with secure naming."""
|
| 42 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
| 43 |
+
content = await upload.read()
|
| 44 |
+
tmp.write(content)
|
| 45 |
+
return tmp.name
|
| 46 |
+
|
| 47 |
+
# Create a temporary directory for isolated execution
|
| 48 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 49 |
+
try:
|
| 50 |
+
# Save files in the temporary directory
|
| 51 |
+
ligand_path = await save_upload(ligand_input, ".sdf")
|
| 52 |
+
protein_path = await save_upload(protein_input, ".pdb")
|
| 53 |
+
crystal_path = await save_upload(crystal_input, ".sdf") if crystal_input else None
|
| 54 |
+
|
| 55 |
+
# Move to temporary directory
|
| 56 |
+
os.chdir(tmpdir)
|
| 57 |
+
|
| 58 |
+
if action == "validate_pose":
|
| 59 |
+
cmd = ["bust", ligand_path, "-p", protein_path, "--outfmt", "csv"]
|
| 60 |
+
elif action == "redocking_validation":
|
| 61 |
+
if not crystal_path:
|
| 62 |
+
raise HTTPException(status_code=400, detail="Missing crystal ligand file")
|
| 63 |
+
cmd = ["bust", ligand_path, "-l", crystal_path, "-p", protein_path, "--outfmt", "csv"]
|
| 64 |
+
else:
|
| 65 |
+
raise HTTPException(status_code=400, detail=f"Unknown action: {action}")
|
| 66 |
+
|
| 67 |
+
# Use list form of subprocess.run to avoid shell injection
|
| 68 |
+
# Add timeout of 5 minutes
|
| 69 |
+
result = subprocess.run(
|
| 70 |
+
cmd,
|
| 71 |
+
capture_output=True,
|
| 72 |
+
text=True,
|
| 73 |
+
check=False, # Don't raise on non-zero exit
|
| 74 |
+
timeout=300 # 5 minutes timeout
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
response = format_results_to_mcp(
|
| 78 |
+
result.stdout,
|
| 79 |
+
result.stderr,
|
| 80 |
+
Path(ligand_input.filename).stem
|
| 81 |
+
)
|
| 82 |
+
try:
|
| 83 |
+
validate(instance=response, schema=MCP_PREDICT_RESPONSE_SCHEMA)
|
| 84 |
+
return response
|
| 85 |
+
except ValidationError as e:
|
| 86 |
+
return JSONResponse(
|
| 87 |
+
status_code=500,
|
| 88 |
+
content={
|
| 89 |
+
"object_id": "validation_results",
|
| 90 |
+
"data": {
|
| 91 |
+
"columns": ["ligand_id", "status", "passed/total", "details"],
|
| 92 |
+
"rows": [[
|
| 93 |
+
Path(ligand_input.filename).stem,
|
| 94 |
+
"❌",
|
| 95 |
+
"0/0",
|
| 96 |
+
f"Schema validation error: {str(e)}"
|
| 97 |
+
]]
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
finally:
|
| 103 |
+
# Clean up temporary files
|
| 104 |
+
for path in [ligand_path, protein_path, crystal_path]:
|
| 105 |
+
if path and os.path.exists(path):
|
| 106 |
+
try:
|
| 107 |
+
os.unlink(path)
|
| 108 |
+
except OSError:
|
| 109 |
+
pass
|
| 110 |
+
|
| 111 |
+
except subprocess.TimeoutExpired as e:
|
| 112 |
+
return JSONResponse(
|
| 113 |
+
status_code=500,
|
| 114 |
+
content={
|
| 115 |
+
"object_id": "validation_results",
|
| 116 |
+
"data": {
|
| 117 |
+
"columns": ["ligand_id", "status", "passed/total", "details"],
|
| 118 |
+
"rows": [[
|
| 119 |
+
Path(ligand_input.filename).stem,
|
| 120 |
+
"❌",
|
| 121 |
+
"0/0",
|
| 122 |
+
f"Process timed out after {e.timeout} seconds"
|
| 123 |
+
]]
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
)
|
| 127 |
+
except HTTPException:
|
| 128 |
+
raise
|
| 129 |
+
except Exception as e:
|
| 130 |
+
return JSONResponse(
|
| 131 |
+
status_code=500,
|
| 132 |
+
content={
|
| 133 |
+
"object_id": "validation_results",
|
| 134 |
+
"data": {
|
| 135 |
+
"columns": ["ligand_id", "status", "passed/total", "details"],
|
| 136 |
+
"rows": [[
|
| 137 |
+
Path(ligand_input.filename).stem,
|
| 138 |
+
"❌",
|
| 139 |
+
"0/0",
|
| 140 |
+
str(e)
|
| 141 |
+
]]
|
| 142 |
+
},
|
| 143 |
+
"error": str(e),
|
| 144 |
+
"traceback": traceback.format_exc()
|
| 145 |
+
}
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
@app.get("/mcp/context")
|
| 149 |
+
def context() -> Dict[str, Any]:
|
| 150 |
+
"""
|
| 151 |
+
Return MCP context with empty initial validation results.
|
| 152 |
+
Response is validated against MCP_CONTEXT_SCHEMA.
|
| 153 |
+
"""
|
| 154 |
+
try:
|
| 155 |
+
context = get_mcp_context()
|
| 156 |
+
validate(instance=context, schema=MCP_CONTEXT_SCHEMA)
|
| 157 |
+
return context
|
| 158 |
+
except ValidationError as e:
|
| 159 |
+
return JSONResponse(
|
| 160 |
+
status_code=500,
|
| 161 |
+
content={"error": f"Schema validation error: {str(e)}"}
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
def main():
|
| 165 |
+
"""Run the FastAPI server."""
|
| 166 |
+
import uvicorn
|
| 167 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 168 |
+
|
| 169 |
+
if __name__ == "__main__":
|
| 170 |
+
main()
|
mcp_context.json
CHANGED
|
@@ -1,63 +1,99 @@
|
|
| 1 |
-
{
|
| 2 |
-
"app": {
|
| 3 |
-
"id": "lepanto1571.wrapper.posebusters",
|
| 4 |
-
"name": "PoseBusters MCP wrapper",
|
| 5 |
-
"version": "0.5"
|
| 6 |
-
},
|
| 7 |
-
"windows": [
|
| 8 |
-
{
|
| 9 |
-
"id": "posebusters_view",
|
| 10 |
-
"title": "Validation Panel",
|
| 11 |
-
"view": {
|
| 12 |
-
"type": "form",
|
| 13 |
-
"objects": ["ligand_input", "protein_input", "crystal_input", "validation_results"]
|
| 14 |
-
}
|
| 15 |
-
}
|
| 16 |
-
],
|
| 17 |
-
"objects": [
|
| 18 |
-
{
|
| 19 |
-
"type": "file",
|
| 20 |
-
"id": "ligand_input",
|
| 21 |
-
"name": "Ligand file",
|
| 22 |
-
"format": "sdf"
|
| 23 |
-
},
|
| 24 |
-
{
|
| 25 |
-
"type": "file",
|
| 26 |
-
"id": "protein_input",
|
| 27 |
-
"name": "Protein file",
|
| 28 |
-
"format": "pdb"
|
| 29 |
-
},
|
| 30 |
-
{
|
| 31 |
-
"type": "file",
|
| 32 |
-
"id": "crystal_input",
|
| 33 |
-
"name": "Crystal ligand file (optional)",
|
| 34 |
-
"format": "sdf",
|
| 35 |
-
"optional": true
|
| 36 |
-
},
|
| 37 |
-
{
|
| 38 |
-
"type": "table",
|
| 39 |
-
"id": "validation_results",
|
| 40 |
-
"
|
| 41 |
-
"
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"app": {
|
| 3 |
+
"id": "lepanto1571.wrapper.posebusters",
|
| 4 |
+
"name": "PoseBusters MCP wrapper",
|
| 5 |
+
"version": "0.5"
|
| 6 |
+
},
|
| 7 |
+
"windows": [
|
| 8 |
+
{
|
| 9 |
+
"id": "posebusters_view",
|
| 10 |
+
"title": "Validation Panel",
|
| 11 |
+
"view": {
|
| 12 |
+
"type": "form",
|
| 13 |
+
"objects": ["ligand_input", "protein_input", "crystal_input", "validation_results"]
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
],
|
| 17 |
+
"objects": [
|
| 18 |
+
{
|
| 19 |
+
"type": "file",
|
| 20 |
+
"id": "ligand_input",
|
| 21 |
+
"name": "Ligand file",
|
| 22 |
+
"format": "sdf"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"type": "file",
|
| 26 |
+
"id": "protein_input",
|
| 27 |
+
"name": "Protein file",
|
| 28 |
+
"format": "pdb"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"type": "file",
|
| 32 |
+
"id": "crystal_input",
|
| 33 |
+
"name": "Crystal ligand file (optional)",
|
| 34 |
+
"format": "sdf",
|
| 35 |
+
"optional": true
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"type": "table",
|
| 39 |
+
"id": "validation_results",
|
| 40 |
+
"name": "Validation Results",
|
| 41 |
+
"description": "Results of PoseBusters validation checks",
|
| 42 |
+
"columns": [
|
| 43 |
+
{
|
| 44 |
+
"id": "ligand_id",
|
| 45 |
+
"name": "Ligand ID",
|
| 46 |
+
"type": "string",
|
| 47 |
+
"description": "Identifier of the validated ligand"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"id": "status",
|
| 51 |
+
"name": "Status",
|
| 52 |
+
"type": "string",
|
| 53 |
+
"description": "Validation status (✅ for pass, ❌ for fail)",
|
| 54 |
+
"enum": ["✅", "❌"]
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"id": "passed/total",
|
| 58 |
+
"name": "Tests Passed",
|
| 59 |
+
"type": "string",
|
| 60 |
+
"description": "Number of passed tests vs total tests (format: X/Y)"
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"id": "details",
|
| 64 |
+
"name": "Details",
|
| 65 |
+
"type": "string",
|
| 66 |
+
"description": "List of failed tests or 'All tests passed'"
|
| 67 |
+
}
|
| 68 |
+
],
|
| 69 |
+
"rows": []
|
| 70 |
+
}
|
| 71 |
+
],
|
| 72 |
+
"actions": [
|
| 73 |
+
{
|
| 74 |
+
"name": "validate_pose",
|
| 75 |
+
"description": "Validate ligand–protein structure",
|
| 76 |
+
"parameters": {
|
| 77 |
+
"ligand_input": "ligand_input",
|
| 78 |
+
"protein_input": "protein_input"
|
| 79 |
+
},
|
| 80 |
+
"output": {
|
| 81 |
+
"object_id": "validation_results",
|
| 82 |
+
"type": "table"
|
| 83 |
+
}
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"name": "redocking_validation",
|
| 87 |
+
"description": "Validate redocking using a reference ligand",
|
| 88 |
+
"parameters": {
|
| 89 |
+
"ligand_input": "ligand_input",
|
| 90 |
+
"crystal_input": "crystal_input",
|
| 91 |
+
"protein_input": "protein_input"
|
| 92 |
+
},
|
| 93 |
+
"output": {
|
| 94 |
+
"object_id": "validation_results",
|
| 95 |
+
"type": "table"
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
]
|
| 99 |
+
}
|
pytest.ini
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[pytest]
|
| 2 |
+
testpaths = tests
|
| 3 |
+
python_files = test_*.py
|
| 4 |
+
addopts = -v --tb=short
|
requirements.txt
CHANGED
|
@@ -1,6 +1,15 @@
|
|
| 1 |
-
fastapi
|
| 2 |
-
uvicorn
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.109.0
|
| 2 |
+
uvicorn>=0.27.0
|
| 3 |
+
python-multipart>=0.0.6
|
| 4 |
+
gradio>=4.8.0
|
| 5 |
+
requests>=2.31.0
|
| 6 |
+
pandas>=2.1.3
|
| 7 |
+
rdkit-pypi
|
| 8 |
+
posebusters
|
| 9 |
+
python-magic>=0.4.27 # For MIME type validation
|
| 10 |
+
jsonschema>=4.21.1
|
| 11 |
+
|
| 12 |
+
# Testing dependencies
|
| 13 |
+
pytest>=7.4.0
|
| 14 |
+
pytest-cov>=4.1.0
|
| 15 |
+
pytest-asyncio>=0.21.0 # For async test support
|
schema.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""JSON Schema definitions for MCP context and responses."""
|
| 2 |
+
|
| 3 |
+
MCP_CONTEXT_SCHEMA = {
|
| 4 |
+
"type": "object",
|
| 5 |
+
"required": ["app", "windows", "objects", "actions"],
|
| 6 |
+
"properties": {
|
| 7 |
+
"app": {
|
| 8 |
+
"type": "object",
|
| 9 |
+
"required": ["id", "name", "version"],
|
| 10 |
+
"properties": {
|
| 11 |
+
"id": {"type": "string"},
|
| 12 |
+
"name": {"type": "string"},
|
| 13 |
+
"version": {"type": "string"}
|
| 14 |
+
}
|
| 15 |
+
},
|
| 16 |
+
"windows": {
|
| 17 |
+
"type": "array",
|
| 18 |
+
"items": {
|
| 19 |
+
"type": "object",
|
| 20 |
+
"required": ["id", "title", "view"],
|
| 21 |
+
"properties": {
|
| 22 |
+
"id": {"type": "string"},
|
| 23 |
+
"title": {"type": "string"},
|
| 24 |
+
"view": {
|
| 25 |
+
"type": "object",
|
| 26 |
+
"required": ["type", "objects"],
|
| 27 |
+
"properties": {
|
| 28 |
+
"type": {"type": "string", "enum": ["form"]},
|
| 29 |
+
"objects": {
|
| 30 |
+
"type": "array",
|
| 31 |
+
"items": {"type": "string"}
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
"objects": {
|
| 39 |
+
"type": "array",
|
| 40 |
+
"items": {
|
| 41 |
+
"type": "object",
|
| 42 |
+
"required": ["type", "id", "name"],
|
| 43 |
+
"properties": {
|
| 44 |
+
"type": {"type": "string", "enum": ["file", "table"]},
|
| 45 |
+
"id": {"type": "string"},
|
| 46 |
+
"name": {"type": "string"},
|
| 47 |
+
"format": {"type": "string"},
|
| 48 |
+
"optional": {"type": "boolean"},
|
| 49 |
+
"description": {"type": "string"},
|
| 50 |
+
"columns": {
|
| 51 |
+
"type": "array",
|
| 52 |
+
"items": {
|
| 53 |
+
"type": "object",
|
| 54 |
+
"required": ["id", "name", "type", "description"],
|
| 55 |
+
"properties": {
|
| 56 |
+
"id": {"type": "string"},
|
| 57 |
+
"name": {"type": "string"},
|
| 58 |
+
"type": {"type": "string"},
|
| 59 |
+
"description": {"type": "string"},
|
| 60 |
+
"enum": {
|
| 61 |
+
"type": "array",
|
| 62 |
+
"items": {"type": "string"}
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
},
|
| 67 |
+
"rows": {
|
| 68 |
+
"type": "array",
|
| 69 |
+
"items": {
|
| 70 |
+
"type": "array",
|
| 71 |
+
"items": {"type": "string"}
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"actions": {
|
| 78 |
+
"type": "array",
|
| 79 |
+
"items": {
|
| 80 |
+
"type": "object",
|
| 81 |
+
"required": ["name", "description", "parameters", "output"],
|
| 82 |
+
"properties": {
|
| 83 |
+
"name": {"type": "string"},
|
| 84 |
+
"description": {"type": "string"},
|
| 85 |
+
"parameters": {
|
| 86 |
+
"type": "object",
|
| 87 |
+
"additionalProperties": {"type": "string"}
|
| 88 |
+
},
|
| 89 |
+
"output": {
|
| 90 |
+
"type": "object",
|
| 91 |
+
"required": ["object_id", "type"],
|
| 92 |
+
"properties": {
|
| 93 |
+
"object_id": {"type": "string"},
|
| 94 |
+
"type": {"type": "string"}
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
MCP_PREDICT_RESPONSE_SCHEMA = {
|
| 104 |
+
"type": "object",
|
| 105 |
+
"required": ["object_id", "data"],
|
| 106 |
+
"properties": {
|
| 107 |
+
"object_id": {"type": "string"},
|
| 108 |
+
"data": {
|
| 109 |
+
"type": "object",
|
| 110 |
+
"required": ["columns", "rows"],
|
| 111 |
+
"properties": {
|
| 112 |
+
"columns": {
|
| 113 |
+
"type": "array",
|
| 114 |
+
"items": {"type": "string"}
|
| 115 |
+
},
|
| 116 |
+
"rows": {
|
| 117 |
+
"type": "array",
|
| 118 |
+
"items": {
|
| 119 |
+
"type": "array",
|
| 120 |
+
"items": {"type": "string"}
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
},
|
| 125 |
+
"error": {"type": "string"},
|
| 126 |
+
"traceback": {"type": "string"}
|
| 127 |
+
}
|
| 128 |
+
}
|
tests/__pycache__/test_mcp.cpython-312-pytest-8.3.5.pyc
ADDED
|
Binary file (14.8 kB). View file
|
|
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Add the parent directory to Python path so we can import app modules
|
| 5 |
+
app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 6 |
+
sys.path.insert(0, app_dir)
|
tests/test_mcp.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for MCP context and response validation."""
|
| 2 |
+
import json
|
| 3 |
+
import pytest
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from jsonschema import validate, ValidationError
|
| 6 |
+
|
| 7 |
+
from schema import MCP_CONTEXT_SCHEMA, MCP_PREDICT_RESPONSE_SCHEMA
|
| 8 |
+
from utils import format_results_to_mcp, get_mcp_context
|
| 9 |
+
|
| 10 |
+
def test_mcp_context_schema():
|
| 11 |
+
"""Test that the MCP context follows the schema."""
|
| 12 |
+
context = get_mcp_context()
|
| 13 |
+
validate(instance=context, schema=MCP_CONTEXT_SCHEMA)
|
| 14 |
+
|
| 15 |
+
# Test required fields
|
| 16 |
+
assert "app" in context
|
| 17 |
+
assert "id" in context["app"]
|
| 18 |
+
assert "name" in context["app"]
|
| 19 |
+
assert "version" in context["app"]
|
| 20 |
+
|
| 21 |
+
# Test windows configuration
|
| 22 |
+
assert "windows" in context
|
| 23 |
+
assert len(context["windows"]) > 0
|
| 24 |
+
assert "objects" in context["windows"][0]["view"]
|
| 25 |
+
|
| 26 |
+
# Test objects configuration
|
| 27 |
+
assert "objects" in context
|
| 28 |
+
file_objects = [obj for obj in context["objects"] if obj["type"] == "file"]
|
| 29 |
+
assert len(file_objects) >= 2 # At least ligand and protein
|
| 30 |
+
|
| 31 |
+
# Test actions configuration
|
| 32 |
+
assert "actions" in context
|
| 33 |
+
assert len(context["actions"]) >= 1
|
| 34 |
+
for action in context["actions"]:
|
| 35 |
+
assert "parameters" in action
|
| 36 |
+
assert "output" in action
|
| 37 |
+
|
| 38 |
+
def test_mcp_response_format():
|
| 39 |
+
"""Test that the MCP response formatter produces valid output."""
|
| 40 |
+
# Test successful case
|
| 41 |
+
csv_text = "molecule,test1,test2\nmol1,True,False\n"
|
| 42 |
+
response = format_results_to_mcp(csv_text, "", "mol1")
|
| 43 |
+
validate(instance=response, schema=MCP_PREDICT_RESPONSE_SCHEMA)
|
| 44 |
+
|
| 45 |
+
assert response["object_id"] == "validation_results"
|
| 46 |
+
assert "columns" in response["data"]
|
| 47 |
+
assert "rows" in response["data"]
|
| 48 |
+
assert len(response["data"]["rows"]) == 1
|
| 49 |
+
|
| 50 |
+
# Test error case
|
| 51 |
+
response = format_results_to_mcp("", "Error occurred", "mol1")
|
| 52 |
+
validate(instance=response, schema=MCP_PREDICT_RESPONSE_SCHEMA)
|
| 53 |
+
assert response["data"]["rows"][0][1] == "❌" # Status should be fail
|
| 54 |
+
|
| 55 |
+
def test_mcp_response_content():
|
| 56 |
+
"""Test the content of MCP responses for different scenarios."""
|
| 57 |
+
# Test all tests passing
|
| 58 |
+
csv_text = "molecule,test1,test2\nmol1,True,True\n"
|
| 59 |
+
response = format_results_to_mcp(csv_text, "", "mol1")
|
| 60 |
+
row = response["data"]["rows"][0]
|
| 61 |
+
assert row[1] == "✅" # Status
|
| 62 |
+
assert row[2] == "2/2" # Passed/Total
|
| 63 |
+
assert row[3] == "All tests passed" # Details
|
| 64 |
+
|
| 65 |
+
# Test some tests failing
|
| 66 |
+
csv_text = "molecule,test1,test2,test3\nmol1,True,False,False\n"
|
| 67 |
+
response = format_results_to_mcp(csv_text, "", "mol1")
|
| 68 |
+
row = response["data"]["rows"][0]
|
| 69 |
+
assert row[1] == "❌" # Status
|
| 70 |
+
assert row[2] == "1/3" # Passed/Total
|
| 71 |
+
assert "test2" in row[3] and "test3" in row[3] # Failed tests in details
|
| 72 |
+
|
| 73 |
+
def test_invalid_mcp_context():
|
| 74 |
+
"""Test that invalid MCP context raises ValidationError."""
|
| 75 |
+
invalid_context = {
|
| 76 |
+
"app": {"name": "Test"} # Missing required fields
|
| 77 |
+
}
|
| 78 |
+
with pytest.raises(ValidationError):
|
| 79 |
+
validate(instance=invalid_context, schema=MCP_CONTEXT_SCHEMA)
|
| 80 |
+
|
| 81 |
+
def test_invalid_mcp_response():
|
| 82 |
+
"""Test that invalid MCP response raises ValidationError."""
|
| 83 |
+
invalid_response = {
|
| 84 |
+
"object_id": "validation_results",
|
| 85 |
+
"data": {
|
| 86 |
+
"columns": ["col1"],
|
| 87 |
+
"rows": [["too", "many", "columns"]] # Inconsistent columns
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
with pytest.raises(ValidationError) as exc_info:
|
| 92 |
+
# First validate that rows match column count
|
| 93 |
+
columns_count = len(invalid_response["data"]["columns"])
|
| 94 |
+
for row in invalid_response["data"]["rows"]:
|
| 95 |
+
if len(row) != columns_count:
|
| 96 |
+
raise ValidationError(f"Row length {len(row)} does not match columns length {columns_count}")
|
| 97 |
+
|
| 98 |
+
# Then validate schema
|
| 99 |
+
validate(instance=invalid_response, schema=MCP_PREDICT_RESPONSE_SCHEMA)
|
| 100 |
+
|
| 101 |
+
assert "Row length" in str(exc_info.value)
|
utils.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared utilities for the PoseBusters MCP wrapper."""
|
| 2 |
+
|
| 3 |
+
# Standard library imports
|
| 4 |
+
import csv
|
| 5 |
+
import mimetypes
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any, Dict, List
|
| 8 |
+
import json
|
| 9 |
+
|
| 10 |
+
# Third-party imports
|
| 11 |
+
from fastapi import UploadFile, HTTPException
|
| 12 |
+
|
| 13 |
+
# Constants
|
| 14 |
+
ALLOWED_MIME_TYPES = {
|
| 15 |
+
".sdf": ["chemical/x-mdl-sdfile", "application/octet-stream", "text/plain"],
|
| 16 |
+
".pdb": ["chemical/x-pdb", "application/octet-stream", "text/plain"]
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
def validate_mime_type(file: UploadFile, expected_ext: str) -> None:
|
| 20 |
+
"""Validate file MIME type and extension."""
|
| 21 |
+
if not file.filename:
|
| 22 |
+
raise HTTPException(status_code=400, detail=f"Missing filename for {expected_ext} file")
|
| 23 |
+
|
| 24 |
+
file_ext = Path(file.filename).suffix.lower()
|
| 25 |
+
if file_ext != expected_ext:
|
| 26 |
+
raise HTTPException(
|
| 27 |
+
status_code=400,
|
| 28 |
+
detail=f"Invalid file extension. Expected {expected_ext}, got {file_ext}"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
content_type = file.content_type or mimetypes.guess_type(file.filename)[0] or "application/octet-stream"
|
| 32 |
+
if not content_type or content_type not in ALLOWED_MIME_TYPES[expected_ext]:
|
| 33 |
+
raise HTTPException(
|
| 34 |
+
status_code=400,
|
| 35 |
+
detail=f"Invalid MIME type for {expected_ext} file"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
def format_results_to_mcp(csv_text: str, stderr_text: str = "", ligand_id: str = "unknown") -> Dict[str, Any]:
|
| 39 |
+
"""Format PoseBusters results to MCP standard output format."""
|
| 40 |
+
rows: List[List[str]] = []
|
| 41 |
+
|
| 42 |
+
if not csv_text.strip():
|
| 43 |
+
error_msg = stderr_text.strip().splitlines()[0] if stderr_text.strip() else "PoseBusters output is empty."
|
| 44 |
+
rows.append([ligand_id, "❌", "0/0", error_msg])
|
| 45 |
+
else:
|
| 46 |
+
reader = csv.DictReader(csv_text.strip().splitlines())
|
| 47 |
+
for row in reader:
|
| 48 |
+
ligand_id = row.get("molecule", "?")
|
| 49 |
+
failed_tests = []
|
| 50 |
+
passed_n = total_n = 0
|
| 51 |
+
|
| 52 |
+
for k, v in row.items():
|
| 53 |
+
if k in {"file", "molecule"}:
|
| 54 |
+
continue
|
| 55 |
+
if v == "True":
|
| 56 |
+
passed_n += 1
|
| 57 |
+
total_n += 1
|
| 58 |
+
elif v == "False":
|
| 59 |
+
failed_tests.append(k)
|
| 60 |
+
total_n += 1
|
| 61 |
+
|
| 62 |
+
rows.append([
|
| 63 |
+
ligand_id,
|
| 64 |
+
"✅" if not failed_tests else "❌",
|
| 65 |
+
f"{passed_n}/{total_n}",
|
| 66 |
+
", ".join(failed_tests) if failed_tests else "All tests passed"
|
| 67 |
+
])
|
| 68 |
+
|
| 69 |
+
# Create response
|
| 70 |
+
response = {
|
| 71 |
+
"object_id": "validation_results",
|
| 72 |
+
"data": {
|
| 73 |
+
"columns": ["ligand_id", "status", "passed/total", "details"],
|
| 74 |
+
"rows": rows
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# Validate row lengths
|
| 79 |
+
expected_cols = len(response["data"]["columns"])
|
| 80 |
+
for row in response["data"]["rows"]:
|
| 81 |
+
if len(row) != expected_cols:
|
| 82 |
+
raise ValueError(f"Row length {len(row)} does not match columns length {expected_cols}")
|
| 83 |
+
|
| 84 |
+
return response
|
| 85 |
+
|
| 86 |
+
def get_mcp_context() -> Dict[str, Any]:
|
| 87 |
+
"""Return MCP context from the JSON file."""
|
| 88 |
+
try:
|
| 89 |
+
# Get the directory where the current file (utils.py) is located
|
| 90 |
+
current_dir = Path(__file__).parent
|
| 91 |
+
context_path = current_dir / 'mcp_context.json'
|
| 92 |
+
|
| 93 |
+
# If not found in current directory, try the app root (for Spaces)
|
| 94 |
+
if not context_path.exists():
|
| 95 |
+
context_path = current_dir / '..' / 'mcp_context.json'
|
| 96 |
+
context_path = context_path.resolve()
|
| 97 |
+
|
| 98 |
+
if not context_path.exists():
|
| 99 |
+
raise FileNotFoundError(f"MCP context file not found at {context_path}")
|
| 100 |
+
|
| 101 |
+
with open(context_path, 'r') as f:
|
| 102 |
+
return json.load(f)
|
| 103 |
+
except FileNotFoundError as e:
|
| 104 |
+
raise HTTPException(
|
| 105 |
+
status_code=500,
|
| 106 |
+
detail=str(e)
|
| 107 |
+
)
|
| 108 |
+
except json.JSONDecodeError:
|
| 109 |
+
raise HTTPException(
|
| 110 |
+
status_code=500,
|
| 111 |
+
detail="Invalid MCP context JSON format"
|
| 112 |
+
)
|