ghh1125 commited on
Commit
06f7446
·
verified ·
1 Parent(s): 35a8425

Upload 14 files

Browse files
Dockerfile CHANGED
@@ -1,18 +1,23 @@
1
- FROM python:3.10
2
 
3
- RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
4
- USER user
5
- ENV PATH="/home/user/.local/bin:$PATH"
6
 
7
  WORKDIR /app
8
 
9
- COPY --chown=user ./requirements.txt requirements.txt
10
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
 
 
 
11
 
12
- COPY --chown=user . /app
13
  ENV MCP_TRANSPORT=http
14
  ENV MCP_PORT=7860
15
 
16
  EXPOSE 7860
17
 
18
- CMD ["python", "rdkit/mcp_output/start_mcp.py"]
 
 
 
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: Rdkit
3
- emoji: 📊
4
- colorFrom: purple
5
- colorTo: blue
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 fastapi import FastAPI
 
2
  import os
3
  import sys
 
 
 
 
4
 
5
- mcp_plugin_path = os.path.join(os.path.dirname(__file__), "rdkit", "mcp_output", "mcp_plugin")
6
- sys.path.insert(0, mcp_plugin_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": "Rdkit MCP Service",
18
- "version": "1.0.0",
19
- "status": "running",
20
- "transport": os.environ.get("MCP_TRANSPORT", "http")
 
21
  }
22
 
 
23
  @app.get("/health")
