/
/
/
1"""
2Canned MCP prompts.
3
4These prompts hand the LLM a small, opinionated playbook for common tasks
5("find a song and play it on a specific speaker", "now playing summary",
6"build a party playlist") so an LLM client can chain MCP tools without
7re-deriving the workflow each time.
8"""
9
10from __future__ import annotations
11
12from typing import TYPE_CHECKING, Any
13
14from .constants import CONF_RES_PROMPTS
15
16if TYPE_CHECKING:
17 from music_assistant_models.config_entries import ProviderConfig
18
19
20def register_prompts(mcp: Any, config: ProviderConfig) -> None:
21 """Register canned prompts on the FastMCP root, gated by ``CONF_RES_PROMPTS``."""
22 if not config.get_value(CONF_RES_PROMPTS):
23 return
24
25 @mcp.prompt(name="find_and_play") # type: ignore[untyped-decorator, unused-ignore]
26 def find_and_play(query: str = "", target_player: str = "") -> str:
27 """Search and play media on a player."""
28 target = target_player or "<the user's preferred player>"
29 request = query or "<from the user message>"
30 return (
31 f"Find the best match for the user's request: '{request}'.\n"
32 "Use library_search_tracks (and library_search_albums or "
33 "library_search_artists if needed) to identify the right URI.\n"
34 "If every search returns no results, the item is not available in "
35 "the user's library or enabled providers â tell the user it could "
36 "not be found and stop. Do not retry the same searches or call "
37 "unrelated tools.\n"
38 f"If '{target}' is not already a player_id, resolve it by calling "
39 "players_list_players and fuzzy-matching the name.\n"
40 "Then call playback_play_media with queue_id set to that "
41 "player_id and the resolved URI.\n"
42 "Finally, call queue_get_active_queue to confirm the new state "
43 "and report it back. For positional inserts via queue_add_to_queue "
44 "with index, read QueueBrief.next_insertable_index from "
45 "queue_get_active_queue â not array position alone."
46 )
47
48 @mcp.prompt(name="curate_party_playlist") # type: ignore[untyped-decorator, unused-ignore]
49 def party_playlist(theme: str = "indie 2010s", length_minutes: int = 60) -> str:
50 """Build a party playlist."""
51 return (
52 f"Curate a playlist of roughly {length_minutes} minutes around "
53 f"the theme: '{theme}'.\n"
54 "Use library_search_tracks repeatedly with varied sub-queries "
55 "(genres, eras, similar artists) and metadata_recommendations "
56 "to seed candidates.\n"
57 "Pick tracks the user would dance to.\n"
58 "Then call playlists_create_playlist with a descriptive name, "
59 "and playlists_add_tracks to fill it.\n"
60 "Report the playlist URI when done."
61 )
62
63 @mcp.prompt(name="now_playing_summary") # type: ignore[untyped-decorator, unused-ignore]
64 def now_playing(player_id: str = "") -> str:
65 """Summarise what's currently playing on a player (or all players)."""
66 if player_id:
67 return (
68 f"Use queue_get_active_queue with player_id='{player_id}' "
69 "to fetch the current queue.\n"
70 "Summarise the now-playing track (title, artist, album, "
71 "time remaining) and the next two upcoming items in 3-4 "
72 "sentences."
73 )
74 return (
75 "List players via players_list_players (pass "
76 "include_unavailable=True for offline devices, "
77 "include_disabled=True for admin-disabled devices). "
78 "For each player whose state is 'playing', fetch its active "
79 "queue and summarise the now-playing track. Group by room "
80 "when possible. A player whose state is 'synced' is playing "
81 "as part of another group â its active queue belongs to the "
82 "group's player_id, not its own."
83 )
84