foundry-mcp 0.3.3__py3-none-any.whl → 0.7.0__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/commands/plan.py +10 -3
- foundry_mcp/cli/commands/review.py +19 -4
- foundry_mcp/cli/commands/specs.py +38 -208
- foundry_mcp/cli/output.py +3 -3
- foundry_mcp/config.py +235 -5
- foundry_mcp/core/ai_consultation.py +146 -9
- foundry_mcp/core/discovery.py +6 -6
- foundry_mcp/core/error_store.py +2 -2
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/llm_config.py +20 -2
- foundry_mcp/core/metrics_store.py +2 -2
- foundry_mcp/core/progress.py +70 -0
- 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/claude.py +6 -47
- foundry_mcp/core/providers/codex.py +6 -57
- foundry_mcp/core/providers/cursor_agent.py +3 -44
- foundry_mcp/core/providers/gemini.py +6 -57
- foundry_mcp/core/providers/opencode.py +35 -5
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +425 -0
- foundry_mcp/core/research/models.py +437 -0
- foundry_mcp/core/research/workflows/__init__.py +22 -0
- foundry_mcp/core/research/workflows/base.py +204 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +396 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/responses.py +450 -0
- foundry_mcp/core/spec.py +2438 -236
- foundry_mcp/core/task.py +1064 -19
- foundry_mcp/core/testing.py +512 -123
- foundry_mcp/core/validation.py +313 -42
- 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 +38 -0
- foundry_mcp/tools/unified/__init__.py +4 -2
- foundry_mcp/tools/unified/authoring.py +2423 -267
- foundry_mcp/tools/unified/documentation_helpers.py +69 -6
- foundry_mcp/tools/unified/environment.py +235 -6
- foundry_mcp/tools/unified/error.py +18 -1
- foundry_mcp/tools/unified/lifecycle.py +8 -0
- foundry_mcp/tools/unified/plan.py +113 -1
- foundry_mcp/tools/unified/research.py +658 -0
- foundry_mcp/tools/unified/review.py +370 -16
- foundry_mcp/tools/unified/spec.py +367 -0
- foundry_mcp/tools/unified/task.py +1163 -48
- foundry_mcp/tools/unified/test.py +69 -8
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/METADATA +7 -1
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/RECORD +60 -48
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/WHEEL +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/entry_points.txt +0 -0
- {foundry_mcp-0.3.3.dist-info → foundry_mcp-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -30,17 +30,32 @@ from foundry_mcp.core.responses import (
|
|
|
30
30
|
success_response,
|
|
31
31
|
)
|
|
32
32
|
from foundry_mcp.core.spec import (
|
|
33
|
+
TEMPLATES,
|
|
34
|
+
TEMPLATE_DESCRIPTIONS,
|
|
35
|
+
check_spec_completeness,
|
|
36
|
+
detect_duplicate_tasks,
|
|
37
|
+
diff_specs,
|
|
33
38
|
find_spec_file,
|
|
34
39
|
find_specs_directory,
|
|
40
|
+
list_spec_backups,
|
|
35
41
|
list_specs,
|
|
36
42
|
load_spec,
|
|
37
43
|
)
|
|
38
44
|
from foundry_mcp.core.validation import (
|
|
45
|
+
VALID_NODE_TYPES,
|
|
46
|
+
VALID_STATUSES,
|
|
47
|
+
VALID_TASK_CATEGORIES,
|
|
48
|
+
VALID_VERIFICATION_TYPES,
|
|
39
49
|
apply_fixes,
|
|
40
50
|
calculate_stats,
|
|
41
51
|
get_fix_actions,
|
|
42
52
|
validate_spec,
|
|
43
53
|
)
|
|
54
|
+
from foundry_mcp.core.journal import (
|
|
55
|
+
VALID_BLOCKER_TYPES,
|
|
56
|
+
VALID_ENTRY_TYPES,
|
|
57
|
+
)
|
|
58
|
+
from foundry_mcp.core.lifecycle import VALID_FOLDERS
|
|
44
59
|
from foundry_mcp.tools.unified.router import (
|
|
45
60
|
ActionDefinition,
|
|
46
61
|
ActionRouter,
|
|
@@ -102,6 +117,52 @@ def _handle_find(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
|
|
|
102
117
|
return asdict(success_response(found=False, spec_id=spec_id))
|
|
103
118
|
|
|
104
119
|
|
|
120
|
+
def _handle_get(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
|
|
121
|
+
"""Return raw spec JSON content in minified form."""
|
|
122
|
+
import json as _json
|
|
123
|
+
|
|
124
|
+
spec_id = payload.get("spec_id")
|
|
125
|
+
workspace = payload.get("workspace")
|
|
126
|
+
|
|
127
|
+
if not isinstance(spec_id, str) or not spec_id.strip():
|
|
128
|
+
return asdict(
|
|
129
|
+
error_response(
|
|
130
|
+
"spec_id is required",
|
|
131
|
+
error_code=ErrorCode.MISSING_REQUIRED,
|
|
132
|
+
error_type=ErrorType.VALIDATION,
|
|
133
|
+
remediation="Provide a spec_id parameter",
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
specs_dir = _resolve_specs_dir(config, workspace)
|
|
138
|
+
if not specs_dir:
|
|
139
|
+
return asdict(
|
|
140
|
+
error_response(
|
|
141
|
+
"No specs directory found",
|
|
142
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
143
|
+
error_type=ErrorType.NOT_FOUND,
|
|
144
|
+
remediation="Ensure you're in a project with a specs/ directory or pass workspace.",
|
|
145
|
+
details={"workspace": workspace},
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
spec_data = load_spec(spec_id, specs_dir)
|
|
150
|
+
if spec_data is None:
|
|
151
|
+
return asdict(
|
|
152
|
+
error_response(
|
|
153
|
+
f"Spec not found: {spec_id}",
|
|
154
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
155
|
+
error_type=ErrorType.NOT_FOUND,
|
|
156
|
+
remediation=f"Verify the spec_id exists. Use spec(action='list') to see available specs.",
|
|
157
|
+
details={"spec_id": spec_id},
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Return minified JSON string to minimize token usage
|
|
162
|
+
minified_spec = _json.dumps(spec_data, separators=(",", ":"))
|
|
163
|
+
return asdict(success_response(spec=minified_spec))
|
|
164
|
+
|
|
165
|
+
|
|
105
166
|
def _handle_list(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
|
|
106
167
|
status = payload.get("status", "all")
|
|
107
168
|
include_progress = payload.get("include_progress", True)
|
|
@@ -723,8 +784,287 @@ def _handle_analyze_deps(*, config: ServerConfig, payload: Dict[str, Any]) -> di
|
|
|
723
784
|
)
|
|
724
785
|
|
|
725
786
|
|
|
787
|
+
def _handle_schema(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
|
|
788
|
+
"""Return schema information for all valid values in the spec system."""
|
|
789
|
+
# Build templates with descriptions
|
|
790
|
+
templates_with_desc = [
|
|
791
|
+
{"name": t, "description": TEMPLATE_DESCRIPTIONS.get(t, "")}
|
|
792
|
+
for t in TEMPLATES
|
|
793
|
+
]
|
|
794
|
+
return asdict(
|
|
795
|
+
success_response(
|
|
796
|
+
templates=templates_with_desc,
|
|
797
|
+
node_types=sorted(VALID_NODE_TYPES),
|
|
798
|
+
statuses=sorted(VALID_STATUSES),
|
|
799
|
+
task_categories=sorted(VALID_TASK_CATEGORIES),
|
|
800
|
+
verification_types=sorted(VALID_VERIFICATION_TYPES),
|
|
801
|
+
journal_entry_types=sorted(VALID_ENTRY_TYPES),
|
|
802
|
+
blocker_types=sorted(VALID_BLOCKER_TYPES),
|
|
803
|
+
status_folders=sorted(VALID_FOLDERS),
|
|
804
|
+
)
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _handle_diff(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
|
|
809
|
+
"""Compare two specs and return categorized changes."""
|
|
810
|
+
spec_id = payload.get("spec_id")
|
|
811
|
+
if not spec_id:
|
|
812
|
+
return asdict(
|
|
813
|
+
error_response(
|
|
814
|
+
"spec_id is required for diff action",
|
|
815
|
+
error_code=ErrorCode.MISSING_REQUIRED,
|
|
816
|
+
error_type=ErrorType.VALIDATION,
|
|
817
|
+
remediation="Provide the spec_id of the current spec to compare",
|
|
818
|
+
)
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
# Target can be a backup timestamp or another spec_id
|
|
822
|
+
target = payload.get("target")
|
|
823
|
+
workspace = payload.get("workspace")
|
|
824
|
+
max_results = payload.get("limit")
|
|
825
|
+
|
|
826
|
+
specs_dir = _resolve_specs_dir(config, workspace)
|
|
827
|
+
if not specs_dir:
|
|
828
|
+
return asdict(
|
|
829
|
+
error_response(
|
|
830
|
+
"No specs directory found",
|
|
831
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
832
|
+
error_type=ErrorType.NOT_FOUND,
|
|
833
|
+
remediation="Ensure you're in a project with a specs/ directory",
|
|
834
|
+
)
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
# If no target specified, diff against latest backup
|
|
838
|
+
if not target:
|
|
839
|
+
backups = list_spec_backups(spec_id, specs_dir=specs_dir)
|
|
840
|
+
if backups["count"] == 0:
|
|
841
|
+
return asdict(
|
|
842
|
+
error_response(
|
|
843
|
+
f"No backups found for spec '{spec_id}'",
|
|
844
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
845
|
+
error_type=ErrorType.NOT_FOUND,
|
|
846
|
+
remediation="Create a backup first using spec save operations",
|
|
847
|
+
)
|
|
848
|
+
)
|
|
849
|
+
# Use latest backup as source (older state)
|
|
850
|
+
source_path = backups["backups"][0]["file_path"]
|
|
851
|
+
else:
|
|
852
|
+
# Check if target is a timestamp (backup) or spec_id
|
|
853
|
+
backup_file = specs_dir / ".backups" / spec_id / f"{target}.json"
|
|
854
|
+
if backup_file.is_file():
|
|
855
|
+
source_path = str(backup_file)
|
|
856
|
+
else:
|
|
857
|
+
# Treat as another spec_id
|
|
858
|
+
source_path = target
|
|
859
|
+
|
|
860
|
+
result = diff_specs(
|
|
861
|
+
source=source_path,
|
|
862
|
+
target=spec_id,
|
|
863
|
+
specs_dir=specs_dir,
|
|
864
|
+
max_results=max_results,
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
if "error" in result and not result.get("success", True):
|
|
868
|
+
return asdict(
|
|
869
|
+
error_response(
|
|
870
|
+
result["error"],
|
|
871
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
872
|
+
error_type=ErrorType.NOT_FOUND,
|
|
873
|
+
remediation="Verify both specs exist and are accessible",
|
|
874
|
+
)
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
return asdict(
|
|
878
|
+
success_response(
|
|
879
|
+
spec_id=spec_id,
|
|
880
|
+
compared_to=source_path if not target else target,
|
|
881
|
+
summary=result["summary"],
|
|
882
|
+
changes=result["changes"],
|
|
883
|
+
partial=result["partial"],
|
|
884
|
+
)
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _handle_history(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
|
|
889
|
+
"""List spec history including backups and revision history."""
|
|
890
|
+
spec_id = payload.get("spec_id")
|
|
891
|
+
if not spec_id:
|
|
892
|
+
return asdict(
|
|
893
|
+
error_response(
|
|
894
|
+
"spec_id is required for history action",
|
|
895
|
+
error_code=ErrorCode.MISSING_REQUIRED,
|
|
896
|
+
error_type=ErrorType.VALIDATION,
|
|
897
|
+
remediation="Provide the spec_id to view history",
|
|
898
|
+
)
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
workspace = payload.get("workspace")
|
|
902
|
+
cursor = payload.get("cursor")
|
|
903
|
+
limit = payload.get("limit")
|
|
904
|
+
|
|
905
|
+
specs_dir = _resolve_specs_dir(config, workspace)
|
|
906
|
+
if not specs_dir:
|
|
907
|
+
return asdict(
|
|
908
|
+
error_response(
|
|
909
|
+
"No specs directory found",
|
|
910
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
911
|
+
error_type=ErrorType.NOT_FOUND,
|
|
912
|
+
remediation="Ensure you're in a project with a specs/ directory",
|
|
913
|
+
)
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
# Get backups with pagination
|
|
917
|
+
backups_result = list_spec_backups(
|
|
918
|
+
spec_id, specs_dir=specs_dir, cursor=cursor, limit=limit
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
# Get revision history from spec metadata
|
|
922
|
+
spec_data = load_spec(spec_id, specs_dir)
|
|
923
|
+
revision_history = []
|
|
924
|
+
if spec_data:
|
|
925
|
+
metadata = spec_data.get("metadata", {})
|
|
926
|
+
revision_history = metadata.get("revision_history", [])
|
|
927
|
+
|
|
928
|
+
# Merge and sort entries (backups and revisions)
|
|
929
|
+
history_entries = []
|
|
930
|
+
|
|
931
|
+
# Add backups as history entries
|
|
932
|
+
for backup in backups_result["backups"]:
|
|
933
|
+
history_entries.append({
|
|
934
|
+
"type": "backup",
|
|
935
|
+
"timestamp": backup["timestamp"],
|
|
936
|
+
"file_path": backup["file_path"],
|
|
937
|
+
"file_size_bytes": backup["file_size_bytes"],
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
# Add revision history entries
|
|
941
|
+
for rev in revision_history:
|
|
942
|
+
history_entries.append({
|
|
943
|
+
"type": "revision",
|
|
944
|
+
"timestamp": rev.get("date"),
|
|
945
|
+
"version": rev.get("version"),
|
|
946
|
+
"changes": rev.get("changes"),
|
|
947
|
+
"author": rev.get("author"),
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
return asdict(
|
|
951
|
+
success_response(
|
|
952
|
+
spec_id=spec_id,
|
|
953
|
+
entries=history_entries,
|
|
954
|
+
backup_count=backups_result["count"],
|
|
955
|
+
revision_count=len(revision_history),
|
|
956
|
+
pagination=backups_result["pagination"],
|
|
957
|
+
)
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def _handle_completeness_check(
|
|
962
|
+
*, config: ServerConfig, payload: Dict[str, Any]
|
|
963
|
+
) -> dict:
|
|
964
|
+
"""Check spec completeness and return a score (0-100)."""
|
|
965
|
+
spec_id = payload.get("spec_id")
|
|
966
|
+
if not spec_id or not isinstance(spec_id, str) or not spec_id.strip():
|
|
967
|
+
return asdict(
|
|
968
|
+
error_response(
|
|
969
|
+
"spec_id is required for completeness-check action",
|
|
970
|
+
error_code=ErrorCode.MISSING_REQUIRED,
|
|
971
|
+
error_type=ErrorType.VALIDATION,
|
|
972
|
+
remediation="Provide the spec_id to check completeness",
|
|
973
|
+
)
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
workspace = payload.get("workspace")
|
|
977
|
+
specs_dir = _resolve_specs_dir(config, workspace)
|
|
978
|
+
if not specs_dir:
|
|
979
|
+
return asdict(
|
|
980
|
+
error_response(
|
|
981
|
+
"No specs directory found",
|
|
982
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
983
|
+
error_type=ErrorType.NOT_FOUND,
|
|
984
|
+
remediation="Ensure you're in a project with a specs/ directory",
|
|
985
|
+
)
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
result, error = check_spec_completeness(spec_id, specs_dir=specs_dir)
|
|
989
|
+
if error:
|
|
990
|
+
return asdict(
|
|
991
|
+
error_response(
|
|
992
|
+
error,
|
|
993
|
+
error_code=ErrorCode.SPEC_NOT_FOUND,
|
|
994
|
+
error_type=ErrorType.NOT_FOUND,
|
|
995
|
+
remediation='Verify the spec ID exists using spec(action="list").',
|
|
996
|
+
details={"spec_id": spec_id},
|
|
997
|
+
)
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
return asdict(success_response(**result))
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def _handle_duplicate_detection(
|
|
1004
|
+
*, config: ServerConfig, payload: Dict[str, Any]
|
|
1005
|
+
) -> dict:
|
|
1006
|
+
"""Detect duplicate or near-duplicate tasks in a spec."""
|
|
1007
|
+
spec_id = payload.get("spec_id")
|
|
1008
|
+
if not spec_id or not isinstance(spec_id, str) or not spec_id.strip():
|
|
1009
|
+
return asdict(
|
|
1010
|
+
error_response(
|
|
1011
|
+
"spec_id is required for duplicate-detection action",
|
|
1012
|
+
error_code=ErrorCode.MISSING_REQUIRED,
|
|
1013
|
+
error_type=ErrorType.VALIDATION,
|
|
1014
|
+
remediation="Provide the spec_id to check for duplicates",
|
|
1015
|
+
)
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
workspace = payload.get("workspace")
|
|
1019
|
+
scope = payload.get("scope", "titles")
|
|
1020
|
+
threshold = payload.get("threshold", 0.8)
|
|
1021
|
+
max_pairs = payload.get("max_pairs", 100)
|
|
1022
|
+
|
|
1023
|
+
# Validate threshold
|
|
1024
|
+
if not isinstance(threshold, (int, float)) or not 0.0 <= threshold <= 1.0:
|
|
1025
|
+
return asdict(
|
|
1026
|
+
error_response(
|
|
1027
|
+
"threshold must be a number between 0.0 and 1.0",
|
|
1028
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
1029
|
+
error_type=ErrorType.VALIDATION,
|
|
1030
|
+
)
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
specs_dir = _resolve_specs_dir(config, workspace)
|
|
1034
|
+
if not specs_dir:
|
|
1035
|
+
return asdict(
|
|
1036
|
+
error_response(
|
|
1037
|
+
"No specs directory found",
|
|
1038
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
1039
|
+
error_type=ErrorType.NOT_FOUND,
|
|
1040
|
+
remediation="Ensure you're in a project with a specs/ directory",
|
|
1041
|
+
)
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
result, error = detect_duplicate_tasks(
|
|
1045
|
+
spec_id,
|
|
1046
|
+
scope=scope,
|
|
1047
|
+
threshold=threshold,
|
|
1048
|
+
max_pairs=max_pairs,
|
|
1049
|
+
specs_dir=specs_dir,
|
|
1050
|
+
)
|
|
1051
|
+
if error:
|
|
1052
|
+
return asdict(
|
|
1053
|
+
error_response(
|
|
1054
|
+
error,
|
|
1055
|
+
error_code=ErrorCode.SPEC_NOT_FOUND,
|
|
1056
|
+
error_type=ErrorType.NOT_FOUND,
|
|
1057
|
+
remediation='Verify the spec ID exists using spec(action="list").',
|
|
1058
|
+
details={"spec_id": spec_id},
|
|
1059
|
+
)
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
return asdict(success_response(**result))
|
|
1063
|
+
|
|
1064
|
+
|
|
726
1065
|
_ACTIONS = [
|
|
727
1066
|
ActionDefinition(name="find", handler=_handle_find, summary="Find a spec by ID"),
|
|
1067
|
+
ActionDefinition(name="get", handler=_handle_get, summary="Get raw spec JSON (minified)"),
|
|
728
1068
|
ActionDefinition(name="list", handler=_handle_list, summary="List specs"),
|
|
729
1069
|
ActionDefinition(
|
|
730
1070
|
name="validate", handler=_handle_validate, summary="Validate a spec"
|
|
@@ -744,6 +1084,31 @@ _ACTIONS = [
|
|
|
744
1084
|
handler=_handle_analyze_deps,
|
|
745
1085
|
summary="Analyze spec dependency graph",
|
|
746
1086
|
),
|
|
1087
|
+
ActionDefinition(
|
|
1088
|
+
name="schema",
|
|
1089
|
+
handler=_handle_schema,
|
|
1090
|
+
summary="Get valid values for spec fields",
|
|
1091
|
+
),
|
|
1092
|
+
ActionDefinition(
|
|
1093
|
+
name="diff",
|
|
1094
|
+
handler=_handle_diff,
|
|
1095
|
+
summary="Compare spec against backup or another spec",
|
|
1096
|
+
),
|
|
1097
|
+
ActionDefinition(
|
|
1098
|
+
name="history",
|
|
1099
|
+
handler=_handle_history,
|
|
1100
|
+
summary="List spec backups and revision history",
|
|
1101
|
+
),
|
|
1102
|
+
ActionDefinition(
|
|
1103
|
+
name="completeness-check",
|
|
1104
|
+
handler=_handle_completeness_check,
|
|
1105
|
+
summary="Check spec completeness and return a score (0-100)",
|
|
1106
|
+
),
|
|
1107
|
+
ActionDefinition(
|
|
1108
|
+
name="duplicate-detection",
|
|
1109
|
+
handler=_handle_duplicate_detection,
|
|
1110
|
+
summary="Detect duplicate or near-duplicate tasks",
|
|
1111
|
+
),
|
|
747
1112
|
]
|
|
748
1113
|
|
|
749
1114
|
_SPEC_ROUTER = ActionRouter(tool_name="spec", actions=_ACTIONS)
|
|
@@ -785,6 +1150,7 @@ def register_unified_spec_tool(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
785
1150
|
directory: Optional[str] = None,
|
|
786
1151
|
path: Optional[str] = None,
|
|
787
1152
|
bottleneck_threshold: Optional[int] = None,
|
|
1153
|
+
target: Optional[str] = None,
|
|
788
1154
|
) -> dict:
|
|
789
1155
|
payload = {
|
|
790
1156
|
"spec_id": spec_id,
|
|
@@ -799,6 +1165,7 @@ def register_unified_spec_tool(mcp: FastMCP, config: ServerConfig) -> None:
|
|
|
799
1165
|
"directory": directory,
|
|
800
1166
|
"path": path,
|
|
801
1167
|
"bottleneck_threshold": bottleneck_threshold,
|
|
1168
|
+
"target": target,
|
|
802
1169
|
}
|
|
803
1170
|
return _dispatch_spec_action(action=action, payload=payload, config=config)
|
|
804
1171
|
|