Best Practices for using Coot MCP
Scanned 9/1/2026
Install to Claude Code
npx -y skills add pemsley/coot --skill best-practices --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Best Practices?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/pemsley-best-practices)More formats (shields.io, HTML) on the badges page.
---
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.**
```python
# ✓ CORRECT - single expression, no imports
coot.run_python("1 + 1") # Returns: 2
coot.run_python("coot.molecule_name(0)") # Returns: molecule name
coot.run_python("coot.is_valid_model_molecule(0)") # Returns: 1 or 0
# ✗ WRONG - these will all fail with syntax errors
coot.run_python("import os") # FAILS - import not allowed
coot.run_python("import os; os.getcwd()") # FAILS - semicolon not allowed
coot.run_python("x = 5; x * 2") # FAILS - semicolon not allowed
coot.run_python("import coot_utils; coot_utils.chain_ids(0)") # FAILS
```
**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:
```python
# ✓ CORRECT - use multiline for imports
coot.run_python_multiline("""
import os
result = os.getcwd()
print(result)
""")
# ✓ CORRECT - use multiline for multiple statements
coot.run_python_multiline("""
x = 5
y = x * 2
print(y)
""")
# ✓ CORRECT - use multiline for complex logic
coot.run_python_multiline("""
import coot_utils
chains = coot_utils.chain_ids(0)
for chain in chains:
print(f"Chain: {chain}")
""")
# ✓ CORRECT - use multiline for function definitions
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
```python
# ❌ WRONG - will fail with "invalid syntax"
result = coot.run_python("import coot_utils; coot_utils.chain_ids(0)")
# ✓ CORRECT - use multiline
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
```python
# ✓ CORRECT - no path, just filename
with open('output.txt', 'w') as f:
f.write(content)
# ✓ CORRECT - works for any file type
with open('validation.xml', 'w') as f:
f.write(xml_content)
# ✓ CORRECT - reading also uses just filename
with open('data.txt', 'r') as f:
content = f.read()
```
### ❌ INCORRECT: Don't Specify Paths
```python
# ✗ WRONG - will fail with FileNotFoundError
with open('/home/claude/output.txt', 'w') as f:
f.write(content)
# ✗ WRONG - this path doesn't exist in Coot's context
with open('/mnt/user-data/outputs/file.txt', 'w') as f:
f.write(content)
# ✗ WRONG - even full paths will fail
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
```python
# 1. Download/fetch content (if needed)
url = "https://example.com/data.xml"
content = coot.coot_get_url_as_string_py(url)
# 2. Write to current directory (no path!)
with open('data.xml', 'w') as f:
f.write(content)
# 3. Process the file
import xml.etree.ElementTree as ET
tree = ET.parse('data.xml') # Read from current directory
root = tree.getroot()
# 4. Write results (again, no path!)
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:**
1. Read the `/mnt/skills/user/coot-essential-api/SKILL.md` file
2. Extract all function names mentioned in that file
3. Call `get_function_descriptions()` with the complete list of function names
4. 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:
```python
# After reading coot-essential-api/SKILL.md, call:
Coot:get_function_descriptions([
"set_refinement_immediate_replacement",
"set_imol_refinement_map",
"is_valid_model_molecule",
"is_valid_map_molecule",
"get_hydrogen_bonds_py",
# ... all other functions from the essential API
])
```
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?
1. **Performance**: C++ functions are significantly faster (no Python overhead)
2. **Import requirements**: `coot` is auto-imported, `coot_utils` requires explicit import
3. **Simplicity**: Direct access to the core API without unnecessary abstraction layers
4. **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:**
```python
# This will fail with NameError if coot_utils not imported
coot_utils.chain_ids(0)
# Solution:
import coot_utils
coot_utils.chain_ids(0) # Now works
```
But in this case (for chain-ids), this is preferred:
```python
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
```python
# Get closest atom across all displayed molecules
atom_spec = coot.closest_atom_simple_py()
# Returns: [imol, chain-id, resno, ins-code, atom-name, alt-conf, [x, y, z]]
# Get closest atom in specific molecule
atom_spec = coot.closest_atom_py(0)
# Returns: [imol, chain-id, resno, ins-code, atom-name, alt-conf, [x, y, z]]
# Get raw closest atom (no CA substitution)
atom_spec = coot.closest_atom_raw_py()
```
### ❌ INCORRECT: Don't use coot_utils for simple passthroughs
```python
# DON'T DO THIS - unnecessary import and no added value
import coot_utils
atom_spec = coot_utils.closest_atom(0) # Just calls coot.closest_atom_py()
# DON'T DO THIS EITHER
atom_spec = coot_utils.closest_atom_simple() # Just calls coot.closest_atom_simple_py()
```
### ✅ CORRECT: When to use coot_utils
Use `coot_utils` functions when they provide **genuine convenience or abstraction**:
```python
import coot_utils
# chain_ids() is a convenience wrapper that constructs a list
# It calls coot.chain_id_py() in a loop and builds a list
chains = coot_utils.chain_ids(0) # Returns: ['A', 'B']
# Without coot_utils, you'd have to do:
n = coot.n_chains(0)
chains = [coot.chain_id_py(0, i) for i in range(n)]
```
### Checking Molecule Validity
```python
# ✅ CORRECT: Direct C++ function
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")
# ❌ INCORRECT: Don't use coot_utils for this
import coot_utils
if coot_utils.valid_model_molecule_qm(0): # Unnecessary
pass
```
### Getting Molecule Information
```python
# ✅ CORRECT: Direct access
name = coot.molecule_name(0)
n_chains = coot.n_chains(0)
# ✅ CORRECT: When coot_utils adds value
import coot_utils
chains = coot_utils.chain_ids(0) # Convenience wrapper
```
## 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 `!`:
```python
"//!A" # All chains except A
"//A/(!GLY,ALA)" # Non-GLY, non-ALA residues in chain A
"//A/*/!CA,CB" # All atoms except CA and CB in chain A
"//A/*/[!H]" # All non-hydrogen atoms in chain A
```
### Examples
```python
# Select entire chain
"//A" # All atoms in chain A
"//A,B,C" # All atoms in chains A, B, and C
# Select residue range
"//A/12-130" # Residues 12-130 in chain A
"//A/12-130/CA" # CA atoms from residues 12-130 in chain A
"//A/33.A-120.B" # Residue range with insertion codes
# Select by residue type
"//B/10-20(GLY)" # GLY residues 10-20 in chain B
"//A/*(HIS)" # All HIS residues in chain A
"//A/(GLU,ASP)" # All glutamate and aspartate in chain A
"//A/(!ALA,GLY)" # All residues except ALA and GLY in chain A
# Select specific atom
"//A/50/CA" # CA atom of residue 50 in chain A
"//A/50(HIS)/CA" # CA atom of HIS 50 in chain A
# Element disambiguation
"CA[C]" # C-alpha atoms (carbon), not calcium
"//A/*/[C]" # All carbon atoms in chain A
"//A/*/[!H]" # All non-hydrogen atoms in chain A
# Alt-loc selection
"//A/50/CA:A" # CA in alt-loc A
"[C]:,A" # Carbons with no alt-loc or alt-loc A
# Wildcards
"*" # All atoms in the structure
"/1" # All atoms in model 1
"33-120" # Residues 33-120 in any chain
```
**Note:** Selections containing commas must be quoted.
### Usage Examples
```python
# Create a new molecule with chains A and B
imol_ab = coot.new_molecule_by_atom_selection(0, "//A,B")
# Create a new molecule with CA atoms from residues 10-50 in chain A
imol_ca = coot.new_molecule_by_atom_selection(0, "//A/10-50/CA")
# Superpose using CA atoms
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
)
# Hydrogen bonds between a residue and a chain
coot.get_hydrogen_bonds_py(0, "//A/35", "//A", 0)
```
## Code Execution Patterns
### Single-line expressions
Single-line expressions return their evaluated value:
```python
coot.is_valid_model_molecule(0) # Returns: 1 or 0
```
### Multi-line code blocks
Multi-line blocks require explicit return or final expression:
```python
# ❌ This returns None (print doesn't return a value)
mols = []
for i in range(3):
mols.append(i)
print(mols)
# ✅ CORRECT: Return the value or use final expression
mols = []
for i in range(3):
mols.append(i)
mols # Final expression is returned
# ✅ ALSO CORRECT: List comprehension (single expression)
[i for i in range(3)]
```
## Common Tasks Reference
### Loading Tutorial Data
```python
# ✅ CORRECT function name
coot.load_tutorial_model_and_data()
# ❌ INCORRECT function names that don't exist
# coot.tutorial_model_and_data() # Wrong!
```
### Listing Molecules
```python
# Check molecules 0-5
[(i, coot.is_valid_model_molecule(i), coot.is_valid_map_molecule(i)) for i in range(6)]
# Get molecule names for valid molecules
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
```python
# ✅ CORRECT: Use coot_utils for convenience
import coot_utils
chains = coot_utils.chain_ids(0) # Returns: ['A', 'B', 'C']
# Iterate over chains
for chain in chains:
print(f"Chain {chain}")
```
### Getting Active/Closest Residue
```python
# ✅ Get closest atom across displayed molecules
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]
# ✅ Get active residue (with potential CA substitution)
import coot_utils
active = coot_utils.active_residue()
if active:
imol, chain, resno, ins, atom_name, alt = active
```
## Density Fit Analysis
### Map Correlation Functions
```python
# Get correlation for specific residues
import coot_utils
# Single residue
residue_spec = ["A", 42, ""]
correlation = coot.density_score_residue_py(0, residue_spec, 1)
# Per-residue correlation for a range
residue_specs = [["A", i, ""] for i in range(40, 50)]
results = coot.map_to_model_correlation_per_residue_py(0, residue_specs, 0, 1)
# Returns: [(residue_spec, correlation), ...]
# Main function for "which residue fits worst?"
stats = coot.map_to_model_correlation_stats_per_residue_range_py(
0, "A", 1, 100, 1 # imol, chain, start, end, imol_map
)
```
## 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
```python
# ❌ SLOW: Multiple function calls through Python wrapper
import coot_utils
for i in range(1000):
atom = coot_utils.closest_atom(0) # Unnecessary indirection
# ✅ FAST: Direct C++ calls
for i in range(1000):
atom = coot.closest_atom_py(0)
```
### When Python Wrappers Are Worth It
Python wrappers are valuable when they:
1. **Aggregate multiple C++ calls** (e.g., `chain_ids()` calls `chain_id_py()` in a loop)
2. **Transform data** into more convenient formats
3. **Provide meaningful abstractions** that simplify complex operations
4. **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
```python
# ❌ Will fail with NameError
chains = coot_utils.chain_ids(0)
# ✅ Import first
import coot_utils
chains = coot_utils.chain_ids(0)
```
### 2. Using coot_utils when unnecessary
```python
# ❌ Unnecessary indirection
import coot_utils
atom = coot_utils.closest_atom_simple()
# ✅ Direct and faster
atom = coot.closest_atom_simple_py()
```
### 3. Wrong function names
```python
# ❌ Function doesn't exist
coot.tutorial_model_and_data()
# ✅ Correct name
coot.load_tutorial_model_and_data()
```
### 4. Getting output from multi-line code
```python
# ✅ Use print() to see output - it appears in stdout
result = []
for i in range(5):
result.append(i)
print(result) # Output: [0, 1, 2, 3, 4]
# ❌ A bare expression at the end of multi-line code does NOT return a value
result = []
for i in range(5):
result.append(i)
result # Returns None - this doesn't work!
## API Discovery Tools
### Using search_coot_functions
The `search_coot_functions` tool is your primary method for finding Coot functions. It supports powerful search patterns:
**Space-separated words = Logical AND**
```python
# Find functions containing ALL these words
search_coot_functions("map model correlation")
# Returns functions like: map_to_model_correlation_stats_per_residue_range_py
search_coot_functions("residue range chain")
# Returns functions dealing with residue ranges in chains
search_coot_functions("min max residue")
# Returns functions with all three words (not just any one)
```
**Single words = Simple search**
```python
search_coot_functions("correlation") # All functions with "correlation"
search_coot_functions("validation") # All functions with "validation"
search_coot_functions("rotamer") # All functions with "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
```python
# Looking for functions to get residues in a chain
search_coot_functions("chain residue") # Logical AND
# Looking for min/max residue number functions
search_coot_functions("min max residue") # All three words required
# Looking for correlation analysis
search_coot_functions("correlation residue") # Both words required
```
### ❌ INCORRECT Search Strategy
```python
# DON'T use grep with pipe (|) when you mean AND
# This searches for min OR max OR residue (logical OR)
# Use search_coot_functions with spaces instead
```
### When to use each discovery tool
1. **search_coot_functions(pattern)** - First choice
- Use space-separated words for AND logic
- Returns max 40 results with documentation
- Best for targeted searches
2. **list_coot_categories()** - For browsing
- Returns: ['load', 'read', 'display', 'refinement', 'validation', 'ligand', 'util']
- Use when you want to explore a general area
3. **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
1. **Always prefer `coot.*_py()` functions** when they're simple passthroughs
2. **Only use `coot_utils` functions** when they add genuine convenience
3. **Remember `coot` is auto-imported**, `coot_utils` is not
4. **Use single-line expressions** when possible for cleaner returns
5. **Check function names** - `load_tutorial_model_and_data()` not `tutorial_model_and_data()`
6. **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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!