mcpprobe-scanner 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,201 @@
1
+ """stdio transport: spawn a local MCP server command and speak newline-delimited JSON-RPC.
2
+
3
+ Only use this on packages you run yourself. The scanner does not sandbox the
4
+ child process; see SANDBOX-01 in CHECKS.md for the planned sandboxed runner.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import os
12
+ import shlex
13
+ import time
14
+ from collections import deque
15
+ from collections.abc import Mapping, Sequence
16
+ from typing import Any
17
+
18
+ from mcpprobe.transport.base import RpcResponse, Transport, TransportError
19
+ from mcpprobe.transport.evidence import EvidenceLog
20
+ from mcpprobe.transport.ratelimit import RateLimiter
21
+
22
+ STDERR_TAIL_LINES = 200
23
+ #: Protect against a server that floods stdout with one enormous line.
24
+ MAX_LINE_BYTES = 10_000_000
25
+
26
+
27
+ class StdioTransport(Transport):
28
+ """Child-process MCP server driven over stdin/stdout."""
29
+
30
+ name = "stdio"
31
+
32
+ def __init__(
33
+ self,
34
+ command: str | Sequence[str],
35
+ *,
36
+ env: Mapping[str, str] | None = None,
37
+ cwd: str | None = None,
38
+ timeout: float = 15.0,
39
+ evidence: EvidenceLog | None = None,
40
+ limiter: RateLimiter | None = None,
41
+ inherit_env: bool = True,
42
+ ) -> None:
43
+ super().__init__(evidence=evidence, limiter=limiter)
44
+ self.argv: list[str] = list(shlex.split(command) if isinstance(command, str) else command)
45
+ if not self.argv:
46
+ raise TransportError("empty stdio command")
47
+ self.env = dict(env or {})
48
+ self.cwd = cwd
49
+ self.timeout = timeout
50
+ self.inherit_env = inherit_env
51
+ self.proc: asyncio.subprocess.Process | None = None
52
+ self.stderr_tail: deque[str] = deque(maxlen=STDERR_TAIL_LINES)
53
+ self._pending: dict[int | str, asyncio.Future[dict[str, Any]]] = {}
54
+ self._reader_task: asyncio.Task[None] | None = None
55
+ self._stderr_task: asyncio.Task[None] | None = None
56
+ self.unsolicited: list[dict[str, Any]] = []
57
+
58
+ @property
59
+ def display(self) -> str:
60
+ return " ".join(shlex.quote(a) for a in self.argv)
61
+
62
+ async def start(self) -> None:
63
+ if self.proc is not None:
64
+ return
65
+ env = dict(os.environ) if self.inherit_env else {}
66
+ env.update(self.env)
67
+ try:
68
+ self.proc = await asyncio.create_subprocess_exec(
69
+ *self.argv,
70
+ stdin=asyncio.subprocess.PIPE,
71
+ stdout=asyncio.subprocess.PIPE,
72
+ stderr=asyncio.subprocess.PIPE,
73
+ env=env,
74
+ cwd=self.cwd,
75
+ limit=MAX_LINE_BYTES,
76
+ )
77
+ except (OSError, ValueError) as exc:
78
+ raise TransportError(f"cannot start {self.display}: {exc}") from exc
79
+ self._reader_task = asyncio.create_task(self._read_stdout())
80
+ self._stderr_task = asyncio.create_task(self._read_stderr())
81
+
82
+ async def _read_stdout(self) -> None:
83
+ assert self.proc and self.proc.stdout
84
+ while True:
85
+ try:
86
+ line = await self.proc.stdout.readline()
87
+ except (ValueError, asyncio.LimitOverrunError):
88
+ self.stderr_tail.append("[mcpprobe] stdout line exceeded limit; dropped")
89
+ continue
90
+ if not line:
91
+ break
92
+ text = line.decode("utf-8", errors="replace").strip()
93
+ if not text:
94
+ continue
95
+ try:
96
+ msg = json.loads(text)
97
+ except ValueError:
98
+ self.stderr_tail.append(f"[non-JSON stdout] {text[:200]}")
99
+ continue
100
+ if isinstance(msg, dict) and "id" in msg and ("result" in msg or "error" in msg):
101
+ fut = self._pending.pop(msg["id"], None)
102
+ if fut and not fut.done():
103
+ fut.set_result(msg)
104
+ continue
105
+ self.unsolicited.append(msg if isinstance(msg, dict) else {"raw": msg})
106
+ for fut in self._pending.values():
107
+ if not fut.done():
108
+ fut.set_exception(TransportError("server closed stdout"))
109
+ self._pending.clear()
110
+
111
+ async def _read_stderr(self) -> None:
112
+ assert self.proc and self.proc.stderr
113
+ while True:
114
+ try:
115
+ line = await self.proc.stderr.readline()
116
+ except (ValueError, asyncio.LimitOverrunError):
117
+ continue
118
+ if not line:
119
+ break
120
+ self.stderr_tail.append(line.decode("utf-8", errors="replace").rstrip())
121
+
122
+ async def send_raw(
123
+ self, text: str, *, label: str, want_id: int | str | None, note: str | None = None
124
+ ) -> RpcResponse:
125
+ """Write one line to the child's stdin and (optionally) await a reply."""
126
+ await self.start()
127
+ assert self.proc and self.proc.stdin
128
+ await self.limiter.acquire()
129
+ fut: asyncio.Future[dict[str, Any]] | None = None
130
+ if want_id is not None:
131
+ fut = asyncio.get_running_loop().create_future()
132
+ self._pending[want_id] = fut
133
+ started = time.monotonic()
134
+ error_text: str | None = None
135
+ message: dict[str, Any] | None = None
136
+ try:
137
+ self.proc.stdin.write(text.encode("utf-8") + b"\n")
138
+ await self.proc.stdin.drain()
139
+ if fut is not None:
140
+ message = await asyncio.wait_for(fut, self.timeout)
141
+ except TimeoutError:
142
+ error_text = "timeout waiting for response"
143
+ self._pending.pop(want_id, None) # type: ignore[arg-type]
144
+ except (BrokenPipeError, ConnectionResetError, TransportError) as exc:
145
+ error_text = f"{type(exc).__name__}: {exc}"
146
+ elapsed_ms = (time.monotonic() - started) * 1000
147
+ body = json.dumps(message).encode("utf-8") if message is not None else b""
148
+ rec = self.evidence.record(
149
+ transport="stdio",
150
+ method=label,
151
+ url=self.display,
152
+ request_body=text,
153
+ response_body=body,
154
+ elapsed_ms=elapsed_ms,
155
+ note=note,
156
+ error=error_text,
157
+ )
158
+ return RpcResponse(
159
+ method=label,
160
+ status=None,
161
+ headers={},
162
+ body=body,
163
+ message=message,
164
+ evidence=rec,
165
+ elapsed_ms=elapsed_ms,
166
+ error_text=error_text,
167
+ stream_messages=[message] if message else [],
168
+ )
169
+
170
+ async def rpc(
171
+ self,
172
+ method: str,
173
+ params: dict[str, Any] | None = None,
174
+ *,
175
+ note: str | None = None,
176
+ protocol_version: str | None = None,
177
+ **kwargs: Any,
178
+ ) -> RpcResponse:
179
+ msg = self.build_message(method, params, protocol_version=protocol_version)
180
+ return await self.send_raw(json.dumps(msg), label=method, want_id=msg["id"], note=note)
181
+
182
+ async def notify(self, method: str, params: dict[str, Any] | None = None) -> None:
183
+ msg = self.build_message(method, params, notification=True)
184
+ await self.send_raw(json.dumps(msg), label=method, want_id=None, note="notification")
185
+
186
+ async def close(self) -> None:
187
+ if self.proc is None:
188
+ return
189
+ try:
190
+ if self.proc.stdin:
191
+ self.proc.stdin.close()
192
+ try:
193
+ await asyncio.wait_for(self.proc.wait(), 2.0)
194
+ except TimeoutError:
195
+ self.proc.kill()
196
+ await self.proc.wait()
197
+ finally:
198
+ for task in (self._reader_task, self._stderr_task):
199
+ if task:
200
+ task.cancel()
201
+ self.proc = None
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.5
2
+ Name: mcpprobe-scanner
3
+ Version: 0.1.0
4
+ Summary: Security scanner for Model Context Protocol (MCP) servers: auth, transport, tool-metadata and poisoning checks with JSON and SARIF output.
5
+ Project-URL: Homepage, https://mcpprobe.tech
6
+ Project-URL: Scanning policy, https://mcpprobe.tech/scanning-policy
7
+ Project-URL: Source, https://github.com/mcpprobe-dev/mcpprobe
8
+ Project-URL: Issues, https://github.com/mcpprobe-dev/mcpprobe/issues
9
+ Author-email: mcpprobe contributors <hello@mcpprobe.tech>
10
+ Maintainer-email: mcpprobe contributors <hello@mcpprobe.tech>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: mcp,model-context-protocol,owasp,sarif,scanner,security
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Information Technology
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Security
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: click>=8.1
26
+ Requires-Dist: dnspython>=2.6
27
+ Requires-Dist: httpx>=0.27
28
+ Requires-Dist: pydantic>=2.6
29
+ Requires-Dist: pyjwt>=2.8
30
+ Requires-Dist: rich>=13.7
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=1.2; extra == 'dev'
33
+ Requires-Dist: mypy>=1.10; extra == 'dev'
34
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
35
+ Requires-Dist: pytest>=8.0; extra == 'dev'
36
+ Requires-Dist: ruff>=0.4; extra == 'dev'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # mcpprobe
40
+
41
+ Security scanner for [Model Context Protocol](https://modelcontextprotocol.io) servers.
42
+ It checks authentication, OAuth metadata, transport conformance, tool-description
43
+ poisoning, rug pulls, secrets in metadata, verbose errors and basic resource limits,
44
+ maps every finding to the OWASP MCP, Agentic and LLM Top 10 lists, and writes JSON,
45
+ SARIF 2.1.0 and a readable terminal summary.
46
+
47
+ It runs locally, sends no telemetry, and never calls a tool in its default mode.
48
+
49
+ > **Only scan servers you own or are explicitly authorised to test.**
50
+ > See [Ownership rule](#ownership-rule).
51
+
52
+ ## 60-second quickstart
53
+
54
+ ```console
55
+ $ pipx run --spec mcpprobe-scanner mcpprobe https://your-server.example/mcp
56
+ ```
57
+
58
+ or install it:
59
+
60
+ ```console
61
+ $ pipx install mcpprobe-scanner # or: pip install mcpprobe-scanner; the command is `mcpprobe`
62
+ $ mcpprobe https://your-server.example/mcp --sarif mcpprobe.sarif --json mcpprobe.json
63
+ ```
64
+
65
+ A local stdio server you run yourself:
66
+
67
+ ```console
68
+ $ mcpprobe --stdio "npx -y @your-scope/your-mcp-server@1.2.3"
69
+ ```
70
+
71
+ Passing a bearer token you own (for checks behind your auth):
72
+
73
+ ```console
74
+ $ mcpprobe https://your-server.example/mcp --header "Authorization: Bearer $MCP_TEST_TOKEN"
75
+ ```
76
+
77
+ The summary lists each finding with severity, check ID, confidence, OWASP mapping,
78
+ an evidence excerpt and a plain-English fix, followed by a coverage table showing
79
+ which checks ran, which were skipped and why. `mcpprobe checks` lists every check;
80
+ [CHECKS.md](CHECKS.md) documents them in full.
81
+
82
+ ## What it checks
83
+
84
+ | Area | Checks | Examples |
85
+ |---|---|---|
86
+ | Authentication | AUTH-01..04 | endpoint answers without credentials; missing RFC 9728 protected-resource metadata; no PKCE S256; accepts a self-minted JWT with the wrong audience; token accepted in the query string |
87
+ | Transport | TRANS-01..05 | `Origin` not validated (DNS rebinding); bogus `MCP-Protocol-Version` accepted; weak or forgeable `Mcp-Session-Id`; deprecated HTTP+SSE; plain HTTP, weak TLS, missing headers |
88
+ | Tool metadata | META-01..05 | hidden instructions, cross-tool shadowing, credential paths, URLs, base64, zero-width/bidi/homoglyph Unicode; rug-pull diff against a stored SHA-256 baseline; annotations that contradict capability; schema hygiene; secrets in descriptions or stdio env |
89
+ | Robustness | ERR-01, DOS-01 | stack traces and versions in errors; one oversized argument, one 64-deep nesting probe and 20 paced `tools/list` calls, never more |
90
+ | Active (gated stubs) | INJ-01..05, SUPPLY-01/02, SANDBOX-01 | registered and reported as `not_implemented` in this release |
91
+ | Governance | GOV-01..05 | listed as attestation-only: not assessable by a scan, never marked passed |
92
+
93
+ Protocol revisions: the scanner tries `server/discover` (2026-07-28) first, then
94
+ `initialize` with 2025-11-25 and 2025-06-18, then the deprecated 2024-11-05 HTTP+SSE
95
+ transport, and records which one answered.
96
+
97
+ ## Passive vs active
98
+
99
+ **Passive (default)** sends only what a normal MCP client sends (negotiation, the
100
+ `*/list` methods, well-known metadata documents) plus a handful of benign malformed
101
+ requests (bad `Origin`, bogus version header, forged session ID, self-minted
102
+ wrong-audience JWT, truncated JSON, one oversized and one deeply nested request). It
103
+ never calls a tool.
104
+
105
+ **Active** (`--active`) is for checks that call tools with non-destructive canaries.
106
+ In 0.1.0 these are stubs, but the gate is real and enforced in code. Active mode is
107
+ refused unless you pass all of:
108
+
109
+ - `--authorised-by "<who approved this, e.g. name/role/ticket>"`
110
+ - `--scope "<what is in scope>"`
111
+ - `--verify dns-txt` (HTTP targets) or `--verify local` (stdio servers you run yourself)
112
+
113
+ For `dns-txt`, generate a token and publish it:
114
+
115
+ ```console
116
+ $ mcpprobe token your-server.example
117
+ Publish this DNS TXT record, then run with --active --verify dns-txt:
118
+ _mcpprobe.your-server.example. TXT "mcpprobe-verification=<token>"
119
+ $ mcpprobe https://your-server.example/mcp --active --verify dns-txt \
120
+ --authorised-by "platform security, CHG-1234" --scope "https://your-server.example/mcp" \
121
+ --json scan.json
122
+ ```
123
+
124
+ Every active run writes an authorisation record (`scan.authorisation.json` next to the
125
+ report): who authorised it, the scope, the verification proof, rate cap and excluded
126
+ tools. Use `--exclude-tool NAME` for tools that must never be called.
127
+
128
+ ## Ownership rule
129
+
130
+ Scan only MCP servers you own, operate, or have explicit written permission to test.
131
+ This applies to passive mode too: even benign requests are unauthorised access if
132
+ the system is not yours. mcpprobe identifies itself on every request and honours
133
+ opt-outs (below), but those are courtesy mechanisms, not permission.
134
+
135
+ ## Scanner identification, opt-out and rate limits
136
+
137
+ - User-Agent: `mcpprobe/<version> (+https://mcpprobe.tech/scanning-policy)`.
138
+ - Opt-out, checked before any MCP request and impossible to override:
139
+ - DNS TXT `mcpprobe-opt-out=all` at `_mcpprobe.<host>` (or a parent domain), checked
140
+ before any packet reaches the host;
141
+ - an HTTP `200` at `/.well-known/mcpprobe-opt-out`.
142
+ - Rate: 5 requests/second by default (`--rate`), hard ceiling 10.
143
+
144
+ ## Exit codes
145
+
146
+ | Code | Meaning |
147
+ |---|---|
148
+ | `0` | No finding at or above `--fail-on` (default `high`) |
149
+ | `1` | At least one finding at or above `--fail-on` |
150
+ | `2` | Error: target unreachable or not MCP, bad arguments, ownership verification failed, authorisation refused, or the target opted out |
151
+
152
+ ## SARIF and GitHub code scanning
153
+
154
+ As a GitHub Action (Docker container action, no Python setup needed):
155
+
156
+ ```yaml
157
+ name: MCP security
158
+ on: [push, pull_request]
159
+ permissions:
160
+ contents: read
161
+ security-events: write
162
+ jobs:
163
+ mcpprobe:
164
+ runs-on: ubuntu-latest
165
+ steps:
166
+ - uses: actions/checkout@v4
167
+ - uses: mcpprobe-dev/mcpprobe@v0.1.0
168
+ with:
169
+ target: https://staging.your-server.example/mcp
170
+ fail-on: high
171
+ sarif-output: mcpprobe.sarif
172
+ sarif-location: src/server.py # optional: anchor alerts to a repo file
173
+ - uses: github/codeql-action/upload-sarif@v3
174
+ if: always()
175
+ with:
176
+ sarif_file: mcpprobe.sarif
177
+ category: mcpprobe
178
+ ```
179
+
180
+ Or from any CI with the CLI:
181
+
182
+ ```console
183
+ $ pipx run --spec mcpprobe-scanner mcpprobe https://staging.your-server.example/mcp --sarif mcpprobe.sarif --fail-on high
184
+ ```
185
+
186
+ Results use `error` for critical/high, `warning` for medium and `note` for low/info,
187
+ with GitHub's `security-severity` on each rule and the OWASP IDs as tags. Because a
188
+ scanned endpoint is not a file, results point at the target URL unless you pass
189
+ `--sarif-location`.
190
+
191
+ ## Rug-pull detection
192
+
193
+ ```console
194
+ $ mcpprobe https://your-server.example/mcp --baseline .mcpprobe/baseline.json
195
+ ```
196
+
197
+ The first run stores canonical SHA-256 hashes of every tool, prompt, resource and the
198
+ server instructions. Later runs report anything added, removed or changed (a changed
199
+ description after approval is the classic rug pull) and update the file; use
200
+ `--no-write-baseline` to compare without updating.
201
+
202
+ ## Reports and evidence
203
+
204
+ - `--json FILE`: the full report, including every request/response pair (timestamp,
205
+ method, status, a header subset, bodies truncated to 4 KB). `Authorization` values
206
+ and recognised secret patterns are redacted.
207
+ - `--sarif FILE`: SARIF 2.1.0.
208
+ - `-v`: descriptions and scan notes in the terminal; `-q`: no terminal output.
209
+
210
+ Server-supplied text is shown with control characters made visible and is never
211
+ interpreted as terminal markup.
212
+
213
+ ## Python API and LLM-judge hook
214
+
215
+ ```python
216
+ import asyncio
217
+ from mcpprobe.checks import JudgeVerdict, ScanOptions
218
+ from mcpprobe.scanner import ScanConfig, run_scan
219
+
220
+
221
+ def judge(text: str, task: str) -> JudgeVerdict | None:
222
+ return None # plug in your own model here; mcpprobe makes no LLM calls
223
+
224
+
225
+ report, evidence = asyncio.run(
226
+ run_scan(ScanConfig(target="https://your-server.example/mcp", options=ScanOptions(judge=judge)))
227
+ )
228
+ print(report.counts())
229
+ ```
230
+
231
+ ## Development
232
+
233
+ ```console
234
+ $ pip install -e ".[dev]"
235
+ $ ruff check . && ruff format --check . && mypy && pytest
236
+ $ python -m tests.fake_server --preset vulnerable --port 8765 # deliberately vulnerable local server
237
+ $ mcpprobe http://127.0.0.1:8765/mcp
238
+ ```
239
+
240
+ Security issues: see [SECURITY.md](SECURITY.md). Licence: [MIT](LICENSE).
@@ -0,0 +1,39 @@
1
+ mcpprobe/__init__.py,sha256=DY-sbjlh9JnxQG_v6UkZGtOApvi5LUU2t4oPbtEK_tc,821
2
+ mcpprobe/action.py,sha256=6oag4JcDPvm-_H6Pp2OMY4M3WYeS4KSCwlsSLKtypYU,2366
3
+ mcpprobe/authz.py,sha256=PCql53OFLQsN7oZxm2UtoAqTmT7LWRRw5nmx_50ePik,10595
4
+ mcpprobe/catalogue.py,sha256=j0h-O-2FQq158P97bXf3akD0S3gzxt7P4AUJYMHbYss,4729
5
+ mcpprobe/cli.py,sha256=UyA7KpuvKVCGV5hj-St_vU8sV267vI6RH_Xlp2NCkqs,10731
6
+ mcpprobe/dnsutil.py,sha256=d9AWtU96SHE7QzphZk76L9r61LpI0MuxPYLHEijHK4Y,1958
7
+ mcpprobe/governance.py,sha256=khs0t3jjSbwYzFYAjywnOzy_9m_qXy4ctuaFJy8UkdI,3391
8
+ mcpprobe/models.py,sha256=U7dM-BRp0UZV6S0SErQH8UbdqYgWr917OV1kvlpvgqE,8662
9
+ mcpprobe/policy.py,sha256=VWb6R3Udetp3AFk-BJqulRJREquyvtvP1VS8QhUOMdo,4865
10
+ mcpprobe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ mcpprobe/redact.py,sha256=8VhTwssMnU2cKKL2dm2omLfjpxDWtX1GJTS8f_iWcTM,3775
12
+ mcpprobe/scanner.py,sha256=XSxnyCO9Hr8JCyi5yeUSjZe4jEHw8ImFGgYuXUee1QU,8015
13
+ mcpprobe/checks/__init__.py,sha256=dsf5exmjJlhyeonq4wnFm0gdsfzbzNYccIb8qbhhSqY,2855
14
+ mcpprobe/checks/_util.py,sha256=5_8aLzoU3yfqWz6HcaHweb_j9KxuO0WgRDBiox39GIQ,3125
15
+ mcpprobe/checks/active_stubs.py,sha256=zI-0sw2ncmg6B2tiia0laJOPcfzMFWfagY0G6wfPMfY,9129
16
+ mcpprobe/checks/auth.py,sha256=q6Gu3wRfY3Gl-MyGe5tRCM7cseaqYg-t1kNLyFuw6Ww,21592
17
+ mcpprobe/checks/base.py,sha256=MOTPdE4u8MVPGdzmXpn_rdjIsjaH65DmQeT1iQnwwHU,7884
18
+ mcpprobe/checks/baseline.py,sha256=w-hwkN-OIczFephuXS7XSgjdTqOKkbTOEkHT16XWpK4,9847
19
+ mcpprobe/checks/capability.py,sha256=ZS8bz5GOzFZ1JpzjpzAQ46pziixuwa97AkWOVdSY2oQ,5785
20
+ mcpprobe/checks/metadata.py,sha256=dxoECGSxav8J-uEU0SX6R6HLVktTp0l1NtzT1ZMUik0,16554
21
+ mcpprobe/checks/poisoning.py,sha256=NRA676paxs8IQujSOcPWWC9frn_7BxPdKV-OUjW1yKY,19054
22
+ mcpprobe/checks/robustness.py,sha256=IggjqWHt21mEJk37OD2zr4w4bq7MRduaDVGEnkvMbJI,11865
23
+ mcpprobe/checks/transport_conformance.py,sha256=7m8Lp54m9GOLfzGW7jmSOop4aNKxJAhtcygnUNdD_90,19189
24
+ mcpprobe/report/__init__.py,sha256=n1Q29xF32h-8_VKiOHkvEl7uMf2VdKXyeDc9FLVO8u0,318
25
+ mcpprobe/report/json_report.py,sha256=HGfax7PoH0YbbxDMnySWZFxehoMEnAlb93p8XyN8aFA,1362
26
+ mcpprobe/report/sarif.py,sha256=NGkRiHPpNWBXBMaaAn2Xi_cOJ3hVa4z1ir3pIDcinmQ,13424
27
+ mcpprobe/report/terminal.py,sha256=rK6kCvnX6gJxbrW0dMvtNpA5BbqdQ-kj0u9GHLwleF0,6596
28
+ mcpprobe/transport/__init__.py,sha256=ZmUxTFl9Vm0MWy67Gb3BWM7LwL3ki1ZZ_8r54mK9f3U,863
29
+ mcpprobe/transport/base.py,sha256=UFeFi2DfG1Yf5wh1gV6TTOIXWT7rLp-9wzH39Y_hTGI,5414
30
+ mcpprobe/transport/evidence.py,sha256=rzeqnjc0a7olgDe-6S1QMD2v10Z1yKAcGsOF-VvJJUQ,4373
31
+ mcpprobe/transport/fingerprint.py,sha256=B4A5dg9I-cRk7oWLAB8qt7MOlYUVnT70W5Uc-_HeQ-8,5609
32
+ mcpprobe/transport/http.py,sha256=NHb-qfzAFF8A0B5HxTR7kz_pkceAjyJdOp5xAQbKZtE,13222
33
+ mcpprobe/transport/ratelimit.py,sha256=g6mLxbVyXVVk0ndJ2jwJuC_RqfojbyHlABUTL7ys_9w,1578
34
+ mcpprobe/transport/stdio.py,sha256=b_7cLDTidW5HVK_p7mT1fz0uYAdA7hh5ZlmFG65EBf4,7559
35
+ mcpprobe_scanner-0.1.0.dist-info/METADATA,sha256=f7bpIvTuaFzBZ9VLpM-pS8WBMx8G5EZuCzCuDYn4CTI,9832
36
+ mcpprobe_scanner-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
37
+ mcpprobe_scanner-0.1.0.dist-info/entry_points.txt,sha256=NEMxBH370sFz_Vw8812R7adD_DDQ3ZI7ixcuWZNQCHY,47
38
+ mcpprobe_scanner-0.1.0.dist-info/licenses/LICENSE,sha256=Tzyy4bJyMSytgfl0I9_VubP5nGRQiGO4yYJKvyKUdpE,1078
39
+ mcpprobe_scanner-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcpprobe = mcpprobe.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mcpprobe contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.