How API Commands Are Registered and Handled in the Music Assistant Server

API commands in the Music Assistant server are registered using the @api_command decorator, scanned at startup into a command handler registry, and dispatched via WebSocket with automatic type conversion and role-based authentication.

The Music Assistant server implements a declarative registration mechanism that bridges internal Python functions to external WebSocket API endpoints. This system allows developers to expose functionality using minimal boilerplate while the runtime handles routing, parameter validation, and security enforcement. Understanding how commands are registered and handled is essential for extending the server or debugging API interactions.

Declaring API Commands with the @api_command Decorator

Commands are declared using the @api_command decorator defined in music_assistant/helpers/api.py (lines 64-71). This decorator attaches metadata to function objects without altering their behavior:

@api_command("info")
def get_server_info(self) -> ServerInfoMessage:
    """Return Info of this server."""
    ...

The decorator stores three key attributes on the function object:

  • api_cmd – The command string identifier (e.g., "info", "providers/manifests")
  • api_authenticated – Boolean indicating if the command requires authentication (defaults to True)
  • api_required_role – Optional role string (e.g., "admin") for authorization checks

Scanning and Registering Commands at Runtime

When the server starts, MusicAssistant.start() invokes _register_api_commands() in music_assistant/mass.py (lines 80-13) to discover and register all decorated methods:

for cls in (self, self.config, self.metadata, ...):
    for attr_name in dir(cls):
        obj = getattr(cls, attr_name)
        if hasattr(obj, "api_cmd"):
            authenticated = getattr(obj, "api_authenticated", True)
            required_role = getattr(obj, "api_required_role", None)
            self.register_api_command(obj.api_cmd, obj, authenticated, required_role)

The scanner iterates through a hardcoded tuple of core objects—including the main MusicAssistant instance, config controller, and metadata controller—checking each attribute for the api_cmd marker. Valid commands are passed to register_api_command() (lines 84-09), which instantiates an APICommandHandler and stores it in self.command_handlers, a dictionary mapping command strings to their handlers.

The APICommandHandler Model

The APICommandHandler class in music_assistant/helpers/api.py (lines 70-62) encapsulates the metadata and execution logic for each command. Its static method parse() performs reflection to prepare the handler:

  • Extracts the function signature using inspect.signature
  • Resolves type hints (including forward references) via _get_type_hints_for_api_command
  • Normalizes generic TypeVar instances (e.g., ItemCls to concrete media-item classes)
  • Stores authentication flags and command aliases

This metadata enables the server to validate and convert incoming JSON arguments to the correct Python types at runtime.

Dispatching and Executing Commands

Incoming WebSocket messages are handled by WebsocketClientHandler._handle_command() in music_assistant/controllers/webserver/websocket_client.py (lines 92-41). The dispatch flow follows these steps:

  1. Command lookup – The handler retrieves the command string from the message and looks it up in self.mass.command_handlers. If not found, an InvalidCommand error is returned.

  2. Authentication checks – If the handler requires authentication (handler.authenticated) or a specific role (handler.required_role), the method validates the client's stored user (self._authenticated_user). Failures raise AuthenticationRequired or InsufficientPermissions.

  3. Task creation – Valid commands are scheduled as background tasks:

    self.mass.create_task(self._run_handler(handler, msg))

The _run_handler() method (lines 45-62) executes the actual logic:

  • Parses arguments using parse_arguments() from helpers/api.py, converting JSON values to enums, dates, or model objects
  • Invokes the target function
  • Handles three return types: synchronous values, coroutines (awaited), and async generators (streamed in chunks)
  • Sends the result wrapped in a SuccessResultMessage

Error Handling and Security

Error handling occurs within _run_handler() (lines 63-86). The implementation distinguishes between known MusicAssistantError subclasses and unexpected exceptions:

  • Known errors – Caught, logged at warning level, and returned with their specific error code and translation key
  • Unexpected errors – Logged at debug level (if enabled) and returned as a generic 999 error to prevent information leakage

This architecture ensures that authentication and authorization checks occur before execution, while type safety is enforced through the signature inspection performed during handler construction.

Practical Implementation Examples

Declaring a Protected Admin Command

from music_assistant.helpers.api import api_command

class PlayerController:
    @api_command("players/rename", required_role="admin")
    async def rename_player(self, player_id: str, name: str) -> bool:
        """Rename a player (admin only)."""
        player = self.mass.get_player(player_id)
        await player.rename(name)
        return True

This example registers the method under the "players/rename" path and restricts access to users with the admin role.

Manual Runtime Registration

For dynamic scenarios, you can bypass the decorator and register commands directly:

def my_dynamic_handler(user_id: str) -> dict:
    return {"user_id": user_id, "info": "dynamic"}

# Register at runtime

mass.register_api_command(
    command="dynamic/userinfo",
    handler=my_dynamic_handler,
    authenticated=True,
    required_role=None,
)

This uses MusicAssistant.register_api_command() directly, allowing plugins to extend the API without modifying core controllers.

Summary

  • Use @api_command in music_assistant/helpers/api.py to declare API endpoints with metadata for authentication and roles
  • Registration occurs at startup via MusicAssistant._register_api_commands() in mass.py, which scans core objects for decorated methods
  • Handlers are stored in the self.command_handlers dictionary, mapping command strings to APICommandHandler instances
  • WebSocket dispatch in websocket_client.py handles lookup, authentication, and execution as asynchronous tasks
  • Automatic type conversion converts incoming JSON to Python types using the function signature captured during registration
  • Error handling distinguishes between application errors (with codes) and unexpected exceptions (generic responses)

Frequently Asked Questions

How do I create a new API command in Music Assistant?

Define a method in a controller class and decorate it with @api_command("command/name") from music_assistant/helpers/api.py. The server automatically discovers and registers the method during startup. Optionally specify required_role="admin" if the command should be restricted to administrators.

What authentication options are available for API commands?

The @api_command decorator accepts two security parameters: authenticated (boolean, defaults to True) and required_role (string, defaults to None). When authenticated=True, the WebSocket handler verifies the client has completed authentication. If required_role is set, the handler checks the user's role against the requirement before executing the command.

How does the server handle API command errors?

The _run_handler() method in websocket_client.py catches MusicAssistantError subclasses and returns them with specific error codes and translation keys. Unexpected exceptions are caught and returned as generic 999 errors to avoid exposing internal details, with full tracebacks available in debug logs when enabled.

Can I register API commands dynamically at runtime?

Yes, though this is rare. Call mass.register_api_command() directly with the command string, handler function, and authentication flags. This bypasses the decorator scan and is useful for plugins that need to register commands after the initial startup sequence has completed.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →