Skip to main content
Glama
NRGGIT

Constructor Research MCP

by NRGGIT

Constructor Research MCP

Local stdio MCP for Codex CLI, Claude Code and OpenCode. Submit the whole application once; the MCP builds images, connects services, configures them and starts them. The agent supplies missing configuration and reports the result.

Install

You need Node.js 22+. Then add the MCP to your tool.

Claude Code

claude mcp add --scope user constructor-research -- npx -y constructor-research-mcp@latest

Codex

codex mcp add constructor-research -- npx -y constructor-research-mcp@latest

OpenCode 2.x

opencode mcp add --global constructor-research -- npx -y constructor-research-mcp@latest

OpenCode 1.x (no --global there):

opencode mcp add constructor-research -- npx -y constructor-research-mcp@latest

Then restart the tool and ask the agent anything about Constructor; it signs you in through the sign_in tool on first use. Windows notes, the optional wake-up hooks and troubleshooting are in INSTALL.md. Restarting the tool picks up a new version.

Related MCP server: golive-mcp

Authentication and connection

Each harness signs in once and keeps its own Constructor session; nothing is stored centrally and nothing goes in the MCP settings. When a tool returns needs_auth, the agent tells the user a sign-in window is about to open and calls sign_in:

  1. The MCP opens a separate browser window (Chrome, Edge, Chromium or Brave, on macOS, Windows or Linux; CONSTRUCTOR_BROWSER names another executable) with a fresh temporary profile at the Constructor sign-in page.

  2. The user signs in. The MCP reads the session cookies, closes the window itself and deletes the profile.

  3. It saves ap_access_token, ap_id_token and ap_refresh_token to that harness's session file and answers signed_in. No token is ever returned or printed.

sign_in waits up to wait_seconds (default 20, so it stays within short harness tool timeouts); while the user is still signing in it returns sign_in_pending, and calling it again waits on the same window. With a renewable session already saved it answers signed_in without opening anything (force=true signs in again).

The harness is recognised from the client name in the MCP handshake; its session file is ~/.config/constructor-research/sessions/<harness>.txt (claude-code, codex, opencode; another client gets its own name), mode 600. Separate windows matter because a refresh token works once: two holders of one session would log each other out by renewing. Several sessions of the same harness share its file safely (below).

The access token lasts an hour. When it has under 10 minutes left, the next request first renews it: GET /auth/session with only the refresh token, which answers with new tokens that are saved at once. Renewal runs under <session file>.lock, so processes sharing a file renew once and the others use the saved tokens. Requests send only the access token, so the gateway never renews behind the MCP's back. The server allows renewal until the max_slt claim, 14 days after sign-in; from a day before that, every response carries auth_warning asking the agent to have the user sign in again. If the refresh token was already used elsewhere, it is dropped and, once the access token expires, needs_auth explains why. A token issued by another environment (for example constructor.app) is never sent.

The same sign-in from a terminal (use codex or opencode in place of claude-code, or --to <file>):

npx -y -p constructor-research-mcp constructor-research-login --for claude-code

On a machine without a screen (Linux without DISPLAY/WAYLAND_DISPLAY), sign_in says so; save a session from a copied cURL (DevTools → Network → Copy as cURL) instead:

npx -y -p constructor-research-mcp constructor-research-set-token capture.txt --for codex

It keeps the refresh token when the cURL has one (and warns that the browser it came from shares that session), and prints the expiry, never a token. A plain access token, without a refresh token, cannot be renewed and expires after an hour: under 3 minutes left, operations that change anything are refused (get_deployment still works); under 10 minutes every response carries auth_warning, and deploy_app/update_app/build_image warn from 40 minutes, because a build can outlast the token.

CONSTRUCTOR_COOKIE_FILE pins one session file for every harness instead (for scripts and tests). Every request rereads the session file, so a new sign-in needs no restart.

Variable

Default

