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.
- mcp_groundtruth-0.2.0.dist-info/METADATA +561 -0
- mcp_groundtruth-0.2.0.dist-info/RECORD +21 -0
- mcp_groundtruth-0.2.0.dist-info/WHEEL +4 -0
- mcp_groundtruth-0.2.0.dist-info/entry_points.txt +3 -0
- mcp_groundtruth-0.2.0.dist-info/licenses/LICENSE +202 -0
- mcp_groundtruth-0.2.0.dist-info/licenses/NOTICE +10 -0
- mcpgt/__init__.py +10 -0
- mcpgt/cli.py +242 -0
- mcpgt/contracts.py +362 -0
- mcpgt/fixture.py +540 -0
- mcpgt/normalize.py +90 -0
- mcpgt/provenance.py +102 -0
- mcpgt/py.typed +0 -0
- mcpgt/report.py +288 -0
- mcpgt/runner.py +795 -0
- mcpgt/sandbox.py +84 -0
- mcpgt/scaffold.py +305 -0
- mcpgt/snapshot.py +550 -0
- mcpgt/sources.py +186 -0
- mcpgt/verdict.py +550 -0
- mcpgt/wire.py +217 -0
|
@@ -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
|
+
[](https://github.com/shreyasbhakta/mcp-groundtruth/actions/workflows/ci.yml)
|
|
32
|
+

|
|
33
|
+

|
|
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,,
|