24
- def health_check():
25
- return {"status": "healthy", "service": "rdkit MCP"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  @app.get("/tools")
28
- def list_tools():
29
- try:
30
- from mcp_service import create_app
31
- mcp_app = create_app()
32
- tools = []
33
- for tool_name, tool_func in mcp_app.tools.items():
34
- tools.append({
35
- "name": tool_name,
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 (Model Context Protocol) Service README
2
-
3
- ## 1) Project Introduction
4
-
5
- This project provides an MCP (Model Context Protocol) service wrapper around RDKit for cheminformatics workflows.
6
- It is intended for LLM/tooling integrations that need molecule parsing, descriptor calculation, fingerprinting, similarity, substructure matching, and rendering.
7
-
8
- Typical use cases:
9
- - SMILES/InChI/mol block conversion and validation
10
- - Molecular descriptors (2D/3D, QED, Lipinski, etc.)
11
- - Fingerprint generation and similarity search
12
- - Substructure and MCS matching
13
- - Conformer/geometry operations
14
- - Optional utilities for database workflows and feature extraction
15
-
16
- ---
17
-
18
- ## 2) Installation Method
19
-
20
- ### System requirements
21
- - Python 3.10+ recommended
22
- - RDKit installed with compiled C++ extensions
23
- - `numpy` (required)
24
- - Optional: `Pillow`, `pandas`, `scipy`, `matplotlib`, `IPython`, drawing backends (Cairo/Qt), InChI libs
25
-
26
- ### Recommended install (Conda)
27
- - conda create -n rdkit-mcp -c conda-forge python=3.11 rdkit numpy
28
- - conda activate rdkit-mcp
29
-
30
- ### Optional extras
31
- - pip install pillow pandas scipy matplotlib ipython
32
-
33
- ### Verify installation
34
- - python -c "from rdkit import Chem; print(Chem.MolFromSmiles('CCO') is not None)"
35
-
36
- ---
37
-
38
- ## 3) Quick Start
39
-
40
- ### Basic molecule operations
41
- - Parse: `Chem.MolFromSmiles(...)`
42
- - Canonicalize: `Chem.CanonSmiles(...)`
43
- - Substructure: `mol.HasSubstructMatch(query)`
44
- - Descriptors: `Descriptors.CalcMolDescriptors(mol)`
45
- - Fingerprints + similarity: `DataStructs.FingerprintSimilarity(fp1, fp2)`
46
- - Draw: `Draw.MolToImage(mol)`
47
-
48
- ### Typical MCP (Model Context Protocol) service flow
49
- 1. Client sends molecule input (SMILES/InChI/mol block).
50
- 2. Service validates and normalizes structure.
51
- 3. Service executes requested operation (descriptor/fingerprint/search/render/etc.).
52
- 4. Service returns structured JSON (result, warnings, errors).
53
-
54
- ---
55
-
56
- ## 4) Available Tools and Endpoints List
57
-
58
- Suggested MCP (Model Context Protocol) service endpoints/tools:
59
-
60
- - `parse_molecule`
61
- - Input: SMILES/InChI/mol block
62
- - Output: normalized molecule metadata, canonical SMILES, validation status
63
-
64
- - `compute_descriptors`
65
- - Output: selected descriptors (e.g., MolWt, LogP, TPSA, QED, counts)
66
-
67
- - `generate_fingerprint`
68
- - Output: RDKit/Morgan/AtomPair/Torsion fingerprints (configurable params)
69
-
70
- - `similarity`
71
- - Output: Tanimoto/Dice/Cosine similarity between two molecules/fingerprints
72
-
73
- - `substructure_search`
74
- - Input: target + SMARTS query
75
- - Output: match boolean, atom/bond match indices
76
-
77
- - `find_mcs`
78
- - Output: maximum common substructure (SMARTS, atom/bond counts)
79
-
80
- - `enumerate_stereoisomers`
81
- - Output: stereoisomer set and count
82
-
83
- - `enumerate_heterocycles`
84
- - Output: transformed heterocycle variants
85
-
86
- - `standardize_molecule`
87
- - Output: cleaned/neutralized/tautomer-ordered form (MolStandardize)
88
-
89
- - `draw_molecule`
90
- - Output: PNG/SVG payload or file reference
91
-
92
- - `reaction_enumeration` (optional)
93
- - Output: products from reaction + building blocks
94
-
95
- - `feature_finder_cli_bridge` (optional)
96
- - Bridges RDKit FeatFinderCLI-style functionality
97
-
98
- - `db_create` / `db_search` (optional)
99
- - Wraps `Projects/DbCLI/CreateDb.py` and `SearchDb.py` workflows
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 os
2
- import sys
3
  import importlib
4
  import inspect
5
- from typing import Any, Dict, Optional, List
 
 
 
6
 
7
- source_path = os.path.join(
8
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
9
- "source",
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.mode = "import"
25
- self._modules: Dict[str, Optional[Any]] = {}
26
- self._import_errors: Dict[str, str] = {}
 
 
 
 
 
 
 
 
27
  self._load_modules()
28
 
29
- # ==========================================================================
30
- # Core utilities
31
- # ==========================================================================
 
 
 
32
 
33
- def _result(
34
- self,
35
- status: str,
36
- message: str,
37
- data: Optional[Any] = None,
38
- error: Optional[str] = None,
39
- guidance: Optional[str] = None,
40
- ) -> Dict[str, Any]:
41
  return {
42
- "status": status,
43
  "mode": self.mode,
44
- "message": message,
45
- "data": data,
46
- "error": error,
47
- "guidance": guidance,
 
 
48
  }
49
 
50
- def _import_module(self, module_path: str) -> Optional[Any]:
51
- try:
52
- module = importlib.import_module(module_path)
53
- self._modules[module_path] = module
54
- return module
55
- except Exception as exc:
56
- self._modules[module_path] = None
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 _call_function(
101
- self, module_path: str, func_name: str, *args: Any, **kwargs: Any
102
- ) -> Dict[str, Any]:
103
- module = self._modules.get(module_path)
104
  if module is None:
105
- return self._result(
106
- status="error",
107
- message=f"Module '{module_path}' is not available.",
108
- error=self._import_errors.get(module_path, "Unknown import failure."),
109
- guidance=(
110
- "Verify repository source path injection and local build artifacts. "
111
- "For RDKit modules, ensure compiled extensions and numpy are available."
112
- ),
113
- )
114
- try:
115
- func = getattr(module, func_name, None)
116
- if func is None or not callable(func):
117
- available = [n for n, o in inspect.getmembers(module) if callable(o)]
118
- return self._result(
119
- status="error",
120
- message=f"Function '{func_name}' not found in '{module_path}'.",
121
- error="Missing callable in target module.",
122
- data={"available_callables_sample": available[:25]},
123
- guidance="Check function name spelling and module version compatibility.",
124
  )
125
- result = func(*args, **kwargs)
126
- return self._result(
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
- Parameters:
215
- *args: Positional arguments for CreateDb().
216
- **kwargs: Keyword arguments for CreateDb().
217
-
218
- Returns:
219
- dict: Unified status response.
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
- Returns:
344
- dict: Unified status response.
345
- """
346
- module_path = "rdkit.Chem.FeatFinderCLI"
347
- module = self._modules.get(module_path)
 
 
 
348
  if module is None:
349
- return self._result(
350
- status="error",
351
- message="FeatFinderCLI module is unavailable.",
352
- error=self._import_errors.get(module_path, "Unknown import failure."),
353
- guidance="Confirm RDKit Python modules and compiled backends are importable.",
354
- )
 
 
 
 
 
 
 
 
 
 
 
 
355
  try:
356
- if hasattr(module, "main") and callable(module.main):
357
- result = module.main(argv) if argv is not None else module.main()
358
- return self._result("ok", "FeatFinderCLI executed.", data=result)
359
- return self._result(
360
- status="error",
361
- message="No callable main() found in FeatFinderCLI.",
362
- guidance="Inspect module API and call exported functions directly.",
363
- )
 
364
  except Exception as exc:
365
- return self._result(
366
- status="error",
367
- message="FeatFinderCLI execution failed.",
368
- error=str(exc),
369
- guidance="Check CLI arguments and required feature definition inputs.",
370
- )
371
-
372
- def call_molvs_cli_module(self, argv: Optional[List[str]] = None) -> Dict[str, Any]:
373
- """
374
- Execute Contrib.MolVS.molvs_cli entry-style behavior if available.
375
-
376
- Parameters:
377
- argv: Optional argument list for CLI-like execution.
378
-
379
- Returns:
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 self._result(
386
- status="error",
387
- message="molvs_cli module is unavailable.",
388
- error=self._import_errors.get(module_path, "Unknown import failure."),
389
- guidance="Ensure Contrib modules exist in source path and dependencies are installed.",
390
- )
 
 
 
 
 
 
 
 
 
 
 
 
391
  try:
392
- if hasattr(module, "main") and callable(module.main):
393
- result = module.main(argv) if argv is not None else module.main()
394
- return self._result("ok", "molvs_cli executed.", data=result)
395
- return self._result(
396
- status="error",
397
- message="No callable main() found in molvs_cli.",
398
- guidance="Inspect module for alternate callable entry points.",
399
- )
 
 
400
  except Exception as exc:
401
- return self._result(
402
- status="error",
403
- message="molvs_cli execution failed.",
404
- error=str(exc),
405
- guidance="Validate CLI arguments and molecule input format.",
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
- app = main()
13
- app.run()
 
 
 
 
 
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 os
2
- import sys
3
 
4
- source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
5
- if source_path not in sys.path:
6
- sys.path.insert(0, source_path)
7
 
8
  from fastmcp import FastMCP
9
 
10
- from Regress.Scripts.new_timings import data
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
- mcp = FastMCP("unknown_service")
 
 
20
 
 
 
21
 
22
- @mcp.tool(name="data", description="Auto-wrapped function data")
23
- def data(payload: dict):
24
- try:
25
- if data is None:
26
- return {"success": False, "result": None, "error": "Function data is not available"}
27
- result = data(**payload)
28
- return {"success": True, "result": result, "error": None}
29
- except Exception as e:
30
- return {"success": False, "result": None, "error": str(e)}
31
-
32
- @mcp.tool(name="gif", description="Auto-wrapped function gif")
33
- def gif(payload: dict):
34
- try:
35
- if gif is None:
36
- return {"success": False, "result": None, "error": "Function gif is not available"}
37
- result = gif(**payload)
38
- return {"success": True, "result": result, "error": None}
39
- except Exception as e:
40
- return {"success": False, "result": None, "error": str(e)}
41
-
42
- @mcp.tool(name="svg", description="Auto-wrapped function svg")
43
- def svg(payload: dict):
44
- try:
45
- if svg is None:
46
- return {"success": False, "result": None, "error": "Function svg is not available"}
47
- result = svg(**payload)
48
- return {"success": True, "result": result, "error": None}
49
- except Exception as e:
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
- if initParser is None:
76
- return {"success": False, "result": None, "error": "Function initParser is not available"}
77
- result = initParser(**payload)
78
- return {"success": True, "result": result, "error": None}
79
- except Exception as e:
80
- return {"success": False, "result": None, "error": str(e)}
81
-
82
- @mcp.tool(name="GetMolsFromSDFile", description="Auto-wrapped function GetMolsFromSDFile")
83
- def GetMolsFromSDFile(payload: dict):
 
 
 
84
  try:
85
- if GetMolsFromSDFile is None:
86
- return {"success": False, "result": None, "error": "Function GetMolsFromSDFile is not available"}
87
- result = GetMolsFromSDFile(**payload)
88
- return {"success": True, "result": result, "error": None}
89
- except Exception as e:
90
- return {"success": False, "result": None, "error": str(e)}
91
-
92
- @mcp.tool(name="GetMolsFromSmilesFile", description="Auto-wrapped function GetMolsFromSmilesFile")
93
- def GetMolsFromSmilesFile(payload: dict):
 
 
 
 
 
 
 
94
  try:
95
- if GetMolsFromSmilesFile is None:
96
- return {"success": False, "result": None, "error": "Function GetMolsFromSmilesFile is not available"}
97
- result = GetMolsFromSmilesFile(**payload)
98
- return {"success": True, "result": result, "error": None}
99
- except Exception as e:
100
- return {"success": False, "result": None, "error": str(e)}
101
-
102
- @mcp.tool(name="GetNeighborLists", description="Auto-wrapped function GetNeighborLists")
103
- def GetNeighborLists(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  try:
105
- if GetNeighborLists is None:
106
- return {"success": False, "result": None, "error": "Function GetNeighborLists is not available"}
107
- result = GetNeighborLists(**payload)
108
- return {"success": True, "result": result, "error": None}
109
- except Exception as e:
110
- return {"success": False, "result": None, "error": str(e)}
111
-
112
- @mcp.tool(name="feq", description="Auto-wrapped function feq")
113
- def feq(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  try:
115
- if feq is None:
116
- return {"success": False, "result": None, "error": "Function feq is not available"}
117
- result = feq(**payload)
118
- return {"success": True, "result": result, "error": None}
119
- except Exception as e:
120
- return {"success": False, "result": None, "error": str(e)}
121
-
122
- @mcp.tool(name="feq", description="Auto-wrapped function feq")
123
- def feq(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  try:
125
- if feq is None:
126
- return {"success": False, "result": None, "error": "Function feq is not available"}
127
- result = feq(**payload)
128
- return {"success": True, "result": result, "error": None}
129
- except Exception as e:
130
- return {"success": False, "result": None, "error": str(e)}
131
-
132
- @mcp.tool(name="feq", description="Auto-wrapped function feq")
133
- def feq(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  try:
135
- if feq is None:
136
- return {"success": False, "result": None, "error": "Function feq is not available"}
137
- result = feq(**payload)
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
- def create_app():
145
- """Create and return FastMCP application instance"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  return mcp
147
 
 
148
  if __name__ == "__main__":
149
- mcp.run(transport="http", host="0.0.0.0", port=8000)
 
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
- fastapi
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
- project_root = os.path.dirname(os.path.abspath(__file__))
9
- mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
10
- if mcp_plugin_dir not in sys.path:
11
- sys.path.insert(0, mcp_plugin_dir)
12
 
13
  from mcp_service import create_app
14
 
15
- def main():
16
- """Start FastMCP service"""
 
 
 
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
- app.run(transport="http", host="0.0.0.0", port=port)
 
 
 
25
  else:
26
- # Default to STDIO mode
27
- app.run()
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
- $entryName = if ($env:MCP_ENTRY_NAME) { $env:MCP_ENTRY_NAME } else { "rdkit" }
4
- $entryUrl = if ($env:MCP_ENTRY_URL) { $env:MCP_ENTRY_URL } else { "http://localhost:7914/mcp" }
5
- $imageName = if ($env:MCP_IMAGE_NAME) { $env:MCP_IMAGE_NAME } else { "rdkit-mcp" }
6
- $mcpDir = Join-Path $env:USERPROFILE ".cursor"
7
- $mcpPath = Join-Path $mcpDir "mcp.json"
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 7914:7860 $imageName
 
 
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
- cd "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
4
- mcp_entry_name="${MCP_ENTRY_NAME:-rdkit}"
5
- mcp_entry_url="${MCP_ENTRY_URL:-http://localhost:7914/mcp}"
6
- mcp_dir="${HOME}/.cursor"
7
- mcp_path="${mcp_dir}/mcp.json"
8
- mkdir -p "${mcp_dir}"
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}"