Skip to content

MCP clients

You’ll connect an MCP client, such as Claude Code on your laptop, to the hub so it can chat with project leads.

  1. On the hub, create an agent key and keep it in a file only you can read:

    (umask 077; fleetd hub key create --name laptop --role operate --machines '*' > ~/.fleetd/agent.key)

    The key is printed once, so this file is the only copy. The command also prints a line like this:

    created key_tjw34bxe (fld_mwrgw4, operate:*). The key above is shown only once.
  2. Copy the key to the machine that runs Claude Code, readable only by you. For example, from that machine:

    (umask 077; ssh hub 'cat ~/.fleetd/agent.key' > ~/.fleetd/agent.key)

    hub is the hub machine’s SSH name. Create ~/.fleetd first if this machine has no fleetd.

  3. Add the hub’s MCP endpoint, <hub url>/mcp, to Claude Code:

    claude mcp add --transport http fleetd http://hub.your-tailnet.ts.net:4790/mcp \
    --header "Authorization: Bearer $(cat ~/.fleetd/agent.key)"
  4. In Claude Code, ask it how a project is doing. It reads the chat with chat_history and the status with fleetd_status.

claude mcp add writes to your Claude config. --scope picks where:

Scope Where it applies Note
local (default) This folder, only for you.
project Everyone who opens this repo: written to .mcp.json. Don’t use with a key in --header: you’d commit the key.
user All your folders.

The endpoint speaks MCP over HTTP. A call needs a session: initialize, then tools/call with the session id. This script does both. Save it as mcp.sh:

#!/bin/sh
# mcp.sh <tool> '<json arguments>'
URL=${FLEETD_HUB:-http://hub.your-tailnet.ts.net:4790}/mcp
AUTH="Authorization: Bearer $(cat "$FLEETD_KEY_FILE")"
post() { curl -s "$URL" -H "$AUTH" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" "$@"; }
SID=$(post -D - -o /dev/null -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"script","version":"1"}}}' | tr -d '\r' | awk -F': ' 'tolower($1)=="mcp-session-id"{print $2}')
post -H "Mcp-Session-Id: $SID" -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
post -H "Mcp-Session-Id: $SID" -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"$1\",\"arguments\":${2:-{\}}}}"

Then call a tool:

FLEETD_HUB=http://hub.your-tailnet.ts.net:4790 FLEETD_KEY_FILE=~/.fleetd/agent.key sh mcp.sh status_get '{}'

On a new hub with no projects yet, the output starts like this:

