Backlog: MCP server for ErsatzTV/ChicoryTV API #58
Closed
opened 2026-07-01 20:21:20 +02:00 by timothy
·
7 comments
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
fix/830-add-items-error-surface
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#58
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Backlog
Now that ErsatzTV has a real API surface, investigate and build an MCP server so agents/tools can safely inspect and operate the system without scraping UI or touching SQLite directly.
Rough scope
Depends on
Notes
This is separate from server-management operational automation. It belongs in the app ecosystem because it wraps the ErsatzTV/ChicoryTV API.
Scope clarification
The MCP can start now, but it should be scoped as a thin foundation over the current stable REST/OpenAPI surface, not as the final redesign-aware automation layer.
Recommended increment split:
ErsatzTV/wwwroot/openapi/v1.json; do not scrape the UI or read/write SQLite directly.X-Api-Keywrite-auth story when configured. Prefer explicit, narrow tools over generic arbitrary HTTP calls.create channel from lineupworkflow before #63 exists; that should wrap the composite backend endpoint once available.Known future API gaps from the ChicoryTV design brief:
Worktree guidance: this should be developed in a separate git worktree so #59 design work can continue on
docs/59-ui-redesign-briefwithout branch switching.Done
What was done: Built the v0 read-only MCP foundation over the current stable REST/OpenAPI surface in PR #76. Added
ErsatzTV.Mcpas a stdio JSON-RPC MCP server with explicit tools for channels, collections, smart collections, schedules/items, playout lookup, FFmpeg profiles/resolution, sessions, and version. Added API client handling for configuredX-Api-Keyand focused MCP/API-client/tool-catalog tests.Root cause: n/a.
Files changed:
ErsatzTV.Mcp/*,ErsatzTV.Mcp.Tests/*,ErsatzTV.sln,docs/mcp.md,docs/superpowers/plans/2026-07-02-ersatztv-mcp-v0-plan.md.Verification:
TZ=UTC dotnet restore ErsatzTV.sln -v minimal;TZ=UTC dotnet build ErsatzTV.sln --no-restore -v minimal;TZ=UTC dotnet test ErsatzTV.Mcp.Tests/ErsatzTV.Mcp.Tests.csproj -v minimal(12 passed);TZ=UTC dotnet test ErsatzTV.sln --no-build -v minimal(exit 0, includes MCP tests). Existing NuGet audit warnings remain forScriban.SignedandMicrosoft.OpenApi.Deferred: v0.1 write tools are not included. Redesign-aware workflow tools remain deferred until #63-#68 backend contracts exist.
Follow-up issues: Existing future backend gaps #63-#68. A separate v0.1 write-tools issue can be opened if desired.
Docs updated:
docs/mcp.mddocuments configuration, current v0 tool list, auth header behavior, and deferred workflow scope.Leaving #58 open because this PR completes the requested v0 foundation but not the optional v0.1 write-tool increment or later redesign workflow layer.
HARD GATE: do not close this issue or point the MCP server at prod before the cold adversarial review in #197 (REST API contract + security posture, run from the
adversarial-reviewerproject) has been executed and its MUST-FIX findings resolved. The MCP server is an AI-facing consumer of an API whose auth posture has known deferred soft spots — #197 has the full scope. The review request gets filed in the adversarial-reviewer repo only when it can actually be run (post PR #76 stabilization), not before.Re-scope (2026-07-12) — fresh read+write build against frozen
/api/v1, superseding PR #76Owner decision: PR #76 (v0 read-only foundation) is closed as superseded rather than merged/rebased. The MCP is rebuilt fresh here once the API contract freezes.
Sequencing (the
## Depends on: API surface being sufficiently complete and stablein this body, made concrete):/api/v1versioning) freezes + versions the route surface first — it rewrites every route, so building the MCP before it would mean regenerating the tool contracts immediately after./api/v1: read tools + cautious-write CRUD tools (channels / collections / smart collections / schedules + items / playouts / media-sources — all now have settled write contracts with ETag/If-Match optimistic concurrency).Carry forward from PR #76 / #289 (do not re-derive): runtime read-only posture toggle (
ERSATZTV_ALLOW_WRITES, default false) as the write backstop, JSON-RPC DoS guards (-32700/-32600/-32603, bounded stdin reader), per-request CTS timeout covering headers and streamed body (ResponseHeadersReadgotcha), response-size cap, arg-validation vsInputSchema, reverse-proxy prefix preservation, untrusted-data/injection framing in docs. These are the security baseline.Unchanged HARD GATE: MCP does not go live / point at prod until #197's security-review MUST-FIX findings are resolved (this build gets folded into that pass).
Un-claiming (
in-progressoff) — the build is deferred to after #286; this session is doing #286.Claiming (Claude Code orchestrator session, 2026-07-20).
All three gates from the 2026-07-12 re-scope are now clear:
/api/v1versioned + frozen) — closed 07-13, so the route surface won't shift under the tool contracts.Plan per the re-scope: build fresh (PR #76 stays superseded/closed) as
ErsatzTV.Mcp, a stdio JSON-RPC server over the frozen/api/v1surface — read tools + cautious-write CRUD (channels / collections / smart collections / schedules+items / playouts / media-sources, all now with settled ETag/If-Match write contracts). Carrying forward the PR #76/#289 security baseline verbatim (do not re-derive):ERSATZTV_ALLOW_WRITESdefault-false posture, JSON-RPC DoS guards (-32700/-32600/-32603, bounded stdin reader), per-request CTS covering headers and streamed body, response-size cap, arg-validation vsInputSchema, reverse-proxy prefix preservation, untrusted-data/injection framing in the docs. Composite/workflow tools (#63–#68) stay deferred.Then #487 (populate the empty music collections through the MCP write path) is the live acceptance case. Working in worktree
feat/58-mcp-v1offorigin/main.Live acceptance case passed (#487, Vaporwave) — plus one write-surface finding
Drove the v0.1 write path end-to-end against prod (v26.11.0) to populate the empty Vaporwave collection:
search_all_items tag:"Vaporwave"→ 204musicVideoIds→add_collection_items(collection 20) →get_collection_itemsshows 0 → 204, a re-run stays 204 (idempotent, no dup rows), and playout 28 rebuiltsuccess=truewith 47 items. Full write-through-MCP, no SQL/SPA. Details on #487. The read tools, machine-key auth, ETag surfacing ([etag: "0"]on the empty collection), and theERSATZTV_ALLOW_WRITESgate all behaved correctly against real prod data.Finding — paged list tools don't expose pagination (follow-up)
ersatztv_list_playoutsmaps toGET /api/v1/playouts, which is paged ({totalCount, page[]}, default page ~10), but the tool declares no query args — sopageSize/pageNumare rejected (additionalProperties:false) and you cannot reach page 2+. I worked around it by addressing the playout by id. Any paged list endpoint the MCP wraps (list_playouts, and checklist_channels/list_schedules/list_sessions) should declarepageNum/pageSizeas query params — same as the search tools already do. Small, additive follow-up; noting it here rather than filing a separate issue since #58 stays open for exactly this kind of v0.1 increment.MCP write-path findings from the #487 acceptance case (second, larger run)
The Vaporwave pilot exercised one collection / 204 items. This run drove 11 collections / 1,184 item-adds through the MCP, which surfaced things the small run could not. Reporting them here per #487's "feed findings back to #58" box.
What worked well
ersatztv_add_collection_itemsis genuinely idempotent and existence-checked. Re-applying all 11 collections changed nothing (15→15, 246→246, 394→394, 274→274, 27→27, 204→204, 124→124, 29→29, 49→49, 1→1, 17→17), no duplicateCollectionItemrows.ersatztv_scan_library+ersatztv_search_all_items+ersatztv_get_collection_itemscovered the whole read/decide/write/verify loop with no fallback to SQL or the SPA.Bugs / gaps found
1.
pageNumis 0-based, but the tool description says "1-based".ToolCatalog.cs:235declaresInt("pageNum", "1-based page number (optional)."). It is actually 0-based. This is the dangerous kind of wrong: a loop starting atpageNum=1silently skips the first 100 items and returns a short set with no error. It cost me a verification pass that looked exactly like data loss — collections of ≤100 items came back as0 items, and a 204-item collection came back as 104. Either fix the description or make the param 1-based.2.
pageSizeis capped at 100 for the returned page, but the offset still honors the requested value.So an over-large
pageSizedoesn't clamp coherently — it silently changes the paging arithmetic. Combined with (1), paging is treacherous to drive from a tool. Suggest clampingpageSizeand deriving the offset from the clamped value.3. No way to express an exact tag match.
tag:is an analyzed Lucene field, so a phrase query on a single-word tag matches any tag containing that token:tag:"Glastonbury"→ 246 hits (its own 59 plus all 187Glastonbury Concerts)tag:"Electronic"→ 27 hits (its own 26 plusX-mix 5 - Mr. C - The Electronic Storm)For folder-tag-driven collections this means you cannot say "items whose tag is exactly X". Harmless here (both cases were thematically fine and one was the intended union), but it makes tag-driven automation imprecise. A non-analyzed
tag.raw-style field, or anexactTag:operator, would close it.4.
ersatztv_reset_channel_playouttakes a channel id, butersatztv_list_playoutsrows carry only the playout id (id,channelNumber,channelName— nochannelId). I conflated the two and reset the wrong entity on my first attempt (harmless — a rebuild — but it hit an unrelated channel). AddingchannelIdto the playout list row, or naming the argchannelIdin the schema, would remove the trap.5. Paged list tools still can't reach page 2+ (
list_playouts,list_collections, …) — they expose nopageNum/pageSize. Already noted from the Vaporwave run; re-confirmed. I had to drop to raw/api/v1with an inline key to enumerate 43 playouts.Suggested follow-ups
(1), (2) and (4) are small and worth fixing — (1) especially, because it fails silently and plausibly. (3) and (5) are additive API surface. Happy to file them individually if you'd rather track them separately than on this closed issue.