mcp-groundtruth 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,561 @@
1
+ Metadata-Version: 2.5
2
+ Name: mcp-groundtruth
3
+ Version: 0.2.0
4
+ Summary: Behavioral conformance harness for Model Context Protocol tool annotations.
5
+ Project-URL: Repository, https://github.com/shreyasbhakta/mcp-groundtruth
6
+ Project-URL: Methodology, https://github.com/shreyasbhakta/mcp-groundtruth/blob/main/docs/methodology.md
7
+ Project-URL: Changelog, https://github.com/shreyasbhakta/mcp-groundtruth/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/shreyasbhakta/mcp-groundtruth/issues
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ License-File: NOTICE
12
+ Keywords: conformance,mcp,model-context-protocol,testing,tool-annotations
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.11
26
+ Requires-Dist: mcp==2.3.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # mcp-groundtruth
30
+
31
+ [![CI](https://github.com/shreyasbhakta/mcp-groundtruth/actions/workflows/ci.yml/badge.svg)](https://github.com/shreyasbhakta/mcp-groundtruth/actions/workflows/ci.yml)
32
+ ![Python](https://img.shields.io/badge/python-3.11%20%7C%203.14-blue)
33
+ ![License](https://img.shields.io/badge/license-Apache--2.0-green)
34
+
35
+ **A behavioral conformance harness for Model Context Protocol tool annotations.**
36
+
37
+ MCP servers describe each tool with four behavioral hints: `readOnlyHint`,
38
+ `destructiveHint`, `idempotentHint` and `openWorldHint`. The specification
39
+ calls them hints and requires clients to treat annotations as untrusted
40
+ unless they come from trusted servers [1]. mcp-groundtruth runs the tools
41
+ against observable state and checks whether what a tool declares matches
42
+ what it does.
43
+
44
+ > **Scope of release 0.1.** This release tests only the fixture servers in
45
+ > this repository, each built with a known expected outcome, to show the
46
+ > harness produces correct verdicts. It does not test any third-party MCP
47
+ > server and makes no claims about the MCP ecosystem.
48
+
49
+ ---
50
+
51
+ ## Contents
52
+
53
+ - [How it works](#how-it-works)
54
+ - [Verdict model](#verdict-model)
55
+ - [Quick start](#quick-start)
56
+ - [Command line](#command-line)
57
+ - [Reports](#reports)
58
+ - [Fixtures](#fixtures)
59
+ - [Defining a fixture](#defining-a-fixture)
60
+ - [Auditing third-party servers](#auditing-third-party-servers)
61
+ - [Audit results](#audit-results)
62
+ - [Design guarantees](#design-guarantees)
63
+ - [Limitations](#limitations)
64
+ - [Development](#development)
65
+ - [Related work](#related-work)
66
+ - [References](#references)
67
+ - [License](#license)
68
+
69
+ ---
70
+
71
+ ## How it works
72
+
73
+ ```mermaid
74
+ flowchart TB
75
+ subgraph DEF["1 · Fixture definition"]
76
+ TOML["fixture.toml<br/>adapters · classification · masks · expected verdicts"]
77
+ SEED["seed<br/>SQL script · directory · JSON file"]
78
+ end
79
+
80
+ subgraph EXEC["2 · Execution"]
81
+ LOAD["Fixture loader<br/>strict schema"]
82
+ RUN["Runner<br/>reset · seed · control / once / twice / concurrent sessions"]
83
+ WIRE["Wire client<br/>JSON-RPC over stdio · verbatim capture · timing"]
84
+ end
85
+
86
+ subgraph SUT["3 · System under test"]
87
+ SRV["MCP server<br/>stdio process"]
88
+ STORE[("Backing store<br/>sqlite · files · JSON")]
89
+ end
90
+
91
+ subgraph ANA["4 · Analysis"]
92
+ CON["Contract parser<br/>tri-state hints from raw JSON"]
93
+ SNAP["Snapshot adapters<br/>typed records · coverage"]
94
+ NORM["Normalizer<br/>declared volatile masks"]
95
+ VER["Verdict engine<br/>falsifiable claims · reasons"]
96
+ REP["Reporter<br/>JSON evidence · text summary"]
97
+ end
98
+
99
+ TOML --> LOAD --> RUN
100
+ SEED --> RUN
101
+ RUN --> WIRE
102
+ RUN -- "reset and seed" --> STORE
103
+ WIRE <-- "stdio" --> SRV
104
+ SRV <--> STORE
105
+ WIRE -- "raw tools/list" --> CON
106
+ STORE -- "after each session" --> SNAP
107
+ SNAP --> NORM --> VER
108
+ CON --> VER
109
+ RUN -- "session evidence" --> VER
110
+ VER --> REP
111
+ ```
112
+
113
+ 1. **Reset verification.** The runner resets the store from its seed twice
114
+ and requires the two snapshots to be identical after normalization. If
115
+ they differ, every verdict for that fixture is `RESET_UNVERIFIED`.
116
+ 2. **Sessions.** Every session starts from a clean reset with the same
117
+ opening: start the server, `initialize`, `tools/list` (all pages).
118
+ - A **control** session stops there.
119
+ - A **once** or **twice** session then calls the tool one or two times.
120
+ - **Concurrent** attempts issue two calls at the same time, either on one
121
+ connection or through two server instances sharing the store.
122
+
123
+ State is observed after the server exits.
124
+ 3. **Declared contracts** are parsed from the `tools/list` response text
125
+ exactly as received. Each hint is `TRUE`, `FALSE` or `ABSENT`, and absent
126
+ is never treated as false.
127
+ 4. **Observation.** Adapters snapshot each surface into typed, sorted
128
+ records. The normalizer masks only the volatile columns the fixture
129
+ declares. Deltas are reported separately for **business** state,
130
+ **append-only** state (logs, audit rows) and **unclassified** state.
131
+ 5. **Verdicts** compare the observed behavior with the falsifiable form of
132
+ each hint. Every verdict carries its evidence.
133
+
134
+ | Test | Hint | Compares | Fails when |
135
+ |---|---|---|---|
136
+ | sequential idempotence | `idempotentHint` | once vs twice, separate runs from clean reset | business state differs |
137
+ | concurrent idempotence | `idempotentHint` | once vs two concurrent calls | business state differs in any attempt |
138
+ | read-only | `readOnlyHint` | control vs once | any observed state differs |
139
+ | destructive | `destructiveHint` | control vs once | a pre-existing business or append-only record is gone or overwritten |
140
+
141
+ `openWorldHint` is parsed and reported but not tested in this release: the
142
+ harness makes no network access by design.
143
+
144
+ ---
145
+
146
+ ## Verdict model
147
+
148
+ | Outcome | Meaning |
149
+ |---|---|
150
+ | `PASS` | A claim that could have failed was tested and held. |
151
+ | `VIOLATION` | Behavior contradicts an explicit declaration on observed state, reproduced in at least 3 independent runs (concurrent test: in any attempt). |
152
+ | `INCONCLUSIVE` | No judgment possible; carries a reason code. |
153
+ | `NOT_APPLICABLE` | Nothing falsifiable to test; carries a reason code. |
154
+
155
+ Only three declared values make a claim that behavior can contradict:
156
+ `readOnlyHint: true`, `destructiveHint: false` and `idempotentHint: true`.
157
+ **Violation rates use only these as the denominator.**
158
+
159
+ | Reason | Applies when |
160
+ |---|---|
161
+ | `NO_DECLARATION` | The hint was not sent. |
162
+ | `MALFORMED_DECLARATION` | The hint was sent, but not as a JSON boolean (`"true"`, `1`, `null`, a duplicated key). |
163
+ | `PERMISSIVE_DECLARATION` | The declared value is permissive (`readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`). |
164
+ | `SUPERSEDED_BY_READ_ONLY` | An explicit `readOnlyHint: true` supersedes the destructive and idempotence tests. |
165
+ | `PARTIAL_COVERAGE` | Some declared state location is not observed. Blocks PASS, never VIOLATION. |
166
+ | `UNCLASSIFIED_STATE` | The only change is in state the fixture did not classify. |
167
+ | `RESET_UNVERIFIED` | Two resets did not produce the same state. |
168
+ | `NONDETERMINISTIC` | Runs that should agree did not; the evidence names the differing columns. |
169
+ | `CALL_ERROR` | An error response with no state change, or a dead connection. |
170
+ | `SETUP_FAILED` | Reset, seeding, server start or listing failed. |
171
+ | `NO_OVERLAP_OBSERVED` | Concurrent test: no race seen, and no attempt overlapped. |
172
+ | `TWO_PROCESS_SKIPPED_STARTUP_WRITES` | Concurrent test: two-instance attempts skipped because the server writes state on startup or shutdown. |
173
+
174
+ Each verdict has one primary reason and may list secondary reasons, so a
175
+ `CALL_ERROR` stays visible under `NO_DECLARATION`. The concurrent test is
176
+ one-sided. A single diverging attempt is a conclusive VIOLATION. A PASS only
177
+ means no race was observed. The full rules are in
178
+ [docs/methodology.md](https://github.com/shreyasbhakta/mcp-groundtruth/blob/main/docs/methodology.md).
179
+
180
+ ---
181
+
182
+ ## Quick start
183
+
184
+ Requirements: Python 3.11 or later. macOS or Linux.
185
+
186
+ Install the `mcpgt` command:
187
+
188
+ ```bash
189
+ pip install mcp-groundtruth # once published to PyPI
190
+ uvx --from mcp-groundtruth mcpgt --help # or run it without installing
191
+ uv tool install git+https://github.com/shreyasbhakta/mcp-groundtruth # from source, today
192
+ ```
193
+
194
+ To test your own server, follow [Test your own MCP server](https://github.com/shreyasbhakta/mcp-groundtruth/blob/main/docs/your-server.md),
195
+ which walks a small example server from `mcpgt init` to a report and shows
196
+ how to gate CI on it with this repository's GitHub Action.
197
+ To run this repository's fixtures, use a checkout and
198
+ [uv](https://docs.astral.sh/uv/):
199
+
200
+ ```bash
201
+ git clone https://github.com/shreyasbhakta/mcp-groundtruth.git
202
+ cd mcp-groundtruth
203
+ uv sync
204
+ uv run mcpgt run liar-concurrent
205
+ ```
206
+
207
+ ```text
208
+ liar-concurrent run 20261014T003848101991Z-liar-concurrent (3 runs)
209
+ reset: verified
210
+ coverage: complete
211
+ control sessions: stable
212
+
213
+ tool test declared verdict observed expected
214
+ register sequential_idempotence true PASS idempotent=holds ok
215
+ register concurrent_idempotence true VIOLATION idempotent=fails ok
216
+ attempts: single_connection: 20/20 diverged; two_process: 20/20 diverged,
217
+ overlap in_flight 20; produced by: single_connection, two_process
218
+ register read_only false NOT_APPLICABLE:PERMISSIVE_DECLARATION read_only=fails ok
219
+ register destructive false PASS non_destructive=holds ok
220
+
221
+ expectations: 4/4 matched
222
+ ```
223
+
224
+ This fixture deduplicates with check-then-insert and no uniqueness
225
+ constraint. Sequential calls look idempotent. Concurrent calls both insert.
226
+ (Output columns condensed for width.)
227
+
228
+ ---
229
+
230
+ ## Command line
231
+
232
+ | Command | Purpose |
233
+ |---|---|
234
+ | `mcpgt init <dir> [--name N] [--env K=V] [--force] [--no-sandbox] -- <server command>` | Start a server, list its tools, and write a starting `target.toml` with `TODO(mcpgt)` placeholders for everything only you can decide. |
235
+ | `mcpgt verify-reset <fixture> [--json]` | Reset twice and confirm identical normalized state. |
236
+ | `mcpgt run <fixture> [--tool NAME] [--runs N] [--concurrent-attempts N] [--runs-dir DIR]` | Run the test battery, write a JSON report, print a summary. |
237
+ | `mcpgt prepare <target>` | Fetch and build an audit target's pinned source (the only networked step). |
238
+ | `mcpgt run <target> --audit [--observe-concurrent] [--fail-on-violation]` | Audit a server: no expected verdicts, sandboxed. Exits 0 whatever the verdicts, unless `--fail-on-violation` is given. `--observe-concurrent` adds observation-only concurrent attempts for tools without a falsifiable idempotence claim. |
239
+ | `mcpgt report <run-id> [--format text\|json] [--runs-dir DIR]` | Show a saved report. |
240
+ | `mcpgt --version` | Print the version. |
241
+
242
+ `<fixture>` / `<target>` is a directory name under `fixtures/` or `targets/`,
243
+ or a path to one. `--runs` defaults to and may not be below 3.
244
+ `--concurrent-attempts` (per mode) defaults to 20. `--tool` is repeatable and
245
+ takes a tool name (all its argument variants) or one variant as
246
+ `name[variant]`.
247
+
248
+ | Exit code | Meaning |
249
+ |---|---|
250
+ | `0` | Success: every verdict matched its expectation (`run`), or reset verified (`verify-reset`). |
251
+ | `1` | A verdict did not match its expectation (`run`), any verdict is VIOLATION (`run --fail-on-violation`), or reset not verified (`verify-reset`). |
252
+ | `2` | Invalid fixture definition, arguments, or run id. |
253
+
254
+ ---
255
+
256
+ ## Reports
257
+
258
+ `mcpgt run` writes `runs/<run-id>/report.json` (schema `mcpgt.report/0.2`),
259
+ a self-contained evidence record:
260
+
261
+ - **Provenance**:
262
+ - the fixture's source, git SHA, working-tree dirtiness and content digest;
263
+ - the harness's SHA and dirtiness;
264
+ - the SDK version, Python version and capture time.
265
+ - **Raw protocol data**: every `tools/list` page and the `initialize`
266
+ response, verbatim, plus every tool response.
267
+ - **Reset verification and control sessions**, including the control's own
268
+ effect on the seeded state.
269
+ - **Per verdict**:
270
+ - the full tri-state contract;
271
+ - the observed property and behavior;
272
+ - deltas per run, by state category;
273
+ - affected categories;
274
+ - every errored call (session, position, `tool_error` or
275
+ `protocol_error`, and whether state changed);
276
+ - nondeterminism evidence;
277
+ - coverage, run count, and primary and secondary reasons with details.
278
+ - **Concurrent verdicts** also carry:
279
+ - every attempt, with its mode, overlap and timings;
280
+ - counts per mode;
281
+ - the mode that produced the verdict.
282
+ - **Expectation check** against the fixture's recorded ground truth.
283
+
284
+ Verdicts and observed deltas are deterministic for a given fixture and
285
+ seed. Timestamps, SHAs and timing measurements are capture metadata.
286
+
287
+ ---
288
+
289
+ ## Fixtures
290
+
291
+ Each fixture is a small, real MCP server over stdio with its expected
292
+ verdicts recorded in `fixture.toml`. The harness is correct when every
293
+ fixture gets exactly its expected verdicts. `tests/test_ground_truth.py`
294
+ enforces this for all 72 (tool, test) pairs.
295
+
296
+ | Fixture | Behavior | Key expected verdicts |
297
+ |---|---|---|
298
+ | `honest-readonly` | Declares read-only; only reads. | read-only PASS |
299
+ | `honest-idempotent` | Upsert by key; one audit row per call. | sequential PASS; concurrent PASS |
300
+ | `liar-idempotent` | Declares idempotent; inserts a row per call. | sequential and concurrent VIOLATION |
301
+ | `liar-concurrent` | Check-then-insert with no uniqueness constraint. | sequential PASS; concurrent VIOLATION |
302
+ | `liar-readonly` | Declares read-only; writes an append-only log. | read-only VIOLATION |
303
+ | `silent-safe` | Declares nothing; genuinely idempotent. | NO_DECLARATION; behavior reported as idempotent |
304
+ | `opaque` | Real state in a file no adapter observes. | PARTIAL_COVERAGE, never PASS |
305
+ | `flaky` | Random token per call. | NONDETERMINISTIC, naming the column |
306
+ | `malformed-hints` | Hand-written JSON-RPC server emitting string, number, null, duplicate-key and list annotations. | MALFORMED_DECLARATION |
307
+ | `liar-readonly-error` | Declares read-only; writes, then returns `isError`. | VIOLATION with side effects; a clean failure is CALL_ERROR |
308
+ | `startup-writer` | Writes a boot-log row on every start. | read-only PASS; concurrent NO_OVERLAP_OBSERVED |
309
+
310
+ ---
311
+
312
+ ## Defining a fixture
313
+
314
+ ```toml
315
+ [fixture]
316
+ name = "honest-idempotent"
317
+ command = ["{python}", "server.py"] # {python} is the harness interpreter
318
+
319
+ [state]
320
+ locations = ["sqlite:state.db"] # ground truth: every place the server keeps state
321
+
322
+ [[adapters]]
323
+ kind = "sqlite" # sqlite | fs | json
324
+ label = "state.db" # observed at <state dir>/state.db
325
+ seed = "seed.sql"
326
+ business = ["items"]
327
+ append_only = ["audit"]
328
+ mask = [{ region = "audit", column = "at", kind = "timestamp" }]
329
+
330
+ [[tools]]
331
+ name = "upsert_item"
332
+ arguments = { key = "a", value = "new" }
333
+
334
+ [tools.expected.sequential_idempotence]
335
+ verdict = "PASS"
336
+ append_only_changed = true
337
+ ```
338
+
339
+ The server finds its state directory in `MCPGT_STATE_DIR`. Commands and
340
+ `env` values may use `{python}` (the harness interpreter), `{state}` (the
341
+ state directory), `{target}` (the definition's directory) and, for targets
342
+ with a `[source]`, `{source}` (the prepared checkout). Definitions are parsed
343
+ strictly: unknown keys are errors, every mask rule must match the seeded
344
+ state, and any value still starting with `TODO(mcpgt)` stops the definition
345
+ from loading. This prevents a typo or a forgotten decision from silently
346
+ changing what is compared.
347
+
348
+ ---
349
+
350
+ ## Auditing third-party servers
351
+
352
+ *Since 0.2.0.* Audit targets live in `targets/`.
353
+ Each pins a repository commit and a Node major version, builds it locally,
354
+ and runs it in a sandbox with no network access. Targets never carry
355
+ expected verdicts; audit reports are measurements for review.
356
+
357
+ | Platform | Network | File writes | Notes |
358
+ |---|---|---|---|
359
+ | macOS | denied (`sandbox-exec`) | confined to the state directory | full isolation |
360
+ | Linux | denied (`unshare --net`) | **not confined** | needs unprivileged user namespaces |
361
+ | Linux without user namespaces | — | — | audits refuse to run; sandbox tests are skipped in CI | `targets/memory` pins the reference memory server
362
+ from `modelcontextprotocol/servers`. See
363
+ [docs/methodology.md](https://github.com/shreyasbhakta/mcp-groundtruth/blob/main/docs/methodology.md#audit-targets).
364
+
365
+ ```bash
366
+ export PATH="/path/to/node-v24/bin:$PATH" # the target pins Node 24
367
+ uv run mcpgt prepare memory # fetch the pinned commit and build it
368
+ uv run mcpgt verify-reset memory # prove the seeded state resets exactly
369
+ uv run mcpgt run memory --audit # sandboxed audit, full evidence report
370
+ ```
371
+
372
+ ---
373
+
374
+ ## Audit results
375
+
376
+ One server has been audited: the reference memory server
377
+ (`@modelcontextprotocol/server-memory`) at `modelcontextprotocol/servers@bbb701e`,
378
+ with one seeded graph, under Node 24 on macOS. The results say nothing about
379
+ other servers or other versions of this one.
380
+
381
+ - No violations of a declared hint.
382
+ - Three over-declarations: `create_entities`, `create_relations` and
383
+ `add_observations` declare `idempotentHint: false`, but repeating a call
384
+ with identical arguments changed nothing in any of 12 argument sets tested.
385
+
386
+ Reports, hand review and caveats:
387
+ [results/memory-2026-10-14/REVIEW.md](https://github.com/shreyasbhakta/mcp-groundtruth/blob/main/results/memory-2026-10-14/REVIEW.md).
388
+
389
+ ---
390
+
391
+ ## Design guarantees
392
+
393
+ - **Absence is never false.** Hints are an explicit tri-state type that
394
+ refuses boolean coercion and comparison. The specification's defaults are
395
+ never applied.
396
+ - **Raw-byte contracts.** Contracts are parsed from the response text as
397
+ received, not from SDK models. In `mcp==2.3.0` the SDK models coerce
398
+ `"true"` and `1` to `True`. They also fail the entire listing on one
399
+ malformed tool. `tests/test_sdk_divergence.py` pins this behavior.
400
+ - **No overclaiming.**
401
+ - Incomplete coverage cannot PASS.
402
+ - Unverified resets and unstable runs cannot be judged.
403
+ - Concurrent PASS requires observed overlap.
404
+ - **Evidence-first.** A verdict without its evidence is treated as a defect.
405
+ - **Deterministic and offline.** The harness makes no network calls and
406
+ needs no external services.
407
+
408
+ ---
409
+
410
+ ## Limitations
411
+
412
+ - **stdio servers only.** Streamable HTTP and other transports are not
413
+ supported.
414
+ - **State in SQLite, files or JSON only.** Observation covers SQLite
415
+ databases, files and directory trees, and JSON or JSON Lines files.
416
+ State held anywhere else (process memory, another database, a remote
417
+ service) cannot be observed. If it is declared, coverage is partial and
418
+ verdicts that depend on it are INCONCLUSIVE. If it is not declared, it is
419
+ not seen at all. An `fs` adapter on `.` observes the whole state
420
+ directory, and on macOS audits block writes outside it.
421
+ - **`mcpgt prepare` supports git sources only.** It fetches one commit,
422
+ named by its full 40-character SHA, of a git repository (the host must
423
+ allow fetching by SHA, as GitHub does). It then runs the target's build
424
+ commands in the checkout. Those commands run unsandboxed and with network
425
+ access, so review them. Only a Node major version can be pinned
426
+ (`node = "24"`). Package name and version are read from `package.json`
427
+ only. Toolchain versions are recorded only for `node` and `npm`. Servers
428
+ in other languages can be built with their own commands, but their
429
+ toolchains are neither pinned nor recorded. Targets without a `[source]`
430
+ run the command as given, and their provenance is the definition alone.
431
+ - Tool arguments come from the definition. The harness does not generate
432
+ inputs; `mcpgt init` writes placeholders for you to fill in.
433
+ - On Linux, audit isolation blocks network but does not confine file
434
+ writes; see [Auditing third-party servers](#auditing-third-party-servers).
435
+ - Concurrent overlap for two-instance attempts uses a timing heuristic:
436
+ both calls in flight for longer than the instances' ping round-trip times
437
+ combined. Overlap on a single connection is never assumed.
438
+ - Concurrent verdicts are given only for tools declaring
439
+ `idempotentHint: true`. `--observe-concurrent` records concurrent attempts
440
+ for the others, without changing their verdicts.
441
+ - `openWorldHint` is reported but not tested.
442
+
443
+ ---
444
+
445
+ ## Development
446
+
447
+ ```bash
448
+ uv sync # runtime and development dependencies
449
+ uv run pytest # full suite, including all fixtures end to end
450
+ uv run ruff check . && uv run ruff format --check .
451
+ uv run mypy # strict type checking
452
+ ```
453
+
454
+ ```text
455
+ src/mcpgt/
456
+ contracts.py tri-state hints, strict parsing of raw tools/list
457
+ wire.py JSON-RPC stdio client with verbatim capture and timing
458
+ snapshot.py sqlite / fs / json adapters, coverage, classification, deltas
459
+ normalize.py declared volatile-field masking
460
+ fixture.py strict fixture definitions
461
+ runner.py reset, sessions, concurrent attempts, evidence collection
462
+ verdict.py verdict derivation and declaration coherence
463
+ report.py JSON reports, expectation checks, text summary
464
+ provenance.py source, harness and capture provenance
465
+ sources.py pinned third-party sources: prepare and offline verification
466
+ sandbox.py network-free (and on macOS write-confined) server processes
467
+ cli.py command line
468
+ fixtures/ ground-truth servers
469
+ targets/ audit targets (no expected verdicts)
470
+ docs/ methodology
471
+ tests/
472
+ ```
473
+
474
+ CI runs lint, strict type checking and the full suite on Python 3.11 and
475
+ 3.14.
476
+
477
+ ---
478
+
479
+ ## Related work
480
+
481
+ Other work also executes MCP tools or studies duplicate side effects. The
482
+ descriptions below follow each project's own documentation as checked on
483
+ 2026-10-13.
484
+
485
+ - **junhecui/mcp-conformance** [5] sets out to execute MCP tools under
486
+ observation and compare declared annotations with observed behavior. Its
487
+ design observes at the kernel boundary: an overlayfs upper layer as the
488
+ filesystem changeset, a network namespace as the connection log, and
489
+ seccomp denials as attempted escapes. It also plans a census pipeline that
490
+ measures annotation presence without execution, treats
491
+ `destructiveHint` as semantic rather than mechanically verifiable, and has
492
+ `unverifiable` as a first-class verdict. Its README describes its status as
493
+ "Pre-implementation. No code yet."
494
+ - **studiomeyer-io/mcp-hook-conformance** [6] is a CLI that audits MCP
495
+ servers for being called from agent lifecycle hooks. It checks idempotency
496
+ by comparing a tool's output across identical calls, and checks that
497
+ read-only tools do not mutate state through a configured state probe: a
498
+ call to another tool on the same server. Without a probe that check
499
+ returns `INDETERMINATE`. It also has latency, determinism and
500
+ documentation suites.
501
+ - **Li, "Where Does Exactly-Once Live?"** [7] benchmarks duplicate side
502
+ effects in LLM agents across model, agent harness and tool contract, in a
503
+ deterministic sandbox of six services with injected faults, and measures
504
+ how idempotency keys change the duplicate rate. It studies agent behavior
505
+ around writes rather than whether a tool's annotations are accurate.
506
+
507
+ How this project differs, stated without any claim of priority:
508
+
509
+ - **Idempotence is judged on final backing state**, from two independent
510
+ runs from a clean reset (once against twice), not on tool output.
511
+ - **Observation is record-aware** for databases and structured stores (SQLite
512
+ rows, JSON and JSONL documents), and separates business, append-only and
513
+ unclassified state, so an audit row per call does not make an upsert look
514
+ non-idempotent.
515
+ - **Concurrency is tested**, including two server instances sharing one
516
+ store, with per-attempt overlap evidence.
517
+ - **Control sessions and reset verification** separate a tool's effects from
518
+ server startup and shutdown and from an unreliable reset.
519
+ - **Contracts are parsed strictly at the wire level**, with the documented
520
+ divergence from how the MCP Python SDK reads the same bytes.
521
+
522
+ What the junhecui/mcp-conformance design covers that this project does not:
523
+ **network observation**, so `openWorldHint` can be tested (it is not tested
524
+ here), and **filesystem coverage without per-server configuration**, where
525
+ this project needs adapters, classification and masks declared for each
526
+ server.
527
+
528
+ ---
529
+
530
+ ## References
531
+
532
+ 1. Model Context Protocol. *Specification, version 2025-11-25: Tools*
533
+ (tool annotations and trust).
534
+ <https://modelcontextprotocol.io/specification/2025-11-25/server/tools>
535
+ 2. A. Trofimov and B. Novikov. *When Tool Calls Succeed but Workflows Fail:
536
+ Anomalies at the Agent-Tool Boundary.* arXiv:2609.15397, 2026.
537
+ <https://arxiv.org/abs/2609.15397>
538
+ 3. H. M. Afsar. *What a Random Draw from the MCP Registry Contains, and What
539
+ Tool-Use Benchmarks Contain Instead.* arXiv:2609.10962, 2026.
540
+ <https://arxiv.org/abs/2609.10962>
541
+ 4. Model Context Protocol Python SDK, version 2.3.0 (MIT License).
542
+ <https://github.com/modelcontextprotocol/python-sdk>
543
+ 5. J. Cui. *mcp-conformance: MCP Annotation Conformance Harness.* GitHub
544
+ repository, accessed 2026-10-13.
545
+ <https://github.com/junhecui/mcp-conformance>
546
+ 6. StudioMeyer. *mcp-hook-conformance.* GitHub repository (MIT License),
547
+ accessed 2026-10-13.
548
+ <https://github.com/studiomeyer-io/mcp-hook-conformance>
549
+ 7. J. Li. *Where Does Exactly-Once Live? Model, Harness, and Tool-Contract
550
+ Effects on Duplicate Side Effects in LLM Agents.* arXiv:2609.29095, 2026.
551
+ <https://arxiv.org/abs/2609.29095>
552
+
553
+ References 2 and 3 study what MCP servers declare and expose; this project
554
+ does not reproduce or restate their findings. Work that also executes tools
555
+ is discussed under [Related work](#related-work).
556
+
557
+ ---
558
+
559
+ ## License
560
+
561
+ Apache License 2.0. See [LICENSE](https://github.com/shreyasbhakta/mcp-groundtruth/blob/main/LICENSE) and [NOTICE](https://github.com/shreyasbhakta/mcp-groundtruth/blob/main/NOTICE).
@@ -0,0 +1,21 @@
1
+ mcpgt/__init__.py,sha256=pnj9HOobm2-0PEZCUQ-6tQcIKRi4F-wpoPFJpEmwJjc,334
2
+ mcpgt/cli.py,sha256=OCvJE-2JGSIHs2s5sL5U87jDSocqu7Z8aQT-9jHX4oo,10335
3
+ mcpgt/contracts.py,sha256=WT1z20D-7q2cPTMP0bzoStTbxhI_t95XwjBT2JLanm8,13853
4
+ mcpgt/fixture.py,sha256=gyjFGw7waHuG0Z5zGqSNG6FkizyHBn_HPyVLGb36CQs,21737
5
+ mcpgt/normalize.py,sha256=0ssmvzogbe5hDx7h8pTF--iH5tCHFgLhI7NACTyqJn0,3041
6
+ mcpgt/provenance.py,sha256=skyJ6dp7NeP4oya4rkOoR-Ng7nxUfcw1Y-26PYaSOUg,3760
7
+ mcpgt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ mcpgt/report.py,sha256=oprZruJ8he4Jcrk2M7qABipRjx-IsLSfohMJ3tO7FN0,12475
9
+ mcpgt/runner.py,sha256=d2qfNmNCc1nb-tqscm4ZtBg2IPta8AV7Fn5XHs-o8rU,32304
10
+ mcpgt/sandbox.py,sha256=887nms5sM-esmydT_135yS5RDEAtEUVdYPIo_G57RzE,3057
11
+ mcpgt/scaffold.py,sha256=PdliW1PJ9zrsTRMRkxYNa-zYTtvgNOAnr3_X5gHMtzw,13485
12
+ mcpgt/snapshot.py,sha256=ZqDSN36V6Lh-dF_rz10kxtje6_iY7dkaY6_tmDrCQ7M,21613
13
+ mcpgt/sources.py,sha256=zLkjj9ZaPrDk7HM-A4b-cm-8ptibentxigo2OAxVmM4,7344
14
+ mcpgt/verdict.py,sha256=y9CwmqHBmD6TYKnNwx8VhU07sfJHQODSC60P9AAS0Fo,22765
15
+ mcpgt/wire.py,sha256=_h06X5O2fLjMhgcBxzoFgXq40-89smro1xWAjqr2wCg,8541
16
+ mcp_groundtruth-0.2.0.dist-info/METADATA,sha256=s9swKZIB_bEG8R0h-onZcH83MZ761eFgrotPIVbeDn8,26367
17
+ mcp_groundtruth-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
18
+ mcp_groundtruth-0.2.0.dist-info/entry_points.txt,sha256=QsynYL2p0HsIjD6_iofH-ukBnH4JTInoBTcTAIK_PP8,74
19
+ mcp_groundtruth-0.2.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
20
+ mcp_groundtruth-0.2.0.dist-info/licenses/NOTICE,sha256=asPE7yVMys45a1gTADgo4xEK-x2roXGLYuBCURkJgis,395
21
+ mcp_groundtruth-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ mcp-groundtruth = mcpgt.cli:main
3
+ mcpgt = mcpgt.cli:main