Skip to content

Architecture

Components

The Python server registers read tools and optional action tools, then constructs jobs. RevitChannel serializes calls with an asyncio lock. The pipe host serves REVIT_MCP_HOST=local: it reads heartbeats, talks pipe/1 to the selected add-in and falls back to the file channel. The HTTP host submits jobs and polls results by ID. The PowerShell host publishes jobs and reads responses locally or through SSH. The add-in application creates the Revit ExternalEvent, the pipe listener and the heartbeat. The core project contains parsers, data contracts, serializers and the pipe/1 codec without Revit API references.

flowchart LR
    Client[MCP client] <-->|stdio, optionally through SSH| Server[Python server]
    Server <-->|named pipe pipe/1| Pipe[PipeChannel]
    Server <-->|local PowerShell or SSH| Files[Windows file channel]
    Files --> Watcher[FileSystemWatcher and timer]
    Server <-->|HTTP + bearer token| HTTP[HttpListener]
    Pipe --> Scheduler[JobScheduler]
    Watcher --> Scheduler
    HTTP --> Scheduler
    Scheduler --> Event[ExternalEvent]
    Event --> Handler[Read and action handlers]
    Handler --> Files
    Handler --> Pipe
    Handler --> HTTP

Pipe request lifecycle

  1. The server reads fresh heartbeats in ROOT and selects one target with the same rules as the file channel.
  2. If the heartbeat lists pipe/1, it opens \\.\pipe\RevitModelMcp.<pid> and sends hello; the reply must match the heartbeat pid and instanceId.
  3. The server sends submit with the job; the add-in queues it in the shared scheduler and requests an ExternalEvent.
  4. The add-in pushes state changes and one final message with the command response. Pipe threads never call the Revit API.
  5. On disconnect the add-in cancels that connection's queued jobs; a running job finishes, and a reconnected client can ask for its status.

File request lifecycle

  1. Under its existing job lock, the server discovers processes and heartbeats in ROOT and selects one target.
  2. It pins the PID, startedUtc identity and ROOT\instances\<pid> directory, then confirms the v2 channel with a bounded correlated ping.
  3. It verifies the selected process and identity again, writes a unique mcp_<uuid>.tmp and atomically moves it to job_<jobId>.json in that directory.
  4. The instance's watcher requests an ExternalEvent; a 10-second timer provides a fallback check.
  5. The watcher moves the job into a thread-safe scheduler and removes its file. Revit checks the PID and active document in its API context; actions resolve the addressed open document.
  6. Readers atomically write correlated responses in the same directory. Paged sessions request further ExternalEvent callbacks until complete.
  7. The server polls that fixed directory and validates response correlation and PID. It copies exported PNGs and cleans only the current job's files there.

Jobs contain a command and command-specific fields. Successful responses contain command, success and data. Responses also carry timing, responder and client metadata, jobId and queuedMs. See the response contracts. The server gives each job a fresh correlationId; v2 responses must echo it. Late responses from other jobs are ignored. The server assigns a jobId and a process-wide clientId; clientName comes from MCP initialize. HTTP polls /jobs/{id}. Completed read results expire after ten minutes and action results after 24 hours; see result retention. The asyncio lock serializes calls within one server process only. Clients targeting different PIDs use separate directories. Clients targeting the same PID have separate FIFO queues. The scheduler rotates between clients and executes one job or read-session slice per ExternalEvent.

Revit context and model access

File watcher and timer callbacks request work through ExternalEvent. The event handler calls ControlChannel.Tick in the Revit API context. The default MCP tools query the model and export images without editing model elements. Actions are enabled by default. The server setting and workstation file can independently refuse them. See action behavior for transactions, warning resolution and dialog suppression. Image export uses Document.ExportImage with a selected view set. It does not change the active view. File output, logs and optional window activation are observable side effects.

Instances and heartbeat

Each Revit instance writes ROOT\instance_<processId>.json every five seconds. The heartbeat preserves process ID, Revit version, active document title, path and updatedUtc. It also reports fileChannelVersion=2, a startedUtc identity fixed at startup, and nullable httpPort populated only after a successful listener bind. Discovery version 3 adds a per-process instanceId, pipeName, protocols and every open non-linked document. The first heartbeat is written after the pipe listens, so pipe/1 is never advertised for a pipe that is not accepting clients. The add-in caches document information from Revit events before the timer writes it. Heartbeat replacement uses a temporary file and File.Replace or File.Move. File discovery reads only ROOT and combines fresh heartbeat data with every running Revit process. Missing, malformed or expired heartbeats leave the process visible with pluginResponding=false and empty document fields. For v2, a fresh heartbeat is a pre-check; pluginResponding=true requires a bounded ping confirming correlation, responder PID and unchanged startup identity. Busy or timed-out handshakes never remove a process from discovery.

Each instance watches job_*.json in its own ROOT\instances\<pid> directory. Responses, atomic temporary files and PNGs share that per-PID directory. Cleanup never targets another instance's directory, and later discovery cannot redirect an outstanding job.

document maps to targetDocument in a job. Directed reads match active document titles or file names case-insensitively and require exactly one matching instance before publication. With one running Revit process, every call goes to it. With several, document or process_id selects one, and an unaddressed call fails before publication. Processes without confirmed channels count as running. The PID guard remains in the add-in. After claiming a directed read, the add-in rejects an active document that no longer matches; the correlated error consumes the job file. Actions retain their existing open-document resolution.

Update the server and the add-in together: the server accepts an add-in of the same major version and fileChannelVersion=2 only, and refuses a heartbeat without that field before publication. There is no legacy ROOT channel. HTTP remains one configured endpoint with PID and startup identity checked through /health; no port scanning occurs. See transport compatibility.

Failure behavior

A seventeenth queued job from one client returns queue_full with retryAfterMs. A pickup timeout leaves the pending job file in place. The job can execute after the caller receives that timeout. A response timeout means pickup occurred but no new response was found within the response budget. Network polling retries transient SSH and command timeout failures. Read-command failures become MCP tool errors. Action failures returned by the executor preserve their response object, including error and available action metadata. Transport failures become MCP tool errors for both reads and actions. Model data and add-in error messages retain their source language.

See the feed format for field names and directories and known gaps for protocol limitations.

Batch supervisor

The batch supervisor is a separate net48 process with a Core reference and no Revit API reference. It owns only workers it launched, records phase progress atomically in run.json, and resumes unfinished models after restart. The add-in exposes a small launch, pre-pass, open, snapshot, and close command set. The worker sends batch-snapshot through the existing v2 file channel. See batch collection for routing and failure behavior.