title: MCP Surface Lintemoji: 🔎colorFrom: indigocolorTo: graysdk: gradiosdk_version: 6.20.0app_file: app.pypinned: falselicense: mitshort_description: Find where an MCP tool surface lets a caller fail silentlytags:
- mcp
- mcp-server
- developer-tools
- static-analysis
MCP Surface Lint
Static checks over an MCP tool surface. Every rule answers one question:
Could a caller do the wrong thing and get no error back?
A tool that crashes on bad input is fine. A tool that accepts bad input andreturns something plausible is the expensive kind of broken, because neither thecaller nor the person reading the transcript has any signal that it happened.
Paste a tools/list response, get back the specific places that can happen.
This app is also an MCP server, so an agent can lint an agent interfacewithout going through the UI.
Using it as an MCP server
Launched with demo.launch(mcp_server=True). Point a client at:
https://<space-host>/gradio_api/mcp/sse
Two tools are published:
| Tool | Purpose |
|---|---|
lint_tool_surface(surface_json) |
Run every rule over a surface, return findings with severities |
list_rules() |
Enumerate the checks, their ids, and their severities |
The UI event handlers are deliberately excluded with api_name=False. Withoutthat, Gradio also publishes analyse, analyse_1, and explain as tools: aduplicate-name pair plus a handler taking a gr.State that cannot survive a toolcall. This linter flags all three patterns, so shipping them would be a poor look.
The rules
| Id | Severity | Catches |
|---|---|---|
| MCP001 | high | Tool has no description |
| MCP002 | low | Description too short to distinguish from a neighbour |
| MCP003 | high | No inputSchema at all |
| MCP004 | high | Parameter with no description |
| MCP005 | high | Parameter with no declared type |
| MCP006 | medium | String parameter that is really a closed set, with no enum |
| MCP007 | high | Properties declared but no required list |
| MCP008 | low | Numeric parameter with no bounds |
| MCP009 | medium | Unconstrained object parameter |
| MCP010 | medium | Description never says what failure looks like |
| MCP011 | high | Destructive verb with no guard parameter and no warning |
| MCP012 | medium | Two tools a caller cannot choose between by name |
| MCP013 | medium | Two tools sharing an identical description |
Names are compared after crude stemming, because list_user_records andlist_users_record are the same tool as far as a caller is concerned and rawtoken comparison misses that entirely.
What it found in its own surface
Pointed at its own published schema, the linter returns exactly one finding, andit is not in this repo:
[high] MCP007 lint_tool_surface: Schema declares properties but no 'required' list
Gradio's get_input_schema builds {"type": "object", "properties": {...}} andnever emits a required array. The information is already there: the sameparameter dicts it iterates carry parameter_has_default, and Gradio's owngradio skills command uses that field to mark a parameter required. Thepractical effect is that every Gradio MCP server publishes tools whosearguments all look optional, so a caller that omits a mandatory one produces aschema-valid call.
Reported upstream as gradio-app/gradio#13670.
Running locally
pip install -r requirements.txt
python app.py
Tests:
python -m pytest -q
Rules are only worth what their tests are worth, so there is also a mutationmatrix. It neuters each rule in turn and records which tests fail. A rule with nofailing test is either dead code or unguarded, and you cannot tell which withoutlooking:
python mutation_check.py
It refuses to report anything unless the baseline suite is green first, and itdisables bytecode caching, because every mutated file is the same length andCPython validates a cached .pyc on (mtime, size). Inside one second that pairdoes not change, so the interpreter happily re-imports the previous mutation'sbytecode and the whole matrix reports the wrong rule's result.
License
MIT.