Repository navigation
Bugs, Coverage, and SDK Wave 1 Part 2: Python servers: extensive unit tests + per-file 90% coverage gate #4855
Description
Activity
- changed the title
[-]2026-07-28 Spec Refactor Part 5: Extensive new-spec unit tests for TypeScript servers[/-][+]2026-07-28 Spec Refactor Part 6: Extensive new-spec unit tests for Python servers[/+]on Sep 26, 2026 - added a parent issue
on Sep 26, 2026 - changed the title
[-]2026-07-28 Spec Refactor Part 6: Extensive new-spec unit tests for Python servers[/-][+]2026-07-28 Spec Refactor Part 2: Python servers: extensive unit tests + per-file 90% coverage gate[/+]on Sep 26, 2026 - added a commit that references this issue
on Sep 28, 2026 Here is a coverage baseline for the three Python servers to size this work, from
mainat f46d957 on 2026-09-28. Every file in every server is below the 90% gate. The gap in eachserver.pyisserve(), and the pure functions underneath are well tested.serve()can be driven in-process today with no refactor. The git suite fails on Windows, and its fixture fix should go first.Baseline
Server Tests passing server.pylines / funcs / branchesUncovered in serve()fetch 20 52.3 / 50.0 / 50.0 193-288 git 47 (40 error at teardown on Windows) 71.7 / 75.0 / 58.5 442-581 plus the dead list_repostime 38 69.7 / 62.5 / 42.9 124-131, 182-220 Per-file tables (
uv run --with pytest-cov pytest --cov=src --cov-branch, Python 3.10 and 3.11, coverage.py per-function regions for Funcs)Bold is below the gate.
fetch
File Lines Funcs Branches server.py52.3 50.0 50.0 __init__.py25.0 50.0 50.0 __main__.py0 0 100 git
File Lines Funcs Branches server.py71.7 75.0 58.5 __init__.py52.6 50.0 16.7 __main__.py0 0 100 time
File Lines Funcs Branches server.py69.7 62.5 42.9 __init__.py30.0 50.0 50.0 __main__.py0 0 100 git's 442-581 is
list_toolsandcall_tool. The well-tested pure functions arefetch_url, thegit_*helpers with their flag-injection guards, andTimeServer.Fix first: git fixture on Windows
test_repositorycallsshutil.rmtreewhile GitPython'sgit cat-fileprocesses still have their cwd inside the repo. 40 of 47 tests then error at teardown withPermissionError: [WinError 32]. Callingtest_repo.close()before thermtreefixes it (verified). This also unblocks #1149.Running
serve()in-processPatch
mcp_server_<name>.server.stdio_serverwith anasynccontextmanagerthat yields the server side ofmcp.shared.memory.create_client_server_memory_streams(). Then runserve(...)in an anyio task group and openClientSessionon the client side. This works for all three servers today. Thecreate_server(...) -> Serversplit this issue asks for can follow without changing those tests.More on the in-process route
- The git suite already patches
stdio_serverintest_serve_run_does_not_raise_exceptions. create_server(...)is the cleaner shape forcreate_connected_server_and_client_session.- The low-level
call_toolwrapper validates arguments againstinputSchemabefore the handler runs and returnsisError"Input validation error". - It also folds any handler exception into an
isErrorresult. get_promptletsMcpErrorthrough as a JSON-RPC error.
What the new suites should pin
All verified against the source on
main, most by drivingserve()in-process.fetch
call_toolnever checksname(line 224), socall_tool("nope", {"url": ...})fetches the URL.- Redirects are followed with no private-IP guard (
mcp-server-fetchfollows redirects with no private-IP / metadata SSRF guard | #4838). - Two timeouts are hard-coded (fetch: hardcoded 30s timeout with no CLI/env/per-request override #4448).
- Proxy configuration errors reach the client as bare text (Fetch extension fails with "Unknown scheme for proxy URL URL('socks://127.0.0.1:2080/')" #767, Using SOCKS proxy, but the 'socksio' package is not installed. #1401).
fetch details
- Characterize
mcp-server-fetchfollows redirects with no private-IP / metadata SSRF guard | #4838 so the fix has a test to change. - fetch: hardcoded 30s timeout with no CLI/env/per-request override #4448: 30s on the page and httpx's 5s default on robots.txt.
- Proxy errors are raised at
AsyncClient()construction, outside theexcept HTTPError. - Also cover the
list_toolsschema withexclusiveMaximum/exclusiveMinimum([bug] mcp-server-fetch tool json schema can‘t use in Google Gemini 2.5 pro #1624),call_toolwith onlyurl(fetch fails without optional settings #2035), pagination withstart_index/max_length,raw, and thefetchprompt. - Error branches: invalid URL, robots disallow, HTTP error, empty content, and the robots.txt connection failure at 82-83. That last one is the cheapest uncovered path in the file.
- Dead branches at 231-232 and 245-246 will hold the file below 100% branches and can go.
git
call_toolfor all 12 tools, and the error text for a badrepo_path.- Success messages that lie: git_commit reports success and creates an empty commit when nothing is staged #4762 (PR fix(git): refuse to commit when nothing is staged #4761), git_add reports 'Files staged successfully' when nothing was staged #4763, git_checkout reports "Switched to branch 'X'" when the checkout actually detached HEAD #4804 (PR fix(git): report a detached HEAD from git_checkout #4805), and
git_branchwith an unknownbranch_type. - A nonexistent
--repositorykills the server with a traceback. - A
--repositorypointing at a subdirectory returns early (mcp-server-git: Make--repository .auto-detect git root like standard git commands #3029). list_reposis dead code, although Enforce current roots on every git tool call #3613 assumes otherwise. Delete it or wire it up, then test.- Regression guards for .git folder staged by running 'git add .' #628 and [Git MCP] Blob or Tree named 'XXX.py^0' not found #1682.
git details
git_commitwith nothing staged creates an empty commit (git_commit reports success and creates an empty commit when nothing is staged #4762).git_addstages nothing (git_add reports 'Files staged successfully' when nothing was staged #4763).git_checkoutdetaches HEAD (git_checkout reports "Switched to branch 'X'" when the checkout actually detached HEAD #4804).git_branchreturns "Invalid branch type" as a non-error result (282-283).serve()294-300 catches onlyInvalidGitRepositoryError. A nonexistent path raisesNoSuchPathError.- The
by_roots/by_commandlinebranches oflist_reposare dead too. Nothing ever sendsroots/list. - .git folder staged by running 'git add .' #628 (
git add .staging.git) is fixed at db96050. - [Git MCP] Blob or Tree named 'XXX.py^0' not found #1682 (
git_showwithHEAD:path) and lines 228/232 need a rename-only or binary commit.
time
list_toolsnames and descriptions ([Feature]: Unify the names of the Time MCP server tools #3201 and mcp-server-time: Default Time Zone #2853 request changes to them),call_toolfor both tools, and the error branches.
time details
- Error branches:
timezone: ""and a bad timezone or time string. The SDK catches an absent key. convert_time's missing-key branch at 197-201 is unreachable through the protocol.
all three
serverInfo.versionis themcpSDK version, not the package version (Sync reported server versions with package.json and pyproject.toml #360 applies here too).main()is coverable in-process by patchingserve. Only__main__.pyneeds a subprocess.
Not gaps after all
- [Security] mcp-server-git: validate_repo_path opt-in bypass (architectural complement of CVE-2025-68145) #4550 (no
--repositorycase) and the symlink escape already have unit tests. - mcp-server-fetch hard-codes use_readability=True which requires Node.js (undeclared); fails silently when Node is unavailable or misconfigured #4199: readabilipy already falls back to pure Python when Node is absent. Only a hanging Node subprocess is untestable.
- error with mcp-server-time #1501 is an mcpo proxy issue.
get_local_tzis fully tested.
The scan is scripted and can be rerun against
v2/mainas slices land, with per-file deltas against this baseline.
Scan and verification done with help from Claude Code. The coverage numbers come from a real run; the per-issue verdicts were checked against the source but are a starting point, not a review.
- The git suite already patches
- linked a pull request that will close this issuechore(python): per-file 90% coverage gate in CI and local:gate (#4855) #4976
on Oct 4, 2026 - added 5 commits that reference this issue
on Oct 4, 2026 4 remaining items
- added 8 commits that reference this issue
on Oct 4, 2026 - changed the title
[-]2026-07-28 Spec Refactor Part 2: Python servers: extensive unit tests + per-file 90% coverage gate[/-][+]2026-07-28 Spec Refactor Wave 1 Part 2: Python servers: extensive unit tests + per-file 90% coverage gate[/+]on Oct 5, 2026 - removed a parent issue
on Oct 10, 2026 - added a parent issue
on Oct 10, 2026 - changed the title
[-]2026-07-28 Spec Refactor Wave 1 Part 2: Python servers: extensive unit tests + per-file 90% coverage gate[/-][+]Bugs, Coverage, and SDK Wave 1 Part 2: Python servers: extensive unit tests + per-file 90% coverage gate[/+]on Oct 10, 2026
Part of the 2026-07-28 Spec Refactor tracker #4857. This is the first step, and has no dependencies. It can run in parallel with Part 1 (#4854).
Goal
Before any SDK or spec changes, build an extensive pytest suite that pins down the current behavior of every Python server (
fetch,git,time) onmcp1.x. The suite must pass the coverage quality gate from #4474.This suite is the regression net for Part 4 (#4851). The SDK v2 migration is supposed to be transparent on the wire, and there is no codemod for Python, so the test suite is the only safety net for a port done by hand. Existing tests may be deleted where the new suite covers them better.
Test design
mcpClientSessionto each server over in-memory streams, and assert on the wire-level results: tool lists, call results and errors.uvx/console-script entry point as a subprocess.inputSchema→input_schema) and moves types tomcp_types. Where practical, compare against the wire JSON (e.g.model_dump(by_alias=True, mode="json")) rather than Python attribute names, so Part 4 doesn't have to rewrite assertions.fetch:start_index/max_length) and raw mode.git:time: timezone lookup and conversion, DST edges, invalid input.pytest-asyncioconsistently. Standardize the test directory name:timecurrently usestest/, the others usetests/.Quality gate (from #4474, modeled on the inspector's
v2/mainAGENTS.md)pytest-cov/coverage.py. coverage.py'sfail_underis global, not per-file, so it isn't enough on its own. Implement per-file enforcement, e.g.coverage jsonplus a small check script with the same semantics as the TS gate.# pragma: no cover # <reason>instead.uv run pytestloop. CI posture (amended): CI enforces the per-file gate in a parallel coverage job, matching Part 1 (Bugs, Coverage, and SDK Wave 1 Part 1: TypeScript servers: extensive unit tests + per-file 90% coverage gate #4854) as amended. Maintainers decided this on PR docs: agentic software factory inception (docs/agent-guidance-inception.md) #4861. This issue wires it in (changed from the earlier plan, which left the wiring to Add local:gate, pre-push-gate skill #4871): besides the command and the per-file check script, add the parallel coverage job topython.ymland a Python coverage stage tolocal:gate, and record the stage indocs/quality-gate.mdand thepre-push-gateskill. Add local:gate, pre-push-gate skill #4871 buildslocal:gatefirst, without coverage, so that it does not have to wait for this suite.Acceptance criteria
npm run local:gatefails on it too.main(mcp1.x) in CI.Carried over from #4474, which this issue supersedes
npm run coverage.AGENTS.mdunder "Always test new or modified code", in this issue's PR: per-file ≥ 90 on lines and branches (the dimensions coverage.py measures; it has no native function dimension), and a reason on every# pragma: no cover. Bugs, Coverage, and SDK Wave 1 Part 1: TypeScript servers: extensive unit tests + per-file 90% coverage gate #4854 adds the TypeScript half.