diff --git a/scripts/scaffold.py b/scripts/scaffold.py index f94c9ce..a9188ef 100644 --- a/scripts/scaffold.py +++ b/scripts/scaffold.py @@ -52,6 +52,12 @@ def _build_template_context( tags = metadata.tags or [] has_llm_judge = (submission_dir / "tests" / "llm_judge.py").is_file() + # .mcp.json can be at root or inside supportive/ (legacy location) + has_mcp_json = ( + (submission_dir / ".mcp.json").is_file() + or (submission_dir / "supportive" / ".mcp.json").is_file() + ) + base_context = { "submission_name": metadata.name, "persona": metadata.persona or "general", @@ -62,6 +68,7 @@ def _build_template_context( "has_supportive": (submission_dir / "supportive").is_dir(), "has_scripts": (submission_dir / "scripts").is_dir(), "has_claude_md": (submission_dir / "CLAUDE.md").is_file(), + "has_mcp_json": has_mcp_json, "has_llm_judge": has_llm_judge, # These were formerly ad-hoc dict reads from raw metadata; they are # not SubmissionMetadata fields (extra="forbid" rejects them), so the @@ -135,13 +142,20 @@ def _copy_submission_files( _copy_supportive(submission_dir, build_context_dir, variant) - # Treatment-only assets: CLAUDE.md, scripts/, docs, and skills are + # Treatment-only assets: CLAUDE.md, .mcp.json, scripts/, docs, and skills are # skill knowledge that only the treatment variant should receive. if variant == "treatment": claude_md = submission_dir / "CLAUDE.md" if claude_md.is_file(): shutil.copy2(claude_md, build_context_dir / "CLAUDE.md") + # .mcp.json: check root first, fall back to supportive/ (legacy location) + mcp_json = submission_dir / ".mcp.json" + if not mcp_json.is_file(): + mcp_json = submission_dir / "supportive" / ".mcp.json" + if mcp_json.is_file(): + shutil.copy2(mcp_json, build_context_dir / ".mcp.json") + scripts_src = submission_dir / "scripts" if scripts_src.is_dir(): shutil.copytree( @@ -210,11 +224,12 @@ def scaffold_submission( strategy, ) # Control is a vanilla agent: no supportive files, no scripts, - # no CLAUDE.md. Only instruction.md and tests are shared. + # no CLAUDE.md, no .mcp.json. Only instruction.md and tests are shared. if variant == "control": context["has_supportive"] = False context["has_scripts"] = False context["has_claude_md"] = False + context["has_mcp_json"] = False rendered = _render_templates(jinja_env, context) target_dir.mkdir(parents=True, exist_ok=True) diff --git a/templates/Dockerfile.j2 b/templates/Dockerfile.j2 index f24cc2a..937288f 100644 --- a/templates/Dockerfile.j2 +++ b/templates/Dockerfile.j2 @@ -6,6 +6,9 @@ COPY tests/ /tests/ {% if has_claude_md %} COPY CLAUDE.md /workspace/CLAUDE.md {% endif %} +{% if has_mcp_json %} +COPY .mcp.json /workspace/.mcp.json +{% endif %} {% if has_supportive %} COPY supportive/ /workspace/supportive/ {% endif %} diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py index 5b37a8d..52635e3 100644 --- a/tests/test_scaffold.py +++ b/tests/test_scaffold.py @@ -274,6 +274,83 @@ def test_no_supportive_in_dockerfile_when_absent(self, valid_submission: Path, t content = (treatment / "environment" / "Dockerfile").read_text() assert "COPY supportive/" not in content + def test_mcp_json_at_root_copied_only_for_treatment(self, tmp_path: Path): + """.mcp.json at submission root is copied only for treatment.""" + sub = tmp_path / "mcp-skill" + sub.mkdir() + (sub / "instruction.md").write_text("Do it.\n") + (sub / "skills").mkdir() + (sub / "skills" / "SKILL.md").write_text("# Skill\n") + (sub / "tests").mkdir() + (sub / "tests" / "test_outputs.py").write_text("def test(): pass\n") + (sub / ".mcp.json").write_text('{"mcpServers": {}}\n') + (sub / "metadata.yaml").write_text(yaml.dump({"name": "mcp-skill"})) + + output = tmp_path / "output" + treatment, control = scaffold_submission(sub, output, TEMPLATES_DIR) + assert (treatment / "environment" / ".mcp.json").is_file() + assert not (control / "environment" / ".mcp.json").exists() + + def test_mcp_json_in_supportive_copied_only_for_treatment(self, tmp_path: Path): + """.mcp.json in supportive/ (legacy location) is copied only for treatment.""" + sub = tmp_path / "mcp-skill-legacy" + sub.mkdir() + (sub / "instruction.md").write_text("Do it.\n") + (sub / "skills").mkdir() + (sub / "skills" / "SKILL.md").write_text("# Skill\n") + (sub / "tests").mkdir() + (sub / "tests" / "test_outputs.py").write_text("def test(): pass\n") + supportive = sub / "supportive" + supportive.mkdir() + (supportive / ".mcp.json").write_text('{"mcpServers": {}}\n') + (sub / "metadata.yaml").write_text(yaml.dump({"name": "mcp-skill-legacy"})) + + output = tmp_path / "output" + treatment, control = scaffold_submission(sub, output, TEMPLATES_DIR) + assert (treatment / "environment" / ".mcp.json").is_file() + assert not (control / "environment" / ".mcp.json").exists() + + def test_dockerfile_includes_mcp_json_for_treatment(self, tmp_path: Path): + """Dockerfile includes COPY .mcp.json when present.""" + sub = tmp_path / "mcp-skill" + sub.mkdir() + (sub / "instruction.md").write_text("Do it.\n") + (sub / "skills").mkdir() + (sub / "skills" / "SKILL.md").write_text("# Skill\n") + (sub / "tests").mkdir() + (sub / "tests" / "test_outputs.py").write_text("def test(): pass\n") + (sub / ".mcp.json").write_text('{"mcpServers": {}}\n') + (sub / "metadata.yaml").write_text(yaml.dump({"name": "mcp-skill"})) + + output = tmp_path / "output" + treatment, _ = scaffold_submission(sub, output, TEMPLATES_DIR) + content = (treatment / "environment" / "Dockerfile").read_text() + assert "COPY .mcp.json /workspace/.mcp.json" in content + + def test_dockerfile_excludes_mcp_json_for_control(self, tmp_path: Path): + """Dockerfile excludes .mcp.json for control variant.""" + sub = tmp_path / "mcp-skill" + sub.mkdir() + (sub / "instruction.md").write_text("Do it.\n") + (sub / "skills").mkdir() + (sub / "skills" / "SKILL.md").write_text("# Skill\n") + (sub / "tests").mkdir() + (sub / "tests" / "test_outputs.py").write_text("def test(): pass\n") + (sub / ".mcp.json").write_text('{"mcpServers": {}}\n') + (sub / "metadata.yaml").write_text(yaml.dump({"name": "mcp-skill"})) + + output = tmp_path / "output" + _, control = scaffold_submission(sub, output, TEMPLATES_DIR) + content = (control / "environment" / "Dockerfile").read_text() + assert "COPY .mcp.json" not in content + + def test_no_mcp_json_in_dockerfile_when_absent(self, valid_submission: Path, tmp_path: Path): + """Dockerfile excludes .mcp.json when not present in submission.""" + output = tmp_path / "output" + treatment, _ = scaffold_submission(valid_submission, output, TEMPLATES_DIR) + content = (treatment / "environment" / "Dockerfile").read_text() + assert "COPY .mcp.json" not in content + def test_test_sh_includes_llm_judge_when_present(self, full_submission: Path, tmp_path: Path): output = tmp_path / "output" treatment, _ = scaffold_submission(full_submission, output, TEMPLATES_DIR)