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,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spec modification operations using direct Python APIs.
|
|
3
|
+
Replaces subprocess calls to external CLI tools.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from .spec import load_spec, save_spec, update_node, find_specs_directory
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def apply_modifications(
|
|
13
|
+
spec_id: str,
|
|
14
|
+
modifications: List[Dict[str, Any]],
|
|
15
|
+
specs_dir: Optional[Path] = None,
|
|
16
|
+
dry_run: bool = False,
|
|
17
|
+
) -> Tuple[int, int, List[Dict[str, Any]]]:
|
|
18
|
+
"""
|
|
19
|
+
Apply a list of modifications to a spec.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
spec_id: Specification ID to modify
|
|
23
|
+
modifications: List of modification dictionaries with 'action', 'node_id', etc.
|
|
24
|
+
specs_dir: Path to specs directory (auto-detected if not provided)
|
|
25
|
+
dry_run: If True, don't save changes
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tuple of (applied_count, skipped_count, changes_list)
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If spec not found
|
|
32
|
+
FileNotFoundError: If specs directory not found
|
|
33
|
+
"""
|
|
34
|
+
if specs_dir is None:
|
|
35
|
+
specs_dir = find_specs_directory()
|
|
36
|
+
|
|
37
|
+
if not specs_dir:
|
|
38
|
+
raise FileNotFoundError("Could not find specs directory")
|
|
39
|
+
|
|
40
|
+
spec_data = load_spec(spec_id, specs_dir)
|
|
41
|
+
if not spec_data:
|
|
42
|
+
raise ValueError(f"Specification '{spec_id}' not found")
|
|
43
|
+
|
|
44
|
+
applied = 0
|
|
45
|
+
skipped = 0
|
|
46
|
+
changes: List[Dict[str, Any]] = []
|
|
47
|
+
|
|
48
|
+
for mod in modifications:
|
|
49
|
+
action = mod.get("action")
|
|
50
|
+
node_id = mod.get("node_id")
|
|
51
|
+
|
|
52
|
+
if action == "update_node":
|
|
53
|
+
mod_changes = mod.get("changes", {})
|
|
54
|
+
if update_node(spec_data, node_id, mod_changes):
|
|
55
|
+
applied += 1
|
|
56
|
+
changes.append({
|
|
57
|
+
"action": action,
|
|
58
|
+
"node_id": node_id,
|
|
59
|
+
"status": "applied",
|
|
60
|
+
"changes": mod_changes,
|
|
61
|
+
})
|
|
62
|
+
else:
|
|
63
|
+
skipped += 1
|
|
64
|
+
changes.append({
|
|
65
|
+
"action": action,
|
|
66
|
+
"node_id": node_id,
|
|
67
|
+
"status": "skipped",
|
|
68
|
+
"reason": "node not found",
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
elif action == "add_node":
|
|
72
|
+
node_data = mod.get("data", {})
|
|
73
|
+
parent_id = mod.get("parent")
|
|
74
|
+
if _add_node(spec_data, node_id, node_data, parent_id):
|
|
75
|
+
applied += 1
|
|
76
|
+
changes.append({
|
|
77
|
+
"action": action,
|
|
78
|
+
"node_id": node_id,
|
|
79
|
+
"status": "applied",
|
|
80
|
+
})
|
|
81
|
+
else:
|
|
82
|
+
skipped += 1
|
|
83
|
+
changes.append({
|
|
84
|
+
"action": action,
|
|
85
|
+
"node_id": node_id,
|
|
86
|
+
"status": "skipped",
|
|
87
|
+
"reason": "failed to add node",
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
elif action == "remove_node":
|
|
91
|
+
cascade = mod.get("cascade", False)
|
|
92
|
+
if _remove_node(spec_data, node_id, cascade=cascade):
|
|
93
|
+
applied += 1
|
|
94
|
+
changes.append({
|
|
95
|
+
"action": action,
|
|
96
|
+
"node_id": node_id,
|
|
97
|
+
"status": "applied",
|
|
98
|
+
})
|
|
99
|
+
else:
|
|
100
|
+
skipped += 1
|
|
101
|
+
changes.append({
|
|
102
|
+
"action": action,
|
|
103
|
+
"node_id": node_id,
|
|
104
|
+
"status": "skipped",
|
|
105
|
+
"reason": "node not found",
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
else:
|
|
109
|
+
skipped += 1
|
|
110
|
+
changes.append({
|
|
111
|
+
"action": action,
|
|
112
|
+
"node_id": node_id,
|
|
113
|
+
"status": "skipped",
|
|
114
|
+
"reason": f"unknown action: {action}",
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if not dry_run and applied > 0:
|
|
118
|
+
save_spec(spec_id, spec_data, specs_dir, backup=True)
|
|
119
|
+
|
|
120
|
+
return applied, skipped, changes
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def load_modifications_file(file_path: str) -> List[Dict[str, Any]]:
|
|
124
|
+
"""
|
|
125
|
+
Load modifications from a JSON file.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
file_path: Path to the modifications JSON file
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List of modification dictionaries
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
FileNotFoundError: If file doesn't exist
|
|
135
|
+
json.JSONDecodeError: If file is not valid JSON
|
|
136
|
+
"""
|
|
137
|
+
path = Path(file_path)
|
|
138
|
+
if not path.exists():
|
|
139
|
+
raise FileNotFoundError(f"Modifications file not found: {file_path}")
|
|
140
|
+
|
|
141
|
+
with open(path, 'r') as f:
|
|
142
|
+
data = json.load(f)
|
|
143
|
+
|
|
144
|
+
return data.get("modifications", [])
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _add_node(
|
|
148
|
+
spec_data: Dict[str, Any],
|
|
149
|
+
node_id: str,
|
|
150
|
+
node_data: Dict[str, Any],
|
|
151
|
+
parent_id: Optional[str] = None,
|
|
152
|
+
) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Add a new node to the spec hierarchy.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
spec_data: Spec data dictionary
|
|
158
|
+
node_id: ID for the new node
|
|
159
|
+
node_data: Node data to add
|
|
160
|
+
parent_id: Optional parent node ID
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if node was added, False otherwise
|
|
164
|
+
"""
|
|
165
|
+
hierarchy = spec_data.get("hierarchy", {})
|
|
166
|
+
|
|
167
|
+
# Don't overwrite existing nodes
|
|
168
|
+
if node_id in hierarchy:
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
# Add the node
|
|
172
|
+
hierarchy[node_id] = node_data
|
|
173
|
+
|
|
174
|
+
# Update parent's children list if parent specified
|
|
175
|
+
if parent_id and parent_id in hierarchy:
|
|
176
|
+
parent = hierarchy[parent_id]
|
|
177
|
+
children = parent.get("children", [])
|
|
178
|
+
if node_id not in children:
|
|
179
|
+
children.append(node_id)
|
|
180
|
+
parent["children"] = children
|
|
181
|
+
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _remove_node(
|
|
186
|
+
spec_data: Dict[str, Any],
|
|
187
|
+
node_id: str,
|
|
188
|
+
cascade: bool = False,
|
|
189
|
+
) -> bool:
|
|
190
|
+
"""
|
|
191
|
+
Remove a node from the spec hierarchy.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
spec_data: Spec data dictionary
|
|
195
|
+
node_id: ID of node to remove
|
|
196
|
+
cascade: If True, also remove children recursively
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
True if node was removed, False if not found
|
|
200
|
+
"""
|
|
201
|
+
hierarchy = spec_data.get("hierarchy", {})
|
|
202
|
+
|
|
203
|
+
if node_id not in hierarchy:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
node = hierarchy[node_id]
|
|
207
|
+
|
|
208
|
+
# Remove children first if cascade
|
|
209
|
+
if cascade:
|
|
210
|
+
children = node.get("children", [])
|
|
211
|
+
for child_id in children:
|
|
212
|
+
_remove_node(spec_data, child_id, cascade=True)
|
|
213
|
+
|
|
214
|
+
# Remove this node
|
|
215
|
+
del hierarchy[node_id]
|
|
216
|
+
|
|
217
|
+
# Remove from any parent's children list
|
|
218
|
+
for other_id, other_node in hierarchy.items():
|
|
219
|
+
children = other_node.get("children", [])
|
|
220
|
+
if node_id in children:
|
|
221
|
+
children.remove(node_id)
|
|
222
|
+
other_node["children"] = children
|
|
223
|
+
|
|
224
|
+
return True
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Naming helpers for MCP tool registration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import functools
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
from mcp.server.fastmcp import FastMCP
|
|
13
|
+
from mcp.types import TextContent
|
|
14
|
+
|
|
15
|
+
from foundry_mcp.core.observability import mcp_tool
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _minify_response(result: dict[str, Any]) -> TextContent:
|
|
21
|
+
"""Convert dict to TextContent with minified JSON.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
result: Dictionary to serialize
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
TextContent with minified JSON string
|
|
28
|
+
"""
|
|
29
|
+
return TextContent(
|
|
30
|
+
type="text",
|
|
31
|
+
text=json.dumps(result, separators=(",", ":"), default=str),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def canonical_tool(
|
|
36
|
+
mcp: FastMCP,
|
|
37
|
+
*,
|
|
38
|
+
canonical_name: str,
|
|
39
|
+
**tool_kwargs: Any,
|
|
40
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
41
|
+
"""Decorator that registers a tool under its canonical name.
|
|
42
|
+
|
|
43
|
+
This decorator wraps the tool function to:
|
|
44
|
+
1. Register it with FastMCP under the canonical name
|
|
45
|
+
2. Apply observability instrumentation via mcp_tool
|
|
46
|
+
3. Collect error data when exceptions occur
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
mcp: FastMCP instance
|
|
50
|
+
canonical_name: The canonical name for the tool
|
|
51
|
+
**tool_kwargs: Additional kwargs passed to mcp.tool()
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Decorated function registered as an MCP tool
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
58
|
+
if asyncio.iscoroutinefunction(func):
|
|
59
|
+
# Async function - use async wrapper
|
|
60
|
+
@functools.wraps(func)
|
|
61
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
62
|
+
"""Async wrapper for async underlying functions."""
|
|
63
|
+
start_time = time.perf_counter()
|
|
64
|
+
try:
|
|
65
|
+
result = await func(*args, **kwargs)
|
|
66
|
+
if isinstance(result, dict):
|
|
67
|
+
return _minify_response(result)
|
|
68
|
+
return result
|
|
69
|
+
except Exception as e:
|
|
70
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
71
|
+
_collect_tool_error(
|
|
72
|
+
tool_name=canonical_name,
|
|
73
|
+
error=e,
|
|
74
|
+
input_params=kwargs,
|
|
75
|
+
duration_ms=duration_ms,
|
|
76
|
+
)
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
wrapper = async_wrapper
|
|
80
|
+
else:
|
|
81
|
+
# Sync function - use sync wrapper to preserve sync behavior
|
|
82
|
+
@functools.wraps(func)
|
|
83
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
84
|
+
"""Sync wrapper for sync underlying functions."""
|
|
85
|
+
start_time = time.perf_counter()
|
|
86
|
+
try:
|
|
87
|
+
result = func(*args, **kwargs)
|
|
88
|
+
if isinstance(result, dict):
|
|
89
|
+
return _minify_response(result)
|
|
90
|
+
return result
|
|
91
|
+
except Exception as e:
|
|
92
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
93
|
+
_collect_tool_error(
|
|
94
|
+
tool_name=canonical_name,
|
|
95
|
+
error=e,
|
|
96
|
+
input_params=kwargs,
|
|
97
|
+
duration_ms=duration_ms,
|
|
98
|
+
)
|
|
99
|
+
raise
|
|
100
|
+
|
|
101
|
+
wrapper = sync_wrapper
|
|
102
|
+
|
|
103
|
+
# Apply mcp_tool first, then register with FastMCP
|
|
104
|
+
instrumented = mcp_tool(tool_name=canonical_name)(wrapper)
|
|
105
|
+
return mcp.tool(name=canonical_name, **tool_kwargs)(instrumented)
|
|
106
|
+
|
|
107
|
+
return decorator
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _collect_tool_error(
|
|
111
|
+
tool_name: str,
|
|
112
|
+
error: Exception,
|
|
113
|
+
input_params: dict[str, Any],
|
|
114
|
+
duration_ms: float,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Collect error data for later introspection.
|
|
117
|
+
|
|
118
|
+
Uses lazy import to avoid circular dependencies and only
|
|
119
|
+
collects if error collection is enabled.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
tool_name: Name of the tool that raised the error
|
|
123
|
+
error: The exception that was raised
|
|
124
|
+
input_params: Input parameters passed to the tool
|
|
125
|
+
duration_ms: Duration in milliseconds before error
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
# Lazy import to avoid circular dependencies
|
|
129
|
+
from foundry_mcp.config import get_config
|
|
130
|
+
|
|
131
|
+
config = get_config()
|
|
132
|
+
if not config.error_collection.enabled:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
from foundry_mcp.core.error_collection import get_error_collector
|
|
136
|
+
|
|
137
|
+
collector = get_error_collector()
|
|
138
|
+
collector.collect_tool_error(
|
|
139
|
+
tool_name=tool_name,
|
|
140
|
+
error=error,
|
|
141
|
+
input_params=input_params,
|
|
142
|
+
duration_ms=duration_ms,
|
|
143
|
+
)
|
|
144
|
+
except Exception as collect_error:
|
|
145
|
+
# Never let error collection failures affect tool execution
|
|
146
|
+
logger.debug(f"Error collection failed for {tool_name}: {collect_error}")
|