Chemistry tools for AI assistants¶
SMSD Pro 8.1.0 offers eight molecular tools that an AI assistant can call:
maximum common substructure (MCS), substructure and SMARTS matching,
fingerprints, similarity, small-list screening, standardisation and MCS
depiction. The tools run on your own machine. They are served to an assistant
by a local Model Context Protocol (MCP) server, smsd-mcp, and the same tools
are available from Python as a registry with JSON schemas, so that your own
code can offer them to a model or call them with no model at all. This chapter
shows which route suits which task, what each tool takes and returns, how to
start the server and connect a client, how to read a tool result, and the
limits that keep each call bounded.
Choose a route¶
| Task | Use | Needs | Model involved |
|---|---|---|---|
| Give a desktop or terminal assistant the tools | smsd-mcp, started by the assistant's MCP client |
The mcp extra |
Yours, through the client |
| Offer the tools to a model API from your own program | get_tool_definitions(provider) and call_tool(name, arguments) |
The core package | Yours, through your code |
| Run the tools from a script, a test or a workflow | call_tool(name, arguments) |
The core package | None |
| Search a library, enumerate every match or set detailed chemistry | The Python API: Substructure search, MCS, Screening | The core package | None |
The tools are deliberately narrow: each call takes SMILES or SMARTS text, runs one bounded operation and returns a JSON-compatible dictionary. They suit questions about a few molecules at a time. For a library of thousands of molecules, a saved index or a GPU, use the Python API directly; the tools expose a subset of its options and cap every input.
The tools¶
| Tool | Required inputs | Main optional inputs | Returns |
|---|---|---|---|
find_mcs |
query_smiles, target_smiles |
objective (mcis or mces), connected, timeout_ms, node_limit, chemistry switches |
One common graph as matches, with status, optimal, complete, score and the budget used |
find_substructure |
query, target_smiles |
query_format (smarts or smiles), induced, timeout_ms, node_limit, chemistry switches |
One embedding of the query in the target, with the same proof fields |
search_smarts |
query_smarts, target_smiles |
max_matches (1 to 32), timeout_ms, node_limit, chemistry switches |
Every embedding up to max_matches, and truncated when the list stopped at the cap |
molecular_fingerprint |
smiles |
kind, fp_size, radius, path_length, counts |
values: set-bit positions, or position and count pairs |
molecular_similarity |
query_smiles, target_smiles |
metric (tanimoto, dice or cosine) and the fingerprint inputs |
score, the coefficient of the two fingerprints |
screen_molecules |
query_smiles, targets_smiles (a list) |
threshold, max_hits, offset and the fingerprint inputs |
One page of hits, each a target_index and Tanimoto score, with total_hits and next_offset |
standardise_molecule |
smiles |
remove_atom_maps; cleanup, largest_fragment, neutralise and canonical_tautomer, which need RDKit |
Canonical smiles, the backend used and the operations applied |
depict_mcs |
query_smiles, target_smiles |
The find_mcs inputs, width, height |
The find_mcs result plus svg, a drawing of the pair with the common atoms highlighted |
The chemistry switches are match_stereo, complete_rings_only and
ring_matches_ring_only, all off by default. Every other chemistry option
keeps its default, described in
Getting started, and each search
result lists the options in force under chemistry. The fingerprint kind is
one of ecfp, fcfp, circular, path, torsion or mcs; the
Options section gives every default.
Start the server¶
Install SMSD Pro with the mcp extra, which adds the MCP Python SDK. The
server and the tool definitions need SMSD 8.1.0 or later and Python 3.12 or
later:
smsd-mcp takes no arguments. It talks to its client over standard input and
output, writes diagnostics to standard error and opens no network port, so it
is normally started by the client rather than from a shell; run by hand, it
waits silently for protocol messages. Without the mcp extra it stops with
ImportError: The MCP server requires the official SDK; install smsd[mcp].
To check an installation without an assistant, run
mcp_stdio_client.py from the
examples. It starts the server with the same interpreter, lists the eight
tools and runs one find_mcs call through the protocol.
Connect an MCP client¶
An MCP client needs two things: the command that starts the server and its arguments, which here are none. Many clients take a JSON entry of this form:
Give the absolute path that which smsd-mcp (on Windows,
where.exe smsd-mcp) prints, because a desktop client may not search your
shell's PATH; the path also fixes which Python environment, and so which SMSD
version, the server uses. The server needs no API key and no environment
settings. Once connected, the client lists the eight tools with their
schemas, and the server also sends the client a short note on reading
results: inspect status, optimal and complete, and do not read an
unfinished search as a maximum or as proof of absence.
The configuration file, its location and the command that adds a server
differ from client to client.
Connecting AI assistants gives
the exact steps for several desktop and terminal assistants and for an open
model run on your own machine. Every result carries engine_version; use it
to confirm the SMSD version, because some clients show the SDK's version for
the server instead.
Call the tools from Python¶
smsd.tool_definitions.call_tool(name, arguments) runs a tool locally, with
the arguments as a dictionary of JSON values, and returns the same dictionary
that the server sends to a client. It needs neither the mcp extra nor a
model. The program below runs an MCS, two single substructure searches and a
SMARTS enumeration:
from smsd.tool_definitions import call_tool
# MCS of N-methylacetamide and propanamide: atom-maximising, connected.
mcs = call_tool("find_mcs", {"query_smiles": "CC(=O)NC",
"target_smiles": "CCC(=O)N"})
print("find_mcs:", mcs["found"], mcs["status"], mcs["optimal"], mcs["complete"])
print(" atoms:", mcs["matches"][0]["atom_count"],
"mapping:", mcs["matches"][0]["atom_mapping"])
# One embedding of a SMARTS pattern: a hydroxyl on an aromatic carbon.
hit = call_tool("find_substructure", {"query": "[OX2H]c",
"target_smiles": "CC(C)(C)c1ccc(O)cc1"})
print("find_substructure:", hit["found"], hit["status"],
hit["matches"][0]["atom_mapping"])
# The same call with a molecule as the query.
phenol = call_tool("find_substructure", {"query": "Oc1ccccc1",
"target_smiles": "CC(C)(C)c1ccc(O)cc1",
"query_format": "smiles"})
print("molecule query:", phenol["found"], len(phenol["matches"][0]["atom_mapping"]))
# Every embedding, up to max_matches: the three hydroxyls of glycerol.
every = call_tool("search_smarts", {"query_smarts": "[OX2H]",
"target_smiles": "OCC(O)CO"})
print("search_smarts:", [m["atom_mapping"] for m in every["matches"]],
"complete:", every["complete"], "truncated:", every["truncated"])
assert mcs["optimal"] and mcs["matches"][0]["atom_mapping"] == [[0, 1], [1, 2], [2, 3], [3, 4]]
assert hit["matches"][0]["atom_mapping"] == [[0, 8], [1, 7]]
assert phenol["found"] and len(phenol["matches"][0]["atom_mapping"]) == 7
assert len(every["matches"]) == 3 and every["complete"] and not every["truncated"]
The program prints:
find_mcs: True COMPLETED True True
atoms: 4 mapping: [[0, 1], [1, 2], [2, 3], [3, 4]]
find_substructure: True COMPLETED [[0, 8], [1, 7]]
molecule query: True 7
search_smarts: [[[0, 0]], [[0, 3]], [[0, 5]]] complete: True truncated: False
Mappings are lists of [query_atom, target_atom] pairs, sorted by query atom,
with zero-based atom numbers in the order of the input SMILES, as described in
Atom numbering. find_substructure
expects SMARTS unless query_format is "smiles". Each entry of matches
also holds bond_mapping, fragments, atom_count and bond_count.
The tool functions can also be imported from smsd.mcp_server and called with
keyword arguments; importing that module neither loads the SDK nor starts a
server. Use call_tool for arguments that come from a model or a file: it
checks the whole argument object against the schema first, as the server does.
Read a tool result¶
A search result keeps the native fields of a SearchResult: found,
status, optimal, complete, score, nodes_visited and elapsed_ms,
with the same meanings as in
Reading a result. It adds:
engine_version, the SMSD version that ran the call;proof, which repeatsoptimalandcompletewith the scopenative_search; it describes the search, and is not an independent chemical validation;budget, thetimeout_msandnode_limitin force, andchemistry, the chemistry options;limits, the fixed input and output limits of the server;timings, withwall_elapsed_msfrom tool entry to just before the result is serialised, andnative_search_ms.
The rule for an assistant is the rule for any search: a returned match is
always a valid match, but found: false proves absence only when status is
COMPLETED and optimal is true, and an MCS is proved maximum only when
optimal is true. The program below shows a search stopped by its time limit,
an enumeration stopped by its result cap, and the messages with which
call_tool refuses bad arguments:
from smsd.tool_definitions import call_tool
anthracene = "c1ccc2cc3ccccc3cc2c1"
# A time limit of 0 stops the search at once: found=False proves nothing.
stopped = call_tool("find_substructure", {"query": "c1ccccc1",
"target_smiles": anthracene,
"timeout_ms": 0})
print("stopped:", stopped["found"], stopped["status"],
stopped["optimal"], stopped["complete"])
# A result cap: the matches returned are real, but the list is not the whole.
capped = call_tool("search_smarts", {"query_smarts": "[OX2H]",
"target_smiles": "OCC(O)CO",
"max_matches": 2})
print("capped:", len(capped["matches"]), capped["status"],
"complete:", capped["complete"], "truncated:", capped["truncated"])
def proves_absence(result):
return (not result["found"] and result["status"] == "COMPLETED"
and result["optimal"])
absent = call_tool("find_substructure", {"query": "[N]", "target_smiles": anthracene})
print("absence proved:", proves_absence(absent), "| after a stop:",
proves_absence(stopped))
# Bad arguments are refused before any molecular work, with a short message.
for arguments in ({"query_smiles": "CCO"},
{"query_smiles": "CCO", "target_smiles": "CCN", "timeout_ms": 60000},
{"query_smiles": "CCO", "target_smiles": "CCN", "timeout_ms": "500"},
{"query_smiles": "CCO", "target_smiles": "CCN", "path": "a.sdf"},
{"query_smiles": "C1CC", "target_smiles": "CCN"}):
try:
call_tool("find_mcs", arguments)
except ValueError as error:
print("refused:", error)
assert stopped["status"] == "TIMED_OUT" and not stopped["complete"]
assert capped["status"] == "LIMIT_REACHED" and capped["truncated"]
assert proves_absence(absent) and not proves_absence(stopped)
The program prints:
stopped: False TIMED_OUT False False
capped: 2 LIMIT_REACHED complete: False truncated: True
absence proved: True | after a stop: False
refused: Missing required tool argument
refused: timeout_ms is outside its supported range
refused: timeout_ms must have JSON type integer
refused: Unknown tool argument
refused: Unclosed ring(s): 1
search_smarts reports a capped list as LIMIT_REACHED with
complete: false and truncated: true; the matches it does return are valid,
and optimal stays true because a match was found. Arguments are refused
before any molecular work: unknown tools, unknown or missing arguments, JSON
type changes such as a string for an integer, and values out of range each
raise ValueError with a short message. A SMILES or SMARTS parse error also
raises ValueError. Return that message to the model as the tool's answer, so
that it can correct the call.
Fingerprint, similarity, screening and standardisation results are not
searches. They report status: "COMPLETED", except a canonical tautomer
chosen from a stopped enumeration, described below, and a proof whose scope
names the operation and whose search_proof_applicable is false. A similarity
score says how alike two fingerprints are; it is not an MCS size or a measure
of biological activity.
Fingerprints, similarity and screening¶
molecular_fingerprint returns the fingerprint as positions, which keeps the
result small and readable. molecular_similarity compares two molecules with
one coefficient, and screen_molecules scores a short list against one query
and returns the hits at or above threshold one page at a time:
from smsd.tool_definitions import call_tool
# ECFP4 set bits in a 64-position fingerprint, then position/count pairs.
bits = call_tool("molecular_fingerprint", {"smiles": "OCCO", "fp_size": 64})
counts = call_tool("molecular_fingerprint", {"smiles": "OCCO", "fp_size": 64,
"counts": True})
print("bits:", bits["values"])
print("counts:", counts["values"])
print("proof:", bits["proof"])
# Phenol against 2-methylphenol with each coefficient.
pair = {"query_smiles": "Oc1ccccc1", "target_smiles": "Cc1ccccc1O"}
for metric in ("tanimoto", "dice", "cosine"):
score = call_tool("molecular_similarity", {**pair, "metric": metric})["score"]
print(f"{metric}: {score:.3f}")
# A small list, two hits per page; next_offset leads to the next page.
library = ["Oc1ccccc1", "Cc1ccccc1O", "Nc1ccccc1", "CCO", "Oc1ccccc1", "c1ccccc1"]
request = {"query_smiles": "Oc1ccccc1", "targets_smiles": library,
"threshold": 0.3, "max_hits": 2}
offset, pages = 0, []
while offset is not None:
page = call_tool("screen_molecules", {**request, "offset": offset})
pages.append([(hit["target_index"], round(hit["score"], 3)) for hit in page["hits"]])
offset = page["next_offset"]
print("total hits:", page["total_hits"], "of", page["total_targets"])
for number, hits in enumerate(pages, 1):
print(f"page {number}:", hits)
assert bits["values"] == [4, 5, 6, 27, 40, 62]
assert counts["values"] == [[4, 2], [5, 2], [6, 2], [27, 2], [40, 2], [62, 2]]
assert bits["proof"]["search_proof_applicable"] is False
assert pages == [[(0, 1.0), (4, 1.0)], [(1, 0.346)]]
The program prints:
bits: [4, 5, 6, 27, 40, 62]
counts: [[4, 2], [5, 2], [6, 2], [27, 2], [40, 2], [62, 2]]
proof: {'scope': 'fingerprint_generation', 'optimal': None, 'search_proof_applicable': False}
tanimoto: 0.346
dice: 0.514
cosine: 0.525
total hits: 3 of 6
page 1: [(0, 1.0), (4, 1.0)]
page 2: [(1, 0.346)]
screen_molecules scores every target, keeps those whose Tanimoto score is at
least threshold, sorts them by descending score and then by input index, and
returns at most max_hits from offset. next_offset is the offset of the
next page, or None (null in JSON) on the last page, and truncated is
true when the page does not hold the whole hit list. The screen always uses
Tanimoto; metric belongs to molecular_similarity only. It runs on the CPU
and is not an exact structure search. For a larger library, use the screens
in Screening a library, which are not capped in this way.
Standardisation and depiction¶
standardise_molecule by default parses the molecule and writes canonical
SMILES. It changes the representation only: charges, fragments, isotopes,
stereochemistry and atom maps are kept, and remove_atom_maps is a separate
switch. depict_mcs runs the same search as find_mcs and adds an SVG
drawing of the pair:
from smsd.tool_definitions import call_tool
# The default changes the representation only: charges and fragments stay.
salt = call_tool("standardise_molecule", {"smiles": "[Na+].[O-]C(=O)c1ccccc1"})
print("smiles:", salt["smiles"], "| backend:", salt["backend"])
print("operations:", salt["operations"],
"| chemical transformations:", salt["chemical_transformations"])
mapped = call_tool("standardise_molecule", {"smiles": "[CH3:1][OH:2]",
"remove_atom_maps": True})
print("atom maps removed:", mapped["smiles"], mapped["operations"])
# The MCS of phenol and aniline, with the search result and a drawing.
drawing = call_tool("depict_mcs", {"query_smiles": "Oc1ccccc1",
"target_smiles": "Nc1ccccc1",
"width": 600, "height": 300})
print("depict_mcs:", drawing["status"], drawing["optimal"],
drawing["matches"][0]["atom_count"], "atoms")
print("svg:", "<svg" in drawing["svg"], drawing["mime_type"],
drawing["width"], "x", drawing["height"])
# No common graph, no drawing.
none = call_tool("depict_mcs", {"query_smiles": "O", "target_smiles": "N"})
print("no match:", none["found"], none["svg"])
assert not salt["chemical_transformations"] and "." in salt["smiles"]
assert mapped["operations"] == ["remove_atom_maps", "canonical_smiles"]
assert drawing["optimal"] and drawing["matches"][0]["atom_count"] == 6
assert none["svg"] is None
The program prints:
smiles: C(c1ccccc1)([O-])=O.[Na+] | backend: smsd
operations: ['canonical_smiles'] | chemical transformations: False
atom maps removed: CO ['remove_atom_maps', 'canonical_smiles']
depict_mcs: COMPLETED True 6 atoms
svg: True image/svg+xml 600 x 300
no match: False None
cleanup, largest_fragment, neutralise and canonical_tautomer change
the chemical graph, so each must be asked for explicitly, and they need the
rdkit extra; without it the call raises ImportError, and there is no
silent fallback. With any of them set, backend is rdkit, operations
lists each step in order and chemical_transformations is true. A canonical
tautomer is a choice of representation, not a prediction of the most stable
form. Tautomer enumeration stops at 128 tautomers or 256 transformations; when
it stops, the result reports LIMIT_REACHED and complete: false, and the
choice is made from the tautomers found.
The drawing highlights the mapped atoms and the input bonds between them. For
an MCES search, the selected common bonds are those in bond_mapping; the
drawing can also highlight other bonds between mapped atoms. A search with no
match returns svg: None, and a stopped search keeps its stopped status when
drawn. The SVG is checked before it is returned and contains no scripts,
external references or document declarations.
Tool definitions for a model API¶
get_tool_definitions(provider) returns the eight tools as a list of
dictionaries in one of four shapes. Pass the list to a model API as its tools,
then run each tool call the model makes with call_tool and send the result
back. No client is created and nothing is sent anywhere:
import json
from smsd.tool_definitions import TOOL_REGISTRY, get_tool_definitions
for provider in ("mcp", "input-schema", "function", "chat-function"):
first = get_tool_definitions(provider)[0]
print(f"{provider}:", sorted(first))
schema = TOOL_REGISTRY["find_mcs"].input_schema
print("required:", schema["required"])
print("timeout_ms:", json.dumps(schema["properties"]["timeout_ms"]))
print("objective:", json.dumps(schema["properties"]["objective"]))
print("additionalProperties:", schema["additionalProperties"])
print("tools:", len(TOOL_REGISTRY), "| first:", next(iter(TOOL_REGISTRY)))
assert len(get_tool_definitions()) == 8
assert get_tool_definitions("chat-function")[0]["function"]["strict"] is False
The program prints:
mcp: ['description', 'inputSchema', 'name']
input-schema: ['description', 'input_schema', 'name']
function: ['description', 'name', 'parameters', 'strict', 'type']
chat-function: ['function', 'type']
required: ['query_smiles', 'target_smiles']
timeout_ms: {"type": "integer", "minimum": 0, "maximum": 30000, "default": 1000}
objective: {"type": "string", "enum": ["mcis", "mces"], "default": "mcis"}
additionalProperties: False
tools: 8 | first: find_mcs
provider |
Shape | Typical use |
|---|---|---|
mcp (default) |
name, description, inputSchema |
MCP tool lists |
input-schema |
name, description, input_schema |
APIs that take an input schema per tool |
function |
flat: type: "function", name, description, parameters, strict |
APIs that take flat function definitions |
chat-function |
nested: type: "function", function: {name, description, parameters, strict} |
Chat-completion APIs and most local model runtimes |
Every schema sets additionalProperties: false, marks the required
arguments, gives each optional argument its default, and bounds every number,
text length and list length. The function shapes set strict: false so that
a model may leave out optional arguments and the defaults apply. A client that
drops schema keywords cannot widen the limits, because call_tool and the
server check every argument themselves. The schemas hold single-argument
bounds only; combined limits, such as the atoms of a whole screen, are checked
when the tool runs.
Which shape each vendor's API expects, the changes some need, and complete
loops for several hosted and local models are in
Use the tool definitions from Python.
Optional LangChain and LlamaIndex adapters, langchain_tools() and
llamaindex_tools(), wrap the same callables with the same bounds; see
Function definitions and framework adapters.
A tool loop with fixed replies¶
A tool loop sends the conversation and the tool definitions to a model, runs
each tool call the model asks for, appends the results and asks again, until
the model answers without a tool call. The loop below has that structure, but
a small class returns prepared replies in place of a model, so the program
runs offline and gives the same output every time. This is also a convenient
way to test your own loop before connecting a model. The replies use the
chat-function shape: the first asks for one valid call and one with an
unknown argument, the second corrects the call after reading the refusal, and
the third answers:
import json
from smsd.tool_definitions import call_tool, get_tool_definitions
class FixedReplies:
"""Stands in for a model: returns the next prepared reply on each request."""
def __init__(self, replies):
self.replies, self.requests = list(replies), []
def create(self, messages, tools):
self.requests.append({"messages": list(messages), "tools": tools})
return self.replies.pop(0)
def tool_call(call_id, name, arguments):
return {"id": call_id, "type": "function",
"function": {"name": name, "arguments": json.dumps(arguments)}}
def run_tool(name, arguments):
"""Run one tool locally; a refusal goes back to the model as text."""
try:
return json.dumps(call_tool(name, json.loads(arguments)))
except (ValueError, ImportError) as error:
return json.dumps({"error": str(error)})
pair = {"query_smiles": "c1ccccc1O", "target_smiles": "c1ccccc1N"}
model = FixedReplies([
# Turn 1: one valid call, and one with an argument the tool does not take.
{"role": "assistant", "content": None, "tool_calls": [
tool_call("call_1", "find_mcs", pair),
tool_call("call_2", "find_mcs", {**pair, "timeout": 500})]},
# Turn 2: the corrected call, after reading the refusal.
{"role": "assistant", "content": None, "tool_calls": [
tool_call("call_3", "find_mcs", {**pair, "timeout_ms": 500})]},
# Turn 3: the answer, which reports the proof flags.
{"role": "assistant", "content": "The MCS has 6 atoms; COMPLETED, "
"optimal and complete, so it is proved maximum.", "tool_calls": None},
])
tools = get_tool_definitions("chat-function")
messages = [{"role": "user", "content": "Find the MCS of phenol and aniline."}]
while True:
reply = model.create(messages=messages, tools=tools)
messages.append(reply)
if not reply["tool_calls"]:
break
for call in reply["tool_calls"]:
text = run_tool(call["function"]["name"], call["function"]["arguments"])
messages.append({"role": "tool", "tool_call_id": call["id"], "content": text})
result = json.loads(text)
print(call["id"], "->", result.get("error") or
f"status={result['status']} optimal={result['optimal']} "
f"atoms={result['matches'][0]['atom_count']}")
print("model requests:", len(model.requests))
print("answer:", messages[-1]["content"])
results = [json.loads(m["content"]) for m in messages if m["role"] == "tool"]
assert results[0]["optimal"] and results[0]["matches"][0]["atom_count"] == 6
assert results[1] == {"error": "Unknown tool argument"}
assert results[2]["budget"]["timeout_ms"] == 500
assert model.requests[0]["tools"][0]["function"]["name"] == "find_mcs"
The program prints:
call_1 -> status=COMPLETED optimal=True atoms=6
call_2 -> Unknown tool argument
call_3 -> status=COMPLETED optimal=True atoms=6
model requests: 3
answer: The MCS has 6 atoms; COMPLETED, optimal and complete, so it is proved maximum.
Two points carry over to a loop with a real model. Send every result back in
full, including status, optimal and complete, so that the model can say
whether its answer is proved; and catch ValueError and ImportError from
call_tool and return the message as the tool result, rather than ending the
loop, so that the model can correct its call. To replace the fixed replies
with a model, replace model.create with the client call of your model API;
Connecting AI assistants
shows that change for each API. For a fixed sequence of calls with no model at
all, see agent_workflow.py, which
screens a list, runs an MCS on each hit and draws the best one.
Limits and safety¶
The limits are fixed in the server and apply equally to call_tool. Each
result reports them under limits.
| Limit | Value | Applies to |
|---|---|---|
| Text argument | 16,384 characters | Every SMILES or SMARTS argument |
| Atoms per molecule | 1,024 | Searches and fingerprints |
| Atoms per molecule, depiction | 256 | depict_mcs |
| Atoms per molecule, standardisation | 128 | standardise_molecule |
Search time, timeout_ms |
Default 1,000; at most 30,000; 0 stops at once | find_mcs, find_substructure, search_smarts, depict_mcs |
Search nodes, node_limit |
Default 100,000; from 1 to 1,000,000 | The same four tools |
| Matches or hits returned | At most 32 | search_smarts, a screen_molecules page |
| Screen targets | At most 64 | screen_molecules |
| Combined text | 65,536 characters | The query and targets of screen_molecules |
| Combined atoms | 4,096 | molecular_similarity, screen_molecules |
| Fingerprint | fp_size up to 65,536; radius up to 8; path_length up to 8; at most 2,000,000 estimated path walks |
The fingerprint tools |
| Drawing | 200 to 1,600 by 150 to 1,200 pixels; SVG at most 1 MiB | depict_mcs |
| Result | 2 MiB as JSON text | Every tool |
Points to keep in mind when deploying the tools:
- Local only. The tools read no files, fetch no URLs and contact no network or model service. Molecular arguments are SMILES or SMARTS text, never paths, URLs, expressions or commands. The server has no network listener.
- Hosted assistants. A hosted assistant sends your prompts, the tool arguments and the results, including any structures, to its provider. Use a model run on your own machine when structures must not leave it.
- Time budgets cover the search, not the whole call. They start after the input is parsed and the SMARTS compiled; preparation, depiction and serialisation come on top, and standardisation has no time budget. For a hard deadline, apply process or container limits. The container recipe runs the server with no network access, a read-only file system and memory and CPU caps.
- No zero node limit. A
node_limitof 0 is refused rather than read as unlimited. - Natural-language requests are not mapped to options. Only the switches in the schema exist; an assistant cannot ask for an option the tools lack.
- Approval prompts. Keep your client's tool approval prompts on unless you trust every tool it can call.
Options¶
Search tools¶
find_mcs, depict_mcs, find_substructure and search_smarts share these
arguments where they apply:
| Argument | Type | Default | Meaning |
|---|---|---|---|
objective |
string | "mcis" |
mcis maximises common atoms; mces maximises common bonds (find_mcs, depict_mcs) |
connected |
boolean | true |
A single connected common graph, or fragments when false (find_mcs, depict_mcs) |
query_format |
string | "smarts" |
How query is read: smarts or smiles (find_substructure) |
induced |
boolean | false |
Induced matching: unbonded query atoms must map to unbonded target atoms (find_substructure) |
max_matches |
integer | 16 |
Embeddings to return, 1 to 32 (search_smarts) |
timeout_ms |
integer | 1000 |
Search time limit, 0 to 30,000 ms |
node_limit |
integer | 100000 |
Search node limit, 1 to 1,000,000 |
match_stereo |
boolean | false |
Match tetrahedral and double-bond stereochemistry |
complete_rings_only |
boolean | false |
Matched ring atoms and bonds lie in fully matched rings |
ring_matches_ring_only |
boolean | false |
Ring atoms and bonds match only ring atoms and bonds |
width, height |
integer | 800, 360 |
Drawing size, 200 to 1,600 and 150 to 1,200 (depict_mcs) |
Fingerprint tools¶
molecular_fingerprint, molecular_similarity and screen_molecules share
the fingerprint arguments:
| Argument | Type | Default | Meaning |
|---|---|---|---|
kind |
string | "ecfp" |
ecfp (also circular), fcfp, path, torsion or mcs |
fp_size |
integer | 2048 |
Fingerprint width, 1 to 65,536 |
radius |
integer | 2 |
Radius of ecfp, circular and fcfp, 0 to 8 |
path_length |
integer | 7 |
Longest path, in bonds, of path and mcs, 1 to 8 |
counts |
boolean | false |
Position and count pairs instead of set bits; not available for mcs |
metric |
string | "tanimoto" |
tanimoto, dice or cosine (molecular_similarity) |
threshold |
number | 0.7 |
Lowest Tanimoto score kept, 0 to 1, inclusive (screen_molecules) |
max_hits |
integer | 32 |
Hits per page, 1 to 32 (screen_molecules) |
offset |
integer | 0 |
First hit of the page, 0 to 64 (screen_molecules) |
Fingerprints and similarity describes the fingerprint families and coefficients.
standardise_molecule¶
| Argument | Type | Default | Meaning |
|---|---|---|---|
remove_atom_maps |
boolean | false |
Remove atom-map numbers before writing |
cleanup |
boolean | false |
RDKit clean-up of the structure |
largest_fragment |
boolean | false |
Keep the largest fragment |
neutralise |
boolean | false |
Neutralise charges where possible |
canonical_tautomer |
boolean | false |
Write the canonical tautomer |
The last four need the rdkit extra.
Notes¶
- Each call is independent. The tools keep no state between calls, so a query is prepared again on every call; for many searches with one query, use the Python API.
screen_moleculesfingerprints the query and every target on each call, and so doesmolecular_similarityfor its pair.elapsed_msand thetimingsvary from run to run; do not compare them across machines or ask an assistant to.find_mcsreturns one common graph. For every optimal mapping, constraints or the common graph of a series, use the Python API in Maximum common substructure.- A capped enumeration or a capped screen page is incomplete, not wrong; ask for the next page or a higher cap, up to the limits above.
- An empty or whitespace-only argument is refused, and so is a
thresholdthat is not a finite number from 0 to 1.
See also¶
- Molecular tools and MCP: the reference for the tools, their results and limits.
- Connecting AI assistants: set-up for specific assistants, model APIs and local model runtimes, with complete tool loops.
- Command line and containers: the server among the other command-line tools.
- API reference, molecular tools: the signature of every tool function.
- Getting started: what a search result proves.
- The MCP stdio client and agent workflow examples.