foundry-mcp 0.3.3__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/__init__.py +7 -1
- foundry_mcp/cli/__init__.py +0 -13
- foundry_mcp/cli/commands/plan.py +10 -3
- foundry_mcp/cli/commands/review.py +19 -4
- foundry_mcp/cli/commands/session.py +1 -8
- foundry_mcp/cli/commands/specs.py +38 -208
- foundry_mcp/cli/context.py +39 -0
- foundry_mcp/cli/output.py +3 -3
- foundry_mcp/config.py +615 -11
- foundry_mcp/core/ai_consultation.py +146 -9
- foundry_mcp/core/batch_operations.py +1196 -0
- foundry_mcp/core/discovery.py +7 -7
- foundry_mcp/core/error_store.py +2 -2
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/llm_config.py +28 -2
- foundry_mcp/core/metrics_store.py +2 -2
- foundry_mcp/core/naming.py +25 -2
- foundry_mcp/core/progress.py +70 -0
- foundry_mcp/core/prometheus.py +0 -13
- foundry_mcp/core/prompts/fidelity_review.py +149 -4
- foundry_mcp/core/prompts/markdown_plan_review.py +5 -1
- foundry_mcp/core/prompts/plan_review.py +5 -1
- foundry_mcp/core/providers/__init__.py +12 -0
- foundry_mcp/core/providers/base.py +39 -0
- foundry_mcp/core/providers/claude.py +51 -48
- foundry_mcp/core/providers/codex.py +70 -60
- foundry_mcp/core/providers/cursor_agent.py +25 -47
- foundry_mcp/core/providers/detectors.py +34 -7
- foundry_mcp/core/providers/gemini.py +69 -58
- foundry_mcp/core/providers/opencode.py +101 -47
- 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/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1220 -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 +4020 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/responses.py +690 -0
- foundry_mcp/core/spec.py +2439 -236
- foundry_mcp/core/task.py +1205 -31
- foundry_mcp/core/testing.py +512 -123
- foundry_mcp/core/validation.py +319 -43
- foundry_mcp/dashboard/components/charts.py +0 -57
- foundry_mcp/dashboard/launcher.py +11 -0
- foundry_mcp/dashboard/views/metrics.py +25 -35
- foundry_mcp/dashboard/views/overview.py +1 -65
- foundry_mcp/resources/specs.py +25 -25
- foundry_mcp/schemas/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +33 -5
- foundry_mcp/server.py +0 -14
- foundry_mcp/tools/unified/__init__.py +39 -18
- foundry_mcp/tools/unified/authoring.py +2371 -248
- foundry_mcp/tools/unified/documentation_helpers.py +69 -6
- foundry_mcp/tools/unified/environment.py +434 -32
- foundry_mcp/tools/unified/error.py +18 -1
- foundry_mcp/tools/unified/lifecycle.py +8 -0
- foundry_mcp/tools/unified/plan.py +133 -2
- foundry_mcp/tools/unified/provider.py +0 -40
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +374 -17
- foundry_mcp/tools/unified/review_helpers.py +16 -1
- foundry_mcp/tools/unified/server.py +9 -24
- foundry_mcp/tools/unified/spec.py +367 -0
- foundry_mcp/tools/unified/task.py +1664 -30
- foundry_mcp/tools/unified/test.py +69 -8
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.dist-info}/METADATA +8 -1
- foundry_mcp-0.8.10.dist-info/RECORD +153 -0
- foundry_mcp/cli/flags.py +0 -266
- foundry_mcp/core/feature_flags.py +0 -592
- foundry_mcp-0.3.3.dist-info/RECORD +0 -135
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.dist-info}/WHEEL +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.dist-info}/entry_points.txt +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.8.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -209,6 +209,7 @@ def _handle_move(
|
|
|
209
209
|
spec_id: Optional[str] = None,
|
|
210
210
|
to_folder: Optional[str] = None,
|
|
211
211
|
path: Optional[str] = None,
|
|
212
|
+
force: Optional[bool] = None, # Unused, accepted for router compatibility
|
|
212
213
|
) -> dict:
|
|
213
214
|
action = "move"
|
|
214
215
|
request_id = _request_id()
|
|
@@ -293,7 +294,9 @@ def _handle_activate(
|
|
|
293
294
|
*,
|
|
294
295
|
config: ServerConfig,
|
|
295
296
|
spec_id: Optional[str] = None,
|
|
297
|
+
to_folder: Optional[str] = None, # Unused, accepted for router compatibility
|
|
296
298
|
path: Optional[str] = None,
|
|
299
|
+
force: Optional[bool] = None, # Unused, accepted for router compatibility
|
|
297
300
|
) -> dict:
|
|
298
301
|
action = "activate"
|
|
299
302
|
request_id = _request_id()
|
|
@@ -356,6 +359,7 @@ def _handle_complete(
|
|
|
356
359
|
*,
|
|
357
360
|
config: ServerConfig,
|
|
358
361
|
spec_id: Optional[str] = None,
|
|
362
|
+
to_folder: Optional[str] = None, # Unused, accepted for router compatibility
|
|
359
363
|
force: Optional[bool] = False,
|
|
360
364
|
path: Optional[str] = None,
|
|
361
365
|
) -> dict:
|
|
@@ -429,7 +433,9 @@ def _handle_archive(
|
|
|
429
433
|
*,
|
|
430
434
|
config: ServerConfig,
|
|
431
435
|
spec_id: Optional[str] = None,
|
|
436
|
+
to_folder: Optional[str] = None, # Unused, accepted for router compatibility
|
|
432
437
|
path: Optional[str] = None,
|
|
438
|
+
force: Optional[bool] = None, # Unused, accepted for router compatibility
|
|
433
439
|
) -> dict:
|
|
434
440
|
action = "archive"
|
|
435
441
|
request_id = _request_id()
|
|
@@ -492,7 +498,9 @@ def _handle_state(
|
|
|
492
498
|
*,
|
|
493
499
|
config: ServerConfig,
|
|
494
500
|
spec_id: Optional[str] = None,
|
|
501
|
+
to_folder: Optional[str] = None, # Unused, accepted for router compatibility
|
|
495
502
|
path: Optional[str] = None,
|
|
503
|
+
force: Optional[bool] = None, # Unused, accepted for router compatibility
|
|
496
504
|
) -> dict:
|
|
497
505
|
action = "state"
|
|
498
506
|
request_id = _request_id()
|
|
@@ -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,
|
|
@@ -339,6 +358,7 @@ def perform_plan_review(
|
|
|
339
358
|
|
|
340
359
|
consensus_info: Optional[dict] = None
|
|
341
360
|
provider_used: Optional[str] = None
|
|
361
|
+
provider_reviews: list[dict[str, str]] = []
|
|
342
362
|
|
|
343
363
|
if isinstance(result, ConsultationResult):
|
|
344
364
|
if not result.success:
|
|
@@ -362,16 +382,125 @@ def perform_plan_review(
|
|
|
362
382
|
remediation="Check AI provider configuration or try again later",
|
|
363
383
|
)
|
|
364
384
|
)
|
|
365
|
-
|
|
385
|
+
|
|
366
386
|
providers_consulted = [r.provider_id for r in result.responses]
|
|
367
387
|
provider_used = providers_consulted[0] if providers_consulted else "unknown"
|
|
388
|
+
|
|
389
|
+
# Extract failed provider details for visibility
|
|
390
|
+
failed_providers = [
|
|
391
|
+
{"provider_id": r.provider_id, "error": r.error}
|
|
392
|
+
for r in result.responses
|
|
393
|
+
if not r.success
|
|
394
|
+
]
|
|
395
|
+
# Filter for truly successful responses (success=True AND non-empty content)
|
|
396
|
+
successful_responses = [
|
|
397
|
+
r for r in result.responses if r.success and r.content.strip()
|
|
398
|
+
]
|
|
399
|
+
successful_providers = [r.provider_id for r in successful_responses]
|
|
400
|
+
|
|
368
401
|
consensus_info = {
|
|
369
402
|
"providers_consulted": providers_consulted,
|
|
370
403
|
"successful": result.agreement.successful_providers
|
|
371
404
|
if result.agreement
|
|
372
405
|
else 0,
|
|
373
406
|
"failed": result.agreement.failed_providers if result.agreement else 0,
|
|
407
|
+
"successful_providers": successful_providers,
|
|
408
|
+
"failed_providers": failed_providers,
|
|
374
409
|
}
|
|
410
|
+
|
|
411
|
+
# Save individual provider review files and optionally run synthesis
|
|
412
|
+
if len(successful_responses) >= 2:
|
|
413
|
+
# Multi-model mode: save per-provider files, then synthesize
|
|
414
|
+
specs_dir = find_specs_directory()
|
|
415
|
+
if specs_dir is None:
|
|
416
|
+
return asdict(
|
|
417
|
+
error_response(
|
|
418
|
+
"No specs directory found for storing plan review",
|
|
419
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
420
|
+
error_type=ErrorType.NOT_FOUND,
|
|
421
|
+
remediation="Create a specs/ directory with pending/active/completed/archived subdirectories",
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
plan_reviews_dir = specs_dir / ".plan-reviews"
|
|
426
|
+
plan_reviews_dir.mkdir(parents=True, exist_ok=True)
|
|
427
|
+
|
|
428
|
+
# Save each provider's review to a separate file
|
|
429
|
+
model_reviews_text = ""
|
|
430
|
+
for response in successful_responses:
|
|
431
|
+
provider_file = (
|
|
432
|
+
plan_reviews_dir
|
|
433
|
+
/ f"{plan_name}-{review_type}-{response.provider_id}.md"
|
|
434
|
+
)
|
|
435
|
+
provider_file.write_text(response.content, encoding="utf-8")
|
|
436
|
+
provider_reviews.append(
|
|
437
|
+
{"provider_id": response.provider_id, "path": str(provider_file)}
|
|
438
|
+
)
|
|
439
|
+
model_reviews_text += (
|
|
440
|
+
f"\n---\n## Review by {response.provider_id}\n\n"
|
|
441
|
+
f"{response.content}\n"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Run synthesis call using first provider
|
|
445
|
+
logger.info(
|
|
446
|
+
"Running synthesis for %d provider reviews: %s",
|
|
447
|
+
len(successful_responses),
|
|
448
|
+
successful_providers,
|
|
449
|
+
)
|
|
450
|
+
synthesis_request = ConsultationRequest(
|
|
451
|
+
workflow=ConsultationWorkflow.PLAN_REVIEW,
|
|
452
|
+
prompt_id="SYNTHESIS_PROMPT_V1",
|
|
453
|
+
context={
|
|
454
|
+
"spec_id": plan_name,
|
|
455
|
+
"title": plan_name,
|
|
456
|
+
"num_models": len(successful_responses),
|
|
457
|
+
"model_reviews": model_reviews_text,
|
|
458
|
+
},
|
|
459
|
+
provider_id=successful_providers[0],
|
|
460
|
+
timeout=ai_timeout,
|
|
461
|
+
)
|
|
462
|
+
try:
|
|
463
|
+
synthesis_result = orchestrator.consult(
|
|
464
|
+
synthesis_request, use_cache=consultation_cache
|
|
465
|
+
)
|
|
466
|
+
except Exception as e:
|
|
467
|
+
logger.error("Synthesis call crashed: %s", e, exc_info=True)
|
|
468
|
+
synthesis_result = None
|
|
469
|
+
|
|
470
|
+
# Handle both ConsultationResult and ConsensusResult
|
|
471
|
+
synthesis_success = False
|
|
472
|
+
synthesis_content = None
|
|
473
|
+
if synthesis_result:
|
|
474
|
+
if isinstance(synthesis_result, ConsultationResult) and synthesis_result.success:
|
|
475
|
+
synthesis_content = synthesis_result.content
|
|
476
|
+
consensus_info["synthesis_provider"] = synthesis_result.provider_id
|
|
477
|
+
synthesis_success = bool(synthesis_content and synthesis_content.strip())
|
|
478
|
+
elif isinstance(synthesis_result, ConsensusResult) and synthesis_result.success:
|
|
479
|
+
synthesis_content = synthesis_result.primary_content
|
|
480
|
+
consensus_info["synthesis_provider"] = synthesis_result.responses[0].provider_id if synthesis_result.responses else "unknown"
|
|
481
|
+
synthesis_success = bool(synthesis_content and synthesis_content.strip())
|
|
482
|
+
|
|
483
|
+
if synthesis_success and synthesis_content:
|
|
484
|
+
review_content = synthesis_content
|
|
485
|
+
else:
|
|
486
|
+
# Synthesis failed - fall back to first provider's content
|
|
487
|
+
error_detail = "unknown"
|
|
488
|
+
if synthesis_result is None:
|
|
489
|
+
error_detail = "synthesis crashed (see logs)"
|
|
490
|
+
elif isinstance(synthesis_result, ConsultationResult):
|
|
491
|
+
error_detail = synthesis_result.error or "empty response"
|
|
492
|
+
elif isinstance(synthesis_result, ConsensusResult):
|
|
493
|
+
error_detail = "empty synthesis content"
|
|
494
|
+
logger.warning(
|
|
495
|
+
"Synthesis call failed (%s), falling back to first provider's content",
|
|
496
|
+
error_detail,
|
|
497
|
+
)
|
|
498
|
+
review_content = result.primary_content
|
|
499
|
+
consensus_info["synthesis_failed"] = True
|
|
500
|
+
consensus_info["synthesis_error"] = error_detail
|
|
501
|
+
else:
|
|
502
|
+
# Single successful provider - use its content directly (no synthesis needed)
|
|
503
|
+
review_content = result.primary_content
|
|
375
504
|
else: # pragma: no cover - defensive branch
|
|
376
505
|
logger.error("Unknown consultation result type: %s", type(result))
|
|
377
506
|
return asdict(
|
|
@@ -444,6 +573,8 @@ def perform_plan_review(
|
|
|
444
573
|
"llm_status": llm_status,
|
|
445
574
|
"provider_used": provider_used,
|
|
446
575
|
}
|
|
576
|
+
if provider_reviews:
|
|
577
|
+
response_data["provider_reviews"] = provider_reviews
|
|
447
578
|
if consensus_info:
|
|
448
579
|
response_data["consensus"] = consensus_info
|
|
449
580
|
|
|
@@ -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():
|