4 min read
Contributing to the MCP Python SDK: Exposing progress_callback in ServerSession
Status: Open PR (#2041) · Awaiting maintainer review
What changed: ServerSession.* now accepts progress_callback (in PR branch)
Proof: PR #2041 · CI checks (all green)
Last verified: Feb 13, 2026
What's next: Will post an update if/when merged

The Model Context Protocol Python SDK had a gap: its ServerSession class didn’t expose the progress_callback parameter on high-level methods like create_message, elicit_form, and elicit_url — even though the underlying BaseSession.send_request() fully supported it. This meant servers couldn’t receive progress notifications from clients during sampling or elicitation, a capability that ClientSession.call_tool() already had on the client-to-server side.

I opened PR #2041 to fix issue #1671. Here’s how it went.

The Fix

The implementation is straightforward — adding progress_callback: ProgressFnT | None = None to every affected method and threading it through the three-layer call stack:

Context.elicit() / Context.elicit_url()         # user-facing API
  └─▸ elicit_with_validation() / elicit_url()    # validation helpers
      └─▸ ServerSession.elicit_form()            # session method
          └─▸ BaseSession.send_request()         # transport

Each layer simply passes progress_callback=progress_callback to the next. No new abstractions, no conditional logic — just plumbing a parameter that was already supported at the bottom but not exposed at the top. Four files changed, +143 lines.

I intentionally excluded list_roots() and send_ping() — neither involves long-running work where progress reporting makes sense.

Researching the Maintainer

Before writing the PR, I studied 90+ pull requests reviewed by the issue author to understand what they look for. Key patterns:

  • Performance sensitivity — they flagged repeated inspect.isawaitable calls in a hot path
  • Spec compliance — objected to premature naming before the spec was finalized
  • Testing requirements — explicitly asks for unit tests on bug fixes
  • Approval style — concise and direct

This shaped everything. The PR title uses feat: conventional commit format, the body leads with spec alignment arguments (TypeScript SDK parity, existing ClientSession.call_tool() pattern), and the testing section is detailed.

The CI Surprise

All 1,140 tests passed on the first push. But CI failed across all 20 matrix jobs.

The culprit wasn’t a test failure — it was the 100% branch coverage requirement. My test file had two if context.meta and "progress_token" in context.meta: guards that always evaluate True by design (the test always provides a progress_callback, so the progress token is always present). The false branches were never exercised, dropping total coverage from 100% to 99.99%.

The fix was adding # pragma: no branch to both lines, matching an existing pattern used throughout the codebase. Second commit pushed, all 26 checks went green.

Knowing to search for “Coverage failure” instead of “FAILED” in the CI logs saved significant debugging time — pytest’s “FAILED” keyword appeared 19 times in test parameter names like test_poll_task_exits_on_terminal[failed], all of which were passing tests.

Lessons

Coverage !== correctness. A 100% coverage gate can fail a PR where every test passes. Understanding what the CI actually checks — not just “do tests pass” — is essential.

Maintainer research pays off. Reading reviewed PRs before writing yours surfaces specific preferences that make your PR feel native to the project. The difference between a PR that gets merged and one that languishes is often in the framing, not the code.

The simplest change can be the most impactful. The actual code change is just passing a parameter through a handful of method signatures. But it unlocks a capability that was architecturally supported yet practically inaccessible — and brings the Python SDK into parity with TypeScript.

The PR is open at modelcontextprotocol/python-sdk#2041, awaiting maintainer review.