mcp-validation-server 0.1.0__tar.gz
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_validation_server-0.1.0/LICENSE +21 -0
- mcp_validation_server-0.1.0/PKG-INFO +531 -0
- mcp_validation_server-0.1.0/README.md +502 -0
- mcp_validation_server-0.1.0/pyproject.toml +49 -0
- mcp_validation_server-0.1.0/setup.cfg +4 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server/__init__.py +85 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server/__main__.py +8 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server/app.py +39 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server/contract.py +800 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server/entities.py +652 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server/findings.py +204 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server/py.typed +0 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server/reconcile.py +498 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server/server.py +71 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server/tools.py +671 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server.egg-info/PKG-INFO +531 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server.egg-info/SOURCES.txt +23 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server.egg-info/dependency_links.txt +1 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server.egg-info/entry_points.txt +2 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server.egg-info/requires.txt +7 -0
- mcp_validation_server-0.1.0/src/mcp_validation_server.egg-info/top_level.txt +1 -0
- mcp_validation_server-0.1.0/tests/test_entities.py +310 -0
- mcp_validation_server-0.1.0/tests/test_reconcile.py +254 -0
- mcp_validation_server-0.1.0/tests/test_tool_contract.py +190 -0
- mcp_validation_server-0.1.0/tests/test_validate_rows.py +317 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SK Saimon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-validation-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Model Context Protocol server exposing deterministic data-validation tools: contract validation with quarantine, reconciliation, distinct entity counts, and duplicate review
|
|
5
|
+
Author: devops-gm88
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/devops-gm88/mcp-validation-server
|
|
8
|
+
Keywords: mcp,model-context-protocol,data-validation,data-quality,reconciliation,llm-tools
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: mcp<3,>=2.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
27
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# mcp-validation-server
|
|
31
|
+
|
|
32
|
+
[](https://github.com/devops-gm88/mcp-validation-server/actions/workflows/ci.yml)
|
|
33
|
+
[](https://pypi.org/project/mcp-validation-server/)
|
|
34
|
+
[](https://pypi.org/project/mcp-validation-server/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server that gives a language model a set of
|
|
38
|
+
data-validation tools it cannot argue with.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uvx mcp-validation-server # or: pip install mcp-validation-server
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**MCP in one sentence:** the Model Context Protocol is an open standard that lets an AI application
|
|
45
|
+
call tools running in a separate process, so the tool does the work and returns a result the model has
|
|
46
|
+
to accept.
|
|
47
|
+
|
|
48
|
+
## Why this exists
|
|
49
|
+
|
|
50
|
+
A model asked "is this data clean?" will answer. It will answer confidently whether or not it checked
|
|
51
|
+
anything. The fix is not a better prompt; it is to take the question away from the model and give it to
|
|
52
|
+
code that computes an answer and hands it back.
|
|
53
|
+
|
|
54
|
+
This server exposes four deterministic checks — contract validation, arithmetic reconciliation, distinct
|
|
55
|
+
entity counting, and duplicate detection. Each one returns a structured result with reasons and
|
|
56
|
+
row/column locators. None of them need a model, a confidence score, or reference data. All of them can
|
|
57
|
+
be wrong in ways you can see, which is the only kind of check worth trusting.
|
|
58
|
+
|
|
59
|
+
## The design rule
|
|
60
|
+
|
|
61
|
+
> **A validation failure is a result, not an exception.**
|
|
62
|
+
|
|
63
|
+
Every tool returns a JSON object with the same envelope:
|
|
64
|
+
|
|
65
|
+
| Field | Meaning |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `ok` | `true` only when the data has nothing wrong with it. The one field to branch on. |
|
|
68
|
+
| `verdict` | `clean`, `quarantined`, `flagged`, `does_not_close`, `review_required`, `inputs_rejected` |
|
|
69
|
+
| `findings` | One entry per problem: a stable `code`, a human `message`, and a `locator` |
|
|
70
|
+
| `guidance` | Plain English: what to do next |
|
|
71
|
+
|
|
72
|
+
`ok: false` is a **successful call reporting a failing dataset**. The model reads `findings`, sees
|
|
73
|
+
exactly which row and column failed, and can say so instead of guessing.
|
|
74
|
+
|
|
75
|
+
Only a genuinely malformed *tool argument* raises — a contract that does not parse, a regex that does
|
|
76
|
+
not compile, a key that is not a column name. There is nothing to validate until the call itself is
|
|
77
|
+
valid, so the call is rejected with a message naming the problem.
|
|
78
|
+
|
|
79
|
+
The line this server draws:
|
|
80
|
+
|
|
81
|
+
- **The contract is the call's definition** → a broken contract is an argument error.
|
|
82
|
+
- **The rows are the data** → broken rows are a result, with a reason each.
|
|
83
|
+
|
|
84
|
+
## The four tools
|
|
85
|
+
|
|
86
|
+
### `validate_rows`
|
|
87
|
+
|
|
88
|
+
Applies a column contract — `required`, `type`, `min`, `max`, `range`, `pattern`, `allowed`,
|
|
89
|
+
`not_blank`, and per-column `on_fail` — and returns **the passing rows and an explicit quarantine
|
|
90
|
+
list**. Every quarantined row carries a locator (index, one-based row number, and the column that
|
|
91
|
+
failed), a list of human-readable `reasons`, and machine-readable `findings`.
|
|
92
|
+
|
|
93
|
+
`accepted + quarantined` always equals the number of rows you passed in. Nothing is ever silently
|
|
94
|
+
dropped. A row that is not even an object is quarantined with that as its reason.
|
|
95
|
+
|
|
96
|
+
### `reconcile`
|
|
97
|
+
|
|
98
|
+
Given an opening balance, a list of movements, and a closing balance, reports whether
|
|
99
|
+
`closing = opening + movements` holds within a tolerance, showing both sides and the variance. This is
|
|
100
|
+
the check that does not depend on how plausible any single figure looked: clean extraction with quietly
|
|
101
|
+
wrong numbers is the failure that survives field-level validation, and arithmetic is what catches it.
|
|
102
|
+
|
|
103
|
+
When the variance exactly equals the size of one movement, the result says so — that is arithmetic, not
|
|
104
|
+
a guess, and it is usually the answer. When a movement cannot be read, `closes` becomes `null` rather
|
|
105
|
+
than `false`: a total computed over an incomplete set of movements must never be reported as a pass.
|
|
106
|
+
|
|
107
|
+
### `count_distinct`
|
|
108
|
+
|
|
109
|
+
Counts distinct entities on a caller-supplied stable key and reports **the difference between that and
|
|
110
|
+
the row count**, broken into its two causes: keys repeated across rows, and rows carrying no usable
|
|
111
|
+
key. One customer can appear on fifty rows; "50" is a row count, not a customer count, and the
|
|
112
|
+
difference is the error that survives review because the number looks plausible.
|
|
113
|
+
|
|
114
|
+
Rows with a blank key are excluded from the count and listed individually — never counted as one
|
|
115
|
+
entity, never dropped. Nothing beyond whitespace and letter case is normalised: stripping punctuation
|
|
116
|
+
or applying fuzzy matching changes which records count as the same entity, and that is a decision for
|
|
117
|
+
a person.
|
|
118
|
+
|
|
119
|
+
### `duplicate_report`
|
|
120
|
+
|
|
121
|
+
Finds suspected duplicate groups and **returns them for human review**. It never merges, never deletes,
|
|
122
|
+
and never picks a winner. Every group carries `auto_merged: false`, `decision_required: true`, the full
|
|
123
|
+
rows with locators, and a `review_question` phrased for a person to answer.
|
|
124
|
+
|
|
125
|
+
Three kinds of suspicion, each labelled:
|
|
126
|
+
|
|
127
|
+
- `same_key` — the same normalised key on more than one row. Genuine duplicates, or legitimate repeated
|
|
128
|
+
transactions? The tool cannot tell, so it reports rather than decides.
|
|
129
|
+
- `same_signature` — different keys whose values across `match_fields` are identical. The
|
|
130
|
+
re-registration case.
|
|
131
|
+
- `near_match` — different keys with similar values. Only produced when you supply
|
|
132
|
+
`similarity_threshold`, because "close enough" is a policy choice, not a fact.
|
|
133
|
+
|
|
134
|
+
## Install
|
|
135
|
+
|
|
136
|
+
Fastest, with nothing to clone:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
uvx mcp-validation-server
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Or install it into an environment:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pip install mcp-validation-server
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Or from source, for development:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
git clone https://github.com/devops-gm88/mcp-validation-server.git
|
|
152
|
+
cd mcp-validation-server
|
|
153
|
+
python3 -m venv .venv
|
|
154
|
+
.venv/bin/pip install -e ".[dev]"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Python 3.10 or newer. `mcp` is the only runtime dependency; everything else is the standard library.
|
|
158
|
+
|
|
159
|
+
## Register it with an MCP client
|
|
160
|
+
|
|
161
|
+
The server speaks MCP over **stdio**, so an MCP host launches it as a subprocess and talks to it on
|
|
162
|
+
stdin/stdout. This is the standard configuration block used by MCP hosts that launch local servers
|
|
163
|
+
(Claude Desktop, Claude Code, and others):
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"mcpServers": {
|
|
168
|
+
"data-validation": {
|
|
169
|
+
"command": "/absolute/path/to/mcp-validation-server/.venv/bin/python",
|
|
170
|
+
"args": ["-m", "mcp_validation_server"]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Use the **absolute** path to the interpreter — an MCP host does not inherit your shell's `PATH` or
|
|
177
|
+
virtualenv. If you installed with `pipx` or globally, the console script is equivalent:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"mcpServers": {
|
|
182
|
+
"data-validation": {
|
|
183
|
+
"command": "mcp-validation-server",
|
|
184
|
+
"args": []
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Verify the server starts and lists its tools without involving a client at all:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
.venv/bin/python examples/stdio_handshake.py
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Or with the official MCP Inspector CLI, which speaks to it as a real MCP host does. Point `command` at
|
|
197
|
+
the same absolute interpreter path as the block above:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
cat > /tmp/mcp.json <<JSON
|
|
201
|
+
{
|
|
202
|
+
"mcpServers": {
|
|
203
|
+
"data-validation": {
|
|
204
|
+
"command": "$PWD/.venv/bin/python",
|
|
205
|
+
"args": ["-m", "mcp_validation_server"]
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
JSON
|
|
210
|
+
npx @modelcontextprotocol/inspector --cli --config /tmp/mcp.json --server data-validation --method tools/list
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
To call a tool through the Inspector rather than just list it:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npx @modelcontextprotocol/inspector --cli --config /tmp/mcp.json --server data-validation \
|
|
217
|
+
--method tools/call --tool-name count_distinct \
|
|
218
|
+
--tool-arg 'rows=[{"id":"C-1"},{"id":"C-1"},{"id":""}]' 'key=id'
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### On the tool schemas
|
|
222
|
+
|
|
223
|
+
The Inspector reports **0 errors and 7 warnings** for these tools, and the warnings are deliberate. Four
|
|
224
|
+
are on the `items` of a `rows` or `columns` array; three are on `reconcile`'s `opening_balance`,
|
|
225
|
+
`movements` and `closing_balance`, which accept a number or a numeric string and report anything else as
|
|
226
|
+
a finding rather than refusing the call:
|
|
227
|
+
|
|
228
|
+
> Schema carries no validation keyword at all, so it accepts any value.
|
|
229
|
+
|
|
230
|
+
That looseness is the design rule, applied at the protocol boundary. If `rows` were declared as
|
|
231
|
+
`array of objects`, a client sending a row that is a bare string would be stopped by the input schema and
|
|
232
|
+
the model would get an unrecoverable protocol error — when the useful answer is "row 3 of your data is a
|
|
233
|
+
string, not an object; here it is, held back with that reason". So row content is intentionally
|
|
234
|
+
unconstrained and every unusable row comes back as a quarantine finding with a locator. What *is*
|
|
235
|
+
constrained is the shape of the call: a missing `key`, a `similarity_threshold` outside 0–1, and a
|
|
236
|
+
column spec that will not parse are all rejected before anything runs.
|
|
237
|
+
|
|
238
|
+
The trade is stated plainly rather than papered over: it costs a schema warning, and it buys a model
|
|
239
|
+
that can recover from bad data instead of just failing on it.
|
|
240
|
+
|
|
241
|
+
## Worked example
|
|
242
|
+
|
|
243
|
+
Real output. The script that produced it is `examples/worked_example.py`, and the untrimmed result is
|
|
244
|
+
saved beside it as `examples/worked_example_output.json`.
|
|
245
|
+
|
|
246
|
+
A model calls `validate_rows` on four rows of deliberately broken data:
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"rows": [
|
|
251
|
+
{"invoice": "INV-1001", "amount": "100.50", "date": "2026-04-01"},
|
|
252
|
+
{"invoice": "", "amount": "20", "date": "2026-04-02"},
|
|
253
|
+
{"invoice": "INV-1003", "amount": "-5", "date": "2026-04-03"},
|
|
254
|
+
{"invoice": "INV-1004", "amount": "abc", "date": "04/05/2026"}
|
|
255
|
+
],
|
|
256
|
+
"columns": [
|
|
257
|
+
{"name": "invoice", "type": "string", "required": true, "pattern": "INV-\\d+"},
|
|
258
|
+
{"name": "amount", "type": "number", "min": 0},
|
|
259
|
+
{"name": "date", "type": "date", "required": true}
|
|
260
|
+
],
|
|
261
|
+
"row_key": "invoice"
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
and gets back (`is_error: false` — the call succeeded; the data failed):
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"tool": "validate_rows",
|
|
270
|
+
"ok": false,
|
|
271
|
+
"verdict": "quarantined",
|
|
272
|
+
"finding_count": 4,
|
|
273
|
+
"findings": "<<4 findings, one per quarantine reason; see worked_example_output.json>>",
|
|
274
|
+
"guidance": "3 of 4 rows were quarantined and are listed in `quarantine` with a locator and reasons. Failure codes: below_minimum (1), not_a_date (1), not_a_number (1), required_blank (1). Do not use the accepted rows as if they were the whole dataset: report both counts, and resolve or explicitly exclude the quarantined rows before aggregating.",
|
|
275
|
+
"contract": "<<the parsed contract echoed back; see worked_example_output.json>>",
|
|
276
|
+
"summary": {
|
|
277
|
+
"rows_in": 4,
|
|
278
|
+
"accepted": 1,
|
|
279
|
+
"quarantined": 3,
|
|
280
|
+
"flagged": 0,
|
|
281
|
+
"pass_rate": 0.25,
|
|
282
|
+
"accounted_for": 4
|
|
283
|
+
},
|
|
284
|
+
"accepted": [
|
|
285
|
+
{
|
|
286
|
+
"invoice": "INV-1001",
|
|
287
|
+
"amount": 100.5,
|
|
288
|
+
"date": "2026-04-01"
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
"quarantine": [
|
|
292
|
+
{
|
|
293
|
+
"locator": {
|
|
294
|
+
"row_index": 1,
|
|
295
|
+
"row_number": 2
|
|
296
|
+
},
|
|
297
|
+
"row": {
|
|
298
|
+
"invoice": "",
|
|
299
|
+
"amount": 20.0,
|
|
300
|
+
"date": "2026-04-02"
|
|
301
|
+
},
|
|
302
|
+
"reasons": [
|
|
303
|
+
"invoice: is required but blank"
|
|
304
|
+
],
|
|
305
|
+
"findings": [
|
|
306
|
+
{
|
|
307
|
+
"code": "required_blank",
|
|
308
|
+
"severity": "error",
|
|
309
|
+
"message": "is required but blank",
|
|
310
|
+
"locator": {
|
|
311
|
+
"row_index": 1,
|
|
312
|
+
"row_number": 2,
|
|
313
|
+
"column": "invoice"
|
|
314
|
+
},
|
|
315
|
+
"expected": "a non-blank value"
|
|
316
|
+
}
|
|
317
|
+
]
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"locator": {
|
|
321
|
+
"row_index": 2,
|
|
322
|
+
"row_number": 3,
|
|
323
|
+
"row_key": "INV-1003"
|
|
324
|
+
},
|
|
325
|
+
"row": {
|
|
326
|
+
"invoice": "INV-1003",
|
|
327
|
+
"amount": -5.0,
|
|
328
|
+
"date": "2026-04-03"
|
|
329
|
+
},
|
|
330
|
+
"reasons": [
|
|
331
|
+
"amount: -5 is below the minimum of 0"
|
|
332
|
+
],
|
|
333
|
+
"findings": [
|
|
334
|
+
{
|
|
335
|
+
"code": "below_minimum",
|
|
336
|
+
"severity": "error",
|
|
337
|
+
"message": "-5 is below the minimum of 0",
|
|
338
|
+
"locator": {
|
|
339
|
+
"row_index": 2,
|
|
340
|
+
"row_number": 3,
|
|
341
|
+
"row_key": "INV-1003",
|
|
342
|
+
"column": "amount"
|
|
343
|
+
},
|
|
344
|
+
"value": -5.0
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
"locator": {
|
|
350
|
+
"row_index": 3,
|
|
351
|
+
"row_number": 4,
|
|
352
|
+
"row_key": "INV-1004"
|
|
353
|
+
},
|
|
354
|
+
"row": {
|
|
355
|
+
"invoice": "INV-1004",
|
|
356
|
+
"amount": "abc",
|
|
357
|
+
"date": "04/05/2026"
|
|
358
|
+
},
|
|
359
|
+
"reasons": [
|
|
360
|
+
"amount: 'abc' is not a number",
|
|
361
|
+
"date: '04/05/2026' is not an ISO-8601 date (expected YYYY-MM-DD)"
|
|
362
|
+
],
|
|
363
|
+
"findings": [
|
|
364
|
+
{
|
|
365
|
+
"code": "not_a_number",
|
|
366
|
+
"severity": "error",
|
|
367
|
+
"message": "'abc' is not a number",
|
|
368
|
+
"locator": {
|
|
369
|
+
"row_index": 3,
|
|
370
|
+
"row_number": 4,
|
|
371
|
+
"row_key": "INV-1004",
|
|
372
|
+
"column": "amount"
|
|
373
|
+
},
|
|
374
|
+
"value": "abc",
|
|
375
|
+
"expected": "a value of type number"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"code": "not_a_date",
|
|
379
|
+
"severity": "error",
|
|
380
|
+
"message": "'04/05/2026' is not an ISO-8601 date (expected YYYY-MM-DD)",
|
|
381
|
+
"locator": {
|
|
382
|
+
"row_index": 3,
|
|
383
|
+
"row_number": 4,
|
|
384
|
+
"row_key": "INV-1004",
|
|
385
|
+
"column": "date"
|
|
386
|
+
},
|
|
387
|
+
"value": "04/05/2026",
|
|
388
|
+
"expected": "a value of type date"
|
|
389
|
+
}
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
],
|
|
393
|
+
"flagged": [],
|
|
394
|
+
"errors_by_column": {
|
|
395
|
+
"amount": 2,
|
|
396
|
+
"date": 1,
|
|
397
|
+
"invoice": 1
|
|
398
|
+
},
|
|
399
|
+
"findings_by_code": {
|
|
400
|
+
"below_minimum": 1,
|
|
401
|
+
"not_a_date": 1,
|
|
402
|
+
"not_a_number": 1,
|
|
403
|
+
"required_blank": 1
|
|
404
|
+
},
|
|
405
|
+
"columns_seen": [],
|
|
406
|
+
"columns_required_but_absent": []
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
What a model can now do that it could not before: say **"3 of your 4 rows are unusable, here is which
|
|
411
|
+
one and why"**, and be right. The one clean row came back with its string amount coerced to the number
|
|
412
|
+
`100.5`; the three broken rows came back with their original values intact and a reason each. Row 4
|
|
413
|
+
failed twice, on two different columns, and both reasons are listed.
|
|
414
|
+
|
|
415
|
+
Note the locator gives both `row_index` (0-based, for the machine) and `row_number` (1-based, for the
|
|
416
|
+
person reading the report), and quotes `row_key` when the row has one.
|
|
417
|
+
|
|
418
|
+
## What this does not do
|
|
419
|
+
|
|
420
|
+
- **It does not repair data.** It reports which records are untrustworthy and why. It never fills,
|
|
421
|
+
imputes, or corrects a value.
|
|
422
|
+
- **It does not merge duplicates, ever.** `duplicate_report` returns suspicions with a question
|
|
423
|
+
attached. Merging is a decision with consequences, and it belongs to a person.
|
|
424
|
+
- **It does not score data quality.** There is no 0–100 number and no dashboard. There are findings
|
|
425
|
+
with codes and locators.
|
|
426
|
+
- **It does not prove the data is correct.** Passing a contract means the rows satisfy *the rules you
|
|
427
|
+
supplied and nothing else*. A contract that does not check a column says nothing about that column.
|
|
428
|
+
`reconcile` confirms internal consistency, not completeness: movements that are missing entirely will
|
|
429
|
+
still balance if the closing balance was derived from the same wrong set.
|
|
430
|
+
- **It does not know your domain.** Whether a repeated key is a defect depends on whether the table is a
|
|
431
|
+
transaction log or an entity register. The tools report the number; you decide what it means.
|
|
432
|
+
- **It does not normalise beyond whitespace and case.** No punctuation stripping, no accent folding, no
|
|
433
|
+
nickname tables, no fuzzy matching except where you explicitly ask for a similarity threshold.
|
|
434
|
+
- **It has no persistence, network access, or file access.** Every tool takes its data as an argument
|
|
435
|
+
and returns a result. Nothing is written anywhere.
|
|
436
|
+
- **It does not cap the size of the data you send it.** A large `rows` array comes back in full, in the
|
|
437
|
+
result. `duplicate_report` has a `max_groups` cap that reports what it omitted; `validate_rows` and
|
|
438
|
+
`count_distinct` do not truncate, so pass a summary-sized batch rather than a whole table.
|
|
439
|
+
- **It does not check the caller.** There is no authentication. It is a local stdio server for one
|
|
440
|
+
user's machine.
|
|
441
|
+
|
|
442
|
+
## Relationship to `sheets-data-validation-kit`
|
|
443
|
+
|
|
444
|
+
This project is a **sibling**, not a wrapper. It is self-contained: `pip install -e .` here installs
|
|
445
|
+
everything it needs, and nothing here imports the other package.
|
|
446
|
+
|
|
447
|
+
The ideas and the naming come from
|
|
448
|
+
[`sheets-data-validation-kit`](https://github.com/devops-gm88/sheets-data-validation-kit), a
|
|
449
|
+
standard-library-only Python library of the same primitives — `Field`, `RowContract`, the rule objects,
|
|
450
|
+
`Min`/`Max`/`Range`/`Pattern`, accept-or-quarantine, exact-match entity resolution, and never
|
|
451
|
+
auto-merging a duplicate. The kit is the library you import; this is the MCP server that puts those
|
|
452
|
+
checks behind a protocol so a model can call them.
|
|
453
|
+
|
|
454
|
+
Two differences are worth knowing:
|
|
455
|
+
|
|
456
|
+
| | `sheets-data-validation-kit` | `mcp-validation-server` |
|
|
457
|
+
| --- | --- | --- |
|
|
458
|
+
| Interface | Python objects you construct in code | JSON arguments over MCP |
|
|
459
|
+
| Contract shape | `Field(...)` with `Rule` instances | Column spec objects; parsed, and unknown keys rejected |
|
|
460
|
+
| Locators | Reasons as strings | Findings with codes and row/column locators |
|
|
461
|
+
| Failure model | Returns a result object | Returns a result, plus a `guidance` string for the model |
|
|
462
|
+
|
|
463
|
+
The server's `contract.py` is a re-implementation for JSON-supplied contracts, not a copy: it adds type
|
|
464
|
+
coercion, locators, per-finding codes, and the parsing layer that turns a model's JSON into rules and
|
|
465
|
+
rejects a malformed contract before any data is touched.
|
|
466
|
+
|
|
467
|
+
## Tests
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
pip install -e .
|
|
471
|
+
python3 -m unittest discover -s tests -v
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
139 tests, all passing on Python 3.14.7 — the count and result are from an actual run, not an estimate.
|
|
475
|
+
Every tool has non-MCP unit tests that call the underlying functions directly; no test needs a client,
|
|
476
|
+
a transport, or a subprocess.
|
|
477
|
+
|
|
478
|
+
Without installing first:
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The tests exercise the design rule directly: that `accepted + quarantined` equals the input count, that
|
|
485
|
+
a failing dataset never raises, that an unreadable movement withholds the reconciliation verdict instead
|
|
486
|
+
of producing a false pass, that nothing is ever merged, and that the four tools publish one shared
|
|
487
|
+
envelope.
|
|
488
|
+
|
|
489
|
+
## Layout
|
|
490
|
+
|
|
491
|
+
```
|
|
492
|
+
mcp-validation-server/
|
|
493
|
+
├── examples/
|
|
494
|
+
│ ├── stdio_handshake.py # start the server and list its tools, no client needed
|
|
495
|
+
│ ├── worked_example.py # produces the README's worked example
|
|
496
|
+
│ └── worked_example_output.json # the untrimmed result, as produced
|
|
497
|
+
├── src/mcp_validation_server/
|
|
498
|
+
│ ├── __init__.py # public API
|
|
499
|
+
│ ├── __main__.py # python3 -m mcp_validation_server
|
|
500
|
+
│ ├── app.py # the MCPServer instance and its instructions
|
|
501
|
+
│ ├── tools.py # the four @mcp.tool() functions and their docstrings
|
|
502
|
+
│ ├── findings.py # Finding, Locator, the shared response envelope
|
|
503
|
+
│ ├── contract.py # column specs, rules, RowContract, quarantine
|
|
504
|
+
│ ├── reconcile.py # opening + movements = closing
|
|
505
|
+
│ ├── entities.py # distinct counting, duplicate grouping
|
|
506
|
+
│ └── server.py # entry point and transport selection
|
|
507
|
+
└── tests/ # stdlib unittest, no MCP client required
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Notes on the SDK
|
|
511
|
+
|
|
512
|
+
Built against the official MCP Python SDK, `mcp` 2.2.0. Two things differ from most published examples,
|
|
513
|
+
which were written for `mcp` 1.x:
|
|
514
|
+
|
|
515
|
+
- **`FastMCP` was renamed to `MCPServer`.** In `mcp` 2.x the import is
|
|
516
|
+
`from mcp.server.mcpserver import MCPServer`; `mcp.server.fastmcp` no longer exists and raises a
|
|
517
|
+
`ModuleNotFoundError` pointing at the migration guide. `pyproject.toml` therefore requires
|
|
518
|
+
`mcp>=2.0,<3`.
|
|
519
|
+
- **Only `ToolError` carries a message to the caller.** Any other exception raised inside a tool is
|
|
520
|
+
wrapped by the SDK as `UnexpectedToolError`, whose text is the generic
|
|
521
|
+
`Error executing tool <name>`, with the original logged server-side only. Since a contract error the
|
|
522
|
+
model cannot read is a contract error the model cannot fix, malformed arguments are raised as
|
|
523
|
+
`mcp.server.mcpserver.exceptions.ToolError`.
|
|
524
|
+
- **Return annotations matter for structured output.** Annotating a tool with `typing.Dict[str, Any]`
|
|
525
|
+
makes the SDK publish an output schema of `{"result": {...}}` and nest `structuredContent` one level
|
|
526
|
+
down; the PEP 585 `dict[str, Any]` annotation keeps the payload at the top level. These tools use
|
|
527
|
+
`dict[str, Any]`.
|
|
528
|
+
|
|
529
|
+
## Licence
|
|
530
|
+
|
|
531
|
+
MIT. See [LICENSE](LICENSE).
|