Validation status (2026-10-16):
verified. The acceptance suite has now run twice, 12/12 requests answered each time withpeak_reserved_gib2.0 on every one; seeprovider.toml'sverified_on. Earlier status (2026-10-06): all five setup steps pass. On the NVIDIA GB10 (aarch64, cc 12.1, driver 580.173.02):tu remote setup apnovoreports preflight OK, environment present, kit at the pinned commit, weights 1/1 at their pins, andsmoke: OK — one prediction in 69s, peak reserved 2.0 GiB, which agrees with the manifest's independently measured budget. The weights refusal was tested in both directions. Residency is 5.3x at the published 1000 denoising steps (193.1 s for the design that pays the XLA compile, 36.6 s for the one that reuses it) and far larger at 50 steps (158.5 s and 1.99 s), where compilation rather than compute dominates — plan with the figure at your own step count. Folding and resequencing are deliberately not part of this provider. What has been established on the NVIDIA GB10 (aarch64, cc 12.1, driver 580.173.02) is the environment and the no-weights path: the 72-package lock installs natively,alphafold3's C++ extension builds from source, the pinned stack reportsjax.default_backend() == "gpu"onCudaDevice(id=0)"NVIDIA GB10" cc 12.1, and a design manifest validates and its motif spec parses with no weights and no GPU. The generator weights have been fetched and their sha256 recorded. Not established: the residency gain, the acceptance suite, and scientific quality. Folding and resequencing are deliberately not part of this provider — see the provider README for why. Keep this deployment private.
Read src/tooluniverse/remote/apnovo/README.md before deploying. This
provider's weights are licensed for non-commercial use by non-commercial
organisations only and must not be redistributed, its Output carries terms, and
citation is a licence condition. That is a deployment constraint, not a
footnote: a public or mixed-audience deployment does not satisfy it.
Prerequisites
- Run from the ToolUniverse repository root on Linux with Python 3.12.
- A CUDA GPU is required. The model is held resident between requests; CPU is not a target.
- Keep provider data, weights, caches and credentials outside Git.
- Bind to loopback. A non-loopback bind requires
TOOLUNIVERSE_API_TOKEN; never put it in arguments or results.
Run the standard-library contract check before downloading anything large:
python scripts/remote_validation/setup_skill_preflight.py --implementation apnovo
After exporting provider resources add --check-provider-env; after the server
starts add --live to verify the MCP tool set without running the model.
Create the two environments
The MCP server runs in ToolUniverse's environment; the model runs in its own, built from a pinned lock. They cannot import each other, and that is the point.
python3 -m venv .venvs/apnovo
. .venvs/apnovo/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install fastmcp
The model half lives where TOOLUNIVERSE_APNOVO_PYTHON points, and nothing
above creates it. Create an empty interpreter first; tu remote setup --only environment fills it from
src/tooluniverse/remote/apnovo/environment/requirements.lock with --no-deps,
so no version is silently resolved to something else.
python3 -m venv ~/apnovo-venv # NOT `uv venv` -- see below
export TOOLUNIVERSE_APNOVO_PYTHON=~/apnovo-venv/bin/python
export APNOVO_WEIGHTS_ROOT=~/.apnovo
Use python3 -m venv, not uv venv. The kit step installs upstream
editable with {python} -m pip, and uv venv does not put pip in the
interpreter it creates, so the kit step fails with No module named pip after
the environment step has already reported present. If you have such a venv
already, ~/apnovo-venv/bin/python -m ensurepip --upgrade repairs it in place.
Two things about this lock before it runs.
The jax layer is the half that was hardest to prove on this class of card, and
it is already proven: AP Novo pins jax==0.10.2 and tokamax==0.0.12, exactly
the stack the sibling af3_jax provider verified here. All 13
nvidia-*-cu12 wheels resolve natively at CUDA 12.9.x with cuDNN 9.27.0.42, and
nothing in the lock is pinned by an x86-64 direct URL.
alphafold3 is a git dependency with a C++ extension, and its build fetches
Eigen from gitlab.com during the build. A first attempt here failed three
retries with fatal: remote error: GitLab is currently unable to handle this request; an unchanged retry succeeded minutes later. Retry before
diagnosing — and do not repeat the wrong turn this took: constraining
cmake<4 on the theory that CMake 4 had removed cifpp's deprecated
FetchContent_Populate calls changed nothing, because that was not the problem.
build_data then compiles alphafold3's CCD (ccd.pickle and
chemical_component_sets.pickle); ligand parsing fails without it. If
components.cif cannot be found, set LIBCIFPP_DATA_DIR to its directory
first. The kit step runs this.
The third environment, for resequencing
apnovo_resequence_designs runs LigandMPNN, which pins torch==2.2.1,
triton==2.2.0 and CUDA 12.1 wheels. That cannot share an interpreter with
this provider's jax stack, so it gets its own — which is how upstream's
run_ligandmpnn.py already works, by invoking a separate --python_executable.
It runs on the CPU, and on this card it has to. torch below 2.7 drives no
sm_121 part: 2.5.1 reports cuda.is_available() True, warns that sm_121 is not
in its arch list, then raises no kernel image is available on the first
device allocation, and cuobjdump shows its libtorch_cuda.so carries cubins
sm_50..sm_90a and no PTX to JIT from. PyPI publishes no CUDA aarch64 torch below
2.7 at all.
So install the CPU-only wheel at upstream's own pin. This is not a
preference: LigandMPNN's run.py picks its device with
torch.cuda.is_available(), so a CUDA build here would make it select the card
and die on the first allocation, with a message about kernel images rather than
about devices.
uv venv --python 3.11 ~/ligandmpnn-venv
git clone https://github.com/dauparas/LigandMPNN.git ~/LigandMPNN
# upstream's requirements minus its CUDA 12.1 wheels, which do not exist for
# aarch64 and which we deliberately do not want
grep -v '^nvidia-\|^triton==' ~/LigandMPNN/requirements.txt > /tmp/lmpnn-reqs.txt
VIRTUAL_ENV=~/ligandmpnn-venv uv pip install -r /tmp/lmpnn-reqs.txt "setuptools<82"
export LIGANDMPNN_DIR=~/LigandMPNN
export TOOLUNIVERSE_LIGANDMPNN_PYTHON=~/ligandmpnn-venv/bin/python
setuptools<82 is upstream's own note: ProDy imports pkg_resources, which 82
removed. Verify the one fact that matters before going further:
~/ligandmpnn-venv/bin/python -c "import torch; print(torch.__version__, torch.version.cuda, torch.cuda.is_available())"
# 2.2.1 None False <- `None False` is the point
The checkpoint is fetched by the weights step along with the generator's, under
$APNOVO_WEIGHTS_ROOT/ligandmpnn/. It is MIT licensed, so unlike the
generator it is not behind the acceptance gate — its terms are not AP Novo's.
Both environment variables are optional. Left unset the provider still
starts and the other three operations work; apnovo_resequence_designs then
refuses by name and says which of the three pieces is missing. Measured: one
129-residue design resequenced in 12 s on the CPU.
Why bother. Resequencing is the input to the strongest filter in the paper.
Hold a backbone, generate 7-10 sequences for it, and score the backbone by how
many of them pass your filter set: multi-resequence scoring was worth 2.5x
(Kemp) to 5.4x (serine esterase) at 50% recall retrospectively, and up to 30x
prospectively. num_sequences: 1 is the default pipeline; MR needs 7 to 10.
Accept the weights terms, explicitly
tu remote setup apnovo --only weights refuses by design until you accept:
[apnovo] NOT ACTIVE reason=weights_terms_not_accepted
The refusal prints the terms and the two policy URLs. There is no credential to supply and no form to post — the gate is contractual, not technical, which is why it is an acceptance you make rather than a side effect of running setup:
export APNOVO_ACCEPT_WEIGHTS_TERMS=1
Set this only if your organisation is a non-commercial organisation as the
terms define it. The fetch then downloads generator.bin.zst (547,140,096 B,
sha256 f023bed8…128928) into $APNOVO_WEIGHTS_ROOT/apnovo_generator/, and the
weights step verifies both. A short or off-pin file is named and the launch
stops; nothing is fetched at request time.
Do not relocate these weights to shared or group-readable storage, and do not mirror them. Google grants access either to one individual for their own use, who may not share their copy, or to an authorised organisational representative who may share it within that organisation.
Run setup
tu remote setup apnovo
tu remote check apnovo
tu remote run apnovo
Five steps, each idempotent and each reporting present when it has nothing to
do: preflight → environment → kit → weights → smoke. Each refuses by name and
carries its fix. check is part of the normal path, not a debugging step: run
it before the first run and after anything changes.
Operations
| operation | GPU | weights | what it does |
|---|---|---|---|
apnovo_validate_motif |
no | no | parses a motif CIF + motif_str with upstream's own MotifSpec; reports fixed atoms, ligand chains, whether conditioning is indexed |
apnovo_generate_designs |
yes | AP Novo | runs the diffusion generator over a campaign manifest |
apnovo_resequence_designs |
no (CPU) | LigandMPNN | LigandMPNN over a campaign's backbones, in a third interpreter |
apnovo_ingest_folded |
no | no | writes predictions from elsewhere into the campaign as upstream's folded stage |
apnovo_evaluate_designs |
no | no | runs upstream's metrics over a folded campaign |
apnovo_validate_motif exists because motif_str is the field that silently
wastes GPU-hours: a malformed one is otherwise only visible after generation.
Run it first, always.
apnovo_evaluate_designs needs a folded campaign, and this provider does not
fold. Upstream's evaluation does not fall back to the metrics that need no
structure prediction: it reads each design's metadata, finds no folding seed,
and refuses the run —
ValueError: …/metadata/kemp_0000_metadata.json records no folding seed for
kemp_0000 in …/01_generation, so it describes a design that has not been folded
Read that message closely, because half of it was our bug. It names the
directory it searched, and 01_generation is the wrong one: the flag's own
help says "Directory with the output of run_alphafold.py", and
_discover_folded_designs_for_metadata looks for
<sample>_<state>_confidences_seed<n>.json inside the directory it is given.
A generation stage holds none, so a campaign that HAD been folded failed in
exactly this way too, and the two causes were indistinguishable. Fixed: the
worker now reads 03_folded.
The other half is real and stays: the structures have to come from somewhere,
and that somewhere is not this provider while the AlphaFold 3 weights question
is unsettled. apnovo_ingest_folded closes it. Fold each design's states
with any predictor that has settled its own weights question — openfold3,
protenix_v1, esmfold2, chai1 and rosettafold3 are five on this same
standard — and post the mmCIF text:
{
"run_dir": "/path/to/campaign/returned/by/apnovo_generate_designs",
"states": [
{
"sample_name": "kemp_0000",
"state": "apo",
"seed": 0,
"folded_cif": "data_kemp_0000\n_entry.id kemp_0000\n",
"designed_cif": "kemp_0000.cif"
}
]
}
Then call apnovo_evaluate_designs. Two rules the layout imposes, both read
from design_manifest.py at the pinned commit rather than guessed: seeds
must be contiguous from 0, because upstream counts up and stops at the first
gap (the response reports any gap it would hit), and the first state of each
design must carry designed_cif, because upstream refuses a design whose
first confidence record names no structure to score against. Every other key
in confidences is carried through into the metric row, so a sibling's pTM,
ipTM and pLDDT arrive without any mapping here.
This path has been run. On 2026-10-15, upstream's kemp_test_manifest.json
at 2 designs / 50 steps, folded by protenix_v1 (both derived states, the
complex with ligand CCD_6NT), ingested and scored: status: ok, 2 designs,
547 metrics each, families ['complex', 'monomer', 'sampled', 'sequence'],
of which 68 are kemp-suite metrics including complex/acid_base/distance
6.086 A. kemp_0000: monomer/tm_score 89.47, monomer/rmsd 1.596 A, zero
missing motif residues. The confidence pass-through is visible as
complex/plddt 94.71 and complex/iptm 0.9285.
Two things to know when you fold with a sibling. The predicted mmCIF of a
130-residue monomer is ~100 KB, and a tool result is capped at 100,000
characters by the transport before falling back to raw truncation -- which
corrupts the JSON rather than shortening it -- so ask the folding provider for
out_dir and read the files from disk, not for return_structures. And
apnovo_evaluate_designs reports eval_suite_skipped: upstream SKIPS a
reaction suite it cannot compute, with a warning rather than an error, so an
empty list there is what tells you the suite actually ran.
Troubleshooting
weights_terms_not_accepted — intended. See above.
The environment step fails in alphafold3's build. Read the error before
acting. If it names gitlab.com or Eigen, retry unchanged. If it names the
compiler or cifpp itself, that is a different problem.
jax.default_backend() is "cpu". The verify line in provider.toml
asserts gpu and will refuse the environment. Check the driver first: this card
needs the 580 series for sm_121 to be enumerated at all.
A shared-memory error from XLA, with an invariant byte count. Expect this
class on this card: a kernel tuned for a part with more shared memory per block
than this one has. af3_jax hit it at 110,592 bytes against 101,376 available,
and the fix was upstream's own flag,
--tokamax_autotuning_cache_miss_fallback=autotune. If three attempts to switch
a suspect off leave the number byte-identical, that is a statement about where
the culprit is not — do not read it as three dead ends.
nvidia-smi reports [N/A] for memory.used. Correct on this unified-memory
part, and the reason a VRAM figure here comes from JAX's own
memory_stats()["peak_bytes_in_use"] rather than from nvidia-smi. A JAX
provider's budget is not in the same column as a torch provider's.
Before claiming verified
python scripts/remote_validation/verify_provider.py apnovo
pytest tests/unit/test_remote_schema_parity.py tests/unit/test_remote_setup_preflight.py \
tests/unit/test_remote_runtime.py tests/unit/test_remote_resident.py -q
status = "verified" additionally needs verified_on naming this hardware, and
a vram_budget_gb that was measured rather than guessed. One smoke request is
not a test of a provider that holds state between requests.
Start and verify locally
python -m tooluniverse.remote.apnovo.apnovo_tool
The Streamable HTTP endpoint is http://127.0.0.1:8059/mcp. In a second activated shell run:
python - <<'PY'
import asyncio
from fastmcp import Client
async def main():
async with Client("http://127.0.0.1:8059/mcp") as client:
print([tool.name for tool in await client.list_tools()])
asyncio.run(main())
PY
Confirm discovery contains apnovo_validate_motif, apnovo_generate_designs and apnovo_evaluate_designs; stop on empty, duplicate, or schema-drifted discovery.
The first request at a given step count pays the XLA compile; the ones
after it do not. The axis is the step count, not the design length.
Upstream pads every design to one fixed 512-residue shape, and caches the
compiled trajectory in functools.lru_cache(maxsize=8) keyed on
(num_sampling_steps, num_partial_diffusion_steps) — so a second campaign at
a different step count is cold again however many designs preceded it, and a
ninth distinct step count evicts the first. Measured at the published 1000
denoising steps, as the two designs of one request: 193.1 s for the design
that pays the compile and 36.6 s for the one that reuses it. At 50 steps the
same pair is 158.5 s and 1.99 s. Benchmark at the step count you will
actually run — a ratio
measured at 50 steps says almost nothing about one at 1000, and this provider's
own figures differ by more than an order of magnitude between them.
Connect to ToolUniverse Connect
The tuplatform-connect relay is not yet published on PyPI. Install the
reviewed public wheel below; its SHA-256 is pinned. Interactive sharing uses
browser device authorization, so no key copy/paste is required.
python -m pip install fastmcp pyyaml "tuplatform-connect @ https://connect.aiscientist.tools/downloads/tuplatform_connect-0.3.0-py3-none-any.whl#sha256=3fad5eee5ecf7887a693d93ccd1aa112dc0955617a885d1fc3daded0030f9ae0"
tu doctor --forward http://127.0.0.1:8059/mcp --json
tu serve --share --forward http://127.0.0.1:8059/mcp --name validation-apnovo --workers 1
Authorize once per machine, then share with one command:
tu remote login
tu remote share apnovo
Override the reviewed name and worker count only when needed:
tu remote share apnovo --name my-apnovo-remote --workers 1
Sharing starts the browser flow when the key is missing, expired, or revoked. A
malformed or revoked explicit TOOLUNIVERSE_SERVICE_KEY fails fast rather than
being silently replaced. tu remote logout removes only the local copy;
tu remote logout --revoke revokes the computer-only platform connection
first.
Think before sharing this one. The relay makes the model reachable by whoever holds the link, and the weights licence restricts who may use it and whose Input it may process to eligible non-commercial organisations. Share it inside such an organisation, or not at all.
This implementation was added after the authenticated 2026-08-16 Platform
matrix and was not part of it. That matrix imported each implementation it
covered as unpublished owner drafts and invoked them through
/expert-sessions/{id}/test; this provider was imported as no such draft and
invoked through no such route. It has not been exercised through the Platform at
all: the evidence above is local, on one card, through the loopback endpoint.
Public publication, independent-caller authorization/isolation, saturation and
persistent supervision are untested.
Run a verified example
Operation: apnovo_validate_motif
{"manifest": "{\"defaults\": {\"num_designs\": 1, \"seed_start\": 0, \"num_sampling_steps\": 1000}, \"designs\": [{\"name\": \"kemp_eliminase\", \"output_prefix\": \"kemp\", \"input_file\": \"motif.cif\", \"motif_str\": \"A1,A2|10-100,{},2-80,{},10-100/B1\", \"motif_atoms\": \"A1:OE2,OE1,CD,CG A2:OG,CB,CA\", \"seq_length\": \"120-160\", \"is_author_naming\": false}]}", "files": {"motif.cif": "data_motif\n# the transition-state motif, mmCIF\n"}}
This one is deliberately the cheapest operation: it parses the manifest and
every design's motif spec and touches neither the weights nor the card, so it
is the right first call after tu remote run and the right first call of any
campaign. Replace files["motif.cif"] with your real motif mmCIF — the stub
above is shaped to the schema, and a real motif carries the catalytic
side-chain atoms and the ligand.
This example has been run through the live endpoint, with the stub replaced
by the real Kemp eliminase motif mmCIF. list_tools returned all three
operations, and the call came back {"status": "ok", "designs": [{"name": "kemp_eliminase", "indexed_conditioning": true, "partial_diffusion": false, "parsed_at_seeds": [0, 1, 2]}], ...}. With the stub
CIF above the call is schema-valid but will not parse as a motif, which is why
replacing it is the first thing to do.
Invoke it through the live local MCP endpoint:
python - <<'PY'
import asyncio
import json
from fastmcp import Client
async def main():
arguments = json.loads(r'''{"manifest": "{\"defaults\": {\"num_designs\": 1, \"seed_start\": 0, \"num_sampling_steps\": 1000}, \"designs\": [{\"name\": \"kemp_eliminase\", \"output_prefix\": \"kemp\", \"input_file\": \"motif.cif\", \"motif_str\": \"A1,A2|10-100,{},2-80,{},10-100/B1\", \"motif_atoms\": \"A1:OE2,OE1,CD,CG A2:OG,CB,CA\", \"seq_length\": \"120-160\", \"is_author_naming\": false}]}", "files": {"motif.cif": "data_motif\n# the transition-state motif, mmCIF\n"}}''')
async with Client("http://127.0.0.1:8059/mcp") as client:
result = await client.call_tool("apnovo_validate_motif", arguments)
print(result)
asyncio.run(main())
PY