Conversation
list_containers, list_images and list_volumes returned the full
inspect-level shape for every object, which is unusable at fleet
scale:
* 'state' embeds State.Health.Log — Docker keeps the last 5
healthcheck outputs and never bounds their size. A healthcheck that
curls a web page (or prints a JSON status dump) adds tens of KB per
container, per listing.
* the nested 'image' object repeats repo tags, digests and OCI labels
for every container row.
* 'mounts' carries full mount definitions (source paths, drivers,
propagation) for every row.
Observed on a 22-container host: a single list_containers call
returned ~103 KB, ~90% of it health-check log output.
Add a format parameter ('summary' | 'full', default 'summary') to the
three list tools. Summary returns a docker-ps-style row:
name, short_id, status,
state.{Status,Running,Restarting,Dead,ExitCode,StartedAt},
image (string ref, no nested object),
mounts trimmed to name+type,
health.{Status,FailingStreak} (no Log)
plus a slim image/volume row (no labels/digests). Full keeps the
existing inspect shape for debugging, and caps every Health.Log
output entry at 500 characters in both modes, so even a full listing
is bounded regardless of what a healthcheck prints.
Single-object tools (run_container, stop_container, ...) are
unchanged: docker_to_dict defaults to format='full'.
Example output (real 22-container host, before -> after):
list_containers 102,965 chars -> 11,048 chars (89% smaller)
webapp-a row 24,013 chars -> 326 chars
webapp-b row 19,629 chars -> 323 chars
app-c row 11,201 chars -> 424 chars
Verified against a live Docker daemon over ssh: 40 non-empty
health-check outputs in full mode, 0 exceeding the cap.
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
list_containers,list_images, andlist_volumesreturned the fullinspect-level shape for every object. That's fine for a single object, but
unusable for a listing: the payload is dominated by data a
docker ps-styleanswer never needs, and it's unbounded — it grows with what each
container's healthcheck prints.
On a 22-container host, a single
list_containerscall returned ~103 KB,roughly 90% of which was health-check log output. For an LLM-facing tool, that
is the classic "inspect-everything" antipattern: the server can't know what the
model will need, so it should default to a minimal view and let the caller opt
into detail.
Root cause
docker_to_dict()(shared by every tool) embeds, for each container:state= the raw inspectState, which includesHealth.Log— Docker retains the last 5 healthcheck outputs and neverbounds their size. A healthcheck that
curls a web page (or prints a JSONstatus dump) therefore adds tens of KB per container, per listing.
image= a nesteddocker_to_dict(image)— repo tags, digests and OCIlabels repeated for every row.
mounts= full mount definitions (source paths, driver, propagation).remote-shell-mcp(a different Docker MCP) solved this by making list andinspect separate verbs with a fixed 6-field row type. This PR does the same
thing within a single
formatparameter.What this changes
Adds a
formatparameter —"summary"|"full", default"summary"—to the three list tools.
Summary returns a compact
docker ps-style row:plus a slim image/volume row (no labels/digests).
Full keeps the existing inspect shape for debugging. In both modes,
every
Health.Logoutput entry is capped at 500 characters, so even a fulllisting is bounded regardless of what a healthcheck prints.
Single-object tools (
run_container,stop_container, …) are unchanged:docker_to_dict()still defaults toformat="full".Behavior change (please read)
The default for the three list tools flips from inspect-level output to
the compact shape. Any existing client that parses the old fields from a
list_*call will need to passformat: "full"(or read the summary fields).This is the intended fix — the old default was the bug — but it is a breaking
change for anyone scripting against the old list output.
Example output (real 22-container host, before → after)
list_containers(all 22)list_images(27)list_volumes(13)A summary row:
{ "name": "webapp-a", "short_id": "11aef40ef8cb", "status": "running", "state": { "Status": "running", "Running": true, "Restarting": false, "Dead": false, "ExitCode": 0, "StartedAt": "2026-09-15T15:55:05Z" }, "image": "exampleorg/webapp-a:latest", "mounts": [], "health": { "Status": "healthy", "FailingStreak": 0 } }(In
fullmode,Health.Logentries longer than 500 chars are truncated witha
… [truncated]marker.)Test plan
uv run pytest— 18 passed. One pre-existing test(
test_production_stdio_command_lists_tools) spawns the real server andrequires a live Docker daemon; it fails identically on unmodified
mainon adaemon-less host (verified by stashing this change and re-running) and is
unrelated to this diff.
tests/test_output_schemas.py(13 tests) exercises the realdocker_to_dict— summary shape, full shape, the 500-char health-log cap,the image-ref fallback, overrides, networks — with faithful docker-py models,
plus two end-to-end MCP calls proving
list_containersdefaults to thecompact shape and
run_containerstill returns the full object.uv run ruff checkanduv run ruff format --check(the repo's devbox lintcommands): clean.
ssh://to a 22-containerhost):
list_containersdefault is 11,048 chars vs 102,965 full (89%smaller); 40 non-empty health-check outputs in full mode, 0 exceeding the
cap; the
formatparam is present onlist_containersand absent fromrun_container.Files
src/mcp_server_docker/output_schemas.py— splitdocker_to_dictintosummary/full builders; added
formatparam and the health-log cap.src/mcp_server_docker/server.py— addedformatto the three list tools.tests/test_output_schemas.py— new direct tests of the real serializer.tests/test_server.py— updated thedocker_to_dictmock to accept the newformatkwarg.