Skip to content
Hallway is out (npm i -g hallway): v0, v1 and v2 have shipped, apart from checking it all on a real Mac. See the roadmap.

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).

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:

Terminal window
claude mcp add --transport http hallway http://localhost:4255/mcp

If 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.

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.

Terminal window
claude mcp add hallway -- hallway mcp

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:

ToolInputReturns
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:

ToolInputEffect
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:

ToolInputEffect
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:

ToolInputEffect
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/

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.

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_app and update_app refuse a service block.
  • update_app refuses to change port: 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_app refuses an app Hallway runs as a service, because removing it would stop a process.
  • get_status reports what was lost at the last reboot, but restarting any of it is hallway lost restart or the dashboard, never MCP.
  • run, start, stop, restart, service, lost, thumbs, setup and teardown are 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 with hallway 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.

Two resources carry the same data as the dashboard, as JSON:

URIContents
hallway://stateEverything 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.

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.1 only.
  • A request whose Host or Origin isn’t local gets 403. 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.

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.