agent_os_kernel 3.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.
Files changed (337) hide show
  1. agent_control_plane/__init__.py +662 -0
  2. agent_control_plane/a2a_adapter.py +543 -0
  3. agent_control_plane/adapter.py +417 -0
  4. agent_control_plane/agent_hibernation.py +394 -0
  5. agent_control_plane/agent_kernel.py +470 -0
  6. agent_control_plane/compliance.py +720 -0
  7. agent_control_plane/constraint_graphs.py +478 -0
  8. agent_control_plane/control_plane.py +854 -0
  9. agent_control_plane/example_executors.py +195 -0
  10. agent_control_plane/execution_engine.py +231 -0
  11. agent_control_plane/flight_recorder.py +846 -0
  12. agent_control_plane/governance_layer.py +435 -0
  13. agent_control_plane/hf_utils.py +563 -0
  14. agent_control_plane/interfaces/__init__.py +55 -0
  15. agent_control_plane/interfaces/kernel_interface.py +361 -0
  16. agent_control_plane/interfaces/plugin_interface.py +497 -0
  17. agent_control_plane/interfaces/protocol_interfaces.py +387 -0
  18. agent_control_plane/kernel_space.py +1009 -0
  19. agent_control_plane/langchain_adapter.py +424 -0
  20. agent_control_plane/lifecycle.py +3113 -0
  21. agent_control_plane/mcp_adapter.py +653 -0
  22. agent_control_plane/ml_safety.py +563 -0
  23. agent_control_plane/multimodal.py +727 -0
  24. agent_control_plane/mute_agent.py +422 -0
  25. agent_control_plane/observability.py +787 -0
  26. agent_control_plane/orchestrator.py +482 -0
  27. agent_control_plane/plugin_registry.py +750 -0
  28. agent_control_plane/policy_engine.py +954 -0
  29. agent_control_plane/process_isolation.py +777 -0
  30. agent_control_plane/shadow_mode.py +310 -0
  31. agent_control_plane/signals.py +493 -0
  32. agent_control_plane/supervisor_agents.py +430 -0
  33. agent_control_plane/time_travel_debugger.py +557 -0
  34. agent_control_plane/tool_registry.py +452 -0
  35. agent_control_plane/vfs.py +697 -0
  36. agent_kernel/__init__.py +69 -0
  37. agent_kernel/analyzer.py +435 -0
  38. agent_kernel/auditor.py +36 -0
  39. agent_kernel/completeness_auditor.py +237 -0
  40. agent_kernel/detector.py +203 -0
  41. agent_kernel/kernel.py +744 -0
  42. agent_kernel/memory_manager.py +85 -0
  43. agent_kernel/models.py +374 -0
  44. agent_kernel/nudge_mechanism.py +263 -0
  45. agent_kernel/outcome_analyzer.py +338 -0
  46. agent_kernel/patcher.py +582 -0
  47. agent_kernel/semantic_analyzer.py +316 -0
  48. agent_kernel/semantic_purge.py +349 -0
  49. agent_kernel/simulator.py +449 -0
  50. agent_kernel/teacher.py +85 -0
  51. agent_kernel/triage.py +152 -0
  52. agent_os/__init__.py +409 -0
  53. agent_os/_adversarial_impl.py +200 -0
  54. agent_os/_circuit_breaker_impl.py +232 -0
  55. agent_os/_mcp_metrics.py +193 -0
  56. agent_os/adversarial.py +20 -0
  57. agent_os/agents_compat.py +490 -0
  58. agent_os/audit_logger.py +135 -0
  59. agent_os/base_agent.py +651 -0
  60. agent_os/circuit_breaker.py +34 -0
  61. agent_os/cli/__init__.py +659 -0
  62. agent_os/cli/cmd_audit.py +128 -0
  63. agent_os/cli/cmd_init.py +152 -0
  64. agent_os/cli/cmd_policy.py +41 -0
  65. agent_os/cli/cmd_policy_gen.py +180 -0
  66. agent_os/cli/cmd_validate.py +258 -0
  67. agent_os/cli/mcp_scan.py +265 -0
  68. agent_os/cli/output.py +192 -0
  69. agent_os/cli/policy_checker.py +330 -0
  70. agent_os/compat.py +74 -0
  71. agent_os/constraint_graph.py +234 -0
  72. agent_os/content_governance.py +140 -0
  73. agent_os/context_budget.py +305 -0
  74. agent_os/credential_redactor.py +224 -0
  75. agent_os/diff_policy.py +89 -0
  76. agent_os/egress_policy.py +159 -0
  77. agent_os/escalation.py +276 -0
  78. agent_os/event_bus.py +124 -0
  79. agent_os/exceptions.py +180 -0
  80. agent_os/execution_context_policy.py +141 -0
  81. agent_os/github_enterprise.py +96 -0
  82. agent_os/health.py +20 -0
  83. agent_os/integrations/__init__.py +279 -0
  84. agent_os/integrations/a2a_adapter.py +279 -0
  85. agent_os/integrations/agent_lightning/__init__.py +30 -0
  86. agent_os/integrations/anthropic_adapter.py +420 -0
  87. agent_os/integrations/autogen_adapter.py +620 -0
  88. agent_os/integrations/base.py +1137 -0
  89. agent_os/integrations/compat.py +229 -0
  90. agent_os/integrations/config.py +98 -0
  91. agent_os/integrations/conversation_guardian.py +957 -0
  92. agent_os/integrations/crewai_adapter.py +467 -0
  93. agent_os/integrations/drift_detector.py +425 -0
  94. agent_os/integrations/dry_run.py +124 -0
  95. agent_os/integrations/escalation.py +582 -0
  96. agent_os/integrations/gemini_adapter.py +364 -0
  97. agent_os/integrations/google_adk_adapter.py +633 -0
  98. agent_os/integrations/guardrails_adapter.py +394 -0
  99. agent_os/integrations/health.py +197 -0
  100. agent_os/integrations/langchain_adapter.py +654 -0
  101. agent_os/integrations/llamafirewall.py +343 -0
  102. agent_os/integrations/llamaindex_adapter.py +188 -0
  103. agent_os/integrations/logging.py +191 -0
  104. agent_os/integrations/maf_adapter.py +631 -0
  105. agent_os/integrations/mistral_adapter.py +365 -0
  106. agent_os/integrations/openai_adapter.py +816 -0
  107. agent_os/integrations/openai_agents_sdk.py +406 -0
  108. agent_os/integrations/policy_compose.py +171 -0
  109. agent_os/integrations/profiling.py +144 -0
  110. agent_os/integrations/pydantic_ai_adapter.py +420 -0
  111. agent_os/integrations/rate_limiter.py +130 -0
  112. agent_os/integrations/rbac.py +143 -0
  113. agent_os/integrations/registry.py +113 -0
  114. agent_os/integrations/scope_guard.py +303 -0
  115. agent_os/integrations/semantic_kernel_adapter.py +769 -0
  116. agent_os/integrations/smolagents_adapter.py +629 -0
  117. agent_os/integrations/templates.py +178 -0
  118. agent_os/integrations/token_budget.py +134 -0
  119. agent_os/integrations/tool_aliases.py +190 -0
  120. agent_os/integrations/webhooks.py +177 -0
  121. agent_os/lite.py +208 -0
  122. agent_os/mcp_gateway.py +385 -0
  123. agent_os/mcp_message_signer.py +273 -0
  124. agent_os/mcp_protocols.py +161 -0
  125. agent_os/mcp_response_scanner.py +232 -0
  126. agent_os/mcp_security.py +924 -0
  127. agent_os/mcp_session_auth.py +231 -0
  128. agent_os/mcp_sliding_rate_limiter.py +184 -0
  129. agent_os/memory_guard.py +409 -0
  130. agent_os/metrics.py +134 -0
  131. agent_os/mute.py +428 -0
  132. agent_os/mute_agent.py +209 -0
  133. agent_os/policies/__init__.py +77 -0
  134. agent_os/policies/async_evaluator.py +275 -0
  135. agent_os/policies/backends.py +670 -0
  136. agent_os/policies/bridge.py +169 -0
  137. agent_os/policies/budget.py +85 -0
  138. agent_os/policies/cli.py +294 -0
  139. agent_os/policies/conflict_resolution.py +270 -0
  140. agent_os/policies/data_classification.py +252 -0
  141. agent_os/policies/evaluator.py +239 -0
  142. agent_os/policies/policy_schema.json +228 -0
  143. agent_os/policies/rate_limiting.py +145 -0
  144. agent_os/policies/schema.py +115 -0
  145. agent_os/policies/shared.py +331 -0
  146. agent_os/prompt_injection.py +694 -0
  147. agent_os/providers.py +182 -0
  148. agent_os/py.typed +0 -0
  149. agent_os/retry.py +81 -0
  150. agent_os/reversibility.py +251 -0
  151. agent_os/sandbox.py +432 -0
  152. agent_os/sandbox_provider.py +140 -0
  153. agent_os/secure_codegen.py +525 -0
  154. agent_os/security_skills.py +538 -0
  155. agent_os/semantic_policy.py +422 -0
  156. agent_os/server/__init__.py +15 -0
  157. agent_os/server/__main__.py +25 -0
  158. agent_os/server/app.py +277 -0
  159. agent_os/server/models.py +104 -0
  160. agent_os/shift_left_metrics.py +130 -0
  161. agent_os/stateless.py +742 -0
  162. agent_os/supervisor.py +148 -0
  163. agent_os/task_outcome.py +148 -0
  164. agent_os/transparency.py +181 -0
  165. agent_os/trust_root.py +128 -0
  166. agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
  167. agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
  168. agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
  169. agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
  170. agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
  171. agent_os_observability/__init__.py +27 -0
  172. agent_os_observability/dashboards.py +898 -0
  173. agent_os_observability/metrics.py +398 -0
  174. agent_os_observability/server.py +223 -0
  175. agent_os_observability/tracer.py +232 -0
  176. agent_primitives/__init__.py +24 -0
  177. agent_primitives/failures.py +84 -0
  178. agent_primitives/py.typed +0 -0
  179. amb_core/__init__.py +177 -0
  180. amb_core/adapters/__init__.py +57 -0
  181. amb_core/adapters/aws_sqs_broker.py +376 -0
  182. amb_core/adapters/azure_servicebus_broker.py +340 -0
  183. amb_core/adapters/kafka_broker.py +260 -0
  184. amb_core/adapters/nats_broker.py +285 -0
  185. amb_core/adapters/rabbitmq_broker.py +235 -0
  186. amb_core/adapters/redis_broker.py +262 -0
  187. amb_core/broker.py +145 -0
  188. amb_core/bus.py +481 -0
  189. amb_core/cloudevents.py +509 -0
  190. amb_core/dlq.py +345 -0
  191. amb_core/hf_utils.py +536 -0
  192. amb_core/memory_broker.py +410 -0
  193. amb_core/models.py +141 -0
  194. amb_core/persistence.py +529 -0
  195. amb_core/schema.py +294 -0
  196. amb_core/tracing.py +358 -0
  197. atr/__init__.py +640 -0
  198. atr/access.py +348 -0
  199. atr/composition.py +645 -0
  200. atr/decorator.py +357 -0
  201. atr/executor.py +384 -0
  202. atr/health.py +557 -0
  203. atr/hf_utils.py +449 -0
  204. atr/injection.py +422 -0
  205. atr/metrics.py +440 -0
  206. atr/policies.py +403 -0
  207. atr/py.typed +2 -0
  208. atr/registry.py +452 -0
  209. atr/schema.py +480 -0
  210. atr/tools/safe/__init__.py +75 -0
  211. atr/tools/safe/calculator.py +467 -0
  212. atr/tools/safe/datetime_tool.py +443 -0
  213. atr/tools/safe/file_reader.py +402 -0
  214. atr/tools/safe/http_client.py +316 -0
  215. atr/tools/safe/json_parser.py +374 -0
  216. atr/tools/safe/text_tool.py +537 -0
  217. atr/tools/safe/toolkit.py +175 -0
  218. caas/__init__.py +162 -0
  219. caas/api/__init__.py +7 -0
  220. caas/api/server.py +1328 -0
  221. caas/caching.py +834 -0
  222. caas/cli.py +210 -0
  223. caas/conversation.py +223 -0
  224. caas/decay.py +72 -0
  225. caas/detection/__init__.py +9 -0
  226. caas/detection/detector.py +238 -0
  227. caas/enrichment.py +130 -0
  228. caas/gateway/__init__.py +27 -0
  229. caas/gateway/trust_gateway.py +474 -0
  230. caas/hf_utils.py +479 -0
  231. caas/ingestion/__init__.py +23 -0
  232. caas/ingestion/processors.py +253 -0
  233. caas/ingestion/structure_parser.py +188 -0
  234. caas/models.py +356 -0
  235. caas/pragmatic_truth.py +444 -0
  236. caas/routing/__init__.py +10 -0
  237. caas/routing/heuristic_router.py +58 -0
  238. caas/storage/__init__.py +9 -0
  239. caas/storage/store.py +389 -0
  240. caas/triad.py +213 -0
  241. caas/tuning/__init__.py +9 -0
  242. caas/tuning/tuner.py +329 -0
  243. caas/vfs/__init__.py +14 -0
  244. caas/vfs/filesystem.py +452 -0
  245. cmvk/__init__.py +218 -0
  246. cmvk/audit.py +402 -0
  247. cmvk/benchmarks.py +478 -0
  248. cmvk/constitutional.py +904 -0
  249. cmvk/hf_utils.py +301 -0
  250. cmvk/metrics.py +473 -0
  251. cmvk/profiles.py +300 -0
  252. cmvk/py.typed +0 -0
  253. cmvk/types.py +12 -0
  254. cmvk/verification.py +956 -0
  255. emk/__init__.py +89 -0
  256. emk/causal.py +352 -0
  257. emk/hf_utils.py +421 -0
  258. emk/indexer.py +83 -0
  259. emk/py.typed +0 -0
  260. emk/schema.py +204 -0
  261. emk/sleep_cycle.py +347 -0
  262. emk/store.py +281 -0
  263. iatp/__init__.py +166 -0
  264. iatp/attestation.py +461 -0
  265. iatp/cli.py +317 -0
  266. iatp/hf_utils.py +472 -0
  267. iatp/ipc_pipes.py +580 -0
  268. iatp/main.py +412 -0
  269. iatp/models/__init__.py +447 -0
  270. iatp/policy_engine.py +337 -0
  271. iatp/py.typed +2 -0
  272. iatp/recovery.py +321 -0
  273. iatp/security/__init__.py +270 -0
  274. iatp/sidecar/__init__.py +519 -0
  275. iatp/telemetry/__init__.py +164 -0
  276. iatp/tests/__init__.py +1 -0
  277. iatp/tests/test_attestation.py +370 -0
  278. iatp/tests/test_cli.py +131 -0
  279. iatp/tests/test_ed25519_attestation.py +211 -0
  280. iatp/tests/test_models.py +130 -0
  281. iatp/tests/test_policy_engine.py +347 -0
  282. iatp/tests/test_recovery.py +281 -0
  283. iatp/tests/test_security.py +222 -0
  284. iatp/tests/test_sidecar.py +167 -0
  285. iatp/tests/test_telemetry.py +175 -0
  286. mcp_kernel_server/__init__.py +28 -0
  287. mcp_kernel_server/cli.py +274 -0
  288. mcp_kernel_server/resources.py +217 -0
  289. mcp_kernel_server/server.py +564 -0
  290. mcp_kernel_server/tools.py +1174 -0
  291. mute_agent/__init__.py +68 -0
  292. mute_agent/core/__init__.py +1 -0
  293. mute_agent/core/execution_agent.py +166 -0
  294. mute_agent/core/handshake_protocol.py +201 -0
  295. mute_agent/core/reasoning_agent.py +238 -0
  296. mute_agent/knowledge_graph/__init__.py +1 -0
  297. mute_agent/knowledge_graph/graph_elements.py +65 -0
  298. mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
  299. mute_agent/knowledge_graph/subgraph.py +224 -0
  300. mute_agent/listener/__init__.py +43 -0
  301. mute_agent/listener/adapters/__init__.py +31 -0
  302. mute_agent/listener/adapters/base_adapter.py +189 -0
  303. mute_agent/listener/adapters/caas_adapter.py +344 -0
  304. mute_agent/listener/adapters/control_plane_adapter.py +436 -0
  305. mute_agent/listener/adapters/iatp_adapter.py +332 -0
  306. mute_agent/listener/adapters/scak_adapter.py +251 -0
  307. mute_agent/listener/listener.py +610 -0
  308. mute_agent/listener/state_observer.py +436 -0
  309. mute_agent/listener/threshold_config.py +313 -0
  310. mute_agent/super_system/__init__.py +1 -0
  311. mute_agent/super_system/router.py +204 -0
  312. mute_agent/visualization/__init__.py +10 -0
  313. mute_agent/visualization/graph_debugger.py +502 -0
  314. nexus/README.md +60 -0
  315. nexus/__init__.py +51 -0
  316. nexus/arbiter.py +359 -0
  317. nexus/client.py +466 -0
  318. nexus/dmz.py +444 -0
  319. nexus/escrow.py +430 -0
  320. nexus/exceptions.py +286 -0
  321. nexus/pyproject.toml +36 -0
  322. nexus/registry.py +393 -0
  323. nexus/reputation.py +425 -0
  324. nexus/schemas/__init__.py +51 -0
  325. nexus/schemas/compliance.py +276 -0
  326. nexus/schemas/escrow.py +251 -0
  327. nexus/schemas/manifest.py +225 -0
  328. nexus/schemas/receipt.py +208 -0
  329. nexus/tests/__init__.py +0 -0
  330. nexus/tests/conftest.py +146 -0
  331. nexus/tests/test_arbiter.py +192 -0
  332. nexus/tests/test_dmz.py +194 -0
  333. nexus/tests/test_escrow.py +276 -0
  334. nexus/tests/test_exceptions.py +225 -0
  335. nexus/tests/test_registry.py +232 -0
  336. nexus/tests/test_reputation.py +328 -0
  337. nexus/tests/test_schemas.py +295 -0
