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,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
|