MCP server
v2 Available since 0.2.0. Hallway’s MCP server gives agents sixteen typed tools
for reading what’s running, describing apps and managing bookmarks. It is a convenience layer over the same store the
CLI, the HTTP API and your own hand edits go through: never a separate code path, and never the only way to do
something. An agent that can edit files or run hallway already has everything MCP offers, and more.
What it isn’t: a process manager. No MCP tool starts, stops, restarts or installs anything (see Describe, never launch).
Connecting
Section titled “Connecting”The server runs in the daemon, on the daemon’s own port, at /mcp: http://localhost:4255/mcp by default. It
uses MCP’s Streamable HTTP transport, so there is one service and one port to remember, and because it shares the
daemon’s store and file watcher, every write through MCP shows up live on every open dashboard.
It speaks the 2026-07-28 protocol revision, which is stateless (each request stands alone), and also serves clients of
the 2025 revisions, which open with an initialize handshake, statelessly too.
With Claude Code, over HTTP:
claude mcp add --transport http hallway http://localhost:4255/mcpIf your daemon runs on another port (the port key in config.yaml), use that in the URL.
In fronted mode, the dashboard’s own address works too: https://hallway.localhost/mcp, or
https://<base>/mcp on a custom domain (domain.dashboard in config.yaml). The localhost URL keeps working in both
modes and doesn’t depend on Caddy running, so it’s the one to register.
Over stdio
Section titled “Over stdio”For clients that prefer to launch a stdio server, hallway mcp is a thin bridge: it forwards every message to the
running daemon’s /mcp and writes the replies back, so the daemon stays the only writer. It finds the daemon on the
port in config.yaml, so it needs no URL.
claude mcp add hallway -- hallway mcpOther clients
Section titled “Other clients”Any MCP client that supports Streamable HTTP can connect to http://localhost:4255/mcp (or your daemon’s port) with no
headers and no token. Clients that only launch stdio servers can run hallway mcp as the command, with no arguments.
Reading:
| Tool | Input | Returns |
|---|---|---|
get_status | { filter? } | Apps with state, URL and uptime; counts by state; how many listeners are observed; what was lost at the last reboot, read-only |
list_apps | { source?, state?, tag? } | Apps with their config and live state |
get_app | { slug } | One app in full, with identity and recent history |
get_history | { slug, window: '24h' | '7d' | '90d' } | Uptime buckets and a latency summary |
list_observed | {} | Observed listeners, with identity |
list_bookmarks | { group? } | Bookmark groups and their links |
Apps:
| Tool | Input | Effect |
|---|---|---|
declare_app | { slug, port, name?, description?, host?, expected?, health?, tags?, links?, icon? } | Writes a new apps/<slug>.yaml. Creates only: fails if the app already exists |
update_app | { slug, patch, etag? } | Applies patch as a JSON merge patch (null removes a key; lists are replaced whole). Can’t add a service block or change port |
remove_app | { slug, etag? } | Moves the file to backups/removed/. Refused for an app Hallway runs as a service: use hallway app rm |
Observed listeners:
| Tool | Input | Effect |
|---|---|---|
claim_observed | { id, slug, name?, expected? } | The same as Claim in the dashboard: writes apps/<slug>.yaml from the observation |
ignore_observed | { id, by: 'port' | 'process' | 'dir' } | Adds it to ignore.yaml |
Bookmarks:
| Tool | Input | Effect |
|---|---|---|
upsert_bookmark_group | { group, title?, order?, collapsed?, etag? } | Creates or updates a group |
add_bookmark | { group, url, title?, description?, icon?, tags?, etag? } | Adds a link |
update_bookmark | { group, url | index, patch, etag? } | Edits a link, found by its URL or its position |
remove_bookmark | { group, url | index, etag? } | Removes a link |
remove_bookmark_group | { group, etag? } | Moves the group’s file to backups/ |
Writes, ETags and conflicts
Section titled “Writes, ETags and conflicts”Every write goes through the same store as the HTTP API, the CLI and the dashboard, so it is validated the same way,
written atomically, and keeps the comments and key order of the YAML file it edits. It returns the file’s new
etag.
Pass that etag (or the one from an earlier read) to a later write for optimistic concurrency: if the file changed
in the meantime, whether by you in an editor, another agent or the dashboard, the write fails with a conflict and
changes nothing. Re-read, then try again. Without an etag, the write applies to whatever is there.
Describe, never launch
Section titled “Describe, never launch”Two data sets carry very different risks:
- Bookmarks are inert, so full create, update and delete is safe.
- Apps can be described: declared, edited, claimed, removed. But no MCP tool starts, stops, restarts or installs a process. A tool that registers “run this command” would be an arbitrary-code-execution surface for any agent that has the server configured, outside the shell’s own permission prompts.
So:
declare_appandupdate_apprefuse aserviceblock.update_apprefuses to changeport: apps hardcode their ports, so a changed port breaks things elsewhere (see Your ports stay put). Change it yourself if you really mean to.remove_apprefuses an app Hallway runs as a service, because removing it would stop a process.get_statusreports what was lost at the last reboot, but restarting any of it ishallway lost restartor the dashboard, never MCP.run,start,stop,restart,service,lost,thumbs,setupandteardownare CLI-only, where you, or an agent in your shell under its own permission prompts, run them explicitly. An agent with a shell starts servers withhallway run.
A refusal comes back as a tool error that says why and names the shell command to use instead.
A restart_app tool, limited to services already installed through the CLI, was considered and rejected: it would
still start a process.
Resources
Section titled “Resources”Two resources carry the same data as the dashboard, as JSON:
| URI | Contents |
|---|---|
hallway://state | Everything the dashboard shows: apps, bookmarks, observed listeners, problems |
hallway://apps/<slug> | One app, as get_app returns it |
Clients of the 2026-07-28 revision can subscribe (subscriptions/listen) and receive
notifications/resources/updated whenever a resource changes, including after a hand edit to a file.
Security
Section titled “Security”The localhost security rules apply to /mcp exactly as to the rest of the daemon,
as MCP’s own guidance for Streamable HTTP servers asks:
- The daemon listens on
127.0.0.1only. - A request whose
HostorOriginisn’t local gets403. This is what stops DNS rebinding and other web pages. - Requests must be JSON (
Content-Type: application/json).
There is no auth token. The trust boundary is the same as the data home’s: anything running as you can already
edit ~/.hallway directly, so a token would add friction without adding protection. That holds because MCP can’t
start processes; it would be revisited if that ever changed.
Troubleshooting
Section titled “Troubleshooting”The daemon isn’t running. The tools live in the daemon, so an HTTP client gets a connection error, and
hallway mcp exits with status 1 and a message saying so. Start the daemon with hallway service install (it then
runs at every login) or hallway serve (in the foreground), then reconnect or restart the MCP server in your client.
hallway doctor shows whether the daemon’s service is installed and running.
The daemon is on a different port. The HTTP URL must use the daemon’s port: check the port key in
~/.hallway/config.yaml and re-register with that URL. hallway mcp reads config.yaml itself, so it follows the
port without changes. ($PORT in your shell doesn’t move it: that is usually some app’s port, not Hallway’s.)
A write fails with a conflict. The file changed since the etag you passed was read. Read it again and retry.