Skip to main content
Glama

Paseo MCP

Let an orchestrator agent see and steer your Paseo agents.

An MCP server that gives Hermes, Claude Code or any other MCP client access to the coding agents running in your local Paseo: list them, read what they did, start new ones, send follow-ups, wait for results, answer permission requests.

Paseo MCP Node License


Why

Paseo is a great place to run many Claude Code chats side by side, but it is built for a human in front of the app. When you want one agent to hand work to others (an assistant that reads your mail and starts a coding agent for a bug report, or a lead agent that splits a task), that agent needs a way into Paseo.

This server is that way in. Everything it starts is a normal Paseo chat: you see it in the app, on your phone, and can take over at any time.

It runs in one of two ways, with the same tools and the same agent events:

stdio

Paseo plugin

Runs as

a child process of your MCP client

part of the Paseo daemon

Transport

stdin/stdout

Streamable HTTP at http://127.0.0.1:6790/mcp

Configured in

~/.config/paseo-mcp/config.json

Settings → Plugins → paseo-mcp

Access

whoever can start the process

a bearer token per client, scoped read or full

Set up

Install, or the plugin's launcher ~/.local/bin/paseo-mcp

Plugin mode

Related MCP server: Kranz MCP

Features

Overview

List agents with status, account, and whether they need you (finished, failed, waiting for a permission).

Readable transcripts

The tail of a chat as a compact transcript: user, agent, tool calls, todos, errors.

Start agents

New Claude Code chats with a first prompt, model, thinking level, permission mode, account and optional git worktree branch.

Steer running agents

Send follow-ups; for a running turn choose steer (inject) or interrupt (restart with the message).

Wait for results

Block until a turn ends, fails, or needs a permission, and get the final message back.

Events instead of polling

Agent created, turn started/ended (finished, error, limit), permission requested/resolved, archived, closed. As a long-poll tool with a cursor, as MCP resource notifications, or (plugin) as signed webhooks.

Guard rails

Agents only start inside allowedRoots, everything gets an origin label, a read-only mode, and agent output is marked as data, not instructions.

Paseo plugin

Optional: serve the tools over HTTP from inside Paseo, with per-client tokens and a settings screen. It also keeps a stable stdio launcher, tests the connection and hands out ready client configs.

Tools

Tool

What it does

paseo_list_agents

Agents with status, account, attention flag. Filters: status, labels, archived

paseo_get_agent

One agent's state including pending permission requests

paseo_read_timeline

Last N timeline entries as a compact transcript

paseo_list_accounts

The Claude accounts from your config

paseo_list_models

Models of a provider as claude/<model-id>

paseo_wait_for_events

Agent events since a cursor; waits up to timeout_sec until one happens. See Events

paseo_create_agent

Start a new agent (cwd must be inside allowedRoots)

paseo_send_message

Send a follow-up (when_running: steer or interrupt)

paseo_wait_for_agent

Wait until the turn is done, failed, needs a permission, or times out

paseo_respond_permission

Allow or deny a permission request

paseo_archive_agent

Archive a finished agent

Agent ids can be passed in full or as a unique prefix, like the short ids paseo ls shows. In read-only mode, and for plugin tokens with the read scope, only the first six tools exist (the list/read tools and paseo_wait_for_events). paseo_list_agents also returns events_cursor, the starting point for events.

Resources: paseo://agents (all active agents) and paseo://agents/{id} (one agent), both JSON and subscribable, see Events.

Requirements

  • Paseo 0.15 or newer, running (desktop app or paseo daemon start)

  • Node 22 or newer

  • For plugin mode: plugins enabled in Paseo, and npm on the daemon's PATH (Paseo runs npm ci once on install)

  • An MCP client: Hermes, Claude Code, …

Install

git clone https://github.com/MaxGierlachowski/paseo-mcp.git
cd paseo-mcp
npm install          # also builds dist/

mkdir -p ~/.config/paseo-mcp
cp config.example.json ~/.config/paseo-mcp/config.json

Then edit ~/.config/paseo-mcp/config.json (see below) and connect your client.

Installed the plugin? Then you don't need a clone: the plugin keeps a stable launcher at ~/.local/bin/paseo-mcp and shows ready-made stdio configs, see stdio clients through the plugin.

Hermes

~/.hermes/config.yaml:

mcp_servers:
  paseo:
    command: "node"
    args: ["/path/to/paseo-mcp/dist/index.js"]
    env:
      PASEO_MCP_CONFIG: "/Users/<you>/.config/paseo-mcp/config.json"
    timeout: 300          # paseo_wait_for_agent blocks up to timeout_sec
    connect_timeout: 30

Hermes only passes PATH and HOME to MCP servers; anything else belongs under env.

Claude Code

claude mcp add paseo-agents -- node /path/to/paseo-mcp/dist/index.js

Named paseo-agents because agents running inside Paseo already have an MCP server called paseo.

Configuration

~/.config/paseo-mcp/config.json (another path via PASEO_MCP_CONFIG):

Key

Default

Meaning

daemonUrl

ws://127.0.0.1:6767/ws

WebSocket of the Paseo daemon

password

–

Daemon password, if set. PASEO_PASSWORD wins. Without one, the server signs in with $PASEO_HOME/local-credential (loopback daemons only), like the paseo CLI

allowedRoots

required

Agents may only start inside these folders (symlinks resolved)

defaultProvider

–

provider/model, e.g. claude/claude-sonnet-5-5. Check with paseo_list_models

defaultThinking

–

e.g. low, medium, high

defaultMode

–

Permission mode: auto, default, acceptEdits, plan

accounts

{}

Name → { configDir, note? }, one entry per Claude login

defaultAccount

–

Account used when the caller names none

accountEnvVar

CLAUDE_CONFIG_DIR

Env var set on new agents to the account's configDir

originLabel

hermes

Value of the origin label on every agent this server starts

readOnly

false

Only list/read tools. Also PASEO_MCP_READONLY=1

events

on

{ enabled, previewChars, bufferSize, ttlMinutes }, defaults true, 0, 1000, 60. See Events

Accounts and paseo-multi-account

When an agent is started with an account, the server sets accountEnvVar to that account's configDir for the new agent and adds the label account=<name>. With the default CLAUDE_CONFIG_DIR, Claude Code runs on exactly that login.

If you use paseo-multi-account, the plugin already decides which login Claude runs on and moves chats when you switch. Let it stay in charge: set accountEnvVar to a name Claude Code ignores (e.g. PASEO_CLAUDE_ACCOUNT). The account then only becomes a label you can filter by, and the plugin's switch applies to agents started over MCP like to any other chat.

Plugin mode

Installed as a Paseo plugin, paseo-mcp serves the same tools over Streamable HTTP from inside the Paseo daemon. There is no process to start and no config file: everything is set on the plugin's settings screen. The stdio mode above keeps working unchanged next to it.

Install

paseo plugin install github:MaxGierlachowski/paseo-mcp

On macOS the CLI ships inside the app: /Applications/Paseo.app/Contents/Resources/bin/paseo. Paseo asks you to trust the plugin (plugins run unsandboxed), clones it, runs npm ci --omit=dev and loads it. Plugins must be enabled ("pluginsEnabled": true in ~/.paseo/config.json). The HTTP server stays off until you turn it on.

If another plugin already uses the id paseo-mcp (for example the earlier local "Paseo MCP for Hermes" build), Paseo refuses the install; remove that one first or pass --id. This plugin covers everything the local build did and takes over its launcher.

Set up

  1. Open the settings: ⌘K → MCP server (also finds "hermes", "token", "config"), or Settings → Plugins → paseo-mcp.

  2. Under Agents, add the folders agents may start in (allowedRoots). Without one, paseo_create_agent refuses every start.

  3. Under New token, create one token per client: a label (e.g. Hermes), a scope, optionally an origin label. Copy the token; it is shown once.

  4. Turn on Enabled. The status row shows the URL, or why the server could not start (e.g. the port is taken).

  5. Copy the snippet for your client from Connect a client.

Setting

Default

Meaning

Enabled

off

Run the HTTP server inside the daemon

Port

6790

Any free port except the daemon's own 6767

Bind address

127.0.0.1

Anything else makes the server reachable from your network; the screen warns

Tokens

–

Label, scope (read or full), optional origin label. Stored as SHA-256 hashes

Read-only

off

Only the list/read tools, for every token

Allowed roots

–

Folders agents may start in (symlinks resolved)

Provider, Thinking, Permission mode

–

Defaults for new agents, like defaultProvider, defaultThinking, defaultMode

Origin label

token label

origin=… on agents started by tokens without their own origin label

Account env var

CLAUDE_CONFIG_DIR

Like accountEnvVar above

Claude accounts, default account

