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.

Files changed (153) hide show
  1. foundry_mcp/__init__.py +13 -0
  2. foundry_mcp/cli/__init__.py +67 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +640 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +667 -0
  15. foundry_mcp/cli/commands/session.py +472 -0
  16. foundry_mcp/cli/commands/specs.py +686 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +298 -0
  22. foundry_mcp/cli/logging.py +212 -0
  23. foundry_mcp/cli/main.py +44 -0
  24. foundry_mcp/cli/output.py +122 -0
  25. foundry_mcp/cli/registry.py +110 -0
  26. foundry_mcp/cli/resilience.py +178 -0
  27. foundry_mcp/cli/transcript.py +217 -0
  28. foundry_mcp/config.py +1454 -0
  29. foundry_mcp/core/__init__.py +144 -0
  30. foundry_mcp/core/ai_consultation.py +1773 -0
  31. foundry_mcp/core/batch_operations.py +1202 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/health.py +749 -0
  40. foundry_mcp/core/intake.py +933 -0
  41. foundry_mcp/core/journal.py +700 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1376 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +146 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +387 -0
  57. foundry_mcp/core/prometheus.py +564 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +691 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
  61. foundry_mcp/core/prompts/plan_review.py +627 -0
  62. foundry_mcp/core/providers/__init__.py +237 -0
  63. foundry_mcp/core/providers/base.py +515 -0
  64. foundry_mcp/core/providers/claude.py +472 -0
  65. foundry_mcp/core/providers/codex.py +637 -0
  66. foundry_mcp/core/providers/cursor_agent.py +630 -0
  67. foundry_mcp/core/providers/detectors.py +515 -0
  68. foundry_mcp/core/providers/gemini.py +426 -0
  69. foundry_mcp/core/providers/opencode.py +718 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +308 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +857 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/research/__init__.py +68 -0
  78. foundry_mcp/core/research/memory.py +528 -0
  79. foundry_mcp/core/research/models.py +1234 -0
  80. foundry_mcp/core/research/providers/__init__.py +40 -0
  81. foundry_mcp/core/research/providers/base.py +242 -0
  82. foundry_mcp/core/research/providers/google.py +507 -0
  83. foundry_mcp/core/research/providers/perplexity.py +442 -0
  84. foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
  85. foundry_mcp/core/research/providers/tavily.py +383 -0
  86. foundry_mcp/core/research/workflows/__init__.py +25 -0
  87. foundry_mcp/core/research/workflows/base.py +298 -0
  88. foundry_mcp/core/research/workflows/chat.py +271 -0
  89. foundry_mcp/core/research/workflows/consensus.py +539 -0
  90. foundry_mcp/core/research/workflows/deep_research.py +4142 -0
  91. foundry_mcp/core/research/workflows/ideate.py +682 -0
  92. foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
  93. foundry_mcp/core/resilience.py +600 -0
  94. foundry_mcp/core/responses.py +1624 -0
  95. foundry_mcp/core/review.py +366 -0
  96. foundry_mcp/core/security.py +438 -0
  97. foundry_mcp/core/spec.py +4119 -0
  98. foundry_mcp/core/task.py +2463 -0
  99. foundry_mcp/core/testing.py +839 -0
  100. foundry_mcp/core/validation.py +2357 -0
  101. foundry_mcp/dashboard/__init__.py +32 -0
  102. foundry_mcp/dashboard/app.py +119 -0
  103. foundry_mcp/dashboard/components/__init__.py +17 -0
  104. foundry_mcp/dashboard/components/cards.py +88 -0
  105. foundry_mcp/dashboard/components/charts.py +177 -0
  106. foundry_mcp/dashboard/components/filters.py +136 -0
  107. foundry_mcp/dashboard/components/tables.py +195 -0
  108. foundry_mcp/dashboard/data/__init__.py +11 -0
  109. foundry_mcp/dashboard/data/stores.py +433 -0
  110. foundry_mcp/dashboard/launcher.py +300 -0
  111. foundry_mcp/dashboard/views/__init__.py +12 -0
  112. foundry_mcp/dashboard/views/errors.py +217 -0
  113. foundry_mcp/dashboard/views/metrics.py +164 -0
  114. foundry_mcp/dashboard/views/overview.py +96 -0
  115. foundry_mcp/dashboard/views/providers.py +83 -0
  116. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  117. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  118. foundry_mcp/prompts/__init__.py +9 -0
  119. foundry_mcp/prompts/workflows.py +525 -0
  120. foundry_mcp/resources/__init__.py +9 -0
  121. foundry_mcp/resources/specs.py +591 -0
  122. foundry_mcp/schemas/__init__.py +38 -0
  123. foundry_mcp/schemas/intake-schema.json +89 -0
  124. foundry_mcp/schemas/sdd-spec-schema.json +414 -0
  125. foundry_mcp/server.py +150 -0
  126. foundry_mcp/tools/__init__.py +10 -0
  127. foundry_mcp/tools/unified/__init__.py +92 -0
  128. foundry_mcp/tools/unified/authoring.py +3620 -0
  129. foundry_mcp/tools/unified/context_helpers.py +98 -0
  130. foundry_mcp/tools/unified/documentation_helpers.py +268 -0
  131. foundry_mcp/tools/unified/environment.py +1341 -0
  132. foundry_mcp/tools/unified/error.py +479 -0
  133. foundry_mcp/tools/unified/health.py +225 -0
  134. foundry_mcp/tools/unified/journal.py +841 -0
  135. foundry_mcp/tools/unified/lifecycle.py +640 -0
  136. foundry_mcp/tools/unified/metrics.py +777 -0
  137. foundry_mcp/tools/unified/plan.py +876 -0
  138. foundry_mcp/tools/unified/pr.py +294 -0
  139. foundry_mcp/tools/unified/provider.py +589 -0
  140. foundry_mcp/tools/unified/research.py +1283 -0
  141. foundry_mcp/tools/unified/review.py +1042 -0
  142. foundry_mcp/tools/unified/review_helpers.py +314 -0
  143. foundry_mcp/tools/unified/router.py +102 -0
  144. foundry_mcp/tools/unified/server.py +565 -0
  145. foundry_mcp/tools/unified/spec.py +1283 -0
  146. foundry_mcp/tools/unified/task.py +3846 -0
  147. foundry_mcp/tools/unified/test.py +431 -0
  148. foundry_mcp/tools/unified/verification.py +520 -0
  149. foundry_mcp-0.8.22.dist-info/METADATA +344 -0
  150. foundry_mcp-0.8.22.dist-info/RECORD +153 -0
  151. foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
  152. foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
  153. foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,427 @@
