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,195 @@
1
+ """Cache management for AI consultation results.
2
+
3
+ Provides a simple file-based cache for storing AI consultation results
4
+ (plan reviews, fidelity reviews, etc.) to avoid redundant API calls.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import time
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Optional
13
+
14
+
15
+ @dataclass
16
+ class CacheStats:
17
+ """Cache statistics."""
18
+
19
+ cache_dir: str
20
+ total_entries: int
21
+ active_entries: int
22
+ expired_entries: int
23
+ total_size_bytes: int
24
+ total_size_mb: float
25
+
26
+
27
+ def get_cache_dir() -> Path:
28
+ """Get the cache directory path.
29
+
30
+ Resolution order:
31
+ 1. FOUNDRY_MCP_CACHE_DIR environment variable
32
+ 2. ~/.foundry-mcp/cache
33
+
34
+ Returns:
35
+ Path to the cache directory.
36
+ """
37
+ if cache_dir := os.environ.get("FOUNDRY_MCP_CACHE_DIR"):
38
+ return Path(cache_dir)
39
+
40
+ return Path.home() / ".foundry-mcp" / "cache"
41
+
42
+
43
+ def is_cache_enabled() -> bool:
44
+ """Check if caching is enabled.
45
+
46
+ Returns:
47
+ True if caching is enabled (default), False if disabled.
48
+ """
49
+ disabled = os.environ.get("FOUNDRY_MCP_CACHE_DISABLED", "").lower()
50
+ return disabled not in ("true", "1", "yes")
51
+
52
+
53
+ class CacheManager:
54
+ """Manages the AI consultation cache."""
55
+
56
+ # Default TTL: 7 days in seconds
57
+ DEFAULT_TTL = 7 * 24 * 60 * 60
58
+
59
+ def __init__(self, cache_dir: Optional[Path] = None):
60
+ """Initialize the cache manager.
61
+
62
+ Args:
63
+ cache_dir: Optional override for cache directory.
64
+ """
65
+ self.cache_dir = cache_dir or get_cache_dir()
66
+
67
+ def ensure_dir(self) -> None:
68
+ """Ensure the cache directory exists."""
69
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
70
+
71
+ def get_stats(self) -> Dict[str, Any]:
72
+ """Get cache statistics.
73
+
74
+ Returns:
75
+ Dict with cache statistics.
76
+ """
77
+ if not self.cache_dir.exists():
78
+ return {
79
+ "cache_dir": str(self.cache_dir),
80
+ "total_entries": 0,
81
+ "active_entries": 0,
82
+ "expired_entries": 0,
83
+ "total_size_bytes": 0,
84
+ "total_size_mb": 0.0,
85
+ }
86
+
87
+ now = time.time()
88
+ total_entries = 0
89
+ active_entries = 0
90
+ expired_entries = 0
91
+ total_size = 0
92
+
93
+ for entry_file in self.cache_dir.glob("*.json"):
94
+ total_entries += 1
95
+ total_size += entry_file.stat().st_size
96
+
97
+ try:
98
+ with open(entry_file, "r") as f:
99
+ entry = json.load(f)
100
+ expires_at = entry.get("expires_at", 0)
101
+ if expires_at > now:
102
+ active_entries += 1
103
+ else:
104
+ expired_entries += 1
105
+ except (json.JSONDecodeError, KeyError):
106
+ # Treat malformed entries as expired
107
+ expired_entries += 1
108
+
109
+ return {
110
+ "cache_dir": str(self.cache_dir),
111
+ "total_entries": total_entries,
112
+ "active_entries": active_entries,
113
+ "expired_entries": expired_entries,
114
+ "total_size_bytes": total_size,
115
+ "total_size_mb": round(total_size / (1024 * 1024), 2),
116
+ }
117
+
118
+ def clear(
119
+ self,
120
+ spec_id: Optional[str] = None,
121
+ review_type: Optional[str] = None,
122
+ ) -> int:
123
+ """Clear cache entries with optional filters.
124
+
125
+ Args:
126
+ spec_id: Only clear entries for this spec ID.
127
+ review_type: Only clear entries of this type (fidelity, plan).
128
+
129
+ Returns:
130
+ Number of entries deleted.
131
+ """
132
+ if not self.cache_dir.exists():
133
+ return 0
134
+
135
+ deleted = 0
136
+
137
+ for entry_file in self.cache_dir.glob("*.json"):
138
+ should_delete = True
139
+
140
+ # Apply filters if specified
141
+ if spec_id or review_type:
142
+ try:
143
+ with open(entry_file, "r") as f:
144
+ entry = json.load(f)
145
+
146
+ if spec_id and entry.get("spec_id") != spec_id:
147
+ should_delete = False
148
+
149
+ if review_type and entry.get("review_type") != review_type:
150
+ should_delete = False
151
+
152
+ except (json.JSONDecodeError, KeyError):
153
+ # Delete malformed entries
154
+ pass
155
+
156
+ if should_delete:
157
+ try:
158
+ entry_file.unlink()
159
+ deleted += 1
160
+ except OSError:
161
+ pass
162
+
163
+ return deleted
164
+
165
+ def cleanup_expired(self) -> int:
166
+ """Remove expired cache entries.
167
+
168
+ Returns:
169
+ Number of entries removed.
170
+ """
171
+ if not self.cache_dir.exists():
172
+ return 0
173
+
174
+ now = time.time()
175
+ removed = 0
176
+
177
+ for entry_file in self.cache_dir.glob("*.json"):
178
+ try:
179
+ with open(entry_file, "r") as f:
180
+ entry = json.load(f)
181
+
182
+ expires_at = entry.get("expires_at", 0)
183
+ if expires_at <= now:
184
+ entry_file.unlink()
185
+ removed += 1
186
+
187
+ except (json.JSONDecodeError, KeyError, OSError):
188
+ # Remove malformed entries
189
+ try:
190
+ entry_file.unlink()
191
+ removed += 1
192
+ except OSError:
193
+ pass
194
+
195
+ return removed
@@ -0,0 +1,446 @@
1
+ """
2
+ MCP Capabilities module for foundry-mcp.
3
+
4
+ Provides support for MCP Notifications and Sampling features.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Any, Callable, Dict, List, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ # Schema version for capability responses
18
+ SCHEMA_VERSION = "1.0.0"
19
+
20
+
21
+ # Notification Types
22
+
23
+ @dataclass
24
+ class Notification:
25
+ """
26
+ MCP notification to be sent to clients.
27
+ """
28
+ method: str
29
+ params: Dict[str, Any] = field(default_factory=dict)
30
+ timestamp: str = ""
31
+
32
+ def __post_init__(self):
33
+ if not self.timestamp:
34
+ self.timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
35
+
36
+
37
+ @dataclass
38
+ class ResourceUpdate:
39
+ """
40
+ Notification for resource updates.
41
+ """
42
+ uri: str
43
+ update_type: str # created, updated, deleted
44
+ metadata: Dict[str, Any] = field(default_factory=dict)
45
+
46
+
47
+ # Notification Manager
48
+
49
+ class NotificationManager:
50
+ """
51
+ Manages MCP notifications for resource updates and other events.
52
+
53
+ Supports registering handlers and emitting notifications when
54
+ specs or other resources change.
55
+ """
56
+
57
+ def __init__(self):
58
+ """Initialize notification manager."""
59
+ self._handlers: List[Callable[[Notification], None]] = []
60
+ self._pending: List[Notification] = []
61
+ self._enabled = True
62
+
63
+ def register_handler(self, handler: Callable[[Notification], None]) -> None:
64
+ """
65
+ Register a notification handler.
66
+
67
+ Args:
68
+ handler: Callback function to receive notifications
69
+ """
70
+ self._handlers.append(handler)
71
+ logger.debug(f"Registered notification handler: {handler}")
72
+
73
+ def unregister_handler(self, handler: Callable[[Notification], None]) -> None:
74
+ """
75
+ Unregister a notification handler.
76
+
77
+ Args:
78
+ handler: Handler to remove
79
+ """
80
+ if handler in self._handlers:
81
+ self._handlers.remove(handler)
82
+ logger.debug(f"Unregistered notification handler: {handler}")
83
+
84
+ def emit(self, notification: Notification) -> None:
85
+ """
86
+ Emit a notification to all registered handlers.
87
+
88
+ Args:
89
+ notification: Notification to emit
90
+ """
91
+ if not self._enabled:
92
+ self._pending.append(notification)
93
+ return
94
+
95
+ for handler in self._handlers:
96
+ try:
97
+ handler(notification)
98
+ except Exception as e:
99
+ logger.error(f"Error in notification handler: {e}")
100
+
101
+ def emit_resource_update(
102
+ self,
103
+ uri: str,
104
+ update_type: str,
105
+ metadata: Optional[Dict[str, Any]] = None
106
+ ) -> None:
107
+ """
108
+ Emit a resource update notification.
109
+
110
+ Args:
111
+ uri: Resource URI that was updated
112
+ update_type: Type of update (created, updated, deleted)
113
+ metadata: Optional additional metadata
114
+ """
115
+ notification = Notification(
116
+ method="notifications/resources/updated",
117
+ params={
118
+ "uri": uri,
119
+ "type": update_type,
120
+ "metadata": metadata or {},
121
+ }
122
+ )
123
+ self.emit(notification)
124
+
125
+ def emit_spec_updated(
126
+ self,
127
+ spec_id: str,
128
+ update_type: str = "updated",
129
+ changes: Optional[Dict[str, Any]] = None
130
+ ) -> None:
131
+ """
132
+ Emit a spec update notification.
133
+
134
+ Args:
135
+ spec_id: Spec ID that was updated
136
+ update_type: Type of update (created, updated, deleted, status_changed)
137
+ changes: Optional dict describing what changed
138
+ """
139
+ notification = Notification(
140
+ method="foundry/specs/updated",
141
+ params={
142
+ "spec_id": spec_id,
143
+ "type": update_type,
144
+ "changes": changes or {},
145
+ }
146
+ )
147
+ self.emit(notification)
148
+
149
+ def emit_task_updated(
150
+ self,
151
+ spec_id: str,
152
+ task_id: str,
153
+ update_type: str = "updated",
154
+ changes: Optional[Dict[str, Any]] = None
155
+ ) -> None:
156
+ """
157
+ Emit a task update notification.
158
+
159
+ Args:
160
+ spec_id: Spec ID containing the task
161
+ task_id: Task ID that was updated
162
+ update_type: Type of update (status_changed, journal_added, etc.)
163
+ changes: Optional dict describing what changed
164
+ """
165
+ notification = Notification(
166
+ method="foundry/tasks/updated",
167
+ params={
168
+ "spec_id": spec_id,
169
+ "task_id": task_id,
170
+ "type": update_type,
171
+ "changes": changes or {},
172
+ }
173
+ )
174
+ self.emit(notification)
175
+
176
+ def pause(self) -> None:
177
+ """Pause notification delivery (queue pending)."""
178
+ self._enabled = False
179
+
180
+ def resume(self) -> None:
181
+ """Resume notification delivery and flush pending."""
182
+ self._enabled = True
183
+ pending = self._pending[:]
184
+ self._pending.clear()
185
+ for notification in pending:
186
+ self.emit(notification)
187
+
188
+ def clear_pending(self) -> None:
189
+ """Clear pending notifications without sending."""
190
+ self._pending.clear()
191
+
192
+
193
+ # Sampling Support
194
+
195
+ @dataclass
196
+ class SamplingRequest:
197
+ """
198
+ Request for server-side AI sampling.
199
+ """
200
+ messages: List[Dict[str, Any]]
201
+ model_preferences: Optional[Dict[str, Any]] = None
202
+ system_prompt: Optional[str] = None
203
+ max_tokens: int = 1000
204
+ temperature: float = 0.7
205
+ metadata: Dict[str, Any] = field(default_factory=dict)
206
+
207
+
208
+ @dataclass
209
+ class SamplingResponse:
210
+ """
211
+ Response from server-side AI sampling.
212
+ """
213
+ content: str
214
+ model: str
215
+ usage: Dict[str, int] = field(default_factory=dict)
216
+ stop_reason: Optional[str] = None
217
+ metadata: Dict[str, Any] = field(default_factory=dict)
218
+
219
+
220
+ class SamplingManager:
221
+ """
222
+ Manages MCP sampling requests for server-side AI operations.
223
+
224
+ Used for features like impact analysis that benefit from AI assistance.
225
+ """
226
+
227
+ def __init__(self):
228
+ """Initialize sampling manager."""
229
+ self._handler: Optional[Callable[[SamplingRequest], SamplingResponse]] = None
230
+ self._enabled = False
231
+ self._request_count = 0
232
+ self._total_tokens = 0
233
+
234
+ def set_handler(self, handler: Callable[[SamplingRequest], SamplingResponse]) -> None:
235
+ """
236
+ Set the sampling handler (typically provided by MCP client).
237
+
238
+ Args:
239
+ handler: Function to handle sampling requests
240
+ """
241
+ self._handler = handler
242
+ self._enabled = True
243
+ logger.info("Sampling handler registered")
244
+
245
+ def is_available(self) -> bool:
246
+ """Check if sampling is available."""
247
+ return self._enabled and self._handler is not None
248
+
249
+ def request(self, request: SamplingRequest) -> Optional[SamplingResponse]:
250
+ """
251
+ Make a sampling request.
252
+
253
+ Args:
254
+ request: Sampling request to process
255
+
256
+ Returns:
257
+ SamplingResponse if successful, None otherwise
258
+ """
259
+ if not self.is_available():
260
+ logger.warning("Sampling not available")
261
+ return None
262
+
263
+ try:
264
+ response = self._handler(request)
265
+ self._request_count += 1
266
+ self._total_tokens += response.usage.get("total_tokens", 0)
267
+ return response
268
+ except Exception as e:
269
+ logger.error(f"Sampling request failed: {e}")
270
+ return None
271
+
272
+ def analyze_impact(
273
+ self,
274
+ target: str,
275
+ target_type: str,
276
+ context: Optional[str] = None
277
+ ) -> Optional[Dict[str, Any]]:
278
+ """
279
+ Use sampling to analyze impact of a change.
280
+
281
+ Args:
282
+ target: Name of class/function being changed
283
+ target_type: Type of target (class, function, module)
284
+ context: Optional additional context about the change
285
+
286
+ Returns:
287
+ Analysis results or None if sampling unavailable
288
+ """
289
+ if not self.is_available():
290
+ return None
291
+
292
+ system_prompt = """You are analyzing code change impacts.
293
+ Given a target entity and its type, identify:
294
+ 1. Direct impacts (what directly depends on this)
295
+ 2. Indirect impacts (what might be affected transitively)
296
+ 3. Risk level (low, medium, high)
297
+ 4. Recommended actions before making changes
298
+
299
+ Respond in JSON format with keys: direct_impacts, indirect_impacts, risk_level, recommendations"""
300
+
301
+ messages = [
302
+ {
303
+ "role": "user",
304
+ "content": f"Analyze the impact of changing {target_type} '{target}'."
305
+ + (f"\n\nAdditional context: {context}" if context else "")
306
+ }
307
+ ]
308
+
309
+ request = SamplingRequest(
310
+ messages=messages,
311
+ system_prompt=system_prompt,
312
+ max_tokens=500,
313
+ temperature=0.3,
314
+ metadata={"operation": "impact_analysis", "target": target}
315
+ )
316
+
317
+ response = self.request(request)
318
+ if response:
319
+ try:
320
+ return json.loads(response.content)
321
+ except json.JSONDecodeError:
322
+ return {"raw_response": response.content}
323
+
324
+ return None
325
+
326
+ def get_stats(self) -> Dict[str, Any]:
327
+ """Get sampling usage statistics."""
328
+ return {
329
+ "enabled": self._enabled,
330
+ "available": self.is_available(),
331
+ "request_count": self._request_count,
332
+ "total_tokens": self._total_tokens,
333
+ }
334
+
335
+
336
+ # Capabilities Registry
337
+
338
+ class CapabilitiesRegistry:
339
+ """
340
+ Central registry for server capabilities.
341
+
342
+ Tracks what features are available and their configuration.
343
+ """
344
+
345
+ def __init__(self):
346
+ """Initialize capabilities registry."""
347
+ self.notifications = NotificationManager()
348
+ self.sampling = SamplingManager()
349
+ self._capabilities: Dict[str, bool] = {
350
+ "notifications": True,
351
+ "sampling": False, # Requires handler
352
+ "resources": True,
353
+ "prompts": True,
354
+ "tools": True,
355
+ }
356
+ self._metadata: Dict[str, Any] = {}
357
+
358
+ def enable(self, capability: str) -> None:
359
+ """Enable a capability."""
360
+ self._capabilities[capability] = True
361
+
362
+ def disable(self, capability: str) -> None:
363
+ """Disable a capability."""
364
+ self._capabilities[capability] = False
365
+
366
+ def is_enabled(self, capability: str) -> bool:
367
+ """Check if a capability is enabled."""
368
+ return self._capabilities.get(capability, False)
369
+
370
+ def set_metadata(self, key: str, value: Any) -> None:
371
+ """Set capability metadata."""
372
+ self._metadata[key] = value
373
+
374
+ def get_capabilities(self) -> Dict[str, Any]:
375
+ """
376
+ Get all capabilities and their status.
377
+
378
+ Returns:
379
+ Dict with capability information
380
+ """
381
+ return {
382
+ "schema_version": SCHEMA_VERSION,
383
+ "capabilities": self._capabilities.copy(),
384
+ "notifications": {
385
+ "enabled": self._capabilities.get("notifications", False),
386
+ "methods": [
387
+ "notifications/resources/updated",
388
+ "foundry/specs/updated",
389
+ "foundry/tasks/updated",
390
+ ]
391
+ },
392
+ "sampling": self.sampling.get_stats(),
393
+ "metadata": self._metadata,
394
+ }
395
+
396
+ def load_manifest(self, manifest_path: Optional[Path] = None) -> Dict[str, Any]:
397
+ """
398
+ Load capabilities from manifest file.
399
+
400
+ Args:
401
+ manifest_path: Path to capabilities manifest
402
+
403
+ Returns:
404
+ Loaded manifest data
405
+ """
406
+ if manifest_path is None:
407
+ # Try default locations
408
+ search_paths = [
409
+ Path.cwd() / "mcp" / "capabilities_manifest.json",
410
+ Path(__file__).parent.parent.parent.parent / "mcp" / "capabilities_manifest.json",
411
+ ]
412
+ for path in search_paths:
413
+ if path.exists():
414
+ manifest_path = path
415
+ break
416
+
417
+ if manifest_path and manifest_path.exists():
418
+ try:
419
+ with open(manifest_path, "r") as f:
420
+ return json.load(f)
421
+ except (json.JSONDecodeError, IOError) as e:
422
+ logger.error(f"Failed to load manifest: {e}")
423
+
424
+ return {}
425
+
426
+
427
+ # Global instance
428
+ _registry: Optional[CapabilitiesRegistry] = None
429
+
430
+
431
+ def get_capabilities_registry() -> CapabilitiesRegistry:
432
+ """Get the global capabilities registry."""
433
+ global _registry
434
+ if _registry is None:
435
+ _registry = CapabilitiesRegistry()
436
+ return _registry
437
+
438
+
439
+ def get_notification_manager() -> NotificationManager:
440
+ """Get the notification manager from global registry."""
441
+ return get_capabilities_registry().notifications
442
+
443
+
444
+ def get_sampling_manager() -> SamplingManager:
445
+ """Get the sampling manager from global registry."""
446
+ return get_capabilities_registry().sampling