@yawlabs/ctxlint 0.25.0 → 0.25.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,419 +1,434 @@
1
- # MCP Server Configuration Linting Specification
2
-
3
- **Version:** 1.0.0-draft
4
- **Date:** 2026-04-07
5
- **MCP Spec Compatibility:** 2025-11-25 (Streamable HTTP)
6
- **Maintained by:** [Yaw Labs](https://yaw.sh) / [ctxlint](https://github.com/YawLabs/ctxlint)
7
- **License:** CC BY 4.0
8
-
9
- ---
10
-
11
- ## What is this?
12
-
13
- MCP server configuration files (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, etc.) define which tools an AI agent can access. They are a context interface — alongside instruction files like `CLAUDE.md` and `.cursorrules`, they shape what an agent knows and can do.
14
-
15
- This specification defines a standard set of lint rules for validating MCP server configurations across all major AI coding clients. It is tool-agnostic: any linter, IDE extension, CI check, or AI agent can implement these rules.
16
-
17
- The specification includes:
18
- - A complete reference of MCP config file locations, formats, and client-specific behaviors
19
- - 29 lint rules organized into 8 categories with defined severities
20
- - A machine-readable rule catalog ([`mcp-config-lint-rules.json`](./mcp-config-lint-rules.json))
21
- - Auto-fix definitions for rules that support automated correction
22
-
23
- **Reference implementation:** [ctxlint](https://github.com/YawLabs/ctxlint) (v0.4.0+)
24
-
25
- ---
26
-
27
- ## Related specifications
28
-
29
- This spec is part of a family of open specifications maintained by Yaw Labs for MCP tooling:
30
-
31
- | Spec | Scope | Input |
32
- |---|---|---|
33
- | **mcp-config-lint** (this spec) | Static analysis of MCP client config files | `.cursor/mcp.json`, `.vscode/mcp.json`, `.mcp.json`, etc. |
34
- | [**mcp-compliance**](https://github.com/YawLabs/mcp-compliance/blob/master/MCP_COMPLIANCE_SPEC.md) | Runtime testing of live MCP servers | A live server URL + transport |
35
-
36
- The two are **complementary, not overlapping**. `mcp-config-lint` catches problems before deploy by reading JSON on disk; `mcp-compliance` catches problems after deploy by speaking the protocol to a running server. A production setup typically runs both.
37
-
38
- Both specs target MCP spec version `2025-11-25` and ship machine-readable rule catalogs with stable rule IDs.
39
-
40
- ---
41
-
42
- ## Table of contents
43
-
44
- - [1. MCP Config Landscape Reference](#1-mcp-config-landscape-reference)
45
- - [1.1 Config format](#11-config-format)
46
- - [1.2 Server entry fields](#12-server-entry-fields)
47
- - [1.3 File locations by client](#13-file-locations-by-client)
48
- - [1.4 Environment variable syntax](#14-environment-variable-syntax)
49
- - [1.5 Override precedence](#15-override-precedence)
50
- - [1.6 Platform-specific behaviors](#16-platform-specific-behaviors)
51
- - [2. Lint Rules](#2-lint-rules)
52
- - [2.1 mcp-schema — structural validation](#21-mcp-schema--structural-validation)
53
- - [2.2 mcp-securityhardcoded secrets](#22-mcp-security--hardcoded-secrets)
54
- - [2.3 mcp-commandsstdio command validation](#23-mcp-commands--stdio-command-validation)
55
- - [2.4 mcp-deprecateddeprecated patterns](#24-mcp-deprecated--deprecated-patterns)
56
- - [2.5 mcp-envenvironment variable validation](#25-mcp-env--environment-variable-validation)
57
- - [2.6 mcp-urlsURL validation](#26-mcp-urls--url-validation)
58
- - [2.7 mcp-consistencycross-file consistency](#27-mcp-consistency--cross-file-consistency)
59
- - [2.8 mcp-redundancyunnecessary configs](#28-mcp-redundancy--unnecessary-configs)
60
- - [3. Rule Catalog (machine-readable)](#3-rule-catalog-machine-readable)
61
- - [4. Implementing This Specification](#4-implementing-this-specification)
62
- - [5. Contributing](#5-contributing)
63
-
64
- ---
65
-
66
- ## 1. MCP Config Landscape Reference
67
-
68
- This section documents the full MCP server configuration landscape as of April 2026. Implementors should treat this as the authoritative cross-client reference for file locations, formats, and behaviors.
69
-
70
- ### 1.1 Config format
71
-
72
- Every MCP config file is a JSON object with a root key containing named server entries. Each server entry describes how the client connects to one MCP server.
73
-
74
- There are two active transport types:
75
-
76
- **stdio** — the client launches a local subprocess and communicates over stdin/stdout using JSON-RPC:
77
-
78
- ```json
79
- {
80
- "mcpServers": {
81
- "my-server": {
82
- "command": "npx",
83
- "args": ["-y", "@example/mcp-server"],
84
- "env": { "DEBUG": "true" }
85
- }
86
- }
87
- }
88
- ```
89
-
90
- **Streamable HTTP** — the client connects to a remote URL over HTTP:
91
-
92
- ```json
93
- {
94
- "mcpServers": {
95
- "my-server": {
96
- "type": "http",
97
- "url": "https://my-server.example.com/mcp",
98
- "headers": {
99
- "Authorization": "Bearer ${API_KEY}"
100
- }
101
- }
102
- }
103
- }
104
- ```
105
-
106
- **SSE (Server-Sent Events)** — deprecated as of the March 2025 MCP spec update. Uses `"type": "sse"`. Still supported by most clients but should be migrated to Streamable HTTP.
107
-
108
- ### 1.2 Server entry fields
109
-
110
- | Field | Type | Transport | Required | Description |
111
- |---|---|---|---|---|
112
- | `type` | `"stdio"` \| `"http"` \| `"sse"` | All | No | Transport protocol. Defaults to `stdio` if `command` is present. |
113
- | `command` | string | stdio | Yes | Executable to launch as a subprocess. |
114
- | `args` | string[] | stdio | No | Arguments passed to the command. |
115
- | `env` | Record<string, string> | stdio | No | Environment variables for the subprocess. |
116
- | `url` | string | http, sse | Yes | Remote endpoint URL. |
117
- | `headers` | Record<string, string> | http, sse | No | HTTP headers sent with every request. |
118
- | `disabled` | boolean | All | No | Whether the server is disabled. (Cline-specific) |
119
- | `autoApprove` | string[] | All | No | Tool names to auto-approve without user confirmation. (Cline-specific) |
120
- | `timeout` | number (ms) | All | No | Max response wait time. Default: 60000. (Amazon Q-specific) |
121
- | `oauth` | object | http | No | OAuth 2.0 configuration. (Claude Code-specific) |
122
- | `headersHelper` | string | http | No | Shell command that outputs JSON headers to stdout. (Claude Code-specific) |
123
-
124
- ### 1.3 File locations by client
125
-
126
- #### Project-level configs
127
-
128
- These live relative to the project root and are typically committed to version control.
129
-
130
- | File path | Client | Root key | Notes |
131
- |---|---|---|---|
132
- | `.mcp.json` | Claude Code | `mcpServers` | The universal project-level convention. |
133
- | `.cursor/mcp.json` | Cursor | `mcpServers` | |
134
- | `.vscode/mcp.json` | VS Code / GitHub Copilot | **`servers`** | Only client that uses `servers` instead of `mcpServers`. |
135
- | `.amazonq/mcp.json` | Amazon Q Developer | `mcpServers` | Server names must be unique across project + global. |
136
- | `.continue/mcpServers/*.json` | Continue.dev | varies | Accepts config files from any client format. |
137
-
138
- #### User/global-level configs
139
-
140
- These are user-specific and not committed to version control.
141
-
142
- | File path | Client | Root key | Platform |
143
- |---|---|---|---|
144
- | `~/.claude.json` | Claude Code | `mcpServers` | All |
145
- | `~/.claude/settings.json` | Claude Code | `mcpServers` | All |
146
- | `~/.cursor/mcp.json` | Cursor | `mcpServers` | All |
147
- | `~/Library/Application Support/Claude/claude_desktop_config.json` | Claude Desktop | `mcpServers` | macOS |
148
- | `%APPDATA%\Claude\claude_desktop_config.json` | Claude Desktop | `mcpServers` | Windows |
149
- | `~/.codeium/windsurf/mcp_config.json` | Windsurf | `mcpServers` | All |
150
- | `~/.aws/amazonq/mcp.json` | Amazon Q | `mcpServers` | All |
151
- | VS Code globalStorage `saoudrizwan.claude-dev/settings/cline_mcp_settings.json` | Cline | `mcpServers` | All |
152
-
153
- ### 1.4 Environment variable syntax
154
-
155
- Different clients use different syntax for referencing environment variables in config values.
156
-
157
- | Client | Syntax | Default value support | Example |
158
- |---|---|---|---|
159
- | Claude Code | `${VAR}` | `${VAR:-default}` | `${API_KEY}` |
160
- | Cursor | `${env:VAR}` | No | `${env:API_KEY}` |
161
- | Continue.dev | `${{ secrets.VAR }}` | No | `${{ secrets.API_KEY }}` |
162
- | Windsurf | `${env:VAR}` | No | `${env:API_KEY}` |
163
- | Claude Desktop | Not supported | N/A | Literal values only |
164
- | Amazon Q | Not supported | N/A | Literal values only |
165
-
166
- Env var expansion applies to `command`, `args`, `env`, `url`, and `headers` fields (where supported).
167
-
168
- ### 1.5 Override precedence
169
-
170
- When the same server name exists at multiple scopes, the most specific scope wins.
171
-
172
- **Claude Code** (three-tier):
173
- 1. **Local** (highest) — per-user, per-project overrides in `~/.claude.json` under a project path key
174
- 2. **Project** — `.mcp.json` at the repo root
175
- 3. **User** (lowest) — `~/.claude.json` top-level `mcpServers`
176
-
177
- **Cursor:** project `.cursor/mcp.json` overrides global `~/.cursor/mcp.json`.
178
-
179
- **Amazon Q:** workspace `.amazonq/mcp.json` overrides global `~/.aws/amazonq/mcp.json`. Server names must be unique across both.
180
-
181
- **VS Code:** workspace `.vscode/mcp.json` overrides user-level configuration.
182
-
183
- **Windsurf, Cline:** Single global config. No override behavior.
184
-
185
- ### 1.6 Platform-specific behaviors
186
-
187
- **Windows + npx (stdio):** On native Windows (not WSL), `npx` commands must be wrapped with `cmd /c`:
188
- ```json
189
- {
190
- "command": "cmd",
191
- "args": ["/c", "npx", "-y", "@example/mcp-server"]
192
- }
193
- ```
194
- Without this wrapper, the subprocess fails to spawn. This is the most common Windows MCP config issue.
195
-
196
- **Claude.ai custom connectors:** Only support remote MCP servers over Streamable HTTP. No stdio support — browsers cannot launch local subprocesses. stdio-only servers must be hosted remotely to be used with Claude.ai.
197
-
198
- ---
199
-
200
- ## 2. Lint Rules
201
-
202
- 29 rules organized into 8 categories. Each rule has a unique ID, severity level, trigger condition, and message template.
203
-
204
- Severity levels:
205
- - **error** — the config is broken or has a security issue. Should fail CI.
206
- - **warning** the config has a likely problem. May or may not fail CI depending on strictness.
207
- - **info** — the config has a potential improvement. Never fails CI.
208
-
209
- ### 2.1 mcp-schema — structural validation
210
-
211
- Validates that the config file is well-formed JSON with the correct structure for its target client.
212
-
213
- | Rule ID | Severity | Trigger | Message |
214
- |---|---|---|---|
215
- | `mcp-schema/invalid-json` | error | File is not valid JSON | `MCP config is not valid JSON: {parseError}` |
216
- | `mcp-schema/wrong-root-key` | error | Root key doesn't match expected key for the client | `{file} must use "{expected}" as root key, not "{actual}"` |
217
- | `mcp-schema/missing-root-key` | error | No recognized root key (`mcpServers` or `servers`) | `MCP config has no "{expected}" key` |
218
- | `mcp-schema/missing-command` | error | stdio server has no `command` field | `Server "{name}" has no "command" field` |
219
- | `mcp-schema/missing-url` | error | http/sse server has no `url` field | `Server "{name}" has no "url" field` |
220
- | `mcp-schema/no-name-field` | error | A server entry's key (its name) is the empty string | `Server name cannot be empty` |
221
- | `mcp-schema/unknown-transport` | warning | Transport cannot be classified: a `type` outside `stdio`/`http`/`sse`, a non-string `type`, or an entry with neither `command` nor `url` | `Server "{name}" has unknown transport type "{type}"` (or, with no classifiable fields at all: `Server "{name}" has no recognizable transport — expected "command", "url", or a valid "type"`) |
222
- | `mcp-schema/ambiguous-transport` | warning | Server has both `command` and `url` fields | `Server "{name}" has both "command" and "url" — transport is ambiguous` |
223
- | `mcp-schema/empty-servers` | info | Root key exists but contains no server entries | `MCP config has no server entries` |
224
-
225
- **Auto-fixable:** `wrong-root-key` rename the root key to match the expected key.
226
-
227
- ### 2.2 mcp-securityhardcoded secrets
228
-
229
- Detects secrets committed to version control in MCP config files. The three secret rules (`hardcoded-bearer`, `hardcoded-api-key`, `secret-in-url`) only flag issues in git-tracked files — an untracked config leaks nothing to teammates. `mcp-security/http-no-tls` is a transport concern, independent of version control, and fires regardless of git tracking. When the tracked status cannot be determined at all (git unavailable or failing, as opposed to a determined "untracked"), the linter says so via `mcp-security/secret-scan-skipped` instead of silently passing a possibly-tracked file.
230
-
231
- | Rule ID | Severity | Trigger | Message |
232
- |---|---|---|---|
233
- | `mcp-security/hardcoded-bearer` | error | `Authorization` header contains a literal Bearer token (not an env var reference) in a git-tracked file | `Server "{name}" has a hardcoded Bearer token in a git-tracked file` |
234
- | `mcp-security/hardcoded-api-key` | error | Header or env value matches known API key patterns (or the high-entropy heuristic below) in a git-tracked file | `Server "{name}" has a hardcoded API key in a git-tracked file` |
235
- | `mcp-security/secret-in-url` | error | URL contains query params that look like secrets (`?key=`, `?token=`, `?api_key=`) in a git-tracked file | `Server "{name}" has a secret in the URL query string` |
236
- | `mcp-security/secret-scan-skipped` | info | Git-tracked status could not be determined (git unavailable/failing — not merely untracked), so the three git-gated secret rules were skipped | `Could not determine git-tracked status of {file}; hardcoded-secret rules were skipped` |
237
- | `mcp-security/http-no-tls` | warning | URL uses `http://` for a non-loopback target (loopback = `localhost`, `[::1]`, `127.0.0.0/8`) | `Server "{name}" uses HTTP without TLS` |
238
-
239
- **Known API key patterns:**
240
- ```
241
- sk-ant-[A-Za-z0-9_-]{20,} # Anthropic
242
- sk-proj-[A-Za-z0-9_-]{20,} # OpenAI project-scoped
243
- sk-[a-zA-Z0-9]{20,} # OpenAI classic / generic (alphanumeric-only:
244
- # [-_] would swallow kebab-case identifiers)
245
- ghp_[a-zA-Z0-9]{36} # GitHub personal access token
246
- ghu_[a-zA-Z0-9]{36} # GitHub user token
247
- github_pat_[a-zA-Z0-9_]{80,} # GitHub fine-grained PAT
248
- xoxb-[0-9]{10,} # Slack bot token
249
- xoxp-[0-9]{10,} # Slack user token
250
- AKIA[0-9A-Z]{16} # AWS access key ID
251
- AGE-SECRET-KEY-1[a-zA-Z0-9]+ # age encryption secret key
252
- glpat-[a-zA-Z0-9_\-]{20} # GitLab personal access token
253
- sq0atp-[a-zA-Z0-9_\-]{22} # Square access token
254
- ```
255
-
256
- **High-entropy heuristic:** additionally flag an env value > 20 characters that is entirely alphanumeric/base64 characters, is not an env var reference (`${...}`, `${{ ... }}`), AND whose variable name contains a secret-suggesting keyword (`KEY`, `TOKEN`, `SECRET`, `PASSWORD`, `AUTH`, `CREDENTIAL`, `SIGNING`, `SESSION`, `COOKIE`, ...). The name gate is deliberate: without it, build IDs, commit SHAs, version strings, and feature-flag tokens false-positive.
257
-
258
- **Auto-fixable:** `hardcoded-bearer`, `hardcoded-api-key` replace literal value with an env var reference derived from the server name (e.g., `MY_SERVER_API_KEY`).
259
-
260
- ### 2.3 mcp-commands — stdio command validation
261
-
262
- Validates that stdio server commands and file-path arguments are viable.
263
-
264
- | Rule ID | Severity | Trigger | Message |
265
- |---|---|---|---|
266
- | `mcp-commands/windows-npx-no-wrapper` | error | Platform is Windows and `command` is `npx` without `cmd /c` wrapper | `Server "{name}": npx requires "cmd /c" wrapper on Windows` |
267
- | `mcp-commands/command-not-found` | warning | `command` is a relative path (`./`, `../`) that doesn't exist (project-scope configs only) | `Server "{name}": command "{command}" not found` |
268
- | `mcp-commands/args-path-missing` | warning | An arg matches a file path pattern and the file doesn't exist (relative paths: project-scope configs only; absolute paths: every scope) | `Server "{name}": arg "{arg}" looks like a file path but doesn't exist` |
269
-
270
- **Notes:**
271
- - `windows-npx-no-wrapper` should only flag project-level configs, not global configs (the user may be developing cross-platform).
272
- - `args-path-missing` should only check args that look like file paths (contain `/` with a file extension, or start with `./` / `../`). Skip npm package names and flags.
273
- - Do not validate that system commands (`npx`, `node`, `python`) exist on PATH that is a runtime concern, not a config concern.
274
-
275
- **Auto-fixable:** `windows-npx-no-wrapper` — rewrite `{"command": "npx", "args": [...]}` to `{"command": "cmd", "args": ["/c", "npx", ...]}`.
276
-
277
- ### 2.4 mcp-deprecated — deprecated patterns
278
-
279
- Flags usage of deprecated MCP transport protocols and patterns.
280
-
281
- | Rule ID | Severity | Trigger | Message |
282
- |---|---|---|---|
283
- | `mcp-deprecated/sse-transport` | warning | Server uses `"type": "sse"` | `Server "{name}" uses deprecated SSE transport — use "http" (Streamable HTTP) instead` |
284
-
285
- **Auto-fixable:** `sse-transport` — replace `"sse"` with `"http"`.
286
-
287
- ### 2.5 mcp-env — environment variable validation
288
-
289
- Validates environment variable references for correctness and client compatibility.
290
-
291
- | Rule ID | Severity | Trigger | Message |
292
- |---|---|---|---|
293
- | `mcp-env/wrong-syntax` | error | Env var reference uses wrong syntax for the target client | `Server "{name}": {client} uses {expected}, not {actual}` |
294
- | `mcp-env/unset-variable` | info | Referenced env var is not set in the current environment | `Server "{name}": environment variable "{var}" is not set` |
295
- | `mcp-env/empty-env-block` | info | `env` object is present but empty | `Server "{name}": empty "env" block can be removed` |
296
-
297
- **Syntax validation matrix:**
298
-
299
- | Config file | Expected syntax | Flag if found |
300
- |---|---|---|
301
- | `.mcp.json` | `${VAR}` | `${env:VAR}` |
302
- | `.cursor/mcp.json` | `${env:VAR}` | `${VAR}` (bare, without `env:`) |
303
- | `.continue/mcpServers/*.json` | `${{ secrets.VAR }}` | `${VAR}` or `${env:VAR}` |
304
- | All others | `${VAR}` | — |
305
-
306
- **Notes:**
307
- - `unset-variable` is intentionally `info` severity. Many env vars are set only in CI, `.env` files, or shell profiles that aren't available during linting.
308
- - `unset-variable` is skipped entirely for Continue configs — their `${{ secrets.VAR }}` references resolve from GitHub Actions secrets, not the local environment, so every correct Continue config would false-positive.
309
- - Scan all string values in `command`, `args`, `url`, `headers`, and `env` for env var references.
310
-
311
- **Auto-fixable:** `wrong-syntax` rewrite to the correct syntax for the target client.
312
-
313
- ### 2.6 mcp-urls — URL validation
314
-
315
- Validates remote server URLs for correctness and team usability.
316
-
317
- | Rule ID | Severity | Trigger | Message |
318
- |---|---|---|---|
319
- | `mcp-urls/malformed-url` | error | URL is not parseable (after skipping env var placeholders) | `Server "{name}": invalid URL "{url}"` |
320
- | `mcp-urls/localhost-in-project-config` | warning | URL host is a loopback address (`localhost`, `[::1]`, `127.0.0.0/8` — the same set `http-no-tls` exempts) in a project-level config | `Server "{name}": loopback URL in project config won't work for teammates` |
321
- | `mcp-urls/missing-path` | info | URL has no path or just `/` | `Server "{name}": URL has no path — most MCP servers expect /mcp` |
322
-
323
- **Notes:**
324
- - If the URL contains env var references (`${...}`), skip `malformed-url` — it cannot be validated statically.
325
- - `localhost-in-project-config` should only flag project-scoped files (committed to version control by convention), not global configs where loopback URLs are expected. Strip IPv6 brackets before classifying the host, and treat the whole `127.0.0.0/8` block as loopback — `127.0.0.2` is just as unreachable for a teammate as `127.0.0.1`.
326
-
327
- ### 2.7 mcp-consistency cross-file consistency
328
-
329
- Compares MCP configs across multiple files in the same project. This is a cross-file check that runs after all individual configs are parsed.
330
-
331
- | Rule ID | Severity | Trigger | Message |
332
- |---|---|---|---|
333
- | `mcp-consistency/same-server-different-config` | warning | Server with the same name exists in 2+ same-scope config files (project-project or user-user) with different URLs or commands — cross-scope pairs are client precedence, not drift | `Server "{name}" is configured differently in {file1} and {file2}` |
334
- | `mcp-consistency/duplicate-server-name` | warning | Same server name appears more than once in a single file | `Duplicate server name "{name}" in {file} only the last definition is used` |
335
- | `mcp-consistency/missing-from-client` | info | Server exists in `.mcp.json` but is absent from another client's project config that also exists | `Server "{name}" is in .mcp.json but missing from {file}` |
336
-
337
- **Notes:**
338
- - For `same-server-different-config`, compare `url`/`command`/`args`. Ignore `headers` differences (auth tokens intentionally differ per user).
339
- - `missing-from-client` is informational only. Teams may intentionally have different server sets per client.
340
-
341
- ### 2.8 mcp-redundancy unnecessary configs
342
-
343
- Flags configs that may be unnecessary or stale.
344
-
345
- | Rule ID | Severity | Trigger | Message |
346
- |---|---|---|---|
347
- | `mcp-redundancy/disabled-server` | info | Server has `"disabled": true` | `Server "{name}" is disabled — consider removing if no longer needed` |
348
- | `mcp-redundancy/identical-across-scopes` | info | Same server with identical config at both project and global scope | `Server "{name}" is identically configured in {projectFile} and {globalFile}` |
349
-
350
- **Notes:**
351
- - The `disabled` field is Cline-specific (see [Section 1.2](#12-server-entry-fields)), but `disabled-server` fires on any client's config that carries it a stale `"disabled": true` is dead weight regardless of which client wrote it.
352
-
353
- ---
354
-
355
- ## 3. Rule Catalog (machine-readable)
356
-
357
- A machine-readable JSON catalog of all rules is available at [`mcp-config-lint-rules.json`](./mcp-config-lint-rules.json).
358
-
359
- The catalog enables:
360
- - AI agents to understand what rules exist and when they apply
361
- - Tool authors to import rule definitions programmatically
362
- - CI systems to configure which rules to enable/disable
363
- - Documentation generators to stay in sync with the rule set
364
-
365
- See the JSON file for the full schema.
366
-
367
- ---
368
-
369
- ## 4. Implementing This Specification
370
-
371
- This specification is designed to be implementable by any tool. Here is how the pieces map to a typical linter architecture:
372
-
373
- ### Discovery
374
-
375
- Scan for the project-level config files listed in [Section 1.3](#13-file-locations-by-client). Optionally scan global/user-level configs when the user opts in (these contain personal data and should not be scanned by default).
376
-
377
- ### Parsing
378
-
379
- Parse JSON and normalize into a common structure regardless of which client's format the file uses. Key normalization steps:
380
- 1. Detect the client from the file path
381
- 2. Determine the expected root key (`servers` for VS Code, `mcpServers` for all others)
382
- 3. Infer transport type from fields: `command` present = stdio, `url` present = http/sse, explicit `type` field takes precedence
383
- 4. Extract server entries into a uniform shape
384
-
385
- ### Checking
386
-
387
- Run per-file checks (schema, security, commands, deprecated, env, urls, redundancy) independently per config file. Run cross-file checks (consistency) after all files are parsed.
388
-
389
- ### Reporting
390
-
391
- Rules use the `category/rule-id` naming convention (e.g., `mcp-security/hardcoded-bearer`). This maps cleanly to SARIF rule IDs for GitHub Code Scanning integration.
392
-
393
- ### Fixing
394
-
395
- Rules marked as auto-fixable should apply surgical string replacements to the JSON file without reformatting the user's style (indentation, trailing commas, key ordering). Validate that the result is still valid JSON after applying fixes.
396
-
397
- ---
398
-
399
- ## 5. Contributing
400
-
401
- This specification is maintained at [github.com/YawLabs/ctxlint](https://github.com/YawLabs/ctxlint).
402
-
403
- To propose changes:
404
- - **New rules:** Open an issue describing the rule, its severity, trigger condition, and which clients it applies to.
405
- - **Client additions:** As new MCP clients emerge, submit a PR adding their config file location, root key, and any client-specific behaviors to Section 1.
406
- - **Corrections:** If any client behavior documented here is inaccurate, open an issue with evidence (link to client docs, source code, or reproduction steps).
407
-
408
- ### Versioning
409
-
410
- This specification follows semver:
411
- - **Patch** (1.0.x): Typo fixes, clarifications, no rule changes
412
- - **Minor** (1.x.0): New rules added, new clients documented
413
- - **Major** (x.0.0): Rules removed or semantics changed in breaking ways
414
-
415
- ### Related specifications and tools
416
-
417
- - [Model Context Protocol Specification](https://spec.modelcontextprotocol.io/) — the underlying protocol this config format serves
418
- - [ctxlint](https://github.com/YawLabs/ctxlint) reference implementation of this specification
419
- - [mcp-compliance](https://github.com/YawLabs/mcp-compliance) tests MCP server *behavior* against the protocol spec (complementary to config linting)
1
+ # MCP Server Configuration Linting Specification
2
+
3
+ **Version:** 1.0.0-draft
4
+ **Date:** 2026-04-07
5
+ **MCP Spec Compatibility:** 2025-11-25 (Streamable HTTP)
6
+ **Maintained by:** [Yaw Labs](https://yaw.sh) / [ctxlint](https://github.com/YawLabs/ctxlint)
7
+ **License:** CC BY 4.0
8
+
9
+ ---
10
+
11
+ ## What is this?
12
+
13
+ MCP server configuration files (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, etc.) define which tools an AI agent can access. They are a context interface — alongside instruction files like `CLAUDE.md` and `.cursorrules`, they shape what an agent knows and can do.
14
+
15
+ This specification defines a standard set of lint rules for validating MCP server configurations across all major AI coding clients. It is tool-agnostic: any linter, IDE extension, CI check, or AI agent can implement these rules.
16
+
17
+ The specification includes:
18
+
19
+ - A complete reference of MCP config file locations, formats, and client-specific behaviors
20
+ - 29 lint rules organized into 8 categories with defined severities
21
+ - A machine-readable rule catalog ([`mcp-config-lint-rules.json`](./mcp-config-lint-rules.json))
22
+ - Auto-fix definitions for rules that support automated correction
23
+
24
+ **Reference implementation:** [ctxlint](https://github.com/YawLabs/ctxlint) (v0.4.0+)
25
+
26
+ ---
27
+
28
+ ## Related specifications
29
+
30
+ This spec is part of a family of open specifications maintained by Yaw Labs for MCP tooling:
31
+
32
+ | Spec | Scope | Input |
33
+ | -------------------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------- |
34
+ | **mcp-config-lint** (this spec) | Static analysis of MCP client config files | `.cursor/mcp.json`, `.vscode/mcp.json`, `.mcp.json`, etc. |
35
+ | [**mcp-compliance**](https://github.com/YawLabs/mcp-compliance/blob/master/MCP_COMPLIANCE_SPEC.md) | Runtime testing of live MCP servers | A live server URL + transport |
36
+
37
+ The two are **complementary, not overlapping**. `mcp-config-lint` catches problems before deploy by reading JSON on disk; `mcp-compliance` catches problems after deploy by speaking the protocol to a running server. A production setup typically runs both.
38
+
39
+ Both specs target MCP spec version `2025-11-25` and ship machine-readable rule catalogs with stable rule IDs.
40
+
41
+ ---
42
+
43
+ ## Table of contents
44
+
45
+ - [1. MCP Config Landscape Reference](#1-mcp-config-landscape-reference)
46
+ - [1.1 Config format](#11-config-format)
47
+ - [1.2 Server entry fields](#12-server-entry-fields)
48
+ - [1.3 File locations by client](#13-file-locations-by-client)
49
+ - [1.4 Environment variable syntax](#14-environment-variable-syntax)
50
+ - [1.5 Override precedence](#15-override-precedence)
51
+ - [1.6 Platform-specific behaviors](#16-platform-specific-behaviors)
52
+ - [2. Lint Rules](#2-lint-rules)
53
+ - [2.1 mcp-schemastructural validation](#21-mcp-schema--structural-validation)
54
+ - [2.2 mcp-securityhardcoded secrets](#22-mcp-security--hardcoded-secrets)
55
+ - [2.3 mcp-commandsstdio command validation](#23-mcp-commands--stdio-command-validation)
56
+ - [2.4 mcp-deprecateddeprecated patterns](#24-mcp-deprecated--deprecated-patterns)
57
+ - [2.5 mcp-envenvironment variable validation](#25-mcp-env--environment-variable-validation)
58
+ - [2.6 mcp-urlsURL validation](#26-mcp-urls--url-validation)
59
+ - [2.7 mcp-consistencycross-file consistency](#27-mcp-consistency--cross-file-consistency)
60
+ - [2.8 mcp-redundancy unnecessary configs](#28-mcp-redundancy--unnecessary-configs)
61
+ - [3. Rule Catalog (machine-readable)](#3-rule-catalog-machine-readable)
62
+ - [4. Implementing This Specification](#4-implementing-this-specification)
63
+ - [5. Contributing](#5-contributing)
64
+
65
+ ---
66
+
67
+ ## 1. MCP Config Landscape Reference
68
+
69
+ This section documents the full MCP server configuration landscape as of April 2026. Implementors should treat this as the authoritative cross-client reference for file locations, formats, and behaviors.
70
+
71
+ ### 1.1 Config format
72
+
73
+ Every MCP config file is a JSON object with a root key containing named server entries. Each server entry describes how the client connects to one MCP server.
74
+
75
+ There are two active transport types:
76
+
77
+ **stdio** — the client launches a local subprocess and communicates over stdin/stdout using JSON-RPC:
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "my-server": {
83
+ "command": "npx",
84
+ "args": ["-y", "@example/mcp-server"],
85
+ "env": { "DEBUG": "true" }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Streamable HTTP** — the client connects to a remote URL over HTTP:
92
+
93
+ ```json
94
+ {
95
+ "mcpServers": {
96
+ "my-server": {
97
+ "type": "http",
98
+ "url": "https://my-server.example.com/mcp",
99
+ "headers": {
100
+ "Authorization": "Bearer ${API_KEY}"
101
+ }
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ **SSE (Server-Sent Events)** — deprecated as of the March 2025 MCP spec update. Uses `"type": "sse"`. Still supported by most clients but should be migrated to Streamable HTTP.
108
+
109
+ ### 1.2 Server entry fields
110
+
111
+ | Field | Type | Transport | Required | Description |
112
+ | --------------- | -------------------------------- | --------- | -------- | ------------------------------------------------------------------------- |
113
+ | `type` | `"stdio"` \| `"http"` \| `"sse"` | All | No | Transport protocol. Defaults to `stdio` if `command` is present. |
114
+ | `command` | string | stdio | Yes | Executable to launch as a subprocess. |
115
+ | `args` | string[] | stdio | No | Arguments passed to the command. |
116
+ | `env` | Record<string, string> | stdio | No | Environment variables for the subprocess. |
117
+ | `url` | string | http, sse | Yes | Remote endpoint URL. |
118
+ | `headers` | Record<string, string> | http, sse | No | HTTP headers sent with every request. |
119
+ | `disabled` | boolean | All | No | Whether the server is disabled. (Cline-specific) |
120
+ | `autoApprove` | string[] | All | No | Tool names to auto-approve without user confirmation. (Cline-specific) |
121
+ | `timeout` | number (ms) | All | No | Max response wait time. Default: 60000. (Amazon Q-specific) |
122
+ | `oauth` | object | http | No | OAuth 2.0 configuration. (Claude Code-specific) |
123
+ | `headersHelper` | string | http | No | Shell command that outputs JSON headers to stdout. (Claude Code-specific) |
124
+
125
+ ### 1.3 File locations by client
126
+
127
+ #### Project-level configs
128
+
129
+ These live relative to the project root and are typically committed to version control.
130
+
131
+ | File path | Client | Root key | Notes |
132
+ | ----------------------------- | ------------------------ | ------------- | -------------------------------------------------------- |
133
+ | `.mcp.json` | Claude Code | `mcpServers` | The universal project-level convention. |
134
+ | `.cursor/mcp.json` | Cursor | `mcpServers` | |
135
+ | `.vscode/mcp.json` | VS Code / GitHub Copilot | **`servers`** | Only client that uses `servers` instead of `mcpServers`. |
136
+ | `.amazonq/mcp.json` | Amazon Q Developer | `mcpServers` | Server names must be unique across project + global. |
137
+ | `.continue/mcpServers/*.json` | Continue.dev | varies | Accepts config files from any client format. |
138
+
139
+ #### User/global-level configs
140
+
141
+ These are user-specific and not committed to version control.
142
+
143
+ | File path | Client | Root key | Platform |
144
+ | ------------------------------------------------------------------------------- | -------------- | ------------ | -------- |
145
+ | `~/.claude.json` | Claude Code | `mcpServers` | All |
146
+ | `~/.claude/settings.json` | Claude Code | `mcpServers` | All |
147
+ | `~/.cursor/mcp.json` | Cursor | `mcpServers` | All |
148
+ | `~/Library/Application Support/Claude/claude_desktop_config.json` | Claude Desktop | `mcpServers` | macOS |
149
+ | `%APPDATA%\Claude\claude_desktop_config.json` | Claude Desktop | `mcpServers` | Windows |
150
+ | `~/.codeium/windsurf/mcp_config.json` | Windsurf | `mcpServers` | All |
151
+ | `~/.aws/amazonq/mcp.json` | Amazon Q | `mcpServers` | All |
152
+ | VS Code globalStorage `saoudrizwan.claude-dev/settings/cline_mcp_settings.json` | Cline | `mcpServers` | All |
153
+
154
+ ### 1.4 Environment variable syntax
155
+
156
+ Different clients use different syntax for referencing environment variables in config values.
157
+
158
+ | Client | Syntax | Default value support | Example |
159
+ | -------------- | -------------------- | --------------------- | ------------------------ |
160
+ | Claude Code | `${VAR}` | `${VAR:-default}` | `${API_KEY}` |
161
+ | Cursor | `${env:VAR}` | No | `${env:API_KEY}` |
162
+ | Continue.dev | `${{ secrets.VAR }}` | No | `${{ secrets.API_KEY }}` |
163
+ | Windsurf | `${env:VAR}` | No | `${env:API_KEY}` |
164
+ | Claude Desktop | Not supported | N/A | Literal values only |
165
+ | Amazon Q | Not supported | N/A | Literal values only |
166
+
167
+ Env var expansion applies to `command`, `args`, `env`, `url`, and `headers` fields (where supported).
168
+
169
+ ### 1.5 Override precedence
170
+
171
+ When the same server name exists at multiple scopes, the most specific scope wins.
172
+
173
+ **Claude Code** (three-tier):
174
+
175
+ 1. **Local** (highest) — per-user, per-project overrides in `~/.claude.json` under a project path key
176
+ 2. **Project** — `.mcp.json` at the repo root
177
+ 3. **User** (lowest) — `~/.claude.json` top-level `mcpServers`
178
+
179
+ **Cursor:** project `.cursor/mcp.json` overrides global `~/.cursor/mcp.json`.
180
+
181
+ **Amazon Q:** workspace `.amazonq/mcp.json` overrides global `~/.aws/amazonq/mcp.json`. Server names must be unique across both.
182
+
183
+ **VS Code:** workspace `.vscode/mcp.json` overrides user-level configuration.
184
+
185
+ **Windsurf, Cline:** Single global config. No override behavior.
186
+
187
+ ### 1.6 Platform-specific behaviors
188
+
189
+ **Windows + npx (stdio):** On native Windows (not WSL), `npx` commands must be wrapped with `cmd /c`:
190
+
191
+ ```json
192
+ {
193
+ "command": "cmd",
194
+ "args": ["/c", "npx", "-y", "@example/mcp-server"]
195
+ }
196
+ ```
197
+
198
+ Without this wrapper, the subprocess fails to spawn. This is the most common Windows MCP config issue.
199
+
200
+ **Claude.ai custom connectors:** Only support remote MCP servers over Streamable HTTP. No stdio support — browsers cannot launch local subprocesses. stdio-only servers must be hosted remotely to be used with Claude.ai.
201
+
202
+ ---
203
+
204
+ ## 2. Lint Rules
205
+
206
+ 29 rules organized into 8 categories. Each rule has a unique ID, severity level, trigger condition, and message template.
207
+
208
+ Severity levels:
209
+
210
+ - **error** — the config is broken or has a security issue. Should fail CI.
211
+ - **warning** the config has a likely problem. May or may not fail CI depending on strictness.
212
+ - **info** — the config has a potential improvement. Never fails CI.
213
+
214
+ ### 2.1 mcp-schema — structural validation
215
+
216
+ Validates that the config file is well-formed JSON with the correct structure for its target client.
217
+
218
+ | Rule ID | Severity | Trigger | Message |
219
+ | -------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
220
+ | `mcp-schema/invalid-json` | error | File is not valid JSON | `MCP config is not valid JSON: {parseError}` |
221
+ | `mcp-schema/wrong-root-key` | error | Root key doesn't match expected key for the client | `{file} must use "{expected}" as root key, not "{actual}"` |
222
+ | `mcp-schema/missing-root-key` | error | No recognized root key (`mcpServers` or `servers`) | `MCP config has no "{expected}" key` |
223
+ | `mcp-schema/missing-command` | error | stdio server has no `command` field | `Server "{name}" has no "command" field` |
224
+ | `mcp-schema/missing-url` | error | http/sse server has no `url` field | `Server "{name}" has no "url" field` |
225
+ | `mcp-schema/no-name-field` | error | A server entry's key (its name) is the empty string | `Server name cannot be empty` |
226
+ | `mcp-schema/unknown-transport` | warning | Transport cannot be classified: a `type` outside `stdio`/`http`/`sse`, a non-string `type`, or an entry with neither `command` nor `url` | `Server "{name}" has unknown transport type "{type}"` (or, with no classifiable fields at all: `Server "{name}" has no recognizable transport — expected "command", "url", or a valid "type"`) |
227
+ | `mcp-schema/ambiguous-transport` | warning | Server has both `command` and `url` fields | `Server "{name}" has both "command" and "url" transport is ambiguous` |
228
+ | `mcp-schema/empty-servers` | info | Root key exists but contains no server entries | `MCP config has no server entries` |
229
+
230
+ **Auto-fixable:** `wrong-root-key` — rename the root key to match the expected key.
231
+
232
+ ### 2.2 mcp-security — hardcoded secrets
233
+
234
+ Detects secrets committed to version control in MCP config files. The three secret rules (`hardcoded-bearer`, `hardcoded-api-key`, `secret-in-url`) only flag issues in git-tracked files an untracked config leaks nothing to teammates. `mcp-security/http-no-tls` is a transport concern, independent of version control, and fires regardless of git tracking. When the tracked status cannot be determined at all (git unavailable or failing, as opposed to a determined "untracked"), the linter says so via `mcp-security/secret-scan-skipped` instead of silently passing a possibly-tracked file.
235
+
236
+ | Rule ID | Severity | Trigger | Message |
237
+ | ---------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
238
+ | `mcp-security/hardcoded-bearer` | error | `Authorization` header contains a literal Bearer token (not an env var reference) in a git-tracked file | `Server "{name}" has a hardcoded Bearer token in a git-tracked file` |
239
+ | `mcp-security/hardcoded-api-key` | error | Header or env value matches known API key patterns (or the high-entropy heuristic below) in a git-tracked file | `Server "{name}" has a hardcoded API key in a git-tracked file` |
240
+ | `mcp-security/secret-in-url` | error | URL contains query params that look like secrets (`?key=`, `?token=`, `?api_key=`) in a git-tracked file | `Server "{name}" has a secret in the URL query string` |
241
+ | `mcp-security/secret-scan-skipped` | info | Git-tracked status could not be determined (git unavailable/failing — not merely untracked), so the three git-gated secret rules were skipped | `Could not determine git-tracked status of {file}; hardcoded-secret rules were skipped` |
242
+ | `mcp-security/http-no-tls` | warning | URL uses `http://` for a non-loopback target (loopback = `localhost`, `[::1]`, `127.0.0.0/8`) | `Server "{name}" uses HTTP without TLS` |
243
+
244
+ **Known API key patterns:**
245
+
246
+ ```
247
+ sk-ant-[A-Za-z0-9_-]{20,} # Anthropic
248
+ sk-proj-[A-Za-z0-9_-]{20,} # OpenAI project-scoped
249
+ sk-[a-zA-Z0-9]{20,} # OpenAI classic / generic (alphanumeric-only:
250
+ # [-_] would swallow kebab-case identifiers)
251
+ ghp_[a-zA-Z0-9]{36} # GitHub personal access token
252
+ ghu_[a-zA-Z0-9]{36} # GitHub user token
253
+ github_pat_[a-zA-Z0-9_]{80,} # GitHub fine-grained PAT
254
+ xoxb-[0-9]{10,} # Slack bot token
255
+ xoxp-[0-9]{10,} # Slack user token
256
+ AKIA[0-9A-Z]{16} # AWS access key ID
257
+ AGE-SECRET-KEY-1[a-zA-Z0-9]+ # age encryption secret key
258
+ glpat-[a-zA-Z0-9_\-]{20} # GitLab personal access token
259
+ sq0atp-[a-zA-Z0-9_\-]{22} # Square access token
260
+ ```
261
+
262
+ **High-entropy heuristic:** additionally flag an env value > 20 characters that is entirely alphanumeric/base64 characters, is not an env var reference (`${...}`, `${{ ... }}`), AND whose variable name contains a secret-suggesting keyword (`KEY`, `TOKEN`, `SECRET`, `PASSWORD`, `AUTH`, `CREDENTIAL`, `SIGNING`, `SESSION`, `COOKIE`, ...). The name gate is deliberate: without it, build IDs, commit SHAs, version strings, and feature-flag tokens false-positive.
263
+
264
+ **Auto-fixable:** `hardcoded-bearer`, `hardcoded-api-key` replace literal value with an env var reference derived from the server name (e.g., `MY_SERVER_API_KEY`).
265
+
266
+ ### 2.3 mcp-commands stdio command validation
267
+
268
+ Validates that stdio server commands and file-path arguments are viable.
269
+
270
+ | Rule ID | Severity | Trigger | Message |
271
+ | ------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
272
+ | `mcp-commands/windows-npx-no-wrapper` | error | Platform is Windows and `command` is `npx` without `cmd /c` wrapper | `Server "{name}": npx requires "cmd /c" wrapper on Windows` |
273
+ | `mcp-commands/command-not-found` | warning | `command` is a relative path (`./`, `../`) that doesn't exist (project-scope configs only) | `Server "{name}": command "{command}" not found` |
274
+ | `mcp-commands/args-path-missing` | warning | An arg matches a file path pattern and the file doesn't exist (relative paths: project-scope configs only; absolute paths: every scope) | `Server "{name}": arg "{arg}" looks like a file path but doesn't exist` |
275
+
276
+ **Notes:**
277
+
278
+ - `windows-npx-no-wrapper` should only flag project-level configs, not global configs (the user may be developing cross-platform).
279
+ - `args-path-missing` should only check args that look like file paths (contain `/` with a file extension, or start with `./` / `../`). Skip npm package names and flags.
280
+ - Do not validate that system commands (`npx`, `node`, `python`) exist on PATH — that is a runtime concern, not a config concern.
281
+
282
+ **Auto-fixable:** `windows-npx-no-wrapper` — rewrite `{"command": "npx", "args": [...]}` to `{"command": "cmd", "args": ["/c", "npx", ...]}`.
283
+
284
+ ### 2.4 mcp-deprecated — deprecated patterns
285
+
286
+ Flags usage of deprecated MCP transport protocols and patterns.
287
+
288
+ | Rule ID | Severity | Trigger | Message |
289
+ | ------------------------------ | -------- | --------------------------- | -------------------------------------------------------------------------------------- |
290
+ | `mcp-deprecated/sse-transport` | warning | Server uses `"type": "sse"` | `Server "{name}" uses deprecated SSE transport — use "http" (Streamable HTTP) instead` |
291
+
292
+ **Auto-fixable:** `sse-transport` — replace `"sse"` with `"http"`.
293
+
294
+ ### 2.5 mcp-env environment variable validation
295
+
296
+ Validates environment variable references for correctness and client compatibility.
297
+
298
+ | Rule ID | Severity | Trigger | Message |
299
+ | ------------------------- | -------- | --------------------------------------------------------- | ---------------------------------------------------------- |
300
+ | `mcp-env/wrong-syntax` | error | Env var reference uses wrong syntax for the target client | `Server "{name}": {client} uses {expected}, not {actual}` |
301
+ | `mcp-env/unset-variable` | info | Referenced env var is not set in the current environment | `Server "{name}": environment variable "{var}" is not set` |
302
+ | `mcp-env/empty-env-block` | info | `env` object is present but empty | `Server "{name}": empty "env" block can be removed` |
303
+
304
+ **Syntax validation matrix:**
305
+
306
+ | Config file | Expected syntax | Flag if found |
307
+ | ----------------------------- | -------------------- | ------------------------------- |
308
+ | `.mcp.json` | `${VAR}` | `${env:VAR}` |
309
+ | `.cursor/mcp.json` | `${env:VAR}` | `${VAR}` (bare, without `env:`) |
310
+ | `.continue/mcpServers/*.json` | `${{ secrets.VAR }}` | `${VAR}` or `${env:VAR}` |
311
+ | All others | `${VAR}` | |
312
+
313
+ **Notes:**
314
+
315
+ - `unset-variable` is intentionally `info` severity. Many env vars are set only in CI, `.env` files, or shell profiles that aren't available during linting.
316
+ - `unset-variable` is skipped entirely for Continue configs — their `${{ secrets.VAR }}` references resolve from GitHub Actions secrets, not the local environment, so every correct Continue config would false-positive.
317
+ - Scan all string values in `command`, `args`, `url`, `headers`, and `env` for env var references.
318
+
319
+ **Auto-fixable:** `wrong-syntax` rewrite to the correct syntax for the target client.
320
+
321
+ ### 2.6 mcp-urls URL validation
322
+
323
+ Validates remote server URLs for correctness and team usability.
324
+
325
+ | Rule ID | Severity | Trigger | Message |
326
+ | -------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
327
+ | `mcp-urls/malformed-url` | error | URL is not parseable (after skipping env var placeholders) | `Server "{name}": invalid URL "{url}"` |
328
+ | `mcp-urls/localhost-in-project-config` | warning | URL host is a loopback address (`localhost`, `[::1]`, `127.0.0.0/8` — the same set `http-no-tls` exempts) in a project-level config | `Server "{name}": loopback URL in project config won't work for teammates` |
329
+ | `mcp-urls/missing-path` | info | URL has no path or just `/` | `Server "{name}": URL has no path most MCP servers expect /mcp` |
330
+
331
+ **Notes:**
332
+
333
+ - If the URL contains env var references (`${...}`), skip `malformed-url` it cannot be validated statically.
334
+ - `localhost-in-project-config` should only flag project-scoped files (committed to version control by convention), not global configs where loopback URLs are expected. Strip IPv6 brackets before classifying the host, and treat the whole `127.0.0.0/8` block as loopback — `127.0.0.2` is just as unreachable for a teammate as `127.0.0.1`.
335
+
336
+ ### 2.7 mcp-consistency — cross-file consistency
337
+
338
+ Compares MCP configs across multiple files in the same project. This is a cross-file check that runs after all individual configs are parsed.
339
+
340
+ | Rule ID | Severity | Trigger | Message |
341
+ | ---------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
342
+ | `mcp-consistency/same-server-different-config` | warning | Server with the same name exists in 2+ same-scope config files (project-project or user-user) with different URLs or commands — cross-scope pairs are client precedence, not drift | `Server "{name}" is configured differently in {file1} and {file2}` |
343
+ | `mcp-consistency/duplicate-server-name` | warning | Same server name appears more than once in a single file | `Duplicate server name "{name}" in {file} — only the last definition is used` |
344
+ | `mcp-consistency/missing-from-client` | info | Server exists in `.mcp.json` but is absent from another client's project config that also exists | `Server "{name}" is in .mcp.json but missing from {file}` |
345
+
346
+ **Notes:**
347
+
348
+ - For `same-server-different-config`, compare `url`/`command`/`args`. Ignore `headers` differences (auth tokens intentionally differ per user).
349
+ - `missing-from-client` is informational only. Teams may intentionally have different server sets per client.
350
+
351
+ ### 2.8 mcp-redundancyunnecessary configs
352
+
353
+ Flags configs that may be unnecessary or stale.
354
+
355
+ | Rule ID | Severity | Trigger | Message |
356
+ | ---------------------------------------- | -------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
357
+ | `mcp-redundancy/disabled-server` | info | Server has `"disabled": true` | `Server "{name}" is disabled consider removing if no longer needed` |
358
+ | `mcp-redundancy/identical-across-scopes` | info | Same server with identical config at both project and global scope | `Server "{name}" is identically configured in {projectFile} and {globalFile}` |
359
+
360
+ **Notes:**
361
+
362
+ - The `disabled` field is Cline-specific (see [Section 1.2](#12-server-entry-fields)), but `disabled-server` fires on any client's config that carries it — a stale `"disabled": true` is dead weight regardless of which client wrote it.
363
+
364
+ ---
365
+
366
+ ## 3. Rule Catalog (machine-readable)
367
+
368
+ A machine-readable JSON catalog of all rules is available at [`mcp-config-lint-rules.json`](./mcp-config-lint-rules.json).
369
+
370
+ The catalog enables:
371
+
372
+ - AI agents to understand what rules exist and when they apply
373
+ - Tool authors to import rule definitions programmatically
374
+ - CI systems to configure which rules to enable/disable
375
+ - Documentation generators to stay in sync with the rule set
376
+
377
+ See the JSON file for the full schema.
378
+
379
+ ---
380
+
381
+ ## 4. Implementing This Specification
382
+
383
+ This specification is designed to be implementable by any tool. Here is how the pieces map to a typical linter architecture:
384
+
385
+ ### Discovery
386
+
387
+ Scan for the project-level config files listed in [Section 1.3](#13-file-locations-by-client). Optionally scan global/user-level configs when the user opts in (these contain personal data and should not be scanned by default).
388
+
389
+ ### Parsing
390
+
391
+ Parse JSON and normalize into a common structure regardless of which client's format the file uses. Key normalization steps:
392
+
393
+ 1. Detect the client from the file path
394
+ 2. Determine the expected root key (`servers` for VS Code, `mcpServers` for all others)
395
+ 3. Infer transport type from fields: `command` present = stdio, `url` present = http/sse, explicit `type` field takes precedence
396
+ 4. Extract server entries into a uniform shape
397
+
398
+ ### Checking
399
+
400
+ Run per-file checks (schema, security, commands, deprecated, env, urls, redundancy) independently per config file. Run cross-file checks (consistency) after all files are parsed.
401
+
402
+ ### Reporting
403
+
404
+ Rules use the `category/rule-id` naming convention (e.g., `mcp-security/hardcoded-bearer`). This maps cleanly to SARIF rule IDs for GitHub Code Scanning integration.
405
+
406
+ ### Fixing
407
+
408
+ Rules marked as auto-fixable should apply surgical string replacements to the JSON file without reformatting the user's style (indentation, trailing commas, key ordering). Validate that the result is still valid JSON after applying fixes.
409
+
410
+ ---
411
+
412
+ ## 5. Contributing
413
+
414
+ This specification is maintained at [github.com/YawLabs/ctxlint](https://github.com/YawLabs/ctxlint).
415
+
416
+ To propose changes:
417
+
418
+ - **New rules:** Open an issue describing the rule, its severity, trigger condition, and which clients it applies to.
419
+ - **Client additions:** As new MCP clients emerge, submit a PR adding their config file location, root key, and any client-specific behaviors to Section 1.
420
+ - **Corrections:** If any client behavior documented here is inaccurate, open an issue with evidence (link to client docs, source code, or reproduction steps).
421
+
422
+ ### Versioning
423
+
424
+ This specification follows semver:
425
+
426
+ - **Patch** (1.0.x): Typo fixes, clarifications, no rule changes
427
+ - **Minor** (1.x.0): New rules added, new clients documented
428
+ - **Major** (x.0.0): Rules removed or semantics changed in breaking ways
429
+
430
+ ### Related specifications and tools
431
+
432
+ - [Model Context Protocol Specification](https://spec.modelcontextprotocol.io/) — the underlying protocol this config format serves
433
+ - [ctxlint](https://github.com/YawLabs/ctxlint) — reference implementation of this specification
434
+ - [mcp-compliance](https://github.com/YawLabs/mcp-compliance) — tests MCP server _behavior_ against the protocol spec (complementary to config linting)