A tool contract is a written promise about what a caller can rely on. For a CLI that means the command name, the arguments it accepts, the exit codes it returns, what appears on stdout, what appears on stderr, and any files or state it leaves behind. For an MCP server it means the tool name, the input schema, required fields, authorization scope, success result, and failure behavior. If any of that changes unexpectedly, an agent or a client breaks even though the code still compiles, which is why testing the method in isolation is not enough. A production MCP server should test the contract it exposes over the protocol, including tool discovery, names, input schemas, valid and invalid parameters, error behavior, result structure and regression cases the C# Corner guide to MCP tool contracts.
The cheapest way to keep that promise testable is to split it into clauses. A clause is one sentence a caller could act on: "exits 2 when the required flag is missing", "writes JSON to stdout and progress to stderr", "creates the output directory if it does not exist", "returns code 5 when the resource already exists". Each clause becomes one recorded run. This is deliberate: it forces you to write down the contract before the test, which is the same discipline that makes acceptance criteria useful to an agent. Specific, testable criteria give the agent a clear target and give the validator something concrete to check the dev.to account of validating agent-written code.
Exit codes deserve particular attention because agents treat them as control flow. An agent that gets exit code 5 can decide to skip creation and move on, while an agent that gets 1 for everything has to parse stderr and will sometimes guess wrong the CLI guide for AI agents. Build your case list from the documented codes, not from the branches in your code:
| Contract clause | Case type | Recording needed | Live run |
|---|---|---|---|
| Success path with valid args | Happy path | Yes, one per command | No |
| Missing required flag | Usage error | Yes | No |
| Unknown flag or bad value | Usage error | Yes | No |
| Resource not found | Domain error | Yes | No |
| Permission denied / auth failure | Auth error | Yes, with a synthetic token | Yes, one per release |
| Conflict, resource exists | Domain error | Yes | No |
| Progress and warnings on stderr | Output shape | Yes | No |
| Side effect on the filesystem | Side effect | Yes | No |
| OS-specific behavior (signals, paths) | Platform | Minimal stub | Yes, on that OS |
| Upstream API change | External | No | Yes, scheduled |
The split is a decision rule, not a taste: if the clause depends on state you control (arguments, files, a local database) record it. If it depends on state you do not control (a token, a remote provider, a kernel signal), keep it live and small.



