---
phase: 19-mcp-chunk-response
plan: "01"
type: tdd
wave: 1
depends_on: []
files_modified:
  - src/corpus_analyzer/mcp/server.py
  - tests/mcp/test_server.py
autonomous: true
requirements:
  - CHUNK-03

must_haves:
  truths:
    - "MCP corpus_search results include a 'text' field with full untruncated chunk content"
    - "MCP corpus_search results include 'start_line' and 'end_line' fields"
    - "The 'text' field appears first in result dicts (content-first ordering)"
    - "Legacy rows (chunk_text empty) return text='' without raising"
    - "Results no longer include 'snippet' or 'full_content' (no whole-file reads)"
    - "A test asserts that 'snippet' does NOT appear in results (regression guard)"
  artifacts:
    - path: "src/corpus_analyzer/mcp/server.py"
      provides: "corpus_search tool with chunk-level response fields"
      contains: "chunk_text"
    - path: "tests/mcp/test_server.py"
      provides: "TDD tests for new response shape"
      exports: ["test_corpus_search_text_field", "test_corpus_search_line_bounds", "test_corpus_search_no_snippet_field"]
  key_links:
    - from: "tests/mcp/test_server.py"
      to: "src/corpus_analyzer/mcp/server.py"
      via: "corpus_search function import"
      pattern: "from corpus_analyzer\\.mcp\\.server import corpus_search"
    - from: "src/corpus_analyzer/mcp/server.py"
      to: "LanceDB row dict"
      via: "row.get(\"chunk_text\", \"\")"
      pattern: "row\\.get\\(\"chunk_text\""
---

<objective>
Add `text`, `start_line`, and `end_line` to the MCP `corpus_search` response; remove the old `snippet` (truncated) and `full_content` (whole-file read) fields. Each result becomes a self-contained unit of knowledge for LLM callers — no follow-up file-read needed.

Purpose: Satisfies CHUNK-03. LLM callers currently get a truncated snippet and the entire raw file; after this phase they get the exact chunk content with precise line boundaries.
Output: Updated `mcp/server.py` and `tests/mcp/test_server.py`, with all tests green.
</objective>

<execution_context>
@/home/ollie/.claude/get-shit-done/workflows/execute-plan.md
@/home/ollie/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/19-mcp-chunk-response/19-CONTEXT.md
@src/corpus_analyzer/mcp/server.py
@tests/mcp/test_server.py
</context>

<feature>
  <name>Self-contained MCP chunk response</name>
  <files>src/corpus_analyzer/mcp/server.py, tests/mcp/test_server.py</files>
  <behavior>
    Each result dict in corpus_search return value must:
    - Contain key "text" (str) — value of row["chunk_text"] or "" if missing/empty; positioned first
    - Contain key "start_line" (int) — value of row["start_line"] or 0 if missing
    - Contain key "end_line" (int) — value of row["end_line"] or 0 if missing
    - NOT contain key "snippet" (old truncated preview)
    - NOT contain key "full_content" (old whole-file read)
    - Retain keys: "path", "score", "construct_type", "summary", "file_type", "chunk_name"

    Cases:
    - Normal row with chunk_text="def foo():\n    pass", start_line=10, end_line=12
      → result["text"] == "def foo():\n    pass", result["start_line"] == 10, result["end_line"] == 12
    - Legacy row with chunk_text="" (or missing), start_line=0, end_line=0
      → result["text"] == "", result["start_line"] == 0, result["end_line"] == 0
    - Any result → "snippet" not in result, "full_content" not in result
  </behavior>
  <implementation>
    In server.py corpus_search():
    1. Remove the Path.read_text() file-read block entirely (no more full_content)
    2. Remove extract_snippet import and usage
    3. Build result_dict with these keys in order:
       "text": str(row.get("chunk_text", "") or ""),
       "start_line": int(row.get("start_line", 0) or 0),
       "end_line": int(row.get("end_line", 0) or 0),
       "path": str(row.get("file_path", "")),
       "score": float(row.get("_relevance_score", 0.0)),
       "construct_type": str(row.get("construct_type") or "documentation"),
       "chunk_name": str(row.get("chunk_name", "") or ""),
       "summary": str(row.get("summary") or ""),
       "file_type": str(row.get("file_type", "")),
    4. Remove the OSError/content_error block (no file read = no OSError)
    5. Update the tool docstring to document that text is full untruncated chunk body,
       start_line/end_line are source file line boundaries for navigation
    6. Remove unused imports: Path, extract_snippet

    Existing tests that assert "snippet" in r and "full_content" in r must be updated to
    assert the NEW fields instead. The test asserting "content_error" on OSError must be
    replaced with a test asserting "content_error" does NOT appear (no file read means no OSError path).
  </implementation>
</feature>

<tasks>

<task type="tdd">
  <name>Task 1 RED: Write failing tests for new corpus_search response shape</name>
  <files>tests/mcp/test_server.py</files>
  <action>
    Add new tests to tests/mcp/test_server.py that describe the CHUNK-03 response contract.
    These tests MUST fail before server.py is changed.

    New tests to add (keep existing tests; only update the ones that directly conflict):

    1. test_corpus_search_text_field_present
       Raw row has chunk_text="def foo():\n    pass". Assert result["text"] == "def foo():\n    pass".

    2. test_corpus_search_line_bounds_present
       Raw row has start_line=10, end_line=12. Assert result["start_line"] == 10 and result["end_line"] == 12.

    3. test_corpus_search_legacy_row_empty_text
       Raw row has chunk_text="" (or no chunk_text key), start_line=0, end_line=0.
       Assert result["text"] == "", result["start_line"] == 0, result["end_line"] == 0. No exception.

    4. test_corpus_search_no_snippet_field (regression guard)
       Assert "snippet" not in result dict.

    5. test_corpus_search_no_full_content_field (regression guard)
       Assert "full_content" not in result dict.

    6. test_corpus_search_text_field_is_first_key
       Assert list(result.keys())[0] == "text" (content-first ordering).

    Also update test_corpus_search_returns_results_shape:
    - Remove assertion on "snippet" and "full_content"
    - Add assertions: "text" in r, "start_line" in r, "end_line" in r, "chunk_name" in r

    Also remove or update test_corpus_search_content_error_present_on_oserror and
    test_corpus_search_content_error_absent_on_success — these test the file-read path that
    will be removed. Replace with:
    - test_corpus_search_no_content_error_field: Assert "content_error" not in result (no file read).

    Run: uv run pytest tests/mcp/test_server.py -v
    Confirm the NEW tests FAIL (RED). Existing passing tests may also fail due to shape changes.
    Commit: test(19-01): add RED tests for CHUNK-03 MCP response shape
  </action>
  <verify>
    <automated>uv run pytest tests/mcp/test_server.py::test_corpus_search_text_field_present tests/mcp/test_server.py::test_corpus_search_line_bounds_present tests/mcp/test_server.py::test_corpus_search_no_snippet_field -v 2>&1 | grep -E "FAILED|PASSED|ERROR"</automated>
    <manual>Confirm the three new tests show FAILED (RED phase confirmed)</manual>
    <sampling_rate>run after this task, before Task 2</sampling_rate>
  </verify>
  <done>At least the 6 new tests FAIL. RED phase established for CHUNK-03 contract.</done>
</task>

