foundry-mcp 0.7.0__py3-none-any.whl → 0.8.10__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.
- foundry_mcp/cli/__init__.py +0 -13
- foundry_mcp/cli/commands/session.py +1 -8
- foundry_mcp/cli/context.py +39 -0
- foundry_mcp/config.py +381 -7
- foundry_mcp/core/batch_operations.py +1196 -0
- foundry_mcp/core/discovery.py +1 -1
- foundry_mcp/core/llm_config.py +8 -0
- foundry_mcp/core/naming.py +25 -2
- foundry_mcp/core/prometheus.py +0 -13
- foundry_mcp/core/providers/__init__.py +12 -0
- foundry_mcp/core/providers/base.py +39 -0
- foundry_mcp/core/providers/claude.py +45 -1
- foundry_mcp/core/providers/codex.py +64 -3
- foundry_mcp/core/providers/cursor_agent.py +22 -3
- foundry_mcp/core/providers/detectors.py +34 -7
- foundry_mcp/core/providers/gemini.py +63 -1
- foundry_mcp/core/providers/opencode.py +95 -71
- foundry_mcp/core/providers/package-lock.json +4 -4
- foundry_mcp/core/providers/package.json +1 -1
- foundry_mcp/core/providers/validation.py +128 -0
- foundry_mcp/core/research/memory.py +103 -0
- foundry_mcp/core/research/models.py +783 -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 +5 -2
- foundry_mcp/core/research/workflows/base.py +106 -12
- foundry_mcp/core/research/workflows/consensus.py +160 -17
- foundry_mcp/core/research/workflows/deep_research.py +4020 -0
- foundry_mcp/core/responses.py +240 -0
- foundry_mcp/core/spec.py +1 -0
- foundry_mcp/core/task.py +141 -12
- foundry_mcp/core/validation.py +6 -1
- foundry_mcp/server.py +0 -52
- foundry_mcp/tools/unified/__init__.py +37 -18
- foundry_mcp/tools/unified/authoring.py +0 -33
- foundry_mcp/tools/unified/environment.py +202 -29
- foundry_mcp/tools/unified/plan.py +20 -1
- foundry_mcp/tools/unified/provider.py +0 -40
- foundry_mcp/tools/unified/research.py +644 -19
- foundry_mcp/tools/unified/review.py +5 -2
- foundry_mcp/tools/unified/review_helpers.py +16 -1
- foundry_mcp/tools/unified/server.py +9 -24
- foundry_mcp/tools/unified/task.py +528 -9
- {foundry_mcp-0.7.0.dist-info → foundry_mcp-0.8.10.dist-info}/METADATA +2 -1
- {foundry_mcp-0.7.0.dist-info → foundry_mcp-0.8.10.dist-info}/RECORD +52 -46
- foundry_mcp/cli/flags.py +0 -266
- foundry_mcp/core/feature_flags.py +0 -592
- {foundry_mcp-0.7.0.dist-info → foundry_mcp-0.8.10.dist-info}/WHEEL +0 -0
- {foundry_mcp-0.7.0.dist-info → foundry_mcp-0.8.10.dist-info}/entry_points.txt +0 -0
- {foundry_mcp-0.7.0.dist-info → foundry_mcp-0.8.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -29,27 +29,46 @@ if TYPE_CHECKING: # pragma: no cover - import-time typing only
|
|
|
29
29
|
|
|
30
30
|
def register_unified_tools(mcp: "FastMCP", config: "ServerConfig") -> None:
|
|
31
31
|
"""Register all unified tool routers."""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
disabled = set(config.disabled_tools)
|
|
33
|
+
|
|
34
|
+
if "health" not in disabled:
|
|
35
|
+
register_unified_health_tool(mcp, config)
|
|
36
|
+
if "plan" not in disabled:
|
|
37
|
+
register_unified_plan_tool(mcp, config)
|
|
38
|
+
if "pr" not in disabled:
|
|
39
|
+
register_unified_pr_tool(mcp, config)
|
|
40
|
+
if "error" not in disabled:
|
|
41
|
+
register_unified_error_tool(mcp, config)
|
|
42
|
+
if "metrics" not in disabled:
|
|
43
|
+
register_unified_metrics_tool(mcp, config)
|
|
44
|
+
if "journal" not in disabled:
|
|
45
|
+
register_unified_journal_tool(mcp, config)
|
|
46
|
+
if "authoring" not in disabled:
|
|
47
|
+
register_unified_authoring_tool(mcp, config)
|
|
48
|
+
if "review" not in disabled:
|
|
49
|
+
register_unified_review_tool(mcp, config)
|
|
50
|
+
if "spec" not in disabled:
|
|
51
|
+
register_unified_spec_tool(mcp, config)
|
|
41
52
|
|
|
42
53
|
from importlib import import_module
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
if "task" not in disabled:
|
|
56
|
+
_task_router = import_module("foundry_mcp.tools.unified.task")
|
|
57
|
+
_task_router.register_unified_task_tool(mcp, config)
|
|
58
|
+
if "provider" not in disabled:
|
|
59
|
+
register_unified_provider_tool(mcp, config)
|
|
60
|
+
if "environment" not in disabled:
|
|
61
|
+
register_unified_environment_tool(mcp, config)
|
|
62
|
+
if "lifecycle" not in disabled:
|
|
63
|
+
register_unified_lifecycle_tool(mcp, config)
|
|
64
|
+
if "verification" not in disabled:
|
|
65
|
+
register_unified_verification_tool(mcp, config)
|
|
66
|
+
if "server" not in disabled:
|
|
67
|
+
register_unified_server_tool(mcp, config)
|
|
68
|
+
if "test" not in disabled:
|
|
69
|
+
register_unified_test_tool(mcp, config)
|
|
70
|
+
if "research" not in disabled:
|
|
71
|
+
register_unified_research_tool(mcp, config)
|
|
53
72
|
|
|
54
73
|
|
|
55
74
|
__all__ = [
|
|
@@ -13,7 +13,6 @@ from mcp.server.fastmcp import FastMCP
|
|
|
13
13
|
|
|
14
14
|
from foundry_mcp.config import ServerConfig
|
|
15
15
|
from foundry_mcp.core.context import generate_correlation_id, get_correlation_id
|
|
16
|
-
from foundry_mcp.core.feature_flags import FeatureFlag, FlagState, get_flag_service
|
|
17
16
|
from foundry_mcp.core.intake import IntakeStore, LockAcquisitionError, INTAKE_ID_PATTERN
|
|
18
17
|
from foundry_mcp.core.naming import canonical_tool
|
|
19
18
|
from foundry_mcp.core.observability import audit_log, get_metrics, mcp_tool
|
|
@@ -57,38 +56,6 @@ from foundry_mcp.tools.unified.router import (
|
|
|
57
56
|
logger = logging.getLogger(__name__)
|
|
58
57
|
_metrics = get_metrics()
|
|
59
58
|
|
|
60
|
-
# Register intake_tools feature flag
|
|
61
|
-
_flag_service = get_flag_service()
|
|
62
|
-
try:
|
|
63
|
-
_flag_service.register(
|
|
64
|
-
FeatureFlag(
|
|
65
|
-
name="intake_tools",
|
|
66
|
-
description="Bikelane intake queue tools (add, list, dismiss)",
|
|
67
|
-
state=FlagState.EXPERIMENTAL,
|
|
68
|
-
default_enabled=False,
|
|
69
|
-
)
|
|
70
|
-
)
|
|
71
|
-
except ValueError:
|
|
72
|
-
pass # Flag already registered
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _intake_feature_flag_blocked(request_id: str) -> Optional[dict]:
|
|
76
|
-
"""Check if intake tools are blocked by feature flag."""
|
|
77
|
-
if _flag_service.is_enabled("intake_tools"):
|
|
78
|
-
return None
|
|
79
|
-
|
|
80
|
-
return asdict(
|
|
81
|
-
error_response(
|
|
82
|
-
"Intake tools are disabled by feature flag",
|
|
83
|
-
error_code=ErrorCode.FEATURE_DISABLED,
|
|
84
|
-
error_type=ErrorType.FEATURE_FLAG,
|
|
85
|
-
data={"feature": "intake_tools"},
|
|
86
|
-
remediation="Enable the 'intake_tools' feature flag to use intake actions.",
|
|
87
|
-
request_id=request_id,
|
|
88
|
-
)
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
59
|
_ACTION_SUMMARY = {
|
|
93
60
|
"spec-create": "Scaffold a new SDD specification",
|
|
94
61
|
"spec-template": "List/show/apply spec templates",
|
|
@@ -15,7 +15,6 @@ from mcp.server.fastmcp import FastMCP
|
|
|
15
15
|
|
|
16
16
|
from foundry_mcp.config import ServerConfig, _PACKAGE_VERSION
|
|
17
17
|
from foundry_mcp.core.context import generate_correlation_id, get_correlation_id
|
|
18
|
-
from foundry_mcp.core.feature_flags import FeatureFlag, FlagState, get_flag_service
|
|
19
18
|
from foundry_mcp.core.naming import canonical_tool
|
|
20
19
|
from foundry_mcp.core.observability import audit_log, get_metrics, mcp_tool
|
|
21
20
|
from foundry_mcp.core.responses import (
|
|
@@ -32,18 +31,6 @@ from foundry_mcp.tools.unified.router import (
|
|
|
32
31
|
|
|
33
32
|
logger = logging.getLogger(__name__)
|
|
34
33
|
_metrics = get_metrics()
|
|
35
|
-
_flag_service = get_flag_service()
|
|
36
|
-
try:
|
|
37
|
-
_flag_service.register(
|
|
38
|
-
FeatureFlag(
|
|
39
|
-
name="environment_tools",
|
|
40
|
-
description="Environment readiness and workspace tooling",
|
|
41
|
-
state=FlagState.BETA,
|
|
42
|
-
default_enabled=True,
|
|
43
|
-
)
|
|
44
|
-
)
|
|
45
|
-
except ValueError:
|
|
46
|
-
pass
|
|
47
34
|
|
|
48
35
|
_DEFAULT_TOML_TEMPLATE = """[workspace]
|
|
49
36
|
specs_dir = "./specs"
|
|
@@ -52,23 +39,46 @@ specs_dir = "./specs"
|
|
|
52
39
|
level = "INFO"
|
|
53
40
|
structured = true
|
|
54
41
|
|
|
55
|
-
[
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
[tools]
|
|
43
|
+
# Disable tools to reduce context window usage
|
|
44
|
+
# Available: health, plan, pr, error, metrics, journal, authoring, review,
|
|
45
|
+
# spec, task, provider, environment, lifecycle, verification,
|
|
46
|
+
# server, test, research
|
|
47
|
+
disabled_tools = ["error", "metrics", "health"]
|
|
58
48
|
|
|
59
49
|
[workflow]
|
|
60
50
|
mode = "single"
|
|
61
51
|
auto_validate = true
|
|
62
52
|
journal_enabled = true
|
|
63
53
|
|
|
54
|
+
[implement]
|
|
55
|
+
# Default flags for /implement command (can be overridden via CLI flags)
|
|
56
|
+
auto = false # --auto: skip prompts between tasks
|
|
57
|
+
delegate = false # --delegate: use subagent(s) for implementation
|
|
58
|
+
parallel = false # --parallel: run subagents concurrently (implies delegate)
|
|
59
|
+
|
|
64
60
|
[consultation]
|
|
65
61
|
# priority = [] # Appended by setup based on detected providers
|
|
66
62
|
default_timeout = 300
|
|
63
|
+
|
|
64
|
+
[research]
|
|
65
|
+
# Research tool configuration (chat, consensus, thinkdeep, ideate, deep)
|
|
66
|
+
# default_provider = "[cli]provider:model" # Appended by setup
|
|
67
|
+
# consensus_providers = [] # Appended by setup (same as consultation.priority)
|
|
67
68
|
max_retries = 2
|
|
68
69
|
retry_delay = 5.0
|
|
69
70
|
fallback_enabled = true
|
|
70
71
|
cache_ttl = 3600
|
|
71
72
|
|
|
73
|
+
[research.deep]
|
|
74
|
+
# Deep research workflow settings
|
|
75
|
+
max_iterations = 3
|
|
76
|
+
max_sub_queries = 5
|
|
77
|
+
max_sources_per_query = 5
|
|
78
|
+
follow_links = true
|
|
79
|
+
max_concurrent = 3
|
|
80
|
+
timeout_per_operation = 120
|
|
81
|
+
|
|
72
82
|
[consultation.workflows.fidelity_review]
|
|
73
83
|
min_models = 2
|
|
74
84
|
timeout_override = 600.0
|
|
@@ -220,6 +230,7 @@ _ACTION_SUMMARY = {
|
|
|
220
230
|
"detect": "Detect repository topology (project type, specs/docs)",
|
|
221
231
|
"detect-test-runner": "Detect appropriate test runner for the project",
|
|
222
232
|
"setup": "Complete SDD setup with permissions + config",
|
|
233
|
+
"get-config": "Read configuration sections from foundry-mcp.toml",
|
|
223
234
|
}
|
|
224
235
|
|
|
225
236
|
|
|
@@ -232,19 +243,8 @@ def _request_id() -> str:
|
|
|
232
243
|
|
|
233
244
|
|
|
234
245
|
def _feature_flag_blocked(request_id: str) -> Optional[dict]:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return asdict(
|
|
239
|
-
error_response(
|
|
240
|
-
"Environment tools are disabled by feature flag",
|
|
241
|
-
error_code=ErrorCode.FEATURE_DISABLED,
|
|
242
|
-
error_type=ErrorType.FEATURE_FLAG,
|
|
243
|
-
data={"feature": "environment_tools"},
|
|
244
|
-
remediation="Enable the 'environment_tools' feature flag to call environment actions.",
|
|
245
|
-
request_id=request_id,
|
|
246
|
-
)
|
|
247
|
-
)
|
|
246
|
+
# Feature flags disabled - always allow
|
|
247
|
+
return None
|
|
248
248
|
|
|
249
249
|
|
|
250
250
|
def _validation_error(
|
|
@@ -1056,6 +1056,169 @@ def _handle_setup(
|
|
|
1056
1056
|
)
|
|
1057
1057
|
|
|
1058
1058
|
|
|
1059
|
+
def _handle_get_config(
|
|
1060
|
+
*,
|
|
1061
|
+
config: ServerConfig, # noqa: ARG001 - config object available but we read TOML directly
|
|
1062
|
+
sections: Optional[List[str]] = None,
|
|
1063
|
+
key: Optional[str] = None,
|
|
1064
|
+
**_: Any,
|
|
1065
|
+
) -> dict:
|
|
1066
|
+
"""Read configuration sections from foundry-mcp.toml.
|
|
1067
|
+
|
|
1068
|
+
Returns the requested sections from the TOML config file.
|
|
1069
|
+
Supported sections: implement, git.
|
|
1070
|
+
|
|
1071
|
+
Args:
|
|
1072
|
+
sections: List of section names to return (default: all supported sections)
|
|
1073
|
+
key: Specific key within section (only valid when requesting single section)
|
|
1074
|
+
"""
|
|
1075
|
+
import tomllib
|
|
1076
|
+
|
|
1077
|
+
request_id = _request_id()
|
|
1078
|
+
blocked = _feature_flag_blocked(request_id)
|
|
1079
|
+
if blocked:
|
|
1080
|
+
return blocked
|
|
1081
|
+
|
|
1082
|
+
# Validate sections parameter
|
|
1083
|
+
supported_sections = {"implement", "git"}
|
|
1084
|
+
if sections is not None:
|
|
1085
|
+
if not isinstance(sections, list):
|
|
1086
|
+
return _validation_error(
|
|
1087
|
+
action="get-config",
|
|
1088
|
+
field="sections",
|
|
1089
|
+
message="Expected a list of section names",
|
|
1090
|
+
request_id=request_id,
|
|
1091
|
+
code=ErrorCode.INVALID_FORMAT,
|
|
1092
|
+
)
|
|
1093
|
+
invalid = set(sections) - supported_sections
|
|
1094
|
+
if invalid:
|
|
1095
|
+
return _validation_error(
|
|
1096
|
+
action="get-config",
|
|
1097
|
+
field="sections",
|
|
1098
|
+
message=f"Unsupported sections: {', '.join(sorted(invalid))}. Supported: {', '.join(sorted(supported_sections))}",
|
|
1099
|
+
request_id=request_id,
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
# Validate key parameter
|
|
1103
|
+
if key is not None:
|
|
1104
|
+
if not isinstance(key, str):
|
|
1105
|
+
return _validation_error(
|
|
1106
|
+
action="get-config",
|
|
1107
|
+
field="key",
|
|
1108
|
+
message="Expected a string",
|
|
1109
|
+
request_id=request_id,
|
|
1110
|
+
code=ErrorCode.INVALID_FORMAT,
|
|
1111
|
+
)
|
|
1112
|
+
if sections is None or len(sections) != 1:
|
|
1113
|
+
return _validation_error(
|
|
1114
|
+
action="get-config",
|
|
1115
|
+
field="key",
|
|
1116
|
+
message="The 'key' parameter is only valid when requesting exactly one section",
|
|
1117
|
+
request_id=request_id,
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
metric_key = _metric_name("get-config")
|
|
1121
|
+
try:
|
|
1122
|
+
# Find the TOML config file
|
|
1123
|
+
toml_path = None
|
|
1124
|
+
for candidate in ["foundry-mcp.toml", ".foundry-mcp.toml"]:
|
|
1125
|
+
if Path(candidate).exists():
|
|
1126
|
+
toml_path = Path(candidate)
|
|
1127
|
+
break
|
|
1128
|
+
|
|
1129
|
+
if not toml_path:
|
|
1130
|
+
_metrics.counter(metric_key, labels={"status": "not_found"})
|
|
1131
|
+
return asdict(
|
|
1132
|
+
error_response(
|
|
1133
|
+
"No foundry-mcp.toml config file found",
|
|
1134
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
1135
|
+
error_type=ErrorType.NOT_FOUND,
|
|
1136
|
+
remediation="Run environment(action=setup) to create the config file",
|
|
1137
|
+
request_id=request_id,
|
|
1138
|
+
)
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
# Read and parse TOML
|
|
1142
|
+
with open(toml_path, "rb") as f:
|
|
1143
|
+
data = tomllib.load(f)
|
|
1144
|
+
|
|
1145
|
+
# Determine which sections to return
|
|
1146
|
+
requested = set(sections) if sections else supported_sections
|
|
1147
|
+
|
|
1148
|
+
# Build result with only supported sections
|
|
1149
|
+
result: Dict[str, Any] = {}
|
|
1150
|
+
|
|
1151
|
+
if "implement" in requested and "implement" in data:
|
|
1152
|
+
impl_data = data["implement"]
|
|
1153
|
+
result["implement"] = {
|
|
1154
|
+
"auto": impl_data.get("auto", False),
|
|
1155
|
+
"delegate": impl_data.get("delegate", False),
|
|
1156
|
+
"parallel": impl_data.get("parallel", False),
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if "git" in requested and "git" in data:
|
|
1160
|
+
git_data = data["git"]
|
|
1161
|
+
result["git"] = {
|
|
1162
|
+
"enabled": git_data.get("enabled", True),
|
|
1163
|
+
"auto_commit": git_data.get("auto_commit", False),
|
|
1164
|
+
"auto_push": git_data.get("auto_push", False),
|
|
1165
|
+
"auto_pr": git_data.get("auto_pr", False),
|
|
1166
|
+
"commit_cadence": git_data.get("commit_cadence", "task"),
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
# If sections were requested but not found, include them as empty/defaults
|
|
1170
|
+
for section in requested:
|
|
1171
|
+
if section not in result:
|
|
1172
|
+
if section == "implement":
|
|
1173
|
+
result["implement"] = {
|
|
1174
|
+
"auto": False,
|
|
1175
|
+
"delegate": False,
|
|
1176
|
+
"parallel": False,
|
|
1177
|
+
}
|
|
1178
|
+
elif section == "git":
|
|
1179
|
+
result["git"] = {
|
|
1180
|
+
"enabled": True,
|
|
1181
|
+
"auto_commit": False,
|
|
1182
|
+
"auto_push": False,
|
|
1183
|
+
"auto_pr": False,
|
|
1184
|
+
"commit_cadence": "task",
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
# If a specific key was requested, extract just that value
|
|
1188
|
+
if key is not None:
|
|
1189
|
+
section_name = sections[0] # Already validated to be exactly one section
|
|
1190
|
+
section_data = result.get(section_name, {})
|
|
1191
|
+
if key not in section_data:
|
|
1192
|
+
return _validation_error(
|
|
1193
|
+
action="get-config",
|
|
1194
|
+
field="key",
|
|
1195
|
+
message=f"Key '{key}' not found in section '{section_name}'",
|
|
1196
|
+
request_id=request_id,
|
|
1197
|
+
code=ErrorCode.NOT_FOUND,
|
|
1198
|
+
)
|
|
1199
|
+
result = {section_name: {key: section_data[key]}}
|
|
1200
|
+
|
|
1201
|
+
_metrics.counter(metric_key, labels={"status": "success"})
|
|
1202
|
+
return asdict(
|
|
1203
|
+
success_response(
|
|
1204
|
+
data={"sections": result, "config_file": str(toml_path)},
|
|
1205
|
+
request_id=request_id,
|
|
1206
|
+
)
|
|
1207
|
+
)
|
|
1208
|
+
except Exception as exc:
|
|
1209
|
+
logger.exception("Error reading config")
|
|
1210
|
+
_metrics.counter(metric_key, labels={"status": "error"})
|
|
1211
|
+
return asdict(
|
|
1212
|
+
error_response(
|
|
1213
|
+
f"Failed to read config: {exc}",
|
|
1214
|
+
error_code=ErrorCode.INTERNAL_ERROR,
|
|
1215
|
+
error_type=ErrorType.INTERNAL,
|
|
1216
|
+
remediation="Check foundry-mcp.toml syntax and retry",
|
|
1217
|
+
request_id=request_id,
|
|
1218
|
+
)
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
|
|
1059
1222
|
_ENVIRONMENT_ROUTER = ActionRouter(
|
|
1060
1223
|
tool_name="environment",
|
|
1061
1224
|
actions=[
|
|
@@ -1103,6 +1266,12 @@ _ENVIRONMENT_ROUTER = ActionRouter(
|
|
|
1103
1266
|
summary=_ACTION_SUMMARY["setup"],
|
|
1104
1267
|
aliases=("sdd-setup", "sdd_setup"),
|
|
1105
1268
|
),
|
|
1269
|
+
ActionDefinition(
|
|
1270
|
+
name="get-config",
|
|
1271
|
+
handler=_handle_get_config,
|
|
1272
|
+
summary=_ACTION_SUMMARY["get-config"],
|
|
1273
|
+
aliases=("config", "read-config", "get_config"),
|
|
1274
|
+
),
|
|
1106
1275
|
],
|
|
1107
1276
|
)
|
|
1108
1277
|
|
|
@@ -1143,6 +1312,8 @@ def register_unified_environment_tool(mcp: FastMCP, config: ServerConfig) -> Non
|
|
|
1143
1312
|
permissions_preset: str = "full",
|
|
1144
1313
|
create_toml: bool = True,
|
|
1145
1314
|
dry_run: bool = False,
|
|
1315
|
+
sections: Optional[List[str]] = None,
|
|
1316
|
+
key: Optional[str] = None,
|
|
1146
1317
|
) -> dict:
|
|
1147
1318
|
payload = {
|
|
1148
1319
|
"path": path,
|
|
@@ -1155,6 +1326,8 @@ def register_unified_environment_tool(mcp: FastMCP, config: ServerConfig) -> Non
|
|
|
1155
1326
|
"permissions_preset": permissions_preset,
|
|
1156
1327
|
"create_toml": create_toml,
|
|
1157
1328
|
"dry_run": dry_run,
|
|
1329
|
+
"sections": sections,
|
|
1330
|
+
"key": key,
|
|
1158
1331
|
}
|
|
1159
1332
|
return _dispatch_environment_action(
|
|
1160
1333
|
action=action, payload=payload, config=config
|
|
@@ -19,6 +19,7 @@ from foundry_mcp.core.ai_consultation import (
|
|
|
19
19
|
ConsultationWorkflow,
|
|
20
20
|
ConsensusResult,
|
|
21
21
|
)
|
|
22
|
+
from foundry_mcp.core.llm_config import load_consultation_config
|
|
22
23
|
from foundry_mcp.core.naming import canonical_tool
|
|
23
24
|
from foundry_mcp.core.observability import get_metrics, mcp_tool
|
|
24
25
|
from foundry_mcp.core.providers import available_providers
|
|
@@ -29,6 +30,7 @@ from foundry_mcp.core.responses import (
|
|
|
29
30
|
error_response,
|
|
30
31
|
success_response,
|
|
31
32
|
)
|
|
33
|
+
from foundry_mcp.core.llm_config import load_consultation_config
|
|
32
34
|
from foundry_mcp.core.security import is_prompt_injection
|
|
33
35
|
from foundry_mcp.core.spec import find_specs_directory
|
|
34
36
|
from foundry_mcp.tools.unified.router import (
|
|
@@ -55,6 +57,20 @@ def _extract_plan_name(plan_path: str) -> str:
|
|
|
55
57
|
return Path(plan_path).stem
|
|
56
58
|
|
|
57
59
|
|
|
60
|
+
def _find_config_file(start_path: Path) -> Optional[Path]:
|
|
61
|
+
"""Find foundry-mcp.toml by walking up from start_path."""
|
|
62
|
+
current = start_path if start_path.is_dir() else start_path.parent
|
|
63
|
+
for _ in range(10): # Limit depth to prevent infinite loops
|
|
64
|
+
config_file = current / "foundry-mcp.toml"
|
|
65
|
+
if config_file.exists():
|
|
66
|
+
return config_file
|
|
67
|
+
parent = current.parent
|
|
68
|
+
if parent == current: # Reached root
|
|
69
|
+
break
|
|
70
|
+
current = parent
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
58
74
|
def _parse_review_summary(content: str) -> dict:
|
|
59
75
|
"""Parse review markdown to extract section counts."""
|
|
60
76
|
|
|
@@ -323,7 +339,10 @@ def perform_plan_review(
|
|
|
323
339
|
template_id = REVIEW_TYPE_TO_TEMPLATE[review_type]
|
|
324
340
|
|
|
325
341
|
try:
|
|
326
|
-
|
|
342
|
+
# Load consultation config from workspace to get provider priority list
|
|
343
|
+
config_file = _find_config_file(plan_file)
|
|
344
|
+
consultation_config = load_consultation_config(config_file=config_file)
|
|
345
|
+
orchestrator = ConsultationOrchestrator(config=consultation_config)
|
|
327
346
|
request = ConsultationRequest(
|
|
328
347
|
workflow=ConsultationWorkflow.MARKDOWN_PLAN_REVIEW,
|
|
329
348
|
prompt_id=template_id,
|
|
@@ -11,7 +11,6 @@ from mcp.server.fastmcp import FastMCP
|
|
|
11
11
|
|
|
12
12
|
from foundry_mcp.config import ServerConfig
|
|
13
13
|
from foundry_mcp.core.context import generate_correlation_id, get_correlation_id
|
|
14
|
-
from foundry_mcp.core.feature_flags import FeatureFlag, FlagState, get_flag_service
|
|
15
14
|
from foundry_mcp.core.llm_provider import RateLimitError
|
|
16
15
|
from foundry_mcp.core.naming import canonical_tool
|
|
17
16
|
from foundry_mcp.core.observability import get_metrics, mcp_tool
|
|
@@ -42,19 +41,6 @@ from foundry_mcp.tools.unified.router import (
|
|
|
42
41
|
|
|
43
42
|
logger = logging.getLogger(__name__)
|
|
44
43
|
_metrics = get_metrics()
|
|
45
|
-
_flag_service = get_flag_service()
|
|
46
|
-
try:
|
|
47
|
-
_flag_service.register(
|
|
48
|
-
FeatureFlag(
|
|
49
|
-
name="provider_tools",
|
|
50
|
-
description="LLM provider management and execution tools",
|
|
51
|
-
state=FlagState.BETA,
|
|
52
|
-
default_enabled=True,
|
|
53
|
-
)
|
|
54
|
-
)
|
|
55
|
-
except ValueError:
|
|
56
|
-
# Flag already registered
|
|
57
|
-
pass
|
|
58
44
|
|
|
59
45
|
_ACTION_SUMMARY = {
|
|
60
46
|
"list": "List registered providers with optional unavailable entries",
|
|
@@ -92,22 +78,6 @@ def _validation_error(
|
|
|
92
78
|
)
|
|
93
79
|
|
|
94
80
|
|
|
95
|
-
def _feature_flag_blocked(request_id: str) -> Optional[dict]:
|
|
96
|
-
if _flag_service.is_enabled("provider_tools"):
|
|
97
|
-
return None
|
|
98
|
-
|
|
99
|
-
return asdict(
|
|
100
|
-
error_response(
|
|
101
|
-
"Provider tools are disabled by feature flag",
|
|
102
|
-
error_code=ErrorCode.FEATURE_DISABLED,
|
|
103
|
-
error_type=ErrorType.FEATURE_FLAG,
|
|
104
|
-
data={"feature": "provider_tools"},
|
|
105
|
-
remediation="Enable the 'provider_tools' feature flag to call provider actions.",
|
|
106
|
-
request_id=request_id,
|
|
107
|
-
)
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
111
81
|
def _handle_list(
|
|
112
82
|
*,
|
|
113
83
|
config: ServerConfig, # noqa: ARG001 - reserved for future hooks
|
|
@@ -115,9 +85,6 @@ def _handle_list(
|
|
|
115
85
|
**_: Any,
|
|
116
86
|
) -> dict:
|
|
117
87
|
request_id = _request_id()
|
|
118
|
-
blocked = _feature_flag_blocked(request_id)
|
|
119
|
-
if blocked:
|
|
120
|
-
return blocked
|
|
121
88
|
|
|
122
89
|
include = include_unavailable if isinstance(include_unavailable, bool) else False
|
|
123
90
|
if include_unavailable is not None and not isinstance(include_unavailable, bool):
|
|
@@ -182,9 +149,6 @@ def _handle_status(
|
|
|
182
149
|
**_: Any,
|
|
183
150
|
) -> dict:
|
|
184
151
|
request_id = _request_id()
|
|
185
|
-
blocked = _feature_flag_blocked(request_id)
|
|
186
|
-
if blocked:
|
|
187
|
-
return blocked
|
|
188
152
|
|
|
189
153
|
if not isinstance(provider_id, str) or not provider_id.strip():
|
|
190
154
|
return _validation_error(
|
|
@@ -288,10 +252,6 @@ def _handle_execute(
|
|
|
288
252
|
**_: Any,
|
|
289
253
|
) -> dict:
|
|
290
254
|
request_id = _request_id()
|
|
291
|
-
blocked = _feature_flag_blocked(request_id)
|
|
292
|
-
if blocked:
|
|
293
|
-
return blocked
|
|
294
|
-
|
|
295
255
|
action = "execute"
|
|
296
256
|
|
|
297
257
|
if not isinstance(provider_id, str) or not provider_id.strip():
|