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 :mod:`OCDocker.Workbench.Server` for
the underlying API and :mod:`OCDocker.Workbench.Jobs` for job execution.
.. note::
Nearly all of the actual tool implementations are Python closures defined
*inside* :func:`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.
.. code-block:: bash
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``
(:data:`OCDocker.MCP.Server.DEFAULT_WORKBENCH_API_URL`, derived from
:data:`OCDocker.Workbench.Server.DEFAULT_WORKBENCH_API_HOST` /
:data:`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``):
.. code-block:: 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`` (:data:`OCDocker.MCP.Server.MCP_SERVER_NAME`) and
advertises this instruction string
(:data:`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
:class:`OCDocker.MCP.Server.OCDockerMCPError` (see `Error handling`_ below).
.. list-table:: Read and plan tools (no confirmation, no bearer token)
:header-rows: 1
:widths: 22 46 32
* - Tool
- Parameters
- Workbench API endpoint
* - ``get_health``
- *(none)*
- ``GET /health``
* - ``get_workspace``
- *(none)*
- ``GET /api/ocscore-workspace``
* - ``get_ablation_design_context``
- *(none)*
- ``GET /api/ablation-design``
* - ``preview_ablation_design``
- ``policy: dict``
- ``POST /api/ablation-design/preview``
* - ``plan_ablation_design``
- ``policy: dict``
- ``POST /api/ablation-design/plan``
* - ``get_vs_design_context``
- ``input_dir: str | None``
- ``GET /api/vs-design``
* - ``preview_vs_design``
- ``draft: dict``
- ``POST /api/vs-design/preview``
* - ``plan_vs_design``
- ``draft: dict``
- ``POST /api/vs-design/plan``
* - ``get_vs_campaign_context``
- ``input_dir: str | None``
- ``GET /api/vs-campaign``
* - ``preview_vs_campaign``
- ``manifest: list[dict]``
- ``POST /api/vs-campaign/preview``
* - ``plan_vs_campaign``
- ``manifest: list[dict]``, ``outdir: str | None``
- ``POST /api/vs-campaign/plan``
* - ``get_protocol_similarity``
- ``metric: str | None``, ``reference: str | None``
- ``GET /api/ablation-protocol-similarity``
* - ``list_jobs``
- *(none)*
- ``GET /api/jobs``
* - ``get_job``
- ``job_id: str``
- ``GET /api/jobs/{job_id}``
* - ``get_job_logs``
- ``job_id: str``, ``lines: int = 80``
- ``GET /api/jobs/{job_id}/logs``
* - ``get_campaign_progress``
- ``job_id: str``
- ``GET /api/jobs/{job_id}/campaign-progress``
* - ``plan_job``
- ``kind: WorkbenchJobKind``, ``args: list[str] | None``, ``cwd: str | None``, ``manifest: list[dict] | None``, ``engine: str = "shell"``, ``cores: int = 4``, ``results_dir: str | None``
- ``POST /api/jobs/plan``
.. list-table:: Execute tools (confirmation-gated, bearer-token-authenticated)
:header-rows: 1
:widths: 22 46 32
* - Tool
- Parameters
- Workbench API endpoint
* - ``run_job``
- ``kind: WorkbenchJobKind``, ``args: list[str] | None``, ``cwd: str | None``, ``manifest: list[dict] | None``, ``engine: str = "shell"``, ``cores: int = 4``, ``results_dir: str | None``, ``confirm: bool = False``
- ``POST /api/jobs/plan`` (confirm=False) or ``POST /api/jobs`` (confirm=True)
* - ``cancel_job``
- ``job_id: str``, ``confirm: bool = False``
- ``GET /api/jobs/{job_id}`` (confirm=False) or ``POST /api/jobs/{job_id}/cancel`` (confirm=True)
``WorkbenchJobKind`` is the literal type ``"vs" | "pipeline" | "ocscore_train" |
"ocscore_reduce" | "vs_campaign"`` (:data:`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_model`` status. Returns the same payload as
``GET /health`` on 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``
(:class:`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. ``policy`` is a JSON object matching
the Workbench Design tab's request body: a ``policy`` block
(``include_features``, ``include_patterns``, ``exclude_features``,
``exclude_patterns``, ``allow_missing_excludes``, ...) plus optional
identity/source fields (``name``, ``description``, ``feature_source``).
See :func:`OCDocker.Workbench.AblationDesign.preview_ablation_design` for
the exact body contract.
``plan_ablation_design(policy)``
Generate the policy YAML text and the ``ocdocker ocscore train`` command
string for a draft ablation. Same ``policy`` body shape as
``preview_ablation_design``. This only plans — it does not write the
policy file or launch training; use ``run_job`` with
``kind="ocscore_train"`` for that once the design is finalized. See
:func:`OCDocker.Workbench.AblationDesign.plan_ablation_design`.
``get_vs_design_context(input_dir=None)``
Discover receptor/ligand/box candidates for designing a single-target
``vs``/``pipeline`` run. Best-effort, depth-limited scan by
filename/extension heuristics (receptor: ``.pdb``/``.pdbqt`` with
"receptor" in the name; ligand: ``.smi``/``.sdf``/``.mol2``/``.pdbqt``
with "ligand" in the name or parent directory; box: ``.pdb``/``.txt``
starting with "box") — there is no single fixed input layout in
OCDocker, so results are candidates to choose from, never a
requirement. ``input_dir`` optionally narrows the scan to one
subdirectory of the served root instead of the whole workspace. Same
payload as ``GET /api/vs-design``. See
:func:`OCDocker.Workbench.VSDesign.discover_vs_design_candidates`.
``preview_vs_design(draft)``
Validate one draft single-target VS design without running anything.
``draft`` is a JSON object with ``kind`` (``"vs"`` or ``"pipeline"``),
``receptor``, ``ligand``, ``box`` (paths — absolute, or relative to the
served root), plus kind-specific fields: ``engine`` for ``"vs"``, or
``engines``/``rescoring_engines`` (lists) for ``"pipeline"``. Checks
path existence, extension sanity, and engine names against
:data:`OCDocker.Workbench.Models.VALID_DOCKING_ENGINES`/
:data:`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 as
``POST /api/vs-design/preview``. See
:func:`OCDocker.Workbench.VSDesign.preview_vs_design`.
``plan_vs_design(draft)``
Build the exact ``ocdocker vs``/``pipeline`` argv for a valid draft
design. Same ``draft`` body shape as ``preview_vs_design`` — call that
first and show the user any errors/warnings. On success returns
``{"kind", "args", "cwd", "shell_command"}``, ready to hand directly to
``run_job`` (as ``kind``/``args``/``cwd``) to actually launch it,
subject to the same ``confirm=True`` gate as every other execute tool.
Raises :class:`~OCDocker.MCP.Server.OCDockerMCPError` if the draft is
invalid. Same payload as ``POST /api/vs-design/plan``. See
:func:`OCDocker.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
:func:`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 (:func:`OCDocker.Workbench.Jobs.build_campaign_script`) runs one
``ocdocker vs``/``pipeline`` invocation per row sequentially, logging
``[sample i/N] `` markers before each, **continuing past a failing
row**, and exiting non-zero only if any row failed.
- ``engine="snakemake"`` (requires the ``mcp``/``workflow`` extra's
Snakemake dependency): a bundled multi-sample Snakefile
(:func:`OCDocker.Workbench.Jobs.build_campaign_snakemake_command`,
``OCDocker/Workbench/Snakefiles/vs_campaign.smk``) runs every row through
real Snakemake DAG orchestration — parallel via ``cores``, 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 ``/`` (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 by
``examples/19_Snakefile_ocdocker_pipeline.smk`` and
``examples/20_Snakefile_ocdocker_granular_pipeline.smk``. When
``input_dir`` is omitted, ``/input`` is 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 for
``preview_vs_campaign``. Same payload as ``GET /api/vs-campaign``. See
:func:`OCDocker.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 as
``POST /api/vs-campaign/preview``. See
:func:`OCDocker.Workbench.VSDesign.preview_vs_campaign`.
``plan_vs_campaign(manifest, outdir=None)``
Build the ``vs_campaign`` job payload for a valid manifest — call
``preview_vs_campaign`` first and show the user any errors/warnings,
especially for large manifests. Accepts the same ``engine``/``cores``
settings described above (via the request body's ``engine``/``cores``
fields; not yet exposed as direct MCP tool parameters — pass them through
``run_job``/``plan_job`` instead when actually launching). On success
returns ``{"kind": "vs_campaign", "engine", "manifest", "args",
"results_dir", "cwd", "shell_command"}`` (plus ``"cores"`` for
``engine="snakemake"``), ready to hand directly to ``run_job`` (as
``kind``/``manifest``/``args``/``engine``/``cores``/``results_dir``/``cwd``)
to launch the whole batch. Raises
:class:`~OCDocker.MCP.Server.OCDockerMCPError` if the manifest is
invalid or ``engine`` is unrecognized. Same payload as
``POST /api/vs-campaign/plan``. See
:func:`OCDocker.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 to ``engine: "unknown"`` for a non-``vs_campaign`` job 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 (see
:func:`OCDocker.Workbench.CampaignProgress.parse_campaign_progress`).
Same payload as ``GET /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. ``metric`` selects which metric to overlay on clusters; ``reference``
selects the protocol used for the added/removed feature diff (defaults to
``full_ocscore`` server-side when omitted). Same payload as
``GET /api/ablation-protocol-similarity``.
``list_jobs()``
List every tracked Workbench job — every ``vs``/``pipeline``/
``ocscore_train``/``ocscore_reduce`` run ever launched through this
Workbench API instance, most recent first. Returns ``{"jobs": [...]}``
where each entry is a
:class:`OCDocker.Workbench.Models.WorkbenchJobRecord`.
``get_job(job_id)``
Return one tracked job's current status (``defined`` / ``running`` /
``completed`` / ``failed`` / ``cancelled``), command, PID, and exit code.
Raises :class:`~OCDocker.MCP.Server.OCDockerMCPError` if ``job_id`` is
unknown.
``get_job_logs(job_id, lines=80)``
Return a bounded tail of a job's stdout and stderr
(``{"stdout": {...}, "stderr": {...}}``, each a
:class:`OCDocker.Workbench.Models.RunLogFilePreview`). ``lines`` caps 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 (or
``run_job`` with ``confirm=False``, which does the same thing) before
``run_job(..., confirm=True)`` and show the resulting command to the user.
``manifest`` is required for ``kind="vs_campaign"`` (see `Batch
campaigns`_) and ignored for every other kind. ``engine``, ``cores``, and
``results_dir`` are also only meaningful for ``kind="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. ``manifest`` is required for ``kind="vs_campaign"`` (build
one with ``plan_vs_campaign`` first) and ignored otherwise. ``engine``
(``"shell"`` or ``"snakemake"``), ``cores`` (Snakemake ``--cores``), and
``results_dir`` (shared base output directory, per-sample-nested) are
also only meaningful for ``kind="vs_campaign"`` — see `Batch campaigns`_.
- ``confirm=False`` (the default): **launches nothing.** Returns
``{"launched": false, "message": "...", "plan": {...}}`` — identical to
calling ``plan_job`` — so the caller can inspect and show the command
before committing to it.
- ``confirm=True``: actually calls ``POST /api/jobs`` with the Workbench
job bearer token attached, and returns
``{"launched": true, "job": {...}}`` where ``job`` is the tracked
:class:`~OCDocker.Workbench.Models.WorkbenchJobRecord` (status
``"running"``, PID, log paths).
Only pass ``confirm=True`` after 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 to
``SIGKILL`` after 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 calls ``POST /api/jobs/{job_id}/cancel``
with 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
(:meth:`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:
1. The LLM calls ``plan_job`` (or ``run_job``/``cancel_job`` with
``confirm`` omitted) and shows the resulting command/target to the user.
2. Only after the user explicitly agrees to that specific action does the
LLM call ``run_job``/``cancel_job`` again with ``confirm=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 ``, where the token is
resolved by :func:`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
:doc:`OCDocker.Workbench.Auth`). Resolution order:
1. the ``OCDOCKER_WORKBENCH_TOKEN`` environment variable, if set;
2. 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
:class:`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
----------
.. toctree::
:maxdepth: 4
OCDocker.MCP.Server
Module contents
---------------
.. automodule:: OCDocker.MCP
:members:
:undoc-members:
:show-inheritance: