I2.2 — Dependency MCP Adapter for v0.4.0 MCP vertical slice - #78
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
app/mcp/wiring._current_git_sha() can crash application startup in containerized/non-git environments due to unconditional git rev-parse HEAD execution.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Implements the v0.4.0 I2.2 MCP “Dependency Adapter” vertical slice by wiring MCP tool dispatch to the existing ArchitectureIntelligenceService.get_service_dependencies without altering response semantics, and adds unit/integration coverage to lock in dispatch, sanitization, and read-only behavior.
Changes:
- Add real MCP dispatch for
get_service_dependencies, includingpydantic.ValidationError→ actionableToolErrormapping while relying on SDK sanitization for unexpected failures. - Introduce
app/mcp/wiring.py+ FastAPI lifespan wiring to bridge import-time tool registration with runtime-constructed Neo4j-backed services. - Add comprehensive unit/integration tests for adapter behavior, schema/boundary constraints, and direct-vs-MCP equivalence.
File summaries
| File | Description |
|---|---|
| tests/unit/test_mcp_service_dependencies_adapter.py | Unit coverage for get_service_dependencies dispatch behavior, error mapping, and sanitization regression. |
| tests/unit/test_mcp_read_only_boundary.py | AST-level guard that MCP adapter modules don’t import graph repositories / open sessions / embed Cypher. |
| tests/unit/test_mcp_discovery.py | Updates discovery tests to reflect I2.2 behavior and adds “unconfigured wiring” sanitization coverage. |
| tests/integration/test_mcp_service_dependencies_equivalence.py | End-to-end equivalence and revision-fence immutability checks against real Neo4j. |
| app/mcp/wiring.py | Lazy runtime wiring for the MCP tool to access a real ArchitectureIntelligenceService. |
| app/mcp/tools.py | Real tool body for get_service_dependencies + injectable get_service for testability; get_evidence remains stub. |
| app/main.py | Lifespan startup now configures the MCP wiring using the app’s Neo4j driver. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| def _current_git_sha() -> str: | ||
| result = subprocess.run( | ||
| ["git", "rev-parse", "HEAD"], capture_output=True, text=True, check=True, cwd=_REPO_ROOT | ||
| ) | ||
| return result.stdout.strip() |
|
Reviewed current head Verdict: REQUEST CHANGES. The core I2.2 design is sound: the MCP tool delegates once to
These fixes do not require expanding I2.2 or changing dependency semantics. |
Gives get_service_dependencies a real dispatch body: maps MCP input to the existing I1 request type, calls ArchitectureIntelligenceService exactly once, and returns its answer unchanged as structuredContent (spec §10). Adds app/mcp/wiring.py as the lazy composition root that bridges MCP tool registration (import time) with FastAPI's driver construction (lifespan startup), and proves service/MCP equivalence, determinism, and read-only behavior end to end against real Neo4j. get_evidence remains a stub for I2.3. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tBVicyuqNjpdRVBudmmaJ
…catching 1. app/mcp/wiring._resolve_build_revision() no longer runs `git rev-parse HEAD` unconditionally at lifespan startup - this repo's production Dockerfile (python:3.13-slim, no git binary, no .git dir) crashed on every container start. Now prefers an explicit AIP_BUILD_REVISION env var (wired through Dockerfile's ARG/ENV and the release Docker workflow's build-args from github.sha), validated as a real 40-hex SHA when present, and falls back to git only for local dev - logging a placeholder instead of crashing when neither is available. 2. app/mcp/tools.py no longer catches pydantic.ValidationError broadly around the whole service call. That could echo back a ValidationError raised from constructing internal graph-derived models (SnapshotRef, claims, ServiceDependenciesData, ArchitectureAnswer), not just the caller's own malformed observation-context value - leaking Pydantic's input_value detail past the SDK's own sanitization. The adapter now pre-validates a supplied observation-context's values before dispatch (reusing the same build_observation_context_ref helper the service calls internally), and never calls the service inside a ValidationError handler at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011tBVicyuqNjpdRVBudmmaJ
3d6d62f to
74b970a
Compare
|
Both blockers fixed in 74b970a (rebased onto latest main):
Full suite ( |
|
Re-reviewed current head Verdict: APPROVE — no remaining code findings. Both previous blockers are resolved:
The adapter still calls Current-head lint/tests, dependency audit, and both CodeQL analyses are green, and GitHub reports the PR as mergeable. One housekeeping step remains before merge: resolve the existing Copilot thread about |
Summary
get_service_dependenciesnow has a real MCP dispatch body (spec i2-mcp-vertical-slice-and-evidence-drill-down.md §19 "I2.2 — Dependency MCP Adapter"): maps MCP input to the existing I1ServiceDependenciesRequest, callsArchitectureIntelligenceService.get_service_dependenciesexactly once, and returns its answer unchanged asstructuredContent(spec §10) — no new semantics, claims, or limitations are added.app/mcp/wiring.pybridges MCP tool registration (import time, before FastAPI's lifespan exists) with the real Neo4j driver/Producerbuilt during lifespan startup, without the adapter itself opening a session, running Cypher, or importingapp.graph(spec §8 boundary).get_evidenceremains an unimplemented stub (ToolError), unchanged — its dispatch is I2.3.Notable finding
While implementing the failure-mapping behavior (spec §16), live inspection of the installed
mcpSDK (mcp.server.mcpserver.tools.base.Tool.run) showed it already sanitizes any uncaught non-ToolErrortool-body exception into a genericUnexpectedToolError("Error executing tool <name>")— never interpolating the raw exception text — and separately logs the full traceback server-side. So the adapter only needs to special-casepydantic.ValidationError(the service's own documented, deliberate signal for a malformed observation-context value, e.g. a reversed window) to preserve actionable client feedback; every other exception (e.g. a Neo4j connectivity failure whose message could embed the bolt URI/host) is already safely sanitized by the SDK with no adapter code needed. A regression test (test_unexpected_service_failure_is_sanitized_not_leaked) locks this in.Test plan
uv run ruff check ./uv run ruff format --check .uv run pytest tests/unit(709 passed)uv run pytest tests/integration(197 passed)test_mcp_service_dependencies_adapter.py(dispatch/error-mapping against a stub service, incl. the sanitization regression test),test_mcp_read_only_boundary.py(static AST check thatapp/mcpimports noapp.graph/neo4jsession code, per spec §17 item 17)test_mcp_service_dependencies_equivalence.py(real Neo4j — direct-vs-MCP structured-content equivalence for a confirmed answer and a provider-only refusal, determinism across repeated calls, revision-fence unchanged across success/refusal/validation-error paths)test_mcp_discovery.py's stale "not yet implemented" expectation forget_service_dependencies🤖 Generated with Claude Code
https://claude.ai/code/session_011tBVicyuqNjpdRVBudmmaJ