<task type="tdd">
  <name>Task 2 GREEN: Implement self-contained chunk response in server.py</name>
  <files>src/corpus_analyzer/mcp/server.py</files>
  <action>
    Rewrite the corpus_search result-building loop in server.py to satisfy the RED tests.

    REMOVE:
    - The import of Path (no longer needed for file reads)
    - The import of extract_snippet from search.formatter
    - The try/except OSError block that reads full_content via Path.read_text()
    - The content_error conditional block
    - The OSError logging path

    CHANGE result_dict construction to (in this key order for content-first):
    ```python
    result_dict: dict[str, Any] = {
        "text": str(row.get("chunk_text", "") or ""),
        "start_line": int(row.get("start_line", 0) or 0),
        "end_line": int(row.get("end_line", 0) or 0),
        "path": str(row.get("file_path", "")),
        "score": float(row.get("_relevance_score", 0.0)),
        "construct_type": str(row.get("construct_type") or "documentation"),
        "chunk_name": str(row.get("chunk_name", "") or ""),
        "summary": str(row.get("summary") or ""),
        "file_type": str(row.get("file_type", "")),
    }
    ```

    UPDATE the tool docstring to document:
    - "text": full untruncated chunk body (empty string for legacy rows where chunk_text is empty)
    - "start_line"/"end_line": source file line boundaries (1-indexed; 0 for legacy rows)
    - Remove any mention of "snippet" or "full_content" from the docstring

    After implementation, run full test suite:
      uv run pytest tests/mcp/test_server.py -v
    All tests must PASS (GREEN). Then run:
      uv run ruff check src/corpus_analyzer/mcp/server.py
      uv run mypy src/corpus_analyzer/mcp/server.py
    Both must be clean (no errors).

    Commit: feat(19-01): implement CHUNK-03 self-contained MCP chunk response
  </action>
  <verify>
    <automated>uv run pytest tests/mcp/test_server.py -v && uv run ruff check src/corpus_analyzer/mcp/server.py && uv run mypy src/corpus_analyzer/mcp/server.py</automated>
    <manual>Inspect result dict key order in server.py: "text" must be the first key literal in the dict construction</manual>
    <sampling_rate>run after this task</sampling_rate>
  </verify>
  <done>All MCP server tests pass GREEN. ruff and mypy clean. "text", "start_line", "end_line" in response; "snippet" and "full_content" absent. Regression guards passing.</done>
</task>

<task type="auto">
  <name>Task 3: Full regression check</name>
  <files></files>
  <action>
    Run the full test suite to confirm no regressions introduced outside the MCP module:
      uv run pytest -v --tb=short

    Then run lint and type check:
      uv run ruff check .
      uv run mypy src/

    If any failures exist unrelated to this phase (pre-existing), document them.
    If any failures are caused by this phase's changes, fix before committing.

    Commit if clean: refactor(19-01): verify full suite green after CHUNK-03
    (Only commit if there were actual changes needed; skip commit if this is a pure verification step.)
  </action>
  <verify>
    <automated>uv run pytest -v --tb=short 2>&1 | tail -5</automated>
    <sampling_rate>run after this task</sampling_rate>
  </verify>
  <done>Full test suite passes. ruff and mypy clean across all modules. Count of tests >= 340 (no tests were lost).</done>
</task>

</tasks>

<verification>
After all tasks complete:
1. uv run pytest tests/mcp/test_server.py -v — all tests pass
2. grep "chunk_text" src/corpus_analyzer/mcp/server.py — confirms chunk_text is read from row
3. grep "full_content\|read_text\|extract_snippet" src/corpus_analyzer/mcp/server.py — returns nothing (removed)
4. uv run pytest -v 2>&1 | tail -3 — full suite passes with >= 340 tests
</verification>

<success_criteria>
- CHUNK-03 satisfied: MCP corpus_search results include text (full chunk body), start_line, end_line
- text field is first key in result dict (content-first)
- Legacy rows return text="" without raising
- snippet and full_content fields absent from all results
- Test asserts "snippet" not in result (regression guard present)
- ruff and mypy clean
- All tests pass (count >= 340)
</success_criteria>

<output>
After completion, create `.planning/phases/19-mcp-chunk-response/19-01-SUMMARY.md`
</output>