# Python Craftsman Memory — mcp-tap

## Project
- mcp-tap: Python 3.11+ MCP meta-server
- Build: hatchling | Linter: ruff (line-length=100)
- All dataclasses: `frozen=True, slots=True`
- All I/O: async (httpx, asyncio subprocess)
- Imports: always `from __future__ import annotations`
- `PackageInstaller` Protocol in `installer/base.py`

## Ruff / Linting
- ruff not in PATH by default — use `uv run ruff check ...` or install via `uv pip install ruff`
- ruff enforces single blank line between imports block and first code (not double blank)
- Target Python 3.11: cannot use `type X = ...` (PEP 695) — use plain assignment for TypeAlias
- ruff selects: E, F, W, I, UP, B, SIM, RUF

## Architecture Patterns
- Domain models in `models.py` (zero deps), errors in `errors.py`
- Scanner package: `scanner/{__init__, detector, recommendations}.py`
- `scan_project(path: str) -> ProjectProfile` is the scanner public entry point
- All parsers return `tuple[list[DetectedTechnology], list[str]]` (techs, env_vars)
- Parsers run concurrently via `asyncio.gather(return_exceptions=True)`
- File reads use `asyncio.to_thread(filepath.read_text, encoding="utf-8")`
- Docker-compose parsed via regex (no PyYAML dep)
- `tomllib` from stdlib for TOML (Python 3.11+)
- Use `dataclasses.replace()` for immutable updates on frozen dataclasses

## Tool Patterns
- Tools in `tools/` return `dict[str, object]` (serialized via `dataclasses.asdict`)
- Every tool has a `ctx: Context` param; app context accessed via `ctx.request_context.lifespan_context`
- Error handling: `McpTapError` -> return error dict; `Exception` -> `ctx.error()` + return error dict
- Config reading pattern: `detect_clients()` or `resolve_config_path(MCPClient(client))` -> `read_config()` -> `parse_servers()`
- `test_server_connection()` in `connection/tester.py` is the shared connection test primitive
- Batch operations use `asyncio.gather(*tasks, return_exceptions=True)` and handle `BaseException` results
- `zip()` requires `strict=True` per ruff B905 rule
- Scanner module: `scanner/scoring.py` added for context-aware search relevance scoring

## Test Patterns
- Mock Context: `ctx = MagicMock(); ctx.info = AsyncMock(); ctx.error = AsyncMock()`
- Use `@patch("mcp_tap.tools.<module>.<dependency>")` for mocking
- Fixture directories in `tests/fixtures/` (python_fastapi_project, node_express, etc.)
- `asyncio_mode = "auto"` in pyproject.toml — no need for `@pytest.mark.asyncio`
- VS Code auto-linter may modify files during editing — always re-check imports after edits

## Healing Module (Issue #10)
- `healing/` package: classifier.py, fixer.py, retry.py
- `classify_error(ConnectionTestResult) -> DiagnosisResult` — pattern-matches error strings
- `generate_fix(DiagnosisResult, ServerConfig) -> CandidateFix` — strategies per ErrorCategory
- `heal_and_retry(name, config, error, ...) -> HealingResult` — orchestration loop
- Models: `ErrorCategory(StrEnum)`, `DiagnosisResult`, `CandidateFix`, `HealingAttempt`, `HealingResult`
- `HealingAttempt` fields: `diagnosis`, `fix_applied`, `success` (bool)
- `HealingResult` fields: `fixed`, `attempts`, `fixed_config`, `user_action_needed`
- Env var classifier: requires keywords AND (uppercase env var pattern OR "environment variable" phrase)
- Auto-fixable: COMMAND_NOT_FOUND (path resolution), TIMEOUT (escalating timeouts), TRANSPORT_MISMATCH
- User action required: CONNECTION_REFUSED, AUTH_FAILED, MISSING_ENV_VAR, PERMISSION_DENIED, UNKNOWN
- Integrated into configure.py (_try_heal helper), test.py (auto_heal param), health.py (auto_heal param)
- `_TIMEOUT_ESCALATION = (15, 30, 60)` for timeout category retries

## Security Gate Module (I2)
- `security/` package: `__init__.py`, `gate.py`
- `run_security_gate(pkg, repo_url, command, args, http_client) -> SecurityReport`
- `_check_command()` — sync, checks suspicious commands (bash/sh/curl/wget/cmd/powershell)
- `_check_github()` — async, checks archived/stars/license/staleness via `fetch_repo_metadata`
- Models: `SecurityRisk(StrEnum)`, `SecuritySignal`, `SecurityReport` (with `.passed`, `.warnings`, `.blockers`)
- Integrated in configure.py: Step 3.5 between build config and write config
- `_run_security_check()` helper is NON-BLOCKING (exceptions return None, install proceeds)
- BLOCK signals prevent installation; WARN signals are logged but don't block
- Uses `datetime.UTC` alias (not `timezone.utc`) per ruff UP017 rule
- `TYPE_CHECKING` guard for `httpx` import in configure.py (only used in type hint)

## Stacks Module (I3)
- `stacks/` package: `__init__.py`, `loader.py`, `presets/*.yaml`
- `load_stack(name_or_path: str) -> Stack` — loads builtin or file-based stacks
- `list_builtin_stacks() -> list[dict[str, object]]` — metadata for all builtins
- Built-in presets: data-science, web-dev, devops (YAML in `stacks/presets/`)
- `_parse_yaml(text, source) -> Stack` — uses `yaml.safe_load` (PyYAML dep)
- `apply_stack` tool in `tools/stack.py` — delegates to `configure_server` per server
- Models: `StackServer` (name, package_identifier, registry_type, version, env_vars)
- Models: `Stack` (name, description, servers, version, author)
- `pyyaml>=6.0` added to project dependencies
- Uses `importlib.resources.files()` for reading bundled YAML presets
- Registered as destructive tool in server.py

## Conventions
- Commit messages: English, no Co-Authored-By (per CLAUDE.md)
- Comments/code in English, agent communication in Portuguese (BR)
- Branch naming: `feature/YYYY-MM-DD-description`
- Line length: 100 chars