CONSTRUCTOR_BASE_URL

https://training.constructor.app

CONSTRUCTOR_API_PATH

/api/platform-account

CONSTRUCTOR_STORAGE_PATH

/api/platform-storage

CONSTRUCTOR_COOKIE_FILE

unset: ~/.config/constructor-research/sessions/<harness>.txt

CONSTRUCTOR_BROWSER

first Chrome, Edge, Chromium or Brave found

CONSTRUCTOR_STATE_DIR

~/.local/state/constructor-research-mcp

CONSTRUCTOR_UPLOAD_SETTLE_SECONDS

30

CONSTRUCTOR_READINESS_SECONDS

60

HTTPS is required except on localhost. API-key authentication is not implemented yet.

Fourteen tools

Tool

Agent's task

deploy_app

Describe all services in one request, or resume that deployment with overrides.

update_app

Describe selected changes/additions; the MCP builds and restarts affected services.

start_app / stop_app

Request lifecycle changes for all or selected services.

get_deployment

Map all deployments, inspect a project or app, or wait for background execution.

get_logs

Last 10–50 lines of a build or service log (see "Logs").

build_image

Build an image independently when needed.

list_images

List image IDs, Dockerfiles and native build statuses.

list_files

List a disk's files and folders; recursive:true walks subfolders.

sign_in

Open a sign-in window for the user when a tool returns needs_auth.

create_folder

Create a disk folder, including missing parents.

sync_files

Upload new and changed files; asks once before replacing existing files.

move_path

Rename or move files/folders in one batch, approved once; never overwrites.

delete_path

Delete files/folders in one batch; reports first, deletes only with confirm:true.

No special clone/new-instance workflow is implemented. The fourteen tools use the same standard MCP transport across harnesses.

The agent does not choose a project or desk. It asks the user for the desk link: open the desk in Constructor and copy the browser address, for example

https://training.constructor.app/research/projects/<project_id>/desks/<desk_id>/viewer/file

Every tool accepts it as desk_url, in place of project_id/desk_id. Anything after /desks/<desk_id> is ignored. The link's host must match CONSTRUCTOR_BASE_URL; a link from another environment returns wrong_environment. Explicit IDs, if also sent, must agree with the link. In deploy_app/update_app, builds without their own desk_id use the link's desk; start_app/stop_app and get_deployment take only the project from it.

Normal deployment flow

  1. Inspect with get_deployment: no arguments returns the map of all deployments (every deployment-enabled project → applications → services with names, native statuses, public URLs and ports; other projects are listed by name only); desk_url or project_id returns that project's disks, images, hardware and apps; add application_id for service details.

  2. If the app is built from source, pass build.local_path (the local folder with the Dockerfile) and deploy_app uploads it first, or upload separately with sync_files.

  3. Call deploy_app once with the entire application. Use build sources, existing image IDs, or a mix.

  4. The MCP executes the job in the background. It generates the deployment ID and resolves service references.

  5. If the response is still in progress, call get_deployment with that ID to wait for the result. Do not write loops to advance individual steps.

  6. On failure, report the stage/service and ask the user to check logs in Constructor. No browser or debugging detour, and no automatic fallback to an older image.

Every execution tool accepts wait_seconds from 0 to 20 (default 20). A response after that time is not a timeout/cancellation: execution continues while the MCP process is connected. get_deployment waits for completion or a user-action state; it does not drive the pipeline. wait_seconds:0 returns immediately.

The background executor runs inside the MCP process, not a detached daemon. Closing the harness stops local orchestration; remote builds continue. On the next MCP startup, unfinished checkpoints recover automatically. If a mutation's outcome is uncertain, recovery pauses instead of repeating it.

Whole-application example

