Files
emby-mcp/docs/api/endpoint_validation.md
gronod 42570915ed
Build and publish / Test and build (darwin) (push) Successful in 1m43s
Build and publish / Test and build (linux) (push) Successful in 2m30s
Build and publish / Test and build (windows) (push) Successful in 3m5s
Build and publish / Build and publish Docker image (push) Successful in 1m46s
Add browse, next-episode, and track-control tools
Implements the three roadmap feature sections:

- Browse: retrieve_item_children, retrieve_season_list, retrieve_episode_list;
  search_for_item gains item_types (e.g. "Series") so shows are findable
  without a selected library. Items now expose item_type, series_name, and
  per-user played state/resume position.
- retrieve_next_episode: next_unplayed (Emby NextUp, resume counts) or latest
  (most recent present episode; virtual placeholders skipped).
- retrieve_now_playing + set_subtitle/set_audio_track: stream listing and
  track switching via SetSubtitleStreamIndex/SetAudioStreamIndex GeneralCommands,
  with a PlayNow restart-at-position fallback for sessions that cannot switch
  mid-play (DLNA/Chromecast bridges).

Conformance manifest grows to 34 call sites with three new documented spec
gaps (StartPositionTicks on PlayRequest, missing /Shows/{Id}/Episodes response
schema, undeclared Fields/EnableUserData on /Users/{UserId}/Items/{Id}).
2026-09-21 00:09:49 +01:00

10 KiB
Raw Permalink Blame History

Emby endpoint conformance audit

This audit compares every upstream REST call made by internal/emby with the Emby Server REST API OpenAPI document, version 4.10.0.40, in emby_openapi.yaml. It is a static check of paths, methods, parameters, request bodies, decoded response fields, and enum values; it does not contact a live Emby server.

Run it with:

go test ./internal/emby/ -run TestSpecConformance -v

Inventory

The manifest contains 34 call sites. ✓ means the call is clean against the spec; ⚠ means it passes through a documented known issue.

