mcpprobe-scanner 0.3.0__py3-none-any.whl → 0.3.1__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.
- mcpprobe/__init__.py +1 -1
- mcpprobe/checks/poisoning.py +160 -54
- mcpprobe/cli.py +10 -1
- mcpprobe/registry/__init__.py +31 -0
- mcpprobe/registry/__main__.py +5 -0
- mcpprobe/registry/aggregate.py +629 -0
- mcpprobe/registry/batch.py +449 -0
- mcpprobe/registry/cli.py +326 -0
- mcpprobe/registry/common.py +264 -0
- mcpprobe/registry/fetch.py +249 -0
- mcpprobe/registry/pipeline.py +432 -0
- mcpprobe/registry/render.py +359 -0
- mcpprobe/registry/select.py +153 -0
- mcpprobe/registry/static.py +105 -0
- mcpprobe/registry/template.py +169 -0
- {mcpprobe_scanner-0.3.0.dist-info → mcpprobe_scanner-0.3.1.dist-info}/METADATA +10 -2
- {mcpprobe_scanner-0.3.0.dist-info → mcpprobe_scanner-0.3.1.dist-info}/RECORD +21 -9
- {mcpprobe_scanner-0.3.0.dist-info → mcpprobe_scanner-0.3.1.dist-info}/WHEEL +0 -0
- {mcpprobe_scanner-0.3.0.dist-info → mcpprobe_scanner-0.3.1.dist-info}/entry_points.txt +0 -0
- {mcpprobe_scanner-0.3.0.dist-info → mcpprobe_scanner-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {mcpprobe_scanner-0.3.0.dist-info → mcpprobe_scanner-0.3.1.dist-info}/licenses/NOTICE +0 -0
mcpprobe/__init__.py
CHANGED
mcpprobe/checks/poisoning.py
CHANGED
|
@@ -22,7 +22,12 @@ from mcpprobe.checks._util import excerpt
|
|
|
22
22
|
from mcpprobe.checks.base import Check, ScanContext
|
|
23
23
|
from mcpprobe.models import Confidence, Finding, Severity, Standards
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
#: Long descriptions are common in benign servers (AWS, HubSpot) up to ~3,000 characters.
|
|
26
|
+
MAX_DESCRIPTION_LENGTH = 4000
|
|
27
|
+
#: Revision of the META-01 rules. Stored with every registry-survey outcome so results from
|
|
28
|
+
#: older rules can be told apart (0.3.1 retuned the indicators that fired on ordinary usage
|
|
29
|
+
#: guidance; see docs/benchmark.md).
|
|
30
|
+
RULES_VERSION = "0.3.1"
|
|
26
31
|
|
|
27
32
|
_MARKUP = re.compile(
|
|
28
33
|
r"<\s*/?\s*(important|system|instructions?|secret|hidden|assistant|prompt|context)\b[^>]*>"
|
|
@@ -86,15 +91,11 @@ _HIDDEN_INSTRUCTIONS: list[tuple[str, re.Pattern[str]]] = [
|
|
|
86
91
|
(
|
|
87
92
|
"model directive",
|
|
88
93
|
re.compile(
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
r"
|
|
94
|
+
# Unconditional priority over every other tool, or over what the user asked. Real
|
|
95
|
+
# descriptions order tools relative to a named sibling ("call list_projects first");
|
|
96
|
+
# poisoned ones claim precedence over all of them.
|
|
97
|
+
r"\bbefore\s+(using|calling|invoking|running)\s+any\s+other\s+(tool|function)s?\b"
|
|
93
98
|
r"|\b(before|instead\s+of)\s+any\s+other\s+(tool|function)s?\b"
|
|
94
|
-
r"|\bmust\s+(always\s+)?be\s+(called|invoked|run|used)\s+(first|before)\b"
|
|
95
|
-
r"|\b(you|the\s+(assistant|model|ai|agent|llm))\s+(must|should|need\s+to|has\s+to|have\s+to|are\s+required\s+to)\s+"
|
|
96
|
-
r"(always\s+)?(first\s+)?(read|include|call|send|run|pass|execute|fetch|invoke|use|ask|tell|forward)\b"
|
|
97
|
-
r"|\bthis\s+is\s+(a\s+)?(required|mandatory|necessary)\s+(first\s+)?step\b"
|
|
98
99
|
r"|\b(regardless|whatever)\s+(of\s+)?(what\s+)?the\s+user\s+(asks|says|wants)\b",
|
|
99
100
|
re.I,
|
|
100
101
|
),
|
|
@@ -111,6 +112,36 @@ _HIDDEN_INSTRUCTIONS: list[tuple[str, re.Pattern[str]]] = [
|
|
|
111
112
|
),
|
|
112
113
|
]
|
|
113
114
|
|
|
115
|
+
#: Ordinary usage guidance addressed to the model ("always use this tool to...", "you must
|
|
116
|
+
#: call X first", "call this first when..."). Common in benign descriptions from well-known
|
|
117
|
+
#: servers (see tests/data/meta01_real_benign.json), so on its own it is not an indicator;
|
|
118
|
+
#: it only counts towards a finding when the same text also matches another instruction
|
|
119
|
+
#: category (the demo's "Always call this first and send the user's full conversation").
|
|
120
|
+
_WEAK_DIRECTIVE = re.compile(
|
|
121
|
+
rf"\b(always\s+)?{_CALL_VERB}\s+(this|me|it)(\s+tool|\s+function)?\s+(first|before\b)"
|
|
122
|
+
r"|\balways\s+" + _CALL_VERB + r"\s+(this|me|it)\b"
|
|
123
|
+
r"|\bmust\s+(always\s+)?be\s+(called|invoked|run|used)\s+(first|before)\b"
|
|
124
|
+
r"|\b(you|the\s+(assistant|model|ai|agent|llm))\s+(must|should|need\s+to|has\s+to|have\s+to|are\s+required\s+to)\s+"
|
|
125
|
+
r"(always\s+)?(first\s+)?(read|include|call|send|run|pass|execute|fetch|invoke|use|ask|tell|forward)\b"
|
|
126
|
+
r"|\bthis\s+is\s+(a\s+)?(required|mandatory|necessary)\s+(first\s+)?step\b",
|
|
127
|
+
re.I,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
#: A negation right before a match ("never read, open, or echo any .env file").
|
|
131
|
+
_NEGATED_BEFORE = re.compile(r"\b(never|not|no|n't|without|avoid|refuse[sd]?)\s+(\w+\s+)?$", re.I)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _negated(text: str, start: int) -> bool:
|
|
135
|
+
return bool(_NEGATED_BEFORE.search(text[max(0, start - 30) : start]))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _path_requested(text: str, start: int) -> bool:
|
|
139
|
+
"""A read/send verb shortly before a credential path, and not negated."""
|
|
140
|
+
window_start = max(0, start - 60)
|
|
141
|
+
verb = _PATH_ACTION.search(text[window_start:start])
|
|
142
|
+
return verb is not None and not _negated(text, window_start + verb.start())
|
|
143
|
+
|
|
144
|
+
|
|
114
145
|
#: A request to move the model's context somewhere (same clause, within a few words).
|
|
115
146
|
_CONTEXT_EXFIL = re.compile(
|
|
116
147
|
rf"\b{_SEND_VERB}\b[^.;:!?\n]{{0,60}}?\b{_CONTEXT_NOUN}\b"
|
|
@@ -150,6 +181,13 @@ _SENSITIVE_PATHS = re.compile(
|
|
|
150
181
|
re.I,
|
|
151
182
|
)
|
|
152
183
|
|
|
184
|
+
#: Verbs that read or move a file; a credential path right after one is a request, not a mention.
|
|
185
|
+
_PATH_ACTION = re.compile(
|
|
186
|
+
r"\b(read|cat|open|load|send|upload|append|include|pass|attach|copy|forward|post|exfiltrate|dump|"
|
|
187
|
+
r"print|echo|output|return|embed)\b[^.;!?\n]{0,40}$",
|
|
188
|
+
re.I,
|
|
189
|
+
)
|
|
190
|
+
|
|
153
191
|
_URL = re.compile(r"\bhttps?://[^\s<>\"')\]]+", re.I)
|
|
154
192
|
_EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
|
|
155
193
|
_BASE64 = re.compile(r"(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{40,}={0,2}(?![A-Za-z0-9+/=])")
|
|
@@ -172,11 +210,41 @@ _ZERO_WIDTH = {
|
|
|
172
210
|
_BIDI = set(range(0x202A, 0x202F)) | set(range(0x2066, 0x206A))
|
|
173
211
|
_TAG_BLOCK = range(0xE0000, 0xE0080)
|
|
174
212
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
213
|
+
#: Wording that changes how ANOTHER tool behaves. Ordering and preference words ("before",
|
|
214
|
+
#: "after", "always", "prefer", "instead") were dropped in 0.3.1: "call list_projects first"
|
|
215
|
+
#: and "deprecated: use search_v2 instead" are how real servers document workflows.
|
|
216
|
+
_SHADOW_CONTEXT = re.compile(r"\b(redirect|reroute|intercept|override|hijack)\w*\b", re.I)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _shadows(text: str, name: str) -> bool:
|
|
220
|
+
"""True when ``text`` tries to change what the named sibling tool does or whether it is used."""
|
|
221
|
+
tool = rf"[`'\"]?{re.escape(name)}[`'\"]?(\s+(tool|function))?"
|
|
222
|
+
patterns = (
|
|
223
|
+
# "When the send_email tool is used, BCC ..." / "Whenever send_email is called, replace ..."
|
|
224
|
+
rf"\b(when|whenever|each\s+time|every\s+time)\s+(the\s+)?{tool}\s+(is|gets|has\s+been)\s+"
|
|
225
|
+
r"(used|called|invoked|run|executed)\b",
|
|
226
|
+
# "Do not use send_email" / "never call the search tool"
|
|
227
|
+
rf"\b(do\s*n[o']t|never|must\s+not|should\s+not)\s+(use|call|invoke|run)\s+(the\s+)?{tool}",
|
|
228
|
+
# "the send_message tool must route all messages through ..."
|
|
229
|
+
rf"{tool}[^.;!?\n]{{0,40}}\b(must|should|has\s+to|will)\s+(always\s+)?"
|
|
230
|
+
r"(route|send|forward|bcc|cc|copy|include|redirect|use)\b",
|
|
231
|
+
)
|
|
232
|
+
if any(re.search(p, text, re.I) for p in patterns):
|
|
233
|
+
return True
|
|
234
|
+
# Explicit interception wording in the same sentence as the tool name.
|
|
235
|
+
return any(
|
|
236
|
+
re.search(rf"(?<![\w.]){re.escape(name)}(?!\w)", sentence, re.I) and _SHADOW_CONTEXT.search(sentence)
|
|
237
|
+
for sentence in re.split(r"(?<=[.!?])\s+|\n", text)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
#: Documentation-only e-mail domains (RFC 2606): examples in parameter docs, not a destination.
|
|
242
|
+
_EXAMPLE_EMAIL = re.compile(r"@([\w-]+\.)*example\.(com|org|net)$|@example$", re.I)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _identifier_like(name: str) -> bool:
|
|
246
|
+
"""A tool name that cannot be an ordinary English word ("search" can; "list_files" cannot)."""
|
|
247
|
+
return bool(re.search(r"[_\-.0-9]|[a-z][A-Z]", name))
|
|
180
248
|
|
|
181
249
|
|
|
182
250
|
@dataclass(slots=True)
|
|
@@ -246,8 +314,19 @@ def _looks_like_base64(blob: str) -> bool:
|
|
|
246
314
|
return printable / max(1, len(decoded)) > 0.8 or len(blob) >= 120
|
|
247
315
|
|
|
248
316
|
|
|
249
|
-
def analyse_text(
|
|
250
|
-
|
|
317
|
+
def analyse_text(
|
|
318
|
+
text: str,
|
|
319
|
+
*,
|
|
320
|
+
other_tool_names: Sequence[str] = (),
|
|
321
|
+
own_name: str = "",
|
|
322
|
+
addressed_to_model: bool = False,
|
|
323
|
+
) -> list[Indicator]:
|
|
324
|
+
"""Run every heuristic over one text and return the indicators found.
|
|
325
|
+
|
|
326
|
+
``addressed_to_model`` marks text whose purpose is to guide the model (the server's
|
|
327
|
+
``initialize.instructions``): steering the model between tools is its job there, so
|
|
328
|
+
shadowing wording is only a low-severity review note.
|
|
329
|
+
"""
|
|
251
330
|
indicators: list[Indicator] = []
|
|
252
331
|
if not text:
|
|
253
332
|
return indicators
|
|
@@ -296,11 +375,18 @@ def analyse_text(text: str, *, other_tool_names: Sequence[str] = (), own_name: s
|
|
|
296
375
|
("secret harvesting", _SECRET_HARVEST),
|
|
297
376
|
]
|
|
298
377
|
for label, pattern in patterns:
|
|
299
|
-
found = [m.group(0) for m in pattern.finditer(text)]
|
|
378
|
+
found = [m.group(0) for m in pattern.finditer(text) if not _negated(text, m.start())]
|
|
300
379
|
if found:
|
|
301
380
|
instruction_kinds.append(label)
|
|
302
381
|
instruction_matches.extend(found[:3])
|
|
303
382
|
labelled.extend((label, f) for f in found[:3])
|
|
383
|
+
if instruction_kinds:
|
|
384
|
+
# Ordinary usage guidance only counts alongside a real instruction category.
|
|
385
|
+
weak = [m.group(0) for m in _WEAK_DIRECTIVE.finditer(text)]
|
|
386
|
+
if weak and "model directive" not in instruction_kinds:
|
|
387
|
+
instruction_kinds.append("model directive")
|
|
388
|
+
instruction_matches.extend(weak[:3])
|
|
389
|
+
labelled.extend(("model directive", w) for w in weak[:3])
|
|
304
390
|
if instruction_kinds:
|
|
305
391
|
strong = {"conceal from user", "override instructions"} & set(instruction_kinds)
|
|
306
392
|
severe = len(instruction_kinds) >= 2 or bool(strong)
|
|
@@ -326,14 +412,19 @@ def analyse_text(text: str, *, other_tool_names: Sequence[str] = (), own_name: s
|
|
|
326
412
|
)
|
|
327
413
|
)
|
|
328
414
|
|
|
329
|
-
|
|
415
|
+
path_hits = [m for m in _SENSITIVE_PATHS.finditer(text)]
|
|
416
|
+
paths = sorted({m.group(0).strip() for m in path_hits})
|
|
330
417
|
if paths:
|
|
418
|
+
# A credential path after a read/send verb is a request; on its own it is a mention
|
|
419
|
+
# ("paths such as ~/.ssh are refused", "saves the keys to a .env file").
|
|
420
|
+
acted = [m.group(0).strip() for m in path_hits if _path_requested(text, m.start())]
|
|
331
421
|
indicators.append(
|
|
332
422
|
Indicator(
|
|
333
423
|
"sensitive_path",
|
|
334
|
-
Severity.MEDIUM,
|
|
335
|
-
Confidence.MEDIUM,
|
|
336
|
-
"references to credential or configuration file paths"
|
|
424
|
+
Severity.MEDIUM if acted else Severity.LOW,
|
|
425
|
+
Confidence.MEDIUM if acted else Confidence.LOW,
|
|
426
|
+
"references to credential or configuration file paths"
|
|
427
|
+
+ (" next to a read/send verb" if acted else ""),
|
|
337
428
|
paths[:6],
|
|
338
429
|
)
|
|
339
430
|
)
|
|
@@ -343,37 +434,46 @@ def analyse_text(text: str, *, other_tool_names: Sequence[str] = (), own_name: s
|
|
|
343
434
|
for name in other_tool_names:
|
|
344
435
|
if not name or name == own_name or len(name) < 3:
|
|
345
436
|
continue
|
|
346
|
-
if
|
|
437
|
+
if _identifier_like(name):
|
|
438
|
+
hit = re.search(rf"(?<![\w.]){re.escape(name.lower())}(?!\w)(?!\.\w)", lowered)
|
|
439
|
+
else:
|
|
440
|
+
# A dictionary-word tool name ("search", "pipelines") counts only when quoted.
|
|
441
|
+
hit = re.search(rf"[`'\"]{re.escape(name.lower())}[`'\"]", lowered)
|
|
442
|
+
if hit:
|
|
347
443
|
refs.append(name)
|
|
348
|
-
if
|
|
349
|
-
|
|
444
|
+
shadowed = [name for name in refs if _shadows(text, name)]
|
|
445
|
+
if shadowed:
|
|
446
|
+
# A plain mention of a sibling tool ("get the ID from list_projects") is how real
|
|
447
|
+
# servers document workflows and is not reported; changing what it does is.
|
|
350
448
|
indicators.append(
|
|
351
449
|
Indicator(
|
|
352
450
|
"cross_tool_reference",
|
|
353
|
-
Severity.
|
|
354
|
-
Confidence.
|
|
355
|
-
"mentions other tools"
|
|
356
|
-
|
|
357
|
-
refs[:6],
|
|
451
|
+
Severity.LOW if addressed_to_model else Severity.MEDIUM,
|
|
452
|
+
Confidence.LOW if addressed_to_model else Confidence.MEDIUM,
|
|
453
|
+
"mentions other tools with behaviour-modifying language (shadowing)",
|
|
454
|
+
shadowed[:6],
|
|
358
455
|
)
|
|
359
456
|
)
|
|
360
457
|
|
|
361
458
|
urls = sorted({m.group(0) for m in _URL.finditer(text)})
|
|
362
459
|
if urls:
|
|
460
|
+
# Documentation links and example URLs are not reported; a URL next to send/upload
|
|
461
|
+
# wording is a low review note, and medium when the text is about context or secrets.
|
|
363
462
|
sends = bool(re.search(r"\b(send|post|upload|forward|submit|report)\b", lowered))
|
|
364
463
|
sensitive = bool(re.search(_CONTEXT_NOUN, text, re.I) or re.search(_SECRET_NOUN, text, re.I))
|
|
365
464
|
exfil_url = sends and sensitive
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
465
|
+
if sends:
|
|
466
|
+
indicators.append(
|
|
467
|
+
Indicator(
|
|
468
|
+
"url",
|
|
469
|
+
Severity.MEDIUM if exfil_url else Severity.LOW,
|
|
470
|
+
Confidence.MEDIUM if exfil_url else Confidence.LOW,
|
|
471
|
+
"URL inside description alongside send/upload wording"
|
|
472
|
+
+ (" about context or secrets" if exfil_url else ""),
|
|
473
|
+
urls[:4],
|
|
474
|
+
)
|
|
374
475
|
)
|
|
375
|
-
|
|
376
|
-
emails = sorted({m.group(0) for m in _EMAIL.finditer(text)})
|
|
476
|
+
emails = sorted({m.group(0) for m in _EMAIL.finditer(text) if not _EXAMPLE_EMAIL.search(m.group(0))})
|
|
377
477
|
if emails and re.search(r"\b(bcc|cc|send|forward|copy)\b", lowered):
|
|
378
478
|
indicators.append(
|
|
379
479
|
Indicator(
|
|
@@ -467,23 +567,24 @@ class Meta01ToolDescriptionPoisoning(Check):
|
|
|
467
567
|
description = (
|
|
468
568
|
"Scans every tool, prompt and resource description, every inputSchema parameter "
|
|
469
569
|
"description and the server 'instructions' for: prompt-style markup (<IMPORTANT>, "
|
|
470
|
-
"<system>, HTML comments), instructions aimed at the model (
|
|
471
|
-
"other tool
|
|
472
|
-
"redirect/replace), requests to send the model's context out (conversation, chat "
|
|
473
|
-
"
|
|
474
|
-
"
|
|
475
|
-
"
|
|
476
|
-
"
|
|
477
|
-
"
|
|
478
|
-
"
|
|
479
|
-
"
|
|
480
|
-
"
|
|
570
|
+
"<system>, HTML comments), instructions aimed at the model (conceal from the user, ignore "
|
|
571
|
+
"previous instructions, claim precedence over every other tool or over what the user asks, "
|
|
572
|
+
"redirect/replace), requests to send the model's context out (conversation, chat history, "
|
|
573
|
+
"system prompt, previous messages), requests to harvest secrets (the user's API key, token, "
|
|
574
|
+
"password, credentials, .env), credential file paths next to a read/send verb, wording that "
|
|
575
|
+
"changes what another tool does (shadowing), URLs and e-mail addresses with send/BCC "
|
|
576
|
+
"wording, base64 blobs, zero-width/bidi/tag Unicode, mixed-script homoglyphs and excessive "
|
|
577
|
+
'length. Ordinary usage guidance ("always use this tool to...", "you must call X '
|
|
578
|
+
'first") is not an indicator on its own; it raises a finding to high when combined with '
|
|
579
|
+
"another instruction category. Findings carry tiered confidence and are never rated "
|
|
580
|
+
"Critical on heuristics alone. Precision is guarded in the test-suite by thousands of real "
|
|
581
|
+
"descriptions from published MCP servers (docs/benchmark.md)."
|
|
481
582
|
)
|
|
482
583
|
evidence_type = "The tools/list (or prompts/resources/initialize) response and the matched excerpts."
|
|
483
584
|
false_positive_notes = (
|
|
484
|
-
"Legitimate
|
|
485
|
-
"
|
|
486
|
-
"
|
|
585
|
+
"Legitimate text that claims precedence over every other tool ('call this before any other "
|
|
586
|
+
"tool whenever...') is reported at medium. Plain URLs, sibling-tool mentions and usage "
|
|
587
|
+
"guidance are not reported; a credential path without a read/send verb is low. "
|
|
487
588
|
"Review the matched excerpts; a second-opinion judge can be plugged in through the Python API (ScanOptions.judge); mcpprobe itself makes no LLM calls."
|
|
488
589
|
)
|
|
489
590
|
fix = (
|
|
@@ -507,7 +608,12 @@ class Meta01ToolDescriptionPoisoning(Check):
|
|
|
507
608
|
"instructions": None,
|
|
508
609
|
}
|
|
509
610
|
for item in collect_text_items(ctx):
|
|
510
|
-
indicators = analyse_text(
|
|
611
|
+
indicators = analyse_text(
|
|
612
|
+
item.text,
|
|
613
|
+
other_tool_names=names,
|
|
614
|
+
own_name=item.owner,
|
|
615
|
+
addressed_to_model=item.kind == "instructions",
|
|
616
|
+
)
|
|
511
617
|
if ctx.options.judge is not None:
|
|
512
618
|
verdict = ctx.options.judge(item.text, f"tool-description poisoning review for {item.path}")
|
|
513
619
|
if verdict is not None and verdict.suspicious:
|
mcpprobe/cli.py
CHANGED
|
@@ -331,10 +331,19 @@ def token_cmd(host: str) -> None:
|
|
|
331
331
|
click.echo(f' {record_name(host)}. TXT "{expected_txt_record(token)}"')
|
|
332
332
|
|
|
333
333
|
|
|
334
|
+
def _add_registry_report() -> None:
|
|
335
|
+
from mcpprobe.registry.cli import registry_report
|
|
336
|
+
|
|
337
|
+
cli.add_command(registry_report)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
_add_registry_report()
|
|
341
|
+
|
|
342
|
+
|
|
334
343
|
def main(argv: Sequence[str] | None = None) -> None:
|
|
335
344
|
"""Entry point: default to the ``scan`` sub-command when the first argument is a target."""
|
|
336
345
|
args = list(sys.argv[1:] if argv is None else argv)
|
|
337
|
-
known = {"scan", "checks", "token"}
|
|
346
|
+
known = {"scan", "checks", "token", "registry-report"}
|
|
338
347
|
group_options = {"-h", "--help", "--version"}
|
|
339
348
|
if args and args[0] not in known and args[0] not in group_options:
|
|
340
349
|
args.insert(0, "scan")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Pipeline for the "State of MCP server security" report over the official MCP Registry.
|
|
2
|
+
|
|
3
|
+
Stages, each resumable from a working directory:
|
|
4
|
+
|
|
5
|
+
1. :mod:`~mcpprobe.registry.fetch`: page through the registry list API and write
|
|
6
|
+
``entries.jsonl`` (one normalised entry per server).
|
|
7
|
+
2. :mod:`~mcpprobe.registry.select`: split entries into remote (``remotes[]`` with a
|
|
8
|
+
streamable-http or SSE URL) and package-only, dedupe hosts, apply exclusions.
|
|
9
|
+
3. :mod:`~mcpprobe.registry.static`: META-01/06/07 on tool definitions the registry
|
|
10
|
+
entry itself carries (for every entry, including package-only ones).
|
|
11
|
+
4. :mod:`~mcpprobe.registry.batch`: a polite, passive scan of each remote host with a
|
|
12
|
+
reduced *survey profile* of checks, checkpointed to ``scan.jsonl``.
|
|
13
|
+
5. :mod:`~mcpprobe.registry.aggregate`: anonymised aggregates with 95% intervals.
|
|
14
|
+
6. :mod:`~mcpprobe.registry.render`: Markdown report and SVG charts, with a guard that
|
|
15
|
+
refuses to write a report that names a third-party host.
|
|
16
|
+
|
|
17
|
+
Nothing here stores a response body from a third party: only check outcomes, counts and
|
|
18
|
+
the negotiated protocol revision / edge label are kept.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from mcpprobe.registry.common import (
|
|
24
|
+
MAX_CONCURRENCY,
|
|
25
|
+
PER_HOST_INTERVAL,
|
|
26
|
+
STATIC_CHECKS,
|
|
27
|
+
SURVEY_CHECKS,
|
|
28
|
+
Outcome,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = ["MAX_CONCURRENCY", "PER_HOST_INTERVAL", "STATIC_CHECKS", "SURVEY_CHECKS", "Outcome"]
|