1
+ """
2
+ Rate limiting module for foundry-mcp.
3
+
4
+ Provides per-tool rate limiting with configurable limits and audit logging.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import os
10
+ import time
11
+ from collections import defaultdict
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Optional
15
+
16
+ from foundry_mcp.core.observability import audit_log
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ # Schema version
22
+ SCHEMA_VERSION = "1.0.0"
23
+
24
+
25
+ @dataclass
26
+ class RateLimitConfig:
27
+ """
28
+ Configuration for a rate limit.
29
+ """
30
+ requests_per_minute: int = 60
31
+ burst_limit: int = 10
32
+ enabled: bool = True
33
+ reason: str = ""
34
+
35
+
36
+ @dataclass
37
+ class RateLimitState:
38
+ """
39
+ Current state of a rate limiter.
40
+ """
41
+ tokens: float = 0.0
42
+ last_update: float = 0.0
43
+ request_count: int = 0
44
+ throttle_count: int = 0
45
+
46
+
47
+ @dataclass
48
+ class RateLimitResult:
49
+ """
50
+ Result of a rate limit check.
51
+ """
52
+ allowed: bool
53
+ remaining: int = 0
54
+ reset_in: float = 0.0
55
+ limit: int = 0
56
+ reason: str = ""
57
+
58
+
59
+ class TokenBucketLimiter:
60
+ """
61
+ Token bucket rate limiter implementation.
62
+
63
+ Provides smooth rate limiting with burst support.
64
+ """
65
+
66
+ def __init__(self, config: RateLimitConfig):
67
+ """
68
+ Initialize rate limiter.
69
+
70
+ Args:
71
+ config: Rate limit configuration
72
+ """
73
+ self.config = config
74
+ self.state = RateLimitState(
75
+ tokens=float(config.burst_limit),
76
+ last_update=time.time()
77
+ )
78
+
79
+ def check(self) -> RateLimitResult:
80
+ """
81
+ Check if a request is allowed without consuming tokens.
82
+
83
+ Returns:
84
+ RateLimitResult indicating if request would be allowed
85
+ """
86
+ if not self.config.enabled:
87
+ return RateLimitResult(allowed=True, remaining=-1)
88
+
89
+ self._refill()
90
+ return RateLimitResult(
91
+ allowed=self.state.tokens >= 1.0,
92
+ remaining=int(self.state.tokens),
93
+ reset_in=self._time_to_next_token(),
94
+ limit=self.config.requests_per_minute,
95
+ )
96
+
97
+ def acquire(self) -> RateLimitResult:
98
+ """
99
+ Attempt to acquire a token for a request.
100
+
101
+ Returns:
102
+ RateLimitResult indicating if request is allowed
103
+ """
104
+ if not self.config.enabled:
105
+ return RateLimitResult(allowed=True, remaining=-1)
106
+
107
+ self._refill()
108
+ self.state.request_count += 1
109
+
110
+ if self.state.tokens >= 1.0:
111
+ self.state.tokens -= 1.0
112
+ return RateLimitResult(
113
+ allowed=True,
114
+ remaining=int(self.state.tokens),
115
+ reset_in=self._time_to_next_token(),
116
+ limit=self.config.requests_per_minute,
117
+ )
118
+
119
+ self.state.throttle_count += 1
120
+ return RateLimitResult(
121
+ allowed=False,
122
+ remaining=0,
123
+ reset_in=self._time_to_next_token(),
124
+ limit=self.config.requests_per_minute,
125
+ reason=self.config.reason or "Rate limit exceeded",
126
+ )
127
+
128
+ def _refill(self) -> None:
129
+ """Refill tokens based on elapsed time."""
130
+ now = time.time()
131
+ elapsed = now - self.state.last_update
132
+ self.state.last_update = now
133
+
134
+ # Calculate tokens to add (tokens per second = rpm / 60)
135
+ tokens_per_second = self.config.requests_per_minute / 60.0
136
+ new_tokens = elapsed * tokens_per_second
137
+
138
+ self.state.tokens = min(
139
+ float(self.config.burst_limit),
140
+ self.state.tokens + new_tokens
141
+ )
142
+
143
+ def _time_to_next_token(self) -> float:
144
+ """Calculate time until next token is available."""
145
+ if self.state.tokens >= 1.0:
146
+ return 0.0
147
+ tokens_per_second = self.config.requests_per_minute / 60.0
148
+ needed = 1.0 - self.state.tokens
149
+ return needed / tokens_per_second
150
+
151
+ def get_stats(self) -> Dict[str, Any]:
152
+ """Get limiter statistics."""
153
+ return {
154
+ "requests": self.state.request_count,
155
+ "throttled": self.state.throttle_count,
156
+ "current_tokens": int(self.state.tokens),
157
+ "limit": self.config.requests_per_minute,
158
+ "burst": self.config.burst_limit,
159
+ "enabled": self.config.enabled,
160
+ }
161
+
162
+
163
+ class RateLimitManager:
164
+ """
165
+ Manages rate limits for multiple tools/operations.
166
+
167
+ Loads configuration from manifest and environment,
168
+ enforces limits, and logs throttling events.
169
+ """
170
+
171
+ def __init__(self):
172
+ """Initialize rate limit manager."""
173
+ self._limiters: Dict[str, TokenBucketLimiter] = {}
174
+ self._tenant_limiters: Dict[str, Dict[str, TokenBucketLimiter]] = defaultdict(dict)
175
+ self._global_config = RateLimitConfig()
176
+ self._tool_configs: Dict[str, RateLimitConfig] = {}
177
+
178
+ def load_from_manifest(self, manifest_path: Optional[Path] = None) -> None:
179
+ """
180
+ Load rate limit configurations from capabilities manifest.
181
+
182
+ Args:
183
+ manifest_path: Path to manifest file (auto-detected if not provided)
184
+ """
185
+ if manifest_path is None:
186
+ search_paths = [
187
+ Path.cwd() / "mcp" / "capabilities_manifest.json",
188
+ Path(__file__).parent.parent.parent / "mcp" / "capabilities_manifest.json",
189
+ ]
190
+ for path in search_paths:
191
+ if path.exists():
192
+ manifest_path = path
193
+ break
194
+
195
+ if not manifest_path or not manifest_path.exists():
196
+ logger.debug("No manifest found for rate limit configuration")
197
+ return
198
+
199
+ try:
200
+ with open(manifest_path, "r") as f:
201
+ manifest = json.load(f)
202
+
203
+ # Load tool-specific rate limits
204
+ for category, tools in manifest.get("tools", {}).items():
205
+ if isinstance(tools, list):
206
+ for tool in tools:
207
+ name = tool.get("name")
208
+ rate_limit = tool.get("rate_limit")
209
+ if name and rate_limit:
210
+ self._tool_configs[name] = RateLimitConfig(
211
+ requests_per_minute=rate_limit.get("requests_per_minute", 60),
212
+ burst_limit=rate_limit.get("burst_limit", 10),
213
+ enabled=True,
214
+ reason=rate_limit.get("reason", ""),
215
+ )
216
+
217
+ logger.info(f"Loaded rate limits for {len(self._tool_configs)} tools from manifest")
218
+
219
+ except (json.JSONDecodeError, IOError) as e:
220
+ logger.error(f"Failed to load manifest: {e}")
221
+
222
+ def load_from_env(self) -> None:
223
+ """
224
+ Load rate limit overrides from environment variables.
225
+
226
+ Supports:
227
+ - FOUNDRY_RATE_LIMIT_DEFAULT: Default requests per minute
228
+ - FOUNDRY_RATE_LIMIT_BURST: Default burst limit
229
+ - FOUNDRY_RATE_LIMIT_{TOOL}: Per-tool override (e.g., FOUNDRY_RATE_LIMIT_RUN_TESTS=5)
230
+ """
231
+ # Global defaults
232
+ default_rpm = os.environ.get("FOUNDRY_RATE_LIMIT_DEFAULT")
233
+ if default_rpm:
234
+ try:
235
+ self._global_config.requests_per_minute = int(default_rpm)
236
+ except ValueError:
237
+ pass
238
+
239
+ default_burst = os.environ.get("FOUNDRY_RATE_LIMIT_BURST")
240
+ if default_burst:
241
+ try:
242
+ self._global_config.burst_limit = int(default_burst)
243
+ except ValueError:
244
+ pass
245
+
246
+ # Per-tool overrides
247
+ for key, value in os.environ.items():
248
+ if key.startswith("FOUNDRY_RATE_LIMIT_") and key not in ("FOUNDRY_RATE_LIMIT_DEFAULT", "FOUNDRY_RATE_LIMIT_BURST"):
249
+ tool_name = key[19:].lower().replace("_", "_") # Keep underscores
250
+ try:
251
+ rpm = int(value)
252
+ if tool_name not in self._tool_configs:
253
+ self._tool_configs[tool_name] = RateLimitConfig()
254
+ self._tool_configs[tool_name].requests_per_minute = rpm
255
+ except ValueError:
256
+ pass
257
+
258
+ def get_limiter(self, tool_name: str, tenant_id: Optional[str] = None) -> TokenBucketLimiter:
259
+ """
260
+ Get or create a rate limiter for a tool.
261
+
262
+ Args:
263
+ tool_name: Name of the tool
264
+ tenant_id: Optional tenant ID for per-tenant limiting
265
+
266
+ Returns:
267
+ TokenBucketLimiter for the tool
268
+ """
269
+ if tenant_id:
270
+ if tool_name not in self._tenant_limiters[tenant_id]:
271
+ config = self._tool_configs.get(tool_name, self._global_config)
272
+ self._tenant_limiters[tenant_id][tool_name] = TokenBucketLimiter(config)
273
+ return self._tenant_limiters[tenant_id][tool_name]
274
+
275
+ if tool_name not in self._limiters:
276
+ config = self._tool_configs.get(tool_name, self._global_config)
277
+ self._limiters[tool_name] = TokenBucketLimiter(config)
278
+ return self._limiters[tool_name]
279
+
280
+ def check_limit(
281
+ self,
282
+ tool_name: str,
283
+ tenant_id: Optional[str] = None,
284
+ log_on_throttle: bool = True
285
+ ) -> RateLimitResult:
286
+ """
287
+ Check and enforce rate limit for a tool invocation.
288
+
289
+ Args:
290
+ tool_name: Name of the tool being invoked
291
+ tenant_id: Optional tenant ID
292
+ log_on_throttle: Whether to log throttle events
293
+
294
+ Returns:
295
+ RateLimitResult indicating if request is allowed
296
+ """
297
+ limiter = self.get_limiter(tool_name, tenant_id)
298
+ result = limiter.acquire()
299
+
300
+ if not result.allowed and log_on_throttle:
301
+ self._log_throttle(tool_name, tenant_id, result)
302
+
303
+ return result
304
+
305
+ def _log_throttle(
306
+ self,
307
+ tool_name: str,
308
+ tenant_id: Optional[str],
309
+ result: RateLimitResult
310
+ ) -> None:
311
+ """Log a throttle event."""
312
+ audit_log(
313
+ "rate_limit_exceeded",
314
+ tool=tool_name,
315
+ tenant_id=tenant_id,
316
+ limit=result.limit,
317
+ reset_in=result.reset_in,
318
+ reason=result.reason,
319
+ success=False,
320
+ )
321
+ logger.warning(
322
+ f"Rate limit exceeded for {tool_name}"
323
+ + (f" (tenant: {tenant_id})" if tenant_id else "")
324
+ + f": {result.reason}"
325
+ )
326
+
327
+ def log_auth_failure(
328
+ self,
329
+ tool_name: str,
330
+ tenant_id: Optional[str] = None,
331
+ reason: str = "Authentication failed"
332
+ ) -> None:
333
+ """
334
+ Log an authentication failure.
335
+
336
+ Args:
337
+ tool_name: Tool that was accessed
338
+ tenant_id: Optional tenant ID
339
+ reason: Failure reason
340
+ """
341
+ audit_log(
342
+ "auth_failure",
343
+ tool=tool_name,
344
+ tenant_id=tenant_id,
345
+ reason=reason,
346
+ success=False,
347
+ )
348
+ logger.warning(
349
+ f"Auth failure for {tool_name}"
350
+ + (f" (tenant: {tenant_id})" if tenant_id else "")
351
+ + f": {reason}"
352
+ )
353
+
354
+ def get_all_stats(self) -> Dict[str, Any]:
355
+ """Get statistics for all limiters."""
356
+ stats = {
357
+ "schema_version": SCHEMA_VERSION,
358
+ "global_config": {
359
+ "requests_per_minute": self._global_config.requests_per_minute,
360
+ "burst_limit": self._global_config.burst_limit,
361
+ },
362
+ "tools": {},
363
+ "tenants": {},
364
+ }
365
+
366
+ for tool_name, limiter in self._limiters.items():
367
+ stats["tools"][tool_name] = limiter.get_stats()
368
+
369
+ for tenant_id, limiters in self._tenant_limiters.items():
370
+ stats["tenants"][tenant_id] = {
371
+ name: limiter.get_stats()
372
+ for name, limiter in limiters.items()
373
+ }
374
+
375
+ return stats
376
+
377
+ def reset(self, tool_name: Optional[str] = None, tenant_id: Optional[str] = None) -> None:
378
+ """
379
+ Reset rate limit state.
380
+
381
+ Args:
382
+ tool_name: Specific tool to reset (all if None)
383
+ tenant_id: Specific tenant to reset (all if None)
384
+ """
385
+ if tool_name and tenant_id:
386
+ if tenant_id in self._tenant_limiters and tool_name in self._tenant_limiters[tenant_id]:
387
+ del self._tenant_limiters[tenant_id][tool_name]
388
+ elif tool_name:
389
+ if tool_name in self._limiters:
390
+ del self._limiters[tool_name]
391
+ elif tenant_id:
392
+ if tenant_id in self._tenant_limiters:
393
+ del self._tenant_limiters[tenant_id]
394
+ else:
395
+ self._limiters.clear()
396
+ self._tenant_limiters.clear()
397
+
398
+
399
+ # Global instance
400
+ _manager: Optional[RateLimitManager] = None
401
+
402
+
403
+ def get_rate_limit_manager() -> RateLimitManager:
404
+ """Get the global rate limit manager."""
405
+ global _manager
406
+ if _manager is None:
407
+ _manager = RateLimitManager()
408
+ _manager.load_from_manifest()
409
+ _manager.load_from_env()
410
+ return _manager
411
+
412
+
413
+ def check_rate_limit(
414
+ tool_name: str,
415
+ tenant_id: Optional[str] = None
416
+ ) -> RateLimitResult:
417
+ """
418
+ Check rate limit for a tool invocation.
419
+
420
+ Args:
421
+ tool_name: Name of the tool
422
+ tenant_id: Optional tenant ID
423
+
424
+ Returns:
425
+ RateLimitResult indicating if request is allowed
426
+ """
427
+ return get_rate_limit_manager().check_limit(tool_name, tenant_id)
@@ -0,0 +1,68 @@
1
+ """Research workflows for multi-model orchestration.
2
+
3
+ This package provides conversation threading, multi-model consensus,
4
+ hypothesis-driven investigation, and creative brainstorming workflows.
5
+ """
6
+
7
+ from foundry_mcp.core.research.models import (
8
+ ConfidenceLevel,
9
+ ConsensusConfig,
10
+ ConsensusState,
11
+ ConsensusStrategy,
12
+ ConversationMessage,
13
+ ConversationThread,
14
+ Hypothesis,
15
+ Idea,
16
+ IdeaCluster,
17
+ IdeationPhase,
18
+ IdeationState,
19
+ InvestigationStep,
20
+ ModelResponse,
21
+ ThreadStatus,
22
+ ThinkDeepState,
23
+ WorkflowType,
24
+ )
25
+ from foundry_mcp.core.research.memory import (
26
+ FileStorageBackend,
27
+ ResearchMemory,
28
+ )
29
+ from foundry_mcp.core.research.workflows import (
30
+ ChatWorkflow,
31
+ ConsensusWorkflow,
32
+ IdeateWorkflow,
33
+ ResearchWorkflowBase,
34
+ ThinkDeepWorkflow,
35
+ )
36
+
37
+ __all__ = [
38
+ # Enums
39
+ "WorkflowType",
40
+ "ConfidenceLevel",
41
+ "ConsensusStrategy",
42
+ "ThreadStatus",
43
+ "IdeationPhase",
44
+ # Conversation models
45
+ "ConversationMessage",
46
+ "ConversationThread",
47
+ # THINKDEEP models
48
+ "Hypothesis",
49
+ "InvestigationStep",
50
+ "ThinkDeepState",
51
+ # IDEATE models
52
+ "Idea",
53
+ "IdeaCluster",
54
+ "IdeationState",
55
+ # CONSENSUS models
56
+ "ModelResponse",
57
+ "ConsensusConfig",
58
+ "ConsensusState",
59
+ # Storage
60
+ "FileStorageBackend",
61
+ "ResearchMemory",
62
+ # Workflows
63
+ "ResearchWorkflowBase",
64
+ "ChatWorkflow",
65
+ "ConsensusWorkflow",
66
+ "ThinkDeepWorkflow",
67
+ "IdeateWorkflow",
68
+ ]