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,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}")