Add Starlight documentation site

Scaffolds docs-site/ with the warehack.ing cookie-cutter
pattern (Caddy + Node multi-stage Dockerfile, caddy-docker-proxy
labels, HMR via WSS).

21 content pages following Diataxis structure:
- Start Here: overview, installation, first-steps tutorial
- How-To Guides: media players, systemd, notifications,
  battery/network, bluetooth, service exploration, permissions
- Reference: discovery tools, interaction tools, shortcut tools,
  resources, prompts, environment variables
- Explanation: architecture, session vs system bus,
  confirmation flow, security layers

Terminal-green + slate theme. Builds to 22 static HTML pages.
This commit is contained in:
Ryan Malloy 2026-03-06 17:34:33 -07:00
parent f09da7259f
commit 5cc14f3dcf
33 changed files with 3380 additions and 0 deletions

6
docs-site/.dockerignore Normal file
View File

@ -0,0 +1,6 @@
node_modules
dist
.astro
.env
*.log
.git

4
docs-site/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
dist/
.astro/
.env

49
docs-site/Dockerfile Normal file
View File

@ -0,0 +1,49 @@
FROM node:22-slim AS base
ENV ASTRO_TELEMETRY_DISABLED=1
WORKDIR /app
# Install dependencies
FROM base AS deps
COPY package.json package-lock.json* ./
RUN npm ci
# Build static site
FROM deps AS build
COPY . .
RUN npm run build
# Production: serve from Caddy
FROM caddy:2-alpine AS prod
COPY --from=build /app/dist /srv
COPY <<'CADDYFILE' /etc/caddy/Caddyfile
:80 {
root * /srv
file_server
encode gzip
@hashed path /_astro/*
header @hashed Cache-Control "public, max-age=31536000, immutable"
@unhashed not path /_astro/*
header @unhashed Cache-Control "public, max-age=3600"
handle_errors {
@404 expression {err.status_code} == 404
handle @404 {
rewrite * /404.html
file_server
}
}
try_files {path} {path}/ /index.html
}
CADDYFILE
EXPOSE 80
# Development: Node with HMR
FROM deps AS dev
ENV ASTRO_TELEMETRY_DISABLED=1
COPY . .
EXPOSE 4321
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

11
docs-site/Makefile Normal file
View File

@ -0,0 +1,11 @@
prod:
docker compose up -d --build
dev:
docker compose --profile dev up --build
down:
docker compose down
logs:
docker compose logs -f

101
docs-site/astro.config.mjs Normal file
View File

@ -0,0 +1,101 @@
import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
import tailwindcss from "@tailwindcss/vite";
import sitemap from "@astrojs/sitemap";
export default defineConfig({
site: "https://mcdbus.warehack.ing",
telemetry: false,
devToolbar: { enabled: false },
integrations: [
starlight({
title: "mcdbus",
description:
"D-Bus MCP server — give Claude access to your Linux desktop bus",
favicon: "/favicon.svg",
social: [
{
icon: "seti:git",
label: "Source",
href: "https://git.supported.systems/warehack.ing/mcdbus",
},
],
customCss: ["./src/styles/global.css"],
sidebar: [
{
label: "Start Here",
items: [
{ label: "What is mcdbus?", slug: "start-here/overview" },
{ label: "Installation", slug: "start-here/installation" },
{ label: "First Steps", slug: "start-here/first-steps" },
],
},
{
label: "How-To Guides",
items: [
{ label: "Control Media Players", slug: "guides/media-players" },
{ label: "Manage Systemd Units", slug: "guides/systemd" },
{ label: "Send Notifications", slug: "guides/notifications" },
{
label: "Check Battery & Network",
slug: "guides/system-status",
},
{ label: "Browse Bluetooth Devices", slug: "guides/bluetooth" },
{
label: "Explore Unknown Services",
slug: "guides/explore-services",
},
{ label: "Lock Down Permissions", slug: "guides/permissions" },
],
},
{
label: "Reference",
items: [
{ label: "Discovery Tools", slug: "reference/discovery-tools" },
{
label: "Interaction Tools",
slug: "reference/interaction-tools",
},
{ label: "Shortcut Tools", slug: "reference/shortcut-tools" },
{ label: "Resources", slug: "reference/resources" },
{ label: "Prompts", slug: "reference/prompts" },
{
label: "Environment Variables",
slug: "reference/environment-variables",
},
],
},
{
label: "Explanation",
collapsed: true,
items: [
{ label: "Architecture", slug: "explanation/architecture" },
{
label: "Session vs System Bus",
slug: "explanation/buses",
},
{
label: "Confirmation Flow",
slug: "explanation/confirmation-flow",
},
{ label: "Security Layers", slug: "explanation/security-layers" },
],
},
],
}),
sitemap(),
],
vite: {
plugins: [tailwindcss()],
server: {
host: "0.0.0.0",
...(process.env.VITE_HMR_HOST && {
hmr: {
host: process.env.VITE_HMR_HOST,
protocol: "wss",
clientPort: 443,
},
}),
},
},
});

View File

@ -0,0 +1,41 @@
services:
docs:
build:
context: .
target: prod
restart: unless-stopped
networks:
- caddy
labels:
caddy: ${DOMAIN}
caddy.reverse_proxy: "{{upstreams 80}}"
docs-dev:
build:
context: .
target: dev
profiles:
- dev
volumes:
- ./src:/app/src
- ./public:/app/public
- ./astro.config.mjs:/app/astro.config.mjs
networks:
- caddy
environment:
- VITE_HMR_HOST=${DOMAIN}
labels:
caddy: ${DOMAIN}
caddy.reverse_proxy: "{{upstreams 4321}}"
caddy.reverse_proxy.flush_interval: "-1"
caddy.reverse_proxy.transport: "http"
caddy.reverse_proxy.transport.read_timeout: "0"
caddy.reverse_proxy.transport.write_timeout: "0"
caddy.reverse_proxy.transport.keepalive: "5m"
caddy.reverse_proxy.transport.keepalive_idle_conns: "10"
caddy.reverse_proxy.stream_timeout: "24h"
caddy.reverse_proxy.stream_close_delay: "5s"
networks:
caddy:
external: true

19
docs-site/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "mcdbus-docs",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/sitemap": "^3.3.1",
"@astrojs/starlight": "^0.37.6",
"@tailwindcss/vite": "^4.1.3",
"astro": "^5.7.10",
"sharp": "^0.33.0",
"tailwindcss": "^4.1.3"
}
}

View File

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- D-Bus bridge icon: a bus/channel with connection nodes -->
<rect x="4" y="13" width="24" height="6" rx="2" fill="#1e293b"/>
<line x1="16" y1="4" x2="16" y2="13" stroke="#4ead6b" stroke-width="2.5" stroke-linecap="round"/>
<line x1="9" y1="7" x2="9" y2="13" stroke="#4ead6b" stroke-width="2.5" stroke-linecap="round"/>
<line x1="23" y1="7" x2="23" y2="13" stroke="#4ead6b" stroke-width="2.5" stroke-linecap="round"/>
<line x1="16" y1="19" x2="16" y2="28" stroke="#4ead6b" stroke-width="2.5" stroke-linecap="round"/>
<line x1="9" y1="19" x2="9" y2="25" stroke="#4ead6b" stroke-width="2.5" stroke-linecap="round"/>
<line x1="23" y1="19" x2="23" y2="25" stroke="#4ead6b" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="16" cy="4" r="2.5" fill="#4ead6b"/>
<circle cx="9" cy="7" r="2" fill="#4ead6b"/>
<circle cx="23" cy="7" r="2" fill="#4ead6b"/>
<circle cx="16" cy="28" r="2.5" fill="#4ead6b"/>
<circle cx="9" cy="25" r="2" fill="#4ead6b"/>
<circle cx="23" cy="25" r="2" fill="#4ead6b"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://mcdbus.warehack.ing/sitemap-index.xml

View File

@ -0,0 +1,6 @@
import { defineCollection } from "astro:content";
import { docsSchema } from "@astrojs/starlight/schema";
export const collections = {
docs: defineCollection({ schema: docsSchema() }),
};

View File

@ -0,0 +1,127 @@
---
title: Architecture
description: Internal structure of the mcdbus D-Bus MCP server, covering module organization, the BusManager, lifespan context, and type serialization.
---
import { Aside } from '@astrojs/starlight/components';
mcdbus is built on [FastMCP 3.0](https://gofastmcp.com/) with [dbus-fast](https://github.com/Bluetooth-Devices/dbus-fast) for async D-Bus communication. This page explains how the pieces fit together.
## 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()`:
```python
import mcdbus._discovery
import mcdbus._interaction
import mcdbus._prompts
import mcdbus._resources
import mcdbus._shortcuts
from 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
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
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.
```python
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
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:
```python
@asynccontextmanager
async 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:
```python
def get_mgr(ctx: Context) -> BusManager:
return ctx.lifespan_context
```
This design means tools never create their own connections. They all share the same BusManager, which maintains at most two connections total.
## 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
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.
<Aside type="note">
Auto-wrapping for Variants checks `bool` before `int` because Python's `bool` is a subclass of `int`. Without this ordering, `True` would be wrapped as integer `1` instead of boolean `true`.
</Aside>
## 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
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 `SignatureTree` parser.
Invalid inputs raise `ValueError` before any D-Bus message is sent.

View File

@ -0,0 +1,93 @@
---
title: Session vs System Bus
description: The two D-Bus buses on Linux, what lives on each, and how mcdbus treats them differently.
---
import { Aside } from '@astrojs/starlight/components';
Linux systems running D-Bus have two distinct message buses. Every mcdbus tool takes a `bus` parameter -- either `"session"` or `"system"` -- that selects which one to talk to. The choice affects what services are available, what permissions are required, and whether mcdbus prompts for confirmation.
## The session bus
The session bus is per-user, per-login-session. It starts when you log in (typically managed by `dbus-daemon --session` or `dbus-broker --scope user`) and dies when you log out. Its address is stored in the `DBUS_SESSION_BUS_ADDRESS` environment variable.
Desktop applications and user services register here. The session bus is where you find:
- Notification daemons (`org.freedesktop.Notifications`)
- Media players via MPRIS2 (`org.mpris.MediaPlayer2.*`)
- Desktop environment components (KDE KWin, GNOME Shell, etc.)
- XDG portals (`org.freedesktop.portal.*`)
- User-level systemd (`org.freedesktop.systemd1` on the session bus manages `systemctl --user` services)
- Input methods, screen savers, file managers
Because the session bus is scoped to your user session, any process running as your user can send messages to any service on it. There is no authentication step -- if you can reach the socket, you can talk. This is by design: desktop applications need to communicate freely within a single user's session.
**mcdbus treats session bus operations as low-risk.** Method calls on the session bus execute without a confirmation prompt. Property mutations (via `set_property`) still prompt because they change state, but the prompt is about intent rather than privilege.
## The system bus
The system bus is system-wide. There is exactly one instance, started at boot by the init system. Its socket lives at `/var/run/dbus/system_bus_socket` (or equivalent). Every user on the machine can connect to it, but access is tightly controlled.
System-level services register here:
- systemd manager (`org.freedesktop.systemd1`)
- NetworkManager (`org.freedesktop.NetworkManager`)
- UPower battery management (`org.freedesktop.UPower`)
- bluez Bluetooth stack (`org.bluez`)
- udisks2 disk management (`org.freedesktop.UDisks2`)
- Avahi mDNS/DNS-SD (`org.freedesktop.Avahi`)
- CUPS printing (`org.cups.cupsd`)
- Accounts service (`org.freedesktop.Accounts`)
The system bus has stricter access control than the session bus. D-Bus bus policy files (XML configs in `/etc/dbus-1/system.d/`) define which users can send messages to which services. Many services also perform polkit authorization checks before executing privileged operations.
**mcdbus treats system bus method calls as sensitive.** Calling `call_method` with `bus="system"` triggers a confirmation prompt (via the [confirmation flow](/explanation/confirmation-flow/)) before the message is sent. Read-only operations through the shortcut tools (`battery_status`, `network_status`, `bluetooth_devices`, `list_systemd_units`) do not trigger confirmation because they only read properties -- they never call service-specific methods that could change state.
## What lives where
| Service | Session bus | System bus | Notes |
|---------|:-----------:|:----------:|-------|
| Notifications | Yes | -- | Desktop notifications are per-user |
| MPRIS2 media players | Yes | -- | Media players are user applications |
| KDE KWin | Yes | -- | Window manager is per-session |
| XDG portals | Yes | -- | Sandboxed app integration |
| systemd | Yes (user units) | Yes (system units) | Different manager instances |
| NetworkManager | -- | Yes | System-wide network config |
| UPower | -- | Yes | Hardware battery status |
| bluez | -- | Yes | Bluetooth adapter and devices |
| udisks2 | -- | Yes | Disk and partition management |
| Avahi | -- | Yes | mDNS service discovery |
| Accounts | -- | Yes | User account management |
<Aside type="note">
Some services register on both buses. systemd is the most prominent example: `org.freedesktop.systemd1` on the session bus manages your user services (`systemctl --user`), while the same name on the system bus manages system services (`systemctl`). They are separate manager instances with different capabilities.
</Aside>
## Security implications
The distinction between the two buses is a security boundary. The session bus assumes all participants are the same user and can be trusted at a basic level. The system bus assumes participants may be different users with different privilege levels and enforces access control accordingly.
For mcdbus, this means:
- **Session bus:** Low friction. Services are user-scoped. The worst case for an unintended call is usually a skipped track or a stray notification.
- **System bus:** Higher friction. Services can affect the entire machine. An unintended call could restart a system service, change network configuration, or trigger a disk operation. The confirmation prompt exists to prevent this.
The [Lock Down Permissions](/guides/permissions/) guide covers how to add additional restrictions beyond mcdbus's built-in confirmation behavior, using the operating system's own D-Bus permission stack.
## The bus parameter
Every mcdbus tool accepts the `bus` parameter as either `"session"` or `"system"`. There are no other valid values. The parameter is a string, not an enum, because MCP tool arguments are JSON-typed.
Shortcut tools that target a specific bus have sensible defaults:
| Tool | Default bus | Reason |
|------|-------------|--------|
| `send_notification` | session (hardcoded) | Notifications are a session bus service |
| `media_player_control` | session (hardcoded) | MPRIS2 players are session bus services |
| `battery_status` | system (hardcoded) | UPower is a system bus service |
| `network_status` | system (hardcoded) | NetworkManager is a system bus service |
| `bluetooth_devices` | system (hardcoded) | bluez is a system bus service |
| `kwin_windows` | session (hardcoded) | KWin is a session bus service |
| `list_systemd_units` | system (default, overridable) | System units are the common case, but `bus="session"` shows user units |
Discovery tools (`list_services`, `list_objects`, `introspect`) and interaction tools (`call_method`, `get_property`, `set_property`, `get_all_properties`) require the `bus` parameter explicitly. There is no default because assuming the wrong bus leads to confusing errors.

View File

@ -0,0 +1,119 @@
---
title: Confirmation Flow
description: How mcdbus decides when to ask for user confirmation and the three-tier fallback cascade for delivering the prompt.
---
import { Aside } from '@astrojs/starlight/components';
mcdbus asks for user confirmation before performing operations that could change system state. This page explains when confirmation triggers, how the three-tier fallback cascade works, and how environment variables control the behavior.
## When confirmation triggers
Two conditions trigger a confirmation prompt:
1. **System bus method calls.** Any call to `call_method` with `bus="system"`. The system bus can affect the entire machine, so every method call requires explicit approval.
2. **Property mutations on any bus.** Any call to `set_property`, regardless of which bus. Changing a property is always a state mutation, even on the session bus.
Read-only operations never trigger confirmation. This includes `list_services`, `introspect`, `list_objects`, `get_property`, `get_all_properties`, and all the shortcut tools (`battery_status`, `network_status`, `bluetooth_devices`, `list_systemd_units`, `send_notification`, `media_player_control`). The shortcut tools either read properties only or operate on the session bus.
<Aside type="note">
`send_notification` and `media_player_control` are session bus method calls, not property mutations, so they execute without confirmation. The session bus is user-scoped and the risk from these operations is minimal.
</Aside>
## The three-tier cascade
When confirmation is required, mcdbus tries three methods in sequence. If one succeeds, the remaining tiers are skipped. If all three are exhausted, the behavior depends on configuration.
### Tier 1: MCP elicitation
The preferred method. mcdbus calls `ctx.elicit()`, which is a native MCP protocol feature that presents the user with a choice dialog in their MCP client. The prompt shows the operation details (service, method, path, arguments) and offers two options: "Yes, proceed" and "No, cancel."
If the MCP client supports elicitation and the user selects "Yes, proceed", the operation continues. If the user selects "No, cancel", the operation is aborted with a `ToolError`.
If the client does not implement elicitation at the protocol level, `ctx.elicit()` either returns a `CancelledElicitation` or throws an exception. In either case, mcdbus catches this and falls through to Tier 2.
### Tier 2: Desktop notification fallback
This tier is opt-in. It activates only when the `MCDBUS_NOTIFY_CONFIRM` environment variable is set to a non-empty value (e.g., `MCDBUS_NOTIFY_CONFIRM=1`).
When active, mcdbus sends a desktop notification through `org.freedesktop.Notifications` on the session bus. The notification has the urgency set to critical and presents two action buttons: "Approve" and "Deny."
The implementation registers D-Bus signal match rules for `ActionInvoked` and `NotificationClosed` before sending the notification, ensuring no signal can be missed due to a race condition. It then waits up to 60 seconds for the user to respond.
If the user clicks "Approve", the operation proceeds. If they click "Deny", dismiss the notification, or let it expire, the operation is aborted. If the notification service itself is unreachable, mcdbus logs the failure and falls through to Tier 3.
<Aside type="caution">
The notification fallback requires a running notification daemon on the session bus and a graphical environment to display it. It will not work in headless or SSH sessions.
</Aside>
### Tier 3: Proceed with warning
If both Tier 1 and Tier 2 fail or are unavailable, mcdbus logs a warning to stderr and allows the operation to proceed.
This is the default behavior when no confirmation method is available. The rationale is that the operation was initiated by Claude at the user's request, and blocking it entirely when no confirmation channel exists would make the tool unusable in environments that lack elicitation support.
The warning is written to stderr (and to the systemd journal if running as a service), creating an audit trail even when no interactive confirmation was possible.
## The decision tree
```
Confirmation required?
|
+-- No --> Execute immediately
|
+-- Yes --> Try MCP elicitation (ctx.elicit)
|
+-- User approved --> Execute
|
+-- User denied --> Abort (ToolError)
|
+-- Elicitation unavailable
|
+-- MCDBUS_REQUIRE_ELICITATION set?
| |
| +-- Yes --> Abort (ToolError: "Elicitation required
| | but client does not support it")
| |
| +-- No --> Continue to fallbacks
|
+-- MCDBUS_NOTIFY_CONFIRM set?
|
+-- Yes --> Send desktop notification
| |
| +-- User clicked Approve --> Execute
| |
| +-- User clicked Deny / dismissed / timed out --> Abort
| |
| +-- Notification service unreachable --> Proceed with warning
|
+-- No --> Proceed with warning (log to stderr)
```
## Environment variables
Two environment variables control the confirmation flow:
**`MCDBUS_REQUIRE_ELICITATION`** -- When set to any non-empty value, mcdbus refuses to proceed if the MCP client does not support elicitation. No fallback to notifications or warn-and-proceed. The operation fails immediately with a `ToolError`. Use this in high-security deployments where you need a guarantee that every sensitive operation was explicitly approved through the MCP protocol.
**`MCDBUS_NOTIFY_CONFIRM`** -- When set to any non-empty value, enables the desktop notification fallback (Tier 2). Without this, mcdbus skips directly from Tier 1 failure to Tier 3 (proceed with warning). Use this when your MCP client does not support elicitation but you have a graphical session with a notification daemon.
Both can be set in the systemd unit file:
```ini
[Service]
Environment=MCDBUS_REQUIRE_ELICITATION=1
Environment=MCDBUS_NOTIFY_CONFIRM=1
```
If both are set, `MCDBUS_REQUIRE_ELICITATION` takes priority. The notification fallback is never reached because elicitation failure immediately aborts the operation.
## Audit logging
Regardless of which tier handles confirmation, mcdbus logs every system bus method call and every property mutation to stderr. The log message includes the bus type, service name, interface, method (or property name), object path, and signature. When running under systemd, these messages go to the journal and can be reviewed with:
```bash
journalctl -u mcdbus.service | grep "mcdbus:"
```
This audit trail exists independently of the confirmation mechanism. Even Tier 3 (proceed with warning) produces a log entry, so you can always reconstruct what operations were performed.

View File

@ -0,0 +1,115 @@
---
title: Security Layers
description: The four-layer security model that mcdbus delegates to the operating system, and how the layers compose.
---
import { Aside } from '@astrojs/starlight/components';
mcdbus does not implement its own access control. By design, it delegates all security decisions to the operating system's existing D-Bus permission stack. The same mechanisms that protect D-Bus from any other client also protect it from mcdbus.
This page explains the rationale and the four independent layers. For concrete deployment steps, see [Lock Down Permissions](/guides/permissions/).
## Design philosophy
When an LLM agent talks to system services through D-Bus, the temptation is to build access control into the bridge itself -- an allowlist of services, a set of blocked methods, a permission database. mcdbus deliberately does not do this, for three reasons.
First, the operating system already has a mature, well-tested permission stack for D-Bus. Reimplementing it inside mcdbus would be redundant and likely less thorough.
Second, the OS permission stack is transparent to system administrators. They already know how to write D-Bus bus policy files, polkit rules, and systemd unit hardening. An mcdbus-specific config format would be one more thing to learn and audit.
Third, the OS permission stack applies regardless of what client is connecting. If you lock down D-Bus policy for the mcdbus user, those restrictions apply whether the user is running mcdbus, `busctl`, `dbus-send`, or any other D-Bus client. There is no way to bypass the restrictions by using a different tool.
The one thing mcdbus does add is the [confirmation flow](/explanation/confirmation-flow/) -- an interactive prompt before system bus method calls and property mutations. This is not access control in the traditional sense. It is an intent verification step between the LLM and the user, not between the process and the kernel.
## Layer 1: systemd service sandboxing
Scope: process isolation. Granularity: filesystem paths, network families, syscalls, capabilities.
Running mcdbus as a systemd service with hardening directives gives you a sandbox around the entire process. The sandbox uses Linux kernel features -- namespaces, seccomp, capability bounding sets -- that are enforced by the kernel itself. There is no userspace component to bypass.
What systemd sandboxing controls:
- Which parts of the filesystem the process can see and write to
- Which socket address families (AF_UNIX, AF_INET, AF_INET6, etc.) the process can create
- Which Linux capabilities the process has (none, in the recommended configuration)
- Which syscalls the process can invoke
- Whether the process can map memory as writable and executable simultaneously
- Resource limits on memory and tasks
What it does not control: which D-Bus services the process can talk to. As long as the process can reach the D-Bus socket (which requires AF_UNIX), the bus daemon handles message routing. To restrict D-Bus communication specifically, you need Layer 2.
## Layer 2: D-Bus bus policy
Scope: message filtering. Granularity: service name, interface, method name, message type.
D-Bus bus policy operates inside the bus daemon (or bus broker). Every message that passes through the bus is checked against the policy rules. If a rule denies a message, the bus daemon drops it silently (or with an error reply). The sending process never knows the message was filtered -- it just gets an access denied error.
Policy files are XML documents installed in `/etc/dbus-1/system.d/` or `/etc/dbus-1/session.d/`. They match on the sender's Unix user or group and filter by destination service name, interface, method name, and message type.
This is the most reliable layer for controlling D-Bus access because it operates at the message transport level. Even if mcdbus or dbus-fast had a vulnerability that allowed arbitrary D-Bus messages, the bus daemon would still enforce the policy.
<Aside type="note">
D-Bus policy files do not support wildcard matching in service names. If you want to allow access to all MPRIS media players (`org.mpris.MediaPlayer2.*`), you must list each one individually. This is a limitation of the D-Bus spec, not of mcdbus.
</Aside>
## Layer 3: polkit
Scope: action authorization. Granularity: per-action, per-user, per-group, per-session.
polkit sits above D-Bus in the stack. When a system service receives a D-Bus method call that requires authorization, the service asks polkit whether the calling user is allowed to perform that action. This check happens inside the service -- it is the service's choice to consult polkit, not something the bus daemon enforces.
Most system services on a modern Linux desktop use polkit for their authorization decisions: systemd checks polkit before starting or stopping units, NetworkManager checks polkit before changing network configuration, udisks2 checks polkit before mounting filesystems.
polkit rules are JavaScript files that can make decisions based on the action ID, the subject's user name, group membership, whether the session is local, and whether the session is active. This allows fine-grained policies like "allow users in the mcdbus group to read systemd unit status but not start or stop units."
The limitation is that polkit action IDs are often coarser than the actual operations. For example, systemd uses `org.freedesktop.systemd1.manage-units` for both reading unit status and stopping units. To distinguish between those two operations, you need Layer 2 (D-Bus bus policy) to filter at the method level.
## Layer 4: xdg-dbus-proxy
Scope: socket-level filtering. Granularity: service name, interface, object path.
`xdg-dbus-proxy` is a Flatpak utility that creates a filtered D-Bus socket. It sits between the client and the real bus socket, inspecting every message and only passing through those that match the configured rules.
This is the most granular layer. You can allow access to a specific method on a specific interface at a specific object path on a specific service. You can also set different access levels per service: `--see` for read-only visibility (introspectable but no method calls), `--talk` for full communication, and `--own` for name registration.
xdg-dbus-proxy is most useful for the session bus, where D-Bus bus policy is less commonly configured. For the system bus, Layers 2 and 3 are usually sufficient. The trade-off is that xdg-dbus-proxy adds a proxy process and a temporary socket, which adds a small amount of latency and operational complexity.
## How layers compose
Each layer is independent. They do not know about each other and do not interfere with each other. When a D-Bus message travels from mcdbus to a service, it passes through whatever layers are active:
```
mcdbus process
|
| (Layer 1: systemd sandbox -- can the process create a Unix socket?)
|
v
xdg-dbus-proxy (if Layer 4 is deployed)
|
| (Layer 4: proxy filtering -- does the message match the allow rules?)
|
v
D-Bus bus daemon
|
| (Layer 2: bus policy -- is this user allowed to send to this destination?)
|
v
Target service
|
| (Layer 3: polkit -- is this user authorized for this action?)
|
v
Operation executes (or is denied)
```
A message must pass every active layer to succeed. Blocking at any layer stops the operation. This means the layers stack: adding a layer can only make the system more restrictive, never less.
## Choosing layers
**Layer 1 alone** covers the vast majority of deployments. It is free to set up (a single systemd unit file), has zero runtime overhead, and provides broad isolation. Even if mcdbus or any of its dependencies had a vulnerability, the sandbox limits what an attacker could do.
**Layers 1 + 2** add service-level filtering when you want to restrict which D-Bus services mcdbus can communicate with. This is the right combination when the threat model includes mcdbus being used to talk to services it should not (e.g., udisks2 for disk operations when only battery and network status are needed).
**Layers 1 + 2 + 3** add per-action authorization for system services that check polkit. This is appropriate for shared systems or environments where multiple users have access and you want different privilege levels.
**Layer 4** fills a niche: fine-grained session bus filtering. Use it when you need to restrict which session bus services mcdbus can see, or when you need per-path filtering that the bus policy cannot express.

View File

@ -0,0 +1,132 @@
---
title: Browse Bluetooth Devices
description: List paired and discovered Bluetooth devices using mcdbus's bluetooth_devices tool and the bluez D-Bus interface.
---
import { Aside, Code } from '@astrojs/starlight/components';
mcdbus provides a `bluetooth_devices` shortcut tool that reads from bluez on the system bus. It uses the ObjectManager interface to get all known Bluetooth devices in a single call, then formats them into a sorted table.
## Listing devices
> "What Bluetooth devices are nearby?"
```
bluetooth_devices()
```
The tool returns a table sorted with connected devices first, then paired devices, then everything else alphabetically:
```markdown
## Bluetooth Devices -- 5 found
| Name | Address | Connected | Paired | Trusted |
|------|---------|-----------|--------|---------|
| WH-1000XM4 | 38:18:4C:XX:XX:XX | yes | yes | yes |
| Keyboard K380 | 34:88:5D:XX:XX:XX | yes | yes | yes |
| Galaxy Buds2 | A0:C5:F2:XX:XX:XX | no | yes | yes |
| iPhone | 7C:04:D0:XX:XX:XX | no | no | no |
| [unknown] | 48:A4:93:XX:XX:XX | no | no | no |
```
### What it reads
The tool calls `org.freedesktop.DBus.ObjectManager.GetManagedObjects` on `org.bluez` at the root path `/`. This returns every managed object in the bluez hierarchy. The tool filters for objects that have the `org.bluez.Device1` interface and extracts:
| Column | Source property | Notes |
|--------|---------------|-------|
| Name | `Name` or `Alias` | Falls back to "Unknown" if neither is set |
| Address | `Address` | Bluetooth MAC address |
| Connected | `Connected` | Boolean -- active link to this device |
| Paired | `Paired` | Boolean -- bonding keys exchanged |
| Trusted | `Trusted` | Boolean -- auto-connect permitted |
## Reading detailed device properties
Each Bluetooth device has an object path under `/org/bluez/hci0/` (or whichever adapter is active). The path encodes the MAC address with underscores replacing colons:
```
/org/bluez/hci0/dev_38_18_4C_XX_XX_XX
```
To read all properties on a specific device:
> "Tell me everything about my WH-1000XM4 headphones."
```
get_all_properties(
bus="system",
service="org.bluez",
object_path="/org/bluez/hci0/dev_38_18_4C_XX_XX_XX",
interface="org.bluez.Device1"
)
```
This returns additional properties beyond what the shortcut tool shows:
| Property | What it contains |
|----------|-----------------|
| `RSSI` | Signal strength in dBm (only present during discovery) |
| `Appearance` | GAP appearance value indicating device category |
| `Icon` | Icon hint string (e.g., "audio-headphones", "input-keyboard") |
| `Adapter` | Object path of the Bluetooth adapter this device was seen by |
| `ServicesResolved` | Whether service discovery has completed |
| `UUIDs` | List of Bluetooth service UUIDs the device advertises |
| `Modalias` | USB-style modalias string for driver matching |
### Battery level
Some Bluetooth devices report their battery level through the `org.bluez.Battery1` interface. To check:
```
get_all_properties(
bus="system",
service="org.bluez",
object_path="/org/bluez/hci0/dev_38_18_4C_XX_XX_XX",
interface="org.bluez.Battery1"
)
```
If the device supports it, this returns a `Percentage` property (0-100). Not all devices report battery level over Bluetooth -- it depends on the device firmware and profile support.
<Aside type="tip">
To check whether the Battery1 interface exists on a device before trying to read it, use `introspect` on the device path first. The response lists all interfaces available at that object.
</Aside>
## Inspecting the adapter
The Bluetooth adapter itself (typically `hci0`) has its own set of properties at `/org/bluez/hci0`:
```
get_all_properties(
bus="system",
service="org.bluez",
object_path="/org/bluez/hci0",
interface="org.bluez.Adapter1"
)
```
This shows the adapter's name, MAC address, power state, discoverability, and supported features.
## Finding device paths
If you are unsure of the exact object path for a device, walk the bluez object tree:
```
list_objects(bus="system", service="org.bluez")
```
This returns paths like:
```
/org/bluez
/org/bluez/hci0
/org/bluez/hci0/dev_38_18_4C_XX_XX_XX
/org/bluez/hci0/dev_34_88_5D_XX_XX_XX
```
Each `dev_` path corresponds to a known device. The MAC address in the path matches the `Address` property with colons replaced by underscores.
<Aside type="note">
The `bluetooth_devices` tool is read-only. It does not initiate scanning, pairing, or connections. To trigger those operations, you would use `call_method` on the adapter or device interfaces -- but those are system bus method calls and will require confirmation.
</Aside>

View File

@ -0,0 +1,182 @@
---
title: Explore Unknown Services
description: Walk through the full discovery workflow for investigating any D-Bus service from scratch using mcdbus tools and prompts.
---
import { Aside, Steps, Code } from '@astrojs/starlight/components';
One of the most valuable uses of mcdbus is exploring D-Bus services you have never worked with before. The discovery tools let you walk the entire object hierarchy, read interfaces, and test methods -- all through conversation with Claude. No prior knowledge of the service is required.
This guide walks through the full workflow using a real example: discovering what the Flatpak system helper exposes on D-Bus.
## The five-step workflow
<Steps>
1. **Find the service.** List everything on the bus and look for the service name.
2. **Walk the object tree.** See what object paths the service exposes.
3. **Introspect an object.** Read its interfaces, methods, properties, and signals.
4. **Read properties.** Get the current state without changing anything.
5. **Call methods.** Interact with the service.
</Steps>
## Step 1: List services
Start by listing all well-known service names on the bus you want to explore:
> "What services are on the system bus?"
```
list_services(bus="system")
```
This returns every well-known service name. On a typical desktop system, there are 30-80 services on the system bus. Claude can scan the list and highlight anything relevant to what you are looking for.
For the session bus, the same approach works:
```
list_services(bus="session")
```
Session bus services tend to include desktop environment components, media players, file managers, and application portals.
<Aside type="tip">
If you already know the service name, skip this step. The discovery tools do not require you to start from scratch every time.
</Aside>
## Step 2: Walk the object tree
Once you have a service name, see what object paths it exposes:
> "Show me the object tree for org.freedesktop.Flatpak.SystemHelper."
```
list_objects(bus="system", service="org.freedesktop.Flatpak.SystemHelper")
```
The tool performs a breadth-first walk starting from `/`, introspecting each node to find children. The result is a flat list of all object paths with their non-standard interfaces:
```
- `/` -- (no interfaces)
- `/org/freedesktop/Flatpak/SystemHelper` -- `org.freedesktop.Flatpak.SystemHelper`
```
Some services have deep trees with hundreds of objects (bluez, systemd). Others, like this one, have just one or two meaningful paths.
The walk is bounded: it stops at 500 objects or 60 seconds, whichever comes first. You can also scope it to a subtree:
```
list_objects(
bus="system",
service="org.freedesktop.systemd1",
root_path="/org/freedesktop/systemd1/unit"
)
```
## Step 3: Introspect
Pick an object path and look at its interfaces in detail:
> "Introspect the Flatpak system helper."
```
introspect(
bus="system",
service="org.freedesktop.Flatpak.SystemHelper",
object_path="/org/freedesktop/Flatpak/SystemHelper"
)
```
The response shows every interface, its methods (with argument names, types, and directions), properties (with type and read/write access), and signals. Standard interfaces like `org.freedesktop.DBus.Peer` and `org.freedesktop.DBus.Introspectable` are filtered out by default. To include them:
```
introspect(
bus="system",
service="org.freedesktop.Flatpak.SystemHelper",
object_path="/org/freedesktop/Flatpak/SystemHelper",
include_standard=true
)
```
## Step 4: Read properties
Before calling any methods, read the service's current state:
> "Read all properties on the Flatpak SystemHelper interface."
```
get_all_properties(
bus="system",
service="org.freedesktop.Flatpak.SystemHelper",
object_path="/org/freedesktop/Flatpak/SystemHelper",
interface="org.freedesktop.Flatpak.SystemHelper"
)
```
Properties are returned as a markdown table with name-value pairs. Values longer than 80 characters are truncated for readability. For the full value of a specific property, use `get_property`:
```
get_property(
bus="system",
service="org.freedesktop.Flatpak.SystemHelper",
object_path="/org/freedesktop/Flatpak/SystemHelper",
interface="org.freedesktop.Flatpak.SystemHelper",
property_name="version"
)
```
## Step 5: Call methods
Once you understand the interface, you can call methods. On the session bus, this happens immediately. On the system bus, mcdbus asks for confirmation first.
> "Call GetServerInformation on the notification daemon."
```
call_method(
bus="session",
service="org.freedesktop.Notifications",
object_path="/org/freedesktop/Notifications",
interface="org.freedesktop.Notifications",
method="GetServerInformation"
)
```
For methods that take arguments, you need to provide the D-Bus type signature and a JSON array of arguments. The introspect output shows the expected types for each method parameter.
## Using the explore_service prompt
Instead of manually stepping through each stage, you can activate the `explore_service` prompt to get a guided walkthrough:
> "Use the explore_service prompt for org.freedesktop.Flatpak.SystemHelper on the system bus."
This loads a step-by-step guide into the conversation context, directing Claude through the same workflow automatically. The prompt accepts two parameters:
- `bus`: "session" or "system" (default: "session")
- `service`: the service name to explore (optional -- if omitted, starts from listing all services)
## Using the debug_service prompt
When a service is not behaving as expected -- method calls failing, properties returning unexpected values, or the service not appearing at all -- use the `debug_service` prompt:
> "Use the debug_service prompt for org.freedesktop.Notifications on the session bus."
This loads a diagnostic workflow that checks:
1. Whether the service is registered on the bus
2. Whether the root path is introspectable
3. The full object tree
4. Current property values on key interfaces
5. Basic connectivity via the Peer interface's Ping method
## Tips for exploring unfamiliar services
**Start with properties, not methods.** Properties are read-only by default and give you a snapshot of the service's state without risk of changing anything.
**Use `include_standard=false` (the default).** Standard D-Bus interfaces are the same on every object. Filtering them out makes the output much easier to read.
**Check both buses.** Some services register on both the session and system bus with different capabilities. NetworkManager, for example, has a presence on the system bus (full control) and sometimes the session bus (limited status).
**Look for ObjectManager.** Services that manage a collection of objects (bluez for Bluetooth devices, UPower for power devices, udisks2 for storage) typically implement `org.freedesktop.DBus.ObjectManager` at their root path. Calling `GetManagedObjects` on that interface returns the entire device tree in one call, which is much faster than walking the tree node by node.

View File

@ -0,0 +1,139 @@
---
title: Control Media Players
description: Use mcdbus to discover and control MPRIS2 media players like Spotify, Firefox, VLC, and mpv through D-Bus.
---
import { Aside, Steps, Code, Tabs, TabItem } from '@astrojs/starlight/components';
MPRIS2 (Media Player Remote Interfacing Specification) is the standard D-Bus interface for media players on Linux. Any application that plays audio or video and follows this spec -- Spotify, Firefox, VLC, mpv, Chromium, Rhythmbox, and many others -- exposes the same set of methods and properties on the session bus. mcdbus can talk to all of them.
## How MPRIS2 works on D-Bus
Every MPRIS2 player registers a well-known service name following the pattern `org.mpris.MediaPlayer2.{player}`. The player object lives at `/org/mpris/MediaPlayer2` and exposes the `org.mpris.MediaPlayer2.Player` interface with transport controls (Play, Pause, Next, Previous, Stop) and properties like Metadata, PlaybackStatus, and Volume.
Because this is a session bus service, no confirmation prompt is needed -- all operations run in your user session.
## Quick control with `media_player_control`
The fastest way to control a player is the dedicated shortcut tool. It auto-discovers MPRIS players if you do not specify one.
> "Pause whatever is playing."
Claude calls:
```
media_player_control(action="pause")
```
The tool finds the first MPRIS player on the session bus, sends the Pause command, and reports back the player name, current status, and the track that was playing.
Available actions: `play`, `pause`, `play-pause`, `next`, `previous`, `stop`.
### Targeting a specific player
When multiple players are active (say Spotify and Firefox both have audio), specify the full service name:
> "Skip to the next track on Spotify."
```
media_player_control(action="next", player="org.mpris.MediaPlayer2.spotify")
```
## Discovering active players
If you want to see which players are running before taking action, list session bus services and filter for the MPRIS prefix:
> "What media players are running right now?"
```
list_services(bus="session")
```
Claude scans the results for service names starting with `org.mpris.MediaPlayer2.` and reports what it finds. Common names include:
- `org.mpris.MediaPlayer2.spotify`
- `org.mpris.MediaPlayer2.firefox.instance_1_42`
- `org.mpris.MediaPlayer2.vlc`
- `org.mpris.MediaPlayer2.mpv`
## Reading playback metadata
To find out what is currently playing without changing anything, read the properties on the Player interface:
> "What song is Spotify playing?"
```
get_all_properties(
bus="session",
service="org.mpris.MediaPlayer2.spotify",
object_path="/org/mpris/MediaPlayer2",
interface="org.mpris.MediaPlayer2.Player"
)
```
The response includes a property table with:
| Property | What it contains |
|----------|-----------------|
| `PlaybackStatus` | "Playing", "Paused", or "Stopped" |
| `Metadata` | Dict with `xesam:title`, `xesam:artist`, `xesam:album`, `mpris:artUrl`, `mpris:length` |
| `Volume` | Float from 0.0 to 1.0 |
| `Position` | Current playback position in microseconds |
| `CanNext` | Whether the player supports skipping forward |
| `CanPrevious` | Whether the player supports skipping backward |
## Example workflow
A typical conversation might go:
<Steps>
1. **Discover players.** Ask Claude to list session bus services. Claude finds `org.mpris.MediaPlayer2.spotify` and `org.mpris.MediaPlayer2.firefox.instance_1_12`.
2. **Check what is playing.** Ask Claude to read the Metadata and PlaybackStatus properties on the Spotify player. Claude reports the current track, artist, album, and that playback is active.
3. **Skip to the next track.** Ask Claude to send the Next command to Spotify. Claude calls `media_player_control(action="next", player="org.mpris.MediaPlayer2.spotify")` and confirms the new track.
</Steps>
<Aside type="tip">
The `media_player_control` tool reads metadata and status automatically after executing the action, so you get confirmation of what is now playing in a single call.
</Aside>
## Adjusting volume
Volume is a writable property on the Player interface. To change it, use `set_property`:
> "Set Spotify's volume to 50%."
```
set_property(
bus="session",
service="org.mpris.MediaPlayer2.spotify",
object_path="/org/mpris/MediaPlayer2",
interface="org.mpris.MediaPlayer2.Player",
property_name="Volume",
value="0.5",
signature="d"
)
```
<Aside type="note">
Setting a property triggers a confirmation prompt because it mutates state. Claude will ask you to approve before the value is changed.
</Aside>
## Going deeper with raw method calls
For actions beyond the shortcut tool -- like setting the playback position, opening a specific URI, or reading the top-level `org.mpris.MediaPlayer2` interface (which has properties like Identity, DesktopEntry, and SupportedMimeTypes) -- use `introspect` to see what is available, then `call_method` to invoke it directly.
> "Open this Spotify URI in the player."
```
call_method(
bus="session",
service="org.mpris.MediaPlayer2.spotify",
object_path="/org/mpris/MediaPlayer2",
interface="org.mpris.MediaPlayer2.Player",
method="OpenUri",
args='["spotify:track:4PTG3Z6ehGkBFwjybzWkR8"]',
signature="s"
)
```

View File

@ -0,0 +1,150 @@
---
title: Send Notifications
description: Send desktop notifications from Claude using mcdbus's send_notification tool and the freedesktop Notifications interface.
---
import { Aside, Code } from '@astrojs/starlight/components';
mcdbus provides a `send_notification` shortcut tool that sends desktop notifications through the standard `org.freedesktop.Notifications` interface on the session bus. Any notification daemon that implements this spec (dunst, mako, GNOME Shell, KDE Plasma) will display them.
## Basic usage
The only required parameter is the summary (title):
> "Send me a notification that says the build is done."
```
send_notification(summary="Build complete")
```
This sends a notification with the title "Build complete", no body text, the default icon, and a 5-second timeout.
## Adding body text
The `body` parameter accepts plain text or basic HTML markup (depending on your notification daemon):
> "Notify me that the deploy finished with details about what was deployed."
```
send_notification(
summary="Deploy finished",
body="Pushed v2026.03.06 to production. All health checks passing."
)
```
## Choosing an icon
The `icon` parameter follows the freedesktop icon naming specification. Your desktop theme provides the actual icon files. Common names include:
| Icon name | Typical use |
|-----------|------------|
| `dialog-information` | General information |
| `dialog-warning` | Warning conditions |
| `dialog-error` | Error conditions |
| `process-completed` | Task completion |
| `network-wireless` | Network-related |
| `battery-low` | Battery alerts |
> "Send a warning notification about high memory usage."
```
send_notification(
summary="High memory usage",
body="System memory at 92%. Consider closing unused applications.",
icon="dialog-warning"
)
```
You can also pass an absolute file path to use a custom icon image:
```
send_notification(
summary="Screenshot saved",
icon="/tmp/screenshot-thumbnail.png"
)
```
## Controlling display duration
The `timeout` parameter sets how long the notification stays visible, in milliseconds. The default is 5000 (5 seconds).
```
send_notification(
summary="Quick heads-up",
body="This disappears fast.",
timeout=2000
)
```
A timeout of `0` tells the notification server to use its own default duration. Some servers treat `0` as "never expire" -- the notification stays until the user dismisses it.
<Aside type="note">
Not all notification daemons respect the timeout value. Some (like GNOME Shell) ignore it entirely and use their own fixed duration. This is permitted by the spec.
</Aside>
## What happens under the hood
The `send_notification` tool calls `org.freedesktop.Notifications.Notify` on the session bus with the D-Bus signature `susssasa{sv}i`. The parameters map to:
| Position | Signature | Parameter | Value |
|----------|-----------|-----------|-------|
| 0 | `s` | app_name | `"mcdbus"` |
| 1 | `u` | replaces_id | `0` (new notification) |
| 2 | `s` | app_icon | Your `icon` value |
| 3 | `s` | summary | Your `summary` value |
| 4 | `s` | body | Your `body` value |
| 5 | `as` | actions | `[]` (empty) |
| 6 | `a{sv}` | hints | `{}` (empty) |
| 7 | `i` | expire_timeout | Your `timeout` value |
The server returns a notification ID (an unsigned integer). The tool reports this ID in its response.
## Replacing an existing notification
The shortcut tool always sends `replaces_id=0`, which creates a new notification. To update or replace an existing notification, use `call_method` directly and pass the notification ID you received from a previous call:
> "Update notification 42 with new progress information."
```
call_method(
bus="session",
service="org.freedesktop.Notifications",
object_path="/org/freedesktop/Notifications",
interface="org.freedesktop.Notifications",
method="Notify",
args='["mcdbus", 42, "", "Build progress", "Step 3 of 5 complete", [], {}, 5000]',
signature="susssasa{sv}i"
)
```
## Closing a notification
To programmatically close a notification by its ID:
```
call_method(
bus="session",
service="org.freedesktop.Notifications",
object_path="/org/freedesktop/Notifications",
interface="org.freedesktop.Notifications",
method="CloseNotification",
args='[42]',
signature="u"
)
```
## Querying the notification server
To find out which features your notification daemon supports:
```
call_method(
bus="session",
service="org.freedesktop.Notifications",
object_path="/org/freedesktop/Notifications",
interface="org.freedesktop.Notifications",
method="GetServerInformation"
)
```
This returns the server name, vendor, version, and spec version. You can also call `GetCapabilities` to see which features (like `body-markup`, `actions`, `persistence`) are supported.

View File

@ -0,0 +1,468 @@
---
title: Lock Down Permissions
description: Deploy mcdbus with systemd sandboxing, D-Bus bus policy, polkit rules, and xdg-dbus-proxy for layered security.
---
import { Aside, Steps, Code, Tabs, TabItem } from '@astrojs/starlight/components';
mcdbus delegates all security to the operating system. It does not implement its own access control. This guide covers four independent layers you can deploy to restrict what mcdbus can do. They are all optional, all independent, and all stack on top of each other.
For the design rationale behind this approach, see [Security Layers](/explanation/security-layers/).
## Quick reference
| Layer | Scope | Granularity | Bus |
|-------|-------|-------------|-----|
| systemd | Process isolation | Filesystem, network, syscalls | Both |
| D-Bus policy | Message filtering | Service, interface, method | Both |
| polkit | Action authorization | Per-action, per-user/group | System (mostly) |
| xdg-dbus-proxy | Socket filtering | Service, interface, path | Session (mostly) |
## Layer 1: systemd service sandboxing
The single most effective thing you can do. Running mcdbus as a systemd service gives you process-level isolation using Linux namespaces, seccomp, and capability dropping -- all with declarative directives.
### The unit file
```ini
[Unit]
Description=mcdbus D-Bus MCP server
Documentation=https://github.com/supported-systems/mcdbus
After=dbus.service
[Service]
Type=simple
ExecStart=/usr/bin/env mcdbus
DynamicUser=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectClock=yes
ProtectProc=invisible
ProcSubset=pid
ReadWritePaths=
RestrictAddressFamilies=AF_UNIX
PrivateNetwork=no
CapabilityBoundingSet=
AmbientCapabilities=
NoNewPrivileges=yes
SystemCallFilter=@system-service
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
MemoryDenyWriteExecute=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
RestrictNamespaces=yes
Environment=MCDBUS_TIMEOUT=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=mcdbus
MemoryMax=256M
TasksMax=64
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
```
### What it does
`DynamicUser=yes` creates an ephemeral Unix user for each invocation. No home directory, no persistent UID, no leftover state.
**Filesystem restrictions:** `ProtectSystem=strict` makes `/usr`, `/boot`, and `/efi` read-only. `ProtectHome=yes` hides `/home`, `/root`, and `/run/user` entirely. `PrivateTmp=yes` gives the process its own `/tmp`.
**Network restrictions:** `RestrictAddressFamilies=AF_UNIX` limits socket creation to Unix domain sockets. D-Bus only needs `AF_UNIX`, so this blocks any TCP, UDP, or raw socket activity.
**Capabilities:** `CapabilityBoundingSet=` (empty) drops every Linux capability. The process cannot change file ownership, load kernel modules, bind privileged ports, or perform any other capability-gated operation.
**Syscall filtering:** `SystemCallFilter=@system-service` restricts the process to the syscall set that a typical well-behaved service needs. Things like `mount`, `reboot`, and `kexec_load` are blocked. Blocked syscalls return `EPERM`.
**Memory protection:** `MemoryDenyWriteExecute=yes` prevents mapping memory as both writable and executable, which stops most classes of code injection.
**Resource limits:** `MemoryMax=256M` and `TasksMax=64` prevent runaway resource consumption.
### Deploy it
```bash
sudo cp mcdbus.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now mcdbus.service
```
### Test it
systemd ships a built-in security auditor:
```bash
systemd-analyze security mcdbus.service
```
This produces a score from 0.0 (fully exposed) to 10.0 (fully locked down). The unit file above should score above 7.0. The output lists each directive and its impact.
Check the journal for sandbox-related denials:
```bash
journalctl -u mcdbus.service -f
```
### Environment variables
The unit file sets `MCDBUS_TIMEOUT=30` by default. To override or add other variables, create a drop-in:
```bash
sudo systemctl edit mcdbus.service
```
```ini
[Service]
Environment=MCDBUS_TIMEOUT=60
Environment=MCDBUS_REQUIRE_ELICITATION=1
```
---
## Layer 2: D-Bus bus policy
D-Bus itself has a message filtering layer at the bus daemon (or bus broker) level. Policy files are XML documents that control which messages any client can send or receive based on the sender's Unix identity.
### The policy file
```xml
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- Default deny for mcdbus user -->
<policy user="mcdbus">
<deny send_destination="*"/>
<!-- Bus daemon (required for discovery) -->
<allow send_destination="org.freedesktop.DBus"/>
<!-- Desktop notifications -->
<allow send_destination="org.freedesktop.Notifications"
send_interface="org.freedesktop.Notifications"/>
<allow send_destination="org.freedesktop.Notifications"
send_interface="org.freedesktop.DBus.Introspectable"/>
<!-- MPRIS media players (one block per player) -->
<allow send_destination="org.mpris.MediaPlayer2.firefox"/>
<allow send_destination="org.mpris.MediaPlayer2.chromium"/>
<allow send_destination="org.mpris.MediaPlayer2.spotify"/>
<allow send_destination="org.mpris.MediaPlayer2.vlc"/>
<allow send_destination="org.mpris.MediaPlayer2.mpv"/>
<!-- UPower battery status (read-only) -->
<allow send_destination="org.freedesktop.UPower"
send_interface="org.freedesktop.DBus.Properties"
send_member="Get"/>
<allow send_destination="org.freedesktop.UPower"
send_interface="org.freedesktop.DBus.Properties"
send_member="GetAll"/>
<allow send_destination="org.freedesktop.UPower"
send_interface="org.freedesktop.UPower"
send_member="EnumerateDevices"/>
<allow send_destination="org.freedesktop.UPower"
send_interface="org.freedesktop.DBus.Introspectable"/>
<!-- bluez Bluetooth (read-only) -->
<allow send_destination="org.bluez"
send_interface="org.freedesktop.DBus.ObjectManager"
send_member="GetManagedObjects"/>
<allow send_destination="org.bluez"
send_interface="org.freedesktop.DBus.Properties"
send_member="Get"/>
<allow send_destination="org.bluez"
send_interface="org.freedesktop.DBus.Properties"
send_member="GetAll"/>
<allow send_destination="org.bluez"
send_interface="org.freedesktop.DBus.Introspectable"/>
<!-- NetworkManager (read-only) -->
<allow send_destination="org.freedesktop.NetworkManager"
send_interface="org.freedesktop.DBus.Properties"
send_member="Get"/>
<allow send_destination="org.freedesktop.NetworkManager"
send_interface="org.freedesktop.DBus.Properties"
send_member="GetAll"/>
<allow send_destination="org.freedesktop.NetworkManager"
send_interface="org.freedesktop.DBus.Introspectable"/>
<!-- systemd (list and read only) -->
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.systemd1.Manager"
send_member="ListUnits"/>
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.DBus.Properties"
send_member="Get"/>
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.DBus.Properties"
send_member="GetAll"/>
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.DBus.Introspectable"/>
</policy>
</busconfig>
```
### How it works
The policy engine evaluates rules top to bottom. The last matching rule wins. The pattern is: default deny everything, then selectively allow specific destinations, interfaces, and methods.
Each `<allow>` or `<deny>` element can filter on `send_destination` (service name), `send_interface`, `send_member` (method name), and `send_type` (message type). Combining attributes narrows the filter -- an allow with both `send_destination` and `send_member` only permits that specific method on that specific service.
<Aside type="note">
D-Bus policy files do not support wildcards in service names. You must add one `<allow>` block per MPRIS player. The example covers common players -- add more as needed.
</Aside>
### Deploy it
For the system bus:
```bash
sudo cp mcdbus.conf /etc/dbus-1/system.d/
sudo systemctl reload dbus.service
```
For `dbus-broker` instead of `dbus-daemon`:
```bash
sudo systemctl reload dbus-broker.service
```
### Test it
Denied messages appear in the bus daemon journal:
```bash
journalctl -u dbus-broker.service --since "5 min ago" | grep -i deny
```
---
## Layer 3: polkit rules
polkit provides per-action authorization. Where D-Bus bus policy controls which messages can be sent, polkit controls whether a specific action is authorized for a specific user. Many system services (systemd, NetworkManager, UPower, udisks2) check polkit before performing privileged operations.
### The rules file
```javascript
polkit.addRule(function(action, subject) {
// Only apply to users in the mcdbus group
if (!subject.isInGroup("mcdbus")) {
return polkit.Result.NOT_HANDLED;
}
// Read-only systemd operations
var systemdReadActions = [
"org.freedesktop.systemd1.manage-units",
"org.freedesktop.systemd1.reload-daemon"
];
// NetworkManager: allow reading connection and device info
var nmReadActions = [
"org.freedesktop.NetworkManager.network-control",
"org.freedesktop.NetworkManager.settings.modify.system"
];
// UPower: allow reading battery status
var upowerActions = [
"org.freedesktop.upower.get-history",
"org.freedesktop.upower.get-statistics"
];
// UDisks2: allow reading disk info
var udisksReadActions = [
"org.freedesktop.udisks2.ata-smart-selftest"
];
if (systemdReadActions.indexOf(action.id) >= 0) {
if (subject.local && subject.active) {
return polkit.Result.YES;
}
}
if (nmReadActions.indexOf(action.id) >= 0) {
if (subject.local && subject.active) {
return polkit.Result.YES;
}
}
if (upowerActions.indexOf(action.id) >= 0) {
return polkit.Result.YES;
}
if (udisksReadActions.indexOf(action.id) >= 0) {
if (subject.local && subject.active) {
return polkit.Result.YES;
}
}
return polkit.Result.NOT_HANDLED;
});
```
### Create the group and deploy
```bash
sudo groupadd mcdbus
sudo usermod -aG mcdbus $(whoami)
# Log out and back in for the group membership to take effect
sudo cp 50-mcdbus.rules /etc/polkit-1/rules.d/
sudo systemctl restart polkit.service
```
<Aside type="caution">
If running mcdbus under systemd with `DynamicUser=yes`, the dynamic user is not in any supplementary groups by default. Either add `SupplementaryGroups=mcdbus` to the unit file, or switch to `User=mcdbus` with a real system account.
</Aside>
### Test it
```bash
pkcheck --action-id org.freedesktop.systemd1.manage-units \
--process $$ --allow-user-interaction
```
List all registered polkit actions to find the ones relevant to your deployment:
```bash
pkaction | grep -E 'systemd|NetworkManager|UPower|udisks'
```
---
## Layer 4: xdg-dbus-proxy
`xdg-dbus-proxy` (part of Flatpak) creates a filtered D-Bus socket. You point it at the real bus socket, declare which services to expose, and it creates a new socket that only passes through matching messages. mcdbus connects to the proxy socket and never sees the rest of the bus.
### The proxy script
```bash
#!/usr/bin/env bash
set -euo pipefail
MCDBUS_CMD="${MCDBUS_CMD:-mcdbus}"
if ! command -v xdg-dbus-proxy &>/dev/null; then
echo "error: xdg-dbus-proxy not found. Install it first." >&2
exit 1
fi
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
echo "error: DBUS_SESSION_BUS_ADDRESS is not set." >&2
exit 1
fi
PROXY_DIR="$(mktemp -d /tmp/mcdbus-proxy.XXXXXX)"
PROXY_SOCKET="${PROXY_DIR}/bus"
cleanup() {
if [ -n "${PROXY_PID:-}" ]; then
kill "$PROXY_PID" 2>/dev/null || true
wait "$PROXY_PID" 2>/dev/null || true
fi
rm -rf "$PROXY_DIR"
}
trap cleanup EXIT INT TERM
xdg-dbus-proxy "$DBUS_SESSION_BUS_ADDRESS" "$PROXY_SOCKET" \
--filter \
--talk=org.freedesktop.Notifications \
--talk=org.mpris.MediaPlayer2 \
--see=org.freedesktop.UPower \
--see=org.freedesktop.NetworkManager \
--see=org.bluez \
--see=org.freedesktop.systemd1 \
--talk=org.kde.KWin \
&
PROXY_PID=$!
for i in $(seq 1 50); do
if [ -e "$PROXY_SOCKET" ]; then
break
fi
sleep 0.1
done
if [ ! -e "$PROXY_SOCKET" ]; then
echo "error: proxy socket did not appear at $PROXY_SOCKET" >&2
exit 1
fi
export DBUS_SESSION_BUS_ADDRESS="unix:path=${PROXY_SOCKET}"
exec $MCDBUS_CMD
```
### Policy levels
xdg-dbus-proxy supports three levels of access per service:
| Level | Effect |
|-------|--------|
| `--see=NAME` | Service appears in `ListNames` and can be introspected, but method calls are blocked. Read-only visibility. |
| `--talk=NAME` | Full bidirectional communication. Method calls, property reads and writes, and signals all pass through. |
| `--own=NAME` | The client can register (own) the given bus name. mcdbus does not need this. |
You can also filter at the interface and path level:
```bash
--call=org.freedesktop.UPower=org.freedesktop.DBus.Properties.GetAll@/org/freedesktop/UPower/*
```
### Install and deploy
<Tabs>
<TabItem label="Arch">
```bash
sudo pacman -S xdg-dbus-proxy
```
</TabItem>
<TabItem label="Debian / Ubuntu">
```bash
sudo apt install xdg-dbus-proxy
```
</TabItem>
<TabItem label="Fedora">
```bash
sudo dnf install xdg-dbus-proxy
```
</TabItem>
</Tabs>
```bash
chmod +x mcdbus-proxy.sh
./mcdbus-proxy.sh
```
Or integrate it into the systemd unit:
```ini
[Service]
ExecStart=/usr/local/bin/mcdbus-proxy.sh
```
---
## Choosing layers
**Layer 1 alone** is sufficient for most deployments. It costs nothing to deploy and provides broad process-level isolation.
**Layers 1 + 2 + 3** together provide defense in depth for high-security environments or shared systems. The bus policy restricts which services mcdbus can talk to, and polkit controls which privileged operations are authorized.
**Layer 4** fills the gap when you need fine-grained session bus filtering that the other layers cannot express. The trade-off is an additional proxy process.
All four layers are independent. Deploy whichever combination matches your threat model.

View File

@ -0,0 +1,121 @@
---
title: Check Battery and Network
description: Read battery charge level and network connection status using mcdbus's shortcut tools for UPower and NetworkManager.
---
import { Aside, Code } from '@astrojs/starlight/components';
mcdbus includes two read-only shortcut tools for checking hardware status: `battery_status` reads from UPower on the system bus, and `network_status` reads from NetworkManager on the system bus. Both tools only read properties -- they do not change any state and require no confirmation prompt.
## Battery status
> "What's my battery level?"
```
battery_status()
```
The tool enumerates all power devices through UPower, filters for battery-type devices (type 2 in the UPower spec), and returns a summary for each battery found:
```markdown
## Battery Status
### DELL XXXX
- **Charge:** 73%
- **State:** Discharging
- **Time:** 2h 14m remaining
- **Power draw:** 12.3 W
```
### What it reads
The tool calls `org.freedesktop.UPower.EnumerateDevices` to find all power devices, then reads `org.freedesktop.UPower.Device` properties on each one. The fields reported are:
| Field | Source property | Notes |
|-------|---------------|-------|
| Charge | `Percentage` | 0-100 float |
| State | `State` | Mapped from integer: 1=Charging, 2=Discharging, 3=Empty, 4=Fully charged, 5=Pending charge, 6=Pending discharge |
| Time remaining | `TimeToEmpty` | Seconds, shown as hours and minutes when discharging |
| Time to full | `TimeToFull` | Seconds, shown when charging |
| Power draw | `EnergyRate` | Watts |
| Model | `Model` | Battery model string from the hardware |
### Reading additional battery properties
The shortcut tool shows the most useful fields. For deeper information (voltage, energy capacity, charge cycles, technology), use `get_all_properties` on the battery device path:
```
get_all_properties(
bus="system",
service="org.freedesktop.UPower",
object_path="/org/freedesktop/UPower/devices/battery_BAT0",
interface="org.freedesktop.UPower.Device"
)
```
<Aside type="tip">
The device path varies by hardware. If `battery_BAT0` does not exist on your system, use `list_objects` on `org.freedesktop.UPower` to find the correct path. The battery device is typically under `/org/freedesktop/UPower/devices/`.
</Aside>
### No battery found
On desktop machines without a battery, `battery_status` returns "No batteries found." This is normal -- UPower may still report other device types like line power or UPS units, but the tool filters for batteries specifically.
## Network status
> "Am I connected to the internet?"
```
network_status()
```
The tool reads properties from `org.freedesktop.NetworkManager` on the system bus and returns overall connectivity state, wireless status, and a table of active connections:
```markdown
## Network Status
- **State:** Connected (global)
- **Wireless:** enabled
| Connection | Type | State |
|------------|------|-------|
| Ethernet 1 | 802-3-ethernet | Activated |
| MyWiFi | 802-11-wireless | Activated |
```
### What it reads
The tool calls `Properties.GetAll` on the `org.freedesktop.NetworkManager` interface to get the top-level state, then iterates over `ActiveConnections` to read each connection's properties.
**Top-level properties:**
| Field | Source property | Notes |
|-------|---------------|-------|
| State | `State` | Integer mapped to: 0=Unknown, 10=Asleep, 20=Disconnected, 30=Disconnecting, 40=Connecting, 50=Connected (local), 60=Connected (site), 70=Connected (global) |
| Wireless | `WirelessEnabled` | Boolean |
**Per-connection properties** (read from `org.freedesktop.NetworkManager.Connection.Active` on each active connection object path):
| Field | Source property |
|-------|---------------|
| Connection | `Id` |
| Type | `Type` |
| State | `State` (mapped: 1=Activating, 2=Activated, 3=Deactivating, 4=Deactivated) |
### Reading more network details
For detailed information about a specific network device (IP addresses, DNS servers, signal strength for wireless), use `get_all_properties` on the device's object path. Network devices live under `/org/freedesktop/NetworkManager/Devices/`:
```
list_objects(
bus="system",
service="org.freedesktop.NetworkManager",
root_path="/org/freedesktop/NetworkManager/Devices"
)
```
Then introspect a specific device to see which interfaces it exposes (Wired, Wireless, Statistics) and read their properties.
<Aside type="note">
Both `battery_status` and `network_status` require their respective system services to be running. If UPower or NetworkManager is not installed or not active, the tools return an error message indicating the service is not available.
</Aside>

View File

@ -0,0 +1,148 @@
---
title: Manage Systemd Units
description: List and inspect systemd services, timers, and sockets using mcdbus's list_systemd_units tool and raw D-Bus introspection.
---
import { Aside, Steps, Code } from '@astrojs/starlight/components';
systemd exposes its manager interface on the system D-Bus bus at `org.freedesktop.systemd1`. The `list_systemd_units` shortcut tool wraps the `ListUnits` method and returns a markdown table you can filter with glob patterns.
## Listing units
The simplest call lists every loaded unit:
> "Show me all systemd services."
```
list_systemd_units(bus="system")
```
This returns a table like:
| Unit | Load | Active | Sub | Description |
|------|------|--------|-----|-------------|
| `dbus.service` | loaded | active | running | D-Bus System Message Bus |
| `docker.service` | loaded | active | running | Docker Application Container Engine |
| `sshd.service` | loaded | active | running | OpenSSH Daemon |
### Filtering with glob patterns
The `pattern` parameter accepts standard glob syntax. It matches against the unit name column.
> "Which Docker-related units are loaded?"
```
list_systemd_units(bus="system", pattern="docker*")
```
> "Show me all timer units."
```
list_systemd_units(bus="system", pattern="*.timer")
```
> "Are there any failed services?"
```
list_systemd_units(bus="system", pattern="*.service")
```
Claude reads the returned table and filters the Active column for "failed" entries.
### User session units
systemd also manages per-user services on the session bus. Pass `bus="session"` to see them:
> "List my user-level systemd services."
```
list_systemd_units(bus="session", pattern="*.service")
```
This shows units managed by `systemctl --user` -- things like Pipewire, the XDG desktop portal, and any user-installed services.
## Reading unit properties
Each systemd unit has an object path under `/org/freedesktop/systemd1/unit/`. To get detailed properties for a specific unit, use `get_all_properties` on its object path.
<Steps>
1. **Find the unit's object path.** The path encodes the unit name with underscores replacing special characters. For `docker.service`, the path is `/org/freedesktop/systemd1/unit/docker_2eservice`.
2. **Read its properties.** Ask Claude to read all properties on the `org.freedesktop.systemd1.Unit` interface:
```
get_all_properties(
bus="system",
service="org.freedesktop.systemd1",
object_path="/org/freedesktop/systemd1/unit/docker_2eservice",
interface="org.freedesktop.systemd1.Unit"
)
```
3. **Interpret the results.** The properties include `ActiveState`, `SubState`, `LoadState`, `Description`, `ActiveEnterTimestamp`, `InactiveEnterTimestamp`, `MainPID`, memory and CPU accounting values, and the full dependency graph (`Requires`, `After`, `WantedBy`).
</Steps>
<Aside type="tip">
If you are unsure of the exact object path encoding, use `list_objects` to walk the systemd object tree:
```
list_objects(bus="system", service="org.freedesktop.systemd1", root_path="/org/freedesktop/systemd1/unit")
```
This returns all unit object paths. The tree walk is bounded to 500 nodes and 60 seconds, which is usually enough for the unit subtree.
</Aside>
## Inspecting the manager interface
The systemd manager itself sits at `/org/freedesktop/systemd1` and exposes methods for starting, stopping, restarting, and reloading units. To see everything available:
> "What methods does the systemd manager expose?"
```
introspect(
bus="system",
service="org.freedesktop.systemd1",
object_path="/org/freedesktop/systemd1",
interface_filter="org.freedesktop.systemd1.Manager"
)
```
This returns the full list of methods (StartUnit, StopUnit, RestartUnit, ReloadUnit, ListUnitFiles, and many more), properties (Version, Features, Virtualization), and signals (UnitNew, UnitRemoved, JobNew).
## Calling systemd methods
Because systemd lives on the system bus, `call_method` triggers a confirmation prompt before execution. This protects against accidental service disruption.
> "Restart the nginx service."
```
call_method(
bus="system",
service="org.freedesktop.systemd1",
object_path="/org/freedesktop/systemd1",
interface="org.freedesktop.systemd1.Manager",
method="RestartUnit",
args='["nginx.service", "replace"]',
signature="ss"
)
```
Claude presents the confirmation prompt. After you approve, systemd restarts the unit and returns the job object path.
<Aside type="caution">
Starting, stopping, and restarting units may also require polkit authorization depending on your system configuration. If the call fails with an authorization error, see the [Lock Down Permissions](/guides/permissions/) guide for configuring polkit rules.
</Aside>
## Common patterns
**Find all failed services and read their status:**
Ask Claude to list all `.service` units, identify any with `Active=failed`, then read their properties to see the `Result`, `ExecMainStatus`, and log output hints.
**Check when a service last restarted:**
Read the `ActiveEnterTimestamp` property on the unit's object path. systemd reports this as a microsecond Unix timestamp.
**List all enabled timers and their next trigger time:**
List units matching `*.timer`, then for each timer read the `NextElapseUSecRealtime` property from the `org.freedesktop.systemd1.Timer` interface on the timer's object path.

View File

@ -0,0 +1,55 @@
---
title: mcdbus
description: D-Bus MCP server — give Claude access to your Linux desktop bus
template: splash
hero:
tagline: Bridge Linux IPC into the Model Context Protocol. Discover and interact with session and system D-Bus services through introspection-first discovery.
actions:
- text: Get Started
link: /start-here/overview/
icon: right-arrow
- text: View on PyPI
link: https://pypi.org/project/mcdbus/
variant: minimal
---
import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components';
```bash
# Install and register with Claude Code in one shot
claude mcp add mcdbus -- uvx mcdbus
```
No configuration files, no service definitions, no API keys. The server discovers what is available on your D-Bus at runtime.
<CardGrid>
<Card title="Discovery" icon="magnifier">
`list_services`, `introspect`, and `list_objects` map out your bus. Start from nothing and build a complete picture of any service — its object tree, interfaces, methods, properties, and signals.
</Card>
<Card title="Interaction" icon="rocket">
`call_method`, `get_property`, `set_property`, and `get_all_properties` let Claude read state and invoke operations on any D-Bus service. System bus calls and property mutations require confirmation.
</Card>
<Card title="Shortcuts" icon="setting">
Pre-wired tools for common operations: desktop notifications, MPRIS media player control, systemd unit listing, NetworkManager status, UPower battery state, bluez Bluetooth devices, and KDE KWin window listing.
</Card>
</CardGrid>
<CardGrid>
<LinkCard
title="Installation"
description="pip, uvx, or from source — plus Claude Code registration."
href="/start-here/installation/"
/>
<LinkCard
title="First Steps"
description="Walk through your first D-Bus exploration with mcdbus."
href="/start-here/first-steps/"
/>
<LinkCard
title="Tool Reference"
description="Full documentation for all 16 tools, 3 resources, and 2 prompts."
href="/reference/discovery-tools/"
/>
</CardGrid>

View File

@ -0,0 +1,148 @@
---
title: Discovery Tools
description: Tools for listing D-Bus services, introspecting objects, and walking object trees.
---
The discovery tools form the first step in any D-Bus interaction. They answer the questions "what services exist?", "what does this object expose?", and "what objects does this service own?".
## list_services
List well-known D-Bus service names on a bus.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `bus` | `string` | *required* | `"session"` or `"system"` |
| `include_unique` | `bool` | `false` | Include unique connection names like `:1.42` |
### Return format
Markdown list with a count header, sorted alphabetically.
```markdown
## D-Bus session bus — 47 services
- `org.freedesktop.DBus`
- `org.freedesktop.Notifications`
- `org.freedesktop.portal.Desktop`
- `org.kde.KWin`
- `org.mpris.MediaPlayer2.firefox`
```
When `include_unique` is `true`, unique connection names (those starting with `:`) are included in the list alongside well-known names.
### Example
```
list_services(bus="session")
```
---
## introspect
Introspect a D-Bus object to see its interfaces, methods, properties, and signals. Reports progress from 0 to 4 as it connects, introspects, formats, and completes.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `bus` | `string` | *required* | `"session"` or `"system"` |
| `service` | `string` | *required* | Service name, e.g. `"org.freedesktop.Notifications"` |
| `object_path` | `string` | *required* | Object path, e.g. `"/org/freedesktop/Notifications"` |
| `include_standard` | `bool` | `false` | Include standard D-Bus interfaces (Peer, Introspectable, Properties, ObjectManager) |
### Return format
Markdown document listing each interface with its methods (including in/out argument signatures), properties (with D-Bus type signature and access mode), signals, and child nodes.
```markdown
## `org.freedesktop.Notifications` at `/org/freedesktop/Notifications`
### Interface: `org.freedesktop.Notifications`
**Methods:**
- `GetCapabilities() -> capabilities: as`
- `Notify(app_name: s, replaces_id: u, app_icon: s, summary: s, body: s, actions: as, hints: a{sv}, expire_timeout: i) -> id: u`
- `CloseNotification(id: u)`
- `GetServerInformation() -> name: s, vendor: s, version: s, spec_version: s`
**Signals:**
- `NotificationClosed(id: u, reason: u)`
- `ActionInvoked(id: u, action_key: s)`
```
Standard interfaces (`org.freedesktop.DBus.Peer`, `org.freedesktop.DBus.Introspectable`, `org.freedesktop.DBus.Properties`, `org.freedesktop.DBus.ObjectManager`) are hidden by default. Set `include_standard` to `true` to show them.
### Example
```
introspect(
bus="session",
service="org.freedesktop.Notifications",
object_path="/org/freedesktop/Notifications"
)
```
---
## list_objects
Recursively walk the D-Bus object tree for a service using breadth-first search, bounded by depth and node count.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `bus` | `string` | *required* | `"session"` or `"system"` |
| `service` | `string` | *required* | Service name |
| `root_path` | `string` | `"/"` | Starting path for the walk |
| `max_depth` | `int` | `20` | Maximum tree depth to descend |
### Bounds
The walk enforces two safety limits:
| Limit | Value | Behavior when hit |
|-------|-------|-------------------|
| `MAX_TREE_NODES` | 500 | Walk stops, output marked as truncated |
| `TOTAL_WALK_TIMEOUT` | 60 seconds | Walk stops with a warning logged via context |
Progress is reported as `(nodes_visited, None)` during the walk, so clients that support progress tracking can display a counter.
### Return format
Sorted list of object paths. Each path shows its non-standard interfaces (standard `org.freedesktop.DBus.*` interfaces are omitted for clarity). When limits are hit, the header indicates truncation.
```markdown
## Object tree for `org.freedesktop.systemd1` on system bus — 412 objects
- `/` -- `org.freedesktop.systemd1.Manager`
- `/org/freedesktop/systemd1/job/12345`
- `/org/freedesktop/systemd1/unit/bluetooth_2eservice` -- `org.freedesktop.systemd1.Unit`, `org.freedesktop.systemd1.Service`
- `/org/freedesktop/systemd1/unit/docker_2eservice` -- `org.freedesktop.systemd1.Unit`, `org.freedesktop.systemd1.Service`
```
When truncated:
```markdown
## Object tree for `org.bluez` on system bus — 500 objects (truncated at 500)
```
### Example
```
list_objects(bus="system", service="org.freedesktop.systemd1")
```
Walk a subtree to narrow results:
```
list_objects(
bus="system",
service="org.freedesktop.systemd1",
root_path="/org/freedesktop/systemd1/unit",
max_depth=2
)
```

View File

@ -0,0 +1,161 @@
---
title: Environment Variables
description: Configuration variables that control mcdbus server behavior, confirmation flow, and timeouts.
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
## Summary
| Variable | Default | Purpose |
|----------|---------|---------|
| `MCDBUS_NOTIFY_CONFIRM` | unset | Enable desktop notification fallback for confirmations |
| `MCDBUS_REQUIRE_ELICITATION` | unset | Hard-fail if MCP elicitation is unavailable |
| `MCDBUS_TIMEOUT` | `30` | Timeout in seconds for individual D-Bus method calls |
---
## MCDBUS_NOTIFY_CONFIRM
**Default:** unset (disabled)
Enables a desktop notification fallback when the MCP client does not support the elicitation protocol. When set to any non-empty value, confirmation-gated operations (system bus method calls and all property mutations) will display a desktop notification with **Approve** and **Deny** action buttons instead of failing or proceeding silently.
### How it works
1. A tool requests user confirmation via `ctx.elicit()`
2. The MCP client does not support elicitation (returns `CancelledElicitation` or throws)
3. mcdbus sends an `org.freedesktop.Notifications.Notify` call on the session bus with:
- Urgency level `critical` (value `2`)
- Two action buttons: Approve and Deny
- Icon: `dialog-warning`
4. The server subscribes to `ActionInvoked` and `NotificationClosed` signals
5. If the user clicks **Approve** within 60 seconds, the operation proceeds
6. If the user clicks **Deny**, dismisses the notification, or 60 seconds elapse, the operation is denied
7. Match rules and the notification are cleaned up regardless of outcome
If the notification service itself is unreachable (e.g. headless server, no desktop session), the fallback fails gracefully: a warning is logged to stderr and the operation proceeds.
### Configuration
<Tabs>
<TabItem label="Claude Code">
```sh
claude mcp add -e MCDBUS_NOTIFY_CONFIRM=1 mcdbus -- uvx mcdbus
```
</TabItem>
<TabItem label="JSON config">
```json
{
"mcpServers": {
"mcdbus": {
"command": "uvx",
"args": ["mcdbus"],
"env": {
"MCDBUS_NOTIFY_CONFIRM": "1"
}
}
}
}
```
</TabItem>
</Tabs>
---
## MCDBUS_REQUIRE_ELICITATION
**Default:** unset (disabled)
When set to any non-empty value, mcdbus will raise a `ToolError` if the MCP client does not support the elicitation protocol. No fallback methods are attempted -- not desktop notifications, not silent proceed.
This variable takes precedence over `MCDBUS_NOTIFY_CONFIRM`. If both are set, `MCDBUS_REQUIRE_ELICITATION` wins and the notification fallback is never tried.
### Use case
Deployments where unconfirmed state-changing operations must never proceed. If you are running mcdbus in an environment where a human must explicitly approve every system bus call and property mutation, set this variable to guarantee that requirement is enforced at the protocol level.
### Configuration
<Tabs>
<TabItem label="Claude Code">
```sh
claude mcp add -e MCDBUS_REQUIRE_ELICITATION=1 mcdbus -- uvx mcdbus
```
</TabItem>
<TabItem label="JSON config">
```json
{
"mcpServers": {
"mcdbus": {
"command": "uvx",
"args": ["mcdbus"],
"env": {
"MCDBUS_REQUIRE_ELICITATION": "1"
}
}
}
}
```
</TabItem>
</Tabs>
### Precedence
| `MCDBUS_REQUIRE_ELICITATION` | `MCDBUS_NOTIFY_CONFIRM` | Behavior when elicitation unavailable |
|-----|-----|-----|
| unset | unset | Warning logged to stderr, operation proceeds |
| unset | set | Desktop notification fallback attempted |
| set | unset | `ToolError` raised |
| set | set | `ToolError` raised (require wins) |
---
## MCDBUS_TIMEOUT
**Default:** `30` (seconds)
Sets the timeout for individual D-Bus method calls made through `call_bus_method`. This applies to all tool invocations that make D-Bus calls, including discovery, interaction, and shortcut tools.
The value is parsed as a float, so fractional seconds are supported (e.g. `"2.5"`).
### Relationship to other timeouts
The `list_objects` tool enforces its own `TOTAL_WALK_TIMEOUT` of 60 seconds for the entire breadth-first walk. This is a wall-clock deadline for the full tree traversal, separate from the per-call timeout. Each individual `introspect` call within the walk still respects `MCDBUS_TIMEOUT`. A slow service that takes 25 seconds per introspection call would hit the walk timeout after two or three nodes even though no single call timed out.
### Configuration
<Tabs>
<TabItem label="Claude Code">
```sh
claude mcp add -e MCDBUS_TIMEOUT=10 mcdbus -- uvx mcdbus
```
</TabItem>
<TabItem label="JSON config">
```json
{
"mcpServers": {
"mcdbus": {
"command": "uvx",
"args": ["mcdbus"],
"env": {
"MCDBUS_TIMEOUT": "10"
}
}
}
}
```
</TabItem>
</Tabs>

View File

@ -0,0 +1,241 @@
---
title: Interaction Tools
description: Tools for calling D-Bus methods, reading properties, and setting property values.
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
The interaction tools operate on specific D-Bus objects once you know their service name, object path, and interface. Use the [discovery tools](/reference/discovery-tools/) to find those coordinates first.
## call_method
Call a D-Bus method and return the result.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `bus` | `string` | *required* | `"session"` or `"system"` |
| `service` | `string` | *required* | Service name, e.g. `"org.freedesktop.Notifications"` |
| `object_path` | `string` | *required* | Object path, e.g. `"/org/freedesktop/Notifications"` |
| `interface` | `string` | *required* | Interface name, e.g. `"org.freedesktop.Notifications"` |
| `method` | `string` | *required* | Method name, e.g. `"Notify"` |
| `args` | `string` | `"[]"` | JSON array of arguments |
| `signature` | `string` | `""` | D-Bus type signature string; leave empty for no-arg methods |
### Confirmation
System bus calls require user confirmation before execution. Session bus calls proceed without prompting (they operate within user scope). See [Confirmation Flow](/explanation/confirmation-flow/) for details on how confirmation is resolved.
### Argument parsing
The `args` parameter accepts a JSON array string. Each element is deserialized according to the corresponding type in the `signature` string.
For Variant (`v`) signatures, two modes are supported:
<Tabs>
<TabItem label="Auto-wrapping">
Simple Python types are automatically wrapped into D-Bus Variants:
| Python type | Inferred signature |
|-------------|--------------------|
| `bool` | `"b"` |
| `str` | `"s"` |
| `int` | `"i"` |
| `float` | `"d"` |
| `list` | `"as"` (array of strings) |
| `dict` | `"a{sv}"` (dict of string to variant) |
```json
["hello", true, 42]
```
</TabItem>
<TabItem label="Explicit Variant">
For complex variant values where auto-inference would produce the wrong type, pass an object with `signature` and `value` keys:
```json
[{"signature": "ai", "value": [1, 2, 3]}]
```
This wraps the array as `Variant("ai", [1, 2, 3])` rather than the auto-inferred `Variant("as", [1, 2, 3])`.
</TabItem>
</Tabs>
### Return format
- Single-element responses are unwrapped from their containing array for cleaner output
- Multi-element responses are returned as a JSON array
- Void methods return the string `"Method returned no data (void)."`
- All values are JSON-formatted with 2-space indentation
### Examples
Send a desktop notification:
```
call_method(
bus="session",
service="org.freedesktop.Notifications",
object_path="/org/freedesktop/Notifications",
interface="org.freedesktop.Notifications",
method="Notify",
args='["mcdbus", 0, "", "Hello", "World", [], {}, 5000]',
signature="susssasa{sv}i"
)
```
Call a no-arg method:
```
call_method(
bus="session",
service="org.freedesktop.Notifications",
object_path="/org/freedesktop/Notifications",
interface="org.freedesktop.Notifications",
method="GetServerInformation"
)
```
---
## get_property
Read a single D-Bus property value.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `bus` | `string` | *required* | `"session"` or `"system"` |
| `service` | `string` | *required* | Service name |
| `object_path` | `string` | *required* | Object path |
| `interface` | `string` | *required* | Interface that owns the property |
| `property_name` | `string` | *required* | Property name to read |
### How it works
Calls `org.freedesktop.DBus.Properties.Get` with the specified interface and property name. The returned Variant is unwrapped and JSON-formatted.
### Return format
JSON-formatted property value with 2-space indentation. Returns `"No value returned."` if the property read yields no data.
### Example
```
get_property(
bus="session",
service="org.mpris.MediaPlayer2.firefox",
object_path="/org/mpris/MediaPlayer2",
interface="org.mpris.MediaPlayer2.Player",
property_name="PlaybackStatus"
)
```
Returns:
```json
"Playing"
```
---
## set_property
Set a D-Bus property value.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `bus` | `string` | *required* | `"session"` or `"system"` |
| `service` | `string` | *required* | Service name |
| `object_path` | `string` | *required* | Object path |
| `interface` | `string` | *required* | Interface that owns the property |
| `property_name` | `string` | *required* | Property name to set |
| `value` | `string` | *required* | JSON-encoded value |
| `signature` | `string` | *required* | D-Bus type signature of the property value (e.g. `"b"` for boolean) |
### Confirmation
This tool **always** requires user confirmation regardless of which bus is targeted. Property mutations are state-changing operations and the confirmation prompt is not skippable. See [Confirmation Flow](/explanation/confirmation-flow/) for the full resolution chain.
### How it works
1. The `value` JSON string is parsed
2. The parsed value is wrapped in a `Variant` with the provided `signature`
3. After confirmation, `org.freedesktop.DBus.Properties.Set` is called
4. The operation is audit-logged to stderr with the bus, service, interface, property name, value, and signature
### Return format
Confirmation string: `Set {interface}.{property_name} = {value}`
### Example
```
set_property(
bus="session",
service="org.mpris.MediaPlayer2.firefox",
object_path="/org/mpris/MediaPlayer2",
interface="org.mpris.MediaPlayer2.Player",
property_name="Volume",
value="0.5",
signature="d"
)
```
---
## get_all_properties
Read all properties on a D-Bus interface.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `bus` | `string` | *required* | `"session"` or `"system"` |
| `service` | `string` | *required* | Service name |
| `object_path` | `string` | *required* | Object path |
| `interface` | `string` | *required* | Interface to read properties from |
### How it works
Calls `org.freedesktop.DBus.Properties.GetAll` with the specified interface name. Returns all properties as a formatted table.
### Return format
Markdown table with Property and Value columns, sorted alphabetically by property name. Values are JSON-serialized and truncated at 80 characters to keep the table readable.
```markdown
## Properties of `org.mpris.MediaPlayer2.Player` at `/org/mpris/MediaPlayer2`
| Property | Value |
|----------|-------|
| `CanControl` | `true` |
| `CanGoNext` | `true` |
| `CanGoPrevious` | `true` |
| `CanPause` | `true` |
| `CanPlay` | `true` |
| `Metadata` | `{"xesam:title": "Starman", "xesam:artist": ["David Bowie"], "mpris:le...` |
| `PlaybackStatus` | `"Playing"` |
| `Volume` | `1.0` |
```
Returns `"No properties found."` if the interface exposes no properties.
### Example
```
get_all_properties(
bus="session",
service="org.mpris.MediaPlayer2.firefox",
object_path="/org/mpris/MediaPlayer2",
interface="org.mpris.MediaPlayer2.Player"
)
```

View File

@ -0,0 +1,67 @@
---
title: Prompts
description: MCP prompt templates for guided D-Bus exploration and debugging workflows.
---
mcdbus provides two prompt templates that generate step-by-step instructions for common workflows. Prompts do not execute tools directly -- they produce text that guides the model through a sequence of tool calls.
---
## explore_service
Generate a walkthrough for exploring a D-Bus service or an entire bus.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `bus` | `string` | `"session"` | `"session"` or `"system"` |
| `service` | `string` | `""` | Service name to explore; omit for a full bus walkthrough |
### Without a service (5 steps)
When `service` is empty, the prompt generates a full exploration starting from the bus level:
1. `list_services(bus="{bus}")` to see available services
2. Pick a service and use `list_objects()` to see its object tree
3. `introspect()` on interesting objects to see methods, properties, and signals
4. `get_all_properties()` to read current state
5. `call_method()` to interact with methods
### With a service (4 steps)
When a specific `service` is provided, the prompt skips service listing and starts from the object tree:
1. `list_objects(bus="{bus}", service="{service}")` to see the object tree
2. `introspect()` on interesting objects to see methods, properties, and signals
3. `get_all_properties()` to read current state
4. `call_method()` to interact with methods
### When to use
First encounter with an unfamiliar D-Bus service. The prompt provides a structured approach to understanding what a service exposes before interacting with it.
---
## debug_service
Generate a diagnostic walkthrough for troubleshooting a D-Bus service.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `service` | `string` | *required* | Service name to debug |
| `bus` | `string` | `"session"` | `"session"` or `"system"` |
### Generated steps (5)
1. **Check registration** -- `list_services(bus="{bus}")` to verify the service appears on the bus
2. **Introspect root** -- `introspect(bus="{bus}", service="{service}", object_path="/")` to see the top-level structure
3. **Walk the object tree** -- find all endpoints the service exposes
4. **Read properties** -- read all properties on key interfaces to check state
5. **Ping** -- call `Ping` on `org.freedesktop.DBus.Peer` to verify the service is responsive
### When to use
Troubleshooting a service that is not responding as expected, returning errors, or that you suspect may not be running. The diagnostic sequence moves from coarse (is it registered?) to fine (does it respond to pings?), helping isolate where the problem lies.

View File

@ -0,0 +1,127 @@
---
title: Resources
description: MCP resource templates for browsing D-Bus services, object trees, and interfaces.
---
mcdbus exposes three MCP resource templates that provide read-only snapshots of D-Bus structure. Resources use a `dbus://` URI scheme and return plain-text, newline-separated data.
## When to use resources vs tools
**Resources** are best for loading context into a conversation. An MCP client can attach a resource URI and the server returns the current state as a text block. Resources are stateless, read-only, and produce compact output suitable for context windows.
**Tools** are better for interactive exploration where you need formatted output, progress reporting, filtering options, or write operations. The `list_services` tool, for example, returns a Markdown-formatted list with a count header, while the `dbus://{bus}/services` resource returns raw service names one per line.
Use resources when you want to seed a conversation with structural context. Use tools when you need to drill into specifics or take action.
---
## dbus://\{bus\}/services
Live list of well-known service names on a D-Bus bus.
### Template parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `bus` | `string` | `"session"` or `"system"` |
### Behavior
Queries `org.freedesktop.DBus.ListNames` on each access. Unique connection names (those starting with `:`) are filtered out. The result is sorted alphabetically.
### Return format
Newline-separated list of well-known service names.
```
org.freedesktop.DBus
org.freedesktop.Notifications
org.freedesktop.portal.Desktop
org.kde.KWin
org.mpris.MediaPlayer2.firefox
```
### Example URI
```
dbus://session/services
dbus://system/services
```
---
## dbus://\{bus\}/\{service\}/objects
Object tree for a D-Bus service, produced by a bounded breadth-first walk.
### Template parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `bus` | `string` | `"session"` or `"system"` |
| `service` | `string` | Well-known service name |
### Bounds
| Limit | Value |
|-------|-------|
| Maximum nodes | 200 |
| Maximum depth | 15 |
These limits are tighter than the `list_objects` tool (which allows 500 nodes and depth 20) because resources are intended for context loading, not exhaustive enumeration.
### Return format
Newline-separated, sorted list of object paths.
```
/
/org/freedesktop/Notifications
```
### Example URI
```
dbus://session/org.freedesktop.Notifications/objects
dbus://system/org.freedesktop.systemd1/objects
```
---
## dbus://\{bus\}/\{service\}/\{path\}/interfaces
List of interfaces available at a specific D-Bus object path.
### Template parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `bus` | `string` | `"session"` or `"system"` |
| `service` | `string` | Well-known service name |
| `path` | `string` | URL-encoded object path |
### Path encoding
The `path` segment must be URL-encoded. Forward slashes become `%2F`. The server automatically URL-decodes the path and prepends a leading `/` if one is missing after decoding.
| Object path | Encoded `path` segment |
|-------------|----------------------|
| `/org/freedesktop/Notifications` | `%2Forg%2Ffreedesktop%2FNotifications` |
| `/` | `%2F` |
### Return format
Newline-separated list of interface names, including standard D-Bus interfaces.
```
org.freedesktop.DBus.Peer
org.freedesktop.DBus.Introspectable
org.freedesktop.DBus.Properties
org.freedesktop.Notifications
```
### Example URI
```
dbus://session/org.freedesktop.Notifications/%2Forg%2Ffreedesktop%2FNotifications/interfaces
```

View File

@ -0,0 +1,228 @@
---
title: Shortcut Tools
description: Pre-wired convenience tools for common D-Bus operations that skip the discovery step.
---
Shortcut tools wrap common D-Bus interactions into single calls with friendly parameters. They target specific well-known services and handle the service name, object path, interface, and method details internally.
Each shortcut could be replicated using `call_method`, `get_property`, and `get_all_properties` from the [interaction tools](/reference/interaction-tools/), but shortcuts save the discovery round-trip and produce formatted output.
---
## send_notification
Send a desktop notification via the freedesktop Notifications service on the session bus.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `summary` | `string` | *required* | Notification title |
| `body` | `string` | `""` | Notification body text |
| `icon` | `string` | `""` | Icon name or file path (empty for default) |
| `timeout` | `int` | `5000` | Display duration in milliseconds |
### D-Bus details
Calls `org.freedesktop.Notifications.Notify` on the session bus with `app_name` set to `"mcdbus"` and `replaces_id` of `0` (always creates a new notification).
### Return format
```
Notification sent (id: 42)
```
The returned ID can be used with `call_method` to call `CloseNotification` if needed.
---
## list_systemd_units
List systemd units, optionally filtered by a glob pattern.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `bus` | `string` | `"system"` | `"session"` for user units, `"system"` for system units |
| `pattern` | `string` | `""` | Glob filter using `fnmatch` syntax (e.g. `"docker*"`, `"*.service"`, `"ssh*"`) |
### D-Bus details
Calls `org.freedesktop.systemd1.Manager.ListUnits` and extracts the first five fields from each unit tuple: name, description, load state, active state, and sub state.
### Return format
Markdown table sorted alphabetically by unit name.
```markdown
## Systemd units on system bus -- 3 matches
| Unit | Load | Active | Sub | Description |
|------|------|--------|-----|-------------|
| `docker.service` | loaded | active | running | Docker Application Container Engine |
| `docker.socket` | loaded | active | listening | Docker Socket for the API |
| `docker-compose@media.service` | loaded | active | running | Docker Compose stack: media |
```
---
## media_player_control
Control an MPRIS2-compatible media player on the session bus.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `action` | `string` | *required* | One of: `play`, `pause`, `next`, `previous`, `stop`, `play-pause` |
| `player` | `string` | `""` | Full MPRIS service name; auto-discovers the first available player if empty |
### Auto-discovery
When `player` is empty, the tool lists all session bus services matching `org.mpris.MediaPlayer2.*`, sorts them alphabetically, and uses the first one. If multiple players are found, the others are logged via context info.
### Return format
```
Player: org.mpris.MediaPlayer2.firefox
Action: play-pause
Status: Paused
Now: David Bowie -- Starman
```
The current track is read from the player's `Metadata` property (`xesam:artist` and `xesam:title` fields). If metadata is unavailable, both fields show `"Unknown"`.
Returns `"No MPRIS media players found on session bus."` if no players are discovered.
---
## network_status
Show NetworkManager connection status and active connections. Takes no parameters.
### D-Bus details
Reads all properties from `org.freedesktop.NetworkManager` on the system bus, then iterates over each active connection path to read its `Id`, `Type`, and `State` properties.
### State mapping
The numeric `State` property is mapped to a human-readable string:
| Code | State |
|------|-------|
| 0 | Unknown |
| 10 | Asleep |
| 20 | Disconnected |
| 30 | Disconnecting |
| 40 | Connecting |
| 50 | Connected (local) |
| 60 | Connected (site) |
| 70 | Connected (global) |
### Return format
```markdown
## Network Status
- **State:** Connected (global)
- **Wireless:** enabled
| Connection | Type | State |
|------------|------|-------|
| Wired connection 1 | 802-3-ethernet | Activated |
| MyWiFi | 802-11-wireless | Activated |
```
---
## battery_status
Show battery status from UPower on the system bus. Takes no parameters.
### D-Bus details
Calls `org.freedesktop.UPower.EnumerateDevices`, then reads properties from each device via `org.freedesktop.UPower.Device`. Only devices with `Type=2` (Battery) are included.
### State mapping
| Code | State |
|------|-------|
| 0 | Unknown |
| 1 | Charging |
| 2 | Discharging |
| 3 | Empty |
| 4 | Fully charged |
| 5 | Pending charge |
| 6 | Pending discharge |
### Return format
```markdown
## Battery Status
### BAT0
- **Charge:** 73%
- **State:** Discharging
- **Time:** 4h 12m remaining
- **Power draw:** 8.3 W
```
Time remaining is calculated from `TimeToEmpty` (when discharging) or `TimeToFull` (when charging). Power draw is shown only when `EnergyRate` is greater than zero. Returns `"No batteries found."` if no battery-type devices are present.
---
## bluetooth_devices
List discovered and paired Bluetooth devices from bluez on the system bus. Takes no parameters.
### D-Bus details
Calls `org.freedesktop.DBus.ObjectManager.GetManagedObjects` on `org.bluez` at `/`, then filters for objects that implement the `org.bluez.Device1` interface.
### Return format
Markdown table sorted by connection priority: connected devices first, then paired devices, then alphabetically by name.
```markdown
## Bluetooth Devices -- 4 found
| Name | Address | Connected | Paired | Trusted |
|------|---------|-----------|--------|---------|
| WH-1000XM4 | `AA:BB:CC:DD:EE:01` | yes | yes | yes |
| Keyboard K380 | `AA:BB:CC:DD:EE:02` | no | yes | yes |
| MX Master 3 | `AA:BB:CC:DD:EE:03` | no | yes | no |
| [Unknown] | `AA:BB:CC:DD:EE:04` | no | no | no |
```
Returns `"No Bluetooth devices found."` if no `org.bluez.Device1` objects exist, or raises a `ToolError` if bluez is not available.
---
## kwin_windows
List open windows from KDE KWin via the KRunner window runner on the session bus. Takes no parameters. Requires a running KDE Plasma session.
### D-Bus details
Calls `org.kde.krunner1.Match` on `org.kde.KWin` at `/WindowsRunner` with an empty query string, which returns all windows. Each match contains an ID, caption, icon name, type code, relevance score, and properties. Type code `8` indicates a virtual desktop entry; all other types are treated as windows.
### Return format
Window table sorted by application name then caption, followed by virtual desktop names if present.
```markdown
## KWin Windows -- 5 open
| Window | Application |
|--------|-------------|
| mcdbus reference -- Firefox | firefox |
| ~/projects/mcdbus -- Konsole | konsole |
| server.py -- Kate | kate |
| Dolphin | dolphin |
| System Settings | systemsettings |
**Virtual desktops:** Desktop 1, Desktop 2
```
Returns `"No windows found (KWin WindowsRunner returned empty)."` if KWin reports no matches, or raises a `ToolError` if KWin is not available.

View File

@ -0,0 +1,137 @@
---
title: First Steps
description: "Tutorial: your first D-Bus exploration with mcdbus"
---
This tutorial walks through the discovery workflow hands-on. You will list services, introspect an object, call a method, and send a desktop notification -- all through mcdbus tools in a Claude conversation.
Make sure you have [installed mcdbus and registered it with Claude Code](/start-here/installation/) before continuing.
## 1. List services on the session bus
Start by seeing what is available. Ask Claude to list services, or it will call the tool directly:
```
> What D-Bus services are running on my session bus?
```
Claude calls `list_services(bus="session")` and gets back something like:
```
## D-Bus session bus — 42 services
- `org.freedesktop.DBus`
- `org.freedesktop.Notifications`
- `org.freedesktop.portal.Desktop`
- `org.kde.KWin`
- `org.kde.StatusNotifierWatcher`
- `org.kde.kglobalaccel`
- `org.kde.plasmashell`
- `org.mpris.MediaPlayer2.firefox`
...
```
The exact list depends on your desktop environment and running applications. Every Linux desktop will have `org.freedesktop.Notifications` available -- that is the one we will explore next.
## 2. Introspect the Notifications service
Now look at what the notification service can do:
```
> Introspect the org.freedesktop.Notifications service at /org/freedesktop/Notifications
```
Claude calls:
```
introspect(
bus="session",
service="org.freedesktop.Notifications",
object_path="/org/freedesktop/Notifications"
)
```
The output shows the interface, its methods, properties, and signals:
```
## `org.freedesktop.Notifications` at `/org/freedesktop/Notifications`
### Interface: `org.freedesktop.Notifications`
**Methods:**
- `Notify(app_name: s, replaces_id: u, app_icon: s, summary: s, body: s, actions: as, hints: a{sv}, expire_timeout: i) -> id: u`
- `CloseNotification(id: u)`
- `GetCapabilities() -> capabilities: as`
- `GetServerInformation() -> name: s, vendor: s, version: s, spec_version: s`
```
This is the D-Bus introspection XML rendered into a readable format. You can see the `Notify` method takes 8 arguments (application name, icon, summary, body, etc.) and returns a notification ID. The `GetServerInformation` method takes no arguments and returns four strings.
## 3. Call GetServerInformation
Try a simple method call first -- one that takes no arguments and has no side effects:
```
> Call GetServerInformation on the Notifications service
```
Claude calls:
```
call_method(
bus="session",
service="org.freedesktop.Notifications",
object_path="/org/freedesktop/Notifications",
interface="org.freedesktop.Notifications",
method="GetServerInformation"
)
```
The response depends on your notification daemon. On KDE Plasma:
```json
["Plasma", "KDE", "2.0", "1.2"]
```
On a system running dunst:
```json
["dunst", "knopwob", "1.11.0", "1.2"]
```
The four values are the server name, vendor, version, and the notification spec version it implements.
## 4. Send a test notification
Now use the shortcut tool to send a notification without having to specify all 8 arguments to the `Notify` method manually:
```
> Send me a test notification saying "Hello from mcdbus"
```
Claude calls:
```
send_notification(
summary="Hello from mcdbus",
body="Your D-Bus bridge is working!"
)
```
A notification appears on your desktop. The tool returns:
```
Notification sent (id: 47)
```
The notification ID can be used to replace or close the notification later through the raw `call_method` tool.
## 5. What to do next
You have completed the core discovery workflow: list services, introspect an object, call a method. From here:
- **[Control Media Players](/guides/media-players/)** -- pause, play, skip tracks on any MPRIS2-compatible player
- **[Manage Systemd Units](/guides/systemd/)** -- list and filter systemd services and timers
- **[Check Battery and Network](/guides/system-status/)** -- read system state through UPower and NetworkManager
- **[Explore Unknown Services](/guides/explore-services/)** -- use the discovery workflow to map out any D-Bus service on your system
- **[Tool Reference](/reference/discovery-tools/)** -- full documentation for every tool, resource, and prompt

View File

@ -0,0 +1,79 @@
---
title: Installation
description: Install mcdbus and register it as an MCP server with Claude Code.
---
## Prerequisites
- **Python 3.11 or later**
- **A running D-Bus daemon** -- standard on any Linux desktop (GNOME, KDE Plasma, Sway, Hyprland, etc.). If you are on a headless server without a session bus, the system bus is still available for systemd, NetworkManager, and similar services.
## Install methods
### From PyPI (recommended)
```bash
pip install mcdbus
```
### Run without installing
`uvx` downloads and runs the package in an isolated environment. Nothing is installed globally.
```bash
uvx mcdbus
```
### From source
```bash
git clone https://git.supported.systems/warehack.ing/mcdbus.git
cd mcdbus
uv sync
uv run mcdbus
```
## Register with Claude Code
The simplest registration — Claude Code will start the server automatically when it needs D-Bus access:
```bash
claude mcp add mcdbus -- uvx mcdbus
```
To enable the desktop notification confirmation fallback (recommended if your MCP client does not support elicitation):
```bash
claude mcp add -e MCDBUS_NOTIFY_CONFIRM=1 mcdbus -- uvx mcdbus
```
See the [Confirmation Flow](/explanation/confirmation-flow/) page for details on what this enables and when it triggers.
## Environment variables
| Variable | Default | Description |
|---|---|---|
| `MCDBUS_NOTIFY_CONFIRM` | unset | Enable desktop notification fallback for confirmation dialogs. When set to `1`, the server sends a notification with Approve/Deny buttons if the MCP client does not support elicitation. |
| `MCDBUS_REQUIRE_ELICITATION` | unset | Hard-fail if the client does not support MCP elicitation. Overrides the notification fallback -- operations that need confirmation will be rejected outright. |
| `MCDBUS_TIMEOUT` | `30` | D-Bus call timeout in seconds. Increase this if you interact with services that take a long time to respond (some systemd operations, large object tree walks). |
Pass environment variables during registration with `-e`:
```bash
claude mcp add \
-e MCDBUS_NOTIFY_CONFIRM=1 \
-e MCDBUS_TIMEOUT=60 \
mcdbus -- uvx mcdbus
```
## Verify it works
Run the server directly to confirm it starts without errors:
```bash
uvx mcdbus
```
You should see `mcdbus: D-Bus MCP server starting` printed to stderr. The server is now listening for MCP messages on stdin/stdout. Press `Ctrl+C` to stop it.
If the D-Bus daemon is not reachable, you will see a connection error immediately. On a headless system without a session bus, the server will still start -- it connects lazily when a tool is called, so a session bus error only appears when you try to use session bus tools.

View File

@ -0,0 +1,51 @@
---
title: What is mcdbus?
description: A D-Bus MCP server that bridges Linux desktop IPC into the Model Context Protocol, giving Claude native access to session and system bus services.
---
## D-Bus in 30 seconds
D-Bus is the interprocess communication system that ties a Linux desktop together. It provides a message bus where services register under well-known names (like `org.freedesktop.Notifications` or `org.freedesktop.NetworkManager`), expose objects at hierarchical paths, and publish interfaces with methods, properties, and signals. Nearly every desktop component speaks D-Bus: the notification daemon, media players, systemd, NetworkManager, UPower, bluez, KDE Plasma, GNOME Shell, and hundreds of others.
Two separate bus instances run on a typical Linux system:
- **Session bus** -- scoped to the logged-in user. Desktop services live here: notifications, media players, file managers, portals, KDE/GNOME shell interfaces. No elevated privileges required.
- **System bus** -- shared across all users, managed by the system. Services like systemd, NetworkManager, UDisks2, UPower, and bluez register here. Access is governed by D-Bus security policies (polkit).
The [Session vs System Bus](/explanation/buses/) page covers the security and scoping differences in detail.
## Why an MCP bridge matters
D-Bus already has CLI tools. `dbus-send` and `busctl` let you list services, introspect objects, and call methods from a terminal. But they are designed for humans typing one-off commands -- they do not compose well, and they require you to already know the service name, object path, interface, and method signature before you can do anything useful.
mcdbus exposes the same D-Bus capabilities as MCP tools that an LLM client can chain together. Instead of memorizing the incantation to check your battery level through UPower, you ask Claude "what is my battery status?" and the server handles the rest: discovering the right service, walking the object tree, reading the correct properties, and formatting the response.
The key difference is **introspection-first discovery**. The server carries no hardcoded knowledge of specific D-Bus services. It discovers what is available on your bus at runtime, introspects the interfaces, and figures out how to interact with them. If you install a new service that exposes a D-Bus interface, mcdbus can work with it immediately.
## The discovery workflow
Every interaction with mcdbus follows the same pattern, whether you are doing it explicitly or the server's shortcut tools are doing it under the hood:
```
list_services --> list_objects --> introspect --> call_method / get_property
```
1. **`list_services`** -- enumerate the well-known names registered on a bus. This tells you what is running and reachable.
2. **`list_objects`** -- walk the object tree for a specific service. D-Bus objects are arranged in a filesystem-like hierarchy (e.g., `/org/freedesktop/UPower/devices/battery_BAT0`).
3. **`introspect`** -- examine a specific object to see its interfaces, methods, properties, and signals. This is where you learn what you can actually do.
4. **`call_method`** / **`get_property`** -- invoke a method or read a property once you know the service name, object path, interface, and member name.
The shortcut tools (`send_notification`, `battery_status`, `network_status`, and others) skip this workflow by hardcoding the well-known paths for common operations. They exist for convenience, but the generic discovery tools can reach anything on the bus.
## How mcdbus compares to dbus-send and busctl
| | dbus-send / busctl | mcdbus |
|---|---|---|
| Interface | Command line | MCP tools (used by Claude or any MCP client) |
| Discovery | Manual -- you must know the service name | `list_services` enumerates available services |
| Introspection | `busctl introspect` output is tabular text | `introspect` returns structured Markdown |
| Composition | Shell pipes | Claude chains tools together in a single conversation |
| Confirmation | None (you own the terminal) | System bus calls and property mutations require user approval |
| Shortcuts | None -- every call is fully qualified | Pre-wired tools for notifications, media, systemd, network, battery, bluetooth, KDE |
mcdbus does not replace these CLI tools. It serves a different audience: LLM clients that need programmatic, composable access to D-Bus without prior knowledge of the bus layout.

View File

@ -0,0 +1,32 @@
@import "tailwindcss";
/* Terminal-green + slate — infrastructure aesthetic */
:root {
--sl-color-accent-low: #0d2818;
--sl-color-accent: #4ead6b;
--sl-color-accent-high: #b8e6c8;
--sl-color-white: #f8fafc;
--sl-color-gray-1: #e2e8f0;
--sl-color-gray-2: #cbd5e1;
--sl-color-gray-3: #94a3b8;
--sl-color-gray-4: #64748b;
--sl-color-gray-5: #334155;
--sl-color-gray-6: #1e293b;
--sl-color-black: #0f172a;
}
:root[data-theme="light"] {
--sl-color-accent-low: #dcfce7;
--sl-color-accent: #16803d;
--sl-color-accent-high: #0d3320;
--sl-color-white: #0f172a;
--sl-color-gray-1: #1e293b;
--sl-color-gray-2: #334155;
--sl-color-gray-3: #64748b;
--sl-color-gray-4: #94a3b8;
--sl-color-gray-5: #e2e8f0;
--sl-color-gray-6: #f1f5f9;
--sl-color-black: #f8fafc;
}

3
docs-site/tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}