# MCP tool(s) Call site Method and path Operation ID Status
1 startup auth library.go:19 POST /Users/AuthenticateByName postUsersAuthenticatebyname ✓
2 shutdown library.go:33 POST /Sessions/Logout postSessionsLogout ✓
3 retrieve_user_list users.go:20 GET /Users/{Id} getUsersById ✓
4 retrieve_user_list users.go:25 GET /Users/Public getUsersPublic ✓
5 retrieve_library_list, select_library library.go:39 GET /Library/MediaFolders getLibraryMediafolders ✓
6 retrieve_genre_list library.go:58 GET /Genres getGenres ✓
7 search_for_item, retrieve_next_episode (series resolve) items.go:135 GET /Users/{UserId}/Items getUsersByUseridItems ⚠
8 retrieve_item_children shows.go:28 GET /Users/{UserId}/Items getUsersByUseridItems ⚠
9 retrieve_season_list shows.go:48 GET /Shows/{Id}/Seasons getShowsByIdSeasons ⚠
10 retrieve_episode_list shows.go:75 GET /Shows/{Id}/Episodes getShowsByIdEpisodes ⚠
11 retrieve_next_episode (next_unplayed) shows.go:97 GET /Shows/NextUp getShowsNextup ⚠
12 retrieve_next_episode (latest) shows.go:120 GET /Shows/{Id}/Episodes getShowsByIdEpisodes ⚠
13 retrieve_playlist_list, create_playlist, modify_playlist_name playlists.go:76 GET /Users/{UserId}/Items getUsersByUseridItems ✓
14 retrieve_playlist_list playlists.go:99 GET /Users/ItemAccess getUsersItemaccess ⚠
15 retrieve_playlist_items playlists.go:120 GET /Playlists/{Id}/Items getPlaylistsByIdItems ⚠
16 create_playlist playlists.go:175 POST /Playlists postPlaylists ✓
17 modify_playlist_name, create_playlist (overview) playlists.go:211 GET /Users/{UserId}/Items/{Id} getUsersByUseridItemsById ✓
18 modify_playlist_name, create_playlist (overview) playlists.go:223 POST /Items/{ItemId} postItemsByItemid ✓
19 add_items_to_playlist, create_playlist playlists.go:230 POST /Playlists/{Id}/Items postPlaylistsByIdItems ✓
20 remove_items_from_playlist playlists.go:242 POST /Playlists/{Id}/Items/Delete postPlaylistsByIdItemsDelete ✓
21 reorder_items_on_playlist playlists.go:248 POST /Playlists/{Id}/Items/{ItemId}/Move/{NewIndex} postPlaylistsByIdItemsByItemidMoveByNewindex ✓
22 share_playlist_public playlists.go:257 POST /Items/{Id}/MakePublic postItemsByIdMakepublic ✓
23 stop_sharing_playlist playlists.go:259 POST /Items/{Id}/MakePrivate postItemsByIdMakeprivate ✓
24 share_playlist_user_access playlists.go:269 POST /Items/Access postItemsAccess ✓
25 retrieve_player_list sessions.go:41 GET /Sessions getSessions ✓
26 retrieve_player_queue sessions.go:146 GET /Sessions/PlayQueue getSessionsPlayqueue ✓
27 retrieve_now_playing, set_subtitle, set_audio_track sessions.go:183 GET /Sessions getSessions ✓
28 set_subtitle, set_audio_track sessions.go:202 POST /Sessions/{Id}/Command postSessionsByIdCommand ✓
29a control_media_player (PlayNow) sessions.go:260 POST /Sessions/{Id}/Playing postSessionsByIdPlaying ⚠
29b set_subtitle, set_audio_track (restart fallback) sessions.go:227 POST /Sessions/{Id}/Playing postSessionsByIdPlaying ⚠
30 control_media_player (other commands) sessions.go:281 POST /Sessions/{Id}/Playing/{Command} postSessionsByIdPlayingByCommand ✓
31 retrieve_now_playing, set_subtitle, set_audio_track items.go:165 GET /Users/{UserId}/Items/{Id} getUsersByUseridItemsById ⚠
32 http auth (token→user) sessions.go:121 GET /Sessions getSessions ✓
33 http auth (X-Emby-User-Id) users.go:42 GET /Users/{Id} getUsersById ✓
34 startup check (http) system.go:16 GET /System/Info/Public getSystemInfoPublic ✓

Findings

1. SPEC-GAP — GET /Users/ItemAccess does not declare ItemId

The Go client sends ItemId to restrict the access lookup to the current playlist, but the OpenAPI operation lists only IsHidden, IsDisabled, StartIndex, Limit, NameStartsWithOrGreater, and SortOrder. The upstream Python implementation contains a hotfix that adds item_id to this exact request, which is independent evidence that the server endpoint accepts it.

The test allowlists the missing parameter and fails if the spec later declares it without the registry being updated. Client behavior is unchanged.

2. CODE-DEVIATION — PlayNow sends PlayCommand in the JSON body

For POST /Sessions/{Id}/Playing, the client sends playRequest{PlayCommand, ControllingUserId}. The spec’s PlayRequest schema contains ControllingUserId, SubtitleStreamIndex, AudioStreamIndex, MediaSourceId, and StartIndex, but not PlayCommand. ItemIds and PlayCommand are correctly sent as required query parameters.

This is report-only. A later cleanup could remove PlayCommand from the body struct; no client behavior is changed by this audit.

3. SPEC-QUIRK — MediaStream.Extradata spelling

The spec spells the subtitle payload property Extradata; Emby JSON emits ExtraData. Go’s case-insensitive JSON field matching means the existing ExtraData field still decodes the server value. The spelling is therefore documented as a spec quirk rather than treated as a client defect.

4. SPEC-QUIRK — PlayNow ItemIds is typed as array<int64>

The /Sessions/{Id}/Playing operation describes ItemIds as an integer array, while its description says the values are comma-delimited and real Emby item IDs are strings. The client sends the expected comma-separated string query value. The test keeps a stale guard for this spec shape and does not alter the client encoding.

