# Making an MCP client library checkable: a runnable example, bounded pagination and a weekly conformance run

> Source: <https://dev.to/yusufihsangorgel/making-an-mcp-client-library-checkable-a-runnable-example-bounded-pagination-and-a-weekly-i49>
> Published: 2026-09-23 09:02:58+00:00

A Dart package for the Model Context Protocol earns trust in three ways. A reader can clone the repository, run a client and a server, and watch them talk. A caller can use helpers that list everything a server has without risking an infinite loop. A scheduled job checks the package against the protocol's own conformance suite on a regular cadence. This article walks through the pull requests that made those three things true for `package:dart_mcp`, plus two protocol edge cases and the documentation that holds it together.

The repository already had a server example at `example/streamable_http_server.dart`, but no client to pair with it. The example readme described how to poke the server with a curl command. That is a workable smoke test, but it does not exercise the package from the client side, and it does not show a reader what a real client session looks like.

[PR #671](https://github.com/dart-lang/ai/pull/671) added a streamable HTTP client example. It takes the URL the server prints when it starts, discovers the server, lists its tools, calls `greet`, and prints the progress notification that arrives while the call runs. The example readme now describes the pair of programs instead of the curl command.

This is the kind of change that looks small and pays out repeatedly. Anyone can now run both sides locally, watch the handshake and the tool call happen, and confirm that progress notifications actually flow. The work was part of issue #668.

The MCP list requests can page their results. A server returns a page of items and a `nextCursor`, and the client sends the cursor back to get the next page. A client that wants every tool from a server that pages its list has to thread the cursor by hand: call, check for a cursor, call again with it, accumulate, repeat.

[PR #682](https://github.com/dart-lang/ai/pull/682) moved that loop into the package. Four new methods, `listAllTools`, `listAllResources`, `listAllResourceTemplates` and `listAllPrompts`, walk the pages and yield the items as a `Stream`. Callers get a stream of items and never think about cursors.

Two details in that pull request matter more than the convenience.

First, each single-page method stays byte-identical. Existing callers keep their exact behaviour, and the new helpers sit beside them rather than replacing them. If you only want the first page, the old method still gives you exactly that.

Second, the helpers cannot loop forever. A buggy or hostile server could return a cursor on every page indefinitely, and a naive `while (cursor != null)` loop would spin without end. The new methods carry a default bound of 64 pages and throw when they exceed it. Passing `null` as the bound lifts it for callers who genuinely want unbounded walking. The tests cover the cursor threading, the bound, the argument check and an empty page.

The bound is the part I would argue for hardest. A helper that can hang turns a server bug into a stuck client process. Making the safe behaviour the default and the unsafe one an explicit opt-in is the right shape for this API. Worth noting that the conformance fixture under `tool/` drops `nextCursor` today, meaning the existing fixture never exercises pagination, and the tests had to cover it directly. This work was part of issue #28.

Two smaller pull requests fixed corners of the protocol surface.

[PR #685](https://github.com/dart-lang/ai/pull/685) addressed sampling content. In the schema, both `SamplingMessage.content` and `CreateMessageResult.content` accept five block types or an array of them. The getter cast straight to a single block, and a list on the wire threw. Issue #672 flagged that array-valued message content wanted its own change. After the pull request, both shapes read as a list, with one block still going out as that block on serialization. Reading accepts what the schema permits, while writing keeps the simple form simple.

[PR #684](https://github.com/dart-lang/ai/pull/684) fixed a status code on the server side. A branch that rejects a request based on its media type answered with 415, while the schema's `HeaderMismatch` carries `400 Bad Request`. A revision that requires the `Mcp-Method`, `Mcp-Name` and `MCP-Protocol-Version` headers does not count a media type among them, and elsewhere in the same file that code always pairs with 400. The branch now matches the oversized-body case, which already gets a generic invalid request error with its own 413. One bookkeeping note: since the handler landed after 0.5.2, its changelog line states the behaviour rather than describing a change.

[PR #675](https://github.com/dart-lang/ai/pull/675) made the repository run the MCP conformance suite against both fixtures under `tool/` on a schedule. The triggers are a weekly schedule, a manual dispatch, or any pull request that changes the package.

The details of the job reflect where the suite itself stands. The suite is still an alpha npm package, and the job carries `continue-on-error`, meaning a failure stays visible without failing the run. That is the honest configuration for a dependency that is expected to shift underneath you: you want to see the signal without gating every commit on an alpha tool.

The results give a concrete picture. Every scored 2026-07-28 scenario passes on the server run, with only the `tasks` extension failing. On the client run, a baseline file names the auth scenarios. The package has no OAuth client, and those listed scenarios are the accepted failures. Anything outside that list failing causes the job to fail, as does a listed scenario that starts passing. The second rule is the one I care about: when a known failure quietly disappears, the job fails until the baseline is updated. The list cannot go stale without anyone noticing.

This pull request was a follow-up to #491. A pair of example programs proves the package works for one conversation. The conformance suite checks it against the protocol's own definition of correct, every week, without anyone remembering to ask.

Code that a contributor cannot navigate does not stay healthy. [PR #673](https://github.com/dart-lang/ai/pull/673) added a `DEVELOPING.md` for the package. It is short and covers what someone needs to work on the package: where the schema files live, the checks CI runs, the SDK the format check needs, the conformance fixtures, and the changelog convention. I named it `DEVELOPING.md` rather than a second `CONTRIBUTING.md`, since the repository root already has one, and the new file links there. The same pull request fixed a stale pointer: the library comment in `api.dart` still referenced the 2025-06-18 schema file, and it now points at the schema directory. This closes issue #20.

[PR #677](https://github.com/dart-lang/ai/pull/677) cleaned up the README after a feature removal. Taking elicitation, sampling and roots out of the server left a readme section describing a call pattern that no longer exists. It now describes answering with an `InputRequiredResult`, and that answer serves every revision. Streamable HTTP also moved from a construction marker to supported in the support table. Neither the handler nor the client channel carries an unfinished piece, and the row's note already names what the revision removed.

Documentation drift is not cosmetic here. A README section describing a removed pattern sends every new reader down a dead end, and a support table with stale markers misstates what the package can do.

These changes landed over one week: the conformance run on September 15, the README on September 16, pagination, both protocol fixes and `DEVELOPING.md` on September 21, and the client example on September 22. Looking back at them, three choices are the ones I would repeat. New helpers went beside the existing single-page methods instead of replacing them, and no caller had to change a line. The safe behaviour became the default: a 64-page bound that throws, with `null` as an explicit opt-out. And a dependency that is still alpha got a job that shows its failures without gating every pull request, with a baseline checked in both directions. None of this asks a reader to trust anything they cannot run, bound, or see checked on a schedule.
