95 Commits

Author SHA1 Message Date
github-actions[bot] 4d5c824d33 chore(develop): release 1.5.1 (#149) v1.5.1 2026-10-10 17:53:34 +02:00
Sven Scharmentke b2f8b36f7a fix(cdb): open a remote session whose target is running (#145) 2026-10-10 17:51:32 +02:00
Sven Scharmentke 58bf9758cc ci: let the publish call upload the signed exe to the release (#148) 2026-10-10 17:51:27 +02:00
Sven Scharmentke 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.
2026-10-10 16:47:04 +02:00
github-actions[bot] 7d988361e1 chore(develop): release 1.5.0 (#142) v1.5.0 2026-10-09 00:22:59 +02:00
Sven Scharmentke 59a2d09c6b docs: add the 1.5.0 changelog entry (#141)
Release-As: 1.5.0
2026-10-09 00:21:26 +02:00
Sven Scharmentke 48875bcaeb fix(server): bound retained output and keep diagnostics off stdout (#139) 2026-10-09 00:11:52 +02:00
Sven Scharmentke 95505f7240 fix(debugger): cancel timed-out commands on dump sessions (#138) 2026-10-08 23:48:53 +02:00
dependabot[bot] 0de53835e3 deps(python): update pymdown-extensions requirement from >=12.0.1 to >=12.1 in the python group (#132) 2026-10-08 23:46:59 +02:00
Sven Scharmentke 3e31084a2a fix(debugger): make completion markers unique per session (#137) 2026-10-08 23:36:33 +02:00
Sven Scharmentke 9194e85664 fix(server): close undisclosed sessions when an open fails (#136) 2026-10-08 23:28:17 +02:00
Sven Scharmentke bd7052b8e0 docs: point readers to Microsoft's official WinDbg MCP (#133) 2026-10-08 01:09:04 +02:00
Sven Scharmentke 3d7dbbf5cf chore: add a script to switch between released and local mcp-windbg (#123) 2026-10-01 01:53:49 +02:00
github-actions[bot] 6b0adf6459 chore(develop): release 1.4.0 (#122) v1.4.0 2026-10-01 01:37:07 +02:00
Sven Scharmentke 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
2026-10-01 01:31:17 +02:00
Ramana Reddy Polaka 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>
2026-10-01 00:02:45 +02:00
Sven Scharmentke 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.
2026-09-30 00:35:13 +02:00
github-actions[bot] 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))
([c8e2355](https://github.com/svnscha/mcp-windbg/commit/c8e2355696afab62b79b75eccd5eeef8b9e36708))
* **debugger:** report debugger process exit instead of timing out
([#115](https://github.com/svnscha/mcp-windbg/issues/115))
([5bf4213](https://github.com/svnscha/mcp-windbg/commit/5bf42133131a328ed827e19497f9bde5833546ce))
* keep debugger operations off the request loop
([#110](https://github.com/svnscha/mcp-windbg/issues/110))
([44fbccc](https://github.com/svnscha/mcp-windbg/commit/44fbcccadd69de13c0eb39145764ce8ebb9ad7b4))
* return debugger errors without a stack trace
([#118](https://github.com/svnscha/mcp-windbg/issues/118))
([c3a8d29](https://github.com/svnscha/mcp-windbg/commit/c3a8d29bc32162de82676683438fa4ef02d41c30))


### Dependencies

* consolidate dependency updates (1 PR)
([#116](https://github.com/svnscha/mcp-windbg/issues/116))
([438719a](https://github.com/svnscha/mcp-windbg/commit/438719a0148beb91d7f276d37f14e5d2c2d621ec))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sven Scharmentke <sven.scharmentke@liasoft.de>
v1.3.1
2026-09-29 23:58:17 +02:00
Sven Scharmentke 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.
2026-09-29 23:53:52 +02:00
Ramana Reddy Polaka 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>
2026-09-29 00:45:57 +02:00
Oscar 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>
2026-09-29 00:39:47 +02:00
ADITYA 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 35b6cdb49d, and
passes all 9 with this patch. This does not change public tool schemas
or timeouts.

## How it was verified

- uv run --no-sync pytest src/mcp_windbg/tests/ -q --tb=short: 110
passed, 12 skipped.
- Both-process coverage with MCP_WINDBG_COVERAGE=1, coverage run -m
pytest, coverage combine: 110 passed, 12 skipped, 87% coverage. The
required 88% floor is **not met** on this host. The same run against
unchanged develop is 101 passed, 12 skipped, 80% coverage and also fails
the floor. No threshold or exclusions were changed.
- uv lock --check: passed.
- Public MCP registry schema validation: passed.
- uv build and twine check of both generated distributions: passed.
- Markdown typography check of all 35 Markdown files and git diff
--check: passed.

This is intentionally a draft. No CDB executable, Git LFS crash dump, or
real kernel target is available here; the 12 debugger/kernel scenarios
skip. The new tests establish dispatch, error, and concurrent-close
behavior using fake sessions, not real debugger operation. Real CDB/KD
scenarios and the required coverage floor remain landing gates. No
kernel runtime, release version, tags, or target configuration were
changed.

---------

Co-authored-by: Sven Scharmentke <sven.scharmentke@liasoft.de>
2026-09-29 00:31:48 +02:00
Sven Scharmentke 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>
2026-09-29 00:19:01 +02:00
github-actions[bot] 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
2026-09-10 01:26:58 +02:00
Sven Scharmentke 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.
2026-09-10 01:24:35 +02:00
github-actions[bot] 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
([d8acc17](https://github.com/svnscha/mcp-windbg/commit/d8acc17885ca79dbe2cfc472897694b7b91392bf))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sven Scharmentke <sven.scharmentke@liasoft.de>
v1.2.2
2026-09-05 10:25:05 +02:00
Sven Scharmentke 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.
2026-09-05 10:23:05 +02:00
github-actions[bot] 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))
([cd15a78](https://github.com/svnscha/mcp-windbg/commit/cd15a7842a50225915d9c07c9603c0fe4526bc5d))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sven Scharmentke <sven.scharmentke@liasoft.de>
v1.2.1
2026-08-27 22:47:55 +02:00
Sven Scharmentke 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.
2026-08-27 22:46:18 +02:00
Sven Scharmentke 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.
2026-08-27 22:31:35 +02:00
github-actions[bot] 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))
([d3ddd90](https://github.com/svnscha/mcp-windbg/commit/d3ddd9058874992caa04e9b64cd88afac197ac45))
* ship a Claude Code plugin and host a marketplace for it
([#88](https://github.com/svnscha/mcp-windbg/issues/88))
([9045730](https://github.com/svnscha/mcp-windbg/commit/90457306fef4d48a8c36a7bc3bbb130fc3f601c6))


### Bug Fixes

* **plugin:** stop the crash-analyst agent hard-coding the plugin name
([#92](https://github.com/svnscha/mcp-windbg/issues/92))
([f146c0b](https://github.com/svnscha/mcp-windbg/commit/f146c0bc6ab4455ec9a67fca1ed34da7e8c76e1c))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sven Scharmentke <sven.scharmentke@liasoft.de>
v1.2.0
2026-08-27 22:17:44 +02:00
Sven Scharmentke 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.
2026-08-27 22:09:51 +02:00
Sven Scharmentke 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.
2026-08-27 21:53:56 +02:00
Sven Scharmentke 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.
2026-08-27 21:46:21 +02:00
Sven Scharmentke 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.
2026-08-27 21:34:14 +02:00
Sven Scharmentke 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`.
2026-08-27 21:28:12 +02:00
Sven Scharmentke 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.
2026-08-27 21:25:08 +02:00
Sven Scharmentke 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.
2026-08-27 21:15:51 +02:00
Sven Scharmentke 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.
2026-08-27 20:08:07 +02:00
github-actions[bot] 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))
([b943e1c](https://github.com/svnscha/mcp-windbg/commit/b943e1c46d37da94b81eb22ee447e9a19291612f))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v1.1.0
2026-08-22 18:00:14 +02:00
Sven Scharmentke 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.
2026-08-22 17:58:44 +02:00
Sven Scharmentke 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.
2026-08-22 17:50:16 +02:00
Sven Scharmentke 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
2026-08-22 17:43:26 +02:00
Sven Scharmentke 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.
2026-08-22 17:35:32 +02:00
Sven Scharmentke 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).
2026-08-22 16:31:13 +02:00
Sven Scharmentke 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>
2026-08-22 00:55:39 +02:00
Sven Scharmentke 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
2026-08-21 23:56:22 +02:00
Paul Huang 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.
2026-08-21 23:41:37 +02:00
Sven Scharmentke 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>
2026-08-21 23:34:41 +02:00
Sven Scharmentke 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.
2026-07-16 21:10:07 +02:00