{"id":1,"jsonrpc":"2.0","result":{"content":[{"text":"{\"rows\":[]}", …
Command Needs
fleetd status, fleetd time Nothing on the hub machine. Elsewhere, --hub <url> --key-file <file>.
fleetd steer, fleetd steers Always --key-file with a human key. --hub <url> off the hub machine.
fleetd work --key-file, or FLEETD_KEY_FILE: any operate key, such as the one Claude Code uses. --hub <url> off the hub machine.

To make a human key file on the hub:

(umask 077; fleetd hub key create --human --name terminal > ~/.fleetd/human.key)
fleetd steers W41 --key-file ~/.fleetd/human.key

fleetd keeps one ledger of the work in flight on each repo: its agents, the PRs on its merge train, and what people claimed from their own sessions. A session that checks it first doesn’t build what an agent is already building.

Claude Code needs nothing more than the connection above. The hub’s MCP instructions, which Claude Code adds to every session’s system prompt, tell it to call work_claim before a code change and work_done when the change lands or is dropped, and to tell you when the work is taken. A session might go like this:

> add a dark mode toggle to settings
● fleetd - work_claim (repo: "git@gitlab.example.com:acme/shop.git", title: "dark mode toggle in settings",
branch: "ada/dark-mode", paths: ["app/theme.ts"])
⎿ status: possible_duplicate. may repeat T12 (an agent of devbox/shop): dark mode toggle for settings
(3 shared words: dark, mode, toggle)
● fleetd's agent T12 is already building a dark mode toggle for settings. Shall I wait for its PR, or is this
different work?
Tool Does Role
work_claim {repo | projectRef, title, base?, branch?, keys?, paths?, changed?, authorEmail?, notDuplicateOf?, notDuplicateWhy?, anyway?} Checks the intent against the work in flight on the repo’s base and claims it in one call. status is claimed; renewed (your claim on the same branch, updated); taken (someone else has the same issue, PR, branch or plan item); possible_duplicate (most of the same words, or the same words and files), or yours (fleetd’s own agent on its task’s branch: the task already holds it). Keys and paths a renewal adds are checked like a new claim’s. For a likely duplicate, call again with notDuplicateOf and notDuplicateWhy if it’s different work. With a taken item, anyway: true claims it regardless, but only with a human key: any other key may be one of fleetd’s own agents. operate
work_list {repo | projectRef, base?, paths?} What is in flight on the repo’s base, newest first, with each item’s keys and files (paths: false leaves out what each writer has changed so far, which asks its machine). Expired claims come last. recentLandings lists what reached the base outside the merge train in the last 24 h, and how. read
work_done {claimId, outcome, landing?, reason?} Ends your claim: landed (with its commit) or dropped. A human key can end anyone’s. operate

repo is the origin remote in any form, on any host: git@github.com:acme/shop.git, https://gitlab.example.com/g/shop, /srv/git/shop.git. With no base, the base is the project’s (or that of a project on the repo), else main. A claim lasts 12 hours after your last work_claim on it, then shows as expired for a day. Matches only list it once it has expired. When no project is known to be on the repo yet, the hub first reads the remotes of your projects that it hasn’t matched to a repo, waiting a few seconds. If none matches, the answer’s warning says the agents’ work may be missing from it.

From a terminal, or a session without the MCP, fleetd work does the same. It reads the repo, base, branch, user.email and changed paths (since the branch left the base, plus uncommitted and untracked files) from the checkout you run it in:

export FLEETD_KEY_FILE=~/.fleetd/agent.key
fleetd work claim "dark mode toggle in settings" --issue SHOP-12 # exit 0 claimed, 1 taken or a likely duplicate
fleetd work # what is in flight here
fleetd work done # the claim on this branch landed
Role Can
read Read chats, status, time and prompts.
operate Also write to leads, set a project’s max agents, and manage projects.
admin Also change hub settings, such as timeline retention.

--machines '*' gives the role on every machine; --machines devbox,laptop limits it to those.

Command Does
fleetd hub key list Lists keys.
fleetd hub key revoke <keyId> Ends a key at once, also on a running hub.

A human key (fleetd hub key create --human) is for a person’s device. Some tools, such as stopping a project, answering an approval or entering a secret, need one.

Tool Does Role
chat_send {projectRef, text, attachments?, clientRequestId} Write to a project’s lead. It starts a turn when the lead is idle, steers it when it works, or waits while it starts. The lead reads it as your words with a human key, as an agent’s with any other. operate
chat_history {projectRef, before?, after?, limit?} A page of a project’s chat, oldest first; up to 100 messages. Without before or after, the newest. read
approval_answer {requestId, decision, message?} Approve or deny an approval item: decision is approve or deny; message is a denial’s reason for the lead. operate, human key
secret_submit {requestId, sealed} Answer a secret item with a value sealed to the target machine. operate, human key
decisions_list {projectRef, limit?} Every decision in the project, project-wide or for one order, newest first: in force, replaced by a newer one, or revoked (by whom, when, why). orders names each order by its title. read
decisions_overturn {decisionId, value?, why} Take back a decision in force. Without value it is revoked and the lead asks again when it comes up; with value a new decision on the same question replaces it, as yours. The lead reads the change, and your why, at its next wake. operate, human key
requests_list {projectRef?, state?} Secret and approval requests, newest first, with their state. A secret’s value is never in it, only its fingerprint. read
project_stop {projectRef, reason?} Stop every running and queued agent of the project and tell the lead in its chat. Merged PRs, the lead and the chat stay. operate, human key
attachment_put {name, mime, dataB64, uploadId?} Upload a file (image, text or PDF, at most 25 MiB) for a message; up to 20 go with one message. operate
attachment_part {dataB64, uploadId?, offset?} One piece of a file too large for one request, in order: pass the uploadId it returns and the bytes sent so far as offset with the next piece, and the last piece to attachment_put with the uploadId. operate
attachment_get {projectRef, attachmentId} Read a file attached to a project’s chat. read
project_files {machineId, projectId, query} A project’s files, for @-mentions. read
status_get One row per project: its state word, its work in groups, agents running and the last merge. read
agents_list {projectRef?, endedSince?} Every agent of your projects: its role, task, what it’s doing now, when it started and its PR. read
agent_work {machineId, threadId, before?, limit?} A page of one agent’s recent messages, commands and edits, oldest first. Secrets show as [secret]. read
pr_list {projectRef, closedSince?, cursor?}, pr_get {repo, number} A project’s PRs, up to 60 a page (nextCursor, passed as cursor, reads the next), and one in detail: description, checks, reviews with their findings, its place in the merge queue and the agent that wrote it. read
pr_files {repo, number, offset?}, pr_diff {repo, number, path, offset?} A PR’s changed files, and one file’s diff a page at a time. read
events_list {projectRef, before?, types?, hubWide?, limit?} A project’s events, newest first, back through the last 30 days: agents started and finished, reviews, merges, requests. hubWide adds the hub’s own, such as a device paired. read
workstream_list, order_get, thread_tail, fleetd_status Lower-level views of the lead’s work. read

projectRef is {"machineId": "devbox", "projectId": "<id>"}; fleetd project list prints both, as <machineId>/<projectId>.

clientRequestId is any unique string you choose per request. Sending the same one again returns the same message instead of a second one. events_wait waits for changes, so a client doesn’t need to poll: a new message is a chat.message event.

Tools for projects: project_discover, project_add {path | url} (a path is a git checkout or a plain folder; with dryRun to see its CI and merge rules, and where a clone would go, without adding it) and project_remove. Tools for Navis: navi_create, navi_list, navi_rename and navi_archive. A clone answers with an addId, then sends project.clone_progress events, then project.added or project.add_failed.

Every tool, generated from the code: Agent tools.