| name | coot-best-practices |
| description | Best Practices for using Coot MCP |
Coot Python API Best Practices
Overview
This skill provides best practices for interacting with Coot's Python API through the MCP server. Following these guidelines ensures optimal performance, correct usage, and reliable results.
Python Execution in Coot MCP
CRITICAL: Understanding run_python() vs run_python_multiline()
Coot MCP provides two tools for executing Python code with different capabilities and limitations:
run_python() - Simple Expressions Only
Use for single expressions that return a value. Cannot handle imports or semicolons.
coot.run_python("1 + 1")
coot.run_python("coot.molecule_name(0)")
coot.run_python("coot.is_valid_model_molecule(0)")
coot.run_python("import os")
coot.run_python("import os; os.getcwd()")
coot.run_python("x = 5; x * 2")
coot.run_python("import coot_utils; coot_utils.chain_ids(0)")
Rule: Use run_python() ONLY for:
- Single expressions with no semicolons
- No import statements
- Direct function calls that return a value
run_python_multiline() - Everything Else
Use for any code requiring imports, multiple statements, or function definitions:
coot.run_python_multiline("""
import os
result = os.getcwd()
print(result)
""")
coot.run_python_multiline("""
x = 5
y = x * 2
print(y)
""")
coot.run_python_multiline("""
import coot_utils
chains = coot_utils.chain_ids(0)
for chain in chains:
print(f"Chain: {chain}")
""")
coot.run_python_multiline("""
def validate_residue(imol, chain, resno):
info = coot.residue_info_py(imol, chain, resno, "")
return len(info)
result = validate_residue(0, "A", 42)
print(f"Atoms: {result}")
""")
Rule: Use run_python_multiline() for:
- Any import statements
- Multiple statements (even without imports)
- Function definitions
- Loops and control flow
- Anything with complexity
Key Differences Table
| Feature | run_python() | run_python_multiline() |
|---|
| Import statements | ❌ Fails | ✓ Works |
| Semicolons | ❌ Fails | ✓ Works |
| Multiple lines | ❌ Fails | ✓ Works |
| Function defs | ❌ Fails | ✓ Works |
| Simple expressions | ✓ Works | ✓ Works |
| Return value | Returns value | Returns None (use print) |
Common Mistake Pattern
result = coot.run_python("import coot_utils; coot_utils.chain_ids(0)")
coot.run_python_multiline("""
import coot_utils
chains = coot_utils.chain_ids(0)
print(chains)
""")
When in Doubt
Use run_python_multiline() - it handles everything run_python() can do, plus more. The only downside is that it returns None rather than a value, so use print() to see output.
File Writing from Coot Python
CRITICAL: When writing files from Coot's Python, ALWAYS write to the current directory without specifying a path.
The Problem
Coot's Python interpreter runs in a specific working directory (/Users/pemsley/Projects/coot/git/coot-main/build/src), which is different from both:
- The bash working directory (
/home/claude)
- Mounted shared directories (
/mnt/user-data/...)
Attempting to write to an explicit path that doesn't exist in Coot's context will cause FileNotFoundError.
✅ CORRECT: Write to Current Directory
with open('output.txt', 'w') as f:
f.write(content)
with open('validation.xml', 'w') as f:
f.write(xml_content)
with open('data.txt', 'r') as f:
content = f.read()
❌ INCORRECT: Don't Specify Paths
with open('/home/claude/output.txt', 'w') as f:
f.write(content)
with open('/mnt/user-data/outputs/file.txt', 'w') as f:
f.write(content)
import os
output_file = os.path.join('/home/claude', 'output.txt')
with open(output_file, 'w') as f:
f.write(content)
Pattern for Working with Files
url = "https://example.com/data.xml"
content = coot.coot_get_url_as_string_py(url)
with open('data.xml', 'w') as f:
f.write(content)
import xml.etree.ElementTree as ET
tree = ET.parse('data.xml')
root = tree.getroot()
with open('results.txt', 'w') as f:
f.write(analysis_results)
Why This Works
Coot's Python interpreter automatically uses its working directory. By omitting paths:
- Files are created in a location that exists
- No permission issues
- No cross-filesystem problems
- Simple and reliable
User Talk
When the user says "here" in an ambiguous way, they typically mean "applying the relevant function to this
residue (or atom or chain)" - i.e. the residue (or atom or chain) at the centre of the screen.
Getting Files to the User [warning: this needs review!]
After creating files in Coot's working directory, use bash tools to copy them to /mnt/user-data/outputs where the user can access them, or share results via print output.
Startup Procedure
CRITICAL: On every "Coot Mode" start, before doing anything else:
- Read the
/mnt/skills/user/coot-essential-api/SKILL.md file
- Extract all function names mentioned in that file
- Call
get_function_descriptions() with the complete list of function names
- This loads the essential API documentation into context, providing immediate access to the ~25 core functions needed for typical validation and model-building workflows
Why get_function_descriptions and not search_coot_functions?
search_coot_functions can return sparse or incomplete docs for some functions.
get_function_descriptions retrieves the full docstring including return value structure.
Always prefer get_function_descriptions for known function names — use search_coot_functions
only when you don't know the function name yet.
Example:
Coot:get_function_descriptions([
"set_refinement_immediate_replacement",
"set_imol_refinement_map",
"is_valid_model_molecule",
"is_valid_map_molecule",
"get_hydrogen_bonds_py",
])
Only after completing this startup should you proceed with the user's task.
Critical Rule: Prefer C++ Functions Over Python Wrappers
ALWAYS use coot.*_py() functions directly instead of coot_utils.* equivalents when they are simple passthroughs.
Why?
- Performance: C++ functions are significantly faster (no Python overhead)
- Import requirements:
coot is auto-imported, coot_utils requires explicit import
- Simplicity: Direct access to the core API without unnecessary abstraction layers
- Reliability: Fewer layers means fewer potential points of failure
Module Import Requirements
Auto-imported
coot - The core C++/SWIG binding is automatically available
- No import statement needed
Requires explicit import
coot_utils - Python utility library built on top of coot
- Must execute:
import coot_utils before using any of its functions
Example of the problem:
coot_utils.chain_ids(0)
import coot_utils
coot_utils.chain_ids(0)
But in this case (for chain-ids), this is preferred:
coot.get_chain_ids_py(0)
Function Naming Conventions
C++ Functions (SWIG bindings)
- End with
_py() suffix
- Examples:
closest_atom_simple_py(), is_valid_model_molecule(), chain_id_py()
- These are the core functions - prefer these
Python Wrapper Functions
- No
_py() suffix
- Examples:
closest_atom(), closest_atom_simple(), chain_ids()
- Only use when they provide genuine convenience
Specific Function Guidance
✅ CORRECT: Getting Closest Atom
atom_spec = coot.closest_atom_simple_py()
atom_spec = coot.closest_atom_py(0)
atom_spec = coot.closest_atom_raw_py()
❌ INCORRECT: Don't use coot_utils for simple passthroughs
import coot_utils
atom_spec = coot_utils.closest_atom(0)
atom_spec = coot_utils.closest_atom_simple()
✅ CORRECT: When to use coot_utils
Use coot_utils functions when they provide genuine convenience or abstraction:
import coot_utils
chains = coot_utils.chain_ids(0)
n = coot.n_chains(0)
chains = [coot.chain_id_py(0, i) for i in range(n)]
Checking Molecule Validity
if coot.is_valid_model_molecule(0):
print("Molecule 0 is a valid model")
if coot.is_valid_map_molecule(1):
print("Molecule 1 is a valid map")
import coot_utils
if coot_utils.valid_model_molecule_qm(0):
pass
Getting Molecule Information
name = coot.molecule_name(0)
n_chains = coot.n_chains(0)
import coot_utils
chains = coot_utils.chain_ids(0)
MMDB Atom Selection Syntax
When using functions like new_molecule_by_atom_selection(), superpose_with_atom_selection(),
or get_hydrogen_bonds_py(), use MMDB CID (Coordinate ID) strings to specify which atoms
to include.
Format
/mdl/chn/s1.i1-s2.i2/atm[elm]:aloc
or for residue-name-based selection:
/mdl/chn/*(res)/atm[elm]:aloc
Components
mdl - Model number (0 or * for any model; /1/ for model 1; // is shorthand for any model)
chn - Chain ID (e.g., A, B, X)
- Comma-separated list:
A,B,C
- Negation with
!: !A,B selects all chains except A and B
* for all chains (default)
s1-s2 - Residue sequence number range:
- Single:
50
- Range:
10-20
- With insertion codes:
33.A-120.B (residue 33 ins A through residue 120 ins B)
* for all residues (default)
(res) - Residue name filter in parentheses
- Single:
(HIS)
- Comma-separated list:
(ALA,SER,GLY)
- Negation:
(!ALA,SER) selects everything except ALA and SER
.ic - Insertion code (e.g., .A)
atm - Atom name (e.g., CA, N, O)
- Comma-separated list:
CA,N,O
- Negation:
!CA,CB selects all atoms except CA and CB
[elm] - Element in square brackets (e.g., [C], [N], [FE])
- Useful for disambiguation:
CA[C] selects C-alpha (carbon), not calcium
- Comma-separated list:
[C,N,O]
- Negation:
[!H] selects all non-hydrogen atoms
:aloc - Alternate location indicator
:A selects alt-loc A
:,A selects atoms with no alt-loc or alt-loc A
- Defaults to
"" (no alt-loc) when atom or element is specified
All components are optional and default to * (match everything) when omitted.
Negation
Any of the comma-separated list fields (chain, residue name, atom name, element, alt-loc)
can be negated by prefixing with !:
"//!A"
"//A/(!GLY,ALA)"
"//A/*/!CA,CB"
"//A/*/[!H]"
Examples
"//A"
"//A,B,C"
"//A/12-130"
"//A/12-130/CA"
"//A/33.A-120.B"
"//B/10-20(GLY)"
"//A/*(HIS)"
"//A/(GLU,ASP)"
"//A/(!ALA,GLY)"
"//A/50/CA"
"//A/50(HIS)/CA"
"CA[C]"
"//A/*/[C]"
"//A/*/[!H]"
"//A/50/CA:A"
"[C]:,A"
"*"
"/1"
"33-120"
Note: Selections containing commas must be quoted.
Usage Examples
imol_ab = coot.new_molecule_by_atom_selection(0, "//A,B")
imol_ca = coot.new_molecule_by_atom_selection(0, "//A/10-50/CA")
coot.superpose_with_atom_selection(
imol1=0,
imol2=1,
mmdb_atom_sel_str_1="//A/10-100/CA",
mmdb_atom_sel_str_2="//A/10-100/CA",
move_imol2_copy_flag=0
)
coot.get_hydrogen_bonds_py(0, "//A/35", "//A", 0)
Code Execution Patterns
Single-line expressions
Single-line expressions return their evaluated value:
coot.is_valid_model_molecule(0)
Multi-line code blocks
Multi-line blocks require explicit return or final expression:
mols = []
for i in range(3):
mols.append(i)
print(mols)
mols = []
for i in range(3):
mols.append(i)
mols
[i for i in range(3)]
Common Tasks Reference
Loading Tutorial Data
coot.load_tutorial_model_and_data()
Listing Molecules
[(i, coot.is_valid_model_molecule(i), coot.is_valid_map_molecule(i)) for i in range(6)]
for i in range(10):
if coot.is_valid_model_molecule(i):
print(f"Model {i}: {coot.molecule_name(i)}")
elif coot.is_valid_map_molecule(i):
print(f"Map {i}: {coot.molecule_name(i)}")
Working with Chain IDs
import coot_utils
chains = coot_utils.chain_ids(0)
for chain in chains:
print(f"Chain {chain}")
Getting Active/Closest Residue
atom = coot.closest_atom_simple_py()
if atom:
imol, chain, resno, ins, atom_name, alt, coords = atom[0], atom[1], atom[2], atom[3], atom[4], atom[5], atom[6]
import coot_utils
active = coot_utils.active_residue()
if active:
imol, chain, resno, ins, atom_name, alt = active
Density Fit Analysis
Map Correlation Functions
import coot_utils
residue_spec = ["A", 42, ""]
correlation = coot.density_score_residue_py(0, residue_spec, 1)
residue_specs = [["A", i, ""] for i in range(40, 50)]
results = coot.map_to_model_correlation_per_residue_py(0, residue_specs, 0, 1)
stats = coot.map_to_model_correlation_stats_per_residue_range_py(
0, "A", 1, 100, 1
)
Zoom and View Settings
When adjusting the view in Coot, remember that higher zoom values mean the molecule appears larger on screen (i.e., zoomed in), while lower values show more of the scene (zoomed out). Typical ranges:
- 150-300: Whole-molecule overview (appropriate for ribbons, surfaces, overall architecture)
- 50-100: Domain or region level
- 20-50: Residue-level detail (inspecting side chains, density fit, rotamers)
Small proteins like RNase A (~124 residues) may appear compact even at zoom 200, while larger complexes will fill the screen at lower zoom values. When presenting a ribbon diagram or other overview representation, consider turning off the bond representation (coot.set_mol_displayed(imol, 0)) and hiding electron density maps (coot.set_map_displayed(imol_map, 0)) to reduce visual clutter while keeping the ribbon mesh visible.
Use coot.zoom_factor() to query the current zoom level and coot.set_zoom(value) to set it. For interactive exploration, users can also adjust zoom with the scroll wheel.
Performance Considerations
Function Call Overhead
import coot_utils
for i in range(1000):
atom = coot_utils.closest_atom(0)
for i in range(1000):
atom = coot.closest_atom_py(0)
When Python Wrappers Are Worth It
Python wrappers are valuable when they:
- Aggregate multiple C++ calls (e.g.,
chain_ids() calls chain_id_py() in a loop)
- Transform data into more convenient formats
- Provide meaningful abstractions that simplify complex operations
- Add error handling or validation logic
Decision Tree
Need to call a Coot function?
│
├─ Does it require coot_utils for convenience features?
│ └─ YES → import coot_utils and use it
│ Examples: chain_ids(), active_residue()
│
└─ NO → Use coot.*_py() directly
Examples: closest_atom_simple_py(), is_valid_model_molecule()
Quick Reference Table
| Task | ❌ Avoid | ✅ Use Instead | Reason |
|---|
| Get closest atom (all molecules) | coot_utils.closest_atom_simple() | coot.closest_atom_simple_py() | Direct C++, no import needed |
| Get closest atom (specific mol) | coot_utils.closest_atom(imol) | coot.closest_atom_py(imol) | Direct C++, no import needed |
| Get chain IDs | Multiple C++ calls | coot_utils.chain_ids(imol) | Convenience wrapper adds value |
| Check if valid model | coot_utils.valid_model_molecule_qm() | coot.is_valid_model_molecule(imol) | Direct C++, clearer name |
| Get molecule name | N/A | coot.molecule_name(imol) | Direct C++ only |
| Load tutorial data | coot.tutorial_model_and_data() | coot.load_tutorial_model_and_data() | Correct function name |
Common Mistakes to Avoid
1. Using coot_utils without import
chains = coot_utils.chain_ids(0)
import coot_utils
chains = coot_utils.chain_ids(0)
2. Using coot_utils when unnecessary
import coot_utils
atom = coot_utils.closest_atom_simple()
atom = coot.closest_atom_simple_py()
3. Wrong function names
coot.tutorial_model_and_data()
coot.load_tutorial_model_and_data()
4. Getting output from multi-line code
result = []
for i in range(5):
result.append(i)
print(result)
result = []
for i in range(5):
result.append(i)
result
The `search_coot_functions` tool is your primary method for finding Coot functions. It supports powerful search patterns:
**Space-separated words = Logical AND**
```python
search_coot_functions("map model correlation")
search_coot_functions("residue range chain")
search_coot_functions("min max residue")
Single words = Simple search
search_coot_functions("correlation")
search_coot_functions("validation")
search_coot_functions("rotamer")
Common search patterns:
"map correlation" - density fit functions
"residue validation" - geometry checking
"chain residue" - chain/residue operations
"ligand environment" - ligand analysis
"ramachandran" - backbone validation
"density fit" - map fitting functions
✅ CORRECT Search Strategy
search_coot_functions("chain residue")
search_coot_functions("min max residue")
search_coot_functions("correlation residue")
❌ INCORRECT Search Strategy
When to use each discovery tool
-
search_coot_functions(pattern) - First choice
- Use space-separated words for AND logic
- Returns max 40 results with documentation
- Best for targeted searches
-
list_coot_categories() - For browsing
- Returns: ['load', 'read', 'display', 'refinement', 'validation', 'ligand', 'util']
- Use when you want to explore a general area
-
get_functions_in_category(category) - For comprehensive lists
- Returns all functions in a category (50-200 functions)
- Use after identifying the right category
Search Tips
- Start with 2-3 specific words that describe what you need
- If too many results, add more words to narrow down
- If no results, try synonyms or broader terms
- Common terms: validation, correlation, residue, chain, map, model, ligand, fit, geometry
Summary
- Always prefer
coot.*_py() functions when they're simple passthroughs
- Only use
coot_utils functions when they add genuine convenience
- Remember
coot is auto-imported, coot_utils is not
- Use single-line expressions when possible for cleaner returns
- Check function names -
load_tutorial_model_and_data() not tutorial_model_and_data()
- Use
search_coot_functions with space-separated words for AND logic when searching the API
Following these practices ensures optimal performance and correct API usage when working with Coot through the MCP server.