foundry-mcp 0.8.22__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.
Potentially problematic release.
This version of foundry-mcp might be problematic. Click here for more details.
- foundry_mcp/__init__.py +13 -0
- foundry_mcp/cli/__init__.py +67 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +640 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +667 -0
- foundry_mcp/cli/commands/session.py +472 -0
- foundry_mcp/cli/commands/specs.py +686 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +298 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +1454 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1773 -0
- foundry_mcp/core/batch_operations.py +1202 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/journal.py +700 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1376 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +146 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +387 -0
- foundry_mcp/core/prometheus.py +564 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +691 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
- foundry_mcp/core/prompts/plan_review.py +627 -0
- foundry_mcp/core/providers/__init__.py +237 -0
- foundry_mcp/core/providers/base.py +515 -0
- foundry_mcp/core/providers/claude.py +472 -0
- foundry_mcp/core/providers/codex.py +637 -0
- foundry_mcp/core/providers/cursor_agent.py +630 -0
- foundry_mcp/core/providers/detectors.py +515 -0
- foundry_mcp/core/providers/gemini.py +426 -0
- foundry_mcp/core/providers/opencode.py +718 -0
- foundry_mcp/core/providers/opencode_wrapper.js +308 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +857 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1234 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +25 -0
- foundry_mcp/core/research/workflows/base.py +298 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +539 -0
- foundry_mcp/core/research/workflows/deep_research.py +4142 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +1624 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +4119 -0
- foundry_mcp/core/task.py +2463 -0
- foundry_mcp/core/testing.py +839 -0
- foundry_mcp/core/validation.py +2357 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +177 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +300 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +164 -0
- foundry_mcp/dashboard/views/overview.py +96 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +414 -0
- foundry_mcp/server.py +150 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +92 -0
- foundry_mcp/tools/unified/authoring.py +3620 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +268 -0
- foundry_mcp/tools/unified/environment.py +1341 -0
- foundry_mcp/tools/unified/error.py +479 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +640 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +876 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +589 -0
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +1042 -0
- foundry_mcp/tools/unified/review_helpers.py +314 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +565 -0
- foundry_mcp/tools/unified/spec.py +1283 -0
- foundry_mcp/tools/unified/task.py +3846 -0
- foundry_mcp/tools/unified/test.py +431 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.8.22.dist-info/METADATA +344 -0
- foundry_mcp-0.8.22.dist-info/RECORD +153 -0
- foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
- foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security utilities for foundry-mcp.
|
|
3
|
+
|
|
4
|
+
Provides input validation, size limits, and prompt injection defense
|
|
5
|
+
for MCP tools. See docs/mcp_best_practices/04-validation-input-hygiene.md
|
|
6
|
+
and docs/mcp_best_practices/08-security-trust-boundaries.md for guidance.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Final, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Input Size Limits
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# These constants define maximum sizes for various input types to prevent
|
|
20
|
+
# resource exhaustion and denial-of-service attacks. Adjust based on your
|
|
21
|
+
# specific requirements, but be conservative.
|
|
22
|
+
|
|
23
|
+
MAX_INPUT_SIZE: Final[int] = 100_000
|
|
24
|
+
"""Maximum total input payload size in bytes (100KB).
|
|
25
|
+
|
|
26
|
+
Use this to validate the overall size of request payloads before processing.
|
|
27
|
+
Prevents memory exhaustion from oversized requests.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
MAX_ARRAY_LENGTH: Final[int] = 1_000
|
|
31
|
+
"""Maximum number of items in array/list inputs.
|
|
32
|
+
|
|
33
|
+
Use this to limit iteration counts and prevent algorithmic complexity attacks.
|
|
34
|
+
Arrays larger than this should be paginated or streamed.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
MAX_STRING_LENGTH: Final[int] = 10_000
|
|
38
|
+
"""Maximum length for individual string fields (10K characters).
|
|
39
|
+
|
|
40
|
+
Use this for text fields like descriptions, content, etc.
|
|
41
|
+
Longer content should use dedicated file/blob handling.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
MAX_NESTED_DEPTH: Final[int] = 10
|
|
45
|
+
"""Maximum nesting depth for JSON structures.
|
|
46
|
+
|
|
47
|
+
Prevents stack overflow from deeply nested payloads.
|
|
48
|
+
Most legitimate use cases require < 5 levels of nesting.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
MAX_FIELD_COUNT: Final[int] = 100
|
|
52
|
+
"""Maximum number of fields in an object/dict.
|
|
53
|
+
|
|
54
|
+
Prevents resource exhaustion from objects with excessive properties.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# =============================================================================
|
|
58
|
+
# Prompt Injection Detection Patterns
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# These regex patterns detect common prompt injection attempts in LLM-generated
|
|
61
|
+
# input. MCP tools receiving untrusted input should check against these patterns.
|
|
62
|
+
# See docs/mcp_best_practices/08-security-trust-boundaries.md for details.
|
|
63
|
+
|
|
64
|
+
INJECTION_PATTERNS: Final[list[str]] = [
|
|
65
|
+
# Instruction override attempts
|
|
66
|
+
r"ignore\s+(all\s+)?(previous|prior)\s+(instructions?|prompts?)",
|
|
67
|
+
r"disregard\s+(all\s+)?(previous|prior|above)",
|
|
68
|
+
r"forget\s+(everything|all)\s+(above|before)",
|
|
69
|
+
r"new\s+instructions?\s*:",
|
|
70
|
+
|
|
71
|
+
# System prompt injection
|
|
72
|
+
r"system\s*:\s*",
|
|
73
|
+
r"<\s*system\s*>",
|
|
74
|
+
|
|
75
|
+
# Special tokens (model-specific)
|
|
76
|
+
r"<\|.*?\|>", # OpenAI-style special tokens
|
|
77
|
+
r"\[INST\]|\[/INST\]", # Llama instruction markers
|
|
78
|
+
r"<\|im_start\|>|<\|im_end\|>", # ChatML markers
|
|
79
|
+
r"<<SYS>>|<</SYS>>", # Llama system markers
|
|
80
|
+
|
|
81
|
+
# Code block injection attempts
|
|
82
|
+
r"```system",
|
|
83
|
+
r"```\s*<\s*system",
|
|
84
|
+
|
|
85
|
+
# Role injection
|
|
86
|
+
r"^(assistant|user|system)\s*:",
|
|
87
|
+
]
|
|
88
|
+
"""Regex patterns for detecting prompt injection attempts.
|
|
89
|
+
|
|
90
|
+
Each pattern targets a specific injection technique:
|
|
91
|
+
- Instruction overrides: attempts to ignore/discard previous context
|
|
92
|
+
- System prompt injection: attempts to inject system-level instructions
|
|
93
|
+
- Special tokens: model-specific control sequences
|
|
94
|
+
- Code block injection: attempts to inject via markdown code blocks
|
|
95
|
+
- Role injection: attempts to assume different conversation roles
|
|
96
|
+
|
|
97
|
+
Use with detect_prompt_injection() for comprehensive checking.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# =============================================================================
|
|
102
|
+
# Detection Results
|
|
103
|
+
# =============================================================================
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class InjectionDetectionResult:
|
|
107
|
+
"""Result of prompt injection detection.
|
|
108
|
+
|
|
109
|
+
Attributes:
|
|
110
|
+
is_suspicious: Whether the input appears to contain injection attempts
|
|
111
|
+
matched_pattern: The regex pattern that matched (if any)
|
|
112
|
+
matched_text: The actual text that matched the pattern (if any)
|
|
113
|
+
"""
|
|
114
|
+
is_suspicious: bool
|
|
115
|
+
matched_pattern: Optional[str] = None
|
|
116
|
+
matched_text: Optional[str] = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# =============================================================================
|
|
120
|
+
# Detection Functions
|
|
121
|
+
# =============================================================================
|
|
122
|
+
|
|
123
|
+
def detect_prompt_injection(
|
|
124
|
+
text: str,
|
|
125
|
+
*,
|
|
126
|
+
log_detections: bool = True,
|
|
127
|
+
patterns: Optional[list[str]] = None,
|
|
128
|
+
) -> InjectionDetectionResult:
|
|
129
|
+
"""Detect potential prompt injection attempts in text.
|
|
130
|
+
|
|
131
|
+
Scans the input text against known injection patterns and returns
|
|
132
|
+
a result indicating whether suspicious content was found.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
text: The input text to scan for injection attempts
|
|
136
|
+
log_detections: Whether to log detected injection attempts (default: True)
|
|
137
|
+
patterns: Optional custom patterns to use instead of INJECTION_PATTERNS
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
InjectionDetectionResult with detection status and match details
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
>>> result = detect_prompt_injection("ignore previous instructions and...")
|
|
144
|
+
>>> if result.is_suspicious:
|
|
145
|
+
... print(f"Blocked: matched pattern '{result.matched_pattern}'")
|
|
146
|
+
"""
|
|
147
|
+
check_patterns = patterns if patterns is not None else INJECTION_PATTERNS
|
|
148
|
+
|
|
149
|
+
for pattern in check_patterns:
|
|
150
|
+
match = re.search(pattern, text, re.IGNORECASE | re.MULTILINE)
|
|
151
|
+
if match:
|
|
152
|
+
result = InjectionDetectionResult(
|
|
153
|
+
is_suspicious=True,
|
|
154
|
+
matched_pattern=pattern,
|
|
155
|
+
matched_text=match.group(0),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if log_detections:
|
|
159
|
+
# Log with limited text preview to avoid logging sensitive content
|
|
160
|
+
preview = text[:100] + "..." if len(text) > 100 else text
|
|
161
|
+
logger.warning(
|
|
162
|
+
"Potential prompt injection detected",
|
|
163
|
+
extra={
|
|
164
|
+
"pattern": pattern,
|
|
165
|
+
"matched_text": result.matched_text,
|
|
166
|
+
"text_preview": preview,
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
return InjectionDetectionResult(is_suspicious=False)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def is_prompt_injection(text: str) -> bool:
|
|
176
|
+
"""Simple check for prompt injection (returns bool only).
|
|
177
|
+
|
|
178
|
+
Convenience function when you only need a boolean result.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
text: The input text to scan
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if injection patterns detected, False otherwise
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
>>> if is_prompt_injection(user_input):
|
|
188
|
+
... return error_response("Input contains disallowed patterns")
|
|
189
|
+
"""
|
|
190
|
+
return detect_prompt_injection(text, log_detections=False).is_suspicious
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# =============================================================================
|
|
194
|
+
# Size Validation Functions
|
|
195
|
+
# =============================================================================
|
|
196
|
+
|
|
197
|
+
@dataclass
|
|
198
|
+
class SizeValidationResult:
|
|
199
|
+
"""Result of input size validation.
|
|
200
|
+
|
|
201
|
+
Attributes:
|
|
202
|
+
is_valid: Whether all size checks passed
|
|
203
|
+
violations: List of (field_name, violation_message) tuples
|
|
204
|
+
"""
|
|
205
|
+
is_valid: bool
|
|
206
|
+
violations: list[Tuple[str, str]]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def validate_size(
|
|
210
|
+
value: Any,
|
|
211
|
+
field_name: str = "input",
|
|
212
|
+
*,
|
|
213
|
+
max_size: Optional[int] = None,
|
|
214
|
+
max_length: Optional[int] = None,
|
|
215
|
+
max_string_length: Optional[int] = None,
|
|
216
|
+
) -> SizeValidationResult:
|
|
217
|
+
"""Validate size constraints on a value.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
value: The value to validate
|
|
221
|
+
field_name: Name of the field (for error messages)
|
|
222
|
+
max_size: Maximum byte size for serialized value (default: MAX_INPUT_SIZE)
|
|
223
|
+
max_length: Maximum length for arrays/lists (default: MAX_ARRAY_LENGTH)
|
|
224
|
+
max_string_length: Maximum length for strings (default: MAX_STRING_LENGTH)
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
SizeValidationResult with validation status and any violations
|
|
228
|
+
"""
|
|
229
|
+
import json
|
|
230
|
+
|
|
231
|
+
violations = []
|
|
232
|
+
|
|
233
|
+
# Check serialized size
|
|
234
|
+
effective_max_size = max_size if max_size is not None else MAX_INPUT_SIZE
|
|
235
|
+
try:
|
|
236
|
+
serialized = json.dumps(value) if not isinstance(value, str) else value
|
|
237
|
+
if len(serialized.encode('utf-8')) > effective_max_size:
|
|
238
|
+
violations.append((
|
|
239
|
+
field_name,
|
|
240
|
+
f"Exceeds maximum size ({effective_max_size} bytes)"
|
|
241
|
+
))
|
|
242
|
+
except (TypeError, ValueError):
|
|
243
|
+
pass # Can't serialize, skip size check
|
|
244
|
+
|
|
245
|
+
# Check array length
|
|
246
|
+
effective_max_length = max_length if max_length is not None else MAX_ARRAY_LENGTH
|
|
247
|
+
if isinstance(value, (list, tuple)):
|
|
248
|
+
if len(value) > effective_max_length:
|
|
249
|
+
violations.append((
|
|
250
|
+
field_name,
|
|
251
|
+
f"Array exceeds maximum length ({effective_max_length} items)"
|
|
252
|
+
))
|
|
253
|
+
|
|
254
|
+
# Check string length
|
|
255
|
+
effective_max_string = max_string_length if max_string_length is not None else MAX_STRING_LENGTH
|
|
256
|
+
if isinstance(value, str):
|
|
257
|
+
if len(value) > effective_max_string:
|
|
258
|
+
violations.append((
|
|
259
|
+
field_name,
|
|
260
|
+
f"String exceeds maximum length ({effective_max_string} characters)"
|
|
261
|
+
))
|
|
262
|
+
|
|
263
|
+
return SizeValidationResult(
|
|
264
|
+
is_valid=len(violations) == 0,
|
|
265
|
+
violations=violations,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# =============================================================================
|
|
270
|
+
# Validation Decorators
|
|
271
|
+
# =============================================================================
|
|
272
|
+
|
|
273
|
+
def validate_input_size(
|
|
274
|
+
*,
|
|
275
|
+
max_size: Optional[int] = None,
|
|
276
|
+
max_array_length: Optional[int] = None,
|
|
277
|
+
max_string_length: Optional[int] = None,
|
|
278
|
+
check_injection: bool = False,
|
|
279
|
+
):
|
|
280
|
+
"""Decorator to validate input size limits on tool parameters.
|
|
281
|
+
|
|
282
|
+
Validates all string and collection parameters against size limits
|
|
283
|
+
before the function executes. Returns an error response if validation fails.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
max_size: Maximum total input size in bytes (default: MAX_INPUT_SIZE)
|
|
287
|
+
max_array_length: Maximum array/list length (default: MAX_ARRAY_LENGTH)
|
|
288
|
+
max_string_length: Maximum string length (default: MAX_STRING_LENGTH)
|
|
289
|
+
check_injection: Also check for prompt injection patterns (default: False)
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Decorator function
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
@mcp.tool()
|
|
296
|
+
@validate_input_size(max_string_length=5000, check_injection=True)
|
|
297
|
+
def process_text(text: str, items: list) -> dict:
|
|
298
|
+
# Parameters are validated before this runs
|
|
299
|
+
return {"result": process(text)}
|
|
300
|
+
|
|
301
|
+
Note:
|
|
302
|
+
This decorator should be applied AFTER @mcp.tool() to ensure
|
|
303
|
+
validation runs before the tool handler.
|
|
304
|
+
"""
|
|
305
|
+
import functools
|
|
306
|
+
from dataclasses import asdict
|
|
307
|
+
|
|
308
|
+
def decorator(func):
|
|
309
|
+
@functools.wraps(func)
|
|
310
|
+
def wrapper(*args, **kwargs):
|
|
311
|
+
all_violations = []
|
|
312
|
+
|
|
313
|
+
# Validate keyword arguments
|
|
314
|
+
for name, value in kwargs.items():
|
|
315
|
+
result = validate_size(
|
|
316
|
+
value,
|
|
317
|
+
field_name=name,
|
|
318
|
+
max_size=max_size,
|
|
319
|
+
max_length=max_array_length,
|
|
320
|
+
max_string_length=max_string_length,
|
|
321
|
+
)
|
|
322
|
+
all_violations.extend(result.violations)
|
|
323
|
+
|
|
324
|
+
# Check for injection if enabled and value is string
|
|
325
|
+
if check_injection and isinstance(value, str):
|
|
326
|
+
injection_result = detect_prompt_injection(value)
|
|
327
|
+
if injection_result.is_suspicious:
|
|
328
|
+
all_violations.append((
|
|
329
|
+
name,
|
|
330
|
+
f"Contains disallowed patterns: {injection_result.matched_text}"
|
|
331
|
+
))
|
|
332
|
+
|
|
333
|
+
if all_violations:
|
|
334
|
+
# Import here to avoid circular dependency
|
|
335
|
+
try:
|
|
336
|
+
from foundry_mcp.core.responses import error_response
|
|
337
|
+
return asdict(error_response(
|
|
338
|
+
"Input validation failed",
|
|
339
|
+
error_code="VALIDATION_ERROR",
|
|
340
|
+
details={
|
|
341
|
+
"validation_errors": [
|
|
342
|
+
{"field": field, "message": msg}
|
|
343
|
+
for field, msg in all_violations
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
))
|
|
347
|
+
except ImportError:
|
|
348
|
+
# Fallback if responses module not available
|
|
349
|
+
return {
|
|
350
|
+
"success": False,
|
|
351
|
+
"error": "Input validation failed",
|
|
352
|
+
"data": {
|
|
353
|
+
"validation_errors": [
|
|
354
|
+
{"field": field, "message": msg}
|
|
355
|
+
for field, msg in all_violations
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return func(*args, **kwargs)
|
|
361
|
+
|
|
362
|
+
# Handle async functions
|
|
363
|
+
@functools.wraps(func)
|
|
364
|
+
async def async_wrapper(*args, **kwargs):
|
|
365
|
+
all_violations = []
|
|
366
|
+
|
|
367
|
+
for name, value in kwargs.items():
|
|
368
|
+
result = validate_size(
|
|
369
|
+
value,
|
|
370
|
+
field_name=name,
|
|
371
|
+
max_size=max_size,
|
|
372
|
+
max_length=max_array_length,
|
|
373
|
+
max_string_length=max_string_length,
|
|
374
|
+
)
|
|
375
|
+
all_violations.extend(result.violations)
|
|
376
|
+
|
|
377
|
+
if check_injection and isinstance(value, str):
|
|
378
|
+
injection_result = detect_prompt_injection(value)
|
|
379
|
+
if injection_result.is_suspicious:
|
|
380
|
+
all_violations.append((
|
|
381
|
+
name,
|
|
382
|
+
f"Contains disallowed patterns: {injection_result.matched_text}"
|
|
383
|
+
))
|
|
384
|
+
|
|
385
|
+
if all_violations:
|
|
386
|
+
try:
|
|
387
|
+
from foundry_mcp.core.responses import error_response
|
|
388
|
+
return asdict(error_response(
|
|
389
|
+
"Input validation failed",
|
|
390
|
+
error_code="VALIDATION_ERROR",
|
|
391
|
+
details={
|
|
392
|
+
"validation_errors": [
|
|
393
|
+
{"field": field, "message": msg}
|
|
394
|
+
for field, msg in all_violations
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
))
|
|
398
|
+
except ImportError:
|
|
399
|
+
return {
|
|
400
|
+
"success": False,
|
|
401
|
+
"error": "Input validation failed",
|
|
402
|
+
"data": {
|
|
403
|
+
"validation_errors": [
|
|
404
|
+
{"field": field, "message": msg}
|
|
405
|
+
for field, msg in all_violations
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return await func(*args, **kwargs)
|
|
411
|
+
|
|
412
|
+
import asyncio
|
|
413
|
+
if asyncio.iscoroutinefunction(func):
|
|
414
|
+
return async_wrapper
|
|
415
|
+
return wrapper
|
|
416
|
+
|
|
417
|
+
return decorator
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# Export all constants and functions
|
|
421
|
+
__all__ = [
|
|
422
|
+
# Constants
|
|
423
|
+
"MAX_INPUT_SIZE",
|
|
424
|
+
"MAX_ARRAY_LENGTH",
|
|
425
|
+
"MAX_STRING_LENGTH",
|
|
426
|
+
"MAX_NESTED_DEPTH",
|
|
427
|
+
"MAX_FIELD_COUNT",
|
|
428
|
+
"INJECTION_PATTERNS",
|
|
429
|
+
# Types
|
|
430
|
+
"InjectionDetectionResult",
|
|
431
|
+
"SizeValidationResult",
|
|
432
|
+
# Functions
|
|
433
|
+
"detect_prompt_injection",
|
|
434
|
+
"is_prompt_injection",
|
|
435
|
+
"validate_size",
|
|
436
|
+
# Decorators
|
|
437
|
+
"validate_input_size",
|
|
438
|
+
]
|