fix: exclude html_content from list_boards response (#276) #277

Merged
ldraney merged 2 commits from fix/list-boards-exclude-html into main 2026-06-13 16:53:56 +00:00
Owner

Summary

list_boards returned 57K chars because every board included html_content. One board alone carried 10K of HTML. This exceeded the MCP token limit, breaking the tool call. Fix excludes html_content from the listing endpoint; detail endpoint is unchanged.

Changes

  • Adds BoardListOut schema (inherits BoardOut minus html_content)
  • GET /boards uses BoardListOut — 70% smaller response (57K -> 17K chars)
  • GET /boards/{slug} still returns full BoardOut with html_content
  • Extracts _board_counts() helper to deduplicate count logic

Test Plan

  • 709 tests pass, zero failures
  • Verify list_boards MCP tool returns inline after deploy (no overflow)
  • Verify get_board(slug) still includes html_content

Review Checklist

  • No new dependencies
  • Backward compatible (field removed from listing only, still on detail)
  • MCP server and SDK consume raw JSON — no schema coupling to update

Closes #276

## Summary `list_boards` returned 57K chars because every board included `html_content`. One board alone carried 10K of HTML. This exceeded the MCP token limit, breaking the tool call. Fix excludes `html_content` from the listing endpoint; detail endpoint is unchanged. ## Changes - Adds `BoardListOut` schema (inherits `BoardOut` minus `html_content`) - `GET /boards` uses `BoardListOut` — 70% smaller response (57K -> 17K chars) - `GET /boards/{slug}` still returns full `BoardOut` with `html_content` - Extracts `_board_counts()` helper to deduplicate count logic ## Test Plan - [x] 709 tests pass, zero failures - [ ] Verify `list_boards` MCP tool returns inline after deploy (no overflow) - [ ] Verify `get_board(slug)` still includes `html_content` ## Review Checklist - [x] No new dependencies - [x] Backward compatible (field removed from listing only, still on detail) - [x] MCP server and SDK consume raw JSON — no schema coupling to update ## Related Notes Closes #276
Listing 59 boards returned 57K chars due to html_content on every board.
New BoardListOut schema drops html_content for the listing endpoint;
get_board(slug) still returns the full BoardOut with html_content.

Reduces list_boards response from ~57K to ~17K chars (70% reduction).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

PR #277 Review

DOMAIN REVIEW

Tech stack: Python / FastAPI / Pydantic / SQLAlchemy

Schema design (schemas.py):
The inheritance direction is correct -- BoardListOut is the base with shared fields, BoardOut extends it by adding html_content. This is clean Pydantic inheritance and means any future shared field additions only need to touch BoardListOut. The model_config on the base class is inherited by BoardOut, which is correct behavior.

Helper extraction (boards.py):
_board_counts() is cleanly extracted. The count loop logic is now in one place, called by both _board_to_list_out() and _board_to_out(). No behavioral change in the count logic itself.

Route change:
GET /boards switches from response_model=list[BoardOut] to response_model=list[BoardListOut] and uses the new _board_to_list_out() helper. GET /boards/{slug} and POST /boards are unchanged and still return full BoardOut. Correct.

BLOCKERS

None.

NITS

  1. DRY in helper functions: _board_to_list_out() and _board_to_out() duplicate the same field mapping (id, slug, name, project_slug, item_counts, created_at, updated_at). Since BoardOut inherits from BoardListOut, _board_to_out could construct a BoardListOut first and unpack it:

    def _board_to_out(board_note: Note, db: Session) -> BoardOut:
        base = _board_to_list_out(board_note, db)
        return BoardOut(**base.model_dump(), html_content=board_note.html_content or None)
    

    This keeps the field mapping in one place. Not blocking since the field list is short and localized.

  2. Test coverage for the new behavior: No existing test validates that html_content is absent from GET /boards responses or present in GET /boards/{slug} responses. The existing test_list_boards (line 85) only checks status code and count. The test_existing_fields_preserved (line 1130) validates POST /boards (which still returns BoardOut), not the listing endpoint. Adding two assertions would lock down the contract:

    • GET /boards response items do NOT contain html_content
    • GET /boards/{slug} response DOES contain html_content

    Not a blocker since this is a subtractive change with 709 passing tests, but it would prevent accidental regressions.

  3. Blank line removal (boards.py): The original _board_to_out had a blank line between the query and the counts = BoardItemCounts() line. The new _board_counts removes it. Trivial, but consistent with the rest of the file style (no blank lines between sequential statements in helpers).

SOP COMPLIANCE

  • PR body has ## Summary, ## Changes, ## Test Plan, ## Related
  • Commit message is descriptive and references parent issue (#276)
  • No secrets, .env files, or credentials committed
  • No unnecessary file changes (scope is tight: 2 files, 25 additions, 8 deletions)
  • Closes #276 in PR body

PROCESS OBSERVATIONS

  • Change failure risk: Low. Subtractive change to a listing endpoint. No migration, no model change, no new dependencies.
  • Deployment frequency: Clean, small PR. Good candidate for fast merge.
  • Documentation: PR body clearly explains the 57K -> 17K improvement and the MCP token limit motivation.

VERDICT: APPROVED

## PR #277 Review ### DOMAIN REVIEW **Tech stack:** Python / FastAPI / Pydantic / SQLAlchemy **Schema design (schemas.py):** The inheritance direction is correct -- `BoardListOut` is the base with shared fields, `BoardOut` extends it by adding `html_content`. This is clean Pydantic inheritance and means any future shared field additions only need to touch `BoardListOut`. The `model_config` on the base class is inherited by `BoardOut`, which is correct behavior. **Helper extraction (boards.py):** `_board_counts()` is cleanly extracted. The count loop logic is now in one place, called by both `_board_to_list_out()` and `_board_to_out()`. No behavioral change in the count logic itself. **Route change:** `GET /boards` switches from `response_model=list[BoardOut]` to `response_model=list[BoardListOut]` and uses the new `_board_to_list_out()` helper. `GET /boards/{slug}` and `POST /boards` are unchanged and still return full `BoardOut`. Correct. ### BLOCKERS None. ### NITS 1. **DRY in helper functions:** `_board_to_list_out()` and `_board_to_out()` duplicate the same field mapping (id, slug, name, project_slug, item_counts, created_at, updated_at). Since `BoardOut` inherits from `BoardListOut`, `_board_to_out` could construct a `BoardListOut` first and unpack it: ```python def _board_to_out(board_note: Note, db: Session) -> BoardOut: base = _board_to_list_out(board_note, db) return BoardOut(**base.model_dump(), html_content=board_note.html_content or None) ``` This keeps the field mapping in one place. Not blocking since the field list is short and localized. 2. **Test coverage for the new behavior:** No existing test validates that `html_content` is *absent* from `GET /boards` responses or *present* in `GET /boards/{slug}` responses. The existing `test_list_boards` (line 85) only checks status code and count. The `test_existing_fields_preserved` (line 1130) validates `POST /boards` (which still returns `BoardOut`), not the listing endpoint. Adding two assertions would lock down the contract: - `GET /boards` response items do NOT contain `html_content` - `GET /boards/{slug}` response DOES contain `html_content` Not a blocker since this is a subtractive change with 709 passing tests, but it would prevent accidental regressions. 3. **Blank line removal (boards.py):** The original `_board_to_out` had a blank line between the query and the `counts = BoardItemCounts()` line. The new `_board_counts` removes it. Trivial, but consistent with the rest of the file style (no blank lines between sequential statements in helpers). ### SOP COMPLIANCE - [x] PR body has ## Summary, ## Changes, ## Test Plan, ## Related - [x] Commit message is descriptive and references parent issue (#276) - [x] No secrets, .env files, or credentials committed - [x] No unnecessary file changes (scope is tight: 2 files, 25 additions, 8 deletions) - [x] `Closes #276` in PR body ### PROCESS OBSERVATIONS - **Change failure risk:** Low. Subtractive change to a listing endpoint. No migration, no model change, no new dependencies. - **Deployment frequency:** Clean, small PR. Good candidate for fast merge. - **Documentation:** PR body clearly explains the 57K -> 17K improvement and the MCP token limit motivation. ### VERDICT: APPROVED
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ldraney deleted branch fix/list-boards-exclude-html 2026-06-13 16:53:56 +00:00
Sign in to join this conversation.
No description provided.