Polybar Notification MCP
# ohai
MCP for displaying Quickshell-based notifications on Linux.
## Installation
```bash
npm install
npm run build
```
## Tools
### `ohai`
Display a notification popup.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `message` | string | yes | Message body |
| `title` | string | no | Popup title (defaults to message) |
| `severity` | `info` \| `warn` \| `crit` | no | Accent color (default: `info`) |
| `color` | string | no | Custom accent color (CSS/hex; overrides severity) |
| `timeoutSeconds` | number | no | Auto-hide delay (default: 8; 0 = persistent) |
| `pattern` | string | no | Background pattern ID or path |
| `image` | string | no | Image ID or path (default: `ghost`) |
| `projectPath` | string | no | Path used to resolve configured project visual defaults |
| `workspace` | string | no | Workspace to switch to on backtick |
| `app` | string | no | App/window to focus on backtick |
| `windowId` | number | no | Niri window id to focus from the notification action button |
| `transition` | `glow` \| `ghost` \| `ripple` \| `none` | no | Entry transition effect (default: `glow`) |
### `ohai_status`
Check if Quickshell is running with the ohai IPC target registered.
## Transition Effects
| Effect | Description |
|--------|-------------|
| `glow` | Soft blur glow pulse (default) |
| `ghost` | Expanding/fading copies of actual notification content |
| `ripple` | Border ripples emanating outward |
| `none` | No transition effect |
Set a default transition via environment variable:
```bash
export OHAI_TRANSITION=ghost
```
## Quickshell Setup
1. Start the popup daemon:
```bash
qs -p /path/to/ohai-mcp/ohai/shell.qml
```
2. Verify the IPC target is registered:
```bash
qs ipc -p /path/to/ohai-mcp/ohai/shell.qml --any-display show
# Should list: target ohai
```
3. Test manually:
```bash
qs ipc -p /path/to/ohai-mcp/ohai/shell.qml --any-display call ohai notify "Hello" "Test message" "info" 5 "" "" "" "" "glow"
```
4. Controls while popup is visible:
- `Escape` - hide popup
- Backtick (`) - switch to workspace or focus app (if configured), then hide
### Testing with `ohai-test`
A CLI helper for testing notification appearance:
```bash
bin/ohai-test [preset] # Run a preset
bin/ohai-test themes # List project visual themes
bin/ohai-test theme <name> # Preview a project visual theme
bin/ohai-test theme all # Cycle through all project visual themes
bin/ohai-test project <path> # Preview the project profile matched by a path
bin/ohai-test custom [options] # Custom notification
```
**Presets:**
| Preset | Description |
|--------|-------------|
| `info` | Basic info notification (default) |
| `warn` | Warning notification |
| `crit` | Critical notification |
| `long` | Long text (test layout) |
| `short` | Minimal notification |
| `claude` | Claude-themed |
| `openai` | OpenAI-themed |
| `grid` | Grid pattern background |
| `stripes` | Stripes pattern background |
| `waves` | Waves pattern background |
| `glow` | Test glow transition effect |
| `ghost` | Test ghost echo transition effect |
| `ripple` | Test ripple transition effect |
| `all` | Cycle through all presets |
**Theme previews:**
```bash
bin/ohai-test themes
bin/ohai-test theme aquatic
bin/ohai-test theme all
```
**Project profile diagnostics:**
```bash
bin/ohai-theme resolve ~/d/software/ohai
bin/ohai-test project ~/d/software/ohai
```
`ohai-theme resolve` prints JSON with the config path, normalized project path,
matched project, resolved visuals, and focus target. `ohai-test project` sends a
real notification using the same resolved profile values.
**Custom options:**
```bash
bin/ohai-test custom \
-t "Title" \
-b "Body text" \
-s warn \
-i claude \
-p grid-01 \
-d 10 \
-c "#ff00ff" \
-x ghost
```
| Option | Description |
|--------|-------------|
| `-t, --title` | Title text |
| `-b, --body` | Body text |
| `-s, --severity` | `info` \| `warn` \| `crit` |
| `-i, --image` | Image ID or path |
| `-p, --pattern` | Pattern ID or path |
| `-d, --duration` | Timeout in seconds |
| `-c, --color` | Custom accent color (hex) |
| `-w, --workspace` | Workspace to switch on backtick |
| `-a, --app` | App to focus on backtick |
| `-x, --transition` | `glow` \| `ghost` \| `ripple` \| `none` |
### Bundled Assets
**Patterns** (background overlays):
- `waves-01`, `grid-01`, `stripes-01`, `sunset-01`
**Images** (left panel):
- `ghost` (default), `claude`, `openai`
Use an absolute path for custom assets.
### Asset Trust Model
`image` and `pattern` accept bundled asset IDs or custom local asset paths. Custom
paths are treated as trusted desktop input and are passed to Quickshell for
rendering; ohai validates basic string shape at the MCP boundary, but it does not
sandbox QML asset loading.
Prefer bundled IDs for routine notifications. Use custom absolute paths only for
local files you intend Quickshell to read, and configure this MCP server only for
trusted clients.
## Integration
Use environment variables to configure defaults per client.
### Claude Code
To add via the CLI for user scope:
```
claude mcp add ohai \
--scope user \
--env OHAI_TRANSITION=glow \
--env OHAI_DEFAULT_IMAGE=claude \
-- node <OHAI_PATH>/bin/ohai-mcp
```
`~/.claude.json`:
```json
{
"mcpServers": {
"ohai": {
"type": "stdio",
"command": "node",
"args": ["/path/to/ohai-mcp/bin/ohai-mcp"],
"env": {
"OHAI_DEFAULT_IMAGE": "claude",
"OHAI_TRANSITION": "glow"
}
}
}
}
```
### Cursor
Add to MCP settings:
```json
{
"mcpServers": {
"ohai": {
"command": "node",
"args": ["/path/to/ohai-mcp/bin/ohai-mcp"],
"env": {
"OHAI_DEFAULT_IMAGE": "claude",
"OHAI_TRANSITION": "ghost"
}
}
}
}
```
### codex-cli
`~/.codex/config.toml`:
```toml
[mcp_servers.ohai]
command = "node"
args = ["/path/to/ohai-mcp/bin/ohai-mcp"]
[mcp_servers.ohai.env]
OHAI_DEFAULT_IMAGE = "openai"
OHAI_TRANSITION = "glow"
```
### Auto-launch (Hyprland)
```
exec-once = qs -p /path/to/ohai-mcp/ohai/shell.qml
```
## Environment Variables
| Variable | Description |
|----------|-------------|
| `OHAI_DEFAULT_IMAGE` | Default image when `image` param is not specified (e.g., `claude`, `openai`, `ghost`) |
| `OHAI_TRANSITION` | Default transition effect (e.g., `glow`, `ghost`, `ripple`, `none`) |
| `OHAI_BEZEL` | Bezel rendering mode. Unset (default): translucent plate suitable for compositor blur. `fake`: opaque plate with static noise overlay (use when compositor blur is unavailable). |
| `OHAI_CONFIG_PATH` | Override project-profile config path. Defaults to `$XDG_CONFIG_HOME/ohai/config.yaml`, or `~/.config/ohai/config.yaml` when `XDG_CONFIG_HOME` is unset. |
| `OHAI_COMPOSITOR` | Override the global focus-action compositor. Supported values: `hyprland`, `niri`. |
| `OHAI_ORIGIN_CAPTURE` | Override origin-window capture. Supported values: `off`, `focusedWindow`. |
| `NIRI_PATH` | Override the `niri` binary used for focused-window origin capture. |
## Project Profiles
Use project profiles to give notifications a visual identity based on the
originating project. The MCP caller passes `projectPath`; ohai matches it against
configured project roots and applies the matched profile's theme defaults.
Config path:
- `OHAI_CONFIG_PATH` when set
- `$XDG_CONFIG_HOME/ohai/config.yaml` when `XDG_CONFIG_HOME` is set
- `~/.config/ohai/config.yaml` otherwise
Global focus actions can be made compositor-aware with `compositor: hyprland` or
`compositor: niri`. `OHAI_COMPOSITOR` overrides the YAML value. If neither is
set, workspace and app focus actions keep the existing Hyprland command shape.
Origin capture is opt-in with `originCapture: focusedWindow` or
`OHAI_ORIGIN_CAPTURE=focusedWindow`.
Example:
```yaml
compositor: niri
originCapture: focusedWindow
projects:
- id: ohai
name: ohai
roots:
- ~/d/software/ohai
theme: aquatic
workspace: dev
- id: infra
name: Infrastructure
roots:
- ~/d/infra
theme: terminal
transition: ripple
```
Built-in themes:
| Theme | Image | Color | Pattern | Transition |
|-------|-------|-------|---------|------------|
| `ghost` | `ghost` | `#7ad7ff` | none | `ghost` |
| `aquatic` | `openai` | `#7ad7ff` | `waves-01` | `glow` |
| `frog` | `ghost` | `#c3e88d` | `grid-01` | `ripple` |
| `ember` | `claude` | `#ff9e64` | `stripes-01` | `glow` |
| `violet` | `openai` | `#bb9af7` | `grid-01` | `ghost` |
| `terminal` | `ghost` | `#9ece6a` | `stripes-01` | `none` |
| `sunset` | `claude` | `#f7768e` | `sunset-01` | `ripple` |
| `matrix` | `openai` | `#73daca` | `grid-01` | `glow` |
Profile matching uses normalized roots and chooses the longest matching prefix.
Visual precedence is: explicit `ohai` tool input, then project overrides, then
theme defaults, then environment defaults and built-in defaults. Unknown themes
make config loading fail early.
Profiles may also define one focus target for notification actions:
```yaml
compositor: niri
originCapture: focusedWindow
projects:
- id: ohai
roots:
- ~/d/software/ohai
theme: aquatic
workspace: dev
- id: terminal-tools
roots:
- ~/d/tools
theme: ghost
app: kitty
```
Supported profile focus fields are `workspace`, `app`, and `windowId`. Specify
only one per profile. Explicit `workspace`, `app`, or `windowId` values passed to
the `ohai` tool override the matched project profile focus target.
With `compositor: niri`, `workspace` uses
`niri msg action focus-workspace <workspace>` and `windowId` uses
`niri msg action focus-window --id <id>`. `app` focus is Hyprland-only because
niri does not expose a direct app-title focus action; for niri use `workspace`
or `windowId`.
With `originCapture: focusedWindow`, ohai runs
`niri msg --json focused-window` when sending a notification without an explicit
or project-profile focus target. If niri returns a focused window id, ohai uses
that id as the notification action target. Focus precedence is: explicit MCP
target, then project profile target, then captured focused window, then no focus
target.
Use the CLI helpers to inspect profile matching before wiring an MCP client:
```bash
bin/ohai-theme resolve ~/d/software/ohai
```
Example shape, with paths abbreviated for readability:
```json
{
"configPath": "~/.config/ohai/config.yaml",
"projectPath": "~/d/software/ohai",
"matched": true,
"project": {
"id": "ohai",
"name": "ohai",
"theme": "aquatic",
"matchedRoot": "~/d/software/ohai"
},
"compositor": "niri",
"originCapture": "focusedWindow",
"visuals": {
"image": "openai",
"color": "#7ad7ff",
"pattern": "waves-01",
"transition": "glow"
},
"focus": {
"type": "workspace",
"value": "dev"
}
}
```
When no profile matches, `matched` is `false` and `project` is `null`.
## Hyprland Compositor Blur (recommended)
For the frosted-glass bezel effect, add the following to your `hyprland.conf`:
```
layerrule = blur, ^ohai$
layerrule = ignorezero, ^ohai$
```
The anchored regex (`^ohai$`) ensures the rule applies only to the ohai notification surface. `ignorezero` tells Hyprland to skip blur on transparent regions of the surface — important because the popup window includes transparent padding around the visible bezel for transition-effect overflow; without this rule, the entire rectangular surface gets blurred.
Verify with `hyprctl layers` after reloading Hyprland — you should see `ohai` listed with the rules applied.
If you don't add these rules, the bezel will render as a translucent dark plate without compositor blur. This is still acceptable, just less visually distinct. Alternatively, set `OHAI_BEZEL=fake` for a more opaque bezel with a static noise texture.
## Architecture
```
┌─────────────┐ qs ipc call ┌─────────────────┐
│ MCP Client │ ──────────────────▶ │ Quickshell │
│ (Claude) │ │ (shell.qml) │
└─────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ ohai-mcp │ │ IpcHandler │
│ (Node.js) │ │ target: "ohai" │
└─────────────┘ └─────────────────┘
```
The MCP server communicates with Quickshell via its built-in IPC mechanism (`qs ipc call`), which uses a Unix socket for instant, reliable delivery.
The internal `notify` IPC call takes 9 ordered arguments: `title`, `body`, `severity`, `timeoutSeconds`, `pattern`, `image`, `actionTarget`, `color`, `transition`. `actionTarget` is empty, an app/window title, `workspace:<id>`, `window:<id>`, or a compositor-prefixed target such as `niri:workspace:<name>` or `hyprland:app:<title>`.
## Development
```bash
npm run dev # Watch mode
npm run build # Build
npm run lint # Lint
npm run format # Format
```
## Requirements
- Linux with Quickshell
- Node.js 18+
# Credit
Original implementation adapted from [neotanx/neomcps - sound notification mcp](https://github.com/neotanx/neo-mcps/tree/main/servers/sound-notification).
TDQS
Scored across 2 tools
The two tools have overlapping purposes—both are described as 'useful for notifying the user when an operation is complete or when waiting for user input.' This creates ambiguity, as an agent might struggle to choose between displaying a message in the polybar status bar versus showing a popup notification, since their use cases are not clearly differentiated. The descriptions do not specify distinct scenarios or advantages for each tool, leading to potential misselection.
The tool names follow a consistent verb_noun pattern with clear, descriptive terms: 'display_polybar_message' and 'show_popup_notification.' Both use snake_case and start with action verbs ('display' and 'show'), making them predictable and easy to understand. There are no deviations in naming conventions, ensuring readability and coherence.
With only 2 tools, the server feels thin for a notification domain that could benefit from more granular operations, such as different notification types, priorities, or durations. While the count is minimal, it may be too few to cover common notification workflows effectively, limiting the server's utility and causing agents to work around gaps in functionality.
Inferring the domain as user notifications, the tool set is severely incomplete. It lacks operations for managing notifications (e.g., dismissing, updating, or clearing notifications), handling different notification styles or priorities, or integrating with other notification systems. This creates significant gaps that could lead to agent failures when trying to perform common notification-related tasks beyond basic display.