---
url: https://hyprpilot.kilic.dev/runtime/mcp-http.md
---

# {{ $frontmatter.title }}

Every in-tree MCP server speaks stdio by default, spawned by the vendor CLI that hyprpilot launched. `--transport http` serves the same surface over a port instead, for clients that are **not** that vendor — another editor, a second machine, a script.

```sh
hyprpilot mcp skills  --transport http --listen 127.0.0.1:7777 --skill-dir '{"dir":"~/.config/hyprpilot/skills","ignore":[],"watch":true}'
hyprpilot mcp serve   --transport http --listen 127.0.0.1:7778
hyprpilot mcp harness --transport http --listen 127.0.0.1:7779 --token-file ~/.config/hyprpilot/mcp-token
hyprpilot mcp passthrough --transport http --listen 127.0.0.1:7780 --tool '{"name":"decide","inputSchema":{"type":"object"},"url":"http://127.0.0.1:8080/decide"}'
```

| Flag                      | Meaning                                                                                                               |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `--transport stdio\|http` | Default `stdio`. Nothing about the stdio path changes.                                                                |
| `--listen <ADDR>`         | Required with `--transport http`. No default: each server needs its own port, and an ephemeral one addresses nothing. |
| `--token-file <PATH>`     | Bearer token clients must present. Overrides `HYPRPILOT_MCP_TOKEN`.                                                   |
| `--allow-remote`          | Answer requests whose `Host` is not a loopback name.                                                                  |

::: warning This is a server you run, not one hyprpilot starts

The launcher still auto-injects a **stdio** entry for every server your `mcp` config enables, and still starts nothing else. An HTTP server has no supervisor here: you run it, you restart it, and when it dies every harness session dies with it.

:::

## Wiring a client to it

Auto-injection is unchanged, so point clients at the URL yourself — the standard `mcpServers` shape, which all three vendors already understand:

```json
{ "mcpServers": { "skills-http": { "url": "http://127.0.0.1:7777/mcp" } } }
```

::: danger The reserved name replaces your entry

Naming that catalogue entry `hyprpilot-skills` (or whatever `[mcp.<server>] name` resolves to) does **not** work for a hyprpilot-launched session: the launcher drops any configured server matching a reserved name and inserts its own stdio entry at the front. It warns, but the launch still uses stdio.

For a hyprpilot-launched vendor to reach the HTTP server, either use a different catalogue key (as above) or turn the auto-injected one off with `[mcp.skills] enabled = false`.

:::

## Authentication is optional

With no `--token-file` and no `HYPRPILOT_MCP_TOKEN`, the server is **unauthenticated** and the startup log says so at `warn`. That is a supported configuration and it is your call:

* **Every local process can reach a loopback port**, and on the harness that means calling `spawn`, which runs an arbitrary binary as you.
* **Every client shares the harness's sessions.** They all appear in `session_list`, and any client can `session_send`, steer or kill a conversation another one started.

There is no config key for the token. The `mcp` subcommands deliberately never load config — that is what keeps a broken `[[profiles]]` list from killing a sidecar — and there is no `--token <value>` flag either, because argv is world-readable through `/proc`.

The 401 is a bare one. This is a single shared token, not OAuth, so it carries no `WWW-Authenticate` pointing at RFC 9728 resource metadata: advertising a discovery document for an endpoint that does not exist would be worse than saying nothing.

## What the browser guard does

`Origin` validation is **on**, allowing only this server's own origin. A cross-origin `fetch()` from any page you happen to have open is refused with `403` before it reaches a handler:

```console
$ curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:7777/mcp \
    -H 'Origin: http://evil.example' -H 'Mcp-Method: initialize' -d '{...}'
403
```

`Host` validation is separate and defaults to loopback names. `--allow-remote` widens **both** the reachable bind and that check — without it a non-loopback bind accepts the connection and then refuses every request, which reads as a bug rather than a policy.

## Notifications degrade, and the cache ttl says so

MCP `2026-07-28` removed sessions (SEP-2567), so rmcp serves every request of that revision **statelessly**: the peer a request carries dies with its response. Consequences, all of them deliberate:

| Channel                                                           | Over HTTP                                                                                                                                                                                                                                                                                               |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resources/updated`, `resources/list_changed` to a **subscriber** | Works. A `subscriptions/listen` stream's sink outlives the request that opened it, and every clone of the handler shares one registry.                                                                                                                                                                  |
| The same, **broadcast** to a non-subscriber                       | Gone. There is no ambient peer to broadcast through.                                                                                                                                                                                                                                                    |
| `notifications/claude/channel` (harness turn finished)            | Gone, and not advertised over HTTP. Rides a peer captured at startup.                                                                                                                                                                                                                                   |
| SEP-2663 tasks themselves                                         | Only for a client on `2026-07-28` that attaches `io.modelcontextprotocol/clientCapabilities` to each request — statelessness leaves nowhere else to keep it. Otherwise `spawn` returns its ordinary result and no task is minted. Tasks are polled with `tasks/get`; there is no push on any transport. |

So **every HTTP result carries `ttlMs: 0`** where the stdio path carries 24 hours. The long ttl is honest only because every mutable surface fires an invalidation the client actually receives; over HTTP a non-subscribing client would be caching for a day against a notification that never comes.

An HTTP harness caller therefore polls `session_status`, or subscribes.

## Building without it

`--transport http` lives behind a cargo feature that is **on by default**. `cargo build --no-default-features` compiles it out; the flag still parses and says what to do:

```console
$ hyprpilot mcp skills --transport http --listen 127.0.0.1:7777 --skill-dir '{...}'
Error: mcp: this hyprpilot was built without the `http` feature, so `--transport http` cannot serve. Rebuild with `--features http` (it is on by default).
```
