/
/
/
1"""Volume control: set, up/down, mute, group volume."""
2# ruff: noqa: TID252 -- relative imports are the canonical MA-provider pattern.
3
4from __future__ import annotations
5
6from typing import TYPE_CHECKING
7
8from fastmcp import FastMCP
9from mcp.types import ToolAnnotations
10
11from ..tags import Tag
12from ._common import TIMEOUT_FAST
13
14if TYPE_CHECKING:
15 from music_assistant.mass import MusicAssistant
16
17
18def _vol_annotations(*, title: str, idempotent: bool) -> ToolAnnotations:
19 """Build default volume-tool annotations: never destructive, never open-world."""
20 return ToolAnnotations(
21 title=title,
22 readOnlyHint=False,
23 destructiveHint=False,
24 idempotentHint=idempotent,
25 openWorldHint=False,
26 )
27
28
29def build_volume_server(mass: MusicAssistant) -> FastMCP:
30 """Construct the ``volume/*`` sub-server."""
31 sub: FastMCP = FastMCP(name="volume")
32
33 @sub.tool(
34 tags={Tag.CONTROL_VOLUME},
35 annotations=_vol_annotations(title="Set volume", idempotent=True),
36 timeout=TIMEOUT_FAST,
37 ) # type: ignore[untyped-decorator, unused-ignore]
38 async def volume_set(player_id: str, level: int) -> None:
39 """
40 Set absolute volume on a single player.
41
42 Use ``volume_up`` / ``volume_down`` for relative steps and
43 ``group_volume_set`` for a sync group. Returns nothing.
44
45 :param player_id: Player identifier from ``PlayerBrief.player_id``.
46 :param level: Target volume ``0`` (silent) to ``100`` (max);
47 out-of-range values are clamped.
48 """
49 await mass.players.cmd_volume_set(player_id, max(0, min(100, int(level))))
50
51 @sub.tool(
52 tags={Tag.CONTROL_VOLUME},
53 annotations=_vol_annotations(title="Volume up", idempotent=False),
54 timeout=TIMEOUT_FAST,
55 ) # type: ignore[untyped-decorator, unused-ignore]
56 async def volume_up(player_id: str) -> None:
57 """
58 Increase volume by one device-defined step (typically ``5``).
59
60 Use ``volume_set`` instead when a precise target level is needed.
61 Returns nothing.
62
63 :param player_id: Player identifier from ``PlayerBrief.player_id``.
64 """
65 await mass.players.cmd_volume_up(player_id)
66
67 @sub.tool(
68 tags={Tag.CONTROL_VOLUME},
69 annotations=_vol_annotations(title="Volume down", idempotent=False),
70 timeout=TIMEOUT_FAST,
71 ) # type: ignore[untyped-decorator, unused-ignore]
72 async def volume_down(player_id: str) -> None:
73 """
74 Decrease volume by one device-defined step (typically ``5``).
75
76 Use ``volume_set`` instead when a precise target level is needed.
77 Returns nothing.
78
79 :param player_id: Player identifier from ``PlayerBrief.player_id``.
80 """
81 await mass.players.cmd_volume_down(player_id)
82
83 @sub.tool(
84 tags={Tag.CONTROL_VOLUME},
85 annotations=_vol_annotations(title="Mute / unmute", idempotent=True),
86 timeout=TIMEOUT_FAST,
87 ) # type: ignore[untyped-decorator, unused-ignore]
88 async def volume_mute(player_id: str, muted: bool) -> None:
89 """
90 Mute or unmute a player.
91
92 Mute is a latch â it does not change the underlying volume level, so
93 unmuting restores the original level. Use ``volume_set(level=0)``
94 instead if you want the volume itself to be ``0`` across a
95 mute/unmute cycle. Returns nothing.
96
97 :param player_id: Player identifier from ``PlayerBrief.player_id``.
98 :param muted: ``True`` to mute, ``False`` to unmute.
99 """
100 await mass.players.cmd_volume_mute(player_id, muted)
101
102 @sub.tool(
103 tags={Tag.CONTROL_VOLUME},
104 annotations=_vol_annotations(title="Set group volume", idempotent=True),
105 timeout=TIMEOUT_FAST,
106 ) # type: ignore[untyped-decorator, unused-ignore]
107 async def group_volume_set(player_id: str, level: int) -> None:
108 """
109 Set the overall volume of a sync group.
110
111 Use ``volume_set`` for an individual (non-group) player. Returns
112 nothing.
113
114 :param player_id: Player identifier of the sync-group leader (the
115 ``PlayerBrief`` entry whose group membership identifies it as
116 the leader of the group to control).
117 :param level: Target volume ``0`` (silent) to ``100`` (max);
118 out-of-range values are clamped.
119 """
120 await mass.players.cmd_group_volume(player_id, max(0, min(100, int(level))))
121
122 return sub
123