{
  "project_id": "PROJECT_ID",
  "application_name": "subfinder",
  "files_uploaded": true,
  "services": [
    {
      "name": "backend",
      "build": { "desk_id": "DISK_ID", "dockerfile": "/backend/Dockerfile" },
      "ports": [{ "number": 8080, "expose_to_internet": true }]
    },
    {
      "name": "frontend",
      "build": { "desk_id": "DISK_ID", "dockerfile": "/frontend/Dockerfile" },
      "ports": [{ "number": 3000, "expose_to_internet": true }],
      "environment_variables": {
        "NUXT_PUBLIC_API_BASE_URL": {
          "service": "backend",
          "exposure": "public"
        }
      }
    }
  ]
}

No request ID is required. An optional request_id is a retry key (descriptive strings and legacy UUIDs are accepted); reusing it with the original request resumes the same operation. Save the returned deployment_id. After an interrupted initial call without a retry key, an undisclosed checkpoint may exist; inspect state/resources rather than blindly resubmitting.

Service references

Public URL, resolved after the target service is created:

{ "service": "backend", "exposure": "public" }

Internal host and port:

{ "service": "database", "port": 5432 }

Internal HTTP URL:

{ "service": "backend", "port": 8080, "scheme": "http" }

References imply dependency ordering. depends_on can add explicit ordering. Unknown targets, cycles and unconfigured target ports are rejected. Public references require a public target port and use HTTPS. Internal addresses follow svc-{name}.app-{project_id_first4}{application_id_first4}.svc.cluster.local:{port} for this environment. Public URLs use the API link or https://{service_id}.constructor.pro.

Private DNS works between containers; browser JavaScript needs a public backend URL or a frontend server proxy. Build-time frontend variables are not injected into image builds by this MCP: runtime wiring requires an application that reads runtime configuration.

Image reuse and limits

Replace a service's build with "image_id":"IMAGE_ID" to skip building. list_images returns compact records with not_started, snapshot_creation, in_progress, done or error; detailed:true returns full records.

  • Build storage is omitted by default, using the API's documented 50 GB default. Explicit ephemeral_build_disk_size_gb overrides must be 10–1000 GB.

  • Each service can have multiple ports (1–65535), with at most one public port. That port must match where the container listens.

  • Hardware: omit it. New services get the smallest available non-GPU option; existing services keep theirs. Only when the app needs more, pass minimums hardware: {cpu, ram_gb, gpu}; the MCP picks the smallest available option that meets them (GPU options only when gpu:true). get_deployment with desk_url lists hardware_options as plain specs (CPU, RAM, GPU), without names or IDs.

  • Up to three volumes, each 10–2000 GB, with a mount path.

  • Service names are lowercase DNS labels, max 63 characters.

Missing prerequisites

  • No/expired token: needs_auth with the file and format to use (see Authentication). Replace the token, then check the same deployment again.

  • No project/disk: omit project_id, optionally supply project_name, and use build sources without disk IDs. MCP creates the project/disk and enables deployment tools.

  • Files absent: needs_upload lists disks and Dockerfile paths. Upload with sync_files (or manually), then call get_deployment with deployment_id and files_uploaded:true.

Before any build, the MCP reads each Dockerfile from its disk. A missing one returns dockerfile_missing and nothing is created. When every build's Dockerfile is present, the upload pause is skipped. The MCP also returns dockerfile_warnings (in message too) for: a name other than Dockerfile/<name>.dockerfile, no FROM, no EXPOSE or a service port that is not exposed, localhost/127.0.0.1 in ENV/ARG/CMD/ENTRYPOINT, a docker-compose file next to the Dockerfile, compose-style default hosts in ENV (http://cerbos:3592, temporal:7233), an ENTRYPOINT/CMD script copied from the build context with no chmod (files uploaded to a desk lose the executable bit, so such a container exits before printing anything), and listen [::] in an nginx .conf/.template the Dockerfile copies (nginx exits where the cluster has no IPv6). The same rules are in the tool descriptions, so agents see them before writing a Dockerfile. An existing project with a conflicting name requires an explicit ID; the MCP does not guess.

Resume with a replacement image

After the user selects a replacement, call the original tool (deploy_app or update_app) with the same deployment ID and only the service overrides:

{
  "deployment_id": "RETURNED_DEPLOYMENT_UUID",
  "services": [{ "name": "frontend", "image_id": "WORKING_IMAGE_ID" }]
}

Successful builds and unchanged service definitions are retained. Environment overrides merge with saved values. Supplying a new build source retries that service's build; the previous successful builds are reused. Overrides cannot add services that were not in the saved operation; use a new update for additions.

A failed build returns failure.stage, failure.service, failure.dockerfile and failure.image_id. A service API failure includes the stage and service. The message is concise: “Please check the logs in Constructor.” Logs and browser access are not part of this workflow.

  • An environment variable can be {service, port} instead of a string. The MCP turns it into the other service's internal address as a bare host:port (e.g. svc-temporal.app-1234abcd.svc.cluster.local:7233), which suits gRPC, databases and Temporal. Add scheme: "http" to get http://host:port. {service, exposure: "public"} gives the public https URL.

  • A volume belongs to one service and the container can write to it. Data lives as long as the service: it survives stop/start, restarts and image changes. It is wiped when the service is deleted, or when the volume is removed or its name, mount point or size changes. update_app therefore returns confirm_required with volumes_to_wipe before such a change; after the user approves, repeat with confirm_volume_wipe=true. Another service cannot mount it; a request that mounts the same volume on two services is rejected before anything changes. Share data over an internal link instead.

Updates and lifecycle

update_app takes project_id, application_id and selected services. Match an existing service by exact name, or supply service_id when renaming. New names add services and require an image or build. Services omitted from the request remain untouched.

Omitted image/hardware/ports/volumes are retained. Environment variables merge by name; deletion of environment variables is not implemented. Supplied port and volume lists replace those lists, preserving matching IDs by port number/volume name. Empty lists remove attachments; no volume-data migration occurs.

All required builds must succeed before existing services are changed. The MCP stops affected running services, sends a complete merged PATCH and starts them. Multiple services are handled internally, not through agent-managed start/stop loops. This is not an atomic rollout; a failure can leave some services updated. Avoid concurrent UI edits and finish one operation on an app before starting another.

start_app and stop_app take project_id, application_id and optional service_names. started/stopped mean requests accepted, not verified readiness/teardown. Native statuses (starting, running, stopping, stopped, failed) remain visible. A finished job's service status can be refreshed with get_deployment; the job's historical outcome is distinct from current native status. Readiness is always unverified.

Build and start reliability

Learned from live runs on training (28 Oct 2026):

  • Upload settle. A build started right after its Dockerfile was uploaded failed every time; with a pause it built first time. After any upload to a disk, builds on that disk wait until 30 s have passed (status preparing, message says so). CONSTRUCTOR_UPLOAD_SETTLE_SECONDS changes it; 0 disables.

  • Confirmed failure, one automatic rebuild. The platform briefly reports error and then in_progress again, so an error counts only when seen twice for the same image. The first confirmed failure is rebuilt once under a new image name, after re-uploading local_path sources and re-checking the Dockerfile on the disk. build_retried names the image that failed.

  • Actionable failure. A build that fails again returns failure.image_name, first_image_name, desk_link, and a message saying how to see the log: Constructor does not keep build logs, so rebuild that Dockerfile from the UI and watch it live.

  • Readiness check. After start, the MCP requests each public URL until it answers (any HTTP status below 500) or 60 s pass (status verifying). readiness is then http_ok, not_responding (with readiness_checks and a hint; services of the deployment that printed nothing since starting are named, with an ordered checklist: entrypoint not executable, wrong entrypoint path, nginx IPv6 listen, wrong listen address or port), or no_public_url. CONSTRUCTOR_READINESS_SECONDS changes the window; 0 disables.

