mcp-medic 1.0.0 → 1.0.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.
package/README.md CHANGED
@@ -1,27 +1,43 @@
1
- # mcp-doctor
1
+ # mcp-medic
2
+
3
+ [![CI](https://github.com/shivam039/mcp-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/shivam039/mcp-doctor/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/mcp-medic.svg)](https://www.npmjs.com/package/mcp-medic)
5
+ [![license](https://img.shields.io/npm/l/mcp-medic.svg)](./LICENSE)
2
6
 
3
7
  Diagnose broken MCP (Model Context Protocol) server configs before they break your agent silently.
4
8
 
5
- `mcp-doctor` validates MCP server configurations, executes full protocol initialization handshakes across stdio/SSE/HTTP transports, checks all exposed tool JSON schemas against standard specifications, and simulates sample calls — providing actionable suggestions, in-editor VS Code diagnostics, fleet management, and CI-ready exit codes.
9
+ `mcp-medic` validates MCP server configurations, executes full protocol initialization handshakes across stdio/SSE/HTTP transports, checks all exposed tool JSON schemas against standard specifications, and simulates sample calls — providing actionable suggestions and CI-ready exit codes.
10
+
11
+ ## Why this exists
12
+
13
+ A broken MCP server config usually doesn't fail loudly — it fails as your agent silently missing a tool, retrying a handshake forever, or getting a malformed schema it can't reason about. Those bugs are miserable to track down after the fact. `mcp-medic` catches them at the config level, before an agent ever touches the server: it actually connects (real `initialize` handshake, real `tools/list`), so "the config parses" and "the server actually works" are checked together, in CI, with a real exit code.
14
+
15
+ ## ⚠️ How this works — please read before pointing it at a config
16
+
17
+ `mcp-medic` validates a config by **actually connecting to the servers in it**:
18
+ - `stdio` transport → it **spawns the configured `command`** as a real child process on your machine.
19
+ - `sse`/`http` transport → it **makes real network requests** to the configured `url`, including any `headers` you've set (e.g. auth tokens).
20
+
21
+ This is the whole point (a real handshake, not a schema guess) — but it means you should only run it against configs you trust, the same way you'd only `npm install` a `package.json` you trust. See [SECURITY.md](./SECURITY.md) for the full threat model.
6
22
 
7
23
  ---
8
24
 
9
25
  ## Features
10
26
 
11
- - 🔍 **Auto-Discovery**: Run `mcp-doctor check` with no arguments to auto-discover Claude Desktop, `.mcp.json`, and VS Code/Cursor MCP configuration paths across macOS, Windows, and Linux.
27
+ - 🔍 **Auto-Discovery**: Run `mcp-medic check` with no arguments to auto-discover Claude Desktop, `.mcp.json`, and VS Code/Cursor MCP configuration paths across macOS, Windows, and Linux.
12
28
  - 💡 **Auto-Fix Suggestions**: Diagnose issues with clear, actionable fix suggestions using `--show-fixes`.
13
- - 🌐 **Registry Validation**: Validate published registry entries directly using `mcp-doctor check --registry <server-id>`.
14
- - 🏢 **Fleet Validation**: Scan and validate monorepos or multi-team configurations with `mcp-doctor check-all "<glob>"`.
15
- - ⚖️ **Drift Detection**: Catch environment divergence between staging and production configs with `mcp-doctor diff <configA> <configB>`.
16
- - 📜 **Policy-as-Code**: Enforce organizational constraints (e.g., banned transports, domain allowlists, minimum description lengths) via `.mcp-doctor-policy.json` / `--policy`.
29
+ - 🌐 **Registry Validation**: Validate published registry entries directly using `mcp-medic check --registry <server-id>`.
30
+ - 🧪 **Fleet Validation** (experimental): Scan and validate monorepos or multi-team configurations with `mcp-medic check-all "<glob>"`.
31
+ - 🧪 **Drift Detection** (experimental): Catch environment divergence between staging and production configs with `mcp-medic diff <configA> <configB>`.
32
+ - 📜 **Policy-as-Code**: Enforce organizational constraints (e.g., banned transports, domain allowlists, minimum description lengths) via `.mcp-medic-policy.json` / `--policy`.
17
33
  - 📸 **Snapshot Baseline Mode**: Filter out legacy diagnostics with `--snapshot <baseline.json>` to gate only on newly introduced regressions.
18
34
  - 📊 **CI Reporting**: Export standard JUnit XML (`--export-junit <file.xml>`) and JSON (`--export-json <file.json>`) for seamless CI dashboard visualization.
19
- - 👀 **Watch Mode**: Re-run validation on save using `mcp-doctor watch <path>`.
35
+ - 👀 **Watch Mode**: Re-run validation on save using `mcp-medic watch <path>`.
20
36
  - ⚡ **Transport Hardening**: Full handshake validation across stdio, HTTP (with OAuth token refresh), and SSE (with automatic retry resilience).
21
- - 💻 **VS Code Extension**: In-editor squiggles and hover tooltips showing diagnostics and suggested fixes.
37
+ - 🧪 **VS Code Extension** (experimental, not yet on the Marketplace): in-editor squiggles and hover tooltips runnable from source today, see [vscode-extension/](./vscode-extension/).
22
38
  - 🚦 **CI Usability & Exit Codes**: Strict exit code taxonomy (`0` clean, `1` diagnostic failures, `2` usage/syntax errors) and `--fail-on <error|warning>`.
23
- - 🤖 **GitHub Action**: Drop-in CI integration via `shivam039/mcp-doctor@main` (or `mcp-doctor-action`).
24
- - 🧩 **Community Checks**: Conformance test helper (`runCheckConformanceSuite`) to build custom `mcp-doctor-check-*` check plugins.
39
+ - 🤖 **GitHub Action**: Drop-in CI integration via `shivam039/mcp-doctor@main` (or `mcp-medic-action`).
40
+ - 🧩 **Community Checks** (framework ready, no packages published yet): a conformance test helper (`runCheckConformanceSuite`) so anyone can build and publish their own `mcp-medic-check-*` plugin.
25
41
 
26
42
  ---
27
43
 
@@ -29,29 +45,29 @@ Diagnose broken MCP (Model Context Protocol) server configs before they break yo
29
45
 
30
46
  ```bash
31
47
  # Run against auto-discovered configs in current project / Claude Desktop
32
- npx mcp-doctor
48
+ npx mcp-medic
33
49
 
34
50
  # Run check on a specific configuration file
35
- npx mcp-doctor check path/to/config.json
51
+ npx mcp-medic check path/to/config.json
36
52
 
37
53
  # Validate all configs across a monorepo
38
- npx mcp-doctor check-all "configs/**/*.json"
54
+ npx mcp-medic check-all "configs/**/*.json"
39
55
 
40
56
  # Compare two configs to detect drift
41
- npx mcp-doctor diff staging.mcp.json prod.mcp.json
57
+ npx mcp-medic diff staging.mcp.json prod.mcp.json
42
58
 
43
59
  # Validate a published registry server directly without a local config
44
- npx mcp-doctor check --registry @modelcontextprotocol/server-memory
45
- npx mcp-doctor check --registry smithery:username/my-server
60
+ npx mcp-medic check --registry @modelcontextprotocol/server-memory
61
+ npx mcp-medic check --registry smithery:username/my-server
46
62
 
47
63
  # Apply organizational policy rules and export to JUnit XML
48
- npx mcp-doctor check path/to/config.json --policy .mcp-doctor-policy.json --export-junit results.xml
64
+ npx mcp-medic check path/to/config.json --policy .mcp-medic-policy.json --export-junit results.xml
49
65
 
50
66
  # Display suggested fixes for flagged diagnostics
51
- npx mcp-doctor check path/to/config.json --show-fixes
67
+ npx mcp-medic check path/to/config.json --show-fixes
52
68
 
53
69
  # Watch mode (re-runs checks on save)
54
- npx mcp-doctor watch path/to/config.json
70
+ npx mcp-medic watch path/to/config.json
55
71
  ```
56
72
 
57
73
  ---
@@ -65,8 +81,9 @@ npx mcp-doctor watch path/to/config.json
65
81
  | `diff <configA> <configB>` | Detect drift between two configuration files |
66
82
  | `check --registry <id>` | Validate a published registry server directly |
67
83
  | `watch <path>` | Watch configuration file and re-run checks on file save |
84
+ | `fix <path>` | Interactively apply mechanical suggested fixes (see [Auto-Fix](#auto-fix-mcp-medic-fix) below) |
68
85
  | `--config <path>` | Explicit configuration path |
69
- | `--policy <path>` | Apply organizational policy rules (`.mcp-doctor-policy.json`) |
86
+ | `--policy <path>` | Apply organizational policy rules (`.mcp-medic-policy.json`) |
70
87
  | `--snapshot <path>` | Compare against baseline snapshot, reporting regressions only |
71
88
  | `--update-snapshot <path>` | Save diagnostic report as new baseline snapshot |
72
89
  | `--export-junit <file>` | Export report in JUnit XML format |
@@ -76,6 +93,8 @@ npx mcp-doctor watch path/to/config.json
76
93
  | `--verbose`, `-v` | Output raw JSON-RPC traffic and debug messages |
77
94
  | `--json` | Output full diagnostic report in JSON |
78
95
  | `--timeout <ms>` | Per-server handshake timeout in milliseconds (default: `5000`) |
96
+ | `--help`, `-h` | Show usage help |
97
+ | `--version`, `-V` | Print the installed version |
79
98
 
80
99
  ### Exit Codes
81
100
 
@@ -85,7 +104,27 @@ npx mcp-doctor watch path/to/config.json
85
104
 
86
105
  ---
87
106
 
88
- ## Policy-as-Code (`.mcp-doctor-policy.json`)
107
+ ## Auto-Fix (`mcp-medic fix`)
108
+
109
+ For the small subset of diagnostics that carry a mechanical fix (today: upgrading a `security.untrusted-remote` server's `http://` URL to `https://`), `mcp-medic fix` will show you a diff and ask for confirmation before touching your config file:
110
+
111
+ ```bash
112
+ mcp-medic fix path/to/config.json
113
+
114
+ # Preview every available fix without prompting or writing anything
115
+ mcp-medic fix path/to/config.json --dry-run
116
+
117
+ # Only offer fixes from one specific check
118
+ mcp-medic fix path/to/config.json --check security.untrusted-remote
119
+ ```
120
+
121
+ - Fixes are **never bulk-applied** — each one is shown as a diff and requires an explicit `y`/`N`.
122
+ - A `.bak` copy of the original file is written before any change, unconditionally.
123
+ - Most diagnostics (schema issues, the two other security checks) don't have a mechanical fix — `fix` reports "No auto-fixable diagnostics found" for those rather than guessing.
124
+
125
+ ---
126
+
127
+ ## Policy-as-Code (`.mcp-medic-policy.json`)
89
128
 
90
129
  Define organization-wide policies that compose with built-in checks:
91
130
 
@@ -99,11 +138,9 @@ Define organization-wide policies that compose with built-in checks:
99
138
 
100
139
  ---
101
140
 
102
- ## VS Code Extension
141
+ ## VS Code Extension (experimental)
103
142
 
104
- The `mcp-doctor` VS Code extension provides:
105
- - Inline squiggles on `.mcp.json`, `mcp.json`, and `claude_desktop_config.json` files as you edit.
106
- - Hover tooltips showing the full diagnostic explanation and actionable suggested fix.
143
+ **Not published on the VS Code Marketplace yet.** The diagnostics logic (inline squiggles + hover tooltips on `.mcp.json`, `mcp.json`, and `claude_desktop_config.json` files) is real and working, but it's currently only runnable from source as an Extension Development Host, or packaged locally as a `.vsix`. See [vscode-extension/README.md](./vscode-extension/README.md) for setup — it takes about five minutes.
107
144
 
108
145
  ---
109
146
 
@@ -133,25 +170,47 @@ jobs:
133
170
 
134
171
  | Check ID | Package | Scope | Description |
135
172
  |---|---|---|---|
136
- | `schema.malformed` | `mcp-doctor` | **Official** | Verifies inputSchema is a valid JSON schema object |
137
- | `schema.missing-required` | `mcp-doctor` | **Official** | Flags required fields missing from properties |
138
- | `schema.type-mismatch` | `mcp-doctor` | **Official** | Flags invalid JSON schema types and enum mismatches |
139
- | `schema.missing-description` | `mcp-doctor` | **Official** | Flags tools and properties missing documentation |
140
- | `schema.sample-call-simulation` | `mcp-doctor` | **Official** | Simulates and validates synthetic call payloads |
141
- | `policy.*` | `mcp-doctor` | **Official** | Evaluates policy-as-code rules (transports, domains, length) |
142
- | `community.strict-typing` | `mcp-doctor-check-strict-typing` | *Community* | Enforces strict property type annotations |
143
- | `community.no-empty-enums` | `mcp-doctor-check-no-empty-enums` | *Community* | Ensures non-empty enum option lists |
173
+ | `schema.malformed` | `mcp-medic` | **Official** | Verifies inputSchema is a valid JSON schema object |
174
+ | `schema.missing-required` | `mcp-medic` | **Official** | Flags required fields missing from properties |
175
+ | `schema.type-mismatch` | `mcp-medic` | **Official** | Flags invalid JSON schema types and enum mismatches |
176
+ | `schema.missing-description` | `mcp-medic` | **Official** | Flags tools and properties missing documentation |
177
+ | `schema.sample-call-simulation` | `mcp-medic` | **Official** | Simulates and validates synthetic call payloads |
178
+ | `security.untrusted-remote` | `mcp-medic` | **Official** (heuristic) | Flags non-HTTPS or raw-IP SSE/HTTP server URLs |
179
+ | `security.overbroad-permissions` | `mcp-medic` | **Official** (heuristic) | Flags tools with unscoped shell/filesystem/network parameters |
180
+ | `security.prompt-injection-risk` | `mcp-medic` | **Official** (heuristic) | Flags instruction-like language in tool descriptions aimed at the model |
181
+ | `policy.*` | `mcp-medic` | **Official** | Evaluates policy-as-code rules (transports, domains, length) |
182
+ | `community.strict-typing` | `mcp-medic-check-strict-typing` | *Planned / example* | Would enforce strict property type annotations |
183
+ | `community.no-empty-enums` | `mcp-medic-check-no-empty-enums` | *Planned / example* | Would ensure non-empty enum option lists |
184
+
185
+ The two `community.*` rows above are examples of what a check plugin could look like — **those packages aren't published yet**. `runCheckConformanceSuite()` (used by the official checks' own tests) is the tool for validating a plugin conforms to the `Check` interface; see [Authoring Custom Checks](./docs/AUTHORING_CHECKS.md) if you want to build and publish one.
186
+
187
+ The `security.*` checks are heuristic — they pattern-match on what a server *declares* (URLs, tool descriptions, schemas), not what it actually does at runtime. Every diagnostic they produce says so explicitly; they're a signal to investigate, not proof of a problem.
188
+
189
+ ---
144
190
 
145
- To write and publish your own check plugin, see [Authoring Custom Checks](./docs/AUTHORING_CHECKS.md).
191
+ ## Known Limitations
192
+
193
+ - **Handshake timeout defaults to 5000ms** per server (`--timeout <ms>` to change it). A slow-starting stdio server or a server behind a slow network path can fail with `status: 'timeout'` even though it would eventually respond.
194
+ - **`schema.sample-call-simulation`** builds synthetic payloads from a tool's declared JSON Schema and checks the schema is internally consistent (e.g. catches an empty `enum`, or conflicting `minimum`/`maximum`) — it does **not** actually invoke the tool, and it does not validate business logic, side effects, or whether the tool's real output matches its declared schema.
195
+ - **`security.*` checks are heuristic pattern-matching**, not a security audit — see the note above. They can both miss real issues and flag benign configs (e.g. a legitimate local dev server on plain `http://`).
196
+ - **Fleet commands (`check-all`, `diff`) are newer and less battle-tested** than `check`/`watch` — the core check pipeline they're built on is the same, but edge cases in glob matching or drift diffing are more likely.
197
+ - **The VS Code extension and community check packages are not shipped/published** — see the sections above.
198
+ - **First run via `npx`** pays a one-time cost to resolve and download the package; once installed (or on a warm npx cache), `--help`/`--version` return in well under 100ms.
146
199
 
147
200
  ---
148
201
 
202
+ ## Contributing & Support
203
+
204
+ - 🐛 [Report a bug](https://github.com/shivam039/mcp-doctor/issues/new?template=bug_report.md) / 💡 [Request a feature](https://github.com/shivam039/mcp-doctor/issues/new?template=feature_request.md)
205
+ - 📋 See [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a PR
206
+ - 📝 [CHANGELOG](./CHANGELOG.md)
207
+
149
208
  ## Governance, Stability & Security
150
209
 
151
210
  - 🏛️ [Governance & Project Sustainability](./GOVERNANCE.md)
152
- - 📜 [1.0 Stability & Deprecation Policy](./docs/STABILITY_POLICY.md)
211
+ - 📜 [Stability & Deprecation Policy](./docs/STABILITY_POLICY.md) — v1.0's initial stable surface
153
212
  - 🔒 [Security Policy & Threat Model](./SECURITY.md)
154
- - 💡 [RFC Process](./docs/RFC_PROCESS.md)
213
+ - 💡 [RFC Process](./docs/RFC_PROCESS.md) (for changes to the frozen 1.0 surface)
155
214
 
156
215
  ---
157
216
 
@@ -164,6 +223,9 @@ npm run typecheck
164
223
  npm run test
165
224
  ```
166
225
 
226
+ The VS Code extension (`vscode-extension/`) is a separate, independently-installed package — see [vscode-extension/README.md](./vscode-extension/README.md).
227
+
167
228
  ## License
168
229
 
169
230
  MIT
231
+
package/action.yml CHANGED
@@ -1,6 +1,6 @@
1
- name: 'mcp-doctor-action'
1
+ name: 'mcp-medic-action'
2
2
  description: 'Diagnose broken MCP (Model Context Protocol) server configs in CI before they break your agent silently'
3
- author: 'mcp-doctor'
3
+ author: 'Shivam Dixit'
4
4
  branding:
5
5
  icon: 'check-circle'
6
6
  color: 'green'
@@ -35,7 +35,7 @@ runs:
35
35
  with:
36
36
  node-version: '20'
37
37
 
38
- - name: Run mcp-doctor
38
+ - name: Run mcp-medic
39
39
  shell: bash
40
40
  run: |
41
41
  ARGS="check"
@@ -54,4 +54,4 @@ runs:
54
54
  if [ "${{ inputs.verbose }}" = "true" ]; then
55
55
  ARGS="$ARGS --verbose"
56
56
  fi
57
- npx --yes mcp-doctor $ARGS
57
+ npx --yes mcp-medic $ARGS
@@ -3,4 +3,7 @@ export { missingRequiredFieldsCheck } from './missing-required-fields.js';
3
3
  export { typeMismatchCheck } from './type-mismatch.js';
4
4
  export { missingDescriptionCheck } from './missing-description.js';
5
5
  export { sampleCallSimulationCheck } from './sample-call-simulation.js';
6
+ export { securityUntrustedRemoteCheck } from './security-untrusted-remote.js';
7
+ export { securityOverbroadPermissionsCheck } from './security-overbroad-permissions.js';
8
+ export { securityPromptInjectionRiskCheck } from './security-prompt-injection-risk.js';
6
9
  export declare const allChecks: import("../types.js").Check[];
@@ -3,15 +3,24 @@ export { missingRequiredFieldsCheck } from './missing-required-fields.js';
3
3
  export { typeMismatchCheck } from './type-mismatch.js';
4
4
  export { missingDescriptionCheck } from './missing-description.js';
5
5
  export { sampleCallSimulationCheck } from './sample-call-simulation.js';
6
+ export { securityUntrustedRemoteCheck } from './security-untrusted-remote.js';
7
+ export { securityOverbroadPermissionsCheck } from './security-overbroad-permissions.js';
8
+ export { securityPromptInjectionRiskCheck } from './security-prompt-injection-risk.js';
6
9
  import { malformedSchemaCheck } from './malformed-schema.js';
7
10
  import { missingRequiredFieldsCheck } from './missing-required-fields.js';
8
11
  import { typeMismatchCheck } from './type-mismatch.js';
9
12
  import { missingDescriptionCheck } from './missing-description.js';
10
13
  import { sampleCallSimulationCheck } from './sample-call-simulation.js';
14
+ import { securityUntrustedRemoteCheck } from './security-untrusted-remote.js';
15
+ import { securityOverbroadPermissionsCheck } from './security-overbroad-permissions.js';
16
+ import { securityPromptInjectionRiskCheck } from './security-prompt-injection-risk.js';
11
17
  export const allChecks = [
12
18
  malformedSchemaCheck,
13
19
  missingRequiredFieldsCheck,
14
20
  typeMismatchCheck,
15
21
  missingDescriptionCheck,
16
22
  sampleCallSimulationCheck,
23
+ securityUntrustedRemoteCheck,
24
+ securityOverbroadPermissionsCheck,
25
+ securityPromptInjectionRiskCheck,
17
26
  ];
@@ -0,0 +1,2 @@
1
+ import type { Check } from '../types.js';
2
+ export declare const securityOverbroadPermissionsCheck: Check;
@@ -0,0 +1,117 @@
1
+ import { HEURISTIC_DISCLAIMER } from './security-shared.js';
2
+ // Ordered by specificity; matched against each top-level inputSchema property name.
3
+ const PROPERTY_RULES = [
4
+ {
5
+ category: 'shell-command',
6
+ namePattern: /^(cmd|command|shell|bash|sh|exec|script|code)$/i,
7
+ hint: 'accepts an unconstrained shell/command string',
8
+ },
9
+ {
10
+ category: 'filesystem-path',
11
+ namePattern: /^(path|filepath|file_path|dir|directory|folder)$/i,
12
+ hint: 'accepts an unconstrained filesystem path',
13
+ },
14
+ {
15
+ category: 'network-request',
16
+ namePattern: /^(url|endpoint|host|target|uri)$/i,
17
+ hint: 'accepts an unconstrained network destination',
18
+ },
19
+ ];
20
+ const DESCRIPTION_RULES = [
21
+ {
22
+ category: 'shell-command',
23
+ pattern: /\b(execute|executes|run|runs)\s+(any|arbitrary)\s+(shell|command|code)\b|\barbitrary\s+shell\s+command/i,
24
+ hint: 'description states it executes arbitrary shell commands or code',
25
+ },
26
+ {
27
+ category: 'filesystem-path',
28
+ pattern: /\bfull\s+filesystem\s+access\b|\baccess(?:es)?\s+(?:to\s+)?any\s+file\b|\b(?:read|write|delete)\s+any\s+file\b/i,
29
+ hint: 'description states it can access any file on the filesystem',
30
+ },
31
+ {
32
+ category: 'network-request',
33
+ pattern: /\b(unrestricted|arbitrary)\s+(network|http|url)\s+(access|request)\b/i,
34
+ hint: 'description states it allows unrestricted network access',
35
+ },
36
+ ];
37
+ function isUnconstrainedString(propDef) {
38
+ if (!propDef || typeof propDef !== 'object' || Array.isArray(propDef))
39
+ return false;
40
+ const prop = propDef;
41
+ if (prop.type !== 'string')
42
+ return false;
43
+ if (Array.isArray(prop.enum) && prop.enum.length > 0)
44
+ return false;
45
+ if (typeof prop.pattern === 'string' && prop.pattern.length > 0)
46
+ return false;
47
+ return true;
48
+ }
49
+ function checkTool(tool, serverName) {
50
+ const results = [];
51
+ if (typeof tool.description === 'string') {
52
+ for (const rule of DESCRIPTION_RULES) {
53
+ if (rule.pattern.test(tool.description)) {
54
+ results.push({
55
+ checkId: 'security.overbroad-permissions',
56
+ severity: 'warning',
57
+ message: `Tool "${tool.name}" ${rule.hint} — review before granting it broad access. (${HEURISTIC_DISCLAIMER})`,
58
+ serverName,
59
+ toolName: tool.name,
60
+ details: { category: rule.category, source: 'description' },
61
+ suggestedFix: {
62
+ description: 'Confirm this scope is intentional after reviewing the server\'s source; prefer a server that exposes narrower, purpose-built tools instead of one broad tool.',
63
+ },
64
+ });
65
+ }
66
+ }
67
+ }
68
+ const schema = tool.inputSchema;
69
+ if (schema && typeof schema === 'object' && !Array.isArray(schema)) {
70
+ const schemaObj = schema;
71
+ const properties = schemaObj.properties && typeof schemaObj.properties === 'object' && !Array.isArray(schemaObj.properties)
72
+ ? schemaObj.properties
73
+ : {};
74
+ for (const [propName, propDef] of Object.entries(properties)) {
75
+ if (!isUnconstrainedString(propDef))
76
+ continue;
77
+ const rule = PROPERTY_RULES.find((r) => r.namePattern.test(propName));
78
+ if (!rule)
79
+ continue;
80
+ results.push({
81
+ checkId: 'security.overbroad-permissions',
82
+ severity: 'warning',
83
+ message: `Tool "${tool.name}" parameter "${propName}" ${rule.hint} (no enum or pattern constraining it). (${HEURISTIC_DISCLAIMER})`,
84
+ serverName,
85
+ toolName: tool.name,
86
+ details: { category: rule.category, source: 'schema', property: propName },
87
+ suggestedFix: {
88
+ description: `Constrain "${propName}" with an enum of allowed values or a validating pattern, or split it into narrower parameters, if you control this server.`,
89
+ },
90
+ });
91
+ }
92
+ }
93
+ return results;
94
+ }
95
+ export const securityOverbroadPermissionsCheck = {
96
+ id: 'security.overbroad-permissions',
97
+ description: `Flags tools whose description or schema implies unscoped filesystem/shell/network access (${HEURISTIC_DISCLAIMER}).`,
98
+ run(connection) {
99
+ const results = [];
100
+ try {
101
+ if (!connection.tools || !Array.isArray(connection.tools))
102
+ return results;
103
+ for (const tool of connection.tools) {
104
+ results.push(...checkTool(tool, connection.server.name));
105
+ }
106
+ }
107
+ catch (err) {
108
+ results.push({
109
+ checkId: 'security.overbroad-permissions',
110
+ severity: 'error',
111
+ message: `check failed internally: ${err instanceof Error ? err.message : String(err)}`,
112
+ serverName: connection.server.name,
113
+ });
114
+ }
115
+ return results;
116
+ },
117
+ };
@@ -0,0 +1,2 @@
1
+ import type { Check } from '../types.js';
2
+ export declare const securityPromptInjectionRiskCheck: Check;
@@ -0,0 +1,94 @@
1
+ import { HEURISTIC_DISCLAIMER } from './security-shared.js';
2
+ // Instruction-like language aimed at the model reading the tool description,
3
+ // rather than at a human deciding whether to use the tool — a known MCP
4
+ // supply-chain risk pattern (tool descriptions are injected into the
5
+ // model's context).
6
+ const INJECTION_PATTERNS = [
7
+ /\balways\s+call\s+this\s+(tool\s+)?first\b/i,
8
+ /\bignore\s+(all\s+)?(previous|prior|above)\s+instructions\b/i,
9
+ /\bdisregard\s+(the\s+)?(system\s+prompt|previous\s+instructions)\b/i,
10
+ /\bdo\s+not\s+(tell|inform)\s+the\s+user\b/i,
11
+ /\bnever\s+(mention|tell)\s+.*\bthe\s+user\b/i,
12
+ /\byou\s+must\s+(always|never)\s+\w/i,
13
+ /\bthis\s+is\s+a\s+system\s+(prompt|instruction)\b/i,
14
+ /\boverride\s+(your|any)\s+(previous|prior)\s+(instructions|guidelines)\b/i,
15
+ /\bact\s+as\s+(if\s+you\s+are|though\s+you\s+are)\b/i,
16
+ ];
17
+ function findMatch(text) {
18
+ for (const pattern of INJECTION_PATTERNS) {
19
+ const match = pattern.exec(text);
20
+ if (match) {
21
+ return { pattern: pattern.source, snippet: match[0] };
22
+ }
23
+ }
24
+ return undefined;
25
+ }
26
+ export const securityPromptInjectionRiskCheck = {
27
+ id: 'security.prompt-injection-risk',
28
+ description: `Flags tool/parameter descriptions containing instruction-like language aimed at the model rather than a human (${HEURISTIC_DISCLAIMER}).`,
29
+ run(connection) {
30
+ const results = [];
31
+ try {
32
+ if (!connection.tools || !Array.isArray(connection.tools))
33
+ return results;
34
+ for (const tool of connection.tools) {
35
+ if (typeof tool.description === 'string') {
36
+ const match = findMatch(tool.description);
37
+ if (match) {
38
+ results.push({
39
+ checkId: 'security.prompt-injection-risk',
40
+ severity: 'warning',
41
+ message: `Tool "${tool.name}" description contains instruction-like language aimed at the model ` +
42
+ `("${match.snippet}") rather than describing the tool to a human — a known MCP supply-chain ` +
43
+ `risk pattern. (${HEURISTIC_DISCLAIMER})`,
44
+ serverName: connection.server.name,
45
+ toolName: tool.name,
46
+ details: { source: 'description', matchedPattern: match.pattern, snippet: match.snippet },
47
+ suggestedFix: {
48
+ description: "Review this tool's description and the server's source before trusting it; rewrite or remove instruction-like language if you control this server.",
49
+ },
50
+ });
51
+ }
52
+ }
53
+ const schema = tool.inputSchema;
54
+ if (schema && typeof schema === 'object' && !Array.isArray(schema)) {
55
+ const properties = schema.properties;
56
+ if (properties && typeof properties === 'object' && !Array.isArray(properties)) {
57
+ for (const [propName, propDef] of Object.entries(properties)) {
58
+ if (!propDef || typeof propDef !== 'object' || Array.isArray(propDef))
59
+ continue;
60
+ const description = propDef.description;
61
+ if (typeof description !== 'string')
62
+ continue;
63
+ const match = findMatch(description);
64
+ if (match) {
65
+ results.push({
66
+ checkId: 'security.prompt-injection-risk',
67
+ severity: 'warning',
68
+ message: `Tool "${tool.name}" parameter "${propName}" description contains instruction-like language ` +
69
+ `aimed at the model ("${match.snippet}") rather than describing the parameter to a human. ` +
70
+ `(${HEURISTIC_DISCLAIMER})`,
71
+ serverName: connection.server.name,
72
+ toolName: tool.name,
73
+ details: { source: 'parameter-description', property: propName, matchedPattern: match.pattern, snippet: match.snippet },
74
+ suggestedFix: {
75
+ description: "Review this tool's description and the server's source before trusting it; rewrite or remove instruction-like language if you control this server.",
76
+ },
77
+ });
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ catch (err) {
85
+ results.push({
86
+ checkId: 'security.prompt-injection-risk',
87
+ severity: 'error',
88
+ message: `check failed internally: ${err instanceof Error ? err.message : String(err)}`,
89
+ serverName: connection.server.name,
90
+ });
91
+ }
92
+ return results;
93
+ },
94
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * All security.* checks are heuristic pattern-matching over what a server
3
+ * *declares* (config URL, tool descriptions, tool schemas) — they cannot
4
+ * see what a server actually does. Every diagnostic they emit must say so
5
+ * (FR3-2.4): this is not a substitute for reading a third-party MCP
6
+ * server's source before trusting it.
7
+ */
8
+ export declare const HEURISTIC_DISCLAIMER = "heuristic flag, not a guarantee \u2014 review this server's source before trusting it";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * All security.* checks are heuristic pattern-matching over what a server
3
+ * *declares* (config URL, tool descriptions, tool schemas) — they cannot
4
+ * see what a server actually does. Every diagnostic they emit must say so
5
+ * (FR3-2.4): this is not a substitute for reading a third-party MCP
6
+ * server's source before trusting it.
7
+ */
8
+ export const HEURISTIC_DISCLAIMER = "heuristic flag, not a guarantee — review this server's source before trusting it";
@@ -0,0 +1,2 @@
1
+ import type { Check } from '../types.js';
2
+ export declare const securityUntrustedRemoteCheck: Check;
@@ -0,0 +1,71 @@
1
+ import { HEURISTIC_DISCLAIMER } from './security-shared.js';
2
+ function toHttps(url) {
3
+ return url.replace(/^http:\/\//i, 'https://');
4
+ }
5
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1']);
6
+ function isIpLiteralHost(hostname) {
7
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname))
8
+ return true; // IPv4
9
+ if (hostname.includes(':') && /^[0-9a-fA-F:]+$/.test(hostname))
10
+ return true; // IPv6 (URL strips brackets)
11
+ return false;
12
+ }
13
+ export const securityUntrustedRemoteCheck = {
14
+ id: 'security.untrusted-remote',
15
+ description: `For SSE/HTTP servers, flags non-HTTPS URLs or raw IP-address hosts instead of domain names (${HEURISTIC_DISCLAIMER}).`,
16
+ run(connection) {
17
+ const results = [];
18
+ try {
19
+ const { server } = connection;
20
+ if (server.transport !== 'sse' && server.transport !== 'http')
21
+ return results;
22
+ if (typeof server.url !== 'string' || server.url.length === 0)
23
+ return results;
24
+ let parsed;
25
+ try {
26
+ parsed = new URL(server.url);
27
+ }
28
+ catch {
29
+ return results; // malformed URLs are the config-loader/protocol layer's concern, not this check's
30
+ }
31
+ if (parsed.protocol === 'http:') {
32
+ results.push({
33
+ checkId: 'security.untrusted-remote',
34
+ severity: 'warning',
35
+ message: `Server "${server.name}" connects over unencrypted "http://"; requests, responses, and any headers ` +
36
+ `(including auth tokens) travel in plaintext. Not blocking — legitimate local dev configs exist. ` +
37
+ `(${HEURISTIC_DISCLAIMER})`,
38
+ serverName: server.name,
39
+ details: { url: server.url, reason: 'non-https' },
40
+ suggestedFix: {
41
+ description: `Switch to "${toHttps(server.url)}" if the server supports TLS.`,
42
+ patch: { serverName: server.name, set: { url: toHttps(server.url) } },
43
+ },
44
+ });
45
+ }
46
+ if (isIpLiteralHost(parsed.hostname) && !LOOPBACK_HOSTS.has(parsed.hostname)) {
47
+ results.push({
48
+ checkId: 'security.untrusted-remote',
49
+ severity: 'warning',
50
+ message: `Server "${server.name}" points at a raw IP address ("${parsed.hostname}") instead of a domain name, ` +
51
+ `making it hard to verify who actually operates the endpoint or notice if it changes hands. ` +
52
+ `Not blocking — legitimate local dev configs exist. (${HEURISTIC_DISCLAIMER})`,
53
+ serverName: server.name,
54
+ details: { url: server.url, reason: 'ip-literal-host' },
55
+ suggestedFix: {
56
+ description: 'Point this server at a stable domain name you control or trust, if one is available.',
57
+ },
58
+ });
59
+ }
60
+ }
61
+ catch (err) {
62
+ results.push({
63
+ checkId: 'security.untrusted-remote',
64
+ severity: 'error',
65
+ message: `check failed internally: ${err instanceof Error ? err.message : String(err)}`,
66
+ serverName: connection.server.name,
67
+ });
68
+ }
69
+ return results;
70
+ },
71
+ };
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  export interface ParsedArgs {
3
- command: 'check' | 'watch' | 'check-all' | 'diff' | 'help';
3
+ command: 'check' | 'watch' | 'check-all' | 'diff' | 'fix' | 'help' | 'version';
4
4
  configPath?: string;
5
5
  configPathB?: string;
6
6
  globPattern?: string;
@@ -15,6 +15,13 @@ export interface ParsedArgs {
15
15
  showFixes: boolean;
16
16
  verbose: boolean;
17
17
  failOn: 'error' | 'warning';
18
+ checkFilter?: string;
19
+ dryRun: boolean;
18
20
  }
21
+ /** Prompts the user with `question` and resolves true for an explicit "y"/"yes" answer. */
22
+ export type ConfirmFn = (question: string) => Promise<boolean>;
19
23
  export declare function parseArgs(argv: string[]): ParsedArgs;
20
- export declare function main(argv?: string[]): Promise<number>;
24
+ export interface CliDeps {
25
+ confirm?: ConfirmFn;
26
+ }
27
+ export declare function main(argv?: string[], deps?: CliDeps): Promise<number>;