mcp-lint-server
README.md
# mcp-lint
[](https://www.npmjs.com/package/mcp-lint)
[](https://www.npmjs.com/package/mcp-lint)
[](LICENSE)
[](https://nodejs.org)
[](#mcp-server)
[](https://glama.ai/mcp/servers/robert19001-cmyk/mcp-lint)
**Lint MCP server tool schemas, then check whether an agent can recover when a tool fails.**
Proof page, with a live bench: [mcp-lint-alpha.vercel.app](https://mcp-lint-alpha.vercel.app/)
MCP servers expose tool schemas consumed by **Claude**, **Cursor**, **Gemini**, **VS Code Copilot**, **Windsurf**, **Cline**, **OpenAI Agents SDK**, and **Continue.dev**. Each client has different JSON Schema support and quirks — a schema that works in Claude may silently break in Cursor or OpenAI. `mcp-lint` catches these issues before your users do.
This repository is **1.0.2**. npm `latest` is 1.0.2. Design notes: [PRODUCT.md](PRODUCT.md), [ROADMAP.md](ROADMAP.md), [ARCHITECTURE.md](ARCHITECTURE.md), [SECURITY.md](SECURITY.md), [RELEASE.md](RELEASE.md). Those files describe the current code. They are not extra commands.
```
$ npx mcp-lint check tools.json
search-tool
✖ Schema node at "inputSchema.properties.query" has `required: false` which is not valid JSON Schema.
inputSchema.properties.query [no-required-false]
⚠ Parameter "limit" in tool "search-tool" is missing a description.
inputSchema.properties.limit [description-exists]
1 error, 1 warning
```
---
## Installation
```bash
# In a repo that already has tools.json
npx mcp-lint
# Or point at a file
npx mcp-lint check tools.json
# Global
npm install -g mcp-lint
# Per-project dev dependency
npm install --save-dev mcp-lint
```
---
## Quick Start
```bash
# Look in this directory, and one level down, for tools.json, tools.yaml,
# tools.yml, mcp-tools.json, mcp-tools.yaml, or mcp-tools.yml
npx mcp-lint
# Check a JSON or YAML file
mcp-lint check tools.json
# See compatibility across all clients at a glance
mcp-lint compat tools.json
# Learn what a rule does
mcp-lint explain openai/no-additional-properties
# Auto-fix what can be fixed
mcp-lint fix tools.json --in-place
# Generate a config file
mcp-lint init
# Run as an MCP server
mcp-lint-server
# Score whether a tool failure tells the agent which tool to call next
mcp-lint recover examples/recover/bad-credential.json
# Lint the stdio servers already configured for Cursor, Claude, and VS Code
mcp-lint check --installed
# Save a live tool list, then fail CI when a later snapshot gets worse
mcp-lint snapshot -o tools.json --server stdio -- node server.js
mcp-lint diff tools.json tools.next.json
```
---
## `mcp-lint compat` — Compatibility Matrix
See exactly which tools work with which clients:
```
$ mcp-lint compat tools.json
claude cursor gemini vscode windsurf cline openai continue
search-tool ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✓
file-reader ✓ ✓ ✗ ✓ ✓ ✓ ✓ ✗
database-query ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✓
2 tools have compatibility issues. Run `mcp-lint check` for details.
```
```bash
mcp-lint compat tools.json --format markdown # for PRs and GitHub Step Summary
mcp-lint compat tools.json --format json # machine-readable
mcp-lint compat tools.json --client openai # single client column
mcp-lint compat --server stdio -- node server.js
```
---
## `mcp-lint explain` — Rule Documentation
```
$ mcp-lint explain openai/no-additional-properties
Rule: openai/no-additional-properties
Severity: error
Clients: openai
Fixable: yes
Description
OpenAI Agents SDK strict mode requires `additionalProperties: false` on the root inputSchema.
Why
OpenAI's strict function calling mode rejects schemas that don't explicitly set
`additionalProperties: false` at the root level.
Bad example
{
"type": "object",
"properties": { "q": { "type": "string" } }
}
Good example
{
"type": "object",
"properties": { "q": { "type": "string" } },
"additionalProperties": false
}
Auto-fix
Add `"additionalProperties": false` to the root inputSchema.
```
---
## CLI Reference
### `mcp-lint check [input]`
```bash
mcp-lint check tools.json
# Output formats
mcp-lint check tools.json --format terminal # default, colored output
mcp-lint check tools.json --format json # machine-readable (CI/CD)
mcp-lint check tools.json --format markdown # for PRs and GitHub summaries
# Filters
mcp-lint check tools.json --clients claude,cursor,openai # only these clients' rules
mcp-lint check tools.json --severity error # errors only
mcp-lint check tools.json --quiet # same as --severity error
mcp-lint check tools.json --ignore "debug-tool,internal-tool"
# Config
mcp-lint check tools.json --config path/to/.mcplintrc.json
mcp-lint check tools.json --no-color
# Quality score (0–100 per tool with A-F grades)
mcp-lint check tools.json --score
# Watch mode (re-lints on file change)
mcp-lint check tools.json --watch
# Live servers
mcp-lint check --server stdio -- node my-server.js
mcp-lint check --server stdio -- python my_server.py arg1 arg2
mcp-lint check --server sse --url http://localhost:3000/sse
```
**Exit codes:**
| Code | Meaning |
|------|---------|
| `0` | No schema errors. Warnings do not fail the command. |
| `1` | One or more schema errors, or the command could not read its input. |
| `2` | `--watch` was used without a file. |
### `mcp-lint report <file>`
Writes the versioned audit report for one tools file. `check --format json` still prints the diagnostic array. `report --format json` prints `MCPAuditReport`.
```bash
mcp-lint report tools.json
mcp-lint report tools.json --format sarif --output results.sarif
mcp-lint report tools.json --format html --output report.html
mcp-lint report tools.json --format markdown
```
Formats are `terminal`, `json`, `markdown`, `sarif`, and `html`. The HTML file has inline CSS and no script. Token totals are a local `chars / 4` estimate. The compatibility section is filled from that same lint. A cell is `measured` or `not_observed`. An empty tool list leaves the section not run. Recovery, preflight, and live inspection stay marked as not run. When the run has no schema errors, the report says: "No schema errors in this run. Recovery, preflight, and live inspection were not measured."
```bash
mcp-lint report tools.json --baseline audit.baseline.json
mcp-lint report tools.json --update-baseline audit.baseline.json
```
`--baseline` exits 1 when the stable snapshot differs. The snapshot pins `generatedAt` and keeps `tool.version`. Pass either `--baseline` or `--update-baseline`, not both. Exit `1` when the report contains an error, or when the file cannot be read.
### `mcp-lint catalog`
Lists resources and prompts from one live server. It does not read installed configs and it does not call a tool.
```bash
mcp-lint catalog --server stdio -- node server.js
mcp-lint catalog --server http --url https://example.com/mcp --format json
```
`--server` is `stdio`, `sse`, or `http`. Remote transports need `--url`. `--timeout`, `--max-items`, and `--max-bytes` bound each list. A method the server does not implement is `unsupported` and exits 0. Any other list failure exits 1. Usage errors exit 2. `--format` is `terminal` or `json`.
---
### `mcp-lint diff <before> <after>`
Compare lint results between two versions of your schema — useful in CI to catch regressions.
```bash
mcp-lint diff tools-v1.json tools-v2.json
mcp-lint diff tools-v1.json tools-v2.json --format markdown >> $GITHUB_STEP_SUMMARY
mcp-lint diff tools-v1.json tools-v2.json --format json
```
**Exit codes:** `0` = no new errors introduced, `1` = new errors found, `2` = error
---
### `mcp-lint check --installed`
Reads the MCP configs already on this machine and lints each local stdio server. Nothing is uploaded. Remote URLs are skipped unless you pass `--remote`.
```bash
mcp-lint check --installed
mcp-lint check --from .cursor/mcp.json
mcp-lint check --from .mcp.json --timeout 10000 --strict
```
Looked-up files:
| Client | Path |
|--------|------|
| Cursor | `~/.cursor/mcp.json` and `./.cursor/mcp.json` |
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) |
| Claude Code | `~/.claude.json`, `~/.claude/settings.json`, and `./.mcp.json`. `~/.claude.json` also contributes each `projects.<path>.mcpServers` entry. |
| VS Code | `~/Library/Application Support/Code/User/mcp.json` and `./.vscode/mcp.json` |
`--strict` also exits 1 when a configured server fails to start. Without it, a start failure is printed and the exit code follows the schema errors. If two started servers expose the same tool name, both reports include `tool-name/server-collision`.
---
### `mcp-lint snapshot`
Writes the live `tools/list` as a stable JSON file. Commit that file and compare it with `diff` when the server changes. `diff` exits 1 when the new file introduces an error the old file did not have.
```bash
mcp-lint snapshot -o tools.json --server stdio -- node server.js
mcp-lint snapshot -o tools.json --server http --url http://127.0.0.1:3000/mcp
mcp-lint diff tools.json tools.next.json
```
---
### `mcp-lint fix [input]`
Writes fixes marked `safe`. `review` and `manual` fixes are printed by `--dry-run` and left in the file. mcp-lint does not invent titles, descriptions, or hints.
```bash
mcp-lint fix tools.json # fixed output to stdout
mcp-lint fix tools.json --output fixed-tools.json # write to file
mcp-lint fix tools.json --in-place # overwrite original
mcp-lint fix tools.json --dry-run # show what would be fixed
mcp-lint fix tools.json --rules no-required-false,no-empty-enum # specific rules only
```
---
### `mcp-lint init`
Creates a default `.mcplintrc.json` in the current directory.
```bash
mcp-lint init # fails if file already exists
mcp-lint init --force # overwrite existing
```
---
## Rules
### Universal (all clients)
| Rule | Severity | Fixable | Description |
|------|----------|---------|-------------|
| `no-required-false` | 🔴 error | ✅ | `required: false` on properties is not valid JSON Schema |
| `no-content-encoding` | 🔴 error | ✅ | `contentEncoding` is not supported by MCP clients |
| `no-empty-enum` | 🔴 error | ✅ | Empty `enum: []` makes a parameter impossible to satisfy |
| `no-recursive-refs` | 🔴 error | ❌ | Circular `$ref` causes crashes in most clients |
| `valid-json-schema-subset` | 🔴 error | ❌ | Unsupported keywords: `oneOf`, `anyOf`, `allOf`, `if/then/else`, `not`, `patternProperties`… |
| `description-exists` | 🟡 warning | ❌ | Tools and parameters should have descriptions |
| `max-depth` | 🟡 warning | ❌ | Schema nesting deeper than 5 levels hurts LLM comprehension |
| `no-unsupported-formats` | 🟡 warning | ✅ | Obscure `format` values (`iri`, `json-pointer`, `regex`…) not widely supported |
### Tool names
| Rule | Severity | Fixable | Description |
|------|----------|---------|-------------|
| `tool-name/client-safe` | 🔴 error, 🟡 warning | ❌ | Names must match `^[A-Za-z0-9_-]{1,64}$`. A leading digit is a warning because Gemini rejects it. |
| `tool-name/duplicate` | 🔴 error | ❌ | The same name appears twice in one tools list. |
| `tool-name/server-collision` | 🔴 error | ❌ | The same name is exposed by two servers during `check --installed` or `--from`. |
OpenAI and Azure use that pattern. A space, slash, or dot makes the tool disappear. See `examples/names/unsafe.json`.
### Claude-specific
| Rule | Severity | Fixable | Description |
|------|----------|---------|-------------|
| `claude/no-type-array` | 🟡 warning | ✅ | `"type": ["string", "null"]` array syntax may not work correctly in Claude |
### Cursor-specific
| Rule | Severity | Fixable | Description |
|------|----------|---------|-------------|
| `cursor/no-default-without-type` | 🔴 error | ✅ | Cursor requires explicit `type` when a `default` value is present |
| `cursor/no-missing-title` | 🟡 warning | ❌ | Cursor displays tool and parameter titles in the tool picker |
### Gemini-specific
| Rule | Severity | Fixable | Description |
|------|----------|---------|-------------|
| `gemini/no-optional-without-default` | 🟡 warning | ❌ | Gemini handles optional params better when `default` is explicit |
| `gemini/no-nested-objects` | 🟡 warning | ❌ | Gemini has limited support for object properties nested more than 2 levels |
### VS Code Copilot-specific
| Rule | Severity | Fixable | Description |
|------|----------|---------|-------------|
| `vscode/max-params` | 🟡 warning | ❌ | VS Code Copilot performance degrades with more than 15 parameters |
### Windsurf-specific
| Rule | Severity | Fixable | Description |
|------|----------|---------|-------------|
| `windsurf/no-union-types` | 🟡 warning | ❌ | `anyOf` with more than 2 variants not supported |
### Cline-specific
| Rule | Severity | Fixable | Description |
|------|----------|---------|-------------|
| `cline/description-max-length` | 🟡 warning | ✅ | Descriptions over 200 characters are truncated by Cline |
### OpenAI Agents SDK-specific
| Rule | Severity | Fixable | Description |
|------|----------|---------|-------------|
| `openai/no-additional-properties` | 🔴 error | ✅ | Strict mode requires `additionalProperties: false` on root inputSchema |
| `openai/strict-types` | 🔴 error | ❌ | Only `string`, `number`, `boolean`, `object`, `array`, `null` allowed |
### Continue.dev-specific
| Rule | Severity | Fixable | Description |
|------|----------|---------|-------------|
| `continue/no-default-values` | 🟡 warning | ❌ | Continue.dev ignores `default` fields — document defaults in descriptions instead |
---
## Configuration
`mcp-lint` auto-discovers `.mcplintrc.json` starting from the current directory up to your home folder. Override with `--config`.
```json
{
"rules": {
"no-required-false": "error",
"description-exists": "warning",
"max-depth": "off",
"claude/no-type-array": "error"
},
"clients": ["claude", "cursor", "gemini", "vscode", "windsurf", "cline", "openai", "continue"],
"ignore": ["internal-debug-tool"],
"maxDepth": 5
}
```
### Presets
Use `"extends"` to start from a built-in preset:
```json
{ "extends": "recommended" }
```
```json
{
"extends": "strict",
"rules": {
"description-exists": "warning"
}
}
```
| Preset | Description |
|--------|-------------|
| `recommended` | All 8 clients enabled, default severities |
| `strict` | All rules set to `error`, `maxDepth: 3` |
Rule severities: `"error"` | `"warning"` | `"info"` | `"off"`
---
## CI/CD — GitHub Actions
### Using the official Action
```yaml
name: MCP Schema Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: robert19001-cmyk/mcp-lint@v1.0.2
# Tag v1.0.2 installs mcp-lint@1 unless version is set.
# Tag v1.0.0 still installs npm latest.
with:
input: tools.json
severity: warning
score: 'true'
fail_on: error
```
**Action inputs:**
| Input | Default | Description |
|-------|---------|-------------|
| `input` | first matching file | `tools.json`, `tools.yaml`, `tools.yml`, `mcp-tools.json`, `mcp-tools.yaml`, or `mcp-tools.yml` in the repo root, then one directory down |
| `format` | `terminal` | `terminal\|json\|markdown` |
| `severity` | `info` | Minimum severity to report |
| `clients` | all | Comma-separated client filter |
| `fail_on` | `error` | Fail the job at this severity or above. `warning` and `info` read the full diagnostic list. The CLI itself exits 1 only for schema errors. |
| `score` | `false` | Show quality score |
| `recover` | empty | Path to a recovery JSON file. The job fails when recover exits non-zero |
| `sarif` | `false` | Upload SARIF when this `mcp-lint` release includes `report`. The workflow needs `security-events: write`. |
| `version` | `1` | npm dist-tag or version. `1` installs `mcp-lint@1`. Tag `v1.0.0` ignores this input and installs `latest`. |
The action in this repository writes a `::error`, `::warning`, or `::notice` annotation from `check --format json`. With an empty `input` it also looks one directory down, skipping `node_modules` and dot directories. `sarif: true` uploads SARIF when the installed CLI has `report`. Tag `v1.0.2` installs `mcp-lint@1` unless `version` is set. Tag `v1.0.1` does the same. Tag `v1.0.0` still installs npm `latest`. The workflow needs `security-events: write`.
### Manual workflow
```yaml
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
# Fail the build on errors
- run: npx mcp-lint check ./src/tools.json
# Post compatibility matrix to PR summary
- run: npx mcp-lint compat ./src/tools.json --format markdown >> $GITHUB_STEP_SUMMARY
if: always()
# Diff against previous version
- run: npx mcp-lint diff tools-before.json tools-after.json --format markdown >> $GITHUB_STEP_SUMMARY
if: always()
```
---
## Plugin API
Share and reuse rule sets via npm packages.
### Using a plugin
```bash
npm install mcp-lint-config-nextjs
```
```json
{
"plugins": ["mcp-lint-config-nextjs"],
"extends": "recommended"
}
```
### Writing a plugin
A plugin is an npm package that exports a `rules` array of `Rule` objects:
```typescript
// mcp-lint-config-myserver/index.ts
import type { Rule } from 'mcp-lint';
const myRule: Rule = {
id: 'myserver/no-large-params',
severity: 'warning',
description: 'My server limits param count to 10',
clients: ['claude', 'cursor'],
check(tool) {
const count = Object.keys(tool.inputSchema.properties ?? {}).length;
if (count > 10) return [{
toolName: tool.name,
ruleId: 'myserver/no-large-params',
severity: 'warning',
message: `Too many params (${count})`,
path: 'inputSchema.properties',
}];
return [];
},
};
export const rules = [myRule];
```
---
## Programmatic API
```typescript
import { LintEngine } from 'mcp-lint';
import { allRules } from 'mcp-lint/rules';
import { loadFile } from 'mcp-lint/loaders';
import { applyFixes } from 'mcp-lint/fixer';
// Check
const tools = await loadFile('tools.json');
const engine = new LintEngine(allRules, {
clients: ['claude', 'cursor', 'openai'],
ignore: ['debug-tool'],
});
const diagnostics = engine.lint(tools);
// Fix
const fixed = applyFixes(tools, diagnostics);
```
---
## `mcp-lint preflight` — Runtime Decision Firewall
**Lint catches bad schemas. Preflight catches bad actions.**
At runtime, your agent is about to call a tool — delete a file, send a request, charge a card. Preflight sits between the agent and the action and answers:
- Is it allowed?
- How risky is it? (0.0–1.0 deterministic score)
- Is it reversible?
- Does it need human approval?
- Is there a safer alternative?
```bash
$ mcp-lint preflight action.json --policy preflight.yml
Preflight Decision
────────────────────────────────────────
Decision: DENY
Risk score: 1.00
Reversibility: irreversible
Policies: block-destructive-prod
Reasons: base_shell, destructive_pattern, sensitive_target, irreversible_operation
```
### Action format
```json
{
"tool_type": "shell",
"tool_name": "bash",
"action": "rm -rf /prod/data",
"target": "/prod/data",
"context": { "environment": "prod" }
}
```
### Policy file (YAML)
```yaml
version: 1
defaults:
approval_threshold: 0.70
block_threshold: 0.92
rules:
- id: block-prod-delete
when:
tool_type: shell
action_matches: ["rm -rf"]
target_matches: ["/prod", "/etc"]
effect: deny
- id: rewrite-tmp-delete
when:
tool_type: shell
action_matches: ["rm -rf ./tmp"]
effect: allow_with_rewrite
rewrite:
tool_type: file_delete
action: move_to_trash
target: ./tmp
- id: approval-payments
when:
tool_type: payment
effect: require_approval
```
Sample policies in `examples/policies/`: `default.yml`, `strict.yml`, `permissive-dev.yml`.
### SDK — embed in your MCP server or agent runtime
```typescript
import { preflight, loadPolicy } from 'mcp-lint/preflight';
const policy = await loadPolicy('./preflight.yml');
const decision = preflight(
{
tool_type: 'shell',
tool_name: 'bash',
action: 'rm -rf /tmp',
target: '/tmp',
},
policy,
);
if (decision.decision === 'deny') {
throw new Error(`Blocked: ${decision.reasons.join(', ')}`);
}
if (decision.decision === 'require_approval') {
await askUser(decision);
}
if (decision.decision === 'allow_with_rewrite') {
return executeSafer(decision.safe_alternative);
}
```
### Exit codes
| Code | Meaning |
|------|---------|
| `0` | allow / allow_with_rewrite |
| `1` | require_approval |
| `2` | deny / error |
Use them directly in shell wrappers or CI gates.
---
## `mcp-lint recover` — Can the agent get back up?
Schema lint does not say what happens when a tool fails. `recover` reads the error text and checks four things an agent limited to MCP tools needs:
- A credential or permission error names a tool on this server, such as `login`.
- A rate limit or timeout names the call to repeat. "Wait and retry" is not enough.
- The next step is not a terminal command, a config edit, or a web page.
- The failure is not silent, and a hang is cut off at 15 seconds.
```bash
$ mcp-lint recover examples/recover/bad-credential.json
search#1
✖ Credential or permission error does not name a tool on this server. Say which tool to call, for example `login`, then retry.
[recover/names-tool] permanent
✖ Next step asks for a terminal command, which an agent limited to MCP tools cannot do. Name a tool from this server instead.
[recover/agent-reachable] permanent
search#2
✖ Retry text does not name the call to repeat. Name `search` instead of "wait and retry".
[recover/names-retry] transient
3 errors, 1 recoverable of 3, 1 transient, 2 permanent
```
`rate_limit` and `timeout` count as transient. Every other kind counts as permanent. Exit `1` when any sample has an error. Exit `0` when every scored failure is recoverable.
A live probe lists tools and does not call them. Calling tools requires both `--sandbox` and `--invoke`. Tools with no required arguments are still skipped, so an empty call cannot fire a tool that declares no inputs.
```bash
mcp-lint recover --server stdio -- node server.js
mcp-lint recover --server stdio --sandbox --invoke -- node server.js
mcp-lint recover errors.json --hang-ms 8000 --format json
```
File shape:
```json
{
"tools": ["login", "search"],
"samples": [
{
"tool": "search",
"kind": "credential",
"message": "Unauthorized. Call the login tool, then retry search."
}
]
}
```
`kind` is `credential`, `permission`, `rate_limit`, `explicit`, `silent`, `timeout`, or `malformed`.
```typescript
import { scoreRecovery } from 'mcp-lint/recover';
const report = scoreRecovery(
[{ tool: 'search', kind: 'rate_limit', message: 'Wait before retrying.' }],
{ tools: ['login', 'search'] },
);
```
---
## MCP Server
`mcp-lint` also ships as a stdio MCP server for Claude Desktop, Claude Code, Cursor, VS Code, and registry scanners such as Glama.
```bash
npx -y mcp-lint@latest mcp-lint-server
```
Client config:
```json
{
"mcpServers": {
"mcp-lint": {
"command": "npm",
"args": ["exec", "--yes", "--package", "mcp-lint@latest", "--", "mcp-lint-server"]
}
}
}
```
Available MCP tools:
| Tool | Purpose |
|------|---------|
| `mcp_lint_list_rules` | List built-in rules, clients, severities, and fixability |
| `mcp_lint_explain_rule` | Explain one rule with examples and auto-fix notes |
| `mcp_lint_check_tools` | Lint MCP tool schemas passed as JSON |
| `mcp_lint_fix_tools` | Return safely auto-fixed tool schemas without writing files |
| `mcp_lint_preflight_action` | Score a proposed agent action and return allow/deny/approval/rewrite |
| `mcp_lint_score_recovery` | Score a recorded tool failure for whether an agent can recover |
All server tools are read-only from the host perspective: they return diagnostics, fixed JSON, or policy decisions and never execute the action being evaluated.
### Docker
```bash
docker build -t mcp-lint-server .
docker run --rm -i mcp-lint-server
```
The repository includes `glama.json` and a production Dockerfile so MCP directories can build, start, and introspect the server.
---
## Site
The proof page is in `site/`. Version 1.0.2. It runs the same recovery scorer, schema linter, and preflight function as the CLI.
```bash
npm run site
```
The public page is https://mcp-lint-alpha.vercel.app/. The same page runs locally:
```bash
npm run site
```
Open http://127.0.0.1:4731. The built page is not part of the npm package.
---
## License
MIT © Robert
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing