Upload 14 files
Browse files- Dockerfile +13 -8
- README.md +55 -5
- app.py +50 -33
- port.json +1 -5
- rdkit/mcp_output/README_MCP.md +101 -128
- rdkit/mcp_output/mcp_plugin/adapter.py +165 -381
- rdkit/mcp_output/mcp_plugin/main.py +3 -9
- rdkit/mcp_output/mcp_plugin/mcp_service.py +288 -129
- rdkit/mcp_output/requirements.txt +1 -4
- rdkit/mcp_output/start_mcp.py +17 -18
- requirements.txt +2 -3
- run_docker.ps1 +6 -24
- run_docker.sh +6 -73
Dockerfile
CHANGED
|
@@ -1,18 +1,23 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
|
| 7 |
WORKDIR /app
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
COPY --chown=user . /app
|
| 13 |
ENV MCP_TRANSPORT=http
|
| 14 |
ENV MCP_PORT=7860
|
| 15 |
|
| 16 |
EXPOSE 7860
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 4 |
+
ENV PYTHONUNBUFFERED=1
|
|
|
|
| 5 |
|
| 6 |
WORKDIR /app
|
| 7 |
|
| 8 |
+
RUN useradd -m -u 1000 appuser
|
| 9 |
+
|
| 10 |
+
COPY requirements.txt /app/requirements.txt
|
| 11 |
+
RUN pip install --no-cache-dir -r /app/requirements.txt
|
| 12 |
+
|
| 13 |
+
COPY rdkit /app/rdkit
|
| 14 |
+
COPY app.py /app/app.py
|
| 15 |
|
|
|
|
| 16 |
ENV MCP_TRANSPORT=http
|
| 17 |
ENV MCP_PORT=7860
|
| 18 |
|
| 19 |
EXPOSE 7860
|
| 20 |
|
| 21 |
+
USER appuser
|
| 22 |
+
|
| 23 |
+
ENTRYPOINT ["python", "rdkit/mcp_output/start_mcp.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,60 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: rdkit MCP Service
|
| 3 |
+
emoji: 🔧
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# rdkit MCP Service
|
| 12 |
+
|
| 13 |
+
This deployment package wraps RDKit with FastMCP and exposes core cheminformatics tools over MCP.
|
| 14 |
+
|
| 15 |
+
## Available Tools
|
| 16 |
+
|
| 17 |
+
- `rdkit_health`
|
| 18 |
+
- `list_rdkit_modules`
|
| 19 |
+
- `parse_smiles`
|
| 20 |
+
- `compute_descriptors`
|
| 21 |
+
- `substructure_match`
|
| 22 |
+
- `tanimoto_similarity`
|
| 23 |
+
- `morgan_fingerprint_bits`
|
| 24 |
+
|
| 25 |
+
Detailed parameter documentation is in `rdkit/mcp_output/README_MCP.md`.
|
| 26 |
+
|
| 27 |
+
## Local stdio (Claude Desktop / CLI)
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
cd rdkit/mcp_output
|
| 31 |
+
python mcp_plugin/main.py
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
or
|
| 35 |
+
|
| 36 |
+
```bash
|
| 37 |
+
cd rdkit/mcp_output
|
| 38 |
+
MCP_TRANSPORT=stdio python start_mcp.py
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
## HTTP client connection
|
| 42 |
+
|
| 43 |
+
Run MCP in HTTP mode:
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
cd rdkit/mcp_output
|
| 47 |
+
MCP_TRANSPORT=http MCP_PORT=7860 python start_mcp.py
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
MCP endpoint:
|
| 51 |
+
|
| 52 |
+
`http://localhost:7860/mcp`
|
| 53 |
+
|
| 54 |
+
In Docker / HuggingFace Spaces, this service is started by:
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
python rdkit/mcp_output/start_mcp.py
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
with `MCP_TRANSPORT=http` and `MCP_PORT=7860` preconfigured.
|
app.py
CHANGED
|
@@ -1,45 +1,62 @@
|
|
| 1 |
-
from
|
|
|
|
| 2 |
import os
|
| 3 |
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
sys.path
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
app = FastAPI(
|
| 9 |
-
title="Rdkit MCP Service",
|
| 10 |
-
description="Auto-generated MCP service for rdkit",
|
| 11 |
-
version="1.0.0"
|
| 12 |
-
)
|
| 13 |
|
| 14 |
@app.get("/")
|
| 15 |
-
def root():
|
| 16 |
return {
|
| 17 |
-
"service": "
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
|
|
|
| 21 |
}
|
| 22 |
|
|
|
|
| 23 |
@app.get("/health")
|
| 24 |
-
def
|
| 25 |
-
return {"status": "healthy"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
@app.get("/tools")
|
| 28 |
-
def list_tools():
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
"description": tool_func.__doc__ or "No description available"
|
| 37 |
-
})
|
| 38 |
-
return {"tools": tools}
|
| 39 |
-
except Exception as e:
|
| 40 |
-
return {"error": f"Failed to load tools: {str(e)}"}
|
| 41 |
-
|
| 42 |
-
if __name__ == "__main__":
|
| 43 |
-
import uvicorn
|
| 44 |
-
port = int(os.environ.get("PORT", 7860))
|
| 45 |
-
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
import os
|
| 4 |
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from fastapi import FastAPI
|
| 9 |
|
| 10 |
+
PLUGIN_DIR = Path(__file__).resolve().parent / "rdkit" / "mcp_output" / "mcp_plugin"
|
| 11 |
+
if str(PLUGIN_DIR) not in sys.path:
|
| 12 |
+
sys.path.insert(0, str(PLUGIN_DIR))
|
| 13 |
+
|
| 14 |
+
app = FastAPI(title="rdkit MCP info app", version="1.0.0")
|
| 15 |
+
PORT = int(os.getenv("PORT", "7860"))
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
@app.get("/")
|
| 19 |
+
def root() -> dict[str, Any]:
|
| 20 |
return {
|
| 21 |
+
"service": "rdkit-mcp-service",
|
| 22 |
+
"description": "Supplementary API for MCP service metadata (not MCP transport endpoint).",
|
| 23 |
+
"mcp_transport": os.getenv("MCP_TRANSPORT", "stdio"),
|
| 24 |
+
"mcp_port": os.getenv("MCP_PORT", "8000"),
|
| 25 |
+
"info_app_port": PORT,
|
| 26 |
}
|
| 27 |
|
| 28 |
+
|
| 29 |
@app.get("/health")
|
| 30 |
+
def health() -> dict[str, str]:
|
| 31 |
+
return {"status": "healthy"}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _extract_tools(mcp_obj: Any) -> list[dict[str, str | None]]:
|
| 35 |
+
tool_entries = getattr(mcp_obj, "tools", None)
|
| 36 |
+
if tool_entries is None:
|
| 37 |
+
return []
|
| 38 |
+
|
| 39 |
+
tools: list[dict[str, str | None]] = []
|
| 40 |
+
if isinstance(tool_entries, dict):
|
| 41 |
+
iterator = tool_entries.items()
|
| 42 |
+
for name, tool in iterator:
|
| 43 |
+
description = getattr(tool, "description", None)
|
| 44 |
+
tools.append({"name": str(name), "description": description})
|
| 45 |
+
elif isinstance(tool_entries, list):
|
| 46 |
+
for tool in tool_entries:
|
| 47 |
+
name = getattr(tool, "name", str(tool))
|
| 48 |
+
description = getattr(tool, "description", None)
|
| 49 |
+
tools.append({"name": name, "description": description})
|
| 50 |
+
|
| 51 |
+
return tools
|
| 52 |
+
|
| 53 |
|
| 54 |
@app.get("/tools")
|
| 55 |
+
def list_tools() -> dict[str, Any]:
|
| 56 |
+
from mcp_service import create_app
|
| 57 |
+
|
| 58 |
+
mcp = create_app()
|
| 59 |
+
return {
|
| 60 |
+
"count": len(_extract_tools(mcp)),
|
| 61 |
+
"tools": _extract_tools(mcp),
|
| 62 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
port.json
CHANGED
|
@@ -1,5 +1 @@
|
|
| 1 |
-
{
|
| 2 |
-
"repo": "rdkit",
|
| 3 |
-
"port": 7914,
|
| 4 |
-
"timestamp": 1773264143
|
| 5 |
-
}
|
|
|
|
| 1 |
+
{"port": 7860}
|
|
|
|
|
|
|
|
|
|
|
|
rdkit/mcp_output/README_MCP.md
CHANGED
|
@@ -1,128 +1,101 @@
|
|
| 1 |
-
# RDKit MCP
|
| 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 |
-
|
| 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 |
-
## 5) Common Issues and Notes
|
| 104 |
-
|
| 105 |
-
- RDKit build/runtime mismatch:
|
| 106 |
-
- Ensure the Python environment uses the same RDKit binary installation.
|
| 107 |
-
- Missing optional drawing deps:
|
| 108 |
-
- Install Pillow/Cairo/Qt backends for image generation reliability.
|
| 109 |
-
- InChI failures:
|
| 110 |
-
- Confirm InChI support is present in your RDKit build.
|
| 111 |
-
- Large batch performance:
|
| 112 |
-
- Prefer batched calls; avoid per-molecule process startup.
|
| 113 |
-
- Cache parsed molecules/fingerprints when possible.
|
| 114 |
-
- Threading:
|
| 115 |
-
- Some operations are CPU-heavy; use process pools for throughput-critical services.
|
| 116 |
-
- Error handling:
|
| 117 |
-
- Return clear parse/validation errors rather than failing silently.
|
| 118 |
-
|
| 119 |
-
---
|
| 120 |
-
|
| 121 |
-
## 6) Reference Links or Documentation
|
| 122 |
-
|
| 123 |
-
- RDKit repository: https://github.com/rdkit/rdkit
|
| 124 |
-
- Main README: https://github.com/rdkit/rdkit/blob/master/README.md
|
| 125 |
-
- RDKit Book / docs: https://github.com/rdkit/rdkit/tree/master/Docs/Book
|
| 126 |
-
- Installation guide: https://github.com/rdkit/rdkit/blob/master/Docs/Book/Install.md
|
| 127 |
-
- C++/Python getting started: https://github.com/rdkit/rdkit/blob/master/Docs/Book/GettingStartedInC%2B%2B.md
|
| 128 |
-
- Contrib utilities: https://github.com/rdkit/rdkit/tree/master/Contrib
|
|
|
|
| 1 |
+
# RDKit MCP Tools
|
| 2 |
+
|
| 3 |
+
This package exposes core RDKit capabilities through an MCP server implemented with FastMCP.
|
| 4 |
+
|
| 5 |
+
## Exposed Tools
|
| 6 |
+
|
| 7 |
+
### 1) `rdkit_health`
|
| 8 |
+
- **Purpose**: Returns adapter/module loading health.
|
| 9 |
+
- **Parameters**: none
|
| 10 |
+
|
| 11 |
+
### 2) `list_rdkit_modules`
|
| 12 |
+
- **Purpose**: Lists loaded and failed RDKit modules.
|
| 13 |
+
- **Parameters**: none
|
| 14 |
+
|
| 15 |
+
### 3) `parse_smiles`
|
| 16 |
+
- **Purpose**: Parse a SMILES and compute canonical/basic properties.
|
| 17 |
+
- **Parameters**:
|
| 18 |
+
- `smiles` (str): input SMILES
|
| 19 |
+
- `sanitize` (bool, default `true`): sanitize molecule on parse
|
| 20 |
+
|
| 21 |
+
### 4) `compute_descriptors`
|
| 22 |
+
- **Purpose**: Compute common descriptors (MolWt, LogP, TPSA, H-bond counts, rings).
|
| 23 |
+
- **Parameters**:
|
| 24 |
+
- `smiles` (str): input SMILES
|
| 25 |
+
|
| 26 |
+
### 5) `substructure_match`
|
| 27 |
+
- **Purpose**: Search SMARTS in target molecule.
|
| 28 |
+
- **Parameters**:
|
| 29 |
+
- `smiles` (str): target molecule
|
| 30 |
+
- `smarts` (str): SMARTS query
|
| 31 |
+
- `uniquify` (bool, default `true`): keep unique matches only
|
| 32 |
+
- `max_matches` (int, default `100`): upper limit on returned matches
|
| 33 |
+
|
| 34 |
+
### 6) `tanimoto_similarity`
|
| 35 |
+
- **Purpose**: Compute Tanimoto similarity with Morgan fingerprints.
|
| 36 |
+
- **Parameters**:
|
| 37 |
+
- `smiles_a` (str): first molecule
|
| 38 |
+
- `smiles_b` (str): second molecule
|
| 39 |
+
- `radius` (int, default `2`): Morgan radius
|
| 40 |
+
- `n_bits` (int, default `2048`): fingerprint bit size
|
| 41 |
+
|
| 42 |
+
### 7) `morgan_fingerprint_bits`
|
| 43 |
+
- **Purpose**: Return active bit indices from Morgan fingerprint.
|
| 44 |
+
- **Parameters**:
|
| 45 |
+
- `smiles` (str): input molecule
|
| 46 |
+
- `radius` (int, default `2`)
|
| 47 |
+
- `n_bits` (int, default `2048`)
|
| 48 |
+
- `max_bits` (int, default `256`): max returned bit indices
|
| 49 |
+
|
| 50 |
+
All tools return:
|
| 51 |
+
|
| 52 |
+
```json
|
| 53 |
+
{
|
| 54 |
+
"success": true,
|
| 55 |
+
"result": {},
|
| 56 |
+
"error": null
|
| 57 |
+
}
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
or
|
| 61 |
+
|
| 62 |
+
```json
|
| 63 |
+
{
|
| 64 |
+
"success": false,
|
| 65 |
+
"result": null,
|
| 66 |
+
"error": "error message"
|
| 67 |
+
}
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
## Local stdio run
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
python mcp_plugin/main.py
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
or
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
MCP_TRANSPORT=stdio python start_mcp.py
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
## HTTP run
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
MCP_TRANSPORT=http MCP_PORT=7860 python start_mcp.py
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
MCP endpoint is served at:
|
| 89 |
+
|
| 90 |
+
`http://127.0.0.1:7860/mcp`
|
| 91 |
+
|
| 92 |
+
## Example tool invocation payload
|
| 93 |
+
|
| 94 |
+
```json
|
| 95 |
+
{
|
| 96 |
+
"name": "compute_descriptors",
|
| 97 |
+
"arguments": {
|
| 98 |
+
"smiles": "CCO"
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rdkit/mcp_output/mcp_plugin/adapter.py
CHANGED
|
@@ -1,406 +1,190 @@
|
|
| 1 |
-
import
|
| 2 |
-
|
| 3 |
import importlib
|
| 4 |
import inspect
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
)
|
| 11 |
-
sys.path.insert(0, source_path)
|
| 12 |
|
| 13 |
|
| 14 |
class Adapter:
|
| 15 |
-
"""
|
| 16 |
-
MCP Import-mode adapter for RDKit repository modules discovered via analysis.
|
| 17 |
-
|
| 18 |
-
This adapter attempts direct module imports first ("import" mode). If imports fail,
|
| 19 |
-
it gracefully falls back to "fallback" mode and returns actionable guidance.
|
| 20 |
-
All public methods return a unified dictionary payload with a required `status` field.
|
| 21 |
-
"""
|
| 22 |
-
|
| 23 |
def __init__(self) -> None:
|
| 24 |
-
self.
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
self._load_modules()
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
data: Optional[Any] = None,
|
| 38 |
-
error: Optional[str] = None,
|
| 39 |
-
guidance: Optional[str] = None,
|
| 40 |
-
) -> Dict[str, Any]:
|
| 41 |
return {
|
| 42 |
-
"status":
|
| 43 |
"mode": self.mode,
|
| 44 |
-
"
|
| 45 |
-
"
|
| 46 |
-
"
|
| 47 |
-
"
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
-
def
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
self.
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
self._import_errors[module_path] = (
|
| 58 |
-
f"Failed to import '{module_path}': {exc}"
|
| 59 |
-
)
|
| 60 |
-
return None
|
| 61 |
-
|
| 62 |
-
def _load_modules(self) -> None:
|
| 63 |
-
target_modules = [
|
| 64 |
-
"Regress.Scripts.new_timings",
|
| 65 |
-
"Web.RDExtras.MolImage",
|
| 66 |
-
"Web.RDExtras.MolDepict",
|
| 67 |
-
"Projects.DbCLI.CreateDb",
|
| 68 |
-
"Projects.DbCLI.SearchDb",
|
| 69 |
-
"Code.DataManip.MetricMatrixCalc.Wrap.testMatricCalc",
|
| 70 |
-
"Code.DataStructs.Wrap.testBV",
|
| 71 |
-
"Code.DataStructs.Wrap.testSparseIntVect",
|
| 72 |
-
"rdkit.Chem.FeatFinderCLI",
|
| 73 |
-
"Contrib.MolVS.molvs_cli",
|
| 74 |
-
]
|
| 75 |
-
for module_path in target_modules:
|
| 76 |
-
self._import_module(module_path)
|
| 77 |
-
|
| 78 |
-
if all(self._modules.get(m) is None for m in target_modules):
|
| 79 |
-
self.mode = "fallback"
|
| 80 |
-
|
| 81 |
-
def health(self) -> Dict[str, Any]:
|
| 82 |
-
"""
|
| 83 |
-
Return adapter import health status.
|
| 84 |
-
|
| 85 |
-
Returns:
|
| 86 |
-
dict: Unified status dict including loaded modules and import errors.
|
| 87 |
-
"""
|
| 88 |
-
loaded = [k for k, v in self._modules.items() if v is not None]
|
| 89 |
-
failed = {k: e for k, e in self._import_errors.items()}
|
| 90 |
-
return self._result(
|
| 91 |
-
status="ok" if loaded else "degraded",
|
| 92 |
-
message="Adapter health check completed.",
|
| 93 |
-
data={"loaded_modules": loaded, "failed_modules": failed},
|
| 94 |
-
guidance=(
|
| 95 |
-
"If critical modules failed, ensure C++ extension-backed RDKit build artifacts "
|
| 96 |
-
"are available under the source tree and required runtime deps are installed."
|
| 97 |
-
),
|
| 98 |
-
)
|
| 99 |
|
| 100 |
-
def
|
| 101 |
-
|
| 102 |
-
) -> Dict[str, Any]:
|
| 103 |
-
module = self._modules.get(module_path)
|
| 104 |
if module is None:
|
| 105 |
-
return
|
| 106 |
-
status
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
)
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
data={"available_callables_sample": available[:25]},
|
| 123 |
-
guidance="Check function name spelling and module version compatibility.",
|
| 124 |
)
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
status="ok",
|
| 128 |
-
message=f"Function '{func_name}' executed successfully.",
|
| 129 |
-
data=result,
|
| 130 |
-
)
|
| 131 |
-
except Exception as exc:
|
| 132 |
-
return self._result(
|
| 133 |
-
status="error",
|
| 134 |
-
message=f"Function '{func_name}' execution failed.",
|
| 135 |
-
error=str(exc),
|
| 136 |
-
guidance=(
|
| 137 |
-
"Validate arguments and required environment. "
|
| 138 |
-
"If the callable depends on RDKit backend libs, ensure they are compiled."
|
| 139 |
-
),
|
| 140 |
-
)
|
| 141 |
-
|
| 142 |
-
# ==========================================================================
|
| 143 |
-
# Regress.Scripts.new_timings
|
| 144 |
-
# ==========================================================================
|
| 145 |
-
|
| 146 |
-
def call_new_timings_data(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 147 |
-
"""
|
| 148 |
-
Call Regress.Scripts.new_timings.data.
|
| 149 |
-
|
| 150 |
-
Parameters:
|
| 151 |
-
*args: Positional arguments forwarded to data().
|
| 152 |
-
**kwargs: Keyword arguments forwarded to data().
|
| 153 |
-
|
| 154 |
-
Returns:
|
| 155 |
-
dict: Unified status response with callable result or actionable error.
|
| 156 |
-
"""
|
| 157 |
-
return self._call_function("Regress.Scripts.new_timings", "data", *args, **kwargs)
|
| 158 |
-
|
| 159 |
-
# ==========================================================================
|
| 160 |
-
# Web.RDExtras.MolImage
|
| 161 |
-
# ==========================================================================
|
| 162 |
-
|
| 163 |
-
def call_molimage_gif(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 164 |
-
"""
|
| 165 |
-
Call Web.RDExtras.MolImage.gif.
|
| 166 |
-
|
| 167 |
-
Parameters:
|
| 168 |
-
*args: Positional arguments for gif().
|
| 169 |
-
**kwargs: Keyword arguments for gif().
|
| 170 |
-
|
| 171 |
-
Returns:
|
| 172 |
-
dict: Unified status response.
|
| 173 |
-
"""
|
| 174 |
-
return self._call_function("Web.RDExtras.MolImage", "gif", *args, **kwargs)
|
| 175 |
-
|
| 176 |
-
def call_molimage_svg(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 177 |
-
"""
|
| 178 |
-
Call Web.RDExtras.MolImage.svg.
|
| 179 |
-
|
| 180 |
-
Parameters:
|
| 181 |
-
*args: Positional arguments for svg().
|
| 182 |
-
**kwargs: Keyword arguments for svg().
|
| 183 |
-
|
| 184 |
-
Returns:
|
| 185 |
-
dict: Unified status response.
|
| 186 |
-
"""
|
| 187 |
-
return self._call_function("Web.RDExtras.MolImage", "svg", *args, **kwargs)
|
| 188 |
-
|
| 189 |
-
# ==========================================================================
|
| 190 |
-
# Web.RDExtras.MolDepict
|
| 191 |
-
# ==========================================================================
|
| 192 |
-
|
| 193 |
-
def call_moldepict_page(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 194 |
-
"""
|
| 195 |
-
Call Web.RDExtras.MolDepict.page.
|
| 196 |
-
|
| 197 |
-
Parameters:
|
| 198 |
-
*args: Positional arguments for page().
|
| 199 |
-
**kwargs: Keyword arguments for page().
|
| 200 |
-
|
| 201 |
-
Returns:
|
| 202 |
-
dict: Unified status response.
|
| 203 |
-
"""
|
| 204 |
-
return self._call_function("Web.RDExtras.MolDepict", "page", *args, **kwargs)
|
| 205 |
-
|
| 206 |
-
# ==========================================================================
|
| 207 |
-
# Projects.DbCLI.CreateDb
|
| 208 |
-
# ==========================================================================
|
| 209 |
-
|
| 210 |
-
def call_createdb_createdb(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 211 |
-
"""
|
| 212 |
-
Call Projects.DbCLI.CreateDb.CreateDb.
|
| 213 |
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
"""
|
| 221 |
-
return self._call_function("Projects.DbCLI.CreateDb", "CreateDb", *args, **kwargs)
|
| 222 |
-
|
| 223 |
-
def call_createdb_initparser(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 224 |
-
"""
|
| 225 |
-
Call Projects.DbCLI.CreateDb.initParser.
|
| 226 |
-
|
| 227 |
-
Parameters:
|
| 228 |
-
*args: Positional arguments for initParser().
|
| 229 |
-
**kwargs: Keyword arguments for initParser().
|
| 230 |
-
|
| 231 |
-
Returns:
|
| 232 |
-
dict: Unified status response.
|
| 233 |
-
"""
|
| 234 |
-
return self._call_function("Projects.DbCLI.CreateDb", "initParser", *args, **kwargs)
|
| 235 |
-
|
| 236 |
-
# ==========================================================================
|
| 237 |
-
# Projects.DbCLI.SearchDb
|
| 238 |
-
# ==========================================================================
|
| 239 |
-
|
| 240 |
-
def call_searchdb_get_mols_from_sdfile(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 241 |
-
"""
|
| 242 |
-
Call Projects.DbCLI.SearchDb.GetMolsFromSDFile.
|
| 243 |
-
|
| 244 |
-
Parameters:
|
| 245 |
-
*args: Positional arguments for GetMolsFromSDFile().
|
| 246 |
-
**kwargs: Keyword arguments for GetMolsFromSDFile().
|
| 247 |
-
|
| 248 |
-
Returns:
|
| 249 |
-
dict: Unified status response.
|
| 250 |
-
"""
|
| 251 |
-
return self._call_function("Projects.DbCLI.SearchDb", "GetMolsFromSDFile", *args, **kwargs)
|
| 252 |
-
|
| 253 |
-
def call_searchdb_get_mols_from_smiles_file(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 254 |
-
"""
|
| 255 |
-
Call Projects.DbCLI.SearchDb.GetMolsFromSmilesFile.
|
| 256 |
-
|
| 257 |
-
Parameters:
|
| 258 |
-
*args: Positional arguments for GetMolsFromSmilesFile().
|
| 259 |
-
**kwargs: Keyword arguments for GetMolsFromSmilesFile().
|
| 260 |
-
|
| 261 |
-
Returns:
|
| 262 |
-
dict: Unified status response.
|
| 263 |
-
"""
|
| 264 |
-
return self._call_function("Projects.DbCLI.SearchDb", "GetMolsFromSmilesFile", *args, **kwargs)
|
| 265 |
-
|
| 266 |
-
def call_searchdb_get_neighbor_lists(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 267 |
-
"""
|
| 268 |
-
Call Projects.DbCLI.SearchDb.GetNeighborLists.
|
| 269 |
-
|
| 270 |
-
Parameters:
|
| 271 |
-
*args: Positional arguments for GetNeighborLists().
|
| 272 |
-
**kwargs: Keyword arguments for GetNeighborLists().
|
| 273 |
-
|
| 274 |
-
Returns:
|
| 275 |
-
dict: Unified status response.
|
| 276 |
-
"""
|
| 277 |
-
return self._call_function("Projects.DbCLI.SearchDb", "GetNeighborLists", *args, **kwargs)
|
| 278 |
-
|
| 279 |
-
# ==========================================================================
|
| 280 |
-
# Code.DataManip.MetricMatrixCalc.Wrap.testMatricCalc
|
| 281 |
-
# ==========================================================================
|
| 282 |
-
|
| 283 |
-
def call_testmatriccalc_feq(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 284 |
-
"""
|
| 285 |
-
Call Code.DataManip.MetricMatrixCalc.Wrap.testMatricCalc.feq.
|
| 286 |
-
|
| 287 |
-
Parameters:
|
| 288 |
-
*args: Positional arguments for feq().
|
| 289 |
-
**kwargs: Keyword arguments for feq().
|
| 290 |
-
|
| 291 |
-
Returns:
|
| 292 |
-
dict: Unified status response.
|
| 293 |
-
"""
|
| 294 |
-
return self._call_function(
|
| 295 |
-
"Code.DataManip.MetricMatrixCalc.Wrap.testMatricCalc", "feq", *args, **kwargs
|
| 296 |
-
)
|
| 297 |
-
|
| 298 |
-
# ==========================================================================
|
| 299 |
-
# Code.DataStructs.Wrap.testBV
|
| 300 |
-
# ==========================================================================
|
| 301 |
-
|
| 302 |
-
def call_testbv_feq(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 303 |
-
"""
|
| 304 |
-
Call Code.DataStructs.Wrap.testBV.feq.
|
| 305 |
-
|
| 306 |
-
Parameters:
|
| 307 |
-
*args: Positional arguments for feq().
|
| 308 |
-
**kwargs: Keyword arguments for feq().
|
| 309 |
-
|
| 310 |
-
Returns:
|
| 311 |
-
dict: Unified status response.
|
| 312 |
-
"""
|
| 313 |
-
return self._call_function("Code.DataStructs.Wrap.testBV", "feq", *args, **kwargs)
|
| 314 |
-
|
| 315 |
-
# ==========================================================================
|
| 316 |
-
# Code.DataStructs.Wrap.testSparseIntVect
|
| 317 |
-
# ==========================================================================
|
| 318 |
-
|
| 319 |
-
def call_testsparseintvect_feq(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 320 |
-
"""
|
| 321 |
-
Call Code.DataStructs.Wrap.testSparseIntVect.feq.
|
| 322 |
-
|
| 323 |
-
Parameters:
|
| 324 |
-
*args: Positional arguments for feq().
|
| 325 |
-
**kwargs: Keyword arguments for feq().
|
| 326 |
-
|
| 327 |
-
Returns:
|
| 328 |
-
dict: Unified status response.
|
| 329 |
-
"""
|
| 330 |
-
return self._call_function("Code.DataStructs.Wrap.testSparseIntVect", "feq", *args, **kwargs)
|
| 331 |
-
|
| 332 |
-
# ==========================================================================
|
| 333 |
-
# CLI-oriented helper wrappers (from analysis)
|
| 334 |
-
# ==========================================================================
|
| 335 |
-
|
| 336 |
-
def call_featfindercli_module(self, argv: Optional[List[str]] = None) -> Dict[str, Any]:
|
| 337 |
-
"""
|
| 338 |
-
Execute rdkit.Chem.FeatFinderCLI entry-style behavior if available.
|
| 339 |
-
|
| 340 |
-
Parameters:
|
| 341 |
-
argv: Optional argument list. If omitted, module defaults are used.
|
| 342 |
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
| 348 |
if module is None:
|
| 349 |
-
return
|
| 350 |
-
status
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
try:
|
| 356 |
-
if
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
return
|
| 360 |
-
status
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
|
|
|
| 364 |
except Exception as exc:
|
| 365 |
-
return
|
| 366 |
-
status
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
def
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
dict: Unified status response.
|
| 381 |
-
"""
|
| 382 |
-
module_path = "Contrib.MolVS.molvs_cli"
|
| 383 |
-
module = self._modules.get(module_path)
|
| 384 |
if module is None:
|
| 385 |
-
return
|
| 386 |
-
status
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
try:
|
| 392 |
-
if
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
return
|
| 396 |
-
status
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
|
|
|
|
|
|
| 400 |
except Exception as exc:
|
| 401 |
-
return
|
| 402 |
-
status
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
import importlib
|
| 4 |
import inspect
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from types import ModuleType
|
| 8 |
+
from typing import Any
|
| 9 |
|
| 10 |
+
SOURCE_DIR = Path(__file__).resolve().parent.parent.parent / "source"
|
| 11 |
+
if SOURCE_DIR.exists() and str(SOURCE_DIR) not in sys.path:
|
| 12 |
+
sys.path.insert(0, str(SOURCE_DIR))
|
|
|
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
class Adapter:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def __init__(self) -> None:
|
| 17 |
+
self._target_modules = [
|
| 18 |
+
"rdkit",
|
| 19 |
+
"rdkit.Chem",
|
| 20 |
+
"rdkit.Chem.AllChem",
|
| 21 |
+
"rdkit.Chem.Descriptors",
|
| 22 |
+
"rdkit.Chem.rdMolDescriptors",
|
| 23 |
+
"rdkit.DataStructs",
|
| 24 |
+
]
|
| 25 |
+
self.loaded_modules: dict[str, ModuleType] = {}
|
| 26 |
+
self.failed_modules: dict[str, str] = {}
|
| 27 |
+
self.mode = "normal"
|
| 28 |
self._load_modules()
|
| 29 |
|
| 30 |
+
def _load_modules(self) -> None:
|
| 31 |
+
for module_name in self._target_modules:
|
| 32 |
+
try:
|
| 33 |
+
self.loaded_modules[module_name] = importlib.import_module(module_name)
|
| 34 |
+
except Exception as exc:
|
| 35 |
+
self.failed_modules[module_name] = str(exc)
|
| 36 |
|
| 37 |
+
if not self.loaded_modules:
|
| 38 |
+
self.mode = "blackbox"
|
| 39 |
+
|
| 40 |
+
def health(self) -> dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
return {
|
| 42 |
+
"status": "ok" if self.loaded_modules else "fallback",
|
| 43 |
"mode": self.mode,
|
| 44 |
+
"loaded_count": len(self.loaded_modules),
|
| 45 |
+
"failed_count": len(self.failed_modules),
|
| 46 |
+
"loaded_modules": sorted(self.loaded_modules.keys()),
|
| 47 |
+
"failed_modules": self.failed_modules,
|
| 48 |
+
"source_dir": str(SOURCE_DIR),
|
| 49 |
+
"source_dir_exists": SOURCE_DIR.exists(),
|
| 50 |
}
|
| 51 |
|
| 52 |
+
def list_modules(self) -> dict[str, Any]:
|
| 53 |
+
return {
|
| 54 |
+
"status": "ok" if self.loaded_modules else "fallback",
|
| 55 |
+
"mode": self.mode,
|
| 56 |
+
"loaded": sorted(self.loaded_modules.keys()),
|
| 57 |
+
"failed": self.failed_modules,
|
| 58 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
def list_symbols(self, module_name: str, include_private: bool = False) -> dict[str, Any]:
|
| 61 |
+
module = self.loaded_modules.get(module_name)
|
|
|
|
|
|
|
| 62 |
if module is None:
|
| 63 |
+
return {
|
| 64 |
+
"status": "error",
|
| 65 |
+
"error": f"module not loaded: {module_name}",
|
| 66 |
+
"available_modules": sorted(self.loaded_modules.keys()),
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
symbols = []
|
| 70 |
+
for symbol_name in dir(module):
|
| 71 |
+
if not include_private and symbol_name.startswith("_"):
|
| 72 |
+
continue
|
| 73 |
+
try:
|
| 74 |
+
obj = getattr(module, symbol_name)
|
| 75 |
+
symbols.append(
|
| 76 |
+
{
|
| 77 |
+
"name": symbol_name,
|
| 78 |
+
"kind": self._symbol_kind(obj),
|
| 79 |
+
}
|
|
|
|
|
|
|
| 80 |
)
|
| 81 |
+
except Exception:
|
| 82 |
+
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
+
return {
|
| 85 |
+
"status": "ok",
|
| 86 |
+
"module": module_name,
|
| 87 |
+
"count": len(symbols),
|
| 88 |
+
"symbols": symbols,
|
| 89 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
+
def call_function(
|
| 92 |
+
self,
|
| 93 |
+
module_name: str,
|
| 94 |
+
function_name: str,
|
| 95 |
+
args: list[Any] | None = None,
|
| 96 |
+
kwargs: dict[str, Any] | None = None,
|
| 97 |
+
) -> dict[str, Any]:
|
| 98 |
+
module = self.loaded_modules.get(module_name)
|
| 99 |
if module is None:
|
| 100 |
+
return {
|
| 101 |
+
"status": "error",
|
| 102 |
+
"error": f"module not loaded: {module_name}",
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
if not hasattr(module, function_name):
|
| 106 |
+
return {
|
| 107 |
+
"status": "error",
|
| 108 |
+
"error": f"function not found: {module_name}.{function_name}",
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
fn = getattr(module, function_name)
|
| 112 |
+
if not callable(fn):
|
| 113 |
+
return {
|
| 114 |
+
"status": "error",
|
| 115 |
+
"error": f"symbol is not callable: {module_name}.{function_name}",
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
try:
|
| 119 |
+
call_args = args if args is not None else []
|
| 120 |
+
call_kwargs = kwargs if kwargs is not None else {}
|
| 121 |
+
result = fn(*call_args, **call_kwargs)
|
| 122 |
+
return {
|
| 123 |
+
"status": "ok",
|
| 124 |
+
"module": module_name,
|
| 125 |
+
"function": function_name,
|
| 126 |
+
"result": result,
|
| 127 |
+
}
|
| 128 |
except Exception as exc:
|
| 129 |
+
return {
|
| 130 |
+
"status": "error",
|
| 131 |
+
"error": str(exc),
|
| 132 |
+
"module": module_name,
|
| 133 |
+
"function": function_name,
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
def create_instance(
|
| 137 |
+
self,
|
| 138 |
+
module_name: str,
|
| 139 |
+
class_name: str,
|
| 140 |
+
args: list[Any] | None = None,
|
| 141 |
+
kwargs: dict[str, Any] | None = None,
|
| 142 |
+
) -> dict[str, Any]:
|
| 143 |
+
module = self.loaded_modules.get(module_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
if module is None:
|
| 145 |
+
return {
|
| 146 |
+
"status": "error",
|
| 147 |
+
"error": f"module not loaded: {module_name}",
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
if not hasattr(module, class_name):
|
| 151 |
+
return {
|
| 152 |
+
"status": "error",
|
| 153 |
+
"error": f"class not found: {module_name}.{class_name}",
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
cls = getattr(module, class_name)
|
| 157 |
+
if not inspect.isclass(cls):
|
| 158 |
+
return {
|
| 159 |
+
"status": "error",
|
| 160 |
+
"error": f"symbol is not a class: {module_name}.{class_name}",
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
try:
|
| 164 |
+
call_args = args if args is not None else []
|
| 165 |
+
call_kwargs = kwargs if kwargs is not None else {}
|
| 166 |
+
instance = cls(*call_args, **call_kwargs)
|
| 167 |
+
return {
|
| 168 |
+
"status": "ok",
|
| 169 |
+
"module": module_name,
|
| 170 |
+
"class": class_name,
|
| 171 |
+
"instance_type": type(instance).__name__,
|
| 172 |
+
"instance_repr": repr(instance),
|
| 173 |
+
}
|
| 174 |
except Exception as exc:
|
| 175 |
+
return {
|
| 176 |
+
"status": "error",
|
| 177 |
+
"error": str(exc),
|
| 178 |
+
"module": module_name,
|
| 179 |
+
"class": class_name,
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
@staticmethod
|
| 183 |
+
def _symbol_kind(obj: Any) -> str:
|
| 184 |
+
if inspect.isclass(obj):
|
| 185 |
+
return "class"
|
| 186 |
+
if inspect.ismodule(obj):
|
| 187 |
+
return "module"
|
| 188 |
+
if callable(obj):
|
| 189 |
+
return "callable"
|
| 190 |
+
return "value"
|
rdkit/mcp_output/mcp_plugin/main.py
CHANGED
|
@@ -1,13 +1,7 @@
|
|
| 1 |
-
"""
|
| 2 |
-
MCP Service Auto-Wrapper - Auto-generated
|
| 3 |
-
"""
|
| 4 |
from mcp_service import create_app
|
| 5 |
|
| 6 |
-
def main():
|
| 7 |
-
"""Main entry point"""
|
| 8 |
-
app = create_app()
|
| 9 |
-
return app
|
| 10 |
|
| 11 |
if __name__ == "__main__":
|
| 12 |
-
|
| 13 |
-
app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from mcp_service import create_app
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
if __name__ == "__main__":
|
| 5 |
+
# Local stdio entry point only (Claude Desktop / CLI), not for Docker/Web deployment.
|
| 6 |
+
app = create_app()
|
| 7 |
+
app.run(transport="stdio")
|
rdkit/mcp_output/mcp_plugin/mcp_service.py
CHANGED
|
@@ -1,149 +1,308 @@
|
|
| 1 |
-
import
|
| 2 |
-
import sys
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
|
| 8 |
from fastmcp import FastMCP
|
| 9 |
|
| 10 |
-
from
|
| 11 |
-
from Web.RDExtras.MolImage import svg, gif
|
| 12 |
-
from Web.RDExtras.MolDepict import page
|
| 13 |
-
from Projects.DbCLI.CreateDb import initParser, CreateDb
|
| 14 |
-
from Projects.DbCLI.SearchDb import GetMolsFromSDFile, GetNeighborLists, GetMolsFromSmilesFile
|
| 15 |
-
from Code.DataManip.MetricMatrixCalc.Wrap.testMatricCalc import feq
|
| 16 |
-
from Code.DataStructs.Wrap.testBV import feq
|
| 17 |
-
from Code.DataStructs.Wrap.testSparseIntVect import feq
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
| 23 |
-
def
|
| 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 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 51 |
-
|
| 52 |
-
@mcp.tool(name="page", description="Auto-wrapped function page")
|
| 53 |
-
def page(payload: dict):
|
| 54 |
-
try:
|
| 55 |
-
if page is None:
|
| 56 |
-
return {"success": False, "result": None, "error": "Function page is not available"}
|
| 57 |
-
result = page(**payload)
|
| 58 |
-
return {"success": True, "result": result, "error": None}
|
| 59 |
-
except Exception as e:
|
| 60 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 61 |
-
|
| 62 |
-
@mcp.tool(name="CreateDb", description="Auto-wrapped function CreateDb")
|
| 63 |
-
def CreateDb(payload: dict):
|
| 64 |
-
try:
|
| 65 |
-
if CreateDb is None:
|
| 66 |
-
return {"success": False, "result": None, "error": "Function CreateDb is not available"}
|
| 67 |
-
result = CreateDb(**payload)
|
| 68 |
-
return {"success": True, "result": result, "error": None}
|
| 69 |
-
except Exception as e:
|
| 70 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 71 |
-
|
| 72 |
-
@mcp.tool(name="initParser", description="Auto-wrapped function initParser")
|
| 73 |
-
def initParser(payload: dict):
|
| 74 |
try:
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
| 84 |
try:
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
try:
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
try:
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
try:
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
try:
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
try:
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
return {"success": True, "result": result, "error": None}
|
| 139 |
-
except Exception as e:
|
| 140 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
return mcp
|
| 147 |
|
|
|
|
| 148 |
if __name__ == "__main__":
|
| 149 |
-
mcp.run(
|
|
|
|
| 1 |
+
from __future__ import annotations
|
|
|
|
| 2 |
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
|
| 7 |
from fastmcp import FastMCP
|
| 8 |
|
| 9 |
+
from adapter import Adapter
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
SOURCE_DIR = Path(__file__).resolve().parent.parent.parent / "source"
|
| 12 |
+
if SOURCE_DIR.exists() and str(SOURCE_DIR) not in sys.path:
|
| 13 |
+
sys.path.insert(0, str(SOURCE_DIR))
|
| 14 |
|
| 15 |
+
adapter = Adapter()
|
| 16 |
+
mcp = FastMCP("rdkit-mcp-service")
|
| 17 |
|
| 18 |
+
|
| 19 |
+
def _ok(result: Any) -> dict[str, Any]:
|
| 20 |
+
return {"success": True, "result": result, "error": None}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _err(message: str) -> dict[str, Any]:
|
| 24 |
+
return {"success": False, "result": None, "error": message}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _safe_smiles_to_mol(smiles: str, sanitize: bool = True):
|
| 28 |
+
status = adapter.call_function(
|
| 29 |
+
"rdkit.Chem",
|
| 30 |
+
"MolFromSmiles",
|
| 31 |
+
args=[smiles],
|
| 32 |
+
kwargs={"sanitize": sanitize},
|
| 33 |
+
)
|
| 34 |
+
if status["status"] != "ok":
|
| 35 |
+
raise ValueError(status.get("error", "failed to parse SMILES"))
|
| 36 |
+
return status["result"]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@mcp.tool(name="rdkit_health", description="Get MCP adapter health and module loading status")
|
| 40 |
+
def rdkit_health() -> dict[str, Any]:
|
| 41 |
+
"""Return adapter health information.
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
Standardized response with mode, loaded modules, and failures.
|
| 45 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
try:
|
| 47 |
+
return _ok(adapter.health())
|
| 48 |
+
except Exception as exc:
|
| 49 |
+
return _err(str(exc))
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@mcp.tool(name="list_rdkit_modules", description="List loaded and failed RDKit modules")
|
| 53 |
+
def list_rdkit_modules() -> dict[str, Any]:
|
| 54 |
+
"""List module import outcomes.
|
| 55 |
+
|
| 56 |
+
Returns:
|
| 57 |
+
Standardized response containing loaded and failed module lists.
|
| 58 |
+
"""
|
| 59 |
try:
|
| 60 |
+
return _ok(adapter.list_modules())
|
| 61 |
+
except Exception as exc:
|
| 62 |
+
return _err(str(exc))
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@mcp.tool(name="parse_smiles", description="Parse SMILES and return canonical structure metadata")
|
| 66 |
+
def parse_smiles(smiles: str, sanitize: bool = True) -> dict[str, Any]:
|
| 67 |
+
"""Parse a SMILES string into an RDKit molecule and compute basic properties.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
smiles: Input SMILES string.
|
| 71 |
+
sanitize: Whether to sanitize molecule during parsing.
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
Canonical SMILES, formula, exact mass, atom count, and bond count.
|
| 75 |
+
"""
|
| 76 |
try:
|
| 77 |
+
mol = _safe_smiles_to_mol(smiles=smiles, sanitize=sanitize)
|
| 78 |
+
if mol is None:
|
| 79 |
+
return _err("invalid SMILES")
|
| 80 |
+
|
| 81 |
+
canon = adapter.call_function(
|
| 82 |
+
"rdkit.Chem",
|
| 83 |
+
"MolToSmiles",
|
| 84 |
+
args=[mol],
|
| 85 |
+
kwargs={"canonical": True},
|
| 86 |
+
)
|
| 87 |
+
formula = adapter.call_function("rdkit.Chem.rdMolDescriptors", "CalcMolFormula", args=[mol])
|
| 88 |
+
exact_wt = adapter.call_function("rdkit.Chem.rdMolDescriptors", "CalcExactMolWt", args=[mol])
|
| 89 |
+
atom_count = mol.GetNumAtoms()
|
| 90 |
+
bond_count = mol.GetNumBonds()
|
| 91 |
+
|
| 92 |
+
result = {
|
| 93 |
+
"input_smiles": smiles,
|
| 94 |
+
"canonical_smiles": canon.get("result") if canon["status"] == "ok" else None,
|
| 95 |
+
"molecular_formula": formula.get("result") if formula["status"] == "ok" else None,
|
| 96 |
+
"exact_molecular_weight": exact_wt.get("result") if exact_wt["status"] == "ok" else None,
|
| 97 |
+
"num_atoms": atom_count,
|
| 98 |
+
"num_bonds": bond_count,
|
| 99 |
+
}
|
| 100 |
+
return _ok(result)
|
| 101 |
+
except Exception as exc:
|
| 102 |
+
return _err(str(exc))
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
@mcp.tool(name="compute_descriptors", description="Compute common RDKit molecular descriptors")
|
| 106 |
+
def compute_descriptors(smiles: str) -> dict[str, Any]:
|
| 107 |
+
"""Compute common descriptors for a molecule.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
smiles: Input SMILES string.
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
Dictionary of standard descriptors such as MolWt, LogP, TPSA and H-bond counts.
|
| 114 |
+
"""
|
| 115 |
try:
|
| 116 |
+
mol = _safe_smiles_to_mol(smiles=smiles, sanitize=True)
|
| 117 |
+
if mol is None:
|
| 118 |
+
return _err("invalid SMILES")
|
| 119 |
+
|
| 120 |
+
descriptors = {}
|
| 121 |
+
descriptor_calls = {
|
| 122 |
+
"MolWt": ("rdkit.Chem.Descriptors", "MolWt"),
|
| 123 |
+
"ExactMolWt": ("rdkit.Chem.rdMolDescriptors", "CalcExactMolWt"),
|
| 124 |
+
"LogP": ("rdkit.Chem.Descriptors", "MolLogP"),
|
| 125 |
+
"TPSA": ("rdkit.Chem.rdMolDescriptors", "CalcTPSA"),
|
| 126 |
+
"NumHDonors": ("rdkit.Chem.rdMolDescriptors", "CalcNumHBD"),
|
| 127 |
+
"NumHAcceptors": ("rdkit.Chem.rdMolDescriptors", "CalcNumHBA"),
|
| 128 |
+
"NumRotatableBonds": ("rdkit.Chem.rdMolDescriptors", "CalcNumRotatableBonds"),
|
| 129 |
+
"RingCount": ("rdkit.Chem.rdMolDescriptors", "CalcNumRings"),
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
for key, (module_name, function_name) in descriptor_calls.items():
|
| 133 |
+
call = adapter.call_function(module_name, function_name, args=[mol])
|
| 134 |
+
descriptors[key] = call.get("result") if call["status"] == "ok" else None
|
| 135 |
+
|
| 136 |
+
return _ok({"smiles": smiles, "descriptors": descriptors})
|
| 137 |
+
except Exception as exc:
|
| 138 |
+
return _err(str(exc))
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
@mcp.tool(name="substructure_match", description="Run SMARTS substructure search on a target molecule")
|
| 142 |
+
def substructure_match(
|
| 143 |
+
smiles: str,
|
| 144 |
+
smarts: str,
|
| 145 |
+
uniquify: bool = True,
|
| 146 |
+
max_matches: int = 100,
|
| 147 |
+
) -> dict[str, Any]:
|
| 148 |
+
"""Search for SMARTS pattern matches in a molecule.
|
| 149 |
+
|
| 150 |
+
Args:
|
| 151 |
+
smiles: Target molecule SMILES.
|
| 152 |
+
smarts: SMARTS query.
|
| 153 |
+
uniquify: Return unique matches only when True.
|
| 154 |
+
max_matches: Maximum number of matches to return.
|
| 155 |
+
|
| 156 |
+
Returns:
|
| 157 |
+
Match count and atom index tuples.
|
| 158 |
+
"""
|
| 159 |
try:
|
| 160 |
+
target = _safe_smiles_to_mol(smiles=smiles, sanitize=True)
|
| 161 |
+
query_status = adapter.call_function("rdkit.Chem", "MolFromSmarts", args=[smarts])
|
| 162 |
+
if query_status["status"] != "ok":
|
| 163 |
+
return _err(query_status.get("error", "failed to parse SMARTS"))
|
| 164 |
+
|
| 165 |
+
query = query_status["result"]
|
| 166 |
+
if query is None:
|
| 167 |
+
return _err("invalid SMARTS")
|
| 168 |
+
|
| 169 |
+
matches = [
|
| 170 |
+
list(match)
|
| 171 |
+
for match in target.GetSubstructMatches(query, uniquify=uniquify, maxMatches=max_matches)
|
| 172 |
+
]
|
| 173 |
+
return _ok(
|
| 174 |
+
{
|
| 175 |
+
"smiles": smiles,
|
| 176 |
+
"smarts": smarts,
|
| 177 |
+
"match_count": len(matches),
|
| 178 |
+
"matches": matches,
|
| 179 |
+
}
|
| 180 |
+
)
|
| 181 |
+
except Exception as exc:
|
| 182 |
+
return _err(str(exc))
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@mcp.tool(name="tanimoto_similarity", description="Compute Tanimoto similarity between two molecules")
|
| 186 |
+
def tanimoto_similarity(
|
| 187 |
+
smiles_a: str,
|
| 188 |
+
smiles_b: str,
|
| 189 |
+
radius: int = 2,
|
| 190 |
+
n_bits: int = 2048,
|
| 191 |
+
) -> dict[str, Any]:
|
| 192 |
+
"""Calculate Tanimoto similarity between Morgan fingerprints.
|
| 193 |
+
|
| 194 |
+
Args:
|
| 195 |
+
smiles_a: First molecule SMILES.
|
| 196 |
+
smiles_b: Second molecule SMILES.
|
| 197 |
+
radius: Morgan fingerprint radius.
|
| 198 |
+
n_bits: Morgan fingerprint bit vector size.
|
| 199 |
+
|
| 200 |
+
Returns:
|
| 201 |
+
Similarity score and canonicalized SMILES for both molecules.
|
| 202 |
+
"""
|
| 203 |
try:
|
| 204 |
+
mol_a = _safe_smiles_to_mol(smiles=smiles_a, sanitize=True)
|
| 205 |
+
mol_b = _safe_smiles_to_mol(smiles=smiles_b, sanitize=True)
|
| 206 |
+
if mol_a is None or mol_b is None:
|
| 207 |
+
return _err("one or both SMILES are invalid")
|
| 208 |
+
|
| 209 |
+
fp_a = adapter.call_function(
|
| 210 |
+
"rdkit.Chem.AllChem",
|
| 211 |
+
"GetMorganFingerprintAsBitVect",
|
| 212 |
+
args=[mol_a, radius],
|
| 213 |
+
kwargs={"nBits": n_bits},
|
| 214 |
+
)
|
| 215 |
+
fp_b = adapter.call_function(
|
| 216 |
+
"rdkit.Chem.AllChem",
|
| 217 |
+
"GetMorganFingerprintAsBitVect",
|
| 218 |
+
args=[mol_b, radius],
|
| 219 |
+
kwargs={"nBits": n_bits},
|
| 220 |
+
)
|
| 221 |
+
if fp_a["status"] != "ok" or fp_b["status"] != "ok":
|
| 222 |
+
return _err("failed to compute fingerprints")
|
| 223 |
+
|
| 224 |
+
sim = adapter.call_function(
|
| 225 |
+
"rdkit.DataStructs",
|
| 226 |
+
"TanimotoSimilarity",
|
| 227 |
+
args=[fp_a["result"], fp_b["result"]],
|
| 228 |
+
)
|
| 229 |
+
if sim["status"] != "ok":
|
| 230 |
+
return _err(sim.get("error", "failed to compute similarity"))
|
| 231 |
+
|
| 232 |
+
canon_a = adapter.call_function("rdkit.Chem", "MolToSmiles", args=[mol_a], kwargs={"canonical": True})
|
| 233 |
+
canon_b = adapter.call_function("rdkit.Chem", "MolToSmiles", args=[mol_b], kwargs={"canonical": True})
|
| 234 |
+
|
| 235 |
+
return _ok(
|
| 236 |
+
{
|
| 237 |
+
"smiles_a": smiles_a,
|
| 238 |
+
"smiles_b": smiles_b,
|
| 239 |
+
"canonical_smiles_a": canon_a.get("result") if canon_a["status"] == "ok" else None,
|
| 240 |
+
"canonical_smiles_b": canon_b.get("result") if canon_b["status"] == "ok" else None,
|
| 241 |
+
"radius": radius,
|
| 242 |
+
"n_bits": n_bits,
|
| 243 |
+
"tanimoto_similarity": sim["result"],
|
| 244 |
+
}
|
| 245 |
+
)
|
| 246 |
+
except Exception as exc:
|
| 247 |
+
return _err(str(exc))
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
@mcp.tool(name="morgan_fingerprint_bits", description="Generate Morgan fingerprint and return active bit indices")
|
| 251 |
+
def morgan_fingerprint_bits(
|
| 252 |
+
smiles: str,
|
| 253 |
+
radius: int = 2,
|
| 254 |
+
n_bits: int = 2048,
|
| 255 |
+
max_bits: int = 256,
|
| 256 |
+
) -> dict[str, Any]:
|
| 257 |
+
"""Generate a Morgan fingerprint bit vector and return active bit indices.
|
| 258 |
+
|
| 259 |
+
Args:
|
| 260 |
+
smiles: Input SMILES string.
|
| 261 |
+
radius: Morgan fingerprint radius.
|
| 262 |
+
n_bits: Fingerprint bit vector length.
|
| 263 |
+
max_bits: Maximum active bit indices to return.
|
| 264 |
+
|
| 265 |
+
Returns:
|
| 266 |
+
Active bit indices and count.
|
| 267 |
+
"""
|
| 268 |
try:
|
| 269 |
+
mol = _safe_smiles_to_mol(smiles=smiles, sanitize=True)
|
| 270 |
+
if mol is None:
|
| 271 |
+
return _err("invalid SMILES")
|
|
|
|
|
|
|
|
|
|
| 272 |
|
| 273 |
+
fp_status = adapter.call_function(
|
| 274 |
+
"rdkit.Chem.AllChem",
|
| 275 |
+
"GetMorganFingerprintAsBitVect",
|
| 276 |
+
args=[mol, radius],
|
| 277 |
+
kwargs={"nBits": n_bits},
|
| 278 |
+
)
|
| 279 |
+
if fp_status["status"] != "ok":
|
| 280 |
+
return _err(fp_status.get("error", "failed to compute fingerprint"))
|
| 281 |
|
| 282 |
+
fp = fp_status["result"]
|
| 283 |
+
bits_status = adapter.call_function("rdkit.DataStructs", "BitVectToText", args=[fp])
|
| 284 |
+
if bits_status["status"] != "ok":
|
| 285 |
+
return _err(bits_status.get("error", "failed to serialize fingerprint"))
|
| 286 |
|
| 287 |
+
bitstring = bits_status["result"]
|
| 288 |
+
active_bits = [idx for idx, value in enumerate(bitstring) if value == "1"]
|
| 289 |
+
return _ok(
|
| 290 |
+
{
|
| 291 |
+
"smiles": smiles,
|
| 292 |
+
"radius": radius,
|
| 293 |
+
"n_bits": n_bits,
|
| 294 |
+
"active_bit_count": len(active_bits),
|
| 295 |
+
"active_bits": active_bits[:max_bits],
|
| 296 |
+
"truncated": len(active_bits) > max_bits,
|
| 297 |
+
}
|
| 298 |
+
)
|
| 299 |
+
except Exception as exc:
|
| 300 |
+
return _err(str(exc))
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def create_app() -> FastMCP:
|
| 304 |
return mcp
|
| 305 |
|
| 306 |
+
|
| 307 |
if __name__ == "__main__":
|
| 308 |
+
mcp.run()
|
rdkit/mcp_output/requirements.txt
CHANGED
|
@@ -1,6 +1,3 @@
|
|
| 1 |
fastmcp
|
| 2 |
-
|
| 3 |
-
uvicorn[standard]
|
| 4 |
-
pydantic>=2.0.0
|
| 5 |
-
rdkit compiled Python package (C++ extension modules)
|
| 6 |
numpy
|
|
|
|
| 1 |
fastmcp
|
| 2 |
+
rdkit
|
|
|
|
|
|
|
|
|
|
| 3 |
numpy
|
rdkit/mcp_output/start_mcp.py
CHANGED
|
@@ -1,30 +1,29 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
"""
|
| 3 |
-
MCP Service Startup Entry
|
| 4 |
-
"""
|
| 5 |
-
import sys
|
| 6 |
import os
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
sys.path.insert(0, mcp_plugin_dir)
|
| 12 |
|
| 13 |
from mcp_service import create_app
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
| 17 |
app = create_app()
|
| 18 |
-
# Use environment variable to configure port, default 8000
|
| 19 |
-
port = int(os.environ.get("MCP_PORT", "8000"))
|
| 20 |
-
|
| 21 |
-
# Choose transport mode based on environment variable
|
| 22 |
-
transport = os.environ.get("MCP_TRANSPORT", "stdio")
|
| 23 |
if transport == "http":
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
| 25 |
else:
|
| 26 |
-
|
| 27 |
-
|
| 28 |
|
| 29 |
if __name__ == "__main__":
|
| 30 |
main()
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import os
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
|
| 7 |
+
PLUGIN_DIR = Path(__file__).resolve().parent / "mcp_plugin"
|
| 8 |
+
if str(PLUGIN_DIR) not in sys.path:
|
| 9 |
+
sys.path.insert(0, str(PLUGIN_DIR))
|
|
|
|
| 10 |
|
| 11 |
from mcp_service import create_app
|
| 12 |
|
| 13 |
+
|
| 14 |
+
def main() -> None:
|
| 15 |
+
transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower()
|
| 16 |
+
port = int(os.getenv("MCP_PORT", "8000"))
|
| 17 |
+
|
| 18 |
app = create_app()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
if transport == "http":
|
| 20 |
+
try:
|
| 21 |
+
app.run(transport="http", host="0.0.0.0", port=port)
|
| 22 |
+
except TypeError:
|
| 23 |
+
app.run(transport="http", port=port)
|
| 24 |
else:
|
| 25 |
+
app.run(transport="stdio")
|
| 26 |
+
|
| 27 |
|
| 28 |
if __name__ == "__main__":
|
| 29 |
main()
|
requirements.txt
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
fastmcp
|
| 2 |
-
fastapi
|
| 3 |
-
uvicorn[standard]
|
| 4 |
-
pydantic>=2.0.0
|
| 5 |
rdkit
|
| 6 |
numpy
|
|
|
|
|
|
|
|
|
| 1 |
fastmcp
|
|
|
|
|
|
|
|
|
|
| 2 |
rdkit
|
| 3 |
numpy
|
| 4 |
+
fastapi
|
| 5 |
+
uvicorn
|
run_docker.ps1
CHANGED
|
@@ -1,26 +1,8 @@
|
|
| 1 |
-
cd $PSScriptRoot
|
| 2 |
$ErrorActionPreference = "Stop"
|
| 3 |
-
|
| 4 |
-
$
|
| 5 |
-
$
|
| 6 |
-
$
|
| 7 |
-
|
| 8 |
-
if (!(Test-Path $mcpDir)) { New-Item -ItemType Directory -Path $mcpDir | Out-Null }
|
| 9 |
-
$config = @{}
|
| 10 |
-
if (Test-Path $mcpPath) {
|
| 11 |
-
try { $config = Get-Content $mcpPath -Raw | ConvertFrom-Json } catch { $config = @{} }
|
| 12 |
-
}
|
| 13 |
-
$serversOrdered = [ordered]@{}
|
| 14 |
-
if ($config -and ($config.PSObject.Properties.Name -contains "mcpServers") -and $config.mcpServers) {
|
| 15 |
-
$existing = $config.mcpServers
|
| 16 |
-
if ($existing -is [pscustomobject]) {
|
| 17 |
-
foreach ($p in $existing.PSObject.Properties) { if ($p.Name -ne $entryName) { $serversOrdered[$p.Name] = $p.Value } }
|
| 18 |
-
} elseif ($existing -is [System.Collections.IDictionary]) {
|
| 19 |
-
foreach ($k in $existing.Keys) { if ($k -ne $entryName) { $serversOrdered[$k] = $existing[$k] } }
|
| 20 |
-
}
|
| 21 |
-
}
|
| 22 |
-
$serversOrdered[$entryName] = @{ url = $entryUrl }
|
| 23 |
-
$config = @{ mcpServers = $serversOrdered }
|
| 24 |
-
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $mcpPath -Encoding UTF8
|
| 25 |
docker build -t $imageName .
|
| 26 |
-
docker run --rm -p
|
|
|
|
|
|
|
| 1 |
$ErrorActionPreference = "Stop"
|
| 2 |
+
|
| 3 |
+
$portConfig = Get-Content -Raw -Path "port.json" | ConvertFrom-Json
|
| 4 |
+
$port = [int]$portConfig.port
|
| 5 |
+
$imageName = "rdkit-mcp"
|
| 6 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
docker build -t $imageName .
|
| 8 |
+
docker run --rm -p "${port}:${port}" $imageName
|
run_docker.sh
CHANGED
|
@@ -1,75 +1,8 @@
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
set -euo pipefail
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
if command -v python3 >/dev/null 2>&1; then
|
| 10 |
-
python3 - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
|
| 11 |
-
import json, os, sys
|
| 12 |
-
path, name, url = sys.argv[1:4]
|
| 13 |
-
cfg = {"mcpServers": {}}
|
| 14 |
-
if os.path.exists(path):
|
| 15 |
-
try:
|
| 16 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 17 |
-
cfg = json.load(f)
|
| 18 |
-
except Exception:
|
| 19 |
-
cfg = {"mcpServers": {}}
|
| 20 |
-
if not isinstance(cfg, dict):
|
| 21 |
-
cfg = {"mcpServers": {}}
|
| 22 |
-
servers = cfg.get("mcpServers")
|
| 23 |
-
if not isinstance(servers, dict):
|
| 24 |
-
servers = {}
|
| 25 |
-
ordered = {}
|
| 26 |
-
for k, v in servers.items():
|
| 27 |
-
if k != name:
|
| 28 |
-
ordered[k] = v
|
| 29 |
-
ordered[name] = {"url": url}
|
| 30 |
-
cfg = {"mcpServers": ordered}
|
| 31 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 32 |
-
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
| 33 |
-
PY
|
| 34 |
-
elif command -v python >/dev/null 2>&1; then
|
| 35 |
-
python - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
|
| 36 |
-
import json, os, sys
|
| 37 |
-
path, name, url = sys.argv[1:4]
|
| 38 |
-
cfg = {"mcpServers": {}}
|
| 39 |
-
if os.path.exists(path):
|
| 40 |
-
try:
|
| 41 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 42 |
-
cfg = json.load(f)
|
| 43 |
-
except Exception:
|
| 44 |
-
cfg = {"mcpServers": {}}
|
| 45 |
-
if not isinstance(cfg, dict):
|
| 46 |
-
cfg = {"mcpServers": {}}
|
| 47 |
-
servers = cfg.get("mcpServers")
|
| 48 |
-
if not isinstance(servers, dict):
|
| 49 |
-
servers = {}
|
| 50 |
-
ordered = {}
|
| 51 |
-
for k, v in servers.items():
|
| 52 |
-
if k != name:
|
| 53 |
-
ordered[k] = v
|
| 54 |
-
ordered[name] = {"url": url}
|
| 55 |
-
cfg = {"mcpServers": ordered}
|
| 56 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 57 |
-
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
| 58 |
-
PY
|
| 59 |
-
elif command -v jq >/dev/null 2>&1; then
|
| 60 |
-
name="${mcp_entry_name}"; url="${mcp_entry_url}"
|
| 61 |
-
if [ -f "${mcp_path}" ]; then
|
| 62 |
-
tmp="$(mktemp)"
|
| 63 |
-
jq --arg name "$name" --arg url "$url" '
|
| 64 |
-
.mcpServers = (.mcpServers // {})
|
| 65 |
-
| .mcpServers as $s
|
| 66 |
-
| ($s | with_entries(select(.key != $name))) as $base
|
| 67 |
-
| .mcpServers = ($base + {($name): {"url": $url}})
|
| 68 |
-
' "${mcp_path}" > "${tmp}" && mv "${tmp}" "${mcp_path}"
|
| 69 |
-
else
|
| 70 |
-
printf '{ "mcpServers": { "%s": { "url": "%s" } } }
|
| 71 |
-
' "$name" "$url" > "${mcp_path}"
|
| 72 |
-
fi
|
| 73 |
-
fi
|
| 74 |
-
docker build -t rdkit-mcp .
|
| 75 |
-
docker run --rm -p 7914:7860 rdkit-mcp
|
|
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
PORT="$(python3 -c 'import json; print(json.load(open("port.json"))["port"])')"
|
| 5 |
+
IMAGE_NAME="rdkit-mcp"
|
| 6 |
+
|
| 7 |
+
docker build -t "${IMAGE_NAME}" .
|
| 8 |
+
docker run --rm -p "${PORT}:${PORT}" "${IMAGE_NAME}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|