Skip to content

Migrate to mcp 2.0 and raise test coverage to 99% - #1

Open
FWDavide wants to merge 4 commits into
mainfrom
davide/mcp2-migration-and-coverage
Open

Migrate to mcp 2.0 and raise test coverage to 99%#1
FWDavide wants to merge 4 commits into
mainfrom
davide/mcp2-migration-and-coverage

Conversation

@FWDavide

@FWDavide FWDavide commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary

Unblocks uvx basecamp-cli-mcp, which had been dead on arrival since the mcp 2.0 release, then migrates properly to the 2.0 API and backfills the test suite that failed to catch the break.

Four commits, each self-contained:

  1. Pin mcp<2 (v0.7.2) — the unbounded mcp>=1.2 range meant every fresh uvx install resolved mcp 2.0, which removed the @server.list_tools() / @server.call_tool() decorators, and died with AttributeError before serving a request. Pin so the published package installs again.
  2. Migrate to the mcp 2.0 low-level Server API — port the two handlers to the on_list_tools / on_call_tool callbacks passed to Server(), and require mcp>=2,<3. Behaviour is preserved: tools still advertise the raw JSON Schema from tools.json (types.Tool now takes input_schema rather than inputSchema), and a failing call is still reported as an error result carrying the CLI message plus stderr. That last part is now explicit: 1.x's decorator wrapper turned a raised exception into isError: true for us, whereas a raw on_call_tool handler would surface it as a JSON-RPC error, so the handler returns CallToolResult(is_error=True) itself.
  3. Raise test coverage from 50% to 99% — see below.
  4. Ignore coverage artifacts.coverage is regenerated on every run.

Why the test work

The mcp 2.0 break exposed how thin the suite was: it stayed green while the server could not be constructed at all, because three modules had no tests and the two that did stubbed out their own boundaries.

  • Runner._invoke (tests/test_runner_invoke.py) — the seam neither existing suite touched. Runner tests overrode _invoke; server tests faked the runner. Nothing exercised argv reaching a process, or the {ok, data} / {ok: false, error} envelope becoming a value or a BasecampError. Driven by a real shell stub, so subprocess, stderr, and exit codes are real.
  • setup_cmd (tests/test_setup_cmd.py) — the only code that writes outside the repo, and untested through the last three releases. Covers the config merge, the abort paths that must leave a malformed file untouched, backup creation, both tool-set choices, and the installer flow.
  • generator (tests/test_generator.py) — the tree walk that produces the shipped contract, via a subclass replaying canned CLI output. Worth having before the pending regeneration against basecamp CLI 0.9.0.
  • tools.json (tests/test_tools_contract.py) — the committed contract now has to load, satisfy the runner's expectations, pass SEP-986 tool naming, and build a server across all 255 entries.
  • cli (tests/test_cli.py) — flag plumbing through to server.run.
  • End-to-end protocol tests over in-memory streams (tests/test_server_protocol.py) — real handshake, tools/list, tools/call.

Two tests deliberately pin surprising behaviour rather than assert what it ought to be, so a future change surfaces it: alias dedupe drops sibling actions that share a short description, and the todos-update workaround cannot unassign everyone. Both are documented in place.

Also adds pytest-cov and coverage config.

Test plan

uv run pytest — 151 passed.

Name                                  Stmts   Miss Branch BrPart  Cover
-----------------------------------------------------------------------
src/basecamp_cli_mcp/__init__.py          5      2      0      0    60%
src/basecamp_cli_mcp/cli.py              35      0      6      0   100%
src/basecamp_cli_mcp/generator.py        90      0     36      0   100%
src/basecamp_cli_mcp/help_parser.py     115      0     54      1    99%
src/basecamp_cli_mcp/runner.py          120      0     60      0   100%
src/basecamp_cli_mcp/server.py           59      0     12      0   100%
src/basecamp_cli_mcp/setup_cmd.py       101      0     32      0   100%
-----------------------------------------------------------------------
TOTAL                                   525      2    200      1    99%

🤖 Generated with Claude Code

FWDavide and others added 4 commits August 6, 2026 10:23
mcp 2.0 removed the low-level Server decorator API (@server.list_tools /
@server.call_tool), so the unbounded mcp>=1.2 range meant every fresh
`uvx basecamp-cli-mcp` resolved mcp 2.0 and died with AttributeError
before serving a single request. Pin to <2 and bump to 0.7.2 so the
published package installs again; the 2.0 migration follows separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mcp 2.0 removed the @server.list_tools() / @server.call_tool() decorators and
replaced them with on_list_tools / on_call_tool callbacks passed to Server(),
so port the two handlers over and require mcp>=2,<3.

