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,98 @@
1
+ """Context helpers shared by unified tool routers.
2
+
3
+ These helpers keep `server(action=...)` focused on routing/validation while
4
+ ensuring context/LLM status responses remain consistent and response-v2
5
+ compliant.
6
+
7
+ This module intentionally lives under `tools.unified` to avoid reintroducing
8
+ non-unified public tool surfaces.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from dataclasses import asdict
15
+ from typing import Any, Dict, Optional
16
+
17
+ from foundry_mcp.config import ServerConfig
18
+ from foundry_mcp.core.responses import (
19
+ ErrorCode,
20
+ ErrorType,
21
+ error_response,
22
+ success_response,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def build_llm_status_response(*, request_id: Optional[str] = None) -> dict:
29
+ """Return a standardized envelope describing LLM configuration."""
30
+
31
+ try:
32
+ from foundry_mcp.core.review import get_llm_status
33
+
34
+ llm_status = get_llm_status()
35
+ return asdict(success_response(llm_status=llm_status, request_id=request_id))
36
+ except Exception as exc:
37
+ logger.exception("Failed to build llm_status response")
38
+ return asdict(
39
+ error_response(
40
+ f"Failed to build llm_status response: {exc}",
41
+ error_code=ErrorCode.INTERNAL_ERROR,
42
+ error_type=ErrorType.INTERNAL,
43
+ remediation="Check server logs.",
44
+ request_id=request_id,
45
+ )
46
+ )
47
+
48
+
49
+ def build_server_context_response(
50
+ config: ServerConfig,
51
+ *,
52
+ include_llm: bool = True,
53
+ include_workflow: bool = True,
54
+ include_workspace: bool = True,
55
+ include_capabilities: bool = True,
56
+ request_id: Optional[str] = None,
57
+ ) -> dict:
58
+ """Build a standardized server context payload."""
59
+
60
+ payload: Dict[str, Any] = {
61
+ "server": {
62
+ "name": config.server_name,
63
+ "version": config.server_version,
64
+ "log_level": config.log_level,
65
+ },
66
+ "paths": {
67
+ "specs_dir": str(config.specs_dir) if config.specs_dir else None,
68
+ "journals_path": str(config.journals_path)
69
+ if config.journals_path
70
+ else None,
71
+ },
72
+ }
73
+
74
+ if include_workspace:
75
+ payload["workspace"] = {"roots": [str(p) for p in config.workspace_roots]}
76
+
77
+ if include_workflow:
78
+ payload["workflow"] = {"git": asdict(config.git)}
79
+
80
+ if include_llm:
81
+ try:
82
+ from foundry_mcp.core.review import get_llm_status
83
+
84
+ payload["llm_status"] = get_llm_status()
85
+ except Exception as exc:
86
+ logger.debug("Failed to compute llm_status: %s", exc)
87
+ payload["llm_status"] = {"configured": False, "error": "unavailable"}
88
+
89
+ if include_capabilities:
90
+ try:
91
+ from foundry_mcp.core.discovery import get_capabilities
92
+
93
+ payload["capabilities"] = get_capabilities()
94
+ except Exception as exc:
95
+ logger.debug("Failed to compute capabilities: %s", exc)
96
+ payload["capabilities"] = {}
97
+
98
+ return asdict(success_response(data=payload, request_id=request_id))
@@ -0,0 +1,198 @@
1
+ """Helpers for building review context sections (implementation artifacts, requirements, etc)."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+ from pathlib import Path
5
+
6
+
7
+ def _build_spec_requirements(
8
+ spec_data: Dict[str, Any], task_id: Optional[str], phase_id: Optional[str]
9
+ ) -> str:
10
+ lines: list[str] = []
11
+ if task_id:
12
+ task = _find_task(spec_data, task_id)
13
+ if task:
14
+ lines.append(f"### Task: {task.get('title', task_id)}")
15
+ lines.append(f"- **Status:** {task.get('status', 'unknown')}")
16
+ if task.get("metadata", {}).get("details"):
17
+ lines.append("- **Details:**")
18
+ for detail in task["metadata"]["details"]:
19
+ lines.append(f" - {detail}")
20
+ if task.get("metadata", {}).get("file_path"):
21
+ lines.append(f"- **Expected file:** {task['metadata']['file_path']}")
22
+ elif phase_id:
23
+ phase = _find_phase(spec_data, phase_id)
24
+ if phase:
25
+ lines.append(f"### Phase: {phase.get('title', phase_id)}")
26
+ lines.append(f"- **Status:** {phase.get('status', 'unknown')}")
27
+ child_nodes = _get_child_nodes(spec_data, phase)
28
+ if child_nodes:
29
+ lines.append("- **Tasks:**")
30
+ for child in child_nodes:
31
+ lines.append(
32
+ f" - {child.get('id', 'unknown')}: {child.get('title', 'Unknown task')}"
33
+ )
34
+ else:
35
+ lines.append(f"### Specification: {spec_data.get('title', 'Unknown')}")
36
+ if spec_data.get("description"):
37
+ lines.append(f"- **Description:** {spec_data['description']}")
38
+ if spec_data.get("assumptions"):
39
+ lines.append("- **Assumptions:**")
40
+ for assumption in spec_data["assumptions"][:5]:
41
+ if isinstance(assumption, dict):
42
+ lines.append(f" - {assumption.get('text', str(assumption))}")
43
+ else:
44
+ lines.append(f" - {assumption}")
45
+ return "\n".join(lines) if lines else "*No requirements available*"
46
+
47
+
48
+ def _build_implementation_artifacts(
49
+ spec_data: Dict[str, Any],
50
+ task_id: Optional[str],
51
+ phase_id: Optional[str],
52
+ files: Optional[List[str]],
53
+ incremental: bool,
54
+ base_branch: str,
55
+ ) -> str:
56
+ lines: list[str] = []
57
+ file_paths: list[str] = []
58
+ if files:
59
+ file_paths = list(files)
60
+ elif task_id:
61
+ task = _find_task(spec_data, task_id)
62
+ if task and task.get("metadata", {}).get("file_path"):
63
+ file_paths = [task["metadata"]["file_path"]]
64
+ elif phase_id:
65
+ phase = _find_phase(spec_data, phase_id)
66
+ if phase:
67
+ for child in _get_child_nodes(spec_data, phase):
68
+ if child.get("metadata", {}).get("file_path"):
69
+ file_paths.append(child["metadata"]["file_path"])
70
+ if incremental:
71
+ try:
72
+ import subprocess
73
+
74
+ result = subprocess.run(
75
+ ["git", "diff", "--name-only", base_branch],
76
+ capture_output=True,
77
+ text=True,
78
+ timeout=10,
79
+ )
80
+ if result.returncode == 0:
81
+ changed_files = (
82
+ result.stdout.strip().split("\n") if result.stdout else []
83
+ )
84
+ if file_paths:
85
+ file_paths = [path for path in file_paths if path in changed_files]
86
+ else:
87
+ file_paths = changed_files
88
+ lines.append(
89
+ f"*Incremental review: {len(file_paths)} changed files since {base_branch}*\n"
90
+ )
91
+ except Exception:
92
+ lines.append(f"*Warning: Could not get git diff from {base_branch}*\n")
93
+ for file_path in file_paths[:5]:
94
+ path = Path(file_path)
95
+ if path.exists():
96
+ try:
97
+ content = path.read_text(encoding="utf-8")
98
+ if len(content) > 10_000:
99
+ content = content[:10_000] + "\n... [truncated] ..."
100
+ file_type = path.suffix.lstrip(".") or "text"
101
+ lines.append(f"### File: `{file_path}`")
102
+ lines.append(f"```{file_type}")
103
+ lines.append(content)
104
+ lines.append("```\n")
105
+ except Exception as exc:
106
+ lines.append(f"### File: `{file_path}`")
107
+ lines.append(f"*Error reading file: {exc}*\n")
108
+ else:
109
+ lines.append(f"### File: `{file_path}`")
110
+ lines.append("*File not found*\n")
111
+ if not lines:
112
+ lines.append("*No implementation artifacts available*")
113
+ return "\n".join(lines)
114
+
115
+
116
+ def _build_test_results(
117
+ spec_data: Dict[str, Any], task_id: Optional[str], phase_id: Optional[str]
118
+ ) -> str:
119
+ journal = spec_data.get("journal", [])
120
+ test_entries = [
121
+ entry
122
+ for entry in journal
123
+ if "test" in entry.get("title", "").lower()
124
+ or "verify" in entry.get("title", "").lower()
125
+ ]
126
+ if test_entries:
127
+ lines = ["*Recent test-related journal entries:*"]
128
+ for entry in test_entries[-3:]:
129
+ lines.append(
130
+ f"- **{entry.get('title', 'Unknown')}** ({entry.get('timestamp', 'unknown')})"
131
+ )
132
+ if entry.get("content"):
133
+ content = entry["content"][:500]
134
+ if len(entry["content"]) > 500:
135
+ content += "..."
136
+ lines.append(f" {content}")
137
+ return "\n".join(lines)
138
+ return "*No test results available*"
139
+
140
+
141
+ def _build_journal_entries(
142
+ spec_data: Dict[str, Any], task_id: Optional[str], phase_id: Optional[str]
143
+ ) -> str:
144
+ journal = spec_data.get("journal", [])
145
+ if task_id:
146
+ journal = [entry for entry in journal if entry.get("task_id") == task_id]
147
+ if journal:
148
+ lines = [f"*{len(journal)} journal entries found:*"]
149
+ for entry in journal[-5:]:
150
+ entry_type = entry.get("entry_type", "note")
151
+ timestamp = (
152
+ entry.get("timestamp", "unknown")[:10]
153
+ if entry.get("timestamp")
154
+ else "unknown"
155
+ )
156
+ lines.append(
157
+ f"- **[{entry_type}]** {entry.get('title', 'Untitled')} ({timestamp})"
158
+ )
159
+ return "\n".join(lines)
160
+ return "*No journal entries found*"
161
+
162
+
163
+ def _find_task(spec_data: Dict[str, Any], task_id: str) -> Optional[Dict[str, Any]]:
164
+ hierarchy_nodes = _get_hierarchy_nodes(spec_data)
165
+ if task_id in hierarchy_nodes:
166
+ return hierarchy_nodes[task_id]
167
+ return None
168
+
169
+
170
+ def _find_phase(spec_data: Dict[str, Any], phase_id: str) -> Optional[Dict[str, Any]]:
171
+ hierarchy_nodes = _get_hierarchy_nodes(spec_data)
172
+ if phase_id in hierarchy_nodes:
173
+ return hierarchy_nodes[phase_id]
174
+ return None
175
+
176
+
177
+ def _get_hierarchy_nodes(spec_data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
178
+ hierarchy = spec_data.get("hierarchy", {})
179
+ nodes: Dict[str, Dict[str, Any]] = {}
180
+ if isinstance(hierarchy, dict):
181
+ if all(isinstance(value, dict) for value in hierarchy.values()):
182
+ for node_id, node in hierarchy.items():
183
+ node_copy = dict(node)
184
+ node_copy.setdefault("id", node_id)
185
+ nodes[node_id] = node_copy
186
+ return nodes
187
+
188
+
189
+ def _get_child_nodes(
190
+ spec_data: Dict[str, Any], node: Dict[str, Any]
191
+ ) -> List[Dict[str, Any]]:
192
+ hierarchy_nodes = _get_hierarchy_nodes(spec_data)
193
+ children = node.get("children", [])
194
+ return [
195
+ hierarchy_nodes[child_id]
196
+ for child_id in children
197
+ if child_id in hierarchy_nodes
198
+ ]