Source code for tooluniverse.molgluedb_tool
# molgluedb_tool.py
"""
MolGlueDB tools for ToolUniverse -- molecular glue degrader compounds.
MolGlueDB (molgluedb.com) is a curated database of molecular glue
degraders (MGDs): small molecules that induce proximity between a target
protein and a recruiting protein (e.g. an E3 ligase) without the
warhead-linker-ligand architecture of a PROTAC. ToolUniverse's existing
PROTAC-DB tools (protacdb_tool.py) cover that other, bifunctional
degrader modality; MolGlueDB fills the monofunctional-glue gap, with
structural, degradation, and physicochemical data plus curated
pharmacophore/scaffold classifications (e.g. "Glutarimide" /
"LenalidomideType") that PROTAC-DB has no equivalent for.
There is no documented public API, but the site's own frontend calls a
same-origin JSON backend at /api/query_bj_data (search) and
/api/query_bj_data_by_id (single record). That backend sits behind a
WAF that silently serves the SPA shell (HTTP 200, no error) to requests
lacking a browser-like User-Agent and Referer -- both are set here for
that reason, not to evade any access control (the data itself is fully
public and described by its publisher as free and open-access).
No authentication required.
"""
from typing import Any, Dict, Optional
import requests
from .base_tool import BaseTool
from .tool_registry import register_tool
MOLGLUEDB_BASE_URL = "https://www.molgluedb.com/api"
_HEADERS = {
"User-Agent": "Mozilla/5.0 (compatible; ToolUniverse/1.0)",
"Referer": "https://www.molgluedb.com/",
"Content-Type": "application/json",
"Accept": "application/json",
}
_FIELDS = (
"id",
"Name",
"SMILES",
"IUPAC",
"StdInChIKey",
"Formula",
"MolWeight",
"Pharmacophore",
"Core",
"ModeOfAction",
"ResearchStage",
"PrimaryTarget",
"PrimaryTargetDegInfo",
"SecondaryTarget",
"SecondaryTargetDegInfo",
"RecruitingProtein",
"RecruitingProtein_UniProtID",
"PDB",
"SourceName",
"SourceAddress_Website",
)
def _summarize(rec: Dict[str, Any]) -> Dict[str, Any]:
return {field: rec.get(field) for field in _FIELDS}
[docs]
@register_tool("MolGlueDBTool")
class MolGlueDBTool(BaseTool):
"""
Tool for querying MolGlueDB, dispatched by fields.operation:
- "search_compounds" : full-text keyword search over molecular glues
- "get_compound" : a single compound by its MolGlueDB id
No authentication required.
"""
[docs]
def __init__(self, tool_config: Dict[str, Any]):
super().__init__(tool_config)
self.timeout = tool_config.get("timeout", 30)
self.operation = tool_config.get("fields", {}).get(
"operation", "search_compounds"
)
[docs]
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
try:
if self.operation == "search_compounds":
return self._search_compounds(arguments)
if self.operation == "get_compound":
return self._get_compound(arguments)
return {
"status": "error",
"error": f"Unknown operation: {self.operation}",
}
except requests.exceptions.Timeout:
return {
"status": "error",
"error": f"MolGlueDB request timed out after {self.timeout}s",
}
except requests.exceptions.RequestException as e:
return {"status": "error", "error": f"MolGlueDB request failed: {e}"}
except ValueError:
return {
"status": "error",
"error": "MolGlueDB returned a non-JSON response",
}
[docs]
def _limit(self, arguments: Dict[str, Any], default: int = 30) -> int:
try:
return max(1, min(int(arguments.get("limit") or default), 200))
except (TypeError, ValueError):
return default
[docs]
def _search_compounds(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
keyword = (arguments.get("keyword") or "").strip()
if not keyword:
return {
"status": "error",
"error": "keyword is required, e.g. a target protein "
"('IKZF2'), recruiting protein ('CRBN'), or compound name "
"('Lenalidomide').",
}
limit = self._limit(arguments)
resp = requests.post(
f"{MOLGLUEDB_BASE_URL}/query_bj_data",
json={"page": 1, "pageSize": limit, "keyword": keyword},
headers=_HEADERS,
timeout=self.timeout,
)
resp.raise_for_status()
payload = resp.json()
if payload.get("status") != "success":
return {
"status": "error",
"error": f"MolGlueDB search failed: {payload}",
}
rows = payload.get("data") or []
return {
"status": "success",
"data": [_summarize(r) for r in rows],
"metadata": {
"keyword": keyword,
"total_matching": payload.get("total"),
"returned": len(rows),
"source": "MolGlueDB (molgluedb.com)",
},
}
[docs]
def _get_compound(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
compound_id = arguments.get("compound_id")
try:
compound_id = int(compound_id)
except (TypeError, ValueError):
return {
"status": "error",
"error": "compound_id is required and must be an integer, "
"e.g. 1.",
}
resp = requests.get(
f"{MOLGLUEDB_BASE_URL}/query_bj_data_by_id",
params={"id": compound_id},
headers=_HEADERS,
timeout=self.timeout,
)
if resp.status_code == 404:
return {
"status": "error",
"error": f"No MolGlueDB compound found for id {compound_id}.",
}
resp.raise_for_status()
payload = resp.json()
record = payload.get("data")
if not record:
return {
"status": "error",
"error": f"No MolGlueDB compound found for id {compound_id}.",
}
return {
"status": "success",
"data": _summarize(record),
"metadata": {
"compound_id": compound_id,
"source": "MolGlueDB (molgluedb.com)",
},
}