OCDocker.MCP package¶
MCP (Model Context Protocol) server
exposing the OCDocker Workbench API to LLM clients (Claude Code, Claude
Desktop, or any other MCP client) over stdio. It is a thin adapter: every tool
call is translated into an HTTP request against an already-running
ocdocker workbench serve API and the JSON response is returned as-is — no
business logic is duplicated here. See OCDocker.Workbench.Server for
the underlying API and OCDocker.Workbench.Jobs for job execution.
Note
Nearly all of the actual tool implementations are Python closures defined
inside OCDocker.MCP.Server.build_ocdocker_mcp_server() (required by
how the mcp SDK’s @server.tool() decorator registers tools against
one running server instance). Sphinx’s automodule only documents
module-level names, so it cannot see these tools or their docstrings — the
full reference below is maintained by hand and is the authoritative list.
Requirements and setup¶
Requires the mcp optional extra (mcp package, pulls in httpx) and
a separately running Workbench API (the api extra). This server does
not start ocdocker workbench serve itself — it only talks to one that
is already running.
pip install "ocdocker[api,mcp]"
# Terminal 1: start the Workbench API for one served OCScore/workspace root.
ocdocker workbench serve /path/to/OCScore/output --port 8765
# Terminal 2 (or launched directly by an MCP client, see below):
ocdocker mcp serve --workbench-api-url http://127.0.0.1:8765
--workbench-api-url defaults to http://127.0.0.1:8765
(OCDocker.MCP.Server.DEFAULT_WORKBENCH_API_URL, derived from
OCDocker.Workbench.Server.DEFAULT_WORKBENCH_API_HOST /
OCDocker.Workbench.Server.DEFAULT_WORKBENCH_API_PORT) and rarely needs
to be set explicitly unless the Workbench API is bound to a non-default host
or port.
Transport¶
stdio only. The MCP client is expected to spawn ocdocker mcp serve as
a subprocess and communicate over its stdin/stdout, which is how every
example below is wired. There is no HTTP/SSE transport in this version —
adding one later is a small, isolated change since the tool implementations
never touch the transport layer directly (they only call the Workbench HTTP
API via httpx).
Connecting a client¶
Any MCP-compatible client that supports stdio servers works. For Claude
Desktop or Claude Code, add an entry like this to the client’s MCP server
configuration (e.g. claude_desktop_config.json):
{
"mcpServers": {
"ocdocker-workbench": {
"command": "ocdocker",
"args": ["mcp", "serve", "--workbench-api-url", "http://127.0.0.1:8765"]
}
}
}
The server identifies itself to the client as
ocdocker-workbench (OCDocker.MCP.Server.MCP_SERVER_NAME) and
advertises this instruction string
(OCDocker.MCP.Server.MCP_SERVER_INSTRUCTIONS) so the model
understands the confirmation-gating contract before it calls anything:
Tools for inspecting an OCDocker Workbench workspace (OCScore studies, ablations, jobs) and for designing, launching, and monitoring vs/pipeline/ocscore_train/ocscore_reduce jobs. Read/plan/preview tools are always safe to call. run_job and cancel_job execute real, possibly long-running work: call plan_job first, show the plan to the user, and only call run_job with confirm=True after they agree. Never set confirm=True without an explicit go-ahead from the user for that specific job.
Tool reference¶
Nineteen tools are registered, in two groups. All tools return the parsed JSON
body of the underlying Workbench API response (a plain dict); tool
functions never raise a raw httpx exception — every failure surfaces as
OCDocker.MCP.Server.OCDockerMCPError (see Error handling below).
Tool |
Parameters |
Workbench API endpoint |
|---|---|---|
|
(none) |
|
|
(none) |
|
|
(none) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(none) |
|
|
|
|
|
|
|
|
|
|
|
|
|
Tool |
Parameters |
Workbench API endpoint |
|---|---|---|
|
|
|
|
|
|
WorkbenchJobKind is the literal type "vs" | "pipeline" | "ocscore_train" |
"ocscore_reduce" | "vs_campaign" (OCDocker.Workbench.Models.WorkbenchJobKind)
— it selects which ocdocker subcommand prefix a job runs (ocdocker vs ...,
ocdocker pipeline ..., ocdocker ocscore train ..., ocdocker ocscore
reduce ...), except "vs_campaign", which runs a generated shell script
covering every row of manifest instead (see Batch campaigns below).
args are extra CLI flags appended after that prefix — or, for
"vs_campaign", common flags appended to every row’s command — e.g.
["--protocol", "production.yml", "--output-dir", "runs/run-001"].
Read and plan tools¶
get_health()Check whether the Workbench API is reachable, and its
read_only/dashboard_modelstatus. Returns the same payload asGET /healthon the Workbench API (ok,service,api_version,root,read_only,dashboard_model).get_workspace()Return the served OCScore workspace summary: baseline study, ablation studies, per-replica status, and curated metrics. Same payload as
GET /api/ocscore-workspace(OCDocker.Workbench.Models.WorkbenchOCScoreWorkspace).get_ablation_design_context()Return the bundled and workspace-discovered ablation feature policies plus defaults for designing a new one. Same payload as
GET /api/ablation-design.preview_ablation_design(policy)Preview which features a draft ablation feature-policy would keep or exclude, without writing anything.
policyis a JSON object matching the Workbench Design tab’s request body: apolicyblock (include_features,include_patterns,exclude_features,exclude_patterns,allow_missing_excludes, …) plus optional identity/source fields (name,description,feature_source). SeeOCDocker.Workbench.AblationDesign.preview_ablation_design()for the exact body contract.plan_ablation_design(policy)Generate the policy YAML text and the
ocdocker ocscore traincommand string for a draft ablation. Samepolicybody shape aspreview_ablation_design. This only plans — it does not write the policy file or launch training; userun_jobwithkind="ocscore_train"for that once the design is finalized. SeeOCDocker.Workbench.AblationDesign.plan_ablation_design().get_vs_design_context(input_dir=None)Discover receptor/ligand/box candidates for designing a single-target
vs/pipelinerun. Best-effort, depth-limited scan by filename/extension heuristics (receptor:.pdb/.pdbqtwith “receptor” in the name; ligand:.smi/.sdf/.mol2/.pdbqtwith “ligand” in the name or parent directory; box:.pdb/.txtstarting with “box”) — there is no single fixed input layout in OCDocker, so results are candidates to choose from, never a requirement.input_diroptionally narrows the scan to one subdirectory of the served root instead of the whole workspace. Same payload asGET /api/vs-design. SeeOCDocker.Workbench.VSDesign.discover_vs_design_candidates().preview_vs_design(draft)Validate one draft single-target VS design without running anything.
draftis a JSON object withkind("vs"or"pipeline"),receptor,ligand,box(paths — absolute, or relative to the served root), plus kind-specific fields:enginefor"vs", orengines/rescoring_engines(lists) for"pipeline". Checks path existence, extension sanity, and engine names againstOCDocker.Workbench.Models.VALID_DOCKING_ENGINES/OCDocker.Workbench.Models.VALID_RESCORING_ENGINES. Covers exactly one receptor, one ligand, one box; for many samples in one job, see Batch campaigns below. Same payload asPOST /api/vs-design/preview. SeeOCDocker.Workbench.VSDesign.preview_vs_design().plan_vs_design(draft)Build the exact
ocdocker vs/pipelineargv for a valid draft design. Samedraftbody shape aspreview_vs_design— call that first and show the user any errors/warnings. On success returns{"kind", "args", "cwd", "shell_command"}, ready to hand directly torun_job(askind/args/cwd) to actually launch it, subject to the sameconfirm=Truegate as every other execute tool. RaisesOCDockerMCPErrorif the draft is invalid. Same payload asPOST /api/vs-design/plan. SeeOCDocker.Workbench.VSDesign.plan_vs_design().
Batch campaigns¶
A campaign runs many receptor/ligand/box samples as one tracked job — no
per-row confirmations. Its manifest reuses the same row shape
discover_vs_campaign_candidates() produces: sample, row_kind
("vs" or "pipeline" — rows may mix both), receptor, ligand,
box, engines (list), optional rescoring_engines (list).
Two execution engines, chosen via engine on plan_vs_campaign/
run_job/plan_job:
engine="shell"(default, no extra dependency): a generated POSIX shell script (OCDocker.Workbench.Jobs.build_campaign_script()) runs oneocdocker vs/pipelineinvocation per row sequentially, logging[sample i/N] <name>markers before each, continuing past a failing row, and exiting non-zero only if any row failed.engine="snakemake"(requires themcp/workflowextra’s Snakemake dependency): a bundled multi-sample Snakefile (OCDocker.Workbench.Jobs.build_campaign_snakemake_command(),OCDocker/Workbench/Snakefiles/vs_campaign.smk) runs every row through real Snakemake DAG orchestration — parallel viacores, resumable via--rerun-incomplete, also continuing past a failing row (--keep-going).
Either way the job’s own completed/failed status reflects the whole
campaign. results_dir, if given, is a shared base directory — every
row still writes to its own <results_dir>/<sample> (a literal shared
--outdir across every row would make samples overwrite each other, since
ocdocker vs/pipeline write straight under --outdir with no
nesting of their own).
get_vs_campaign_context(input_dir=None)Discover a draft multi-sample manifest from an
input/{sample}/...layout — one subdirectory per sample, matching the convention used byexamples/19_Snakefile_ocdocker_pipeline.smkandexamples/20_Snakefile_ocdocker_granular_pipeline.smk. Wheninput_diris omitted,<served root>/inputis used automatically if present, otherwise the served root itself. Best-effort — a workspace not organized this way returns an empty manifest with an explanatory issue, not an error; a manifest can also be hand-authored directly forpreview_vs_campaign. Same payload asGET /api/vs-campaign. SeeOCDocker.Workbench.VSDesign.discover_vs_campaign_candidates().preview_vs_campaign(manifest)Validate a draft manifest without running anything: path existence, engine names, and duplicate sample names, per row (errors/warnings are prefixed
Row i (sample): ...). Same payload asPOST /api/vs-campaign/preview. SeeOCDocker.Workbench.VSDesign.preview_vs_campaign().plan_vs_campaign(manifest, outdir=None)Build the
vs_campaignjob payload for a valid manifest — callpreview_vs_campaignfirst and show the user any errors/warnings, especially for large manifests. Accepts the sameengine/coressettings described above (via the request body’sengine/coresfields; not yet exposed as direct MCP tool parameters — pass them throughrun_job/plan_jobinstead when actually launching). On success returns{"kind": "vs_campaign", "engine", "manifest", "args", "results_dir", "cwd", "shell_command"}(plus"cores"forengine="snakemake"), ready to hand directly torun_job(askind/manifest/args/engine/cores/results_dir/cwd) to launch the whole batch. RaisesOCDockerMCPErrorif the manifest is invalid orengineis unrecognized. Same payload asPOST /api/vs-campaign/plan. SeeOCDocker.Workbench.VSDesign.plan_vs_campaign().get_campaign_progress(job_id)Return structured per-sample progress for a tracked job, parsed live from its own log text — no Snakemake internals, no extra process. Returns
{"engine": "snakemake" | "shell" | "unknown", "overall": {...} | None, "samples": {name: {"status": "pending" | "running" | "done" | "failed"}}}. Degrades toengine: "unknown"for a non-vs_campaignjob or an unrecognized log format — never an error.engine="snakemake"campaigns get reliable per-sample status;engine="shell"campaigns only reliable aggregate success/failure counts, since the shell loop never echoes an individual row’s outcome (seeOCDocker.Workbench.CampaignProgress.parse_campaign_progress()). Same payload asGET /api/jobs/{job_id}/campaign-progress.get_protocol_similarity(metric=None, reference=None)Return pairwise Jaccard feature-similarity across ablation protocols, clustered, with an optional outcome-metric overlay and reference-protocol diff.
metricselects which metric to overlay on clusters;referenceselects the protocol used for the added/removed feature diff (defaults tofull_ocscoreserver-side when omitted). Same payload asGET /api/ablation-protocol-similarity.list_jobs()List every tracked Workbench job — every
vs/pipeline/ocscore_train/ocscore_reducerun ever launched through this Workbench API instance, most recent first. Returns{"jobs": [...]}where each entry is aOCDocker.Workbench.Models.WorkbenchJobRecord.get_job(job_id)Return one tracked job’s current status (
defined/running/completed/failed/cancelled), command, PID, and exit code. RaisesOCDockerMCPErrorifjob_idis unknown.get_job_logs(job_id, lines=80)Return a bounded tail of a job’s stdout and stderr (
{"stdout": {...}, "stderr": {...}}, each aOCDocker.Workbench.Models.RunLogFilePreview).linescaps how many trailing lines are returned per stream.plan_job(kind, args=None, cwd=None, manifest=None, engine="shell", cores=4, results_dir=None)Preview the exact command a job would run —
{"kind", "command", "cwd"}— without launching it and without any side effects. Always call this (orrun_jobwithconfirm=False, which does the same thing) beforerun_job(..., confirm=True)and show the resulting command to the user.manifestis required forkind="vs_campaign"(see Batch campaigns) and ignored for every other kind.engine,cores, andresults_dirare also only meaningful forkind="vs_campaign".
Execute tools¶
run_job(kind, args=None, cwd=None, manifest=None, engine="shell", cores=4, results_dir=None, confirm=False)Launch a tracked job as a background subprocess on the machine running the Workbench API.
manifestis required forkind="vs_campaign"(build one withplan_vs_campaignfirst) and ignored otherwise.engine("shell"or"snakemake"),cores(Snakemake--cores), andresults_dir(shared base output directory, per-sample-nested) are also only meaningful forkind="vs_campaign"— see Batch campaigns.confirm=False(the default): launches nothing. Returns{"launched": false, "message": "...", "plan": {...}}— identical to callingplan_job— so the caller can inspect and show the command before committing to it.confirm=True: actually callsPOST /api/jobswith the Workbench job bearer token attached, and returns{"launched": true, "job": {...}}wherejobis the trackedWorkbenchJobRecord(status"running", PID, log paths).
Only pass
confirm=Trueafter the user has explicitly agreed to run that specific command — never infer consent from an ambiguous or general instruction. See Authentication for how the bearer token is resolved.cancel_job(job_id, confirm=False)Cancel a running tracked job (sends
SIGTERM, escalating toSIGKILLafter a grace period on the Workbench API side).confirm=False(the default): cancels nothing. Returns{"cancelled": false, "message": "...", "job": {...}}with the job’s current status, so the caller can confirm this is the right job before cancelling it.confirm=True: actually callsPOST /api/jobs/{job_id}/cancelwith the bearer token attached, and returns{"cancelled": true, "job": {...}}with the job’s post-cancel status.
Cancelling an already-finished job is a no-op on the Workbench API side — the returned record simply reflects its existing terminal status.
The confirm two-step pattern¶
run_job and cancel_job are deliberately not split into separate
plan_*/run_* tool pairs per job kind — they are single tools with a
confirm flag, because the underlying command construction
(OCDocker.Workbench.Jobs.JobManager.plan()) is generic across all four
job kinds and duplicating it per kind would only add tools without adding
precision. The pattern is:
The LLM calls
plan_job(orrun_job/cancel_jobwithconfirmomitted) and shows the resulting command/target to the user.Only after the user explicitly agrees to that specific action does the LLM call
run_job/cancel_jobagain withconfirm=True.
This mirrors the “ask before hard-to-reverse, resource-consuming actions” principle: an LLM cannot launch a multi-hour docking or training job, or cancel one, from a single ambiguous instruction — a job is only ever launched or cancelled from an explicit, second, confirmed tool call.
Authentication¶
Read/plan tools require no authentication (they map to the Workbench API’s
unauthenticated read-only endpoints). run_job and cancel_job with
confirm=True attach Authorization: Bearer <token>, where the token is
resolved by OCDocker.Workbench.Auth.resolve_workbench_job_token() — the
same function, and therefore the same token, that
ocdocker workbench serve itself uses to gate its execute endpoints (see
OCDocker.Workbench.Auth module). Resolution order:
the
OCDOCKER_WORKBENCH_TOKENenvironment variable, if set;the token file at
~/.config/ocdocker/workbench_token, auto-generated (mode 600) on first use by either the MCP server or the Workbench API, whichever runs first.
Because both processes resolve the token identically, there is nothing to
configure by hand as long as they run as the same user on the same machine.
Confirm-gated calls made with confirm=False never resolve or need a
token at all — only the actual execute request does.
Error handling¶
Every tool call that fails — a network error reaching the Workbench API, or
an HTTP error status from it — raises
OCDocker.MCP.Server.OCDockerMCPError with a message of the form
"{method} {path} failed ({status_code}): {detail}" (or, for a
connection failure, "Could not reach Workbench API at {url}: {error}").
Tool functions do not catch this themselves; the mcp SDK’s protocol
handler catches it at the MCP request-dispatch layer and turns it into a
standard MCP tool-error result (isError: true with the message as the
content), which the calling LLM sees as a normal, recoverable tool failure —
not a server crash. This was verified end-to-end over the real stdio
transport, not just at the Python-call level.
Submodules¶
Module contents¶
MCP server package exposing the OCDocker Workbench API to LLM clients.
- exception OCDocker.MCP.OCDockerMCPError[source]¶
Bases:
ExceptionRaised when a Workbench API call made from an MCP tool fails.