Skip to content

Execution Server

The shape serve command starts an in-process execution server that accepts Shape code over TCP using the wire protocol. Unlike shape wire-serve (which shells out to shape run), shape serve runs code directly in an embedded VM — eliminating process startup overhead and enabling persistent state.

Terminal window
# Start with defaults (localhost:9527, no auth)
shape serve
# With authentication and concurrency limit
shape serve --auth-token secret --max-concurrent 8
# Custom address
shape serve --address 127.0.0.1:8000
FlagDefaultDescription
--address127.0.0.1:9527Bind address
--tls-certTLS certificate (PEM). Required for non-localhost.
--tls-keyTLS private key (PEM). Required for non-localhost.
--auth-tokenBearer token. If set, clients must authenticate first.
--sandboxstrictSandbox level: strict, permissive, or none
--max-concurrent4Max parallel executions (semaphore-guarded)
-m, --modejitExecution mode: vm or jit (matches the binary’s default — JIT v2 / MirToIR)

shape serve refuses to start on a non-loopback address without --tls-cert and --tls-key. This prevents accidentally exposing code execution to the network without encryption.

The server uses the standard wire protocol: length-prefixed MessagePack frames with optional zstd compression. Every message is a variant of the WireMessage enum.

[4-byte big-endian length] [flags: u8] [MessagePack body...]

All V1 messages (Call, BlobNegotiation, Sidecar) and V2 messages (Execute, Validate, Auth, Ping/Pong) share the same framing.

Discover server capabilities:

→ WireMessage::Ping
← WireMessage::Pong(ServerInfo {
shape_version: "0.2.0",
wire_protocol: 2,
capabilities: ["execute", "validate", "call", "blob-negotiation"]
})

Run Shape source code:

→ WireMessage::Execute(ExecuteRequest {
code: "fn main() { 42 }",
request_id: 1
})
← WireMessage::ExecuteResponse(ExecuteResponse {
request_id: 1,
success: true,
output: Some("42"),
error: None,
diagnostics: [],
metrics: Some(ExecutionMetrics { wall_time_ms: 3, ... })
})

Execution runs in a blocking thread pool (tokio::task::spawn_blocking) with the full Shape engine pipeline: parse → compile → VM execute.

Parse and type-check without executing:

→ WireMessage::Validate(ValidateRequest { code: "let x: int = \"oops\"", request_id: 2 })
← WireMessage::ValidateResponse(ValidateResponse {
request_id: 2,
success: false,
diagnostics: [WireDiagnostic { severity: "error", message: "...", line: Some(1), column: Some(16) }]
})

Required when --auth-token is set. Must be sent before Execute/Validate/Call:

→ WireMessage::Auth(AuthRequest { token: "secret" })
← WireMessage::AuthResponse(AuthResponse { authenticated: true, error: None })

Without prior authentication, Execute/Validate/Call requests return an error.

The function call protocol enables transparent remote execution of compiled Shape functions. The @remote annotation builds on this — when you annotate a function with @remote("addr"), calling it locally serializes the function bytecode and arguments, sends them to the server, and returns the result:

from std::core::remote use { @remote }
@remote("127.0.0.1:9527")
fn compute(data) { data.map(|x| x * 2) }
let result = compute([1, 2, 3])
// result: Ok([2, 4, 6]) — executed on the remote server

This works with all value types (integers, strings, arrays, objects, closures) and with foreign functions (Python, TypeScript) — the remote server executes the Python/TypeScript code using its loaded language extensions.

Under the hood, @remote sends a RemoteCallRequest with the compiled BytecodeProgram and function arguments. Blob negotiation and sidecar splitting work as documented in Wire Protocol.

See Standard Library: Remote for the full API.

Client Server
│ │
│── Ping ────────────────────────────────>│
│<── Pong(ServerInfo) ────────────────────│
│ │
│── Auth(token) ─────────────────────────>│ (if --auth-token set)
│<── AuthResponse(ok) ───────────────────│
│ │
│── Execute(code) ───────────────────────>│ acquire semaphore
│<── ExecuteResponse(output) ────────────│ release semaphore
│ │
│── Execute(code) ───────────────────────>│
│<── ExecuteResponse(output) ────────────│
│ │
│ (disconnect) │

Each TCP connection maintains its own state: authentication status, blob cache for negotiation, and pending sidecars.

The MCP server (shape-mcp) can use shape serve as its execution backend instead of shelling out to shape run.

Set the SHAPE_SERVE_ADDR environment variable:

Terminal window
# Terminal 1: start the execution server
shape serve
# Terminal 2: start MCP with wire backend
SHAPE_SERVE_ADDR=127.0.0.1:9527 shape-mcp

When SHAPE_SERVE_ADDR is set, the MCP server connects to the execution server via MessagePack-over-TCP. This eliminates per-request process startup and provides faster code execution.

If the env var is unset, MCP falls back to CLI shell-out (shape run). If the env var is set but the server is unreachable, MCP returns an error — it does not silently fall back.

shape serveshape wire-serve
ExecutionIn-process VMShell-out to shape run
ProtocolV2 (Execute, Validate, Auth, Ping)V1 (custom WireRequest/WireResponse)
AuthBearer tokenNone
TLS safetyRefuses non-localhost without TLSNo check
ConcurrencySemaphore-limitedUnbounded

shape wire-serve is preserved for backward compatibility but shape serve is the recommended replacement.