Omarchy
FreeNot checkedThis Omarchy plugin exposes Omarchy functions through an MCP server
About
This Omarchy plugin exposes Omarchy functions through an MCP server
README
Lets a local AI agent drive your Omarchy desktop: every command in Omarchy's registry, plus the live shell's IPC targets, exposed over MCP on loopback.
Runs as an Omarchy plugin, so there is no systemd unit to enable, no second install step, and no separate package. The plugin supervises a small daemon; the daemon starts with your session and stops with it.
Getting started
Steps 1–4 get it installed and connected, in about two minutes. Steps 5–7 are a short tour that ends with you having written a real permission rule and seen it take effect. Each step links to the section that goes deeper.
1. Install the plugin
omarchy plugin add https://github.com/bruce-forte/omarchy-mcp-server.git --enable
The first run builds a Python environment under
~/.local/state/io.github.bruce-forte.mcp-server/ — a second or two, and it
needs the network once. Nothing else is installed and no service is enabled;
omarchy-shell supervises the daemon from now on.
A plug icon appears in your bar. That icon is the whole UI: it says whether the server is serving, and clicking it opens the panel where everything else happens. → Install
2. Give yourself a shortcut to the CLI
The daemon's own command lives inside the plugin directory and is not on
your PATH. Everything below, and the panel itself, refers to it as
omarchy-mcpd, so make that true:
echo "alias omarchy-mcpd='~/.config/omarchy/plugins/io.github.bruce-forte.mcp-server/bin/omarchy-mcpd'" \
>> ~/.bashrc && source ~/.bashrc
Skip this if you would rather type the full path each time. It is only ever needed from a terminal — the bar panel needs none of it.
3. Connect your client
The server requires a bearer token, generated on first run. Print the exact line to run:
omarchy-shell io.github.bruce-forte.mcp-server clientConfig
which gives you something like:
claude mcp add --transport http omarchy http://127.0.0.1:8765/mcp \
--header "Authorization: Bearer <your token>"
Run it, and the agent has the desktop. More than one client can attach at once. → Connecting a client
4. Check it is actually serving
omarchy-shell io.github.bruce-forte.mcp-server status
Or just look at the bar: the plug icon shows ! when the daemon is not serving,
and blinks when an agent makes a call. → Checking it works
5. Ask the agent for something, and watch it just happen
Try "what theme am I using, and what else is installed?", then "switch to Tokyo Night".
Both just happen. No prompt — and that is correct, not a fault. The line
this server draws is what is hard to undo, not what writes: switching a
theme is reversed by one more sentence to the agent, so it is safe and it
runs. Wallpapers, volume, brightness, launching an app and moving a window are
all the same. What asks is the guarded tier — installs, removals, migrations,
reboots, shell plugins, and anything in a command group this server has not
classified — and what is never allowed at all is anything needing sudo.
If that is where you want the line, you are done; skip to What to do next. The next two steps move it, which is also how you learn what the prompt looks like without installing anything.
6. Move the line, and watch it take effect
Say you want to be asked before an agent restyles your desktop. Open your rules file — this creates it from a template if you have none:
omarchy-mcpd --edit permissions
Add one rule to the ask list, so the file reads:
{
"permissions": {
"deny": [],
"ask": [{ "kind": "route", "matcher": "omarchy theme set" }],
"allow": []
}
}
Save it. That is the whole deployment. No restart, no reload command: the
daemon re-reads both rule files within about two seconds and the next call is
decided by the new document. The matcher is an exact route here, so
omarchy theme list and omarchy theme current stay unaffected — reading which
themes you have is not the thing you wanted to be asked about.
7. Try it again
Ask for "switch to Catppuccin". (Any theme you actually have — omarchy theme list shows them. Name one you do not have and you get a refusal naming the near
misses instead of a prompt: arguments are checked against your machine before
anybody is asked, so a question is never put about something that does not
exist.)
This time a critical notification appears on your desktop, naming the
command and the theme name it resolved to — Catppuccin, not
omarchy theme set, because a question you cannot see the object of is not a
question. Clicking it opens the panel, where the answers are:
| Press | What happens |
|---|---|
| Allow once | The theme switches. Nothing is written down, and you are asked again next time |
| Deny | Nothing runs |
| Always | Refused here, deliberately — see below |
Ignoring it refuses too, after askTimeoutSeconds. Silence is never a yes.
Always is the interesting one. It is refused with "omarchy theme set is
covered by 'omarchy theme set' in the 'ask' list of permissions.json, so an
allow rule would have no effect" — because rules are read deny, then ask,
then allow, and your own ask rule is reached first. Rather than write a
grant that would never be consulted, the daemon says so and refuses the call.
That precedence is the thing that stops a grant from ever carving a hole in a
restriction you wrote.
To put it back the way it was, delete the rule you just added — same file, same two seconds. Or keep it: it is a real rule, not a demo.
→ What an agent is allowed to run for the
tiers in full, and Being asked, and writing it down
for what Always does when it is not shadowed.
What to do next
| If you want to… | Go to |
|---|---|
| Stop being asked about a command you always approve | Press Always on the prompt, or write an allow rule — Being asked, and writing it down |
| See what an agent has actually done to your desktop | Seeing what it did |
| Understand which commands ask and which do not | What an agent is allowed to run |
| Turn a tool off, or change the port | Configuration |
| Know exactly what a hostile web page can and cannot do to you | SECURITY.md |
| Read the tool reference | TOOLS.md |
How this works
Four pieces, one process each doing one job:
omarchy-shell (Quickshell)
│
├── Service.qml ──spawns──> bin/omarchy-mcpd ──> python -m omarchy_mcp
│ │ (bootstrap) (the daemon)
│ │ │
│ │<─── state + one line per tool call ─────────┤
│ ├──── polls GET /health every 10s ────────────┘
│ │
│ └── IpcHandler: status, recent, review, permissions, start, stop, …
│
└── BarWidget.qml ── the plug icon, and the panel behind it
- The daemon is a small HTTP server bound to
127.0.0.1, speaking MCP behind a bearer token. It holds the policy, the permissions, and the activity log. Service.qmlsupervises it: starts it with your session, restarts it if it dies, and probes/healthrather than trusting that the process exists — a wedged HTTP loop still has a live pid.BarWidget.qmlis the plug icon and its panel. It is the surface you use to answer a question, read what happened, and see your rules.
Nothing here is a hand-written catalogue. omarchy commands --json
describes every command Omarchy ships; the daemon reads that listing live and
classifies each route by rule rather than by a list, so an omarchy update that
adds or renames commands needs no change here.
When an agent calls a tool, the daemon does four things in order: works out what kind of command it is, applies your rules, resolves what the arguments actually name on your machine, and asks you if that is what your rules call for. Only then does it spawn anything — as an argument list, never through a shell.
For the reasoning behind each of those, read ARCHITECTURE.md.
Documentation
| File | For |
|---|---|
README.md |
Using it — install, connect a client, configure, uninstall |
| TOOLS.md | The tool reference, generated from the server's own schemas |
| ARCHITECTURE.md | How it works. Start here to read the source |
| SECURITY.md | What an agent can and cannot do, and why |
| ROADMAP.md | What is done, what is left, what was decided against |
| CONTRIBUTING.md | Working agreement, and Omarchy plugin conventions |
| examples/elicit_client.py | Answer an approval over MCP elicitation — make elicit |
| permissions.example.json | A starting point for your own rules |
| permissions.schema.json | The schema your editor validates them against |
Contents
- Getting started
- How this works
- What this is
- What it does
- Install
- Connecting a client
- Checking it works
- Configuration
- What an agent is allowed to run
- Seeing what it did
- The command line
- Development
- Troubleshooting
- Uninstall
- Requirements
- Security
- License
What this is
One process that exposes the whole of Omarchy to whatever agent you point at it, rather than a chosen subset of it wrapped in hand-written tools.
Complete coverage that maintains itself. Every command in Omarchy's registry
is reachable, including ones added by a release published after this one. The
registry is read live and classified by rules rather than by a list, so there is
no catalogue here to keep in step and nothing to update when omarchy update
renames a route.
The shell's IPC surface at all. The bar, the OSD, media, notifications, and
every loaded plugin are reachable only through Quickshell IPC, which no command
covers. qs ipc show is the only documentation these interfaces have, and this
server republishes it with full method signatures.
Resources, so a person can read what an agent can do. Tools are for acting;
resources are for reading. In Claude Code they are @ mentions — the whole
annotated registry, every IPC target, the desktop's current state — readable
without running anything, by you as much as by the agent.
One daemon, many clients. Claude and Codex attached at the same time share one policy, one configuration, and one place where approvals are decided, because there is one process holding all of it rather than one per client.
It asks you, where you actually are. A guarded command raises a notification on your desktop naming what it would do and what it resolved to — the theme, the monitor, the file. Clicking it opens a panel with Allow once, Always and Deny; ignoring it refuses. The question reaches you at the desktop rather than in whichever terminal the agent happens to be running in, because that is where you are. Always writes the decision down, so you are asked once rather than every time.
What it does
Omarchy has two control surfaces, and this exposes both:
- The command registry.
omarchy commands --jsondescribes several hundred commands — route, arguments, summary, examples, and whether each needs sudo. That listing is read live, so this server never goes stale when Omarchy is upgraded and there is no command catalogue to maintain. - The shell's IPC targets. Everything
omarchy-shelldraws — the bar, the OSD, notifications, media, and every loaded plugin — is reachable only through Quickshell IPC.qs ipc showlists them with full method signatures, and that listing is the only documentation these interfaces have.
Four generic tools cover both surfaces completely:
| Tool | Does |
|---|---|
omarchy_search_commands |
Finds commands, with arguments, examples, and whether they can be run |
omarchy_run |
Runs one. Arguments never touch a shell |
omarchy_shell_targets |
Lists IPC targets and every method signature |
omarchy_shell_call |
Calls one |
One tool per command would put tens of thousands of tokens of schema into a client's context before it did anything, so discovery and dispatch are separate.
Fifteen curated tools sit on top, each earning its place one of two ways.
Some return what the generic runner structurally cannot — an image, or data that is not Omarchy's at all:
| Tool | Does |
|---|---|
omarchy_screenshot |
Returns the screen as an image, so an agent can see it |
omarchy_desktop_state |
Hyprland's monitors, workspaces, windows, and focus |
omarchy_screen_text |
OCR, for reading what something says |
omarchy_clipboard_read / _write |
The clipboard, which is not an Omarchy command |
omarchy_system_status |
Eight probes in one call instead of eight round trips |
The rest are simply asked for constantly, and a search round trip before every volume change is a bad trade:
omarchy_notify, omarchy_osd, omarchy_theme, omarchy_background,
omarchy_audio, omarchy_brightness, omarchy_media, omarchy_toggle,
omarchy_launch.
Curated tools go through the same policy check and executor as omarchy_run — a
better-shaped door onto the same room, never a way around the lock. Any of them
can be switched off in the config, and everything they do stays reachable
through omarchy_run.
Eight resources carry the reference material. Tools are how an agent acts;
resources are how a person reads — in Claude Code they appear as @ mentions:
| URI | Holds |
|---|---|
omarchy://commands |
The whole registry, annotated with what this server may run |
omarchy://permissions |
The rules in force, what each covers here, and every route they decide |
omarchy://shell/targets |
Every IPC target with full method signatures — documented nowhere upstream |
omarchy://desktop/state |
Monitors, workspaces, windows, focus |
omarchy://system/status |
The system status aggregate |
Plus three URI templates — omarchy://command/{route},
omarchy://commands/{group}, omarchy://shell/target/{name} — which between
them cover every command and target without a listing of several hundred
entries.
Full reference, generated from the server's own schemas: TOOLS.md.
Install
omarchy plugin add https://github.com/bruce-forte/omarchy-mcp-server.git --enable
That clones the repository into
~/.config/omarchy/plugins/io.github.bruce-forte.mcp-server/, validates the
manifest, and enables it. Confirm it landed:
omarchy plugin list | grep mcp-server
On first run the plugin builds a Python environment in
~/.local/state/io.github.bruce-forte.mcp-server/. This takes a second or two
and needs a network connection once. omarchy plugin add deliberately runs no
build and no install hook, so this happens lazily rather than at install time.
Connecting a client
The server requires a bearer token, generated on first run. Print the exact command to run:
omarchy-shell io.github.bruce-forte.mcp-server clientConfig
or directly:
~/.config/omarchy/plugins/io.github.bruce-forte.mcp-server/bin/omarchy-mcpd --print-client-config
which prints something like:
claude mcp add --transport http omarchy http://127.0.0.1:8765/mcp \
--header "Authorization: Bearer <your token>"
For clients configured by file rather than by command, add --json:
{
"mcpServers": {
"omarchy": {
"type": "http",
"url": "http://127.0.0.1:8765/mcp",
"headers": { "Authorization": "Bearer <your token>" }
}
}
}
The panel's Copy client config button puts the same line on your clipboard without showing the token on screen, which is the one to use during a screen share.
Checking it works
omarchy-shell io.github.bruce-forte.mcp-server status
curl -s http://127.0.0.1:8765/health
status reports whether the daemon is serving, which is not the same as
running — a wedged HTTP loop still has a live process, so the plugin probes
/health rather than trusting the pid.
The bar widget
The plug icon in the bar says whether the server is serving, and blinks when an agent makes a call — the only thing on the desktop that marks the moment something acted on it. Click it for a panel with three tabs:
| Tab | Shows |
|---|---|
| Summary | whether it is serving and on which port, how many tools are offered, whether either config file failed to load, anything the server has stopped asking about, and a button that opens config.toml in your editor |
| Log | the last 30 records, newest first, each with a severity icon and how long ago it happened |
| Rules | every rule in force and what it covers, flagged when it is not doing what it looks like it does — with Remove on the grants this daemon wrote and Edit on each file |
Two things are not in a tab, because they must not be behind one. A question waiting for an answer sits above the tabs, and these buttons sit below them, on every tab:
| Button | Does |
|---|---|
| Stop / Start | Switches the daemon off, or back on. A Stop lasts across a shell restart and a logout, until you start it again |
| Restart | Bounces the daemon. Needed after changing server.port or the [log] settings; every other key re-reads itself within two seconds |
| Check permissions | Says whether permissions.json would let the daemon start — the one control that is useful precisely when it will not |
| Reload config | Re-reads config.toml now rather than within two seconds |
| Copy client config | Puts the claude mcp add … line on your clipboard. It carries the bearer token, so it is never shown on screen |
It is keyboard-driven. [ and ] move between tabs; j/k or the arrows
walk the controls and h/l move within a row; Enter presses what is lit and
Esc closes. On the Log, where there is nothing to press, j/k scroll
instead. A dim line under the buttons lists whatever applies where you are.
Tab is untouched and still moves to the next panel on the bar, as it does
everywhere else in Omarchy.
The panel reads the activity log and the rules from the files, so it still answers when the daemon is stopped or is refusing to start — which is when the Edit and Check permissions buttons matter most. Arguments are not shown there — see Seeing what it did.
It opens from a keybind or a terminal too:
omarchy-shell shell toggle io.github.bruce-forte.mcp-server
Configuration
Optional. Everything works without it. A commented template is written to
~/.config/omarchy/mcp/config.toml on first run; every key is commented out and
shows its default, so keys you leave alone keep tracking upstream defaults.
[server]
# port = 8765
# timeout_ms = 30000
# max_output_b = 262144
[tools]
# Curated tools to switch off. Everything they do stays reachable through
# omarchy_run; a disabled tool is absent from the client's list, not refused.
# disabled = ["omarchy_screenshot", "omarchy_clipboard_write"]
[log]
# level = "info"
# activity = true
# activity_max_bytes = 1048576
# activity_file = "activity.jsonl"
The full template, with every key explained, is config.example.toml.
What an agent may run is not in this file. It lives beside it in
permissions.json — see Being asked, and writing it down.
Saving either file is enough. The daemon re-reads both within about two seconds: tools switch on and off on any client that is already attached, and rule changes apply to the next call. To not wait:
omarchy-shell io.github.bruce-forte.mcp-server reloadConfig
or press Reload config in the bar panel, which does the same thing.
Two exceptions, both needing a restart, because the daemon is already using
them: server.port (the socket is bound) and the [log] activity settings (the
log file is open).
If the file stops parsing, the daemon keeps running the configuration it already
had rather than falling back to defaults — a stray keystroke must not empty your
deny list or switch disabled tools back on. It says so with a notification,
and the bar panel says so until the file parses again.
The listen address is always 127.0.0.1 and is deliberately not configurable.
See SECURITY.md.
What an agent is allowed to run
Commands are classified automatically from the registry, so the policy does not rot when Omarchy adds commands:
| Tier | Rule | Behaviour |
|---|---|---|
blocked |
needs sudo | Refused always. The daemon has no controlling terminal, so a password prompt could never be answered. Not overridable |
guarded |
installs, removes, migrates, reboots, shell plugins — or a group this server has not classified | Refused unless allowed in your config, or approved by you at the time |
safe |
in a command group this server classifies as safe | Runs |
safe is an allowlist, and that is the security part. A command is safe only
if its group is on a list somebody wrote down. Anything in neither list is
guarded, so a group Omarchy invents after this server's last release asks rather
than running — the classification fails towards the prompt, not towards the
command. The price is that a perfectly harmless new group also asks until the
group is classified or you allow the routes; the server tells you which, in the
prompt and in the bar panel. Both lists are in TOOLS.md, generated
from the code.
The line is what is hard to undo, not what writes. This surprises people, so
it is worth being explicit: switching your theme, setting a wallpaper, changing
the volume or brightness, launching an app, moving a window and sending a
notification are all safe, and an agent does them without asking you.
Undoing any of them is one more sentence to the agent. What is guarded is the
install, remove, migrate, update, dev, plugin and snapshot groups,
plus a handful of individually destructive routes — omarchy system reboot,
omarchy theme remove and omarchy restart shell among them — plus anything in
a group this server has never classified.
If you want the line drawn somewhere else, draw it: an ask rule puts a route
behind a prompt even though it derives as safe, and a deny rule refuses it
outright. To see exactly where it falls on your machine:
omarchy-mcpd --permissions
omarchy_search_commands reports the tier of every result too, so an agent can
see what it may do before trying.
An agent cannot switch this server off. Its own IPC target answers status
and recent to an agent — both read-only — and refuses every other verb, as is
any command that would disable, remove or replace this plugin. The refusal
points at the bar panel, which is where you press Stop, Restart or Reload
config. See SECURITY.md.
Being asked, and writing it down
Deciding once, in advance, in a text editor, is the wrong shape for a decision about a specific command. So by default a guarded command asks you at the time, and what you decide can be written down.
The rules live in their own file, ~/.config/omarchy/mcp/permissions.json,
which is meant to be checked into your dotfiles:
{
"permissions": {
"guardedDefault": "ask",
"deny": [{ "kind": "route", "matcher": "omarchy dev *" }],
"ask": [{ "kind": "route", "matcher": "omarchy install *" }],
"allow": [{ "kind": "route", "matcher": "omarchy theme *" }]
}
}
Rules are read deny, then ask, then allow — the first match decides, and a
narrower rule never jumps the queue. A matcher is either an exact route
(omarchy install app) or a prefix with a trailing * (omarchy install *,
which also covers the bare omarchy install). There is no separate notion of a
group: every route's group is its second word, so omarchy install * is the
install group.
Set "guardedDefault": "deny" to have guarded commands refused outright rather
than asked about.
Beside it, permissions.local.json is the daemon's own file: the only thing it
ever writes, holding the rules you created by pressing Always. Gitignore
that one; the daemon never touches permissions.json, which is yours.
An update can widen a rule you wrote
omarchy install * means the install prefix, not the fifteen routes that
existed the day you typed it. So an omarchy update can put three more commands
inside a sentence you already agreed to, and nothing in your file changed to say
so. The server watches for exactly that:
- a rule that now covers more than it did — the one worth being told about
- a rule that has stopped matching — a
denythat upstream renamed out from under you looks exactly like one that is working - commands in a group this plugin has never classified, which are guarded — so nothing runs unasked — but which cost a prompt on every call until the group is classified or you allow the routes
New commands that fall under an existing allow are held at ask until you
review them: restrictions extend forward, grants do not. A deny or ask rule
covers a new command the moment it arrives.
The bar panel shows all of it, and Acknowledge records what Omarchy ships now so you are only told about the next change. From a terminal:
omarchy-mcpd --review
A fresh install has nothing to compare against, so the first run records a baseline and tells you nothing.
Rules that stop matching anything — a route was renamed out from under one —
accumulate rather than being tidied away behind your back. The panel lists them
and offers Prune, which removes them from permissions.local.json only.
--review prints the same list.
Seeing the rules, and taking one back
The panel's Rules tab lists every rule in force, grouped by the file it came from, with what each one covers and whether it is doing anything at all. Four things get flagged:
| Flag | What it means |
|---|---|
error |
The rule asserts something that can never be honoured — granting a sudo route, say. The daemon refuses to start on one |
void |
The matcher covers nothing on this Omarchy: a typo, or a route that was renamed |
shadowed |
It never decides anything, because an earlier rule with a different effect already covers everything it matches. You believe you granted this and you did not |
redundant |
The same, but the earlier rule agrees with it. Safe to remove |
A shadowed or redundant rule names the rule that got there first, and which file that one is in, because the two fixes are delete this and narrow that.
Grants written by answering always get a Remove button. It takes allow
rules out of permissions.local.json and nothing else — never a deny, never
an ask, and never anything from your own permissions.json. A restriction is
a decision, and it is taken back the way it was written: in an editor, which
each file's Edit button opens.
From a terminal, omarchy-mcpd --permissions prints the same thing, and
omarchy-shell io.github.bruce-forte.mcp-server permissions says how many rules
need attention. That verb, like review and pending, answers you — an
agent asking this plugin's own target gets only status and recent.
Copy permissions.example.json to start, and check your edits before restarting anything — see The command line for all four verbs.
--permissions answers the question you actually have — why can it do that? —
by expanding every rule against the commands your Omarchy ships and listing the
routes the document decides. Agents read the same report as
omarchy://permissions.
A file that does not load stops the server. Not "is ignored with a warning"
— ignoring it would mean running under rules nobody wrote, and an ignored deny
is a protection you think you have and do not. You get a critical notification
naming the problem, the bar panel says so, and the panel's Check permissions
button tells you when the fix is good. (An edit made while the server is running
is gentler: a broken save leaves the rules it already had in force.)
What being asked looks like
A guarded route raises a critical notification naming the command and what it resolved to — the theme, the monitor, the path. Clicking it opens the panel. It does not approve anything: the notification has no buttons, and a click on a toast should not be able to grant a command.
Three answers, one place. The panel shows the pending question with:
| Allow once | runs this call and changes nothing |
| Always | runs it and writes an allow rule for that exact command to permissions.local.json — never a wildcard, however many times you press it, because you consented to what was on the screen |
| Deny | refuses, unambiguously |
The bar icon opens the same panel, so a notification that fails to summon it is not a dead end.
Nothing else approves. Dismissing it, ignoring it, and letting the deadline pass all refuse, because a prompt that granted on expiry would be granting to an empty room.
The agent is told which of those happened, because they mean different things: a refusal is worth respecting, and a silence is worth asking you about directly.
It stops asking before you stop reading. A command you did not approve is not asked about again for a few minutes — longer each time — and twelve prompts in ten minutes stops the asking altogether, whatever you answered. A stream of critical notifications turns a click into a reflex, and a reflexive click is not consent. The agent is told plainly, and told to have you allow the command once instead. The panel shows what is being held back and for how long; it clears itself.
Two things asking never reaches. Anything needing sudo stays refused — no answer
makes it runnable, so no rule may grant it and writing one is an error the
server tells you about rather than a line that quietly does nothing. And
anything a deny rule covers stays refused, because that is a decision you
already took and re-asking it would turn your no into a question.
One route is asked about every time and can never be granted:
omarchy update lock, whose own argument is a command line. Allowing it once
would allow everything.
The other surface: MCP elicitation
If your client supports elicitation over a transport that can carry it, the question appears in the client instead of on your desktop. Both halves matter, and the second is the one that decides it:
| The client negotiates | Back-channel | Where the question goes |
|---|---|---|
2025-11-25 or older, via initialize() |
yes, with SSE | the client, as an elicitation |
2026-07-28, via server/discover |
none, by construction | a desktop notification |
Claude Code does the second, so elicitation cannot reach it — not a client gap and not something to wait out, but the negotiated revision. That is why the notification is the primary surface rather than a nicety beside it.
You can watch the other path work. From a checkout:
make elicit # the form: which theme?
make elicit ARGS='"omarchy theme set" Nord' # the consent question
make elicit ARGS='--accept' # ...and say yes to it
It attaches as a handshake-era client, so the daemon asks it rather than your
desktop, and the question is printed in your terminal.
examples/elicit_client.py explains the negotiation in its own docstring.
It starts its own daemon from the checkout on port 8799 and stops it again,
so what you are testing is the code in front of you. That is not a detail: the
installed plugin is whatever was committed when you last ran
omarchy plugin update, and pointing the demo at that leaves you debugging an
older build — which reads exactly like the feature not working. To use the
installed daemon deliberately:
make py CMD='examples/elicit_client.py --port 8765'
Two different questions, and the bare make elicit shows both. It calls
omarchy_theme(action="set") and deliberately names no theme, which is a
missing parameter rather than a permission question. The server answers with a
form — the installed themes as an enum, which a client renders as a picker.
Neither the question nor the choices are written down anywhere here: the
wording is Omarchy's own summary of omarchy theme set, and the options are
whatever omarchy theme list says right now:
--- the server is asking ---
Apply an Omarchy theme. Which one?
1. Catppuccin
...
21. Tokyo Night
choose 1-23 (enter to decline):
Pick one and the consent question follows it, because choosing a theme from a list is not consent to switch to it. The name you picked goes through the same tier, the same rules, the same resolver and the same approval as one the agent had named itself:
--- the server is asking ---
An agent is asking to run a guarded command.
omarchy theme set 'Tokyo Night'
Target: Tokyo Night
Two things you may see instead of that second question. If no rule makes the
route ask, it simply runs — add the ask rule from
step 6 first. And if you have
already declined it a few times, you get not_asked_again rather than a prompt:
that is the anti-habituation guard, and it clears itself within minutes.
For a client that cannot be asked — Claude Code — none of this changes:
action="set" with no name is the error it always was, telling the agent to
call action="list" first.
Arguments are checked before anything runs
Whatever the tier, an argument that names something is checked against your machine before anything is spawned. A theme name is matched the way Omarchy matches it — case and spaces do not count — and a near miss is refused with the near misses named rather than corrected into a different theme:
// omarchy_theme(action="set", name="Tokoy Night")
{
"error": "no theme named 'Tokoy Night' is installed.",
"unresolved": "theme",
"reason": "not_found",
"did_you_mean": ["Tokyo Night"],
}
The same applies to monitor names, wallpaper paths, and URLs. reason tells an
agent whether the name was wrong (not_found, worth retrying with another) or
whether nothing could be checked (source_unavailable, retrying will not help).
This is also what makes an approval prompt worth answering: it names Tokyo Night, not "an agent wants to run omarchy theme set".
Seeing what it did
Every tool call is appended to an activity log, so what did that agent do to my desktop has an answer after the daemon is gone:
~/.local/state/io.github.bruce-forte.mcp-server/activity.jsonl
One JSON object per line — what was called, what it was understood to be acting on, whether you approved it, how it ended, and how long it took:
{
"ts": "2026-08-31T14:22:07+02:00",
"tool": "omarchy_run",
"route": "omarchy theme set",
"args": ["tokyo-night"],
"target": "Tokyo Night",
"tier": "guarded",
"consent": "accepted",
"outcome": "ok",
"exit": 0,
"ms": 142,
}
Refusals are in there too — a guarded route that was stopped is more interesting
than a safe one that ran. outcome is one of ok, failed, timed_out,
refused, not_installed or error.
Read the end of it without jq, running or not:
omarchy-mcpd --tail 20
Or click the bar icon, which shows the same records without their arguments.
What is never written there: command output. No OCR text, no clipboard
reads, nothing a tool returned. Arguments are written, truncated — they are what
the agent asked for, and they are the point — so the file can contain text you
copied, and it is created 0600 in a 0700 directory. It rotates at 1 MiB
keeping one previous generation, so it costs at most 2 MiB.
Switch it off, resize it, or rename it under [log] in your config; see
Configuration.
The command line
omarchy-mcpd is the daemon, and also the tool for reading its state from a
terminal. It is not on your PATH — it lives inside the plugin directory:
~/.config/omarchy/plugins/io.github.bruce-forte.mcp-server/bin/omarchy-mcpd
Alias it, as in Getting started, or type the path. Every verb below reads files directly, so all of them work whether or not the daemon is running — which is exactly when you need them.
omarchy-mcpd --tail 20 # the last 20 things an agent did
omarchy-mcpd --permissions # what is in force, and what each rule covers
omarchy-mcpd --check-permissions # would the daemon start? exit 0 if yes
omarchy-mcpd --review # what changed under your rules since you looked
omarchy-mcpd --edit permissions # open it in your editor, with a template if new
omarchy-mcpd --print-client-config # the client setup line, with the token
--edit takes permissions, local or config. It opens the file in whatever
editor you use, and if the permissions file is not there yet it writes a
starting one first — an empty rule block and the $schema line, so your editor
checks a matcher as you type it. It never changes a file that already exists.
--tail, --review, --permissions and --print-client-config all take
--json for a machine-readable form. --version prints the version.
The same answers are reachable through the plugin's IPC target, which is what a script or another agent discovers:
omarchy-shell io.github.bruce-forte.mcp-server status
qs ipc -n -p "$OMARCHY_PATH/shell" show # every verb, with signatures
Development
Work on a checkout, then point Omarchy at it:
omarchy plugin add /path/to/omarchy-mcp-server --enable --yes
omarchy plugin update io.github.bruce-forte.mcp-server
plugin add clones, so only committed work gets installed.
make check # tests, ruff, pyright, qmllint, shellcheck, validation, gates
make lsp # point your editor's language server at the dev virtualenv
make test
make tools # regenerate TOOLS.md from the server's schemas
make schema # regenerate permissions.schema.json from the pydantic models
make run # run the daemon in the foreground
make elicit # answer a real approval over MCP elicitation, in your terminal
Use the Makefile rather than bare uv commands: it puts the dev virtualenv
outside the repository, because omarchy plugin validate rejects symlinks
anywhere inside a plugin folder and a virtualenv is largely symlinks.
That is also why your editor needs make lsp. A language server looks for a
.venv beside pyproject.toml, does not find one, and reports every
third-party import as unresolved; make lsp writes a git-ignored
pyrightconfig.json naming the real environment. Run it once per checkout, and
again after changing [tool.pyright].
Reload rules. Editing Python takes effect on the next daemon restart
(omarchy-shell io.github.bruce-forte.mcp-server restart). Editing QML needs
omarchy restart shell.
The source carries its own documentation: every module opens with why it is shaped the way it is, and ARCHITECTURE.md says which order to read them in.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
The bar icon shows ! |
The daemon is not serving. journalctl --user -f | grep omarchy-mcp says why |
omarchy-mcpd: command not found |
It is not on PATH by design — see The command line |
| Client cannot connect | Wrong or stale token. Re-run clientConfig and re-add the server |
address already in use |
Something else has port 8765. Set port in the config, restart, then re-run clientConfig |
| Bootstrap failed on first login | Usually no network yet. omarchy-shell io.github.bruce-forte.mcp-server rebuild |
| A command is refused | Check its tier with omarchy_search_commands, or omarchy-mcpd --permissions. Sudo commands cannot be run at all |
| Tools do not appear in the client | The client caches the tool list; reconnect it |
The server will not start, and the panel blames permissions.json |
Run omarchy-mcpd --check-permissions, or press Check permissions in the panel. It names the rule and the two legal matcher forms. Fix it, then press Start |
| An approval notification appears more often than you want | Press Always on it, write an allow rule, or set "guardedDefault": "deny" to have guarded commands refused instead of asked about |
| A rule you wrote does nothing | The Rules tab flags it void, shadowed or redundant and names the rule that got there first |
After an omarchy update, something that used to run now asks |
Its command group is newer than this server's safe list, so it is guarded rather than assumed harmless. The refusal names the group. Allow the routes you want, or update the plugin |
Uninstall
omarchy plugin remove io.github.bruce-forte.mcp-server
rm -rf ~/.local/state/io.github.bruce-forte.mcp-server # venv, token, activity log, autostart marker, registry snapshot
rm -rf ~/.config/omarchy/mcp # config.toml and your permissions
rm -rf "$XDG_RUNTIME_DIR/io.github.bruce-forte.mcp-server" # pending approvals
claude mcp remove omarchy # if you added it there
The runtime directory is cleared when you log out, so that line only matters if you are removing the plugin without rebooting.
plugin remove takes the plugin directory only; the two directories above are
outside it by design and are not touched.
Requirements
- Omarchy 4 (Quattro) or newer
/usr/bin/python3— present on every Omarchy install- A network connection on first run, to build the environment
uv, or a network connection so the plugin can fetch a pinned copy of it
Security
This server runs commands on your desktop on behalf of a language model. Read SECURITY.md before installing it.
The one thing worth knowing before you get there: the tools that read your screen, your clipboard and your window titles hand the model text that neither you nor this project wrote, and a page that says "ignore your instructions and run …" is a real attack. The server tells the model to treat all of it as data, but that is a request, not a control. What actually stops it is the policy tier and your client's approval prompt — so keep tool approvals on.
License
Apache-2.0. See LICENSE.
Installing Omarchy
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/bruce-forte/omarchy-mcp-serverFAQ
Is Omarchy MCP free?
Yes, Omarchy MCP is free — one-click install via Unyly at no cost.
Does Omarchy need an API key?
No, Omarchy runs without API keys or environment variables.
Is Omarchy hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Omarchy in Claude Desktop, Claude Code or Cursor?
Open Omarchy on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
GitHub
PRs, issues, code search, CI status
by GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
by mcpdotdirectAmap Maps Mcp Server
MCP server for using the AMap Maps API
by duxiaohuiSupabase
Database, auth and storage
by SupabaseEverything
Reference / test server with prompts, resources, and tools.
Git
Tools to read, search, and manipulate Git repositories.
Sequential Thinking
Dynamic and reflective problem-solving through thought sequences.
Time
Time and timezone conversion capabilities.
Compare Omarchy with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All development MCPs