–

Like accounts and defaultAccount above

Changes apply at once: port, bind address and the switch restart the listener; everything else is read on the next tool call. The daemon connection needs no setup: the plugin reaches its own daemon over loopback with the local credential Paseo writes to $PASEO_HOME/local-credential, the way the paseo CLI does.

Connect

Hermes, in ~/.hermes/config.yaml:

mcp_servers:
  paseo:
    url: "http://127.0.0.1:6790/mcp"
    headers:
      Authorization: "Bearer <token>"
    timeout: 300          # paseo_wait_for_agent blocks up to timeout_sec

Claude Code, as a command or in a project .mcp.json:

claude mcp add --transport http paseo-agents http://127.0.0.1:6790/mcp --header "Authorization: Bearer <token>"
{ "mcpServers": { "paseo-agents": { "type": "http", "url": "http://127.0.0.1:6790/mcp", "headers": { "Authorization": "Bearer <token>" } } } }

Any other client: Streamable HTTP, URL http://127.0.0.1:6790/mcp, header Authorization: Bearer <token>.

stdio clients through the plugin

Clients that start the server as a child process don't need the HTTP server or a token. The plugin helps there too:

  • A stable command. On every load the plugin writes ~/.local/bin/paseo-mcp (another path via PASEO_MCP_LAUNCHER), a two-line script that starts dist/index.js of the current plugin checkout. Git installs land in a new folder on every update; the launcher follows them, so client configs never change. It never overwrites a file it did not write.

  • Test connection. Starts the server exactly like a client does (only PATH, HOME and PASEO_MCP_CONFIG), lists the tools and counts the agents.

  • Status. Which config file the stdio server reads, its allowed roots and accounts, and how it signs in to the daemon. The local credential is shown masked and never leaves the daemon.

  • Copyable configs for Hermes (command:), Claude Code (.mcp.json and claude mcp add) and any stdio client.

The stdio server keeps its own config file (~/.config/paseo-mcp/config.json); the screen shows it but never edits it.

Security model

  • Off by default. Turned on, the server binds to 127.0.0.1. Other addresses must be entered explicitly and come with a warning on the screen.

  • A token on every request, also from localhost. Missing or wrong: 401. Tokens are 32 random bytes (pmcp_…), stored only as SHA-256 hashes with createdAt, compared in constant time. When a token was last used is kept in $PASEO_HOME/paseo-mcp.json.

  • Revoking a token removes its hash; its next request gets 401 and its open sessions are closed at once.

  • Scopes. A session registers only the tools of its token's scope: read gets the list/read tools and events, full everything. The global read-only switch applies on top. A token can also be limited to agents with certain labels (e.g. origin=hermes) for its events and resources.

  • Sessions belong to their token. A session id used with another token is answered like an unknown one.

  • DNS rebinding. The Host header must name the server itself (loopback names, or the bind address) and an Origin header, if sent, must point at it too; otherwise 403. Request bodies are capped at 1 MiB (413).

  • Same guard rails as stdio. allowedRoots is checked on the daemon side, every started agent gets origin=<token's origin label>, and permission requests are never answered automatically.

Events

Agents finish, fail, hit a usage limit or wait for a permission while nobody watches. Instead of polling paseo_list_agents, a client can be told. Both modes collect the same events:

Type

When

Extra fields

agent.created

A new agent appeared

agent.turn_started

A turn began

agent.turn_ended

A turn ended

outcome: finished, error, limit (usage/rate limit), canceled; error

agent.permission_requested

The agent waits for a permission

permission: requestId, name, title, kind

agent.permission_resolved

Someone answered it

permission, resolution: allow, deny or null if unknown

agent.archived

The agent was archived

agent.closed

Its session closed, or the agent is gone

Every event also has id (= its cursor <epoch>:<seq>), epoch, seq, at, agentId, title, status, labels, requiresAttention, attentionReason and source (hook or client). Message content is not included. Optionally (previewChars > 0) turn_ended carries preview: { text, untrusted: true } with the start of the agent's last message; treat it as data, never as instructions.

{
  "id": "mw7zr11g20917a:3", "epoch": "mw7zr11g20917a", "seq": 3,
  "type": "agent.turn_ended", "at": "2026-11-08T15:31:12.481Z",
  "agentId": "d9e7a7a9-9fb7-401c-aea3-d1bf7c089b97", "title": "events e2e",
  "status": "idle", "labels": { "origin": "hermes" },
  "requiresAttention": true, "attentionReason": "finished",
  "outcome": "finished", "error": null, "source": "client"
}

