# A2 — Dependency Injection Wiring

- **Date**: 2026-02-20
- **Status**: `done`
- **Branch**: `feature/2026-02-20-a2-di-wiring`
- **Priority**: `medium`

## Problem

Os 12 tools importam implementações concretas de adapters diretamente (ex: `from mcp_tap.connection.tester import test_server_connection`). O AppContext em `server.py` tem apenas 2 campos (`http_client`, `registry`). Os 16 Protocols definidos em A1 não estão conectados.

Isso resulta em:
- Testes com cadeias de `@patch` frágeis
- Adapters não são trocáveis
- Composition root (`server.py`) não cumpre seu papel de wiring

## Context

- A1 (Protocols formais) já está feito — 16 Protocols em 10 `base.py` files
- AppContext existe mas só tem 2 campos
- 933 testes passando com `unittest.mock.patch`
- Mudança puramente interna — zero impacto user-facing

## Solution

### Abordagem Tier A/B (Hybrid DI)

Após análise crítica com agentes de arquitetura e estratégia, adotamos uma abordagem híbrida:

**Tier B (DI via AppContext — stateful / I/O boundaries):**
6 adapter classes criadas, AppContext expandido para 8 campos, 6 tools migrados.

| Campo | Protocol | Adapter Class |
|-------|----------|---------------|
| `http_client` | (raw) | já existia |
| `registry` | `RegistryClientPort` | já existia |
| `github_metadata` | `GitHubMetadataPort` | `DefaultGitHubMetadata(http_client)` |
| `connection_tester` | `ConnectionTesterPort` | `DefaultConnectionTester()` |
| `healing` | `HealingOrchestratorPort` | `DefaultHealingOrchestrator(connection_tester)` |
| `security_gate` | `SecurityGatePort` | `DefaultSecurityGate(http_client)` |
| `readme_fetcher` | `ReadmeFetcherPort` | `DefaultReadmeFetcher(http_client)` |
| `installer_resolver` | `InstallerResolverPort` | `DefaultInstallerResolver()` |

**Tier A (direct imports — stateless pure functions):**
Config detection, reader, writer kept as direct imports. Wrapping stateless functions
in classes adds ceremony without value — `@patch` works well for these.

### Key Design Decisions

1. **`DefaultHealingOrchestrator` breaks adapter coupling**: Receives `ConnectionTesterPort`
   via constructor. The module-level `heal_and_retry()` was refactored to use an injectable
   `_heal_loop()` core function, preserving backwards compatibility.

2. **`AppContext` is `frozen=True, slots=True`**: Follows project convention.

3. **`get_context(ctx)` with runtime assertion**: Catches misconfiguration early.

4. **`PackageInstaller` shadowing fixed**: The type alias in `resolver.py` that shadowed
   the Protocol was removed. New `InstallerResolverPort` added to `installer/base.py`.

## Files Changed

### New:
- `src/mcp_tap/tools/_helpers.py`

### Adapter classes added (in existing files):
- `src/mcp_tap/connection/tester.py` — `DefaultConnectionTester`
- `src/mcp_tap/evaluation/github.py` — `DefaultGitHubMetadata`
- `src/mcp_tap/inspector/fetcher.py` — `DefaultReadmeFetcher`
- `src/mcp_tap/security/gate.py` — `DefaultSecurityGate`
- `src/mcp_tap/healing/retry.py` — `DefaultHealingOrchestrator` + `_heal_loop()` refactor
- `src/mcp_tap/installer/resolver.py` — `DefaultInstallerResolver`
- `src/mcp_tap/installer/base.py` — `InstallerResolverPort`

### Composition root updated:
- `src/mcp_tap/server.py` — AppContext expanded, lifespan wires all adapters

### Tools migrated (Tier B only):
- `src/mcp_tap/tools/configure.py`
- `src/mcp_tap/tools/test.py`
- `src/mcp_tap/tools/health.py`
- `src/mcp_tap/tools/search.py`
- `src/mcp_tap/tools/inspect.py`
- `src/mcp_tap/tools/restore.py`

### Tools unchanged (Tier A only):
- `src/mcp_tap/tools/list.py`
- `src/mcp_tap/tools/scan.py`
- `src/mcp_tap/tools/verify.py`
- `src/mcp_tap/tools/remove.py`
- `src/mcp_tap/tools/stack.py` (delegates to configure_server)

### Tests updated:
- `tests/test_tools_configure.py`
- `tests/test_tools_test.py`
- `tests/test_tools_health.py`
- `tests/test_tools_search.py`
- `tests/test_restore.py`
- `tests/test_conflicts.py`
- `tests/test_security_gate.py`
- `tests/test_healing.py`

## Verification

- [x] Tests pass: `pytest tests/` — 933 passed in 1.21s
- [x] Linter passes: `ruff check src/ tests/`
- [x] Formatter passes: `ruff format --check src/ tests/`
- [ ] Server starts: `python -m mcp_tap`
- [x] Foundation verified independently before tool migrations

## Lessons Learned

- **Tier A/B split is the right approach**: Wrapping stateless functions in classes is
  "Java-brain" in Python. DI only for stateful adapters shows architectural discernment.
- **`_heal_loop()` pattern**: Extracting a core function with injectable callable preserves
  backwards compatibility while enabling DI. Better than duplicating logic.
- **Parallel agent migration works well**: 3 agents migrating 2 tools each completed
  successfully without conflicts.
