foundry-mcp 0.3.3__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.
Files changed (135) hide show
  1. foundry_mcp/__init__.py +7 -0
  2. foundry_mcp/cli/__init__.py +80 -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 +633 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +652 -0
  15. foundry_mcp/cli/commands/session.py +479 -0
  16. foundry_mcp/cli/commands/specs.py +856 -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 +259 -0
  22. foundry_mcp/cli/flags.py +266 -0
  23. foundry_mcp/cli/logging.py +212 -0
  24. foundry_mcp/cli/main.py +44 -0
  25. foundry_mcp/cli/output.py +122 -0
  26. foundry_mcp/cli/registry.py +110 -0
  27. foundry_mcp/cli/resilience.py +178 -0
  28. foundry_mcp/cli/transcript.py +217 -0
  29. foundry_mcp/config.py +850 -0
  30. foundry_mcp/core/__init__.py +144 -0
  31. foundry_mcp/core/ai_consultation.py +1636 -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/feature_flags.py +592 -0
  40. foundry_mcp/core/health.py +749 -0
  41. foundry_mcp/core/journal.py +694 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1350 -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 +123 -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 +317 -0
  57. foundry_mcp/core/prometheus.py +577 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +546 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
  61. foundry_mcp/core/prompts/plan_review.py +623 -0
  62. foundry_mcp/core/providers/__init__.py +225 -0
  63. foundry_mcp/core/providers/base.py +476 -0
  64. foundry_mcp/core/providers/claude.py +460 -0
  65. foundry_mcp/core/providers/codex.py +619 -0
  66. foundry_mcp/core/providers/cursor_agent.py +642 -0
  67. foundry_mcp/core/providers/detectors.py +488 -0
  68. foundry_mcp/core/providers/gemini.py +405 -0
  69. foundry_mcp/core/providers/opencode.py +616 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +302 -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 +729 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/resilience.py +600 -0
  78. foundry_mcp/core/responses.py +934 -0
  79. foundry_mcp/core/review.py +366 -0
  80. foundry_mcp/core/security.py +438 -0
  81. foundry_mcp/core/spec.py +1650 -0
  82. foundry_mcp/core/task.py +1289 -0
  83. foundry_mcp/core/testing.py +450 -0
  84. foundry_mcp/core/validation.py +2081 -0
  85. foundry_mcp/dashboard/__init__.py +32 -0
  86. foundry_mcp/dashboard/app.py +119 -0
  87. foundry_mcp/dashboard/components/__init__.py +17 -0
  88. foundry_mcp/dashboard/components/cards.py +88 -0
  89. foundry_mcp/dashboard/components/charts.py +234 -0
  90. foundry_mcp/dashboard/components/filters.py +136 -0
  91. foundry_mcp/dashboard/components/tables.py +195 -0
  92. foundry_mcp/dashboard/data/__init__.py +11 -0
  93. foundry_mcp/dashboard/data/stores.py +433 -0
  94. foundry_mcp/dashboard/launcher.py +289 -0
  95. foundry_mcp/dashboard/views/__init__.py +12 -0
  96. foundry_mcp/dashboard/views/errors.py +217 -0
  97. foundry_mcp/dashboard/views/metrics.py +174 -0
  98. foundry_mcp/dashboard/views/overview.py +160 -0
  99. foundry_mcp/dashboard/views/providers.py +83 -0
  100. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  101. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  102. foundry_mcp/prompts/__init__.py +9 -0
  103. foundry_mcp/prompts/workflows.py +525 -0
  104. foundry_mcp/resources/__init__.py +9 -0
  105. foundry_mcp/resources/specs.py +591 -0
  106. foundry_mcp/schemas/__init__.py +38 -0
  107. foundry_mcp/schemas/sdd-spec-schema.json +386 -0
  108. foundry_mcp/server.py +164 -0
  109. foundry_mcp/tools/__init__.py +10 -0
  110. foundry_mcp/tools/unified/__init__.py +71 -0
  111. foundry_mcp/tools/unified/authoring.py +1487 -0
  112. foundry_mcp/tools/unified/context_helpers.py +98 -0
  113. foundry_mcp/tools/unified/documentation_helpers.py +198 -0
  114. foundry_mcp/tools/unified/environment.py +939 -0
  115. foundry_mcp/tools/unified/error.py +462 -0
  116. foundry_mcp/tools/unified/health.py +225 -0
  117. foundry_mcp/tools/unified/journal.py +841 -0
  118. foundry_mcp/tools/unified/lifecycle.py +632 -0
  119. foundry_mcp/tools/unified/metrics.py +777 -0
  120. foundry_mcp/tools/unified/plan.py +745 -0
  121. foundry_mcp/tools/unified/pr.py +294 -0
  122. foundry_mcp/tools/unified/provider.py +629 -0
  123. foundry_mcp/tools/unified/review.py +685 -0
  124. foundry_mcp/tools/unified/review_helpers.py +299 -0
  125. foundry_mcp/tools/unified/router.py +102 -0
  126. foundry_mcp/tools/unified/server.py +580 -0
  127. foundry_mcp/tools/unified/spec.py +808 -0
  128. foundry_mcp/tools/unified/task.py +2202 -0
  129. foundry_mcp/tools/unified/test.py +370 -0
  130. foundry_mcp/tools/unified/verification.py +520 -0
  131. foundry_mcp-0.3.3.dist-info/METADATA +337 -0
  132. foundry_mcp-0.3.3.dist-info/RECORD +135 -0
  133. foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
  134. foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
  135. foundry_mcp-0.3.3.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,123 @@
