mirror of
https://github.com/svnscha/mcp-windbg.git
synced 2026-10-16 20:32:50 +08:00
main
95 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4d5c824d33 | chore(develop): release 1.5.1 (#149) v1.5.1 | ||
|
|
b2f8b36f7a | fix(cdb): open a remote session whose target is running (#145) | ||
|
|
58bf9758cc | ci: let the publish call upload the signed exe to the release (#148) | ||
|
|
4cce8b1d65 |
build: add a standalone mcp-windbg.exe build (#143)
Freezes the server into a single self-contained `mcp-windbg.exe` with
PyInstaller,
for users who want nothing to do with a Python toolchain. `cdb.exe`
stays a
prerequisite.
**What the build does.** `scripts/Build-Exe.ps1` builds it, `-Verify`
runs the
scenarios against it, and `MCP_WINDBG_SERVER_EXE` re-points the e2e
harness at a
built binary so the whole suite runs with the same assertions and a
different
launcher. A new `exe` job does both on every PR.
**Signed release asset.** The exe is signed and attached to each release
with a
`SHA256SUMS` taken from the signed binary, since signing rewrites the PE
and a
digest from before it would match nothing anyone can download. The
signature is
verified before the asset is attached.
**Identity.** Version resource generated from `pyproject.toml`, plus an
icon and a
`Comments` field stating the binary is signed by Liasoft GmbH, which
sponsors the
certificate. Windows shows the certificate subject as the verified
publisher, so
without that note the author and the signer read as two unrelated names.
**Why signing uses a self-hosted runner.** The signing service requires
one. Two
rules keep that safe on a public repository, and both must hold if the
job is ever
changed: it stays out of reach of `pull_request`, `pull_request_target`
and
`issue_comment`, which would run a fork's code on it, and it never
checks out the
repository. It declares `permissions: {}`, takes its endpoint from a
secret, and
waits for approval in a `signing` environment.
**Verified.** 29 scenarios pass against the binary (2 kernel skips, no
target).
Signing exercised end to end with a rehearsal run, which builds, signs
and
attaches while publishing nothing: `osslsigncode verify` reports
`Succeeded`,
signer `O=Liasoft GmbH`, RFC3161 countersigned.
**Not yet exercised.** Nothing launches the signed binary, so a
post-sign smoke
step is worth adding.
Out of scope: winget.
|
||
|
|
7d988361e1 | chore(develop): release 1.5.0 (#142) v1.5.0 | ||
|
|
59a2d09c6b |
docs: add the 1.5.0 changelog entry (#141)
Release-As: 1.5.0 |
||
|
|
48875bcaeb | fix(server): bound retained output and keep diagnostics off stdout (#139) | ||
|
|
95505f7240 | fix(debugger): cancel timed-out commands on dump sessions (#138) | ||
|
|
0de53835e3 | deps(python): update pymdown-extensions requirement from >=12.0.1 to >=12.1 in the python group (#132) | ||
|
|
3e31084a2a | fix(debugger): make completion markers unique per session (#137) | ||
|
|
9194e85664 | fix(server): close undisclosed sessions when an open fails (#136) | ||
|
|
bd7052b8e0 | docs: point readers to Microsoft's official WinDbg MCP (#133) | ||
|
|
3d7dbbf5cf | chore: add a script to switch between released and local mcp-windbg (#123) | ||
|
|
6b0adf6459 | chore(develop): release 1.4.0 (#122) v1.4.0 | ||
|
|
a376c7c4ce |
feat: open kernel crash dumps with open_kd_dump (#120)
Adds open_kd_dump, which opens a kernel crash dump with kd.exe -z as a kd session. Closes #111 |
||
|
|
f998e614c9 |
feat: run init commands on every new session (#121)
## What this changes
Adds a way to run debugger commands automatically when a session opens,
for example to `.load`
a debugger extension.
- `--init-command COMMAND` (repeatable), or `MCP_WINDBG_INIT_COMMANDS`
with one command per line
when no flag is given. These run on every `open_*` tool (dump, remote,
kernel), right after the
session opens and before triage. An extension loaded with `.load` is
therefore available to
`!analyze -v` and every later command.
- `--kernel-init-command COMMAND` (repeatable), or
`MCP_WINDBG_KERNEL_INIT_COMMANDS`, for commands
that only apply to kernel targets. They run after the shared commands,
on `open_kd_session`
and on dumps whose header is a kernel dump (`PAGEDUMP`/`PAGEDU64`), even
when opened with
`open_cdb_dump`. User-mode dumps (`MDMP`) and `open_cdb_remote` skip
them.
- Output from both lists leads the open result under `###
Initialization`, each command echoed
as `> command`.
- Docs: new "Init commands" section and two option rows in
`docs/reference/cli.md`; CHANGELOG
entry under Unreleased.
## Why
In-house and third-party debugger extensions (an OS-version helper,
driver-specific dumpers,
`.loadby sos clr` for .NET) have to be loaded by hand in every session,
and the model does not
know to do it. There was no hook for this: the server reads no
environment configuration, and
`additional_args` is not exposed on the CLI.
Some of these commands only make sense on a kernel target. For example,
reading the Windows
edition and LCU from the kernel just prints an error on a user-mode
minidump, which carries no
such data. So kernel-only commands get their own list. The server
decides by dump header rather
than by which tool opened the dump, because kernel dumps are often
opened with `open_cdb_dump`.
## How it was verified
Through the real MCP server over stdio (WinDbg 1.2606.22001.0),
configured as it would be in an
MCP client's `env` block:
`MCP_WINDBG_INIT_COMMANDS=".load <ext>.dll"`,
`MCP_WINDBG_KERNEL_INIT_COMMANDS="!winver"`.
| Dump | Initialization output |
|---|---|
| 66 GB complete kernel dump (`MEMORY.DMP`, `PAGEDU64`), via
`open_cdb_dump` | `.load` then `!winver`, which printed `Windows:
Windows 10 Enterprise ... build 28020, LCU 10.0.28000.2818` |
| `DemoCrash1.exe.7088.dmp` (user-mode, `MDMP`) | `.load` only;
`!winver` skipped |
On the kernel dump, the extension stayed loaded: `!winver` via
`run_cdb_command` later in the same
session worked. Section order was Initialization, then Crash
Information, then Crash Analysis.
Tests:
- New `init_commands.yaml` scenario against real cdb: shared commands
run before
Crash Information, and a `--kernel-init-command` is absent on a
user-mode dump.
- New hermetic tests in `test_init_commands.py`:
- shared commands run before triage on all three `open_*` tools
- no `Initialization` section when nothing is configured
- kernel-only commands run on `open_kd_session` and on
`PAGEDU64`/`PAGEDUMP` dumps, and not on
`MDMP` dumps or `open_cdb_remote`
- an unreadable dump is treated as user mode
- both environment variables are parsed one command per line, and CLI
flags take precedence
- Full suite: `uv run pytest src/mcp_windbg/tests/` gives 147 passed, 2
skipped (the live
kernel-target scenarios; no `MCP_WINDBG_KERNEL_CONNECTION` target was
configured).
- `pwsh scripts/Format-Docs.ps1 -Check` and `mkdocs build --strict` are
clean.
Note: if #120 (`open_kd_dump`) merges first, its handler needs the same
one-line call,
`results.extend(_init_sections(session, _init_for(kernel=True),
effective))`. I can rebase
to add it.
---------
Co-authored-by: Sven Scharmentke <sven.scharmentke@liasoft.de>
|
||
|
|
6a6026618a |
ci: publish to the MCP registry on release (#119)
## What this changes - `publish-mcp-registry.yml` gains a `workflow_call` trigger (`workflow_dispatch` stays for re-publishing by hand). - `release-please.yml` gets a `registry` job that calls it after `publish`, with `id-token: write` for the GitHub OIDC login. - CLAUDE.md lists the new job in the release flow. ## Why The registry workflow was dispatch-only, so the MCP registry stopped at 0.12.2 while PyPI moved on. The registry verifies the package on PyPI, so the job waits for `publish` and is skipped whenever `publish` is. ## How it was verified - Dispatched the existing workflow on `main` for 1.3.1: it succeeded, and the registry now lists 1.3.1 as the latest version. - Both workflow files parse; the chained call only runs on the next release. |
||
|
|
a50e48654f |
chore(develop): release 1.3.1 (#117)
🤖 I have created a release *beep* *boop* --- ## [1.3.1](https://github.com/svnscha/mcp-windbg/compare/v1.3.0...v1.3.1) (2026-09-29) ### Bug Fixes * **debugger:** preserve output on command timeout ([#114](https://github.com/svnscha/mcp-windbg/issues/114)) ([v1.3.1 |
||
|
|
c3a8d29bc3 |
fix: return debugger errors without a stack trace (#118)
## What this changes Tool calls that fail with a `DebuggerError` (timeouts, a debugger that exited, failed connects) now reach the caller as the error message alone. Unexpected exceptions still include the stack trace. ## Why `_dispatch_tool` appended `traceback.format_exc()` to every error. The last line of that trace repeats the exception message, so since #114 a timeout carrying partial output returned it twice: about 130 KB against a real kernel target, instead of the 64 KiB cap. The trace itself tells the model nothing it can act on. ## How it was verified - New hermetic test through the registered tool handler (cdb and kd): fails before the fix, passes after. - Full suite: 132 passed, 2 skipped. - Against a real KDNET target (TESTBOX-1): both kernel scenarios pass, and `!process 0 7` with an 8 s timeout now returns 61 KB with the partial output once and no traceback. |
||
|
|
5bf4213313 |
fix(debugger): report debugger process exit instead of timing out (#115)
## What this changes
When the debugger process exits, the session now notices at once and
raises
`DebuggerExitedError` with the exit code and the debugger's last output,
instead
of waiting out the full timeout and then reporting a timeout.
- `debug_session.py`: when the reader thread reaches EOF, it marks the
session as
exited, publishes the last output it collected, and sets `ready_event`
so
waiters wake up. Every marker waiter (`_send_marked`,
`_wait_for_prompt`,
`_wait_for_break_locked`) and every stdin write checks for the exit and
raises
`DebuggerExitedError`, a `DebuggerError` subclass, so the server's
existing
error handling is unchanged. The probes (`_still_running`,
`_resume_took_effect`,
`_break_in_and_resync`) pass this error through rather than treating it
as
"target still running". Once the debugger has exited, `send_command`
fails fast
and says to close the session and open a new one.
- `kd_session.py`: a new `_on_debugger_exit` hook releases the
connect-banner
wait, so kd exiting before it connects is reported at once with kd's own
message.
- Nonzero exit codes are shown in hex (e.g. `0xC0000005`, `0x8007274C`),
since
they are usually NTSTATUS/HRESULT values.
## Why
The reader loop returned silently at EOF, and nothing woke the waiting
thread.
So whenever cdb/kd exited - `q`, `.kill` on a dump, a failed `-remote`
or `-k`
connect, or the debugger itself crashing - every pending call sat out
its whole
timeout (up to 300s for `wait_for_break`) and then reported the wrong
cause:
- `run_cdb_command("q")` -> `Command timed out after N seconds: q
(session may
need a manual break-in)`
- `open_cdb_remote` with a mistyped server -> `Debugger initialization
timed out`,
and cdb's actual reason was thrown away
- `open_kd_session` with a bad `-k` string or a busy KDNET port -> a
connect
timeout that hid kd's error
## How it was verified
Against real cdb (WinDbg 1.2606.22001.0, Store install):
| Case | Before | After |
|---|---|---|
| `q` on `DemoCrash1.exe.7088.dmp` | full timeout, then "timed out" |
fails at once: `Debugger process exited (exit code 0) while running
'q'.` |
| `open_cdb_remote` to an unreachable `tcp:Port=5999,Server=127.0.0.1` |
60s, then "Debugger initialization timed out" | 2.3s: `exit code
0x8007274C`, plus `DebugConnect failed, Win32 error 0n10060 ...` |
New hermetic tests, using the fake cdb/kd:
- the debugger exits during a command (`q`), and later commands fail
fast
- the debugger crashes mid-command; the error carries the exit status
and its last output
- the debugger exits during startup; the error carries its message
- `wait_for_break` ends when the debugger exits
- kd exits before the connect banner
Full suite: `uv run pytest src/mcp_windbg/tests/` - 116 passed, 2
skipped (the
kernel scenarios, since no `MCP_WINDBG_KERNEL_CONNECTION` target was
configured).
Co-authored-by: Sven Scharmentke <sven.scharmentke@liasoft.de>
|
||
|
|
c8e2355696 |
fix(debugger): preserve output on command timeout (#114)
Fixes #112. ## What this changes - Preserve the debugger lines already read when a marked CDB command times out. - Bound timeout diagnostics to 2,000 lines and 64 KiB, with a truncation note. - Document the preserved output in the timeout troubleshooting guide and changelog. ## Why The reader kept output in a private buffer until the completion marker arrived. A runaway SOS command such as `!clrstack` could therefore print useful frames and then lose all of them when the timeout path fired. The timeout error now includes that partial output and also exposes it on `DebuggerError.partial_output`; marker synchronization and the existing manual break-in warning are unchanged. ## How it was verified - The new timeout regression fails on `35b6cdb` and passes with the fix; the same test asserts the preserved line and the bounded diagnostic behavior. - `test_debug_session.py`: 66 passed. - Hermetic suite: 103 passed, 12 deselected; full suite: 103 passed, 12 skipped because this host has no CDB/KD or Git LFS debugger dumps. - `uv lock --check`, MCP schema validation, docs formatter, `compileall`, `uv build`, `twine check`, and `git diff --check` passed. Co-authored-by: Oscar <319719771+Oscar-Williams@users.noreply.github.com> Co-authored-by: Sven Scharmentke <sven.scharmentke@liasoft.de> |
||
|
|
44fbcccadd |
fix: keep debugger operations off the request loop (#110)
## What this changes
- Offload synchronous debugger startup, command, and shutdown handlers
to AnyIO workers while preserving tool filtering and MCP error
conversion.
- Keep send_ctrl_break on the request loop so it can interrupt a command
even when that command occupies the only worker slot. Existing
debugger-level I/O synchronization is unchanged.
- Claim a session before blocking shutdown; concurrent closes cannot
shut it down twice or replace the first KD close's resume choice.
- Add nine hermetic tests through the server's public registered tool
handler and an Unreleased changelog entry.
## Why
Fixes #109.
The asynchronous request handler currently calls blocking debugger
operations directly. A waiting command prevents the same server from
dispatching the break-in tool intended to interrupt it. Startup and
shutdown also block unrelated tool calls. The existing wait_for_break
worker dispatch already follows this responsiveness model.
The expanded regression suite fails 7 cases and passes 2 against
unchanged develop at
|
||
|
|
438719a014 |
deps: consolidate dependency updates (1 PR) (#116)
Consolidates the following Dependabot PRs into a single update: - #113 - update pymdown-extensions requirement from >=11.0.2 to >=12.0.1 Supersedes #113 Docs build verified locally with `mkdocs build --strict` against pymdown-extensions 12.1. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
35b6cdb49d |
chore(develop): release 1.3.0 (#107)
Release the separate server, skills, and agents plugins with updated installation and migration documentation. Align all package versions and publish the 1.3.0 changelog.v1.3.0 |
||
|
|
7c55ac325f |
feat(plugin): package skills and agents as separate plugins (#106)
Separate the MCP server, four debugging skills, and crash-analyst agent into independently installable plugins. Document installation and migration, support existing MCP connections, and validate every marketplace version. |
||
|
|
0b38a2848d |
chore(develop): release 1.2.2
🤖 I have created a release *beep* *boop*
---
##
[1.2.2](https://github.com/svnscha/mcp-windbg/compare/v1.2.1...v1.2.2)
(2026-09-05)
### Bug Fixes
* read command output from a UTF-16 log on multibyte code pages
([
v1.2.2
|
||
|
|
d8acc17885 |
fix: read command output from a UTF-16 log on multibyte code pages
Fixes #102. ## Problem On Windows whose **system code page is multibyte** (Chinese 936, Japanese 932, Korean 949, or the "Use Unicode UTF-8" setting 65001), the debugger truncates its text output over a pipe: each line is written short by its multibyte expansion, dropping the tail (and newline) of any line that contains non-ASCII text. This lost the end of `du` strings and other Unicode output, and a line cut in the middle of a character left the session unresponsive to every later command. Reproduced on a Windows 11 VM. It affects **every command that prints Unicode**, not just `du`, and no pipe output mode avoids it: interactive, `-c` batch, and `.logopen` to an ANSI file all truncate, because the miscount is in the debugger's writer. ## Fix (transport level, not per-command) `.logopen /u` writes a **UTF-16 log** that is complete and flushes per command. On a multibyte code page the session opens such a log at startup and reads each command's output from it, keyed by the same `.echo` markers the pipe already uses to synchronize. - **The pipe stays the control channel** for markers, timeouts, go-commands and break-in; only the returned command output comes from the log. All the existing sync logic is untouched. - **Only for sessions whose engine is local** - a crash dump and a kernel target on the wire, where the debugger is our own subprocess and the log opens on this machine. A user-mode `-remote` client drives the engine on the server, so it keeps the pipe (see Limitations). - **Single-byte code pages** (Western 1252 and the like), where the pipe is lossless, are detected with `GetCPInfo` and keep the pipe path, byte-for-byte unchanged. - **Prompt scaffolding stripped** from the log transcript: the bare `0:000>`, kernel `0: kd>` / local `lkd>`, WOW64 `1:001:x86>`, and remote `[server (tcp ...)] 0:000>` forms. - **Reader hardened**: the pipe is decoded with the debugger's own ANSI code page and `errors="replace"`, so a split multibyte sequence can no longer raise in the reader thread on the paths that still read the pipe. - **Cleaned up**: the temporary log is deleted on session shutdown (with a short retry, since a force-killed debugger may still be releasing the handle). ## Research: can the pipe itself be UTF-16? Checked before going the log route. A Windows pipe is a raw byte stream; the writer picks the encoding, and cdb/kd convert their internal Unicode to the ANSI code page (the truncating `WriteFile` path) whenever stdout is a redirected handle. There is no cdb.exe switch to emit UTF-16/UTF-8 to the pipe. The only native Unicode sinks the engine offers are the wide output callback `IDebugOutputCallbacksWide` (which would mean hosting `dbgeng.dll` ourselves, a full re-architecture) and the Unicode log (`.logopen /u`, or the command-line `-logou`/`-logau`). A pseudo-console (ConPTY) would make cdb call `WriteConsoleW`, but its output arrives wrapped in VT escape sequences and hard-wrapped at the console width, which is worse to parse than the log. The log is the supported, clean mechanism. ## Verification Reproduced at the 1.2.1 release commit and verified with this branch on a Windows 11 VM, stock SDK cdb/kd 10.0.26100.1742: | System code page | before | this branch | | :-- | :-- | :-- | | 1252 (Western) | works | passthrough, byte-for-byte unchanged | | 936 (Chinese) | empty / truncated / wedged | full Unicode for every command | | 65001 (UTF-8) | empty / truncated / wedged | full Unicode for every command | `du`, `dw`, `db`, `.echo`, `version` and the rest return complete text; the temp log leaves nothing behind. Full suite: 106 passed on the 1252 host and 111 on the VM under both 936 and 65001; the hermetic tests drive the log path on any platform (a fake debugger mirrors a UTF-16 log) and are pinned to the single-byte path by default so they behave the same regardless of the host's real code page. - **Crash dump:** full CJK strings recovered for `du`/`du <addr> L<n>` under 936 and 65001. - **Kernel (kd.exe):** verified against a live kernel via local kernel debugging on the UTF-8 VM - the log transport engages, and `version`, `lm m nt` and a live kernel-memory read return complete output with the `lkd>` prompt stripped and the temp log cleaned up. - **Remote process debugging (`-remote`):** end-to-end against a real `.server` host - attach, process (`|`), threads (`~`), modules (`lm`) and registers all work. The log stays off (engine is on the server), so this is unchanged from before. ## Limitations - A `-remote` **client on a multibyte code page** still truncates Unicode output: the engine runs on the server, so its Unicode log is not on our machine (and for a cross-machine server would not be reachable at all). Remote debugging otherwise works; this is unchanged from before the fix. - A **live target's asynchronous output** (a go/break-in bugcheck banner) still comes from the pipe, so a bugcheck message containing non-ASCII text could still truncate. Ordinary command output, which is what #102 is about, is fully covered. |
||
|
|
d3d1fd06be |
chore(develop): release 1.2.1 (#101)
🤖 I have created a release *beep* *boop*
---
##
[1.2.1](https://github.com/svnscha/mcp-windbg/compare/v1.2.0...v1.2.1)
(2026-08-27)
### Documentation
* restructure the README install flow, and run CI on develop only
([#100](https://github.com/svnscha/mcp-windbg/issues/100))
([
v1.2.1
|
||
|
|
cd15a7842a |
docs: restructure the README install flow, and run CI on develop only (#100)
Two changes. ## README: one heading per audience The install section interleaved three routes - a plugin block, a `pip install` block for "any other client", then a **Configure your client** step that circled back to Claude Code. A reader had to work out which lines applied to them. Now: ``` ## Quick start tip + prerequisites ## Install in Claude Code ### The plugin ### Registering the server yourself ## Install in another client ## Start debugging ``` Each path reads top to bottom without branching. **Python leaves the prerequisites list.** It is not one: the plugin fetches the server through `uvx`, and the native build will not need it either. Each route states its own requirement next to the command it applies to - "Needs uv" for the plugin, "Needs Python 3.10 or higher" for the two pip routes. **The enterprise policy tip moves to the top of Quick start.** It can change whether someone proceeds at all, so it belongs before the prerequisites rather than buried between install steps. I left the `Python 3.10+` badge in place - it is accurate metadata for the PyPI package, and removing it would lose real information for the pip routes. Say the word if you would rather it went too. ## ci.yml: push trigger drops main and master ```yaml branches: [ develop ] ``` - `master` does not exist (`git ls-remote --heads origin master` is empty). - `main` is moved solely by `fast-forward-main`, a `GITHUB_TOKEN` push, which by design starts no workflow. The run history confirms it: **zero** `push main` runs since the migration. Listing it implied a gate that cannot fire. `develop` stays, for two reasons that are easy to miss: 1. **The CI badge reads it** (`?branch=develop`). Remove the trigger and the badge falls back to whatever ran most recently anywhere - which is how it previously ended up showing "failing" off a blocked release-PR run. 2. **Merge skew.** PRs are squash-merged with no up-to-date-branch requirement, so a PR merged while behind `develop` produces a tree nobody tested. The push run is the only thing that exercises the merged result. ## Verified The VS Code JSON snippet parses (it briefly did not - a single-backslash `C:\Symbols` slipped in, the same defect this repo already shipped once in the plugin docs), the shell command keeps its single backslash, `ci.yml` parses with `push branches: [develop]`, and the docs formatter is clean. |
||
|
|
96bc35b657 |
ci: skip CI on release-please pull requests (#99)
The 1.2.0 release PR collected several red CI runs that nobody could act on: ``` CI #193: Pull request #98 synchronize by github-actions (Bot) ✗ 6m 2s CI #192: Pull request #98 opened by github-actions (Bot) ✗ 6m 20s CI #191: Pull request #91 synchronize by github-actions (Bot) ✗ 16s CI #190: Pull request #91 synchronize by github-actions (Bot) ✗ 31s ``` ## Why they fail release-please pushes the version bump, and `sync-lockfile` updates `uv.lock` a moment later. CI triggered in between sees a lockfile that does not match `pyproject.toml`, fails `uv lock --check`, and reports red on a state that will never be merged. The check is right; the timing means it fires on an intermediate commit. ## Why the run is not needed at all The release PR carries version bumps and the CHANGELOG entry. Nothing else. `publish-mcp.yml` runs the full five-version matrix again before anything reaches PyPI, and the `preflight` gate asserts every version and the CHANGELOG before a tag exists. A run on the PR proves nothing that is not proved later, with teeth. ## Correcting an earlier claim I previously recorded that a job-level condition could not help here, because these runs are blocked at `action_required` before job conditions are evaluated. That holds for the run the bot opens on a fresh PR - but the **synchronize** runs execute normally, and those are the ones going red. A job-level `if` does work for them: ```yaml if: ${{ !startsWith(github.head_ref, release-please--) }} ``` `head_ref` is empty on push events, so pushes to `develop` and `main` are unaffected. |
||
|
|
f8c57bc14b |
chore(develop): release 1.2.0 (#98)
🤖 I have created a release *beep* *boop* --- ## [1.2.0](https://github.com/svnscha/mcp-windbg/compare/v1.1.0...v1.2.0) (2026-08-27) ### Features * **plugin:** name the marketplace entry by how it ships ([#97](https://github.com/svnscha/mcp-windbg/issues/97)) ([v1.2.0 |
||
|
|
d3ddd90588 |
feat(plugin): name the marketplace entry by how it ships (#97)
Installing becomes: ``` /plugin marketplace add svnscha/mcp-windbg /plugin install mcp-windbg-uvx@mcp-windbg ``` This frees the plain `mcp-windbg` name for a self-contained build and makes room for `mcp-windbg-pipx` beside it. Doing it **before** 1.2.0 rather than after means there is no rename migration to run: the old name was never published. ## The syntax is plugin@marketplace, not the reverse Worth stating since it drove the shape here. `claude plugin install --help`: > use plugin@marketplace for specific marketplace So `mcp-windbg@uvx` would mean "plugin `mcp-windbg` from a marketplace called `uvx`" and would need three separate marketplaces. The variant has to live in the plugin name, which is what this does. ## The two names do different jobs `plugin.json`s `name` deliberately stays `mcp-windbg`. I verified the split by installing a plugin whose two names differed on purpose: | Name | Governs | Observed | | :--- | :------ | :------- | | marketplace entry | install identity, plugin cache directory | `cache/probe-mp/mcp-windbg-uvx/` | | `plugin.json` `name` | skill and MCP tool scoping | `plugin:mcp-windbg:mcp-windbg` | and a headless session confirmed the skills: ``` /mcp-windbg:analyze-dump /mcp-windbg:debug-remote /mcp-windbg:kernel-debug /mcp-windbg:windbg-doctor ``` That is the good outcome. Every future variant keeps **identical skill names, identical tool names and one set of documentation** - only the install command differs. Renaming `plugin.json` instead would have renamed every skill and every tool with it, and split the docs three ways. The distinction is recorded in CLAUDE.md, because nothing about the two fields makes it obvious. ## Also verified Installed from the real marketplace under the new name: ``` Source: mcp-windbg-uvx@mcp-windbg Skills (4) analyze-dump, debug-remote, kernel-debug, windbg-doctor Agents (1) crash-analyst MCP servers (1) mcp-windbg ``` `claude plugin validate` passes, docs typography clean, and every occurrence of the old install command across the README, plugin README, getting-started, clients, plugin reference and CHANGELOG is updated. Test state removed afterwards. ## Note on sequencing The open release PR (#91) predates this, so its 1.2.0 CHANGELOG entry still shows the old command. Once this merges I will delete the release branch and let release-please rebuild it, which is the documented way to refresh a release PR - `sync-lockfile` now re-does the lockfile commit on its own. |
||
|
|
cab094c0a9 |
fix(ci): detect the release PR instead of reading release-please's pr output (#96)
The `sync-lockfile` job from #95 **skipped on its first real run**: ``` preflight: success release-please: success sync-lockfile: skipped ``` ## Why Its condition read `needs.release-please.outputs.pr_branch`, derived from release-please`s `pr` output. That output is only set on the run that **creates or updates** the release PR - and release-please deliberately leaves an open release PR alone when the version it computes has not changed (CLAUDE.md documents this; it is why a config-only change needs the release branch deleted to take effect). So the job fired only on the run that opened the PR, and skipped on every push afterwards - with the PR sitting there unsynced and its CI red. That is precisely the situation it was written to fix, so it was inert exactly when needed. ## The fix Ask for the PR rather than depending on having just touched it: ```bash branch=$(gh pr list --state open --base develop --json headRefName \ --jq "[.[] | select(.headRefName | startswith(\"release-please--\"))][0].headRefName // \"\"") ``` True whenever a release PR is open, regardless of which run opened it. The now-unused `pr_branch` output is removed rather than left dangling. ## Verified Ran the query against the live repository: ``` release-please--branches--develop--components--mcp-windbg ``` which is #91, the PR currently red on the lockfile check. |
||
|
|
4cb7f753db |
ci: regenerate uv.lock on the release PR (#95)
Fixes the release PR failure at [run 33109222328](https://github.com/svnscha/mcp-windbg/actions/runs/33109222328/job/98647266760): ``` error: The lockfile at `uv.lock` needs to be updated, but `--check` was provided. ``` ## Not a false alarm `uv.lock` records the **project`s own version**, so release-please bumping `pyproject.toml` to 1.2.0 leaves the lockfile behind. This is exactly the drift that stranded `uv.lock` at 1.0.1 through the entire 1.1.0 release - the check added in #93 is doing its job, it just surfaced that nothing maintains the lockfile across a release. ## Why not an `extra-files` entry That was my first instinct and it is the wrong tool. release-please`s updaters **parse and re-serialise** the file they edit rather than patching it in place - which is exactly why `plugin.json`s inline `keywords` array reflowed to one entry per line in the current release PR. Harmless for a small hand-written manifest. Applying it to a 900-line generated lockfile, with a tool that does not own the format, is not a trade worth making, and the TOML updater would additionally need a filter expression (`$.package[?(@.name=="mcp-windbg")].version`) whose support release-please`s docs do not confirm - a naive `$.package[*].version` would rewrite the version of all 67 packages. ## What this does instead A `sync-lockfile` job runs `uv lock` against the open release PR and commits the result only if it moved. uv owns the file, so uv regenerates it. The PR branch comes from release-please`s `pr` output. It is a JSON string, so the expression short-circuits rather than handing `fromJson` an empty value: ```yaml pr_branch: ${{ steps.release.outputs.pr && fromJson(steps.release.outputs.pr).headBranchName }} ``` ## Verified The bump is a single field and needs no dependency re-resolution - setting the `mcp-windbg` entry in `uv.lock` to 1.2.0 by hand makes the check pass: ``` Resolved 67 packages in 1ms exit: 0 ``` I deliberately did **not** hand-fix `uv.lock` on the release branch, so that merging this exercises the new job for real against #91 rather than leaving it untested. |
||
|
|
92f73c2494 |
ci: gate the release on every version and the CHANGELOG agreeing (#94)
Makes `check-version-consistency.ps1` a gate instead of a post-mortem. It ran from the publish build - which is *after* release-please has created the tag and the GitHub release. It reported problems that had already shipped, and the only way to correct one was to cut a second release. CLAUDE.md described this as "caught, but late". It now runs as a `preflight` job that `release-please` depends on, so a release that is not internally consistent never gets a tag. ## What is asserted All seven version sources must carry the same version: | Source | Kept in step by | | :----- | :-------------- | | `pyproject.toml` | release-please, natively | | `server.json` top level and `packages[*]` | two `extra-files` entries | | `.release-please-manifest.json` | release-please, natively | | `plugins/mcp-windbg/.claude-plugin/plugin.json` | `extra-files` | | `.claude-plugin/marketplace.json` | `extra-files` | | the `uvx` pin in the plugin `.mcp.json` | `extra-files` | Plus: `CHANGELOG.md`s top heading names that version, **and has an entry under it**. ## Two assertions added while promoting it - **`.release-please-manifest.json`** was unchecked. It is the version release-please computes the *next* release from, so drift there silently misprices the following release rather than the current one. - **A bare heading**. `## [1.2.0]` with nothing beneath it matched the version comparison perfectly and published empty release notes. The `release-notes` job could only emit a warning about it, after the release existed. ## Why only release merges are checked On an ordinary push `CHANGELOG.md` correctly carries `## [Unreleased]`, which would fail the version comparison on every single commit. The job therefore reads the head commit subject and only enforces on `chore(develop): release ...`. Verified against real subjects: ``` CHECK <- chore(develop): release 1.2.0 (#91) SKIP <- fix(plugin): stop the agent hard-coding (#92) SKIP <- chore(main): release 1.2.0 (#89) ``` ## Verification Three states, exit codes checked directly rather than through a pipe: | State | Expected | Got | | :---- | :------- | :-- | | Today`s tree, `## [Unreleased]` on top | fail | exit 1, names the mismatch | | Simulated 1.2.0 release, all seven sources bumped | pass | exit 0 | | 1.2.0 heading with an empty body | fail | exit 1, names the empty section | The script still runs from the publish build as well, which is what guards a `publish-mcp.yml` started by hand. |
||
|
|
71b84e2e3d |
ci: pin the CI badge to develop and gate the lockfile (#93)
Fixes the red CI badge, a second badge that was about to break silently, and adds the lockfile guard. ## The badge was lying The badge tracked the default branch. Moving the default to `develop` pointed it at a branch with no completed run yet, so GitHub fell back to the most recent run of the workflow overall - which was this: ``` release-please--branches--develop--components--mcp-windbg pull_request completed / action_required actor: github-actions[bot] ``` A permanently blocked run, never executed, awaiting an approval nobody will give. Every real run was green. Pinning the badge to `?branch=develop` makes it deterministic and immune to runs on PR branches; it now reads **passing**. **On whether CI is needed on the release PR: no.** That PR only bumps version fields and the changelog, and `publish-mcp.yml` runs the full matrix before anything ships, so the gate already exists downstream. Worth knowing that the blocked run cannot be filtered away: `pull_request` `branches:` filters the *base* branch, not the head, so there is no trigger-level way to exclude `release-please--**`. A job-level `if` would not help either, since the run is blocked at approval before job conditions are evaluated. It is inert and now cosmetically irrelevant. ## The Docs badge was about to freeze Quieter version of the same problem, introduced by #90. `pages.yml` is now invoked through `workflow_call`, and a called workflow does not produce its own top-level run - so its badge would have stayed pinned to the last direct push to `main` forever, showing "passing" no matter what happened. It now reads the `github-pages` deployment, which is the thing actually being asserted. ## Lockfile guard ```yaml - name: Check uv.lock is in step with pyproject.toml run: uv lock --check ``` Placed *before* `uv sync`, which would otherwise refresh the lockfile and hide the drift. release-please has no updater for `uv.lock`, and it sat a full release behind at 1.0.1 before anyone noticed. ## Verification - `build-and-test.yml` parses; `uv lock --check` passes on this branch. - Pinned CI badge renders `CI - passing`; the deployments badge renders `docs: success`. |
||
|
|
f146c0bc6a |
fix(plugin): stop the crash-analyst agent hard-coding the plugin name (#92)
The `crash-analyst` agent listed its MCP tools in full: ``` tools: mcp__plugin_mcp-windbg_mcp-windbg__list_dumps, ... , Read, Grep, Glob ``` That middle segment, `mcp-windbg`, is **the name the plugin is installed under** - a marketplace entry name, not a fixed string. It works today because there is one entry and it happens to be called `mcp-windbg`. The moment a second entry exists (`mcp-windbg-uvx` alongside a self-contained `mcp-windbg`, for instance) the list matches nothing on that entry, and the agent runs with no debugger tools at all. That is a worse failure than over-permissioning: the agent still starts, still gets asked to analyse a dump, and confidently reports on nothing. ## Why not a narrower allow-list There is no way to name an MCP tool without that segment - `mcp__<server>` and `mcp__<server>__*` still resolve through the same scoped server name. An allow-list therefore cannot be made independent of the installed name. So the restriction is expressed the other way round: ```yaml disallowedTools: Write, Edit, NotebookEdit, Bash ``` This achieves the actual intent - a read-only investigator - without naming the plugin anywhere. ## The trade The agent now inherits the kernel and remote tools it previously lacked (`open_kd_session`, `send_ctrl_break`, and so on). Its prompt scopes it to a single dump, and it can no longer write files or run shell commands, but this is a genuine widening and worth stating rather than glossing over. The alternative - regenerating the agent file per marketplace entry at build time - keeps the tighter list but means the repo copy and the packaged copy differ, which is its own maintenance cost. ## Verification Installed from the repo marketplace and checked the component inventory: ``` Skills (4) analyze-dump, debug-remote, kernel-debug, windbg-doctor Agents (1) crash-analyst MCP servers (1) mcp-windbg ``` The agent still registers and the frontmatter parses with the explanatory YAML comments in place. Local test state was removed afterwards. |
||
|
|
28e71e7982 |
ci: release from develop and fast-forward main onto each release (#90)
Moves the release process to a `main <- develop <- feat/fix` model. `develop` is now the repository default and the base for every feature and fix PR. release-please watches it and keeps a `chore(develop): release X.Y.Z` PR open there; merging that PR is what "release now" means. `main` stops being a branch anyone commits to and becomes a pointer to the last released commit. ## What changed | File | Change | | :--- | :----- | | `release-please.yml` | Triggers on `develop`, `target-branch: develop`, new `fast-forward-main` and `docs` jobs | | `pages.yml` | Gains a `workflow_call` trigger | | `ci.yml` | Runs on pushes to `develop` | | `CLAUDE.md` | Release section rewritten for the two-branch flow | ## The GITHUB_TOKEN trap, twice This repo already calls `publish-mcp.yml` through `needs` rather than from `on: push: tags`, because a release created with the default `GITHUB_TOKEN` does not trigger other workflows. The `main` fast-forward is the same kind of push and hits the same wall. A `pages.yml` left watching `main` would therefore have gone quiet forever after this change - docs would simply have stopped deploying, with nothing failing to say so. So `pages.yml` gets a `workflow_call` trigger and is chained from the release run instead. Its `push: main` trigger is kept as a manual escape hatch. ## Failure mode worth knowing `fast-forward-main` does a plain `git push origin <sha>:refs/heads/main`. If someone pushes to `main` directly, that push stops fast-forwarding and the job fails with an explicit error naming the fix (merge `main` into `develop`, re-run). It never force-pushes, so a divergence is reported rather than silently discarded. `main` is currently unprotected, which is what lets the workflow move it with the default token. A ruleset restricting pushes to the workflow would be a sensible follow-up, but it needs a bypass actor configured correctly or it blocks the very job that advances `main` - worth doing after one release has been observed working. ## Verification - All seven workflow files parse as YAML. - Docs typography check clean. - Release-notes `awk` block, `publish` job, and `release-please-config.json` are untouched. |
||
|
|
90457306fe |
feat: ship a Claude Code plugin and host a marketplace for it (#88)
Installing mcp-windbg into Claude Code meant a `pip install` plus a hand-written server registration with an env var. It becomes two lines: ``` /plugin marketplace add svnscha/mcp-windbg /plugin install mcp-windbg@mcp-windbg ``` This repo hosts the marketplace itself, so there is no second repository to keep in step. ## Why there is no install step at all The plugin launches the server with `uvx`, pinned to the matching release, so uv fetches it from PyPI on first use. That is what makes the two lines *complete* rather than the last two of five. `server.json` already declared `runtimeHint: uvx`, so this is how the package presented itself anyway. ## What ships - The ten tools, through the plugin's `.mcp.json`. - **`/mcp-windbg:analyze-dump`** - crash-dump triage that separates fact from inference and calls out missing symbols rather than reading meaning into `module+0x1234`. - **`/mcp-windbg:debug-remote`** - attaching to a user-mode debug server, and the part that is easy to get wrong by hand: saying plainly whether the target was left running or frozen. - **`/mcp-windbg:kernel-debug`** - driving a live kernel target, including the `g` / `wait_for_break` / `send_ctrl_break` handling from 1.1.0, and closing with `resume: true` so nobody leaves a machine halted. - **`/mcp-windbg:windbg-doctor`** - the first thing to run when nothing works: checks platform, CDB, uv, the `uvx` round trip, and symbols, then reports what to fix rather than one failure at a time. - **`crash-analyst`** agent - hand it a dump and a question and it investigates on its own, returning a verdict, the evidence behind it, what it ruled out, and what to do next. Its `tools:` list uses the scoped `mcp__plugin_mcp-windbg_mcp-windbg__*` names. - A [reference page](https://github.com/svnscha/mcp-windbg/blob/feat/claude-plugin/docs/reference/plugin.md) in the docs site, plus plugin sections in the README, getting-started, and client configuration. Always-on cost is about 400 tokens; a skill or the agent costs more only when it fires. ## Symbols are configured, without overriding yours The plugin sets `_NT_SYMBOL_PATH` as a **default**, not a value: ```json "_NT_SYMBOL_PATH": "${_NT_SYMBOL_PATH:-SRV*C:\Symbols*https://msdl.microsoft.com/download/symbols}" ``` Claude Code's `.mcp.json` expansion supports `${VAR:-default}`, so an existing value wins and a symbol path someone tuned themselves is never overwritten. Without this the out-of-the-box experience is a stack of `module+0x1234` offsets, which is exactly the case where triage is guesswork. ## One thing that cannot be declared **Windows-only.** There is no manifest field for a platform constraint, so installing on macOS or Linux loads the plugin and the server then fails to find a debugger. Stated in the plugin README, the marketplace description, and the reference page; `windbg-doctor` catches it at the first check and stops there. ## Version drift, closed release-please now bumps the plugin manifest, the marketplace entry, and the `uvx` pin along with `pyproject.toml` and `server.json`, and `check-version-consistency.ps1` checks all three. That is the guard that a silently-broken `extra-files` entry does not ship - the exact failure caught by hand during the 1.1.0 release. The `uvx` pin survives the bump because release-please's `generic-json` updater does a substring replace against its version regex rather than overwriting the whole value, so `mcp-windbg@1.1.0` becomes `mcp-windbg@1.2.0` and not a bare `1.2.0`. ## A bug fixed on the way `check-version-consistency.ps1` looped with `foreach ($error in $errors)`. `$error` is a **read-only PowerShell automatic variable**, so the first time it had a mismatch to report it threw `Cannot overwrite variable Error` instead of naming the files. The build still failed, so this was never dangerous - just useless exactly when you needed it. Verified both ways now: it names the mismatching file, and still exits 1. ## Dependencies Floors raised to current releases with the major caps unchanged: `mcp` 2.1.1, `starlette` 1.6.0, `uvicorn` 0.52.4, and `pymdown-extensions` 11.0.2 for the docs build. `uv.lock` is regenerated; it had been stranded at 1.0.1 since the 1.1.0 release, because release-please has no updater for it. A `uv lock --check` step in CI would catch that drift and is worth a follow-up. ## Verification - `claude plugin validate` passes for both the marketplace manifest and the plugin manifest. - Full suite green against the upgraded dependencies: 99 passed, 2 skipped (the kernel scenarios, no target attached). Live CDB scenarios ran. - `validate-server-schema.py` passes; `uv lock --check` reports the lock in sync. - The `.mcp.json` snippet on the reference page is asserted to parse and to equal the real file, after it shipped with a single-backslash `C:\Symbols` that did neither. - The earlier end-to-end check (`claude plugin marketplace add` -> `install` -> `details`, and a headless session seeing the tools through `uvx` with nothing pre-installed) was run when the plugin had two skills and no agent, so it predates the current surface. ## Note You already have a `svnscha` marketplace pointing at `svnscha/claude-plugins`. This adds a second, repo-local one. If you would rather have the plugin listed there instead, say so - the plugin directory moves as-is and only the marketplace manifest changes. |
||
|
|
fdc3b98270 |
chore(main): release 1.1.0 (#87)
🤖 I have created a release *beep* *boop*
---
##
[1.1.0](https://github.com/svnscha/mcp-windbg/compare/v1.0.1...v1.1.0)
(2026-08-22)
### Miscellaneous Chores
* prepare the 1.1.0 release
([#83](https://github.com/svnscha/mcp-windbg/issues/83))
([
v1.1.0
|
||
|
|
bf4412f64b |
ci: allow release-please to be re-run by hand (#86)
Adds `workflow_dispatch` to `release-please.yml`, and documents the gotcha that made it necessary. ## What happened #85 restored the `server.json` `extra-files` entries, but the already-open 1.1.0 release PR did not change. release-please leaves an existing release PR alone when the version it computes has not moved, so a fix that only affects the PR's *contents* is invisible: the branch had been built by the earlier run and was never rebuilt. Recovering means deleting the release branch to force a fresh build - and with `on: push` as the only trigger, the only way to start that build was to push another commit to `main`. This makes it a button. ## Also in CLAUDE.md - An open release PR is not rewritten by a config change; delete the `release-please--branches--main--*` branch and re-run via `workflow_dispatch`. - **Check the release PR's diff before merging.** It should touch `pyproject.toml`, `.release-please-manifest.json`, and all three versions in `server.json`. A missing `server.json` means `extra-files` is not firing - which is exactly how the bug in #85 was found, and the check-version guard would only have caught it after the tag and release already existed. |
||
|
|
5884a5ede4 |
ci: restore the server.json extra-files entries (#85)
Caught by inspecting the first real release PR (#84) rather than trusting it: it bumped `pyproject.toml` and `.release-please-manifest.json` to 1.1.0 but left all three `server.json` versions at 1.0.1. Cause: the `extra-files` block was dropped when the config was switched to `skip-changelog`. `scripts/check-version-consistency.ps1` on the release build would have caught this - but only after the tag and GitHub release already existed, which is exactly the late-failure mode described in CLAUDE.md. Two entries are needed because `server.json`'s top-level `version` and the two `packages[*].version` fields are separate, and one entry carries one JSONPath. Two entries against the same path are applied in sequence rather than clobbering each other, and the wildcard covers both array elements. Once this lands, release-please rewrites #84 and `server.json` should appear in its diff. I will confirm that before merging the release. |
||
|
|
b943e1c46d |
chore: prepare the 1.1.0 release (#83)
Promotes [Unreleased] to [1.1.0], adds a pull request template stating the Conventional Commit title requirement, and records in CLAUDE.md what an unreleasable release-please run looks like in the log. Release-As: 1.1.0 |
||
|
|
2693042b65 |
ci: drive releases with release-please (#82)
Replaces the hand-run release process with [release-please](https://github.com/googleapis/release-please) (`googleapis/release-please-action@v5`). ## The new flow 1. Push to `main` -> release-please opens or rewrites a `chore(main): release X.Y.Z` PR carrying the version bumps. 2. Before merging, add the `CHANGELOG.md` entry to that PR (rename `## [Unreleased]` to `## [X.Y.Z] - date`). 3. Merge -> the tag and GitHub release are created, the release body is set from your CHANGELOG entry, and PyPI publish runs behind the full Windows test matrix. No more editing three files and pushing a tag by hand. ## CHANGELOG stays hand-written `skip-changelog: true`. release-please generates terse bullets from commit subjects; the entries in this repo explain *why* a change happened and credit the people who found it, which is worth more than the automation saves. The `release-notes` job replaces the generated GitHub release body with the CHANGELOG section for that version, preserving what `publish-mcp.yml` used to do. Bonus: because release-please never touches `CHANGELOG.md`, the `## [Unreleased]` heading is a non-issue. It would otherwise have been a trap - release-please finds its insert point with `\n###? v?[0-9[]`, and `[` is inside that character class, so a new release would have been inserted *above* `[Unreleased]`, stranding it. ## Two traps this avoids **Tag-triggered publish would have silently stopped working.** release-please creates the tag with `GITHUB_TOKEN`, and tags pushed by that token do not start workflow runs. `publish-mcp.yml` is now `workflow_call`/`workflow_dispatch` and is invoked via `needs`, so no cross-workflow trigger is involved. Its `create-github-release` job is dropped - release-please owns releases now. **`include-component-in-tag` is set to `false` explicitly.** It defaults to `true` in manifest mode, which with `package-name` set would have tagged `mcp-windbg-v1.1.0` instead of `v1.1.0`. ## A latent bug fixed along the way The awk that extracts the changelog section for the release notes used a dynamic regex. awk treats `"\["` inside a string as a plain `"["`, which collapsed the pattern into a character class matching **every** `## [` heading - it always took the first section regardless of the version requested. It only produced correct notes for v1.0.1 because that heading happened to be on top at tag time; with an `[Unreleased]` section present it would have shipped the wrong content. Rewritten with `index()`/`substr()`, which has no escaping hazard: ```awk awk -v hdr="## [$CLEAN_VERSION]" ' index($0, hdr) == 1 { found = 1; next } found && substr($0, 1, 4) == "## [" { exit } found { print } ' CHANGELOG.md ``` Verified to extract the correct section for both 1.0.1 and 1.0.0, and the job's real shell block was extracted from the YAML and executed locally. ## PR titles now carry meaning Squash-merging makes the PR title the commit subject, which is what release-please reads. It must be a Conventional Commit or the change contributes nothing to the next version. Worth knowing that of the recent merges here, only `fix(deps): pin mcp dependency to <2.0.0` would have counted. `CLAUDE.md` documents the prefix table, the flow, and the two setup requirements. ## Repo setting changed "Allow GitHub Actions to create and approve pull requests" was **off**, which would have made release-please fail with a 403 when opening the release PR. Enabled via the API at your request. `default_workflow_permissions` was left at `read` - each job declares the scopes it needs. ## Verification - Both JSON config files parse; all three workflow YAMLs parse. - The `release-notes` shell block was extracted from the YAML and run against the real `CHANGELOG.md`, producing the correct 1.0.1 body. - 89 hermetic tests still pass. The end-to-end release path cannot be fully proven until the first real release runs - that is the one thing here I have not been able to exercise. |
||
|
|
356a6bdfe1 |
Run on the mcp 2.x SDK (#81)
Closes #78. Lifts the `mcp<2.0.0` cap that 1.0.1 shipped as a stopgap. ## What actually breaks in 2.x The `McpError` -> `MCPError` rename is the half everyone hits first, but it was the easy half. 2.0.0 also removed the low-level `@server.list_tools()` / `@server.call_tool()` decorators that every tool and prompt was registered through. Handlers are now passed to `Server(...)` as constructor arguments, take `(ctx, params)`, and return the SDK's result types instead of bare lists: | 1.x | 2.x | | :--- | :--- | | `@server.list_tools()` -> `list[Tool]` | `on_list_tools=` -> `ListToolsResult` | | `@server.call_tool()` -> `list[TextContent]` | `on_call_tool=` -> `CallToolResult` | | `@server.list_prompts()` / `@server.get_prompt()` | `on_list_prompts=` / `on_get_prompt=` | | `McpError(ErrorData(code=, message=))` | `MCPError(code, message)` | Tool dispatch is split from result wrapping, so the dozen handler returns keep their `list[TextContent]` shape and exactly one place builds the `CallToolResult`. ## What did not change - **Clients see nothing.** Same nine tools, same schemas, both transports. - **Transports needed no work.** `stdio_server`, `StreamableHTTPSessionManager`, and `Server.run` are all signature-compatible. - **Python support.** The 2.x SDK requires 3.10+, exactly this project's existing floor. - **Errors still surface as errors.** Raising `MCPError` from a handler becomes a JSON-RPC error response; the harness already normalized raised errors and `isError` alike, so the scenario assertions were unaffected. ## The harness moved too It drives a real client from the same SDK, so it needed three changes: `MCPError`, `CallToolResult.is_error` (the wire format is still `isError` - only the Python attribute is snake_case), and `streamable_http_client` now yielding two values instead of three. Its note about the stdio teardown race is corrected rather than deleted: **2.0.0 fixed that race** - `stdout_reader` now catches `BrokenResourceError` as well as `ClosedResourceError` around its send. The guard stays for the other transports, and it only suppresses after every step has completed, so it cannot mask a real failure. ## Verification, all on mcp 2.0.0 - 89 hermetic tests - 10 `live` cdb scenarios against real `cdb.exe` - both kernel scenarios against the live KDNET target - coverage 91% against the 88% gate - `mkdocs build --strict` clean, `server.json` valid against the registry schema, CLI smoke-tested ## Note on releasing Left under `[Unreleased]`. Whenever you cut it, this plus #80 make it a 1.1.0: new `wait_for_break` tool, kernel fixes, and a dependency floor move that is breaking for anyone who pinned `mcp` 1.x themselves (nothing needs to, since installing mcp-windbg pulls the SDK it needs). |
||
|
|
30c7dd9d9f |
Fix kernel go/break-in handling, add wait_for_break (#80)
Fixes #71, #72, #73. Supersedes #74. @TempAccountNull diagnosed all three bugs correctly. This keeps every fix and the credit, but replaces the patch - it had a defect that would have broken stepping - and adds one more bug found by backtesting against a real kernel target. ## The three reported bugs - **Named pipe / COM sessions always timed out** (#71). `kd` announces a pipe or serial link with `Kernel Debugger connection established`, not the KDNET `Connected to target ...`. Only the latter was matched, so `open_kd_session` sat waiting for a banner that was never coming on a target that had in fact attached. - **`g` re-froze the target it had just released** (#72). Go-class commands hand the CPU back and the debugger stops reading stdin, so the marker protocol's `.echo` sat unread, the command hit its timeout, and the cancel-on-timeout CTRL+BREAK halted the machine again. - **Any command after `g` did the same** (#73), through the same path - the session had no concept of "the target is running". ## Why #74 is not merged as-is Its `_GO_COMMANDS` set includes `p`, `t`, `pa`, `ta`. Those are *step* commands: they execute one instruction or line and come straight back to the prompt. Treating them as fire-and-forget swallows their output, returns `"Target resumed."` instead, and leaves the session believing the target is running - so the next command fires a spurious CTRL+BREAK. Stepping would have become unusable. The step family keeps the marker round-trip here, with a test pinning it. ## The fourth bug, found by backtesting `_resume_target` wrote `g` and returned immediately. Until `kd` reads it, `kd` is still at its prompt with the resume unread - and a CTRL+BREAK arriving in that window is answered by the prompt rather than reaching the target. The break is silently lost and the caller then waits out a full `wait_for_break` timeout on a machine nothing will stop. That is exactly the sequence a tool caller writes, because `run_kd_command("g")` now returns instantly. Measured on the live KDNET target: | gap after `g` | result | | :--- | :--- | | 0.00s | **break lost** (every attempt) | | 0.05s | landed in 0.04s | | 0.25s / 1s / 2s | landed | The resume is now probed with a marker the debugger can only answer from a prompt. Silence means it stopped reading stdin - the target is away and the resume can no longer swallow a break. An answer means the target never left, which is a bonus fix: a `gu` that returns in microseconds, or a breakpoint hit at once, now reports what it printed instead of falsely claiming the target is running. ## Also in here - `wait_for_break` - block until a target stops (bugcheck, breakpoint, manual break) and return what it printed. Asks the debugger rather than trusting a flag, so it is right about targets this server never resumed. If the wait expires the target is left **running**. - Compound forms: `bp nt!X; g` runs the breakpoint first and returns its result, so a typo'd symbol surfaces as `Couldn't resolve error` rather than a 300s wait for a break that can never come. A `;` inside a quoted command string is left alone. - Qualified forms `~0 g` / `~*g` are recognized. - One operation at a time per session; closing a session ends a wait parked on it. ## Verification Against the real kernel target (Windows 11 26100, KDNET): - `scenarios/kernel_resume_and_wait.yaml` - new, covers resume, auto break-in, and the zero-gap break. **Verified to fail without the fix** with exactly the lost-break symptom, and to pass with it. - `scenarios/kernel_session.yaml` - still passes. - Machine confirmed left running afterwards, with no breakpoints behind. Hermetic and user-mode: - 89 hermetic tests pass (was 29 on `main`). - 10 `live` user-mode scenarios pass, including the new `resume_and_wait.yaml` against a real `cdb.exe`. - Coverage 91% under CI's conditions, against the 88% gate. - `mkdocs build --strict` clean; `docs/reference/tools.md` documents the resume semantics. **One limitation worth stating:** #71 is the one fix I could not exercise end to end - this box has a KDNET target, not a pipe one. It is covered hermetically for both banner wordings, and I did capture `Kernel Debugger connection established.` verbatim from real `kd` output during a KDNET reconnect, so the string is genuine. That it appears *instead of* the KDNET wording on a pipe transport rests on @TempAccountNull's report and their verified before/after. --------- Co-authored-by: TempAccountNull <TempAccountNull@users.noreply.github.com> |
||
|
|
c7ae1a6570 |
Release 1.0.1: cap the mcp requirement below 2.0 (#79)
Ships the `mcp<2.0.0` cap from #75 as an actual release. ## Why a release and not just `[Unreleased]` pip reads dependency metadata from the **published wheel**, so merging the cap to `main` does nothing for anyone hitting #76 - 1.0.0 keeps resolving to `mcp` 2.x and keeps failing at import. The fix only reaches users when it is on PyPI. ## Changes - `pyproject.toml`, `server.json` (top-level and both `packages[*]`), `CHANGELOG.md` bumped to 1.0.1 - `uv.lock` re-resolved - CHANGELOG entry crediting @aphroteus and @arjunarjun07 No source changes beyond the dependency cap already merged in #75. ## Verified locally - `scripts/check-version-consistency.ps1` - all four locations agree on 1.0.1 - `scripts/validate-server-schema.py` - server.json valid against the MCP registry schema - `uv run pytest -m "not live"` - 29 passed - `scripts/Format-Docs.ps1` - clean - Built wheel metadata reads `Requires-Dist: mcp<2.0.0,>=1.28.1`, which is the part that actually fixes the user-visible bug Closes #76. Porting to the 2.x server API is tracked in #78.v1.0.1 |
||
|
|
c72d722f5a |
fix(deps): pin mcp dependency to <2.0.0 (#75)
## Problem The recent release of mcp SDK v2.0.0 introduced breaking changes (removed @server.list_tools() on Server, renamed McpError). Fresh installs currently resolve mcp 2.0.0 and fail at startup: AttributeError: 'Server' object has no attribute 'list_tools' Refer to https://py.sdk.modelcontextprotocol.io/migration/ ## Solution Pin the mcp dependency to mcp>=1.28.1,<2.0.0 in pyproject.toml to ensure stable installations under the supported mcp 1.x architecture. ## Verification • Verified python -m mcp_windbg --help loads and exits successfully with mcp 1.29.0. |
||
|
|
5f27dc812d |
Consolidate dependency updates (2 PRs) (#77)
Consolidates the following Dependabot PRs into a single update: - #68 - ci(deps): bump actions/setup-python from 6 to 7 in the actions group - #69 - deps(python): bump the python group with 2 updates (mkdocs-material 9.7.6 -> 9.7.7, pymdown-extensions 11.0 -> 11.0.1) Supersedes #68 Supersedes #69 No lock file regeneration needed: neither bump touches `pyproject.toml`, and `uv lock --check` passes unchanged. `requirements-docs.txt` is a plain pip requirements file. Verified locally: - `uv run pytest src/mcp_windbg/tests/ -m "not live"` - 29 passed - `python -m mkdocs build --strict` at the bumped docs versions - clean --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
4f3305094d |
Trim the README to what a new reader needs
The README had grown into a second copy of the docs site. Three intro sections (Overview, What is this?, What This is Not) said the same thing; the full CLI option list, the transport table, and the filter-script contract all duplicated pages that own them and will drift from them; and two separate link tables pointed at the same scenarios. Keep what belongs here: what it is, what it can do, the tools it exposes, how to install it, and how to point a client at it. Everything else links to the page that owns it. 233 lines to 138. No release: docs only, no packaged change. |