---
phase: 06-mcp-server
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- pyproject.toml
- src/skill_retriever/mcp/__init__.py
- src/skill_retriever/mcp/schemas.py
- src/skill_retriever/mcp/rationale.py
- src/skill_retriever/mcp/server.py
- tests/test_mcp_server.py
autonomous: true
must_haves:
truths:
- "MCP server responds to tools/list with 5 tools"
- "search_components returns ranked results with rationale"
- "get_component_detail returns full component info"
- "check_dependencies returns transitive deps and conflicts"
- "ingest_repo triggers repository crawling"
- "Tool schemas stay under 300 tokens total"
artifacts:
- path: "src/skill_retriever/mcp/schemas.py"
provides: "Pydantic input/output models for all 5 tools"
exports: ["SearchInput", "SearchResult", "ComponentRecommendation", "InstallInput", "InstallResult", "DependencyCheckResult", "IngestInput", "IngestResult"]
- path: "src/skill_retriever/mcp/rationale.py"
provides: "Path-to-rationale conversion"
exports: ["generate_rationale", "path_to_explanation"]
- path: "src/skill_retriever/mcp/server.py"
provides: "FastMCP server with 5 tool handlers"
exports: ["mcp", "main"]
key_links:
- from: "src/skill_retriever/mcp/server.py"
to: "src/skill_retriever/workflows/pipeline.py"
via: "RetrievalPipeline.retrieve()"
pattern: "pipeline\\.retrieve"
- from: "src/skill_retriever/mcp/server.py"
to: "src/skill_retriever/nodes/ingestion/crawler.py"
via: "RepositoryCrawler.crawl()"
pattern: "crawler\\.crawl"
- from: "src/skill_retriever/mcp/rationale.py"
to: "src/skill_retriever/entities/graph.py"
via: "EdgeType for human descriptions"
pattern: "EdgeType"
---
<objective>
Create FastMCP server exposing 5 tools for component search, detail retrieval, dependency checking, and repository ingestion, with graph-path rationale for recommendations.
Purpose: Enable Claude Code to query the skill-retriever system via MCP protocol, receiving ranked component recommendations with explanations.
Output: Working MCP server with tool definitions, Pydantic schemas, and rationale generator.
</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
# Key source files to integrate with
@src/skill_retriever/workflows/pipeline.py
@src/skill_retriever/workflows/models.py
@src/skill_retriever/entities/graph.py
@src/skill_retriever/entities/components.py
@src/skill_retriever/nodes/retrieval/flow_pruner.py
@src/skill_retriever/nodes/retrieval/context_assembler.py
@src/skill_retriever/nodes/ingestion/crawler.py
</context>
<tasks>
<task type="auto">
<name>Task 1: Add FastMCP dependency and create Pydantic schemas</name>
<files>
pyproject.toml
src/skill_retriever/mcp/schemas.py
</files>
<action>
1. Add fastmcp dependency to pyproject.toml:
```
"fastmcp>=2.14,<3",
```
Pin to v2 to avoid breaking changes from v3 beta.
2. Create `src/skill_retriever/mcp/schemas.py` with Pydantic models:
**Input models (keep descriptions under 10 words):**
- `SearchInput`: query (str), top_k (int=5), component_type (str|None)
- `ComponentDetailInput`: component_id (str)
- `InstallInput`: component_ids (list[str]), target_dir (str=".")
- `DependencyCheckInput`: component_ids (list[str])
- `IngestInput`: repo_url (str)
**Output models:**
- `ComponentRecommendation`: id, name, type, score, rationale, token_cost
- `SearchResult`: components (list[ComponentRecommendation]), total_tokens, conflicts (list[str])
- `ComponentDetail`: id, name, type, description, tags, dependencies, raw_content, token_cost
- `InstallResult`: installed (list[str]), skipped (list[str]), errors (list[str])
- `DependencyCheckResult`: all_components (list[str]), dependencies_added (list[str]), conflicts (list[str])
- `IngestResult`: components_found (int), components_indexed (int), errors (list[str])
Use `Field(description="...")` ONLY where essential. Most fields self-documenting.
Import ComponentType from entities for type hints but keep schema simple (use str for MCP).
</action>
<verify>
- `uv add "fastmcp>=2.14,<3"` succeeds
- `uv run python -c "from skill_retriever.mcp.schemas import SearchInput, SearchResult"` works
- `uv run ruff check src/skill_retriever/mcp/schemas.py` passes
- `uv run pyright src/skill_retriever/mcp/schemas.py` passes
</verify>
<done>
- fastmcp added to dependencies
- All input/output Pydantic models defined and importable
- Models pass linting and type checking
</done>
</task>
<task type="auto">
<name>Task 2: Create rationale generator from graph paths</name>
<files>
src/skill_retriever/mcp/rationale.py
</files>
<action>
1. Create `src/skill_retriever/mcp/rationale.py` with:
**EDGE_DESCRIPTIONS dict** mapping EdgeType to human verbs:
```python
EDGE_DESCRIPTIONS = {
EdgeType.DEPENDS_ON: "requires",
EdgeType.ENHANCES: "enhances",
EdgeType.CONFLICTS_WITH: "conflicts with",
EdgeType.BUNDLES_WITH: "bundles with",
EdgeType.SAME_CATEGORY: "in same category as",
}
```
**path_to_explanation(path_nodes: list[str], graph_store: GraphStore) -> str**
- If len(path_nodes) < 2: return "Direct match"
- Iterate through consecutive node pairs
- For each pair, get edge from graph_store.get_edges()
- Build "A {verb} B" segments
- Join with " -> "
- Return "Graph traversal" if no edges found
**generate_rationale(component_id: str, retrieval_context: RetrievalContext, graph_store: GraphStore) -> str**
- Check if component came from graph (source='graph' or 'hybrid' or 'dependency')
- If source='vector' only: return "Semantic match to query"
- If source='dependency': return "Required dependency of [parent]"
- Otherwise: Look up paths from flow_pruner results if available
- Generate explanation from path
- Keep rationale under 50 words for token efficiency
2. Add TYPE_CHECKING imports for GraphStore, RetrievalContext.
3. Handle edge cases: missing nodes, no edges between nodes.
</action>
<verify>
- `uv run python -c "from skill_retriever.mcp.rationale import generate_rationale, path_to_explanation"` works
- `uv run ruff check src/skill_retriever/mcp/rationale.py` passes
- `uv run pyright src/skill_retriever/mcp/rationale.py` passes
</verify>
<done>
- EdgeType to human description mapping complete
- path_to_explanation converts graph paths to readable text
- generate_rationale produces component-specific explanations
</done>
</task>
<task type="auto">
<name>Task 3: Create FastMCP server with 5 tool handlers</name>
<files>
src/skill_retriever/mcp/__init__.py
src/skill_retriever/mcp/server.py
tests/test_mcp_server.py
</files>
<action>
1. Create `src/skill_retriever/mcp/server.py`:
**Server initialization:**
```python
import logging
from fastmcp import FastMCP
# Configure logging to stderr (CRITICAL: never print to stdout)
logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler()])
mcp = FastMCP("skill-retriever")
```
**Global state (initialized on first call):**
- `_pipeline: RetrievalPipeline | None = None`
- `_graph_store: GraphStore | None = None`
- `_vector_store: FAISSVectorStore | None = None`
- `_init_lock = asyncio.Lock()` for thread-safe initialization
- `async def get_pipeline()` lazy initializer
**Tool handlers (keep docstrings under 10 words):**
a) `@mcp.tool async def search_components(input: SearchInput) -> SearchResult`
- Docstring: "Search components by task."
- Call pipeline.retrieve(query, component_type, top_k)
- Generate rationale for each result using generate_rationale()
- Estimate token cost using estimate_tokens() from context_assembler
- Return SearchResult with conflicts from pipeline result
b) `@mcp.tool async def get_component_detail(input: ComponentDetailInput) -> ComponentDetail`
- Docstring: "Get full component info."
- Look up node in graph_store
- Return ComponentDetail with all fields
- Include token_cost estimate
c) `@mcp.tool async def install_components(input: InstallInput) -> InstallResult`
- Docstring: "Install components to .claude/."
- **Defer actual installation to Plan 02**
- For now: return InstallResult with empty lists and error "Installation not yet implemented"
d) `@mcp.tool async def check_dependencies(input: DependencyCheckInput) -> DependencyCheckResult`
- Docstring: "Check deps and conflicts."
- Call resolve_transitive_dependencies() from dependency_resolver
- Call detect_conflicts()
- Return DependencyCheckResult
e) `@mcp.tool async def ingest_repo(input: IngestInput) -> IngestResult`
- Docstring: "Index a component repository."
- Parse repo_url to extract owner/name (handle GitHub URLs)
- Clone repo to temp directory using git
- Run RepositoryCrawler.crawl()
- Add components to graph_store and vector_store
- Return IngestResult with counts
**Entry point:**
```python
def main() -> None:
mcp.run(transport="stdio")
if __name__ == "__main__":
main()
```
2. Update `src/skill_retriever/mcp/__init__.py`:
```python
from skill_retriever.mcp.server import main, mcp
__all__ = ["main", "mcp"]
```
3. Create `tests/test_mcp_server.py`:
- Test that all 5 tools are registered
- Test SearchInput/SearchResult serialization
- Test rationale generation with mock graph
- Use pytest fixtures for mock stores
4. Add `[project.scripts]` entry in pyproject.toml:
```toml
[project.scripts]
skill-retriever = "skill_retriever.mcp:main"
```
</action>
<verify>
- `uv run python -c "from skill_retriever.mcp import mcp; print(len(mcp._tool_manager._tools))"` shows 5 tools
- `uv run pytest tests/test_mcp_server.py -v` passes
- `uv run ruff check src/skill_retriever/mcp/` passes
- `uv run pyright src/skill_retriever/mcp/` passes
- `uv run skill-retriever --help` shows MCP server entry point
</verify>
<done>
- FastMCP server created with 5 tool handlers
- All tools have minimal docstrings (under 10 words each)
- search_components returns results with rationale and token costs
- ingest_repo triggers crawling and indexing
- Server runnable via `skill-retriever` command
- Tests verify tool registration and basic functionality
</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. `uv run pytest` - all tests pass including new MCP tests
2. `uv run ruff check .` - no linting errors
3. `uv run pyright` - no type errors
4. Tool schema token count check:
```python
# Verify schemas stay under 300 tokens
from skill_retriever.mcp.server import mcp
import json
schemas = json.dumps([t.model_dump() for t in mcp._tool_manager._tools.values()])
print(f"Schema tokens: ~{len(schemas) // 4}") # Should be < 300
```
</verification>
<success_criteria>
- MCP server exposes exactly 5 tools: search_components, get_component_detail, install_components, check_dependencies, ingest_repo
- search_components returns ComponentRecommendation objects with rationale field populated
- Each recommendation includes token_cost estimate
- Tool docstrings are under 10 words each
- Total schema tokens estimated under 300
- All tests pass, linting clean, types valid
</success_criteria>
<output>
After completion, create `.planning/phases/06-mcp-server/06-01-SUMMARY.md`
</output>