Files
gronod 2d5763d6c6 feat: slim tool responses, drop lyrics search, rename module (1.0.11)
Item-shaped results now omit empty fields and drop low-value metadata
(overview, lyrics, file_path, creation_date, premiere_date, bitrate);
absent numeric fields drop the key instead of emitting "". Player
sessions report hh:mm:ss times only; playlists drop overview/date_created.
The lyrics_or_description search parameter is removed, along with the
unused DTO fields, and upstream Fields requests are slimmed to match.
Module path renamed to git.i3omb.com/gronod/emby-mcp.
2026-09-21 23:54:07 +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:125 GET /Users/{UserId}/Items getUsersByUseridItems ✓
8 retrieve_item_children shows.go:25 GET /Users/{UserId}/Items getUsersByUseridItems ✓
9 retrieve_season_list shows.go:45 GET /Shows/{Id}/Seasons getShowsByIdSeasons ✓
10 retrieve_episode_list shows.go:72 GET /Shows/{Id}/Episodes getShowsByIdEpisodes ⚠
11 retrieve_next_episode (next_unplayed) shows.go:94 GET /Shows/NextUp getShowsNextup ✓
12 retrieve_next_episode (latest) shows.go:117 GET /Shows/{Id}/Episodes getShowsByIdEpisodes ⚠
13 retrieve_playlist_list, create_playlist, modify_playlist_name playlists.go:69 GET /Users/{UserId}/Items getUsersByUseridItems ✓
14 retrieve_playlist_list playlists.go:88 GET /Users/ItemAccess getUsersItemaccess ⚠
15 retrieve_playlist_items playlists.go:109 GET /Playlists/{Id}/Items getPlaylistsByIdItems ✓
16 create_playlist playlists.go:159 POST /Playlists postPlaylists ✓
17 modify_playlist_name, create_playlist (overview) playlists.go:195 GET /Users/{UserId}/Items/{Id} getUsersByUseridItemsById ✓
18 modify_playlist_name, create_playlist (overview) playlists.go:207 POST /Items/{ItemId} postItemsByItemid ✓
19 add_items_to_playlist, create_playlist playlists.go:214 POST /Playlists/{Id}/Items postPlaylistsByIdItems ✓
20 remove_items_from_playlist playlists.go:226 POST /Playlists/{Id}/Items/Delete postPlaylistsByIdItemsDelete ✓
21 reorder_items_on_playlist playlists.go:232 POST /Playlists/{Id}/Items/{ItemId}/Move/{NewIndex} postPlaylistsByIdItemsByItemidMoveByNewindex ✓
22 share_playlist_public playlists.go:241 POST /Items/{Id}/MakePublic postItemsByIdMakepublic ✓
23 stop_sharing_playlist playlists.go:243 POST /Items/{Id}/MakePrivate postItemsByIdMakeprivate ✓
24 share_playlist_user_access playlists.go:253 POST /Items/Access postItemsAccess ✓
25 retrieve_player_list sessions.go:39 GET /Sessions getSessions ✓
26 retrieve_player_queue sessions.go:136 GET /Sessions/PlayQueue getSessionsPlayqueue ⚠
27 retrieve_now_playing, set_subtitle, set_audio_track sessions.go:169 GET /Sessions getSessions ✓
28 set_subtitle, set_audio_track sessions.go:188 POST /Sessions/{Id}/Command postSessionsByIdCommand ✓
29a control_media_player (PlayNow) sessions.go:246 POST /Sessions/{Id}/Playing postSessionsByIdPlaying ⚠
29b set_subtitle, set_audio_track (restart fallback) sessions.go:213 POST /Sessions/{Id}/Playing postSessionsByIdPlaying ⚠
30 control_media_player (other commands) sessions.go:267 POST /Sessions/{Id}/Playing/{Command} postSessionsByIdPlayingByCommand ✓
31 retrieve_now_playing, set_subtitle, set_audio_track items.go:144 GET /Users/{UserId}/Items/{Id} getUsersByUseridItemsById ⚠
32 http auth (token→user) sessions.go:111 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 — 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.

4. 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.

5. 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.

6. 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.

7. SPEC-GAP — /Sessions/PlayQueue omits Fields

GetPlayQueueItems sends Fields to slim the upstream payload; the spec’s parameter list declares only Id and DeviceId. Real Emby accepts standard item-query params here (and ignores them harmlessly if not). The parameter is 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 playRequest in internal/emby/sessions.go includes PlayCommand; PlayRequest omits it. Report only; consider a later cleanup.
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.
SPEC-GAP-PLAYQUEUE-FIELDS spec-gap GET /Sessions/PlayQueue omits the Fields param the client sends to slim the response. 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.