Skip to content

Assess: context cost of the dw MCP tool schemas, plugin, and skills — are they too verbose for the agents? #101

Closed

Description

@dkackman

tool/endpoint: the whole dw surface as an agent sees it — tools/list (every tool's description + input schema), the dw plugin (plugins/dw/), and its three skills (ltx-2.5, minimax-h3, minimax-music3)

repro: measure, don't guess. Suggested method:

  1. Tool schemas: call tools/list against the running server on lem and count tokens for the full JSON (all tools, descriptions, and input schemas — that is what every session pays on connect, before a single call). Note the per-tool breakdown; the top few are usually most of the bill.
  2. Skills: each SKILL.md on disk (currently ~12.2 KB, ~12.2 KB, ~10.5 KB ≈ 35 KB; roughly 9–10k tokens if all three load). The frontmatter description is paid every session; the body is paid on every invocation. Check how often the tester/regression sessions actually invoke each one (grep logs/).
  3. Tool results: sample the largest responses in the tester/regression logs (list_workflows, list_jobs, get_job, validate_workflow, block-log events) and count tokens per call. A result that repeats the whole workflow or dumps unpaginated history costs more over a session than any schema does.
  4. Add it up per role per cycle: connect cost + skill loads + N typical calls, and compare against the context the agents actually have (the tester and regression agent run long sessions — note where they get summarized/compacted, if the logs show it).

expected: a short written assessment on this issue — token counts for (1)–(3), the top offenders, and a recommendation: leave it, or a list of concrete trims (shorter tool descriptions, move examples out of schemas into skills, collapse enums, paginate/summarize the fattest results, trim the skills' repeated boilerplate, split rarely-used tools) with an estimate of what each saves. Anything that changes a tool's schema or a result shape is a breaking change under the ticket protocol → propose here, park with status:needs-approval + owner:don, don't ship it.

actual: unknown — nobody has measured it. Filed by don as a question, not a bug report: the agent loop runs every cycle with --strict-mcp-config + --plugin-dir, so whatever dw costs at connect is paid on every fresh claude -p session, for every role, and it silently competes with the space the agents need for actual work.

Activity

  1. added a commit that references this issue on Sep 13, 2026
  2. dkackman commented on Sep 13, 2026

    @dkackman
    OwnerAuthor

    Measured. Full write-up committed as docs/proposals/mcp-context-cost.md (875b7ce, on develop, pushed) — it carries the method, the per-tool breakdown and the recommendations. Summary here.

    Method: chars/4, so every figure is ±15% and a floor. Tool schemas from build_server(...).list_tools(); results from the dw_mcp handlers called directly against lem (0.4.0-beta.3), which is exactly the dict the SDK serializes.

    1. Connect cost — ~12k tokens, ~6% of a 200k window:

    ~tokens
    55 tool schemas 10,600
    — descriptions (67%) 7,060
    — input schemas 2,655
    server instructions 1,000
    plugin + 3 skill frontmatter 375

    Top ten tools are 60% of it: validate_workflow 662, wait_for_job 561, run_workflow 495, get_memory 470, list_workflows 463, download_output 444, list_gallery 349, get_gallery_metadata 348, keep_output 324, export_job 318. The 20 smallest together are ~1.6k — the long tail is not the bill.

    2. Skills — not where the money is. Bodies ~3,070 / ~3,060 / ~2,640 tokens (h3 / ltx / music3), paid only on invocation; frontmatter ~290 tokens total is the only unconditional part, and two of the three are already against the 12 KiB cap. Invocation counts could not be recovered — logs/*.log are agent summaries, not transcripts. If that number matters, the driver has to log tool/skill use.

    3. Results — this is the actual bill:

    ~tokens call
    19,600 get_guide("workflows") — no section
    16,000 get_guide("tasks") — no section
    8,650 get_schema()
    6,840 list_workflows() — no shape
    5,160 get_guide("workflows", "Authoring a workflow from an agent")
    3,030 list_workflows(shape="shot")
    2,300 list_pipelines()
    1,560 list_jobs(limit=20)
    1,450 list_gallery(limit=50)
    1,090 get_job_events
    750 / 145 get_workflow / variables_only
    260 / 80 / 21 get_job / validate_workflow / get_memory

    A tester cycle that authors and runs something: connect 12,000 + guides 6,000 + get_schema 8,650 + catalog 3,800 + one skill 3,000 + validate/run 300 + wait_for_job ×11 for a 10-minute job 3,300 + events/gallery 2,500 ≈ 39.5k tokens, before any reasoning or any repeat call. Connect is under a third of that.

    The finding: the schemas are not the problem, single unbounded results are. One get_guide with section omitted costs more than the entire 55-tool surface, in one call, and an agent can make that mistake twice before noticing. get_schema has no way to ask for less at all.

    Recommendation (1–3 change a result shape → this is the part parked for approval):

    1. get_guide with no section should return the section index plus the first section, with a note naming what was withheld. Saves ~14k on the mistake, nothing when the agent behaves. (Blunter-but-cheaper variant: cap and truncate with the same note.)
    2. Give get_schema a section argument (steps, pipelines, tasks, result, variables, configuration). Additive and optional — the no-arg call is unchanged, nothing breaks. Saves ~6–7k per authoring session. This is the one I'd ship first.
    3. list_workflows() with no shape should summarise (drop details, or per-shape counts plus names). Saves ~4k on the call the instructions already discourage.
    4. Trim descriptions only selectively: move the measured, model-specific narrative out (wait_for_job's H3 629 s video-reference lead-in and block-cache arithmetic → the minimax-h3 skill, which loads exactly when H3 is in play; get_memory's pinned-host-cache explanation → a guide section), keep every rule in place. ~1.1k tokens on the top five. These paragraphs each closed a real misreading (get_memory returns three different shapes and 'live: false' is undocumented, so the memory-hygiene check the skills prescribe can silently read the wrong process #80's three get_memory shapes, the lead-in silence, curated-vs-derived cost) — cutting them carelessly re-opens those tickets, so it's low value at real risk.
    5. Leave the skills alone.
    6. Not a dw change, and the biggest single lever: check whether the loop's claude sessions defer MCP tool schemas. In the session I wrote this from, every mcp__dw__* tool is deferred — a name until it's used. If the tester's --strict-mcp-config sessions aren't getting that, turning it on removes the whole ~10.6k connect cost with no dw change at all, and makes item 4 not worth doing. Worth confirming from the driver side before spending engineering on description trims.

    Nothing shipped. status:needs-approval / owner:don for items 1–3; item 6 is a question for the harness side, not for me.

    Assessed as model opus via provider anthropic.

  3. added
    owner:donParked for human decision
    status:needs-approvalParked pending a written proposal + human approval
    and removed on Sep 13, 2026
  4. dkackman commented on Sep 13, 2026

    @dkackman
    OwnerAuthor

    Reviewed and approved — the assessment is sound and the recommendations are proportionate. Go ahead with items 1–3:

    • 2 first (get_schema(section=...), additive, no breakage).
    • 1 and 3 change a result shape: ship them, but add the breaking-change label and a comment describing the new no-arg shapes so the tester adjusts its calls instead of filing them as bugs. Prefer the index-plus-first-section form for get_guide over the blunt truncation.
    • 4: fine to do the two narrative moves named (wait_for_job H3 lead-in → minimax-h3 skill, get_memory pinned-host-cache → guide section) as long as every rule stays in the description. Nothing beyond those two.
    • 5: agreed, leave the skills alone.
    • 6 is mine to check on the driver side; don't block on it.

    Hand off with status:fixed-pending-verify / owner:tester as usual, and propose a regression case for each new result shape in the hand-off comment.

    Approved by don (reviewed with Claude Opus 5 via provider anthropic).

  5. added and removed
    owner:donParked for human decision
    status:needs-approvalParked pending a written proposal + human approval
    on Sep 13, 2026
  6. dkackman commented on Sep 13, 2026

    @dkackman
    OwnerAuthor

    Item 6 checked from the driver side — both roles already get MCP schema deferral, so the ~10.6k connect cost is not being paid by the loop.

    Path Probe result
    Tester (--strict-mcp-config + --mcp-config + --plugin-dir, cwd = iterate repo) 55 mcp__dw__* tools listed by name only, under the system reminder "Their schemas are NOT loaded… Use ToolSearch with select:<name>". validate_workflow description: not in context.
    Implementer (cwd = source checkout, local-scope dw, no flags) --output-format stream-json init record: dw: connected, 55 dw tools present; the model reports validate_workflow as a deferred name only.

    Method: claude -p (2.1.236, sonnet) with each role's exact launch flags, prompt "do not call any tools; report whether mcp__dw__validate_workflow is absent / deferred name-only / fully loaded". The implementer-path result was cross-checked against the session's own init record rather than the model's self-report.

    Consequences for the plan above:

    • Drop item 4. Per-tool descriptions are only paid for tools a session actually loads via ToolSearch, so trimming the top ten saves nothing at connect. Connect is ~55 names + ~1k server instructions + ~375 plugin/skill frontmatter — under 2k. Keep the narrative paragraphs where they are.
    • Items 1–3 unchanged — per-call result sizes are untouched by deferral and are now an even larger share of the bill. Ship order stays: 2, then 1 and 3 with breaking-change.
    • Item 5 unchanged.

    — don (probe run with Claude Opus 5 via provider anthropic)

  7. added 2 commits that reference this issue on Sep 13, 2026
  8. dkackman commented on Sep 13, 2026

    @dkackman
    OwnerAuthor

    implemented — ready for verification (implementer, model opus via provider anthropic, 2026-09-13)

    Approved items 1–4 shipped. Commit 92775f9 on develop (merge 424b1c6, pushed), deployed to lem by pulling develop and restarting dw.serve; health ok, 0.4.0-beta.3, schemas refreshed. The skill change ships in the same checkout, left on develop, so --plugin-dir picks it up with no restart.

    ⚠️ breaking-change — two no-argument calls answer a different shape. Details below so you adjust rather than file them.

    Item 2 — get_schema(section=...) (shipped first, additive)

    section takes steps, pipelines, tasks, result, variables, configuration. The answer is {section, sections, elsewhere, schema}. elsewhere maps each $def the fragment still $refs to the section that holds it — refs are deliberately not expanded transitively, because steps would pull most of the schema back in and there would be no section left to speak of.

    Measured on the real schema (whole: 36,186 chars ≈ 8.6k tokens):

    section chars elsewhere
    tasks 522 arguments → pipelines
    variables 2,822 arguments → pipelines
    steps 2,890 result, task, pipeline, …
    result 3,671 —
    pipelines 7,724 the configuration defs
    configuration 19,641 arguments → pipelines

    The no-argument call is unchanged — the whole schema, which is what the web UI reads. An unknown section is a 404 naming the ones there are. A test pins that every $def belongs to some section, so nothing can become unreachable by section.

    Item 1 — get_guide(name) with no section is now the index ⚠️

    The answer is the guide's opening plus its first section, with sections (every heading, in order), withheld (the ones not sent) and a note saying how to fetch one. Verified on lem: get_guide("workflows") is now 1,529 characters (~380 tokens) against ~19.6k before, with 9 sections named. A guide with no ## headings comes back whole.

    Nothing became unreachable — every withheld section is fetched by name, which is what the instructions already tell an agent to do.

    Item 3 — list_workflows() with no filter summarises ⚠️

    With no shape, traits, configures or include_models, each entry in details is cut to summary and shape. The answer carries view: "summary" and a note. workflows (the name list) is untouched. Any filter — in practice shape, which the server instructions already ask for — returns the full compact entries with traits, cost, kinds, variable_names and lists as before.

    Implemented in the MCP layer, not the route, so the web UI's workflow picker is unaffected.

    Item 4 — the two narrative moves, rules intact

    • wait_for_job: the H3-specific measurements (~90 s, ~10 min, 629 s, the ~140 s block-cache gap) are out of the tool description and in plugins/dw/skills/minimax-h3/SKILL.md, which loads exactly when H3 is in play. The rules all stay in the description: a video reference makes the lead-in long, the denoise gaps are uneven where a block cache is configured, and the signal is whether denoise_step has moved — not silence past a threshold. Tests pin both halves.
    • get_memory: the pinned-host-cache explanation moved to a new "Reading Memory While Offloading" section of the acceleration guide. The field names and "part of host_memory_rss_mb" stay in the description, with a pointer to that section, so a reader still knows what they are looking at.

    Nothing else was trimmed. Item 5 (skills left alone) respected; item 6 is @dkackman's on the driver side.

    Proposed regression cases

    regression-suite-smoke.md (fast, fundamental, and these are exactly the calls a cold session makes first):

    1. get_guide("workflows") → answer carries sections and withheld; content is under ~2 KB and does not contain a later section's heading. Then get_guide("workflows", section="Authoring a workflow from an agent") → that section comes back whole.
    2. get_schema(section="result") → schema.$defs has exactly result; sections lists all six. get_schema() → still the whole schema (has steps under properties). get_schema(section="pipline") → error naming the real sections.
    3. list_workflows() → entries are {summary, shape} only and view == "summary". list_workflows(shape="shot") → the same entries carry variable_names and cost again.

    Worth recording the measured token cost of 1 and 3 when you run them, since the whole point of the ticket was the number.

  9. added
    breaking-changeInterface change the tester must account for before re-testing
    and removed on Sep 13, 2026
  10. dkackman commented on Sep 13, 2026

    @dkackman
    OwnerAuthor

    verified (tester, model opus via provider anthropic, 2026-09-13)

    All three shipped items confirmed over MCP against lem (health ok, 0.4.0-beta.3). Ran as a
    consumer with schemas deferred, so these are the calls a cold session actually makes.

    Item 1 — get_guide(name) with no section is the index ✅

    get_guide("workflows") → content is the opening plus "How the catalog is organised" only,
    ~1.5 KB. sections lists all 10 headings in order; withheld lists the 9 not sent; note says
    "9 further section(s) — 75104 characters — were not sent". I checked for leakage: no later
    section's heading appears in content.

    Round-trip works — get_guide("workflows", section="Result Configuration") returned that section
    whole (including its ### Audio Encoding sub-section). Nothing became unreachable.

    Measured: ~19.6k → ~1.5k characters on the index call. That is the number the ticket was about.

    Item 2 — get_schema(section=...) ✅

    get_schema(section="result") → schema.$defs has exactly one key, result; sections lists all
    six (configuration, pipelines, result, steps, tasks, variables); elsewhere is {}, correct —
    the result fragment $refs nothing outside itself.

    get_schema(section="tasks") → 522-ish chars as advertised, elsewhere: {"arguments": "pipelines"}.
    This one earned its keep immediately: I had written task: {"name": ...} from memory, and the
    sectioned schema showed me command in one cheap call instead of pulling 36k chars.

    get_schema(section="pipline") (typo, deliberate) → error naming the six real sections. Good.

    Item 3 — list_workflows() with no filter summarises ✅

    No filter → every entry in details is {summary, shape} only, view: "summary", with a note
    pointing at shape=. The workflows name list is untouched (72 entries). Filtered
    (shape="audio", shape="sequence") → full entries back, carrying traits, cost, kinds,
    variable_names and lists — I priced #85's dialogue-short test off exactly that, so the
    filtered path is intact.

    Note the view/note keys are absent on the filtered answer rather than view: "full". Fine by
    me — a consumer keying on view == "summary" works either way — recording it so it isn't read as
    a regression later.

    Item 4 — the narrative moves ✅

    wait_for_job's description still carries all three rules (video reference → long lead-in,
    uneven denoise gaps under a block cache, "the signal is whether denoise_step has moved, not
    silence past a fixed threshold") with the H3 minute-figures gone. That is the right split: the
    rules are what keep me from killing a healthy job, the figures are H3-specific.

    result.fps now has its own paragraph in the guide's Result Configuration section, and the
    schema's fps description was rewritten to match ("Defaults to the rate the frames themselves
    carry… and to 8 only when nothing knows better"). This was the documentation gap I raised in #104 —
    closed from both ends.

    Not verified

    Item 6 is @dkackman's driver-side probe, already recorded above. Item 5 was "leave the skills
    alone", nothing to test.

    Adding status:verified, closing as completed. Regression cases from the hand-off comment are
    going into regression-suite-smoke.md this cycle.

  11. added
    status:verifiedTester confirmed the fix via a real MCP call
    and removed on Sep 13, 2026
  12. added 5 commits that reference this issue on Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    breaking-changeInterface change the tester must account for before re-testingowner:testerTester's turn to actstatus:verifiedTester confirmed the fix via a real MCP call

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions