Droid MCP Controller
Connects Factory Droid runs to Amp's MCP OAuth HTTP endpoint, attaching the amp-puck MCP server on task create and resume so Droid tasks can report into Amp/Puck conversations through the puck tool (with all other Amp tools denied). Includes workspace/reasoning/autonomy tooling for starting, continuing, inspecting, cancelling and listing Droid runs, and discovery of the live authenticated Factory model catalog.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Droid MCP ControllerStart a Droid task to review my latest changes without editing files."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Droid MCP
Droid MCP (droid-mcp) is a small, same-host MCP server that lets Echo (the operator's main assistant) manage Factory Droid
sessions: create, message and steer, status, read, wait, find, update, cancel,
usage, workspaces and models. Echo talks to Amp and to Droid separately; Amp and
Droid do not talk to each other, so sessions run detached (replyTo: null). No UI, database service, Factory daemon or account API. Installation and
secure connection steps are in HANDOFF.md; the full, generated tool
schemas are in docs/TOOLS.md.
Requirements
Linux or macOS (process-group cleanup; enforced at startup) and Node.js 22 or newer.
An installed Factory Droid CLI (verified with 0.236.0, normally
~/.local/bin/droid) and the pinned@factory/droid-sdk0.9.1 frompackage-lock.json.Factory authentication for the service user: an existing CLI login, or Factory's supported API-key setup for
droiditself. The controller inherits that environment and never collects credentials.Absolute, existing approved workspace roots and a private (700) state directory.
Optional: an authenticated HTTPS route for remote access (
transport:"http"with a bearer token file). The Amp reply-back path (ampMcp) is not used in this setup.
Docker is neither required by the source nor sufficient on its own: a container still
needs the CLI, its Factory login, the workspaces and private state. Dependencies are
locked; uuid is overridden to 11.1.1 to fix GHSA-w5hq-g745-h8pq.
npm ci --ignore-scripts
npm run check # syntax + docs/TOOLS.md in sync with the registered schemas
npm test # mock-peer E2E (real MCP client, controller, worker, Factory SDK)
node src/server.mjs --config /absolute/path/to/config.jsonThe session tools
A session is a controller-owned handle for one linear conversation with Droid in one approved workspace. Each message you send is one run (a turn). Factory session UUIDs never leave the controller.
Tool | Does | Droid / SDK capability behind it |
| Start a session and its first turn. | SDK |
| Follow up or steer. Idle starts at once; busy queues (default) or, with | SDK |
|
| Controller records plus the SDK terminal result |
| Paged history. | Terminal SDK result messages the controller retained |
| Join up to 10 sessions; bounded; returns when all settle. | Controller bookkeeping, no Droid call |
| Text (title, labels, bounded output preview/tail) and typed filters, newest created first, keyset cursor. | Controller records (Factory's |
| Title, labels, archive. | Controller metadata (no private Factory files are edited) |
| Interrupt the running turn, drop queued turns. |
|
| Sum of per-turn token usage and Factory credits. |
|
| Approved roots, capacity, launch policy. | Controller configuration |
| Live model catalog. |
|
Typical flow (tool arguments, not shell commands):
{"requestKey":"review-42","workspace":"/approved/repo","prompt":"Review the diff and report findings.","autonomy":"off","replyTo":null,"title":"Diff review"}droid_list_workspaces(roots, capacity, policy includingdefaultModel);droid_modelsonly when you want a model other than the host default.droid_create_sessionwith a stablerequestKey; keepmetadata.session.droid_wait_for_sessions({sessions:[...]})in a loop; a timeout is not failure.Inspect
latestRun.state,needsAttention,notification;droid_read_sessionfor detail.droid_send_messageto continue. Mid-run steering needsinterrupt:true; the defaultinterrupt:falsequeues after the current turn (this differs from Amp thread messages, which do not interrupt either, but do inject).
Concurrency (a hard requirement)
Sessions run at the same time. maxConcurrentRuns defaults to 4 (1 to 16) and is
shown by droid_list_workspaces. Admission is FIFO and applies three rules:
at most
maxConcurrentRunslive turns;at most one live turn per session;
canonical workspace locks:
autonomy:"off"turns are readers and may share an overlapping tree; any other level is a writer and needs its whole canonical tree (ancestors, descendants, symlink spellings) to itself.
A turn that cannot start is queued (it was not submitted to Droid) and starts
when capacity and locks allow, in order, so a waiting writer is never starved by
later readers. Locks are held until the worker and its Droid process are cleaned
up. There is no cross-controller or OS-level lock. Queues are bounded (64 total,
8 per session): beyond that queue_full. Queued prompts live only in memory, so
after a controller crash queued turns become cancelled with queue_lost; they were
never submitted and are never replayed.
Steering is serial interrupt-and-resume
interrupt:true durably cancels older queued turns on that session with superseded,
retaining their request keys and history. It then interrupts the active turn through
the protocol, waits for terminal result or termination and process cleanup, then
passes workspace/capacity admission before resuming the same Droid session with your
message. The SDK's high-level session rejects concurrent streams, so native in-flight
injection is not claimed or implemented. Tool side effects of the interrupted turn
are not undone. Cancelling or steering one session never touches another.
Outcomes, attention and notification
| Meaning |
| Accepted, waiting for capacity or a workspace lock; nothing submitted. |
| Live. |
| Matching terminal SDK result with subtype |
| Terminal SDK result |
| Setup/agent failure; deadline; cancelled without a terminal result (including dropped queued turns). |
| Controller stopped mid-turn. Never replayed, never inferred as success; the session cannot continue. |
A turn whose worker dies without a result is failed, and turns queued behind it on that session are dropped unsubmitted (predecessor_failed) so Puck decides what to do; the queue never resumes work across a failure. That cancelled head still needs attention and exposes latestRun.predecessorFailure with the failed run handle, state and error. A successful resolving follow-up clears attention; historical failures are not permanent flags.
agentState is working (anything queued or live), idle (nothing pending; not a
success claim) or unknown. droid_wait_for_sessions treats every non-working
state as settled, including needsAttention, so Puck is never left blocked.
needsAttention is true for failures, unknown outcomes, an interrupt nobody asked for,
declined permissions (permissionsDeclined), declined AskUser questions
(latestRun.questions, answer them with droid_send_message), and failed reports.
SDK 0.9.1 maps a completed turn to success even after a declined AskUser or tool, so
check these fields and the requested artifacts.
notification is what the controller observed the agent do with
amp-puck___puck (it cannot send for the agent): disabled (detached),
pending (turn running), accepted (Amp accepted a report; not proof Puck read
it), failed (rejected, or sent to a conversation other than replyTo,
reply_misrouted), not_sent (the turn ended without a report). A failed
notification never turns a finished run into a failed run.
Breaking change: session API only
droid_start, droid_continue, droid_status, droid_result, droid_list and
droid_cancel are removed, not hidden. The catalog contains exactly 11 tools.
Old names fail at dispatch; there are no aliases or compatibility flags.
Callers must use session handles with create/send/status/read/find/cancel session
tools and supply replyTo on creation. model is optional (see Launch defaults).
Refresh cached MCP catalogs. droid_models is unchanged. Internal run IDs remain
turn identifiers, not a second API. The repository's smoke/acceptance drivers use
the session API; external clients that imported the removed names must change.
Related MCP server: windlass
Models come from the live authenticated runtime
droid_models initializes a short-lived Droid connection with the same CLI, service
user, HOME and FACTORY_HOME_OVERRIDE as normal runs, captures the live
availableModels, closes the session, and never submits a prompt
or approves a tool. Create and send validate the effective model and reasoningEffort
against that catalog and reject with model_unavailable / reasoning_unsupported. Entries
with disabled:true or deprecated:true are omitted; optional metadata is returned
only when Factory supplies it. CLI 0.236.0 supplies deprecated in initialization;
the pinned SDK strips it, so raw capture is correlated to that initialization's request
ID before SDK parsing. Missing lifecycle metadata is unknown, not proof a model
is non-legacy. Consult Factory's current catalog
and the installed droid exec --help deprecation labels when metadata is absent.
modelCacheTtlMs defaults to 60000 (5000 to 600000); concurrent callers share one
refresh; a failed refresh returns model_discovery_failed, never an expired catalog.
Launch defaults and follow-up inheritance
Every new turn records its effective model, autonomy and reasoningEffort
(shown in latestRun). They are resolved once, at acceptance, against the live catalog:
Setting | Explicit | Omitted on | Omitted on |
| Must be in the live catalog | Host | The session's last accepted model (not the host default) |
| Above | Host | The session's last accepted autonomy, capped at the current |
| Must be supported by the selected model | Host | Same model: the session's last effort if still supported; then host effort if supported; then the model's live default |
"Last accepted" includes queued work, so FIFO follow-ups see the settings of the turn
queued before them. A follow-up therefore never promotes a read-only session to the
host's high default, and a model change never carries the old model's effort (the same
rule Factory documents for subagents switching models). When nothing supported is known
for a changed model, the send is rejected and asks for an explicit reasoningEffort.
defaultModel must be a standard model. A Fast variant is refused automatically: at
startup when the id looks like Fast, and at launch when the live display name does.
This is a name-based heuristic (-fast, Fast Mode), not catalog metadata. A caller
can still choose a Fast model explicitly. The recommended host values are
"defaultModel": "claude-opus-5-5" (standard Opus 5.5, if present in your live
catalog) with "reasoningEffort": "high".
Accepted request keys replay their original effective settings, without consulting
the catalog, even after defaults, the catalog or the ceiling change. Omitted fields in a
replay compare against those recorded values; a different explicit value is
request_key_conflict. Existing records need no migration.
Direct launch from Puck
Routine work needs no Amp preparation thread, runner, worktree or controller restart: Puck calls the connected Droid MCP and the controller starts a Factory Droid session.
One-time host setup (operator, per host or project):
Install, authenticate the service user's Droid CLI, configure state and transport (HANDOFF.md).
Approve each project root in
approvedDirectories; restart the controller when idle.Set
defaultModel(and optionallyreasoningEffort, autonomy policy).Only for reply-back:
ampMcpplus the one-time Amp OAuth sign-in for its exact URL.
Per task, Puck only calls droid_create_session with requestKey, an approved
workspace, prompt and replyTo (null for detached work), then
droid_wait_for_sessions, droid_read_session and droid_send_message. Detached
sessions never contact Amp, so they need no Amp authorization. An unapproved project
fails with workspace_not_approved, whose action names the one-time setup.
Choosing a project
Agents pick a project from the controller, not from memory:
Call
droid_list_workspaces.workspaceslists the approved roots (canonical absolute paths) of this host'sapprovedDirectories.Pass one of those roots, or any existing subdirectory or Git worktree inside one, as
workspacetodroid_create_session. Symlinks are resolved first; a path that resolves outside every root is rejected.Follow-ups use the session handle; the workspace stays fixed for the session.
Adding a project root is an operator change to the private host config, effective at the next controller-only restart. Agents never edit it.
Isolation: writer turns hold a lock on their whole canonical workspace tree, so two
sessions writing the same tree run one after the other. For parallel writers, the
operator provisions separate directories or Git worktrees inside an approved root
once, and Puck targets them directly. The controller does not create worktrees.
Droid's native --worktree places checkouts under worktreeDirectory
(default ~/.factory/worktrees), outside the approved roots, and changes the session
cwd, which the controller's resume check rejects; enabling it would widen the approved
filesystem scope and is left as an explicit operator decision.
Factory web visibility
The controller runs Droid through SDK ProcessTransport (droid exec --input-format stream-jsonrpc) as the service user. Factory documents
cloudSessionSync (default true, org settings can override it) as mirroring CLI
sessions to Factory web, so such sessions are expected at
https://app.factory.ai/sessions/<Factory session UUID>. Whether a given session is
actually visible has to be checked in an authenticated browser; local history alone is
not proof of upload. Factory session UUIDs stay out of the tool surface; the operator
can map controller sessions to them from sessions[*].droidSessionId in the private
state.json. Registered-computer Remote Control (droid computer register,
droid daemon --remote-access) is a different feature. It is not used or enabled here,
and enabling it would let external clients bypass the controller's root locks.
Titles and tags. A session created with an explicit title gets that title in Factory
through SDK rename() after its first turn (renaming earlier would be replaced by
Droid's generated title). A droid_update_session title change is applied after the
session's next turn; the update itself never starts Droid outside admission. Sessions
without a title keep Factory's generated title. Rename is best effort and runs after
the worker has delivered the turn's terminal result, which ends the execution deadline,
so a slow or failed rename cannot make a finished turn time out. It then gets its own
cleanup window of min(10 s, runTimeoutMs / 4) (plus cancelGraceMs); the execution
slot and workspace lock stay held until the worker exits. A failure or an unfinished
rename is logged to the controller's stderr and the run's events and retried on the
next turn. It never changes the turn's outcome or request-key fingerprints. Turns that
are cancelled or time out are not renamed. Labels
are sent as Factory session tags at creation, beside the SDK's own sdk tag (a label
named sdk merges with it). SDK 0.9.1 has no tag update call, so later label changes
stay controller metadata. Whether the Factory web app displays these tags is unverified.
Droid reports and questions use the actual Amp MCP
Reply-back is opt-in per host: "ampMcp": {} in the private config. The endpoint is
generic: https://ampcode.com/mcp?profile=puck, the only profile Amp currently
accepts (others return HTTP 400 Unknown MCP profile; expected one of: puck). Any
other ampMcp.url is refused at startup; there are no aliases. It names no thread and
no recipient; a threadID in the URL, or the old puck.conversationId, is rejected
at startup, because a thread-bound endpoint broke every launch when its thread was
archived and a host default recipient silently misrouted omitted replyTo.
Recipients are per session (replyTo) and are never defaulted.
The controller attaches amp-puck through SDK mcpServers on create and resume,
only for routed sessions (replyTo non-null); detached sessions never touch the
endpoint. Tool exposure is default deny: only amp-puck___puck (send, read_reply) is meant to be
usable. The earlier thread-free profile was observed live to offer manage_amp (admin) and
find_thread / read_thread (read other Amp threads) too, so those start in
disabledToolIds; the preflight then lists what the server actually exposes, denies every
other amp-puck___* tool it finds (a new Amp tool is blocked automatically), and verifies
none remains usable. Saved disables are preserved on resume and when updating settings;
native tools are not restricted. Discovery/settings errors and cancellation submit no
prompt. If denial cannot be achieved the run fails with
amp_mcp_admin_tool_exposed and nothing is submitted. This is client-side filtering, not
credential scoping (see docs/THREAT-MODEL.md). Detached sessions never touch the endpoint.
Factory stores Amp MCP OAuth tokens per exact endpoint URL (observed live: the
thread-free URL and a differently threaded URL both get unauthorized on a host that
authorized only another URL). The operator must complete Factory's supported MCP
OAuth once, as the service user, for the exact configured ampMcp.url. See
docs/CUTOVER.md. The controller never accepts tokens or headers and
never starts consent during task execution.
Before any task prompt is submitted a preflight checks the server and tools and
fails the run with an actionable latestRun.error instead of the generic one:
| Cause |
| Server listed but no OAuth tokens. |
| Server dropped from Droid's listing: usually no stored token for this URL, or Amp rejected the connection. (The SDK does not report these as |
| Server listed as failed or never connected; the sanitized server message is included (for example an archived thread). |
|
|
| Tool discovery failed for another reason (sanitized). |
Every routed turn receives the explicit recipient, the controller session and run
handles, and how to call {action:"send",params:{conversationID,message}} and
{action:"read_reply",params:{replyHandle}}. Routed sessions may approve, as a
single use, only that tool: send to their own replyTo, or read_reply
with a recognized handle proven by an originating-turn approved own-recipient send and
its correlated non-error result (so read-only reviewers can report). Ownership is
durable within the same controller session and uninterrupted recipient route;
continuation and restart preserve it. Accepted detach/retarget invalidates old
handles, even if that turn is cancelled or routing later returns. Missing,
malformed, foreign and unproven handles are denied even at autonomy high.
Replayed call/result notifications without a new approved send cannot grant
ownership. The parser accepts a top-level JSON replyHandle in string/text-block
results; unknown formats fail closed. Other high-autonomy single-use approvals
remain unchanged. These rules are not OAuth credential scoping or OS isolation.
Progress is sent once, fire-and-forget; continue authorized independent work. A
question, blocker or costly/irreversible step needing steering uses one CHECKPOINT
with recipient, session/run handles, marker, completed evidence, proposed action and
decision needed. Consume a completed reply inline, otherwise read only that send's
exact replyHandle, at most 6 reads in the natural tool loop, without messaging
waits. Never resend after timeout/ambiguous failure or use latest-active/empty params.
Without an answer, end BLOCKED with marker, decision and handle; leave dependent work
untouched. Call Puck directly in Spec mode; never exit Spec just to message or guess
AskUser answers. Acceptance/queued/working/silence is not approval; replies steer
only within existing authorization. Wording cannot override permission cancellation;
mock text pins prove wiring, not model obedience.
A controller-owned Amp OAuth client and durable terminal-notice outbox are not
implemented: no supported credential path exists (Amp remote-MCP OAuth is per-account
via ampcode.com; Amp Workload Identity needs Amp-staff access; Factory's token store
must not be read or copied). Terminal outcomes are therefore delivered by
droid_wait_for_sessions and status, and the agent's own report is observed in
notification. See docs/THREAT-MODEL.md for the open question.
OAuth is account authorization, not a demonstrated thread/tool-scoped grant. Client-side tool denial reduces model exposure, not credential rights. See Factory MCP and Amp MCP.
Outcome details
An unknown outcome is settled for waiting but not a known Droid outcome. The
session cannot continue (session_unknown_outcome), even through an older run handle. Inspect/reconcile Droid
history and the workspace locally. Start a new session with a new key
after reconciliation, or use the saved Droid session manually outside the controller.
Do not edit state to manufacture success.
High-autonomy turns record and approve offered single-use permissions with
ProceedOnce, never persistent ProceedAlways rules. Other levels decline
pending permission requests with Cancel; high also declines if no single-use
option is offered. AskUser questions are recorded and declined, not guessed.
SDK 0.9.1 maps a completed turn to success even after declined AskUser and with
empty final text; cancelled maps to interruption. The controller preserves that
SDK outcome, not a judgment that the user's task finished. Inspect recorded
ask_user_declined events and task artifacts before accepting completion. Puck
can answer the blocked question with droid_send_message. There is
no interactive permission queue or arbitrary session import; saved outcomes are
not rewritten to manufacture a different result.
Same-host durability and cleanup
stateDirectory contains private state.json, per-run *.result.json, and an
owner record. Writes use file+directory fsync and atomic replacement. A single
process owns the state; startup refuses a live owner, another host/HOME/profile,
or malformed state. A stale dead-process owner is recovered under a startup
lock. PID reuse conservatively blocks startup rather than risking two owners.
A controller crash stops its workers through IPC disconnect; each worker owns a separate process group containing Droid. Cancellation first sends protocol interrupt, then force-kills that group after its grace period. The session slot is not released until worker cleanup. Terminal results are persisted before cleanup, so restart can retain a known outcome even if cleanup was interrupted. Processes deliberately detached by Droid tools may escape that group: do not authorize background-service work unless separately supervised.
Keep the same user, HOME, FACTORY_HOME_OVERRIDE (if used), Droid session storage,
and workspace. Controller records do not replace Factory's own saved history.
Back up both. Progress is intentionally bounded: last 20 events, 4000 characters
per event field, and 16000-character text/stderr tails. Final SDK results are
stored with submitted user-message copies omitted. There is no automatic deletion
of completed records; archive them locally with the controller stopped, preserving
keys if deduplication is needed.
Timestamps are UTC ISO strings; Puck may display them in Asia/Bangkok.
Original submitted prompts are not retained in controller state. Prompt content
participates in the SHA256 intent fingerprint and is passed to the worker only
over IPC for the accepted execution. A crash before submission becomes unknown,
not a replay. Assistant echoes, permission context, stderr and final results can
still contain sensitive text; Factory's own session history is separate and
may retain prompts. Fingerprints are not encryption.
Only current version 3 state is supported: sessions (title, labels, archive flag,
replyTo, workspace, authoritative headRunId) and runs, including durable
replyHandles and replyRouteId. Older formats and incomplete records refuse
startup without rewriting. There are no automatic migrations or backfills.
Existing current sessions, results, request keys and credentials are preserved.
See docs/STATE-MIGRATION.md for requirements and recovery.
Reference host setup
The setup in use (stdio, localhost only, no tunnel) matches config.example.json:
defaultModel claude-opus-5-5, reasoningEffort high, maxAutonomy high,
maxConcurrentRuns 4 and excludeModelIds [] (no legacy list populated; the runtime
already hides deprecated models and models with a deprecationDate). defaultAutonomy is
omitted, so it falls back to maxAutonomy (high); set it explicitly for a lower default.
Register it as a stdio MCP server named droid-mcp:
node /absolute/path/to/droid-mcp-controller/src/server.mjs --config /absolute/path/to/config.jsonNo env vars are needed when the service user's droid CLI is already logged in. For steering
sessions from the Factory dashboard, run droid daemon --remote-control on the same host
(this host is registered as computer grok-bot).
Configuration and security
Copy config.example.json and replace every path with an absolute path.
JSON paths do not expand ~. Only real directories at/below approved roots
can be launched; traversal, sibling-prefix and symlink escapes are rejected.
Resume must resolve to exactly the recorded workspace. State must be private
(700 directory, 600 files).
Optional settings:
Setting | Default |
|
|
|
|
| Inherits |
| Unset (create then needs |
| Unset; otherwise |
|
|
| 4 (1 to 16); queued turns wait FIFO |
| 3600000 (one hour wall clock, including setup) |
| 5000, also bounds terminal cleanup |
| 60000, bounded to 5000–600000 |
| Unset; |
| 8787, HTTP only; binds 127.0.0.1 only |
| Required for HTTP; private file, ≥32 URL-safe random characters |
| Optional approved HTTPS proxy URL, ending in |
The listener implements stateless Streamable HTTP MCP POST /mcp, with constant-
time Bearer authentication on every request, approved Host/Origin checks, a 1 MiB
body cap and request timeouts. No unauthenticated HTTP option exists. No OAuth
provider, SSE notification channel, or automatic push back to Puck is provided.
Status and results work independently of a connected client; waits are bounded to 60 seconds per call.
Malformed JSON/MCP envelopes return HTTP 400. Unexpected server/transport failures
return generic HTTP 500 Internal MCP server error; details stay in local stderr.
Lifecycle/tool rejections are normal MCP isError responses with {error:{code,message,retryable,action}}, not HTTP 500.
Workspace approval is a launch restriction, not an OS sandbox. Droid runs
as the service user; enabled tools, inherited MCP servers, project hooks, and
commands can access what that user can. Inspect trusted project/Factory config.
For hard file/network isolation use a dedicated restricted account/container
with the approved directories mounted. Read-only/Spec mode is Droid policy,
not a filesystem mount guarantee. Low/medium/high authorize real actions; high
is powerful. The controller never uses --skip-permissions-unsafe.
Treat the bearer token as remote access to this account within the selected ceiling. Anyone with it can read stored results and start tasks. Store it only in local/private credential settings, use TLS for every remote hop, and restrict the HTTPS proxy/tunnel to this controller. Never expose an orb preview as the user's installed controller. Rotate the token by replacing its file and restarting the server; update Amp's stored credential separately.
Workspace approval, outages, and safe upgrades
Workspace rejection: a correct authorization failure, not a reason to remove the allowlist. Ask the operator to add only the explicitly approved project root. Existing directories, real paths, sibling/traversal protection, and resume-cwd checks still apply. Configuration is loaded at process startup; coordinate a controller-only restart with every caller to activate changes.
HTTP 530: a failure at the HTTPS/Cloudflare boundary; it does not establish that Droid failed or stopped. Check the actual public route and Cloudflare's tunnel connection state, DNS, and bounded logs. Loopback readiness/metrics can appear healthy while public routing is down. Synthetic host DNS can also make a local HTTPS probe misleading; cloud-client acceptance is required. Reserve connector-only recovery when public routing fails; preserve active Droid runs. A controller restart cannot repair an unavailable tunnel. Retry read-only status/list/result after a short delay; replay start/continue only with the same key and identical arguments when acceptance is uncertain. Never create a second intent merely because HTTP failed.
Connector DNS: some managed hosts return synthetic addresses that cannot carry tunnel traffic. Fix only the connector's resolution/routing, not shared machine DNS. A pinned-edge override is a host workaround, not a portability guarantee; retain edge TLS verification and revisit it if edge addresses change.
Upgrade/restart: reserve the execution slot, check that all runs are terminal, back up private state, and restart only the changed service. Keep the healthy tunnel, bearer credential, HTTPS URL, user/HOME, and state store unchanged. Recheck remote discovery, prior results, list, and idempotency before resuming. Never rewrite installation state to make a failed upgrade appear successful.
Persistence: process supervision and durable records do not imply machine- boot startup. Use the host's supported startup mechanism when available; a managed container without systemd/launchd needs platform-specific provisioning. Do not claim boot persistence from a successful process restart alone.
Protocol evidence and tests
Uses published @factory/droid-sdk/node ProcessTransport with
createSession({transport}) / resumeSession(id,{transport}). This bypasses
the SDK helper's API-key requirement, not CLI authentication. The supported CLI
invocation is exec --input-format stream-jsonrpc --output-format stream-jsonrpc.
Settings use initialize/update requests, not CLI model/autonomy flags.
Terminal matching is agent_turn_completed.turnId == add_user_message.messageId;
idle, assistant text, silence, unrelated turn IDs and process exit cannot succeed.
npm test drives the real MCP SDK client, controller, worker, Factory SDK and a
mock JSON-RPC peer (test/e2e.test.mjs for transport, SDK and persistence safeguards,
test/session-surface.test.mjs for the session tools). It covers the strict
schemas and errors, idempotency, steering and queueing, N parallel sessions,
capacity queueing, overlapping-workspace serialization (both lock directions,
symlinks, FIFO fairness), mixed autonomy, concurrent reply-back with per-session
recipients, misroute detection, cancel/steer isolation, Amp MCP preflight failures,
launch defaults and follow-up inheritance (test/settings-defaults.test.mjs),
Factory title and tag sync (test/factory-title.test.mjs),
removed-tool rejection, unsupported-state refusal, crash fail-closed and queue loss. See test/README.md.
test/review-regressions.test.mjs and test/worker-ack.test.mjs additionally test
durable queue supersession, persistence ACK fault boundaries, per-run reply handles,
failed-predecessor attention and catalog lifecycle.
npm run smoke runs a two-turn lifecycle against a real CLI, and
scripts/acceptance.mjs is the live acceptance driver for a second instance
(own port, token and state directory), never the production service.
For bounded authenticated read-only verification with disposable private state:
node scripts/verify-live.mjs --droid /absolute/path/to/droid --model CURRENT_ID --workspace /approved/smoke/root.
It checks real SDK acceptance, two exact results, same-UUID continuation and
cancellation of a third turn, then process cleanup; output contains only assertions
and versions, not prompts, session UUIDs or credentials. Pick an economical current
model with low reasoning from the live catalog first. Existing CLI login is required.
node scripts/verify-route.mjs --droid /absolute/path/to/droid separately checks
fresh, resumed, reattached and detached MCP inventories without any model prompt
or Puck message. It requires existing OAuth for the generic Amp endpoint. On CLI
0.236.0 / SDK 0.9.1, omitting attachments on resume does not retain a previous Amp
attachment. This does not prove future CLI behavior or constrain inherited host MCP.
Sources: Factory SDK documentation, Droid exec, published SDK, Node IPC send callback semantics, reference bridge. The reference bridge was cloned/read, not changed or incorporated: its OpenAI-transcript/tool isolation contract differs from this session controller.
License
MIT; see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Local-first task manager: create, edit, and complete tasks, projects, and checklists via MCP.
Hosted MCP server for task-first delegation to remote workstations and workers.
Private-by-default, local-first memory/context/task orchestrator for MCP apps and agents.
Sandboxes, storage, secrets, queues, timers, locks, inboxes and run ledger for AI bots. One MCP.
Related MCP Servers
- AlicenseCqualityDmaintenanceEnables local task management through the Model Context Protocol with tools for creating, updating, and deleting tasks. It also provides read-only resources for viewing task summaries and full task lists.43 npmISC
- AlicenseNot gradedqualityDmaintenanceEnables running and managing automated tasks with retry loops and machine-checkable success criteria via MCP tools.0MIT
- AlicenseNot gradedqualityCmaintenanceEnables MCP clients to run bounded robotics tasks, retrieve independently verified results, and reuse checked data or artifacts from the local WorldFuzz platform.MIT
- AlicenseAqualityCmaintenanceEnables MCP clients to submit tasks to a local file-backed queue and later read or cancel them by ID, with results produced by a separately run worker.3MIT