A Spectral ruleset that checks MCP (Model Context Protocol) tool definitions against best practices.
It lints documents shaped like an MCP tools/list result:
{
"tools": [
{
"name": "createApi",
"title": "Create API",
"description": "Creates a new API from an embedded OpenAPI definition.",
"inputSchema": { "type": "object", "properties": { "...": {} } },
"outputSchema": { "type": "object", "properties": { "...": {} } }
}
]
}- Node.js 18 or later
- Spectral CLI 6.x
npm install -g @stoplight/spectral-cliLint a file:
spectral lint -r mcp-ruleset.yaml path/to/mcp-tools.jsonTry the bundled examples:
spectral lint -r mcp-ruleset.yaml examples/valid.json # no problems
spectral lint -r mcp-ruleset.yaml examples/invalid.json # triggers every ruleBy default Spectral lists everything but only exits non-zero on errors. Useful flags:
| Flag | Effect |
|---|---|
-F warn |
Also fail on warnings |
-D |
Only show results at or above the fail severity |
-f json / -f junit / -f sarif |
Machine-readable output for CI |
Reference it from a .spectral.yaml in your project:
extends:
- https://raw.githubusercontent.com/apiaddicts/mcps-style-guide/main/mcp-ruleset.yamlOr point at a local copy (extends: ./path/to/mcp-ruleset.yaml). The functions/ directory has to stay next to the ruleset.
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install -g @stoplight/spectral-cli
- run: spectral lint -r mcp-ruleset.yaml mcp-tools.json -F warnSee docs/rules.md for the full reference: why each rule exists, passing and failing examples, and the Spectral output it produces.
| Rule | Severity | What it checks |
|---|---|---|
tool-name-required-and-casing |
error | Every tool has a name in camelCase (^[a-z][a-zA-Z0-9]*$) |
tool-title-not-null |
warn | title is a non-null, non-empty string |
tool-description-required |
error | description is present and not empty |
tool-no-duplicate-keys |
error | A tool object has no duplicate keys (e.g. two _meta) |
tool-input-schema-structure |
error | inputSchema exists and its type is object |
tool-property-description |
info | Every property in inputSchema.properties has a description |
openapi-embedded-version-check |
error | Any openapi field inside the input properties starts with 3. |
embedded-schema-required-matches-properties |
error | In embedded OpenAPI schemas, every required name exists in properties |
tool-output-schema-defined |
warn | outputSchema is present and not null |
tool-name-required-and-casing
tool-title-not-null
{ "title": null } // ✗
{ "title": "Create API" } // ✓tool-description-required
{ "description": "" } // ✗
{ "description": "Creates a new API from ..." } // ✓tool-no-duplicate-keys
{ "_meta": { "version": 1 }, "_meta": { "version": 2 } } // ✗
{ "_meta": { "version": 2 } } // ✓tool-input-schema-structure
{ "inputSchema": { "type": "string" } } // ✗
{ "inputSchema": { "type": "object", "properties": {} } } // ✓tool-property-description
"properties": { "name": { "type": "string" } } // ✗
"properties": { "name": { "type": "string", "description": "API name." } } // ✓openapi-embedded-version-check
"default": { "openapi": "2.0" } // ✗
"default": { "openapi": "3.0.3" } // ✓An input property that is itself named openapi (a schema object, not a version string) doesn't trigger this rule.
embedded-schema-required-matches-properties
"schemas": {
"Pet": {
"required": ["id", "name"],
"properties": { "id": { "type": "integer" } } // ✗ "name" missing
}
}tool-output-schema-defined
{ "outputSchema": null } // ✗
{ "outputSchema": { "type": "object" } } // ✓-
Spectral reports duplicate keys on its own as a
parsererror.tool-no-duplicate-keysadds a named error for duplicates at the top level of a tool object. When a file has duplicates you will see both. -
To turn off or change the severity of a rule, override it in your own
.spectral.yaml:extends: [./mcp-ruleset.yaml] rules: tool-property-description: off tool-output-schema-defined: error
mcp-ruleset.yaml Spectral ruleset
functions/
noDuplicateKeys.js duplicate keys, read from parser diagnostics
requiredMatchesProperties.js `required` names vs `properties`
examples/
valid.json passes every rule
invalid.json breaks every rule
- Add or change the rule in
mcp-ruleset.yaml(custom logic goes infunctions/). - Update
examples/valid.jsonandexamples/invalid.jsonso the rule has a passing and a failing case. - Run both examples and check the output.
- Add the rule to the tables in this README.
{ "name": "Create_API" } // ✗ { "name": "createApi" } // ✓