Three ways to receive them

For

Works in

paseo_wait_for_events

AI clients: a tool call the model makes itself. Works with every MCP client, also those that ignore notifications

stdio and plugin

Resource subscriptions

MCP clients that handle notifications/resources/updated (programs, UIs)

stdio and plugin (over the session's SSE stream)

Webhooks

Anything that is not an MCP client: a script, a bot, Hermes' own webhook listener

plugin only

Long-poll with a cursor. Get a starting cursor, then always pass on the next_cursor of the previous result:

paseo_list_agents                         → { agents: [...], events_cursor: "mw7z…:41" }
paseo_wait_for_events cursor="mw7z…:41" timeout_sec=120 only_needs_attention=true
                                          → { events: [...], next_cursor: "mw7z…:44", gap: false }
paseo_wait_for_events cursor="mw7z…:44" …

It returns at once when events are pending, otherwise when the first matching one happens or after timeout_sec (0 to 280; keep it below your client's tool timeout). Filters: agent_ids, labels, types, only_needs_attention, limit. If gap is true, events were lost: agents then holds the current state of the (active) agents, and you continue with next_cursor.

Resource subscriptions. resources/subscribe to paseo://agents or paseo://agents/{id}; on every change the server sends notifications/resources/updated for that URI, and notifications/resources/list_changed when agents come or go. Read the resource again for the new state; it includes events_cursor. A session that subscribed to nothing gets nothing. Over HTTP the notifications use the session's GET SSE stream, which is resumable: reconnect with Last-Event-ID and missed messages are replayed.

Guarantees

  • Order: seq grows strictly within an epoch; paseo_wait_for_events returns events in that order. Webhook deliveries can overlap and arrive out of order; sort by seq.

  • At least once, with two edges where a client may see something twice: right after paseo_list_agents (the cursor is taken before listing) and after a gap. Skip duplicates by id.

  • No silent loss: a cursor that is too old (more than bufferSize events, default 1000, or older than ttlMinutes, default 60) or from an earlier epoch is answered with gap: true and the current state, never with a partial list.

  • Restart: the buffer lives in memory. A restarted stdio server or a reloaded plugin starts a new epoch; every old cursor becomes a gap.

  • One event per happening: in plugin mode, events come from Paseo's lifecycle hooks and the daemon's push updates. Whichever source is first wins; the other is dropped. stdio mode uses push updates only and does not report closed after an archive.

Webhooks (plugin mode)

Settings → Plugins → paseo-mcp → New webhook: label, URL, which events (needs attention / turns / all), optional label filter, optional bundling window. The secret is shown once. It is kept on the daemon ($PASEO_HOME/paseo-mcp.json, mode 600); the settings hold only its last four characters.

Each delivery is a POST with Content-Type: application/json:

{ "delivery_id": "mw7z…:3", "sent_at": "2026-11-08T15:31:12.502Z", "events": [ { "id": "mw7z…:3", "type": "agent.turn_ended", "…": "…" } ] }

Header

Meaning

Paseo-Mcp-Signature

t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>

Paseo-Mcp-Delivery

Delivery id, the same on every retry: use it to ignore repeats

Paseo-Mcp-Event-Id

Id of the first event in the delivery

Verify the signature over the raw body before parsing it, and reject old timestamps:

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, header, rawBody, toleranceSec = 300) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
  const mac = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(v1 ?? "", "hex");
  return given.length === mac.length && timingSafeEqual(given, mac);
}
import hmac, hashlib, time

def verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > tolerance:
        return False
    mac = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, parts.get("v1", ""))
  • Answer 2xx quickly. Network errors, timeouts (10 s), 408, 429 and 5xx are retried up to 6 times with exponential backoff (1, 2, 4, 8, 16 s, plus jitter). Other 4xx are not retried.

  • Auto-pause: after 5 failed deliveries in a row the endpoint pauses; events are skipped (counted) until you press Resume. Test sends a webhook.test event regardless of filters and pause.

  • URLs: https:// anywhere; plain http:// only to localhost or private addresses (10.x, 172.16–31.x, 192.168.x, fc00::/7). No credentials in URLs; redirects are not followed.

  • The screen shows the last deliveries with status, attempts and HTTP code; Rotate replaces the secret at once.