State and recovery

Progress and sanitized results are checkpointed. Configuration secrets remain in owner-only local checkpoint files (0600, directory 0700); tool responses return environment-variable names, never values. Cookies are not checkpointed. Do not commit credentials or state.

Checkpoints are bound to the API endpoint. A shared state directory coordinates local harnesses. resume:false on get_deployment observes without scheduling execution or confirming uploads; already running background work is not paused.

Terminal states: built, started, stopped, failed, needs_reconciliation, superseded. A new update_app, start_app or stop_app for an application ends that application's unfinished deployments as superseded, so an older one cannot apply its images after the newer one; its builds keep running on Constructor but are not used. The new result lists them in replaced, with the services they were changing that the new update leaves out. User-action pauses: needs_auth, needs_upload. Other statuses indicate background progress. An executor stops after one hour rather than retrying forever.

For needs_reconciliation, inspect the possible remote mutation before submitting a new operation. The server never blindly repeats an uncertain create. A .lock left by a process that no longer exists (a server killed or restarted mid-step) is removed automatically on the next operation; a lock held by a live process returns busy. Checkpoints remain available. No rollback or resource deletion is automated.

Logs

Constructor serves logs only live, as a stream: a build log while the build runs, a service log for the service's current container. Each connection replays the log from the start, then streams. Nothing is kept afterwards: a finished build has no log, and a restarted container starts with an empty one, so a crash-looping service shows no log at all. The MCP therefore follows every build and service it starts and keeps only the last 50 lines of each, in memory and in logs/ in the state directory, so it stays small however long the log is.

  • A build is followed from its first status check until it ends; a dropped connection is reopened and replayed lines are skipped. When the stream closes, reopening it answers "not found" and the kept lines are final.

  • A service is followed from its start request for 15 minutes after anyone last asked for it.

  • A failed build or start puts the last 20 lines into failure.log_tail.

  • get_logs takes image_id, service_id or deployment_id (all its builds and services) and lines (10–50, default 20). It also reads builds and services this MCP did not start, but only while they run.

Waiting for long builds

Builds can run for many minutes. The MCP keeps executing in the background, but an agent that has ended its turn is not told when they finish. dist/wait.js (bin constructor-research-wait) blocks until the given deployments, or all in-progress ones when no ID is given, finish, and prints one line each: status, message, services with public URLs, next step. Exit codes: 0 finished, 2 needs the user (failed, token, upload, volume confirmation), 3 still running at --max-minutes (default 60), 4 nothing in progress.

npx -y -p constructor-research-mcp constructor-research-wait <deployment_id>

It watches the checkpoints the MCP writes. For deployments named by ID, if no process has advanced one for a minute (the MCP was closed), it continues it itself, exactly as the MCP would on restart. Without IDs it only watches, since those deployments may belong to other sessions. While a deployment runs, next_action gives the agent the exact command, including the MCP's non-default CONSTRUCTOR_* settings; the wait command needs the same settings as the MCP.

How the agent learns the outcome depends on the client:

Client

Without setup

Optional wake-up

Claude Code

runs the command in the background; Claude Code wakes the agent when it exits

PostToolUse hook with asyncRewake (merge into settings.json): wakes the agent after any deploy/update/build/start/stop

Codex CLI

runs the command in the foreground (no background wake-up exists)

Stop hook (~/.codex/hooks.json, trust it in /hooks): when the agent would stop, waits up to 9 min, then continues the turn with the outcome, or with "still in progress" so the next stop waits again

OpenCode

runs the command in the foreground

plugin (.opencode/plugins/): after a deploy tool call, waits in the background and sends the outcome as a new message

Hooks report each deployment once: one that had already finished when the tool returned is skipped, and only one waiter per deployment runs. The Codex hook covers every in-progress deployment touched in the last two hours, including ones from other sessions. The Codex and OpenCode add-ons follow their documentation (Codex hooks; OpenCode tool.execute.after and client.session.prompt) and have not yet been run in those clients.

