---
phase: 06-mcp-server
plan: 02
type: execute
wave: 2
depends_on: ["06-01"]
files_modified:
- src/skill_retriever/memory/metadata_store.py
- src/skill_retriever/memory/__init__.py
- src/skill_retriever/mcp/installer.py
- src/skill_retriever/mcp/server.py
- tests/test_installer.py
- tests/test_mcp_server.py
autonomous: true
must_haves:
truths:
- "install_components places files in correct .claude/ subdirectories"
- "Settings are deep-merged, not overwritten"
- "Dependencies are auto-resolved before installation"
- "Conflicts are detected and reported before installation"
- "Each installed component reports its token cost"
artifacts:
- path: "src/skill_retriever/memory/metadata_store.py"
provides: "JSON-backed component metadata storage"
exports: ["MetadataStore"]
- path: "src/skill_retriever/mcp/installer.py"
provides: "Component installation engine"
exports: ["ComponentInstaller", "install_component", "merge_settings"]
- path: "tests/test_installer.py"
provides: "Installation tests with temp directories"
min_lines: 50
key_links:
- from: "src/skill_retriever/mcp/installer.py"
to: "src/skill_retriever/memory/metadata_store.py"
via: "MetadataStore for component lookup"
pattern: "metadata_store\\.get"
- from: "src/skill_retriever/mcp/installer.py"
to: "src/skill_retriever/entities/components.py"
via: "ComponentType for path mapping"
pattern: "ComponentType\\."
- from: "src/skill_retriever/mcp/server.py"
to: "src/skill_retriever/mcp/installer.py"
via: "install_components tool handler"
pattern: "installer\\.install"
---
<objective>
Implement component installation engine that places components into the correct .claude/ directory structure with settings deep-merge and automatic dependency resolution.
Purpose: Complete the MCP server's ability to install recommended components, fulfilling INTG-02 and INTG-04.
Output: Working installation engine with settings merge, dependency auto-resolution, and conflict detection.
</objective>
<execution_context>
@C:\Users\33641\.claude/get-shit-done/workflows/execute-plan.md
@C:\Users\33641\.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/06-mcp-server/06-RESEARCH.md
@.planning/phases/06-mcp-server/06-01-SUMMARY.md
# Key source files
@src/skill_retriever/mcp/server.py
@src/skill_retriever/mcp/schemas.py
@src/skill_retriever/entities/components.py
@src/skill_retriever/workflows/dependency_resolver.py
@src/skill_retriever/nodes/retrieval/context_assembler.py
</context>
<tasks>
<task type="auto">
<name>Task 1: Create metadata store for component lookup</name>
<files>
src/skill_retriever/memory/metadata_store.py
src/skill_retriever/memory/__init__.py
</files>
<action>
1. Create `src/skill_retriever/memory/metadata_store.py`:
- Simple JSON-backed store for ComponentMetadata
```python
import json
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from skill_retriever.entities.components import ComponentMetadata
class MetadataStore:
"""JSON-backed store for component metadata."""
def __init__(self, store_path: Path):
self.store_path = store_path
self._cache: dict[str, "ComponentMetadata"] = {}
self._load()
def _load(self) -> None:
if self.store_path.exists():
from skill_retriever.entities.components import ComponentMetadata
data = json.loads(self.store_path.read_text())
for item in data:
meta = ComponentMetadata.model_validate(item)
self._cache[meta.id] = meta
def save(self) -> None:
data = [m.model_dump(mode="json") for m in self._cache.values()]
self.store_path.parent.mkdir(parents=True, exist_ok=True)
self.store_path.write_text(json.dumps(data, indent=2, default=str))
def get(self, component_id: str) -> "ComponentMetadata | None":
return self._cache.get(component_id)
def add(self, metadata: "ComponentMetadata") -> None:
self._cache[metadata.id] = metadata
def add_many(self, components: list["ComponentMetadata"]) -> None:
for comp in components:
self._cache[comp.id] = comp
def __len__(self) -> int:
return len(self._cache)
def __contains__(self, component_id: str) -> bool:
return component_id in self._cache
```
2. Update `src/skill_retriever/memory/__init__.py` to export MetadataStore:
```python
from skill_retriever.memory.metadata_store import MetadataStore
__all__ = ["MetadataStore", ...] # add to existing exports
```
</action>
<verify>
- `uv run python -c "from skill_retriever.memory import MetadataStore"` works
- `uv run ruff check src/skill_retriever/memory/metadata_store.py` passes
- `uv run pyright src/skill_retriever/memory/metadata_store.py` passes
</verify>
<done>
- MetadataStore class created with get/add/add_many/save methods
- JSON persistence for component metadata
- Exported from memory package
</done>
</task>
<task type="auto">
<name>Task 2: Create installer and wire to MCP server</name>
<files>
src/skill_retriever/mcp/installer.py
src/skill_retriever/mcp/server.py
tests/test_installer.py
tests/test_mcp_server.py
</files>
<action>
1. Create `src/skill_retriever/mcp/installer.py`:
**INSTALL_PATHS constant** mapping ComponentType to .claude/ paths:
```python
from skill_retriever.entities.components import ComponentType
INSTALL_PATHS: dict[ComponentType, str] = {
ComponentType.SKILL: ".claude/skills/{name}/SKILL.md",
ComponentType.COMMAND: ".claude/commands/{name}.md",
ComponentType.AGENT: ".claude/agents/{name}.md",
ComponentType.SETTING: ".claude/settings.json",
ComponentType.HOOK: ".claude/hooks/{name}/hook.md",
ComponentType.MCP: ".claude/mcp-servers/{name}/config.json",
ComponentType.SANDBOX: ".claude/sandbox/{name}/sandbox.md",
}
```
**deep_merge(base: dict, overlay: dict) -> dict**
- Recursively merge overlay into base
- For dict values: recurse
- For list values: extend (dedupe)
- For other values: overlay wins
- Return new dict (don't mutate base)
**merge_settings(existing_path: Path, new_settings: dict) -> dict**
- Load existing settings.json if exists
- deep_merge existing with new_settings
- Return merged dict
**install_component(component: ComponentMetadata, target_dir: Path) -> tuple[Path, int]**
- Determine destination path from INSTALL_PATHS and component.component_type
- Replace {name} with component.name (sanitized: lowercase, hyphens)
- Create parent directories (mkdir parents=True, exist_ok=True)
- Special handling for SETTING type:
- If settings.json exists: merge_settings()
- Write merged JSON
- For other types:
- Write component.raw_content to destination
- Return (destination_path, token_cost) where token_cost = estimate_tokens(raw_content)
**class ComponentInstaller:**
```python
from skill_retriever.memory.metadata_store import MetadataStore
class ComponentInstaller:
def __init__(
self,
graph_store: GraphStore,
metadata_store: MetadataStore,
target_dir: Path,
):
self.graph_store = graph_store
self.metadata_store = metadata_store
self.target_dir = target_dir
def install(
self,
component_ids: list[str],
auto_resolve_deps: bool = True,
) -> InstallResult:
"""Install components with optional dependency resolution."""
installed: list[str] = []
skipped: list[str] = []
errors: list[str] = []
# Step 1: Resolve dependencies if requested
if auto_resolve_deps:
all_ids, deps_added = resolve_transitive_dependencies(
component_ids, self.graph_store
)
else:
all_ids = component_ids
deps_added = []
# Step 2: Check for conflicts
conflicts = detect_conflicts(all_ids, self.graph_store)
if conflicts:
# Return early with conflict info
return InstallResult(
installed=[],
skipped=component_ids,
errors=[f"Conflict: {c.component_a} vs {c.component_b}: {c.reason}" for c in conflicts],
)
# Step 3: Install each component
for comp_id in all_ids:
# Get full metadata from metadata_store
component = self.metadata_store.get(comp_id)
if component is None:
errors.append(f"Component not found: {comp_id}")
continue
try:
dest, tokens = install_component(
component=component,
target_dir=self.target_dir,
)
installed.append(comp_id)
except Exception as e:
errors.append(f"Failed to install {comp_id}: {e}")
return InstallResult(
installed=installed,
skipped=skipped,
errors=errors,
)
```
2. Update `src/skill_retriever/mcp/server.py`:
- Add `_metadata_store: MetadataStore | None = None` to global state
- Update `get_pipeline()` to also initialize metadata_store (or add `get_metadata_store()`)
- Update `install_components` tool handler:
```python
@mcp.tool
async def install_components(input: InstallInput) -> InstallResult:
"""Install components to .claude/."""
pipeline = await get_pipeline()
metadata_store = await get_metadata_store()
target = Path(input.target_dir).resolve()
installer = ComponentInstaller(
graph_store=_graph_store,
metadata_store=metadata_store,
target_dir=target,
)
return installer.install(
component_ids=input.component_ids,
auto_resolve_deps=True,
)
```
- Update `ingest_repo` to also add components to metadata_store:
```python
# After crawling
components = crawler.crawl()
metadata_store.add_many(components)
metadata_store.save()
# ... add to graph and vector stores
```
3. Create `tests/test_installer.py`:
- Test INSTALL_PATHS mapping for each ComponentType
- Test deep_merge with nested dicts and lists
- Test merge_settings with existing file
- Test install_component creates correct directory structure
- Test ComponentInstaller with mock graph_store and metadata_store
- Use pytest tmp_path fixture for isolation
4. Update `tests/test_mcp_server.py`:
- Add test for install_components with mock metadata
- Test that dependencies are resolved before installation
- Test conflict detection prevents installation
</action>
<verify>
- `uv run python -c "from skill_retriever.mcp.installer import ComponentInstaller, install_component, merge_settings"` works
- `uv run pytest tests/test_installer.py tests/test_mcp_server.py -v` passes
- `uv run ruff check src/skill_retriever/mcp/installer.py` passes
- `uv run pyright src/skill_retriever/mcp/installer.py` passes
</verify>
<done>
- INSTALL_PATHS maps all 7 ComponentTypes to .claude/ paths
- deep_merge handles nested dicts and lists correctly
- merge_settings preserves existing settings while adding new
- install_component writes to correct paths based on type
- ComponentInstaller uses MetadataStore for component lookup
- install_components tool fully wired to installer
- Dependencies auto-resolved before installation
- Conflicts detected and reported (installation blocked)
- ingest_repo stores metadata for later retrieval
- Tests verify directory structure creation
</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. `uv run pytest` - all tests pass
2. `uv run ruff check .` - no linting errors
3. `uv run pyright` - no type errors
4. Integration test:
```python
# Manual verification
from pathlib import Path
from skill_retriever.mcp.installer import install_component
from skill_retriever.entities.components import ComponentMetadata, ComponentType
# Create test component
comp = ComponentMetadata(
id="test/repo/skill/example-skill",
name="example-skill",
component_type=ComponentType.SKILL,
raw_content="# Example Skill\n\nDoes something useful.",
)
# Install to temp dir
dest, tokens = install_component(comp, Path("/tmp/test-install"))
assert dest == Path("/tmp/test-install/.claude/skills/example-skill/SKILL.md")
assert dest.exists()
print(f"Installed to {dest} ({tokens} tokens)")
```
</verification>
<success_criteria>
- install_components places skills in `.claude/skills/{name}/SKILL.md`
- install_components places commands in `.claude/commands/{name}.md`
- install_components places agents in `.claude/agents/{name}.md`
- install_components merges settings into `.claude/settings.json` (deep merge)
- Dependencies are auto-resolved before installation
- Conflicts block installation with clear error messages
- Each component reports its token cost
- All tests pass, linting clean, types valid
</success_criteria>
<output>
After completion, create `.planning/phases/06-mcp-server/06-02-SUMMARY.md`
</output>