1
+ """Naming helpers for MCP tool registration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import functools
7
+ import logging
8
+ import time
9
+ from typing import Any, Callable
10
+
11
+ from mcp.server.fastmcp import FastMCP
12
+
13
+ from foundry_mcp.core.observability import mcp_tool
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def canonical_tool(
19
+ mcp: FastMCP,
20
+ *,
21
+ canonical_name: str,
22
+ **tool_kwargs: Any,
23
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
24
+ """Decorator that registers a tool under its canonical name.
25
+
26
+ This decorator wraps the tool function to:
27
+ 1. Register it with FastMCP under the canonical name
28
+ 2. Apply observability instrumentation via mcp_tool
29
+ 3. Collect error data when exceptions occur
30
+
31
+ Args:
32
+ mcp: FastMCP instance
33
+ canonical_name: The canonical name for the tool
34
+ **tool_kwargs: Additional kwargs passed to mcp.tool()
35
+
36
+ Returns:
37
+ Decorated function registered as an MCP tool
38
+ """
39
+
40
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
41
+ if asyncio.iscoroutinefunction(func):
42
+ # Async function - use async wrapper
43
+ @functools.wraps(func)
44
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
45
+ """Async wrapper for async underlying functions."""
46
+ start_time = time.perf_counter()
47
+ try:
48
+ return await func(*args, **kwargs)
49
+ except Exception as e:
50
+ duration_ms = (time.perf_counter() - start_time) * 1000
51
+ _collect_tool_error(
52
+ tool_name=canonical_name,
53
+ error=e,
54
+ input_params=kwargs,
55
+ duration_ms=duration_ms,
56
+ )
57
+ raise
58
+
59
+ wrapper = async_wrapper
60
+ else:
61
+ # Sync function - use sync wrapper to preserve sync behavior
62
+ @functools.wraps(func)
63
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
64
+ """Sync wrapper for sync underlying functions."""
65
+ start_time = time.perf_counter()
66
+ try:
67
+ return func(*args, **kwargs)
68
+ except Exception as e:
69
+ duration_ms = (time.perf_counter() - start_time) * 1000
70
+ _collect_tool_error(
71
+ tool_name=canonical_name,
72
+ error=e,
73
+ input_params=kwargs,
74
+ duration_ms=duration_ms,
75
+ )
76
+ raise
77
+
78
+ wrapper = sync_wrapper
79
+
80
+ # Apply mcp_tool first, then register with FastMCP
81
+ instrumented = mcp_tool(tool_name=canonical_name)(wrapper)
82
+ return mcp.tool(name=canonical_name, **tool_kwargs)(instrumented)
83
+
84
+ return decorator
85
+
86
+
87
+ def _collect_tool_error(
88
+ tool_name: str,
89
+ error: Exception,
90
+ input_params: dict[str, Any],
91
+ duration_ms: float,
92
+ ) -> None:
93
+ """Collect error data for later introspection.
94
+
95
+ Uses lazy import to avoid circular dependencies and only
96
+ collects if error collection is enabled.
97
+
98
+ Args:
99
+ tool_name: Name of the tool that raised the error
100
+ error: The exception that was raised
101
+ input_params: Input parameters passed to the tool
102
+ duration_ms: Duration in milliseconds before error
103
+ """
104
+ try:
105
+ # Lazy import to avoid circular dependencies
106
+ from foundry_mcp.config import get_config
107
+
108
+ config = get_config()
109
+ if not config.error_collection.enabled:
110
+ return
111
+
112
+ from foundry_mcp.core.error_collection import get_error_collector
113
+
114
+ collector = get_error_collector()
115
+ collector.collect_tool_error(
116
+ tool_name=tool_name,
117
+ error=error,
118
+ input_params=input_params,
119
+ duration_ms=duration_ms,
120
+ )
121
+ except Exception as collect_error:
122
+ # Never let error collection failures affect tool execution
123
+ logger.debug(f"Error collection failed for {tool_name}: {collect_error}")