Harness configuration

Examples:

  • Codex CLI: ~/.codex/config.toml or a trusted project's .codex/config.toml.

  • Claude Code: merge into project .mcp.json.

  • OpenCode v1: merge into opencode.json/opencode.jsonc.

  • OpenCode v2: entries live under mcp.servers.

Use the executable directly to keep stdout reserved for MCP. structuredContent contains full results; text content is a short summary to avoid duplicated output.

Disk files

The tools use the disk's WebDAV storage (/api/platform-storage/desks/{disk}/dav/project/) with the same cookie. sync_files:

  • compares each local file with the disk by size, then by the server's SHA-256 checksum; only new and changed files are sent, and nothing is deleted;

  • uploads new files and creates folders without asking; if existing files would be replaced, it uploads nothing and returns confirm_required with the whole plan, to be approved once and repeated with confirm:true; dry_run:true only previews;

  • uploads each file to a temporary file in .rolos-temp/, in 4 MB chunks (the server rejects single requests above about 9 MB), then moves it into place and verifies the checksum, so a failed upload never leaves a half-written file;

  • skips .git, node_modules, .env, .venv, __pycache__, .DS_Store, symlinks, files over 100 MB, and any exclude patterns;

  • reports a conflict, and does nothing, when a file would replace a folder or the other way round;

  • stops starting new files after about 25 seconds and returns in_progress; repeating the same call continues, because finished files compare as unchanged.

A local folder's contents go into remote_path; a single file keeps its name. local_path must be absolute.

move_path takes every rename/move for a request as moves: [{from, to}]. Without confirm it returns confirm_required with the moves and any that cannot be done (missing source, taken destination, folder into itself); with confirm:true it performs them (WebDAV MOVE with Overwrite: F). It never overwrites; the destination's parent folder must already exist.

delete_path takes every path for a request as paths. Without confirm it returns confirm_required with what would go (folders with file and subfolder counts); with confirm:true it deletes. Paths inside a folder that is also listed are covered by that folder. Folders go with all their contents and there is no undo. The disk root cannot be deleted.

Builds: the build context is the folder containing the Dockerfile (verified live on training, 28 Oct 2026), so a Dockerfile that builds locally in its folder builds the same way on the platform. Every build gets its own image name in the UI style <service>-v<N> (or the requested build_image name, versioned if already taken).

Confirmation policy: creating folders and uploading new files never ask. Replacing, renaming or deleting asks once per request for the whole list.

A desk nobody opened recently has its file storage stopped (filesystem.status: not_started on the desk; WebDAV answers 502, then 503 while starting). Every file tool and the Dockerfile preflight check this first. If storage is stopped, the MCP starts it with POST /desks/{id}/filesystem (what opening the desk in the browser does) and waits up to 45 s; it took about 27-33 s live. If it is still starting, the tool returns not_ready with nothing changed, and the same call can be repeated.

Storage under load: the gateway answers 502/503/504 now and then. Requests that are safe to repeat are retried three times with backoff (MOVE and DELETE are not; after a failed MOVE the checksum tells whether the file landed). A file whose checksum still fails is left for the next call (in_progress) instead of failing the whole sync. Files this MCP process uploaded or checked are remembered, so a repeated sync does not checksum them again while their size and server time are unchanged. Storage requests run 4 at a time; each call uploads for up to 45 s. A build's local_path upload leaves out files matched by the folder's .dockerignore (not used when it has ! exceptions).

Verification

npm run check
npm test
npm run format:check

Tests build the server first, use a local HTTP simulator and SDK stdio client, and verify background execution without progress calls, public/internal links, recovery, partial overrides, native statuses and secret redaction. Tests never mutate live Constructor resources. Logs, readiness checks, registry imports and rollback remain outside the prototype.

Related MCP Connectors

Related MCP Servers