Behaviour is preserved: tools still advertise the raw JSON Schema from
tools.json (types.Tool now takes input_schema rather than inputSchema), and a
failing call is still reported to the client as an error result carrying the
CLI's message plus stderr. That last part is now explicit - 1.x's decorator
wrapper converted a raised exception into isError:true for us, and a raw
on_call_tool handler would surface it as a JSON-RPC error instead, so the
handler returns CallToolResult(is_error=True) itself. Same convention the
SDK's own MCPServer uses.

Add end-to-end protocol tests over in-memory streams. The old suite only
covered filter_specs, so it passed while the server could not be constructed
at all - the new tests do a real handshake, tools/list and tools/call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mcp 2.0 break exposed how thin the suite was: it stayed green while the
server could not be constructed, because three modules had no tests at all and
the two that did stubbed out their own boundaries.

New coverage, roughly in order of what it protects:

- Runner._invoke (tests/test_runner_invoke.py) - the seam neither existing
  suite touched. The runner tests overrode _invoke and the server tests faked
  the runner, so nothing exercised argv reaching a process or the
  {ok, data} / {ok: false, error} envelope becoming a value or a BasecampError.
  Driven by a real shell stub, so subprocess, stderr and exit codes are real.
- setup_cmd (tests/test_setup_cmd.py) - the only code that writes outside the
  repo. Covers the config merge, the abort paths that must leave a malformed
  file untouched, backup creation, both tool-set choices, and the installer
  flow. The last three releases all edited this file untested.
- generator (tests/test_generator.py) - the tree walk that produces the shipped
  contract, via a subclass replaying canned CLI output. Worth having before the
  pending regeneration against basecamp CLI 0.9.0.
- tools.json (tests/test_tools_contract.py) - the committed contract now has to
  load, satisfy the runner's expectations, pass SEP-986 tool naming, and build
  a server across all 255 entries.
- cli (tests/test_cli.py) - flag plumbing through to server.run.

Two tests deliberately pin surprising behaviour rather than assert what it
ought to be, so a future change surfaces it: alias dedupe drops sibling actions
that share a short description, and the todos-update workaround cannot unassign
everyone. Both are documented in place.

Also adds pytest-cov and coverage config so ============================= test session starts ==============================
platform darwin -- Python 3.12.13, pytest-9.0.3, pluggy-1.6.0
rootdir: /Users/davide/Developer/basecamp-cli-mcp
configfile: pyproject.toml
testpaths: tests
plugins: cov-7.1.0, anyio-4.13.0
collected 151 items

tests/test_cli.py .........                                              [  5%]
tests/test_generator.py ..........................                       [ 23%]
tests/test_help_parser.py ..................                             [ 35%]
tests/test_runner.py ............................                        [ 53%]
tests/test_runner_invoke.py ..........                                   [ 60%]
tests/test_server.py .....                                               [ 63%]
tests/test_server_protocol.py .........                                  [ 69%]
tests/test_setup_cmd.py ....................................             [ 93%]
tests/test_tools_contract.py ..........                                  [100%]

================================ tests coverage ================================
______________ coverage: platform darwin, python 3.12.13-final-0 _______________

Name                                  Stmts   Miss Branch BrPart  Cover
-----------------------------------------------------------------------
src/basecamp_cli_mcp/__init__.py          5      2      0      0    60%
src/basecamp_cli_mcp/cli.py              35      0      6      0   100%
src/basecamp_cli_mcp/generator.py        90      0     36      0   100%
src/basecamp_cli_mcp/help_parser.py     115      0     54      1    99%
src/basecamp_cli_mcp/runner.py          120      0     60      0   100%
src/basecamp_cli_mcp/server.py           59      0     12      0   100%
src/basecamp_cli_mcp/setup_cmd.py       101      0     32      0   100%
-----------------------------------------------------------------------
TOTAL                                   525      2    200      1    99%
============================= 151 passed in 2.54s ============================== works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.coverage slipped into the previous commit; it's regenerated on every run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FWDavide
FWDavide requested a review from FWMatt August 6, 2026 08:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant