Conversation
Step 0 checked a bare `mcpgen` on PATH, which is absent in a uv-managed project, and every command block below it repeated that bare name. Step 0 now probes `mcpgen`, `uv run mcpgen`, and `.venv/bin/mcpgen`, keeps the first that both answers and clears the version floor, and every runnable command block writes `<mcpgen>` for the agent to substitute. Raises the floor to 0.3.0, where a string-valued discriminator renders: below it the overload renderer coerces every variant key with `int()`, so step 4 option 1 dies in step 5 after the whole probe sweep is paid for.
The check told the agent to run `json.loads()` on a raw probe payload. On prose that raises, and a bare `json.loads()` in a shell snippet exits non-zero, so an expected negative result was recorded as a tool error. The check is now a guarded snippet that prints `JSON_UNWRAP` or `NOT_JSON` and states that `NOT_JSON` is an expected outcome. Also splits the unwrap case in two. `_dig` / `_dig_list` are emitted only for a tool with a non-empty `unwrap`, so a parsed object that *is* the record has no key path: `unwrap` stays empty and `return_model` stays null rather than claiming a dict the wrapper never returns.
Step 7 asked the user whether to generate a runner and told a subagent that could not reach `AskUserQuestion` to skip the step, so the runner was never generated in a non-interactive run. Generation adds one new file, so it is now the default and the question is the exception. Names the runner floor (0.7.0, above step 0's) and the two things the runner skill cannot re-derive: the read-only tool set, whose keyword list is narrower than step 2b's, and the resolved `mcpgen` invocation, since the runner skill gates on a bare `mcpgen`.
Nothing said that probes of distinct tools are independent, so agents spent one turn per tool. Distinct tools write distinct part files, so every probe now goes out in a single shell invocation, with `set -e` for fail-fast and an accumulator variant for collect-everything runs. Bounds the parallelism the execution model promised: batch subagents are a local `stdio` optimization, because parallel agents cannot hold the hosted probe interval. Moves recon and batch dispatch behind the ignore preflight, since both make live calls, and states the prune-before-sizing order — the execution model picks its shape from the selected count.
Step 2's discriminator step read a missing advisory as "no candidates", which is also what an unread stderr looks like. It now states the four conditions a candidate must meet — same name, same case, a top-level scalar `type`, and absence from the engine's own denylist — so a genuinely absent advisory is distinguishable from an unread one. Pass 1 no longer re-checks what `list` already drops, and breadth is no longer a disqualifier. Pass 2 confirms by comparing response shapes across three values rather than looking for the parameter in the response, since a server can switch shape on an argument it never echoes back. An inconclusive Pass 2 leaves the tool polymorphic-suspect for step 4 rather than clearing it.
Three shell hazards had no rule. Piping a command that talks to a server discards the artifact on stdout and hands back the pipe's exit status, so a failed run reads as a success; `2>&1` does not fix it, because the bridge and the launchers write their own noise there. Two `call`s chained in one invocation report only the last status and can overwrite one `--out`. A `timeout` binary is not installed on macOS, so the wrapped command dies with 127 and never starts. Also writes the transport tails out in full: an unquoted variable does not word-split in zsh, and a server name can hold spaces or globs.
A bot-protection interstitial was read as a permanent auth failure and the tool was abandoned. The two classes are now separated by what the exception looks like, since `probe` lets httpx and SDK errors raise raw and the body never reaches the agent: 403, any 5xx, `Connection closed`, and a post-handshake stall are challenges and retry on a 60/120 s backoff; a 429 on the tool call and any credential-naming `[mcpgen] error:` line are auth or quota and do not retry. Once one tool exhausts both retries on a marker, that marker stops being read as a challenge. Failure recording splits by whether a payload came back at all — a call that never returned has no shape, and `_observed_shape: "str"` must not be invented for it. Hosted endpoints get a >= 2 s probe interval, widened to ~10 s after a challenge, and the raw payload is captured with `call --out` before any failure is classified, because `probe` keeps structure and drops the words the phrase list matches on.
The keyword fallback missed `add`, `append`, `insert`, `upsert`, `push`, `close`, and `revoke`, and matched substrings against the description as well as the name, so `list_recently_created_items` flagged while `append_row` did not. The test is now defined once, matches whole words in the name only, and carries a single judgment call: a leading read verb exempts a matched noun (`list_add_ons`) but not a second action (`get_or_create_entity`). A `readOnlyHint: true` is still trusted, but two free checks can dispute it — self-contradicting annotations, and a name that passes the keyword test. Every verdict reached without a `readOnlyHint: false` is now recorded: `_mutating_suspect` / `_mutating_reason` in the shape-spec for a probed tool, added after `mcpgen merge` because merge replaces the entry wholesale, or under `## mutating-skipped` in `session-overview.md` for a skipped one.
`npx` writes its install summary to stdout, the stream that carries JSON-RPC frames, so the client logs `Failed to parse JSONRPC message from server` with a full pydantic traceback for each line. Agents recorded those as probe failures. They come from the SDK's own logger, the error is discarded by a handler, and the handshake completes on the first valid frame — so where a part file landed, they are noise. `uv` writes its progress to stderr and causes no parse error. The same lines mean something else twice: a server that logs to stdout repeats them on every call, and a mis-launched server never frames at all. A stdio server that has never framed is not a challenge — no backoff repairs a wrong launch command — while one that framed earlier and then died keeps the challenge routing.
Step 2 was written as an interactive gate with a subagent fallback in a blockquote, so a non-interactive run read the step as inapplicable and dropped it. The default path is now 2c — every non-mutating tool, pruned on the transport — and the question is 2d, an exception taken only on the main thread of a session a user opened directly. `AskUserQuestion` being available is not the test: it sits in the roster on a headless run too, where nothing answers it, so an unanswered question falls back to 2c. Every remaining interactive gate is keyed to that same condition — the >20-variant cap, the sample-id ask, and step 4's option-2 fallback — and each names what to do when it is not met.
Claims the file made about the engine that the code does not support, found while checking the batch above against `mcpgen`: - `codegen --out` does not create parent directories, so a folder target needs `mkdir -p` first. - Only the config form forwards a server's `env` block; direct `--stdio` needs `--env` per variable. - The verify sidecar is named from `<shapes-path>` with `.shapes.json` stripped, not from `<server>`, and a re-probe with empty args does not overwrite its entry. - `--shapes` disables the in-memory parts fallback and takes the file as given, so auto-detection is the only path that tolerates a skipped merge — and it looks for a name built from the server, which a URL-form server argument does not produce. - `merge` widens concrete conflicts to `Any | None` when a null was seen. - `_dig_list` falls back to the last path key at top level. - An empty-list field is omitted from the skeleton and has to be added by hand as the one allowed non-scalar in `fields`. - Media blocks surface as metadata dicts, not as `"str"`. - A required arg that references an existing object needs a real value; only free-text args may be invented. - Enum members render as `Literal` only when scalar. Adds the ignore-rule preflight that has to pass before the first live call, since probing writes ids and PII into three artifacts and `mcpgen` installs no ignore rules.
Line-budget pass over the whole file, measured after the edits above. Cuts the deferred `--check` guard, which describes a mode that does not exist yet, and the enum-`Literal` guard, now stated where enums are chosen in step 3. Compresses the intro, the over-modelling guard, and the single-variant guard without changing what any of them require.
SKILL.md had grown to 1035 lines (69.6 KB) across 89 commits of appended fixes, all of it loaded into context on every trigger. Move the material that only applies on a conditional path into references/, keeping the procedure spine and the rules that measurably change output inline. Per-run context drops 69.6 KB -> 42.5 KB (-39%). Every rule is preserved, and the frontmatter is byte-identical, so triggering behaviour is unchanged. references/probe-failures.md error taxonomy; read only on a failure references/mutating-tools.md step 2b safe-to-probe classification references/shape-spec.md step 4 field reference and scrub rules references/subagent-execution.md fan-out for >4 tools on the main thread Discriminator detection (step 2.e) and the media / JSON-in-string rules stay inline. The 27-server eval baseline shows those are the only additions since the 516-line version that changed generated output: Mode D overloads became reachable, and three tools are now deliberately left untyped rather than modelled from a payload the probe never saw. The failure taxonomy, by contrast, fired on none of the 27 passing runs. Also consolidate the shell rules that were restated in four separate places into one block, and reduce the four Guards that merely restated inline rules to pointers. Bump plugin 0.2.0 -> 0.2.1; skill-only change, engine unchanged at 0.9.0.dev1 and both version floors (0.3.0, 0.7.0) stay put. Claude-Session: https://claude.ai/code/session_01WLrTJYE9Rz6oRBtHhMYM7L
Two passes over what the split left behind. The engine floor was stated in three places, once per requirement; it collapses to the single 0.3.0 line, since that is the only number an agent acts on. Drops the engine-internals prose that survived into the core file — how `merge` deep-merges, what `_atomic_write_text` is for, why the SDK's logger reaches stderr — none of which carries an instruction the agent can act on. What the agent must do about each is stated where it acts.
The advisory only fires for a parameter two or more tools share, so a single-tool discriminator — the common case on a server that exposes one polymorphic search — was never a candidate and its tool was typed from one variant. Step 2.e now scans the schemas directly for a scalar `enum` param whose name reads as a type switch, and treats what it finds as a candidate the advisory did not report. States both preconditions on step 4's option 1: the variants must be enumerable and probed, and the discriminator must be a scalar the overload renderer can pin to a `Literal`. A candidate failing either goes to option 2.
_render_overloaded checked `pname == disc` before consulting the schema's required set, so the discriminator was always emitted without a default and always forwarded in the request body. A parameter the server treats as optional became mandatory on every call, and the wrapper sent it whether or not the caller had chosen a value. The failure was silent. Overload parameters are keyword-only, so Python accepts a non-defaulted parameter after defaulted ones; the module imported, ast.parse passed, and the generated signature simply rejected calls that had worked before. An optional discriminator now renders an extra overload for the omission case. It returns the variant union rather than a specific model because the schema cannot say which variant the server picks when the argument is absent -- guessing one would restate the same false precision the discriminator support exists to avoid. The impl takes `| None = None` and forwards the value only when supplied. A required discriminator renders byte-identically to before. test_discriminator_always_required_in_body asserted the old behaviour directly, down to its name. It is replaced by a pair covering both directions: optional keeps its default and its None-check, required keeps neither. Claude-Session: https://claude.ai/code/session_01WLrTJYE9Rz6oRBtHhMYM7L
Minor rather than patch: 0.2.1 covered the reference split, which was output-neutral by design and measured that way. What followed changed what the skill produces. Step 2.e now sweeps tool descriptions for discriminators the list advisory cannot report, and step 4 states that option 1 needs a scalar discriminator. Validated across the full 27-server corpus at skill v0.0.4-161-gbf232ef: 23 pass, 4 partial, 0 fail, no mixed versions. Every partial is probe_inconclusive -- tools with no observable success payload -- not a skill defect. Five artifacts changed against the previous run and all five are improvements: github found three real discriminators (one of them on a parameter the engine denylist drops), aws-knowledge and semgrep gained typing, linear's oauth skip is classified correctly, and filesystem shed a return type that was false -- directory_tree annotated list[DirectoryNode] while returning a JSON string. Catalog top-level version in marketplace.json is untouched; it tracks the listing, not the plugin. The skill's engine floor stays 0.7.0, below the published 0.8.0, so the tag promises a version users can install. Claude-Session: https://claude.ai/code/session_01WLrTJYE9Rz6oRBtHhMYM7L
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.
Engine release 0.9.0 (bundles plugin v0.3.0, already bumped on dev).
Engine
_render_overloadednow renders an omission overload and forwards the argument only when supplied.Plugin (v0.3.0)
generate-mcp-wrappersSKILL.md split into core + references (mutating-tools, probe-failures, shape-spec, subagent-execution).Skill version floors stay at
mcpgen >= 0.7.0— no new CLI feature required.Gates: ruff check/format, mypy, 676 tests all pass locally.
After merge: tag
v0.9.0(fires PyPI publish) thenplugin-v0.3.0on main.