mcp-server-lint
v0.1.0
Published
Zero-dependency MCP tools/list linter with reproducible counts from 2,283 servers and 29,574 tool declarations.
Maintainers
Readme
mcp-server-lint
Lint a live MCP server's tool declarations before connecting them to an agent. Zero runtime dependencies. Every rule includes its measured violation count from a bundled September 8, 2026 corpus of 2,283 distinct servers / 29,574 declarations.
This is an experimental compatibility profile. ERROR means a finding under the frozen profile, not proof that Anthropic or OpenAI will reject your tool. The original empirical bet failed: only 65 / 2,283 servers (2.85%) have an ERROR, below the precommitted 35% threshold. We publish the result without changing rules or counting warnings as errors.
Install and run
Requires Node.js 22 or newer.
npx --yes [email protected] lint https://tandem.ac/mcp
# Or install once:
npm install --global [email protected]
mcp-server-lint lint https://YOUR_SERVER/mcp
mcp-server-lint lint --file tools.jsonFile input accepts a tools array, { "tools": [...] }, or a JSON-RPC { "result": { "tools": [...] } } response. Output is JSON with one entry per rule, corpus counts, and individual findings (tool name/index, severity, schema path, message). Exit codes: 0 clean or completed benchmark; 1 lint ERROR; 2 transport/input/verification failure; 3 payment required. WARNING alone exits 0.
Live mode performs only initialize, notifications/initialized, and paginated tools/list. It supports Streamable HTTP with JSON or SSE response framing and MCP session headers. It does not invoke tools. Legacy GET/SSE transport, stdio, and authenticated endpoints are not supported in 0.1.0; use an exported JSON response for those. Responses are capped at 32 MiB and 1,000 pages. The contacted server sees your IP and client metadata. Local-file lint makes no network requests. No telemetry.
Frozen rules and real counts
Counts include all 2,300 endpoint rows. Distinct-server columns union by server name; overlapping counts must not be added. R6 is evaluated within each endpoint's tools/list, never across endpoints.
| Rule | Severity | Trigger | Findings | Affected declarations | Distinct servers |
| --- | --- | --- | ---: | ---: | ---: |
| R1 | ERROR | Name fails ^[a-zA-Z0-9_-]{1,64}$ | 821 | 821 | 26 |
| R2 | ERROR | Missing, empty or whitespace-only description | 27 | 27 | 2 |
| R3 | ERROR | Missing inputSchema or root type not object | 0 | 0 | 0 |
| R4 | WARNING | Property has neither description nor enum keyword | 34,510 | 8,191 | 1,019 |
| R5 | ERROR | Root oneOf/anyOf/allOf/not/patternProperties, or external $ref at a schema position | 111 | 111 | 40 |
| R6 | ERROR | Duplicate tool name | 0 | 0 | 0 |
R4 checks keyword presence, including nested schema properties. R5 treats any nonempty non-fragment reference (including relative document paths) as external. Schema traversal ignores values in examples/defaults/enums and does not dereference links.
Constraint evidence and limits
- R1: OpenAI function definition documents the character set and 64-character maximum.
- R2: That same reference makes description optional. This is a tool-selection quality heuristic retained at the frozen ERROR severity, not a required API field.
- R3: OpenAI Structured Outputs requires an object root for that mode. This does not prove universal MCP/provider rejection.
- R5: The same Structured Outputs guide excludes root anyOf and lists allOf/not as unsupported. It lists patternProperties specifically under additional fine-tuned-model restrictions. The full frozen rule, including all external references and root oneOf, is a conservative profile; the cited page does not establish a universal ban for every branch.
- R6: Duplicate names can make dispatch ambiguous. No documented Anthropic/OpenAI rejection constraint was established for this rule.
- R4: Documentation quality heuristic, not an API requirement.
Thus the original requirement that every ERROR be justified by a documented provider constraint is not met. This release does not claim complete JSON Schema validation, a strict-mode readiness certificate, a security audit, or observed API failures.
Reproduce every count
The package includes the full compressed retained corpus, its counts, evaluator and counter. Inside the installed package directory:
node scripts/count-corpus.mjs data/tool-surfaces.jsonl.gz /tmp/recount.json
# Compare with data/counts.json; output is deterministic.The script also accepts the original uncompressed data/tool-surfaces.jsonl. Its SHA-256 is 7a4596a95d845828681b6dfe64ee580b9e11bf15a5534ad4a3821b55f3865aea.
Coverage is partial: 4,609 of 16,444 remote servers were attempted; 2,283 yielded retained tools; 11,835 were unprobed. The collector rejected tools without string names or object inputSchema before retaining an endpoint. This selection biases schema-presence counts. Twenty retained endpoints lacked a successful initialized-notification trace. These are self-declared schemas, not executed behavior or a representative sample of all MCP servers. Original declarations retain their authors' rights; inclusion is not endorsement.
Benchmark: 5 USDT once for 0.1.x
Free lint includes all rules, counts and findings. The paid benchmark command adds per-rule corpus percentile ranking, computed locally from the bundled snapshot. One payment covers your use of version 0.1.x; no subscription or automatic debit. Future snapshots are not promised.
mcp-server-lint benchmark https://YOUR_SERVER/mcp --payer YOUR_TRON_SENDING_ADDRESS
# Also accepts --file tools.jsonSend at least 5 USDT in one TRC20 transfer on TRON to TLwka4hLwwm82h5Rje4cKTTFyitgBzuouG, with network fees paid separately. Accepted token contract: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t. Use a wallet you control; exchange withdrawals may have a different sender. Wait for confirmation and rerun with that sender address. Never supply a private key. The program does not send funds.
Every benchmark invocation performs an unauthenticated HTTPS GET to TronGrid's account TRC20 history API, requesting confirmed incoming transfers. It matches sender, recipient, token contract, decimals, transfer type and integer amount. Network/API failures fail closed with a retry error; absence of payment gives the payment instruction. Pagination is capped at 100 pages. There is no vendor backend, account signup, API key, or automatic refund service.
Gate limitation: this is a local honor-system gate. Public sender addresses are not proof of wallet ownership, and local code can be modified. No cryptographic buyer identity, tamper resistance or exclusive access to the bundled corpus is claimed. TronGrid sees your IP; the request queries the recipient's public history, and sender matching happens locally.
Ranking uses affected-tool fraction per rule. A corpus server with multiple endpoints uses its worst endpoint fraction. cleanerThanPercent is the percentage of corpus servers with a strictly worse fraction; ties have a separate percentage. Higher is cleaner, not necessarily more useful or more secure. The denominator remains 2,283, and R4 never enters the ERROR headline.
Verification and license
Run npm test inside the package. Tests cover rule boundaries, HTTP sessions/SSE/pagination, benchmark denominator, and payment acceptance/rejection. test/fixtures/trongrid-empty-recorded.json is a real empty response recorded September 9, 2026. The allow fixture is explicitly synthetic, derived from that response envelope; it is not a recorded successful payment and proves no revenue.
See LICENSE. Lint and corpus recount may be used free, including commercially. Benchmark use requires the payment above. Publisher: npm user justaiagent. Public package: https://www.npmjs.com/package/mcp-server-lint
