mcp-recon 0.2.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.
Files changed (95) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +271 -0
  3. package/dist/bin/recon.d.ts +18 -0
  4. package/dist/bin/recon.d.ts.map +1 -0
  5. package/dist/bin/recon.js +361 -0
  6. package/dist/bin/recon.js.map +1 -0
  7. package/dist/caveats/index.d.ts +46 -0
  8. package/dist/caveats/index.d.ts.map +1 -0
  9. package/dist/caveats/index.js +186 -0
  10. package/dist/caveats/index.js.map +1 -0
  11. package/dist/caveats/render.d.ts +25 -0
  12. package/dist/caveats/render.d.ts.map +1 -0
  13. package/dist/caveats/render.js +100 -0
  14. package/dist/caveats/render.js.map +1 -0
  15. package/dist/caveats/types.d.ts +94 -0
  16. package/dist/caveats/types.d.ts.map +1 -0
  17. package/dist/caveats/types.js +17 -0
  18. package/dist/caveats/types.js.map +1 -0
  19. package/dist/classify/caveat.d.ts +29 -0
  20. package/dist/classify/caveat.d.ts.map +1 -0
  21. package/dist/classify/caveat.js +103 -0
  22. package/dist/classify/caveat.js.map +1 -0
  23. package/dist/classify/index.d.ts +21 -0
  24. package/dist/classify/index.d.ts.map +1 -0
  25. package/dist/classify/index.js +186 -0
  26. package/dist/classify/index.js.map +1 -0
  27. package/dist/classify/rules.d.ts +62 -0
  28. package/dist/classify/rules.d.ts.map +1 -0
  29. package/dist/classify/rules.js +219 -0
  30. package/dist/classify/rules.js.map +1 -0
  31. package/dist/classify/types.d.ts +45 -0
  32. package/dist/classify/types.d.ts.map +1 -0
  33. package/dist/classify/types.js +9 -0
  34. package/dist/classify/types.js.map +1 -0
  35. package/dist/enumerate.d.ts +79 -0
  36. package/dist/enumerate.d.ts.map +1 -0
  37. package/dist/enumerate.js +62 -0
  38. package/dist/enumerate.js.map +1 -0
  39. package/dist/fuzz/axes/boundary.d.ts +17 -0
  40. package/dist/fuzz/axes/boundary.d.ts.map +1 -0
  41. package/dist/fuzz/axes/boundary.js +143 -0
  42. package/dist/fuzz/axes/boundary.js.map +1 -0
  43. package/dist/fuzz/axes/encoding.d.ts +17 -0
  44. package/dist/fuzz/axes/encoding.d.ts.map +1 -0
  45. package/dist/fuzz/axes/encoding.js +59 -0
  46. package/dist/fuzz/axes/encoding.js.map +1 -0
  47. package/dist/fuzz/axes/path-traversal.d.ts +17 -0
  48. package/dist/fuzz/axes/path-traversal.d.ts.map +1 -0
  49. package/dist/fuzz/axes/path-traversal.js +56 -0
  50. package/dist/fuzz/axes/path-traversal.js.map +1 -0
  51. package/dist/fuzz/axes/schema-violation.d.ts +18 -0
  52. package/dist/fuzz/axes/schema-violation.d.ts.map +1 -0
  53. package/dist/fuzz/axes/schema-violation.js +74 -0
  54. package/dist/fuzz/axes/schema-violation.js.map +1 -0
  55. package/dist/fuzz/axes/type-confusion.d.ts +17 -0
  56. package/dist/fuzz/axes/type-confusion.d.ts.map +1 -0
  57. package/dist/fuzz/axes/type-confusion.js +67 -0
  58. package/dist/fuzz/axes/type-confusion.js.map +1 -0
  59. package/dist/fuzz/axes/url-hostility.d.ts +17 -0
  60. package/dist/fuzz/axes/url-hostility.d.ts.map +1 -0
  61. package/dist/fuzz/axes/url-hostility.js +61 -0
  62. package/dist/fuzz/axes/url-hostility.js.map +1 -0
  63. package/dist/fuzz/index.d.ts +41 -0
  64. package/dist/fuzz/index.d.ts.map +1 -0
  65. package/dist/fuzz/index.js +147 -0
  66. package/dist/fuzz/index.js.map +1 -0
  67. package/dist/fuzz/prng.d.ts +26 -0
  68. package/dist/fuzz/prng.d.ts.map +1 -0
  69. package/dist/fuzz/prng.js +52 -0
  70. package/dist/fuzz/prng.js.map +1 -0
  71. package/dist/fuzz/schema.d.ts +46 -0
  72. package/dist/fuzz/schema.d.ts.map +1 -0
  73. package/dist/fuzz/schema.js +84 -0
  74. package/dist/fuzz/schema.js.map +1 -0
  75. package/dist/fuzz/types.d.ts +53 -0
  76. package/dist/fuzz/types.d.ts.map +1 -0
  77. package/dist/fuzz/types.js +11 -0
  78. package/dist/fuzz/types.js.map +1 -0
  79. package/dist/index.d.ts +25 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +25 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/report/index.d.ts +25 -0
  84. package/dist/report/index.d.ts.map +1 -0
  85. package/dist/report/index.js +133 -0
  86. package/dist/report/index.js.map +1 -0
  87. package/dist/scan/index.d.ts +52 -0
  88. package/dist/scan/index.d.ts.map +1 -0
  89. package/dist/scan/index.js +81 -0
  90. package/dist/scan/index.js.map +1 -0
  91. package/dist/transport.d.ts +43 -0
  92. package/dist/transport.d.ts.map +1 -0
  93. package/dist/transport.js +74 -0
  94. package/dist/transport.js.map +1 -0
  95. package/package.json +72 -0
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+
17
+ Copyright 2026 Euan McRosson
18
+
19
+ Full license text: https://www.apache.org/licenses/LICENSE-2.0.txt
package/README.md ADDED
@@ -0,0 +1,271 @@
1
+ # mcp-recon
2
+
3
+ [![CI](https://github.com/euanmcrosson-dotcom/mcp-recon/actions/workflows/ci.yml/badge.svg)](https://github.com/euanmcrosson-dotcom/mcp-recon/actions/workflows/ci.yml)
4
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
5
+ [![Status: pre-alpha](https://img.shields.io/badge/status-pre--alpha-orange.svg)](docs/SPEC.md)
6
+ [![Tests: 68 passing](https://img.shields.io/badge/tests-68_passing-success.svg)](#tests)
7
+ [![Companion: capnagent](https://img.shields.io/badge/companion-capnagent-9cf.svg)](https://github.com/euanmcrosson-dotcom/capnagent)
8
+
9
+ > **Reverse-engineer any MCP server's tool surface in 30 seconds.**
10
+ > Connects to an MCP server (stdio or HTTP), enumerates its tools,
11
+ > runs a schema-aware adversarial fuzzer, classifies the authority
12
+ > each tool exposes against OWASP LLM Top 10 and MITRE ATLAS, and
13
+ > emits a structured threat profile — JSON for machines, Markdown
14
+ > for humans.
15
+
16
+ The thesis: every team adopting MCP right now is asking *"what
17
+ does this server actually do?"* and there's no tooling for it. The
18
+ agentic ecosystem grew faster than its security tooling. mcp-recon
19
+ is the recon side of that gap. [capnagent](https://github.com/euanmcrosson-dotcom/capnagent)
20
+ is the defensive side: take a recon report, derive a tight
21
+ capability caveat, deny everything outside it.
22
+
23
+ > **Status:** v0.1.2 shipped 2026-04-30. Public dataset of every
24
+ > stdio TypeScript MCP server in Anthropic's `@modelcontextprotocol/*`
25
+ > namespace audited. See [`docs/WRITEUP.md`](docs/WRITEUP.md) for the
26
+ > headline findings (DoS surface on `everything`,
27
+ > missing-bounds finding on `filesystem` example wrapper, full
28
+ > server-maturity ranking).
29
+
30
+ ## Contents
31
+
32
+ - [At a glance](#at-a-glance)
33
+ - [What you get](#what-you-get)
34
+ - [Command cheatsheet](#command-cheatsheet)
35
+ - [Sample output](#sample-output)
36
+ - [Recon → capnagent in one pipe](#recon--capnagent-in-one-pipe)
37
+ - [Why this exists](#why-this-exists)
38
+ - [Installation](#installation)
39
+ - [How it compares](#how-it-compares)
40
+ - [What this is NOT](#what-this-is-not)
41
+ - [Tests](#tests)
42
+ - [Companion project — capnagent](#companion-project--capnagent)
43
+ - [License](#license)
44
+
45
+ ## At a glance
46
+
47
+ | Coverage | Surface | Performance |
48
+ |---|---|---|
49
+ | **4 / 4** Anthropic reference servers scanned | **5** commands · **4** schema-tagged artefacts | scan budget=200 in <60s on 14-tool server |
50
+ | **37 tools classified** across the public dataset | enumerate · fuzz · classify · report · scan | deterministic (seeded PRNG, default `0xC0FFEE`) |
51
+ | **1374 fuzz calls** across the dataset (1 confirmed DoS finding) | rules-based, not LLM-mediated | <256MB memory on 100-tool server |
52
+
53
+ Maps tools to **OWASP LLM01 / LLM06 / LLM08** and **MITRE ATLAS** categories. Every output ships with a copy-pasteable [capnagent](https://github.com/euanmcrosson-dotcom/capnagent) caveat per tool. Reproducibility contract in
54
+ [capnagent's `docs/EVALUATION.md`](https://github.com/euanmcrosson-dotcom/capnagent/blob/master/docs/EVALUATION.md).
55
+
56
+ ## What you get
57
+
58
+ Run `mcp-recon scan` against any MCP server (stdio or HTTP) and get a
59
+ folder of evidence: a tool inventory, a fuzz transcript, a
60
+ classification, and a Markdown threat profile that a security reviewer
61
+ or developer-on-call can actually read. The JSON files are the
62
+ machine-parseable evidence the writeup links to. Run against any of
63
+ the 4 servers in the public dataset and your output matches
64
+ `examples/public-servers/server-<name>/` byte-for-byte.
65
+
66
+ ## Command cheatsheet
67
+
68
+ ```bash
69
+ mcp-recon enumerate <server-spec> # → inventory.json
70
+ mcp-recon fuzz <server-spec> [--budget=N] [--seed=N] # → fuzz.json
71
+ mcp-recon classify <inventory.json> [--fuzz=<fuzz.json>] # → classification.json
72
+ mcp-recon report <inventory.json> <classification.json> [--fuzz=<fuzz.json>] # → report.md
73
+ mcp-recon scan <server-spec> --out=<dir> [--budget=N] [--seed=N] # → 4 artefacts
74
+ ```
75
+
76
+ Server-spec forms: `stdio:<command> [args...]` (spawn process, talk
77
+ over stdio) or `http://host:port` (HTTP transport).
78
+
79
+ ## Sample output
80
+
81
+ ```bash
82
+ $ mcp-recon scan "stdio:npx -y @modelcontextprotocol/server-filesystem /tmp" \
83
+ --out=./reports/filesystem --budget=200
84
+
85
+ mcp-recon: 14 tools, 4 confused-deputy candidates
86
+ mcp-recon: fuzz — ok=4 protocol_error=719 runtime_error=0
87
+ mcp-recon: wrote 4 artefacts to ./reports/filesystem/
88
+
89
+ $ ls ./reports/filesystem/
90
+ inventory.json fuzz.json classification.json report.md
91
+ ```
92
+
93
+ A snippet from the resulting `classification.json` — every tool gets
94
+ a class, an authority level, a confused-deputy verdict, and a
95
+ copy-pasteable capnagent caveat:
96
+
97
+ ```json
98
+ {
99
+ "tool": "edit_file",
100
+ "data_class": "filesystem",
101
+ "authority_level": "write",
102
+ "confused_deputy_candidate": true,
103
+ "confidence": 0.91,
104
+ "rationale": "name match \"\\b(write[_-]?file|edit[_-]?file|create[_-]?directory|move[_-]?file)\\b\" → filesystem/write (0.70); description match → filesystem/read (0.50); schema: arg \"path\" is path-shaped → filesystem (0.40); user-controllable string arg + non-read authority → confused-deputy candidate",
105
+ "recommended_caveat": "tool == \"edit_file\" AND caller == \"<your-caller-id>\" AND arg.path starts_with \"<your-sandbox-prefix>/\" AND now <= @<your-cap-expiry> // WRITE filesystem"
106
+ }
107
+ ```
108
+
109
+ The full headline findings — including the `everything` server's DoS
110
+ surface and the `filesystem` wrapper's missing-bounds — are in
111
+ [`docs/WRITEUP.md`](docs/WRITEUP.md).
112
+
113
+ ## Recon → capnagent in one pipe
114
+
115
+ ```
116
+ ┌──────────────┐ inventory.json ┌──────────────┐
117
+ │ │ fuzz.json │ │
118
+ │ MCP server │ ──▶ classification ──▶│ capnagent │ ──▶ deny anything
119
+ │ │ .json │ issuer │ outside scope
120
+ │ │ report.md │ │
121
+ └──────────────┘ └──────────────┘
122
+ ▲ │
123
+ │ ▼
124
+ └────────── scoped caller ◀────── signed capability
125
+ ```
126
+
127
+ mcp-recon documents the tool surface; capnagent enforces the bound.
128
+ Each project stands alone. Together they're a single security
129
+ posture for any MCP-shaped agent. Run mcp-recon first, paste the
130
+ suggested caveats into your capnagent issuer, ship.
131
+
132
+ ### From recon to a capnagent issuer in one pipe
133
+
134
+ `classification.json` ships a copy-pasteable caveat per tool, but
135
+ manual paste is its own foot-gun. The `caveats` command produces a
136
+ machine-readable issuance plan ready to feed straight into a capnagent
137
+ issuer:
138
+
139
+ ```bash
140
+ $ mcp-recon caveats ./reports/filesystem/classification.json \
141
+ --caller=agent:planner \
142
+ --sandbox-prefix=/var/agent-sandbox/tenant-42 \
143
+ --expiry=2026-12-31T23:59:59Z \
144
+ > ./reports/filesystem/caveats.json
145
+
146
+ mcp-recon: 14 plans (14 ready, 0 flagged) — schema=mcp-recon/v0.1/caveats
147
+ ```
148
+
149
+ The output document (schema `mcp-recon/v0.1/caveats`) is one entry per
150
+ tool, with `caveats: string[]` already split into individual capnagent
151
+ DSL predicates and operator bindings substituted. Plans get flagged
152
+ with a structured reason set (`classification_unknown`, `low_confidence`,
153
+ `cdc_without_arg_constraint`, `unsubstituted_placeholder`) so the
154
+ review surface is machine-checkable.
155
+
156
+ Run with no bindings to get a "review pass" — every plan is flagged,
157
+ but you can see exactly which placeholders need binding before
158
+ committing values. Per-tool overrides (`per_tool_overrides` in the
159
+ library API) let you tighten confused-deputy candidates the
160
+ classifier didn't constrain.
161
+
162
+ ## Why this exists
163
+
164
+ **For the developer adopting MCP.** Before you wire a third-party
165
+ MCP server into your agent, run mcp-recon against it. You get an
166
+ honest threat profile in 30 seconds — what does this thing
167
+ *actually* let an agent do, and what's the smallest cap that
168
+ preserves utility?
169
+
170
+ **For the security team auditing an agent stack.** mcp-recon turns
171
+ "we depend on N MCP servers" into "here's the consolidated tool
172
+ surface, here's what each one is classified as, here's where the
173
+ confused-deputy candidates are." A printable artifact you can
174
+ review.
175
+
176
+ **For the AI-security researcher.** mcp-recon's reports are the
177
+ input to round-N writeups in the
178
+ [capnagent purple-team corpus](https://github.com/euanmcrosson-dotcom/capnagent/tree/master/docs/purple-team).
179
+ Recon → capability gap → attack PoC → fix → CLOSED.
180
+
181
+ ## Installation
182
+
183
+ ```bash
184
+ # From source (the recommended path today; npm package is post-v0.2)
185
+ git clone https://github.com/euanmcrosson-dotcom/mcp-recon
186
+ cd mcp-recon
187
+ npm install
188
+ npm run -w @mcp-recon/cli build
189
+
190
+ # Run the CLI directly via tsx (no build step needed for development)
191
+ npx tsx packages/mcp-recon-cli/src/bin/recon.ts scan \
192
+ "stdio:npx -y @modelcontextprotocol/server-filesystem $HOME/sandbox" \
193
+ --out=./reports/filesystem --budget=200
194
+ ```
195
+
196
+ > **Windows / Git Bash users:** prefix path-shaped flags with `MSYS_NO_PATHCONV=1` to prevent leading-slash path mangling. Example: `MSYS_NO_PATHCONV=1 mcp-recon caveats classification.json --sandbox-prefix=/var/sandbox --expiry=2026-12-31T23:59:59Z`
197
+
198
+ ## Documentation
199
+
200
+ - [`docs/SPEC.md`](docs/SPEC.md) — v0.1 surface, server-spec syntax, output schemas
201
+ - [`docs/METHODOLOGY.md`](docs/METHODOLOGY.md) — classifier rules, fuzz axes, signals, falsifiability
202
+ - [`docs/WRITEUP.md`](docs/WRITEUP.md) — public-dataset findings + headline observations
203
+ - [`schemas/`](schemas/) — formal JSON Schema files for the four wire formats
204
+ - [`findings/`](findings/) — corpus of documented findings (F001–F006)
205
+ - [`SECURITY.md`](SECURITY.md) — vulnerability reporting policy
206
+ - [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to add classifier rules, fuzz axes, dataset entries
207
+
208
+ ## How it compares
209
+
210
+ | | mcp-recon | [NVIDIA garak](https://github.com/NVIDIA/garak) | Burp / ZAP | manual review |
211
+ |---|---|---|---|---|
212
+ | **Scope** | MCP server tool surfaces | model-behavior testing | HTTP fuzzing | everything |
213
+ | **Output** | structured JSON + Markdown | reports | proxy logs | human prose |
214
+ | **Determinism** | yes (seeded PRNG) | partial | no | no |
215
+ | **LLM in the loop** | no (rules-based) | yes | no | yes |
216
+ | **OWASP LLM / MITRE ATLAS mapping** | yes (per-tool) | partial | no | author-dependent |
217
+ | **Companion enforcement** | [capnagent](https://github.com/euanmcrosson-dotcom/capnagent) | none | none | none |
218
+
219
+ mcp-recon is **not** a replacement for any of those — it's the
220
+ piece nobody else is building: a deterministic, schema-aware
221
+ characterization of an MCP server's tool surface, in a format
222
+ that wires straight into a capability-bounded enforcement layer.
223
+
224
+ ## What this is NOT
225
+
226
+ - **Not a replacement for capnagent.** mcp-recon documents what's
227
+ there; capnagent enforces what's allowed. You want both.
228
+ - **Not a vulnerability scanner for the model itself.** Use
229
+ [NVIDIA garak](https://github.com/NVIDIA/garak) for that. We
230
+ test the *tool surface*, not model behavior.
231
+ - **Not an exploitation framework.** We send adversarial schemas
232
+ to characterize handling, not actual exploits.
233
+ - **Not a proxy / MITM tool.** Out of scope. See
234
+ [`docs/SPEC.md`](docs/SPEC.md) §"What v0.1 does NOT do."
235
+
236
+ ## Tests
237
+
238
+ The workspace has **68 unit + property-based tests** passing today
239
+ (`npm test`), covering schema parsing, the seeded PRNG, fuzz
240
+ generators along all six adversarial axes, the classification
241
+ rules, the Markdown report renderer, and end-to-end `scan` flow.
242
+ Two additional integration test files (`enumerate.integration.test.ts`,
243
+ `fuzz.integration.test.ts`) exercise live transport against a
244
+ locally-spawned MCP server when the dev environment provides one.
245
+
246
+ ```bash
247
+ npm test # all packages, vitest
248
+ npm run typecheck # tsc --noEmit, strict mode
249
+ npm run lint # biome check
250
+ ```
251
+
252
+ ## Companion project — capnagent
253
+
254
+ mcp-recon is the offensive complement to
255
+ [capnagent](https://github.com/euanmcrosson-dotcom/capnagent),
256
+ which provides capability-bounded authorization for AI agent tool
257
+ calls. Together they implement the standard
258
+ *recon-then-bound* security workflow:
259
+
260
+ ```
261
+ [ mcp-recon ] → threat profile → [ capnagent ]
262
+ "what is "what should "deny anything
263
+ here?" we allow?" outside that"
264
+ ```
265
+
266
+ Each project stands alone. Together they're a single security
267
+ posture for any MCP-shaped agent.
268
+
269
+ ## License
270
+
271
+ [Apache-2.0](LICENSE).
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `mcp-recon` CLI entry point.
4
+ *
5
+ * v0.1 surface (see docs/SPEC.md §"What v0.1 does"):
6
+ *
7
+ * mcp-recon enumerate <server-spec> [implemented in scaffold]
8
+ * mcp-recon fuzz <server-spec> [stub — v0.1 week 2]
9
+ * mcp-recon classify <inventory.json> [stub — v0.1 week 3]
10
+ * mcp-recon report <i.json> <f.json> <c.json> [stub — v0.1 week 3]
11
+ * mcp-recon scan <server-spec> [stub — v0.1 week 4]
12
+ *
13
+ * Output is JSON to stdout for machine-parseable commands; logs to
14
+ * stderr so a piped invocation (`mcp-recon enumerate ... | jq ...`)
15
+ * works without contamination.
16
+ */
17
+ export {};
18
+ //# sourceMappingURL=recon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recon.d.ts","sourceRoot":"","sources":["../../src/bin/recon.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;GAcG"}
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `mcp-recon` CLI entry point.
4
+ *
5
+ * v0.1 surface (see docs/SPEC.md §"What v0.1 does"):
6
+ *
7
+ * mcp-recon enumerate <server-spec> [implemented in scaffold]
8
+ * mcp-recon fuzz <server-spec> [stub — v0.1 week 2]
9
+ * mcp-recon classify <inventory.json> [stub — v0.1 week 3]
10
+ * mcp-recon report <i.json> <f.json> <c.json> [stub — v0.1 week 3]
11
+ * mcp-recon scan <server-spec> [stub — v0.1 week 4]
12
+ *
13
+ * Output is JSON to stdout for machine-parseable commands; logs to
14
+ * stderr so a piped invocation (`mcp-recon enumerate ... | jq ...`)
15
+ * works without contamination.
16
+ */
17
+ import * as fs from "node:fs";
18
+ import { CAVEATS_SCHEMA, CLASSIFICATION_SCHEMA, classify, closeClient, enumerate, fuzz, openClient, parseServerSpec, planCaveats, renderCaveatsMarkdown, renderMarkdown, scan, } from "../index.js";
19
+ function usage() {
20
+ process.stderr.write([
21
+ "mcp-recon — reverse-engineer MCP server tool surfaces",
22
+ "",
23
+ "Usage:",
24
+ " mcp-recon enumerate <server-spec>",
25
+ " mcp-recon fuzz <server-spec> [--budget=N] [--seed=N]",
26
+ " mcp-recon classify <inventory.json> [--fuzz=<fuzz.json>]",
27
+ " mcp-recon report <inventory.json> <classification.json> [--fuzz=<fuzz.json>]",
28
+ " mcp-recon caveats <classification.json> [--caller=ID] [--sandbox-prefix=PATH] [--expiry=ISO] [--markdown]",
29
+ " mcp-recon scan <server-spec> --out=<dir> [--budget=N] [--seed=N]",
30
+ " [--caller=ID] [--sandbox-prefix=PATH] [--expiry=ISO]",
31
+ "",
32
+ "Server-spec forms:",
33
+ " stdio:<command> [args...] — spawn process, talk over stdio",
34
+ " http://host:port — HTTP transport (v0.1 week 2)",
35
+ "",
36
+ "Examples:",
37
+ " mcp-recon enumerate stdio:npx -y @modelcontextprotocol/server-filesystem /tmp",
38
+ " mcp-recon fuzz stdio:npx -y @modelcontextprotocol/server-filesystem /tmp --budget=20",
39
+ " mcp-recon classify inv.json --fuzz=fz.json > class.json",
40
+ " mcp-recon report inv.json class.json --fuzz=fz.json > report.md",
41
+ " mcp-recon caveats class.json --caller=agent:planner --sandbox-prefix=/srv/sb --expiry=2026-12-31T23:59:59Z > caveats.json",
42
+ " mcp-recon caveats class.json --caller=agent:planner --sandbox-prefix=/srv/sb --expiry=2026-12-31T23:59:59Z --markdown > caveats.md",
43
+ "",
44
+ ].join("\n"));
45
+ process.exit(2);
46
+ }
47
+ async function main() {
48
+ const argv = process.argv.slice(2);
49
+ const cmd = argv[0];
50
+ if (!cmd || cmd === "--help" || cmd === "-h") {
51
+ usage();
52
+ }
53
+ switch (cmd) {
54
+ case "enumerate":
55
+ return await runEnumerate(argv.slice(1));
56
+ case "fuzz":
57
+ return await runFuzz(argv.slice(1));
58
+ case "classify":
59
+ return runClassify(argv.slice(1));
60
+ case "report":
61
+ return runReport(argv.slice(1));
62
+ case "caveats":
63
+ return runCaveats(argv.slice(1));
64
+ case "scan":
65
+ return await runScan(argv.slice(1));
66
+ default:
67
+ process.stderr.write(`mcp-recon: unknown command '${cmd}'.\n`);
68
+ usage();
69
+ }
70
+ }
71
+ async function runEnumerate(args) {
72
+ const spec = args[0];
73
+ if (!spec) {
74
+ process.stderr.write("mcp-recon enumerate: missing <server-spec>\n");
75
+ return 2;
76
+ }
77
+ const parsed = parseServerSpec(spec);
78
+ process.stderr.write(`mcp-recon: connecting to ${spec}...\n`);
79
+ const client = await openClient(parsed);
80
+ try {
81
+ const inventory = await enumerate(client);
82
+ process.stderr.write(`mcp-recon: enumerated ${inventory.tools.length} tools from ${inventory.server.name ?? "unknown server"}\n`);
83
+ process.stdout.write(`${JSON.stringify(inventory, null, 2)}\n`);
84
+ return 0;
85
+ }
86
+ finally {
87
+ await closeClient(client);
88
+ }
89
+ }
90
+ async function runFuzz(args) {
91
+ // Pull --budget=N and --seed=N flags out; the first positional is the spec.
92
+ let budget;
93
+ let seed;
94
+ let spec;
95
+ for (const arg of args) {
96
+ if (arg.startsWith("--budget=")) {
97
+ const n = Number.parseInt(arg.slice("--budget=".length), 10);
98
+ if (Number.isNaN(n) || n <= 0) {
99
+ process.stderr.write(`mcp-recon fuzz: invalid --budget value\n`);
100
+ return 2;
101
+ }
102
+ budget = n;
103
+ }
104
+ else if (arg.startsWith("--seed=")) {
105
+ const n = Number.parseInt(arg.slice("--seed=".length), 10);
106
+ if (Number.isNaN(n)) {
107
+ process.stderr.write(`mcp-recon fuzz: invalid --seed value\n`);
108
+ return 2;
109
+ }
110
+ seed = n;
111
+ }
112
+ else if (!spec) {
113
+ spec = arg;
114
+ }
115
+ else {
116
+ process.stderr.write(`mcp-recon fuzz: unexpected argument ${arg}\n`);
117
+ return 2;
118
+ }
119
+ }
120
+ if (!spec) {
121
+ process.stderr.write("mcp-recon fuzz: missing <server-spec>\n");
122
+ return 2;
123
+ }
124
+ const parsed = parseServerSpec(spec);
125
+ process.stderr.write(`mcp-recon: connecting to ${spec}...\n`);
126
+ const client = await openClient(parsed);
127
+ try {
128
+ const inventory = await enumerate(client);
129
+ process.stderr.write(`mcp-recon: fuzzing ${inventory.tools.length} tools (budget=${budget ?? 200}, seed=${seed ?? "default"})...\n`);
130
+ const opts = {};
131
+ if (budget !== undefined)
132
+ opts.budget = budget;
133
+ if (seed !== undefined)
134
+ opts.seed = seed;
135
+ const results = await fuzz(client, inventory, opts);
136
+ const totalCalls = results.calls.length;
137
+ const totalOk = results.summary.reduce((acc, s) => acc + s.ok, 0);
138
+ const totalProto = results.summary.reduce((acc, s) => acc + s.protocol_error, 0);
139
+ const totalRuntime = results.summary.reduce((acc, s) => acc + s.runtime_error, 0);
140
+ process.stderr.write(`mcp-recon: ${totalCalls} calls — ok=${totalOk} protocol_error=${totalProto} runtime_error=${totalRuntime}\n`);
141
+ process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
142
+ return 0;
143
+ }
144
+ finally {
145
+ await closeClient(client);
146
+ }
147
+ }
148
+ function runClassify(args) {
149
+ let inventoryPath;
150
+ let fuzzPath;
151
+ for (const arg of args) {
152
+ if (arg.startsWith("--fuzz=")) {
153
+ fuzzPath = arg.slice("--fuzz=".length);
154
+ }
155
+ else if (!inventoryPath) {
156
+ inventoryPath = arg;
157
+ }
158
+ else {
159
+ process.stderr.write(`mcp-recon classify: unexpected argument ${arg}\n`);
160
+ return 2;
161
+ }
162
+ }
163
+ if (!inventoryPath) {
164
+ process.stderr.write("mcp-recon classify: missing <inventory.json>\n");
165
+ return 2;
166
+ }
167
+ const inventory = readJson(inventoryPath);
168
+ const fuzzData = fuzzPath ? readJson(fuzzPath) : undefined;
169
+ const result = classify(inventory, fuzzData);
170
+ process.stderr.write(`mcp-recon: classified ${result.classifications.length} tools (fuzz_informed=${result.fuzz_informed})\n`);
171
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
172
+ return 0;
173
+ }
174
+ function runReport(args) {
175
+ let inventoryPath;
176
+ let classificationPath;
177
+ let fuzzPath;
178
+ for (const arg of args) {
179
+ if (arg.startsWith("--fuzz=")) {
180
+ fuzzPath = arg.slice("--fuzz=".length);
181
+ }
182
+ else if (!inventoryPath) {
183
+ inventoryPath = arg;
184
+ }
185
+ else if (!classificationPath) {
186
+ classificationPath = arg;
187
+ }
188
+ else {
189
+ process.stderr.write(`mcp-recon report: unexpected argument ${arg}\n`);
190
+ return 2;
191
+ }
192
+ }
193
+ if (!inventoryPath || !classificationPath) {
194
+ process.stderr.write("mcp-recon report: missing arguments — needs <inventory.json> <classification.json>\n");
195
+ return 2;
196
+ }
197
+ const inventory = readJson(inventoryPath);
198
+ const classification = readJson(classificationPath);
199
+ if (classification.schema !== CLASSIFICATION_SCHEMA) {
200
+ process.stderr.write(`mcp-recon report: classification.json schema is "${classification.schema}", expected "${CLASSIFICATION_SCHEMA}"\n`);
201
+ return 64;
202
+ }
203
+ const fuzzData = fuzzPath ? readJson(fuzzPath) : undefined;
204
+ const md = renderMarkdown(fuzzData ? { inventory, classification, fuzz: fuzzData } : { inventory, classification });
205
+ process.stdout.write(md);
206
+ if (!md.endsWith("\n"))
207
+ process.stdout.write("\n");
208
+ return 0;
209
+ }
210
+ function readJson(filePath) {
211
+ const text = fs.readFileSync(filePath, "utf8");
212
+ return JSON.parse(text);
213
+ }
214
+ function runCaveats(args) {
215
+ let classificationPath;
216
+ let markdown = false;
217
+ const bindings = {};
218
+ for (const arg of args) {
219
+ if (arg === "--markdown") {
220
+ markdown = true;
221
+ }
222
+ else if (arg.startsWith("--caller=")) {
223
+ bindings.caller = arg.slice("--caller=".length);
224
+ }
225
+ else if (arg.startsWith("--sandbox-prefix=")) {
226
+ bindings.sandbox_prefix = arg.slice("--sandbox-prefix=".length);
227
+ }
228
+ else if (arg.startsWith("--expiry=")) {
229
+ const expiry = arg.slice("--expiry=".length);
230
+ // Light validation: must parse as a date.
231
+ const parsed = new Date(expiry);
232
+ if (Number.isNaN(parsed.getTime())) {
233
+ process.stderr.write(`mcp-recon caveats: invalid --expiry value (must be ISO-8601)\n`);
234
+ return 2;
235
+ }
236
+ // Normalize to canonical ISO so downstream consumers see one shape.
237
+ bindings.expiry = parsed.toISOString();
238
+ }
239
+ else if (!classificationPath) {
240
+ classificationPath = arg;
241
+ }
242
+ else {
243
+ process.stderr.write(`mcp-recon caveats: unexpected argument ${arg}\n`);
244
+ return 2;
245
+ }
246
+ }
247
+ if (!classificationPath) {
248
+ process.stderr.write("mcp-recon caveats: missing <classification.json>\n");
249
+ return 2;
250
+ }
251
+ const classification = readJson(classificationPath);
252
+ if (classification.schema !== CLASSIFICATION_SCHEMA) {
253
+ process.stderr.write(`mcp-recon caveats: classification.json schema is "${classification.schema}", expected "${CLASSIFICATION_SCHEMA}"\n`);
254
+ return 64;
255
+ }
256
+ const result = planCaveats(classification, bindings);
257
+ process.stderr.write(`mcp-recon: ${result.summary.total} plans (${result.summary.ready} ready, ${result.summary.flagged} flagged) — schema=${CAVEATS_SCHEMA}\n`);
258
+ if (markdown) {
259
+ const md = renderCaveatsMarkdown(result);
260
+ process.stdout.write(md);
261
+ if (!md.endsWith("\n"))
262
+ process.stdout.write("\n");
263
+ }
264
+ else {
265
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
266
+ }
267
+ return 0;
268
+ }
269
+ async function runScan(args) {
270
+ let spec;
271
+ let outDir;
272
+ let budget;
273
+ let seed;
274
+ const bindings = {};
275
+ for (const arg of args) {
276
+ if (arg.startsWith("--out=")) {
277
+ outDir = arg.slice("--out=".length);
278
+ }
279
+ else if (arg.startsWith("--budget=")) {
280
+ const n = Number.parseInt(arg.slice("--budget=".length), 10);
281
+ if (Number.isNaN(n) || n <= 0) {
282
+ process.stderr.write("mcp-recon scan: invalid --budget value\n");
283
+ return 2;
284
+ }
285
+ budget = n;
286
+ }
287
+ else if (arg.startsWith("--seed=")) {
288
+ const n = Number.parseInt(arg.slice("--seed=".length), 10);
289
+ if (Number.isNaN(n)) {
290
+ process.stderr.write("mcp-recon scan: invalid --seed value\n");
291
+ return 2;
292
+ }
293
+ seed = n;
294
+ }
295
+ else if (arg.startsWith("--caller=")) {
296
+ bindings.caller = arg.slice("--caller=".length);
297
+ }
298
+ else if (arg.startsWith("--sandbox-prefix=")) {
299
+ bindings.sandbox_prefix = arg.slice("--sandbox-prefix=".length);
300
+ }
301
+ else if (arg.startsWith("--expiry=")) {
302
+ const expiry = arg.slice("--expiry=".length);
303
+ const parsed = new Date(expiry);
304
+ if (Number.isNaN(parsed.getTime())) {
305
+ process.stderr.write("mcp-recon scan: invalid --expiry value (must be ISO-8601)\n");
306
+ return 2;
307
+ }
308
+ bindings.expiry = parsed.toISOString();
309
+ }
310
+ else if (!spec) {
311
+ spec = arg;
312
+ }
313
+ else {
314
+ process.stderr.write(`mcp-recon scan: unexpected argument ${arg}\n`);
315
+ return 2;
316
+ }
317
+ }
318
+ if (!spec) {
319
+ process.stderr.write("mcp-recon scan: missing <server-spec>\n");
320
+ return 2;
321
+ }
322
+ if (!outDir) {
323
+ process.stderr.write("mcp-recon scan: --out=<dir> is required\n");
324
+ return 2;
325
+ }
326
+ const hasBindings = bindings.caller !== undefined ||
327
+ bindings.sandbox_prefix !== undefined ||
328
+ bindings.expiry !== undefined;
329
+ const parsed = parseServerSpec(spec);
330
+ process.stderr.write(`mcp-recon: connecting to ${spec}...\n`);
331
+ const client = await openClient(parsed);
332
+ try {
333
+ const opts = {
334
+ outDir,
335
+ ...(budget !== undefined ? { budget } : {}),
336
+ ...(seed !== undefined ? { seed } : {}),
337
+ ...(hasBindings ? { bindings } : {}),
338
+ };
339
+ process.stderr.write(`mcp-recon: running enumerate → fuzz → classify → report...\n`);
340
+ const result = await scan(client, opts);
341
+ const totalOk = result.fuzz.summary.reduce((acc, s) => acc + s.ok, 0);
342
+ const totalProto = result.fuzz.summary.reduce((acc, s) => acc + s.protocol_error, 0);
343
+ const totalRuntime = result.fuzz.summary.reduce((acc, s) => acc + s.runtime_error, 0);
344
+ const cdpCount = result.classification.classifications.filter((c) => c.confused_deputy_candidate).length;
345
+ process.stderr.write(`mcp-recon: ${result.inventory.tools.length} tools, ${cdpCount} confused-deputy candidates\n`);
346
+ process.stderr.write(`mcp-recon: fuzz — ok=${totalOk} protocol_error=${totalProto} runtime_error=${totalRuntime}\n`);
347
+ const artefactCount = result.caveats !== undefined ? 5 : 4;
348
+ process.stderr.write(`mcp-recon: wrote ${artefactCount} artefacts to ${outDir}/\n`);
349
+ return 0;
350
+ }
351
+ finally {
352
+ await closeClient(client);
353
+ }
354
+ }
355
+ main()
356
+ .then((code) => process.exit(code))
357
+ .catch((err) => {
358
+ process.stderr.write(`mcp-recon: ${err instanceof Error ? err.message : String(err)}\n`);
359
+ process.exit(1);
360
+ });
361
+ //# sourceMappingURL=recon.js.map