Skip to main content
Glama

mcp-lint

npm version npm downloads License: MIT Node.js ≥20 MCP server ready robert19001-cmyk/mcp-lint MCP server

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

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, ROADMAP.md, ARCHITECTURE.md, SECURITY.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

# 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

Related MCP server: Design Constraint Validator MCP Server

Quick Start

# 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.
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]

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.

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."

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.

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.

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.

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.

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.

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.

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.

{
  "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:

{ "extends": "recommended" }
{
  "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

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

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

npm install mcp-lint-config-nextjs
{
  "plugins": ["mcp-lint-config-nextjs"],
  "extends": "recommended"
}

Writing a plugin

A plugin is an npm package that exports a rules array of Rule objects:

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

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?

$ 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

{
  "tool_type": "shell",
  "tool_name": "bash",
  "action": "rm -rf /prod/data",
  "target": "/prod/data",
  "context": { "environment": "prod" }
}

Policy file (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

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.

$ 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.

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:

{
  "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.

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.

npx -y mcp-lint@latest mcp-lint-server

Client config:

{
  "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

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.

npm run site

The public page is https://mcp-lint-alpha.vercel.app/. The same page runs locally:

npm run site

Open http://127.0.0.1:4731. The built page is not part of the npm package.


License

MIT © Robert

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Lint agent SKILL.md files and MCP tool schemas to catch issues like missing descriptions, similar skills, unconstrained parameters, and destructive tools without confirmation gates.
    2
    MIT