Configuration

  • Plugin: Settings → Events: message preview length. Webhooks as above. Tokens can carry a label filter.

  • stdio: events in config.json, e.g. "events": { "previewChars": 200 }; "enabled": false turns events off (the tool and resources then do not exist). The stdio server now connects to the daemon at start, not at the first tool call, so it sees everything from then on.

How it works

flowchart LR
    A[Orchestrator<br/>Hermes, Claude Code] -- MCP over stdio --> B[paseo-mcp]
    B -- WebSocket<br/>@getpaseo/client --> C[Paseo daemon]
    C --> D[Claude Code agents]
    C --> E[Paseo app<br/>desktop and phone]
  1. The client starts paseo-mcp as a child process and talks MCP over stdin/stdout.

  2. At start the server connects to the Paseo daemon with the official client library, subscribes to agent updates for events, and reconnects if the daemon restarts.

  3. Tool calls map one to one onto the daemon's agent API (agents.list, create, send, waitForFinish, …). The only state of its own is the in-memory event buffer.

  4. Before an agent starts, cwd is resolved (including symlinks) and checked against allowedRoots; account env, origin and account labels are attached.

In plugin mode the client talks Streamable HTTP to the plugin, which runs inside the daemon and opens one McpServer per session:

flowchart LR
    A[MCP client<br/>Hermes, Claude Code] -- HTTP /mcp<br/>Bearer token --> B[paseo-mcp plugin<br/>inside the Paseo daemon]
    B -- loopback WebSocket<br/>local credential --> C[Paseo daemon]
    C -- lifecycle hooks<br/>push updates --> B
    B -- signed webhooks --> F[Your endpoints]
    C --> D[Claude Code agents]

Security

Starting agents with repository and tool access is powerful. Keep in mind:

  • Text from mail, tickets or web pages is untrusted input. Have your orchestrator ask you before it starts an agent from such content, and never pass it through unfiltered as a prompt.

  • Transcripts and final messages are returned with a note to treat them as data, not instructions.

  • Only let the orchestrator call paseo_respond_permission with allow after you confirmed, unless the request is clearly read-only.

  • To start, use readOnly: true and let the orchestrator only watch.

Development

npm run build                          # compile server/ to dist/
npm test                               # build, then unit and HTTP integration tests
npm run typecheck                      # the plugin entry points and settings screen
SMOKE_COMMAND=~/.local/bin/paseo-mcp npm run smoke   # same, through the launcher
PASEO_MCP_CONFIG=~/.config/paseo-mcp/config.json \
  npm run smoke -- paseo_list_accounts 'paseo_list_agents={"limit":5}'

smoke.mjs starts the server like an MCP client would (only PATH, HOME, PASEO_MCP_CONFIG) and calls the tools you name (tool=<json args>); it exits non-zero when a tool returns an error.

Layout: server/ holds the tools (tools.ts), the HTTP transport (http.ts, auth.ts) and the stdio entry (index.ts, built to dist/index.js); server/plugin/ the daemon side of the plugin; shared/ the settings and RPC contracts; client/ the settings screen. index.server.ts and index.client.tsx are the plugin entry points, compiled by Paseo itself. To try the plugin from a checkout, run npm ci --ignore-scripts && npm run build && npm prune --omit=dev in a copy of it (a directory install skips the manifest's build step, a git install runs exactly these) and paseo plugin install <dir>; test against a separate daemon with --home <tmp-dir> rather than your everyday one.

Tested against Paseo daemon 0.15 and @getpaseo/client 0.15.

Internals of the plugin are in OVERVIEW.md, changes in CHANGELOG.md.

License

Apache 2.0

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      Enables coding agents from different vendors on the same machine to delegate tasks to each other over the A2A protocol, while also allowing the machine to act as an A2A peer.
      7
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Connects coding agents to local development services managed by Kranz. Agents can inspect service health, ports, logs, and run history, and perform lifecycle actions through the same live runtime as the TUI and CLI.
      25
      62
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables an OAuth-protected client to discover Paseo workspaces, dispatch Codex, Claude Code, or OpenCode tasks on a Mac, recover exact request results, and subscribe to agent messages and attention events.
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Lets agents on one Paseo host discover, read, create, prompt, stop, and archive agents running on other paired Paseo daemons, and answer their permission requests. Delivers finish, error, and permission notifications back to the calling agent across machines, with per-peer read/write and path-based access control.
      807 npm
      2
      MIT