@@ -0,0 +1,924 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """MCP Security — defense against tool poisoning, rug pulls, and protocol attacks.
4
+
5
+ Screens MCP tool definitions for adversarial manipulation where attackers
6
+ embed hidden instructions in tool descriptions/metadata that are invisible
7
+ to users but executed by LLMs.
8
+
9
+ Public Preview protections:
10
+ - **Tool poisoning detection**: Catches hidden instructions, invisible
11
+ unicode, markdown/HTML comments, and encoded payloads in tool
12
+ descriptions.
13
+ - **Description injection**: Detects prompt injection patterns
14
+ embedded within MCP tool metadata.
15
+ - **Schema abuse**: Flags overly permissive schemas, hidden required
16
+ fields, and instruction-bearing default values.
17
+ - **Rug pull detection**: Fingerprints registered tools and alerts
18
+ when descriptions or schemas change silently between sessions.
19
+ - **Cross-server attacks**: Detects tool impersonation and
20
+ typosquatting across MCP server boundaries.
21
+ - **Audit trail**: Logs every scan with timestamp and tool identity
22
+ for forensic review.
23
+
24
+ Architecture:
25
+ MCPSecurityScanner
26
+ ├─ scan_tool() — scan a single tool definition
27
+ ├─ scan_server() — batch-scan all tools from a server
28
+ ├─ register_tool() — fingerprint a tool for rug-pull detection
29
+ ├─ check_rug_pull() — compare current definition to fingerprint
30
+ └─ audit_log — inspection trail
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import base64
36
+ import hashlib
37
+ import json
38
+ import logging
39
+ import os
40
+ import re
41
+ import time
42
+ import warnings
43
+ from dataclasses import dataclass, field
44
+ from datetime import datetime, timezone
45
+ from enum import Enum
46
+ from typing import Any, Callable
47
+
48
+ from agent_os._mcp_metrics import MCPMetrics, MCPMetricsRecorder
49
+ from agent_os.mcp_protocols import InMemoryAuditSink, MCPAuditSink
50
+ from agent_os.prompt_injection import PromptInjectionDetector
51
+
52
+ logger = logging.getLogger(__name__)
53
+
54
+ _SAMPLE_DISCLAIMER = (
55
+ "\u26a0\ufe0f These are SAMPLE MCP security rules provided as a starting point. "
56
+ "You MUST review, customise, and extend them for your specific use case "
57
+ "before deploying to production."
58
+ )
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Data models
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ class MCPThreatType(Enum):
67
+ """Classification of an MCP-layer threat."""
68
+
69
+ TOOL_POISONING = "tool_poisoning"
70
+ RUG_PULL = "rug_pull"
71
+ CROSS_SERVER_ATTACK = "cross_server_attack"
72
+ CONFUSED_DEPUTY = "confused_deputy"
73
+ HIDDEN_INSTRUCTION = "hidden_instruction"
74
+ DESCRIPTION_INJECTION = "description_injection"
75
+
76
+
77
+ class MCPSeverity(Enum):
78
+ """Severity of an MCP threat."""
79
+
80
+ INFO = "info"
81
+ WARNING = "warning"
82
+ CRITICAL = "critical"
83
+
84
+
85
+ @dataclass
86
+ class MCPThreat:
87
+ """A single threat finding from an MCP tool scan."""
88
+
89
+ threat_type: MCPThreatType
90
+ severity: MCPSeverity
91
+ tool_name: str
92
+ server_name: str
93
+ message: str
94
+ matched_pattern: str | None = None
95
+ details: dict[str, Any] = field(default_factory=dict)
96
+
97
+
98
+ @dataclass
99
+ class ToolFingerprint:
100
+ """Cryptographic fingerprint of a tool definition."""
101
+
102
+ tool_name: str
103
+ server_name: str
104
+ description_hash: str
105
+ schema_hash: str
106
+ first_seen: float
107
+ last_seen: float
108
+ version: int
109
+
110
+
111
+ @dataclass
112
+ class ScanResult:
113
+ """Aggregate outcome of scanning one or more tools."""
114
+
115
+ safe: bool
116
+ threats: list[MCPThreat]
117
+ tools_scanned: int
118
+ tools_flagged: int
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Detection patterns (compiled at import time, following memory_guard.py style)
123
+ # ---------------------------------------------------------------------------
124
+
125
+ # Invisible unicode characters used to hide instructions
126
+ _INVISIBLE_UNICODE_PATTERNS: list[re.Pattern[str]] = [
127
+ re.compile(r"[\u200b\u200c\u200d\ufeff]"), # zero-width spaces/joiners/BOM
128
+ re.compile(r"[\u202a-\u202e]"), # bidi embedding/override
129
+ re.compile(r"[\u2066-\u2069]"), # bidi isolates
130
+ re.compile(r"[\u00ad]"), # soft hyphen
131
+ re.compile(r"[\u2060\u180e]"), # word joiner, mongolian vowel separator
132
+ ]
133
+
134
+ # Markdown/HTML comments that hide text from users
135
+ _HIDDEN_COMMENT_PATTERNS: list[re.Pattern[str]] = [
136
+ re.compile(r"<!--.*?-->", re.DOTALL), # HTML comments
137
+ re.compile(r"\[//\]:\s*#\s*\(.*?\)", re.DOTALL), # Markdown reference comments
138
+ re.compile(r"\[comment\]:\s*<>\s*\(.*?\)", re.DOTALL), # alternative MD comment
139
+ ]
140
+
141
+ # Instruction-like patterns hidden in descriptions
142
+ _HIDDEN_INSTRUCTION_PATTERNS: list[re.Pattern[str]] = [
143
+ re.compile(r"ignore\s+(all\s+)?previous", re.IGNORECASE),
144
+ re.compile(r"override\s+(the\s+)?(previous|above|original)", re.IGNORECASE),
145
+ re.compile(r"instead\s+of\s+(the\s+)?(above|previous|described)", re.IGNORECASE),
146
+ re.compile(r"actually\s+do", re.IGNORECASE),
147
+ re.compile(r"\bsystem\s*:", re.IGNORECASE),
148
+ re.compile(r"\bassistant\s*:", re.IGNORECASE),
149
+ re.compile(r"do\s+not\s+follow", re.IGNORECASE),
150
+ re.compile(r"disregard\s+(all\s+)?(above|prior|previous)", re.IGNORECASE),
151
+ ]
152
+
153
+ # Encoded payload patterns
154
+ _ENCODED_PAYLOAD_PATTERNS: list[re.Pattern[str]] = [
155
+ re.compile(r"[A-Za-z0-9+/]{40,}={0,2}"), # long base64 strings
156
+ re.compile(r"(?:\\x[0-9a-fA-F]{2}){4,}"), # hex sequences
157
+ ]
158
+
159
+ # Data exfiltration patterns
160
+ _EXFILTRATION_PATTERNS: list[re.Pattern[str]] = [
161
+ re.compile(r"\bcurl\b", re.IGNORECASE),
162
+ re.compile(r"\bwget\b", re.IGNORECASE),
163
+ re.compile(r"\bfetch\s*\(", re.IGNORECASE),
164
+ re.compile(r"https?://", re.IGNORECASE),
165
+ re.compile(r"\bsend\s+email\b", re.IGNORECASE),
166
+ re.compile(r"\bsend\s+to\b", re.IGNORECASE),
167
+ re.compile(r"\bpost\s+to\b", re.IGNORECASE),
168
+ re.compile(r"include\s+the\s+contents?\s+of\b", re.IGNORECASE),
169
+ ]
170
+
171
+ # Privilege escalation in descriptions
172
+ _PRIVILEGE_ESCALATION_PATTERNS: list[re.Pattern[str]] = [
173
+ re.compile(r"\bsudo\b", re.IGNORECASE),
174
+ re.compile(r"\badmin\s+access\b", re.IGNORECASE),
175
+ re.compile(r"\broot\s+access\b", re.IGNORECASE),
176
+ re.compile(r"\belevate\s+privile", re.IGNORECASE),
177
+ re.compile(r"\bexec\s*\(", re.IGNORECASE),
178
+ re.compile(r"\beval\s*\(", re.IGNORECASE),
179
+ ]
180
+
181
+ # Role override patterns
182
+ _ROLE_OVERRIDE_PATTERNS: list[re.Pattern[str]] = [
183
+ re.compile(r"you\s+are\b", re.IGNORECASE),
184
+ re.compile(r"your\s+task\s+is\b", re.IGNORECASE),
185
+ re.compile(r"respond\s+with\b", re.IGNORECASE),
186
+ re.compile(r"always\s+return\b", re.IGNORECASE),
187
+ re.compile(r"you\s+must\b", re.IGNORECASE),
188
+ re.compile(r"your\s+role\s+is\b", re.IGNORECASE),
189
+ ]
190
+
191
+ # Content after excessive whitespace (hidden instructions at the end)
192
+ _EXCESSIVE_WHITESPACE_PATTERN: re.Pattern[str] = re.compile(r"\n{5,}.+", re.DOTALL)
193
+
194
+ # Suspicious keywords in decoded base64
195
+ _SUSPICIOUS_DECODED_KEYWORDS: list[str] = [
196
+ "ignore",
197
+ "override",
198
+ "system",
199
+ "password",
200
+ "secret",
201
+ "admin",
202
+ "root",
203
+ "exec",
204
+ "eval",
205
+ "import os",
206
+ "send",
207
+ "curl",
208
+ "fetch",
209
+ ]
210
+
211
+
212
+ # ---------------------------------------------------------------------------
213
+ # Externalised configuration dataclass
214
+ # ---------------------------------------------------------------------------
215
+
216
+
217
+ @dataclass
218
+ class MCPSecurityConfig:
219
+ """Structured configuration for MCP security scanning, loadable from YAML.
220
+
221
+ Attributes:
222
+ invisible_unicode_patterns: Regex strings for invisible unicode detection.
223
+ hidden_comment_patterns: Regex strings for hidden comments.
224
+ hidden_instruction_patterns: Regex strings for instruction-like text.
225
+ encoded_payload_patterns: Regex strings for encoded payloads.
226
+ exfiltration_patterns: Regex strings for data exfiltration.
227
+ privilege_escalation_patterns: Regex strings for privilege escalation.
228
+ role_override_patterns: Regex strings for role overrides.
229
+ excessive_whitespace_pattern: Regex string for excessive whitespace.
230
+ suspicious_decoded_keywords: Keywords to check in decoded payloads.
231
+ disclaimer: Disclaimer text shown in logs.
232
+ """
233
+
234
+ invisible_unicode_patterns: list[str] = field(
235
+ default_factory=lambda: [p.pattern for p in _INVISIBLE_UNICODE_PATTERNS]
236
+ )
237
+ hidden_comment_patterns: list[str] = field(
238
+ default_factory=lambda: [p.pattern for p in _HIDDEN_COMMENT_PATTERNS]
239
+ )
240
+ hidden_instruction_patterns: list[str] = field(
241
+ default_factory=lambda: [p.pattern for p in _HIDDEN_INSTRUCTION_PATTERNS]
242
+ )
243
+ encoded_payload_patterns: list[str] = field(
244
+ default_factory=lambda: [p.pattern for p in _ENCODED_PAYLOAD_PATTERNS]
245
+ )
246
+ exfiltration_patterns: list[str] = field(
247
+ default_factory=lambda: [p.pattern for p in _EXFILTRATION_PATTERNS]
248
+ )
249
+ privilege_escalation_patterns: list[str] = field(
250
+ default_factory=lambda: [p.pattern for p in _PRIVILEGE_ESCALATION_PATTERNS]
251
+ )
252
+ role_override_patterns: list[str] = field(
253
+ default_factory=lambda: [p.pattern for p in _ROLE_OVERRIDE_PATTERNS]
254
+ )
255
+ excessive_whitespace_pattern: str = field(
256
+ default_factory=lambda: _EXCESSIVE_WHITESPACE_PATTERN.pattern
257
+ )
258
+ suspicious_decoded_keywords: list[str] = field(
259
+ default_factory=lambda: list(_SUSPICIOUS_DECODED_KEYWORDS)
260
+ )
261
+ disclaimer: str = ""
262
+
263
+
264
+ def load_mcp_security_config(path: str) -> MCPSecurityConfig:
265
+ """Load MCP security configuration from a YAML file.
266
+
267
+ Args:
268
+ path: Path to a YAML file with a ``detection_patterns`` section.
269
+
270
+ Returns:
271
+ MCPSecurityConfig populated from the YAML data.
272
+
273
+ Raises:
274
+ FileNotFoundError: If the config file does not exist.
275
+ ValueError: If the YAML is missing the ``detection_patterns`` section.
276
+ """
277
+ import yaml
278
+
279
+ if not os.path.exists(path):
280
+ raise FileNotFoundError(f"MCP security config not found: {path}")
281
+
282
+ with open(path, "r", encoding="utf-8") as fh:
283
+ data = yaml.safe_load(fh.read())
284
+
285
+ if not isinstance(data, dict) or "detection_patterns" not in data:
286
+ raise ValueError(f"YAML file must contain a 'detection_patterns' section: {path}")
287
+
288
+ dp = data["detection_patterns"]
289
+ return MCPSecurityConfig(
290
+ invisible_unicode_patterns=dp.get(
291
+ "invisible_unicode", [p.pattern for p in _INVISIBLE_UNICODE_PATTERNS]
292
+ ),
293
+ hidden_comment_patterns=dp.get(
294
+ "hidden_comments", [p.pattern for p in _HIDDEN_COMMENT_PATTERNS]
295
+ ),
296
+ hidden_instruction_patterns=dp.get(
297
+ "hidden_instructions", [p.pattern for p in _HIDDEN_INSTRUCTION_PATTERNS]
298
+ ),
299
+ encoded_payload_patterns=dp.get(
300
+ "encoded_payloads", [p.pattern for p in _ENCODED_PAYLOAD_PATTERNS]
301
+ ),
302
+ exfiltration_patterns=dp.get("exfiltration", [p.pattern for p in _EXFILTRATION_PATTERNS]),
303
+ privilege_escalation_patterns=dp.get(
304
+ "privilege_escalation", [p.pattern for p in _PRIVILEGE_ESCALATION_PATTERNS]
305
+ ),
306
+ role_override_patterns=dp.get(
307
+ "role_override", [p.pattern for p in _ROLE_OVERRIDE_PATTERNS]
308
+ ),
309
+ excessive_whitespace_pattern=dp.get(
310
+ "excessive_whitespace", _EXCESSIVE_WHITESPACE_PATTERN.pattern
311
+ ),
312
+ suspicious_decoded_keywords=data.get(
313
+ "suspicious_decoded_keywords", list(_SUSPICIOUS_DECODED_KEYWORDS)
314
+ ),
315
+ disclaimer=data.get("disclaimer", ""),
316
+ )
317
+
318
+
319
+ # ---------------------------------------------------------------------------
320
+ # MCPSecurityScanner
321
+ # ---------------------------------------------------------------------------
322
+
323
+
324
+ class MCPSecurityScanner:
325
+ """Scans MCP tool definitions for poisoning, rug pulls, and protocol attacks.
326
+
327
+ Usage::
328
+
329
+ scanner = MCPSecurityScanner()
330
+ threats = scanner.scan_tool(
331
+ "search", "Search the web for information",
332
+ server_name="web-tools"
333
+ )
334
+ if threats:
335
+ print(f"Found {len(threats)} threat(s)")
336
+ """
337
+
338
+ def __init__(
339
+ self,
340
+ metrics: MCPMetricsRecorder | None = None,
341
+ *,
342
+ audit_sink: MCPAuditSink | None = None,
343
+ clock: Callable[[], float] = time.time,
344
+ ) -> None:
345
+ warnings.warn(
346
+ "MCPSecurityScanner() uses built-in sample rules that may not "
347
+ "cover all MCP tool poisoning techniques. For production use, load an "
348
+ "explicit config with load_mcp_security_config(). "
349
+ "See examples/policies/mcp-security.yaml for a sample configuration.",
350
+ stacklevel=2,
351
+ )
352
+ self._tool_registry: dict[str, ToolFingerprint] = {}
353
+ self._audit_log: list[dict[str, Any]] = []
354
+ self._injection_detector = PromptInjectionDetector()
355
+ self._metrics = metrics or MCPMetrics()
356
+ self._audit_sink = audit_sink or InMemoryAuditSink()
357
+ self._clock = clock
358
+
359
+ # -- public API ---------------------------------------------------------
360
+
361
+ def scan_tool(
362
+ self,
363
+ tool_name: str,
364
+ description: str,
365
+ schema: dict[str, Any] | None = None,
366
+ server_name: str = "unknown",
367
+ ) -> list[MCPThreat]:
368
+ """Scan a single MCP tool definition for threats.
369
+
370
+ Args:
371
+ tool_name: Name of the tool.
372
+ description: Tool description (primary attack surface).
373
+ schema: Optional JSON Schema for tool inputs.
374
+ server_name: Name of the MCP server providing this tool.
375
+
376
+ Returns:
377
+ List of ``MCPThreat`` findings (empty if clean).
378
+ """
379
+ try:
380
+ threats: list[MCPThreat] = []
381
+
382
+ threats.extend(self._check_hidden_instructions(description, tool_name, server_name))
383
+ threats.extend(self._check_description_injection(description, tool_name, server_name))
384
+ if schema is not None:
385
+ threats.extend(self._check_schema_abuse(schema, tool_name, server_name))
386
+ threats.extend(self._check_cross_server(tool_name, server_name))
387
+
388
+ rug_pull = self.check_rug_pull(tool_name, description, schema, server_name)
389
+ if rug_pull is not None:
390
+ threats.append(rug_pull)
391
+
392
+ self._record_audit("scan_tool", tool_name, server_name, threats)
393
+ self._metrics.record_scan(
394
+ operation="scan_tool",
395
+ tool_name=tool_name,
396
+ server_name=server_name,
397
+ )
398
+ self._metrics.record_threats_detected(
399
+ len(threats),
400
+ tool_name=tool_name,
401
+ server_name=server_name,
402
+ )
403
+ return threats
404
+ except Exception:
405
+ logger.error(
406
+ "MCP tool scan failed closed | tool=%s server=%s",
407
+ tool_name,
408
+ server_name,
409
+ exc_info=True,
410
+ )
411
+ return [
412
+ MCPThreat(
413
+ threat_type=MCPThreatType.TOOL_POISONING,
414
+ severity=MCPSeverity.CRITICAL,
415
+ tool_name=tool_name,
416
+ server_name=server_name,
417
+ message="Scan error \u2014 fail closed",
418
+ )
419
+ ]
420
+
421
+ def scan_server(
422
+ self,
423
+ server_name: str,
424
+ tools: list[dict[str, Any]],
425
+ ) -> ScanResult:
426
+ """Scan all tools from an MCP server.
427
+
428
+ Args:
429
+ server_name: Name of the MCP server.
430
+ tools: List of tool dicts with keys: ``name``, ``description``,
431
+ and optionally ``inputSchema``.
432
+
433
+ Returns:
434
+ Aggregate ``ScanResult``.
435
+ """
436
+ all_threats: list[MCPThreat] = []
437
+ flagged_tools: set[str] = set()
438
+
439
+ for tool in tools:
440
+ name = tool.get("name", "unknown")
441
+ description = tool.get("description", "")
442
+ schema = tool.get("inputSchema")
443
+ tool_threats = self.scan_tool(name, description, schema, server_name)
444
+ if tool_threats:
445
+ flagged_tools.add(name)
446
+ all_threats.extend(tool_threats)
447
+
448
+ self._metrics.record_scan(
449
+ operation="scan_server",
450
+ tool_name="*",
451
+ server_name=server_name,
452
+ )
453
+ return ScanResult(
454
+ safe=len(all_threats) == 0,
455
+ threats=all_threats,
456
+ tools_scanned=len(tools),
457
+ tools_flagged=len(flagged_tools),
458
+ )
459
+
460
+ def register_tool(
461
+ self,
462
+ tool_name: str,
463
+ description: str,
464
+ schema: dict[str, Any] | None,
465
+ server_name: str,
466
+ ) -> ToolFingerprint:
467
+ """Register a tool with a cryptographic fingerprint.
468
+
469
+ If already registered, updates last_seen and increments version
470
+ only when the definition changed.
471
+
472
+ Returns:
473
+ The ``ToolFingerprint`` for this tool.
474
+ """
475
+ key = f"{server_name}::{tool_name}"
476
+ now = self._clock()
477
+ desc_hash = hashlib.sha256(description.encode("utf-8")).hexdigest()
478
+ schema_hash = hashlib.sha256(
479
+ json.dumps(schema, sort_keys=True, default=str).encode("utf-8") if schema else b""
480
+ ).hexdigest()
481
+
482
+ existing = self._tool_registry.get(key)
483
+ if existing is not None:
484
+ if existing.description_hash != desc_hash or existing.schema_hash != schema_hash:
485
+ existing.description_hash = desc_hash
486
+ existing.schema_hash = schema_hash
487
+ existing.last_seen = now
488
+ existing.version += 1
489
+ else:
490
+ existing.last_seen = now
491
+ return existing
492
+
493
+ fp = ToolFingerprint(
494
+ tool_name=tool_name,
495
+ server_name=server_name,
496
+ description_hash=desc_hash,
497
+ schema_hash=schema_hash,
498
+ first_seen=now,
499
+ last_seen=now,
500
+ version=1,
501
+ )
502
+ self._tool_registry[key] = fp
503
+ return fp
504
+
505
+ def check_rug_pull(
506
+ self,
507
+ tool_name: str,
508
+ description: str,
509
+ schema: dict[str, Any] | None,
510
+ server_name: str,
511
+ ) -> MCPThreat | None:
512
+ """Check if a tool definition changed since registration (rug pull).
513
+
514
+ Returns:
515
+ An ``MCPThreat`` if a rug pull is detected, else ``None``.
516
+ """
517
+ key = f"{server_name}::{tool_name}"
518
+ existing = self._tool_registry.get(key)
519
+ if existing is None:
520
+ return None
521
+
522
+ desc_hash = hashlib.sha256(description.encode("utf-8")).hexdigest()
523
+ schema_hash = hashlib.sha256(
524
+ json.dumps(schema, sort_keys=True, default=str).encode("utf-8") if schema else b""
525
+ ).hexdigest()
526
+
527
+ changes: list[str] = []
528
+ if existing.description_hash != desc_hash:
529
+ changes.append("description")
530
+ if existing.schema_hash != schema_hash:
531
+ changes.append("schema")
532
+
533
+ if changes:
534
+ return MCPThreat(
535
+ threat_type=MCPThreatType.RUG_PULL,
536
+ severity=MCPSeverity.CRITICAL,
537
+ tool_name=tool_name,
538
+ server_name=server_name,
539
+ message=(
540
+ f"Tool definition changed since registration: "
541
+ f"{', '.join(changes)} modified (version {existing.version})"
542
+ ),
543
+ details={"changed_fields": changes, "version": existing.version},
544
+ )
545
+ return None
546
+
547
+ @property
548
+ def audit_log(self) -> list[dict[str, Any]]:
549
+ """Return a copy of the scan audit history."""
550
+ return list(self._audit_log)
551
+
552
+ # -- private detection methods ------------------------------------------
553
+
554
+ def _check_hidden_instructions(
555
+ self,
556
+ description: str,
557
+ tool_name: str,
558
+ server_name: str,
559
+ ) -> list[MCPThreat]:
560
+ """Detect hidden instructions in tool descriptions."""
561
+ threats: list[MCPThreat] = []
562
+
563
+ # 1. Invisible unicode characters
564
+ for pattern in _INVISIBLE_UNICODE_PATTERNS:
565
+ match = pattern.search(description)
566
+ if match:
567
+ threats.append(
568
+ MCPThreat(
569
+ threat_type=MCPThreatType.HIDDEN_INSTRUCTION,
570
+ severity=MCPSeverity.CRITICAL,
571
+ tool_name=tool_name,
572
+ server_name=server_name,
573
+ message="Invisible unicode characters detected in tool description",
574
+ matched_pattern=pattern.pattern,
575
+ details={"char_ord": ord(match.group()[0])},
576
+ )
577
+ )
578
+ break # one finding per category is enough
579
+
580
+ # 2. Markdown/HTML comments hiding text
581
+ for pattern in _HIDDEN_COMMENT_PATTERNS:
582
+ match = pattern.search(description)
583
+ if match:
584
+ threats.append(
585
+ MCPThreat(
586
+ threat_type=MCPThreatType.HIDDEN_INSTRUCTION,
587
+ severity=MCPSeverity.CRITICAL,
588
+ tool_name=tool_name,
589
+ server_name=server_name,
590
+ message="Hidden comment detected in tool description",
591
+ matched_pattern=pattern.pattern,
592
+ details={"comment_preview": match.group()[:80]},
593
+ )
594
+ )
595
+
596
+ # 3. Encoded instructions (base64, hex)
597
+ for pattern in _ENCODED_PAYLOAD_PATTERNS:
598
+ match = pattern.search(description)
599
+ if match:
600
+ candidate = match.group()
601
+ # For base64, try to decode and check for suspicious content
602
+ is_suspicious = False
603
+ if len(candidate) >= 40 and not candidate.startswith("\\x"):
604
+ try:
605
+ decoded = base64.b64decode(candidate).decode("utf-8", errors="ignore")
606
+ decoded_lower = decoded.lower()
607
+ for keyword in _SUSPICIOUS_DECODED_KEYWORDS:
608
+ if keyword in decoded_lower:
609
+ is_suspicious = True
610
+ break
611
+ except Exception:
612
+ pass
613
+ if not is_suspicious:
614
+ # Long base64 in a tool description is suspicious regardless
615
+ is_suspicious = True
616
+
617
+ if is_suspicious or candidate.startswith("\\x"):
618
+ threats.append(
619
+ MCPThreat(
620
+ threat_type=MCPThreatType.HIDDEN_INSTRUCTION,
621
+ severity=MCPSeverity.WARNING,
622
+ tool_name=tool_name,
623
+ server_name=server_name,
624
+ message="Encoded payload detected in tool description",
625
+ matched_pattern=pattern.pattern,
626
+ )
627
+ )
628
+
629
+ # 4. Hidden instructions after excessive whitespace/newlines
630
+ if _EXCESSIVE_WHITESPACE_PATTERN.search(description):
631
+ threats.append(
632
+ MCPThreat(
633
+ threat_type=MCPThreatType.HIDDEN_INSTRUCTION,
634
+ severity=MCPSeverity.WARNING,
635
+ tool_name=tool_name,
636
+ server_name=server_name,
637
+ message="Instructions hidden after excessive whitespace",
638
+ matched_pattern=_EXCESSIVE_WHITESPACE_PATTERN.pattern,
639
+ )
640
+ )
641
+
642
+ # 5. Instruction-like patterns
643
+ for pattern in _HIDDEN_INSTRUCTION_PATTERNS:
644
+ if pattern.search(description):
645
+ threats.append(
646
+ MCPThreat(
647
+ threat_type=MCPThreatType.HIDDEN_INSTRUCTION,
648
+ severity=MCPSeverity.CRITICAL,
649
+ tool_name=tool_name,
650
+ server_name=server_name,
651
+ message=f"Instruction-like pattern in tool description: {pattern.pattern}",
652
+ matched_pattern=pattern.pattern,
653
+ )
654
+ )
655
+
656
+ return threats
657
+
658
+ def _check_description_injection(
659
+ self,
660
+ description: str,
661
+ tool_name: str,
662
+ server_name: str,
663
+ ) -> list[MCPThreat]:
664
+ """Detect prompt injection patterns in tool descriptions."""
665
+ threats: list[MCPThreat] = []
666
+
667
+ # Reuse prompt_injection.py detector
668
+ result = self._injection_detector.detect(
669
+ description, source=f"mcp:{server_name}:{tool_name}"
670
+ )
671
+ if result.is_injection:
672
+ threats.append(
673
+ MCPThreat(
674
+ threat_type=MCPThreatType.DESCRIPTION_INJECTION,
675
+ severity=MCPSeverity.CRITICAL,
676
+ tool_name=tool_name,
677
+ server_name=server_name,
678
+ message=f"Prompt injection detected in description: {result.explanation}",
679
+ matched_pattern=result.matched_patterns[0] if result.matched_patterns else None,
680
+ details={
681
+ "injection_type": result.injection_type.value
682
+ if result.injection_type
683
+ else None
684
+ },
685
+ )
686
+ )
687
+
688
+ # Role assignment patterns
689
+ for pattern in _ROLE_OVERRIDE_PATTERNS:
690
+ if pattern.search(description):
691
+ threats.append(
692
+ MCPThreat(
693
+ threat_type=MCPThreatType.DESCRIPTION_INJECTION,
694
+ severity=MCPSeverity.WARNING,
695
+ tool_name=tool_name,
696
+ server_name=server_name,
697
+ message=f"Role override pattern in description: {pattern.pattern}",
698
+ matched_pattern=pattern.pattern,
699
+ )
700
+ )
701
+
702
+ # Data exfiltration patterns
703
+ for pattern in _EXFILTRATION_PATTERNS:
704
+ if pattern.search(description):
705
+ threats.append(
706
+ MCPThreat(
707
+ threat_type=MCPThreatType.DESCRIPTION_INJECTION,
708
+ severity=MCPSeverity.CRITICAL,
709
+ tool_name=tool_name,
710
+ server_name=server_name,
711
+ message=f"Data exfiltration pattern in description: {pattern.pattern}",
712
+ matched_pattern=pattern.pattern,
713
+ )
714
+ )
715
+
716
+ return threats
717
+
718
+ def _check_schema_abuse(
719
+ self,
720
+ schema: dict[str, Any],
721
+ tool_name: str,
722
+ server_name: str,
723
+ ) -> list[MCPThreat]:
724
+ """Check tool input schemas for suspicious patterns."""
725
+ threats: list[MCPThreat] = []
726
+
727
+ # 1. Overly permissive: top-level type is "object" with no properties
728
+ if schema.get("type") == "object" and not schema.get("properties"):
729
+ if schema.get("additionalProperties") is not False:
730
+ threats.append(
731
+ MCPThreat(
732
+ threat_type=MCPThreatType.TOOL_POISONING,
733
+ severity=MCPSeverity.WARNING,
734
+ tool_name=tool_name,
735
+ server_name=server_name,
736
+ message="Overly permissive schema: object type with no defined properties",
737
+ )
738
+ )
739
+
740
+ properties = schema.get("properties", {})
741
+ required = schema.get("required", [])
742
+
743
+ for prop_name, prop_def in properties.items():
744
+ if not isinstance(prop_def, dict):
745
+ continue
746
+
747
+ # 2. Hidden required fields with suspicious names
748
+ suspicious_field_names = [
749
+ "system_prompt",
750
+ "instructions",
751
+ "override",
752
+ "command",
753
+ "exec",
754
+ "eval",
755
+ "callback_url",
756
+ "webhook",
757
+ "target_url",
758
+ ]
759
+ if prop_name in required:
760
+ for sus_name in suspicious_field_names:
761
+ if sus_name in prop_name.lower():
762
+ threats.append(
763
+ MCPThreat(
764
+ threat_type=MCPThreatType.TOOL_POISONING,
765
+ severity=MCPSeverity.CRITICAL,
766
+ tool_name=tool_name,
767
+ server_name=server_name,
768
+ message=f"Suspicious required field: '{prop_name}'",
769
+ details={"field_name": prop_name},
770
+ )
771
+ )
772
+
773
+ # 3. Default values containing instructions
774
+ default_val = prop_def.get("default")
775
+ if isinstance(default_val, str) and len(default_val) > 10:
776
+ for pattern in _HIDDEN_INSTRUCTION_PATTERNS:
777
+ if pattern.search(default_val):
778
+ threats.append(
779
+ MCPThreat(
780
+ threat_type=MCPThreatType.TOOL_POISONING,
781
+ severity=MCPSeverity.CRITICAL,
782
+ tool_name=tool_name,
783
+ server_name=server_name,
784
+ message=f"Instruction in default value for field '{prop_name}'",
785
+ matched_pattern=pattern.pattern,
786
+ details={"field_name": prop_name},
787
+ )
788
+ )
789
+ break
790
+
791
+ # 4. Hidden instructions in property descriptions
792
+ prop_desc = prop_def.get("description", "")
793
+ if isinstance(prop_desc, str):
794
+ for pattern in _HIDDEN_INSTRUCTION_PATTERNS:
795
+ if pattern.search(prop_desc):
796
+ threats.append(
797
+ MCPThreat(
798
+ threat_type=MCPThreatType.TOOL_POISONING,
799
+ severity=MCPSeverity.CRITICAL,
800
+ tool_name=tool_name,
801
+ server_name=server_name,
802
+ message=f"Hidden instruction in property '{prop_name}' description",
803
+ matched_pattern=pattern.pattern,
804
+ details={"field_name": prop_name},
805
+ )
806
+ )
807
+ break
808
+
809
+ return threats
810
+
811
+ def _check_cross_server(
812
+ self,
813
+ tool_name: str,
814
+ server_name: str,
815
+ ) -> list[MCPThreat]:
816
+ """Check for cross-server attack patterns."""
817
+ threats: list[MCPThreat] = []
818
+
819
+ for _key, fp in self._tool_registry.items():
820
+ # Same tool name from a different server
821
+ if fp.tool_name == tool_name and fp.server_name != server_name:
822
+ threats.append(
823
+ MCPThreat(
824
+ threat_type=MCPThreatType.CROSS_SERVER_ATTACK,
825
+ severity=MCPSeverity.CRITICAL,
826
+ tool_name=tool_name,
827
+ server_name=server_name,
828
+ message=(
829
+ f"Tool '{tool_name}' already registered from server "
830
+ f"'{fp.server_name}' — potential impersonation"
831
+ ),
832
+ details={"original_server": fp.server_name},
833
+ )
834
+ )
835
+
836
+ # Typosquatting: similar name from a different server
837
+ if fp.server_name != server_name and fp.tool_name != tool_name:
838
+ if self._is_typosquat(tool_name, fp.tool_name):
839
+ threats.append(
840
+ MCPThreat(
841
+ threat_type=MCPThreatType.CROSS_SERVER_ATTACK,
842
+ severity=MCPSeverity.WARNING,
843
+ tool_name=tool_name,
844
+ server_name=server_name,
845
+ message=(
846
+ f"Tool name '{tool_name}' resembles "
847
+ f"'{fp.tool_name}' from server '{fp.server_name}' "
848
+ f"— potential typosquatting"
849
+ ),
850
+ details={
851
+ "similar_tool": fp.tool_name,
852
+ "similar_server": fp.server_name,
853
+ },
854
+ )
855
+ )
856
+
857
+ return threats
858
+
859
+ # -- helpers ------------------------------------------------------------
860
+
861
+ @staticmethod
862
+ def _is_typosquat(name_a: str, name_b: str) -> bool:
863
+ """Check if two tool names are suspiciously similar (edit distance ≤ 2)."""
864
+ if name_a == name_b:
865
+ return False
866
+ # Simple Levenshtein check for short names
867
+ la, lb = name_a.lower(), name_b.lower()
868
+ if abs(len(la) - len(lb)) > 2:
869
+ return False
870
+ # Compute Levenshtein distance
871
+ dist = _levenshtein(la, lb)
872
+ # Typosquat if 1-2 edits on names of length ≥ 4
873
+ return 1 <= dist <= 2 and min(len(la), len(lb)) >= 4
874
+
875
+ def _record_audit(
876
+ self,
877
+ action: str,
878
+ tool_name: str,
879
+ server_name: str,
880
+ threats: list[MCPThreat],
881
+ ) -> None:
882
+ record = {
883
+ "timestamp": datetime.fromtimestamp(
884
+ self._clock(),
885
+ timezone.utc,
886
+ ).isoformat(),
887
+ "action": action,
888
+ "tool_name": tool_name,
889
+ "server_name": server_name,
890
+ "threats_found": len(threats),
891
+ "threat_types": [t.threat_type.value for t in threats],
892
+ }
893
+ self._audit_log.append(record)
894
+ self._audit_sink.record(record)
895
+
896
+ if threats:
897
+ logger.warning(
898
+ "MCP scan found %d threat(s) | tool=%s server=%s",
899
+ len(threats),
900
+ tool_name,
901
+ server_name,
902
+ )
903
+ else:
904
+ logger.debug(
905
+ "MCP scan clean | tool=%s server=%s",
906
+ tool_name,
907
+ server_name,
908
+ )
909
+
910
+
911
+ def _levenshtein(s: str, t: str) -> int:
912
+ """Compute Levenshtein edit distance between two strings."""
913
+ if len(s) < len(t):
914
+ return _levenshtein(t, s)
915
+ if len(t) == 0:
916
+ return len(s)
917
+ prev = list(range(len(t) + 1))
918
+ for i, cs in enumerate(s):
919
+ curr = [i + 1]
920
+ for j, ct in enumerate(t):
921
+ cost = 0 if cs == ct else 1
922
+ curr.append(min(curr[j] + 1, prev[j + 1] + 1, prev[j] + cost))
923
+ prev = curr
924
+ return prev[-1]