5. SPEC-GAP — PlayRequest omits StartPositionTicks

PlayNowWithOptions sends StartPositionTicks so set_subtitle / set_audio_track can restart playback at the current position when a session cannot switch streams mid-playback. The spec’s PlayRequest schema omits the field, but Emby has accepted it historically (it is present in real clients’ play requests). The field is allowlisted; the stale guard fails if the spec later declares it.

6. SPEC-GAP — /Shows/{Id}/Episodes has no response schema

The operation declares parameters but no JSON 200-response schema, so the episode-list rows cannot be field-checked. The sibling /Shows/{Id}/Seasons operation returns QueryResult_BaseItemDto, which is what the client decodes. The gap is allowlisted; respField checks are skipped for these rows.

7. SPEC-GAP — /Users/{UserId}/Items/{Id} omits Fields/EnableUserData

GetItem sends both parameters to fetch media streams and per-user watch state for retrieve_now_playing; the spec’s parameter list does not declare them. Real Emby accepts them (they are standard item-query params). The parameters are allowlisted.

Known-issues registry

The registry lives in internal/emby/spec_conformance_test.go. Each entry has a machine-checkable predicate. The test fails on an unexpected violation and also fails when a known issue’s predicate no longer holds, forcing the entry to be removed or revised after a spec/client update.

ID Kind Evidence Action
SPEC-GAP-ITEMACCESS-ITEMID spec-gap Python hotfix Emby.MCP/hotfixes/emby/user_service_api.py adds item_id to GET /Users/ItemAccess. Allowlist; keep client behavior.
CODE-DEVIATION-PLAYREQUEST-PLAYCOMMAND code-deviation internal/emby/sessions.go:168 includes PlayCommand; PlayRequest omits it. Report only; consider a later cleanup.
SPEC-QUIRK-MEDIASTREAM-EXTRADATA spec-quirk Spec has Extradata; server JSON/client field uses ExtraData. Document only.
SPEC-QUIRK-PLAYING-ITEMIDS-TYPE spec-quirk Spec says array<int64> but describes comma-delimited IDs; client sends strings. Document only; retain string query encoding.
SPEC-GAP-PLAYREQUEST-STARTPOSITIONTICKS spec-gap playRequest{StartPositionTicks} sent by PlayNowWithOptions; PlayRequest omits it. Real Emby accepts it for resume-position restarts. Allowlist; keep client behavior.
SPEC-GAP-EPISODES-RESPONSE-SCHEMA spec-gap GET /Shows/{Id}/Episodes has no JSON 200-response schema (sibling Seasons returns QueryResult_BaseItemDto). Allowlist; response fields unchecked on those rows.
SPEC-GAP-ITEMBYID-USERDATA-PARAMS spec-gap GET /Users/{UserId}/Items/{Id} omits Fields/EnableUserData params, which real Emby accepts. Allowlist; keep client behavior.

Scope and adjacent endpoints

The manifest records the parameter names the client may send, including optional parameters that are conditional at runtime. The test does not require the client to send every optional parameter in the spec. The authentication header is also out of the manifest because Client.do adds X-Emby-Authorization to every request centrally.

The spec contains adjacent endpoints that are not used by this client:

  • DELETE /Playlists/{Id}/Items exists alongside the client’s POST /Playlists/{Id}/Items/Delete removal call; both are present in the spec.
  • POST /Sessions/Playing is a playback check-in endpoint and is not used; the client uses the session-control endpoints under /Sessions/{Id}/Playing.

The YAML and JSON copies should remain synchronized. At audit time their SHA256 hashes were:

emby_openapi.yaml  7738d8eefadad7a2646781a7907fdd0373348fe19ae856d3f6f69dfc2987f27f
emby_openapi.json  55bc60c71b3d5df30d772f7ec4496e0e3f3fa371947691eeb62829a4ea91d530

They are different serializations/copies rather than byte-identical files, so changes to one should be reviewed against the other.