Architecture
mcdbus is built on FastMCP 3.0 with dbus-fast for async D-Bus communication. This page explains how the pieces fit together.
Entry point
Section titled “Entry point”The entry point is server.py. It imports every module that registers tools, resources, and prompts with the FastMCP instance, then calls mcp.run():
import mcdbus._discoveryimport mcdbus._interactionimport mcdbus._promptsimport mcdbus._resourcesimport mcdbus._shortcutsfrom mcdbus._state import mcp
def main(): mcp.run()The pyproject.toml declares a script entry point (mcdbus = "mcdbus.server:main"), so running mcdbus on the command line calls this function directly.
Module organization
Section titled “Module organization”Each module has a single responsibility:
| Module | Role | Registers |
|---|---|---|
_state.py | FastMCP instance and lifespan context manager | The mcp server object |
_bus.py | BusManager, D-Bus connection handling, type serialization | Nothing (utility) |
_discovery.py | Service and object discovery | 3 tools: list_services, introspect, list_objects |
_interaction.py | Method calls and property access | 4 tools: call_method, get_property, set_property, get_all_properties |
_shortcuts.py | High-level convenience tools | 7 tools: send_notification, list_systemd_units, media_player_control, network_status, battery_status, bluetooth_devices, kwin_windows |
_resources.py | Dynamic MCP resources for browsing | 3 resources: dbus://{bus}/services, dbus://{bus}/{service}/objects, dbus://{bus}/{service}/{path}/interfaces |
_prompts.py | Guided exploration templates | 2 prompts: explore_service, debug_service |
_notify_confirm.py | Desktop notification fallback for confirmation | Nothing (called by _interaction.py) |
Every module that registers tools imports the mcp instance from _state.py and uses the @mcp.tool() decorator. The imports in server.py ensure all decorators run before mcp.run() is called.
BusManager
Section titled “BusManager”The BusManager class in _bus.py is the central connection manager. It maintains at most two D-Bus connections: one for the session bus and one for the system bus. Connections are lazy — they are established on first use, not at startup.
class BusManager: def __init__(self): self._buses: dict[str, MessageBus] = {} self._locks: dict[str, asyncio.Lock] = { "session": asyncio.Lock(), "system": asyncio.Lock(), }
async def get_bus(self, bus_type: str) -> MessageBus: ...Key properties:
Lazy connection. The first call to get_bus("session") connects to the session bus. Subsequent calls return the cached connection. The system bus is connected independently on first use.
Reconnection. If a cached connection’s connected property is False (the bus daemon dropped the connection, or the process was idle too long), BusManager drops the stale connection and establishes a new one.
Asyncio locks. Each bus type has its own lock. Multiple concurrent tool calls that need the same bus will wait for the connection to be established by the first caller, then share it.
Cleanup. On server shutdown, the lifespan context calls disconnect_all(), which iterates over all cached connections and disconnects them.
Lifespan context
Section titled “Lifespan context”FastMCP supports a lifespan context manager that runs setup/teardown code around the server’s lifetime. mcdbus uses this to create the BusManager and make it available to all tools:
@asynccontextmanagerasync def lifespan(server: FastMCP): mgr = BusManager() try: yield mgr finally: await mgr.disconnect_all()The yielded mgr object becomes ctx.lifespan_context inside every tool function. The helper get_mgr(ctx) extracts it:
def get_mgr(ctx: Context) -> BusManager: return ctx.lifespan_contextThis design means tools never create their own connections. They all share the same BusManager, which maintains at most two connections total.
Introspection-first design
Section titled “Introspection-first design”The discovery tools (list_services, list_objects, introspect) contain no hardcoded service knowledge. They work entirely through D-Bus introspection — the same mechanism that tools like d-feet and busctl use. When you call list_services, mcdbus asks the bus daemon for its list of registered names. When you call introspect, mcdbus asks the target service to describe its own interfaces.
The shortcut tools (battery_status, network_status, etc.) do encode knowledge about specific services, but only at the level of which service name, object path, and interface to query. The data they return is still read from the live bus, not from static definitions.
Type serialization
Section titled “Type serialization”D-Bus has its own type system (integers, strings, booleans, arrays, dicts, variants, structs) that does not map directly to JSON. Two functions in _bus.py handle the conversion:
serialize_variant converts D-Bus response values into JSON-safe Python types. It uses dbus-fast’s unpack_variants to strip Variant wrappers, then recursively converts bytes to strings (or integer lists if not valid UTF-8), tuples to lists, and dict keys to strings.
deserialize_args goes the other direction. It parses a JSON string into a list of Python values, then matches each value against the D-Bus type signature. For Variant arguments (signature v), it either auto-infers the type from the Python value (string, int, float, bool, list) or accepts an explicit {"signature": "...", "value": ...} dict for complex cases.
D-Bus method calls
Section titled “D-Bus method calls”All D-Bus communication goes through call_bus_method in _bus.py. This function constructs a dbus_fast.Message, sends it through the bus connection with asyncio.wait_for for timeout handling, checks the reply type for errors, and passes the response body through serialize_variant.
The timeout defaults to 30 seconds and can be overridden with the MCDBUS_TIMEOUT environment variable. If the timeout expires, a TimeoutError is raised with a descriptive message including the service, interface, member, and path.
Input validation
Section titled “Input validation”Every tool validates its D-Bus inputs before sending them over the wire:
- Bus names and interface names are checked against the D-Bus specification regex: must start with a letter or underscore, contain at least two dot-separated components, and use only alphanumeric characters and underscores.
- Object paths must be
/or a sequence of/-separated alphanumeric-and-underscore segments. - Signatures are checked for valid characters, maximum length (255), and structural validity by passing them through dbus-fast’s
SignatureTreeparser.
Invalid inputs raise ValueError before any D-Bus message is sent.