> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiroleague.com/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTP Server

> The Hiro Server component that exposes the running workspace over local HTTP.

export const DiagramViewer = ({children, title = "Diagram"}) => {
  const dialogRef = useRef(null);
  const viewportRef = useRef(null);
  const contentRef = useRef(null);
  const [isOpen, setIsOpen] = useState(false);
  const [zoom, setZoom] = useState(1);
  const [pan, setPan] = useState({
    x: 0,
    y: 0
  });
  const zoomRef = useRef(1);
  const panRef = useRef({
    x: 0,
    y: 0
  });
  const dragRef = useRef(null);
  const open = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(true);
  };
  const close = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(false);
  };
  const resetView = () => {
    fitDiagramToViewport();
    setPan({
      x: 0,
      y: 0
    });
  };
  const setView = (nextZoom, nextPan) => {
    zoomRef.current = nextZoom;
    panRef.current = nextPan;
    setZoom(nextZoom);
    setPan(nextPan);
  };
  const MaximizeIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5" />
    </svg>;
  const ResetIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
      <path d="M3 3v5h5" />
    </svg>;
  const CloseIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M18 6 6 18M6 6l12 12" />
    </svg>;
  const fitDiagramToViewport = () => {
    const viewport = viewportRef.current;
    const content = contentRef.current;
    const svg = content?.querySelector(".mermaid svg[viewBox]");
    if (!viewport || !svg) return;
    const viewBox = svg.getAttribute("viewBox")?.split(/\s+/).map(Number);
    if (!viewBox || viewBox.length !== 4 || !viewBox[2] || !viewBox[3]) return;
    const [, , width, height] = viewBox;
    svg.style.width = `${width}px`;
    svg.style.maxWidth = "none";
    const rect = viewport.getBoundingClientRect();
    const nextZoom = Math.min(2.2, Math.max(0.75, Number((Math.min((rect.width - 96) / width, (rect.height - 96) / height) * 0.98).toFixed(3))));
    setView(nextZoom, {
      x: 0,
      y: 0
    });
  };
  const handleWheel = event => {
    if (!isOpen) return;
    event.preventDefault();
    event.stopPropagation();
    const viewport = viewportRef.current;
    if (!viewport) return;
    const rect = viewport.getBoundingClientRect();
    const pointerX = event.clientX - rect.left - rect.width / 2;
    const pointerY = event.clientY - rect.top - rect.height / 2;
    const currentZoom = zoomRef.current;
    const currentPan = panRef.current;
    const nextZoom = Math.max(0.25, Math.min(5, Number((currentZoom * (event.deltaY < 0 ? 1.12 : 0.88)).toFixed(3))));
    const ratio = nextZoom / currentZoom;
    setView(nextZoom, {
      x: pointerX - (pointerX - currentPan.x) * ratio,
      y: pointerY - (pointerY - currentPan.y) * ratio
    });
  };
  const onPointerDown = event => {
    if (!isOpen || event.button !== 0) return;
    event.preventDefault();
    dragRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      pan: panRef.current
    };
    try {
      event.currentTarget.setPointerCapture?.(event.pointerId);
    } catch {}
  };
  const onPointerMove = event => {
    const drag = dragRef.current;
    if (!isOpen || !drag || drag.pointerId !== event.pointerId) return;
    event.preventDefault();
    const nextPan = {
      x: drag.pan.x + event.clientX - drag.startX,
      y: drag.pan.y + event.clientY - drag.startY
    };
    panRef.current = nextPan;
    setPan(nextPan);
  };
  const endDrag = event => {
    if (dragRef.current?.pointerId === event.pointerId) {
      dragRef.current = null;
    }
  };
  useEffect(() => {
    const dialog = dialogRef.current;
    if (isOpen && dialog && !dialog.open) {
      dialog.showModal();
      requestAnimationFrame(() => {
        requestAnimationFrame(fitDiagramToViewport);
      });
    }
    if (!isOpen && dialog?.open) {
      dialog.close();
    }
  }, [isOpen]);
  useEffect(() => {
    if (!isOpen) return;
    const onKeyDown = event => {
      if (event.key === "Escape") close();
    };
    const onWheel = event => {
      event.preventDefault();
      event.stopPropagation();
      if (viewportRef.current?.contains(event.target)) {
        handleWheel(event);
      }
    };
    const previousOverflow = document.body.style.overflow;
    const previousHtmlOverflow = document.documentElement.style.overflow;
    document.body.style.overflow = "hidden";
    document.documentElement.style.overflow = "hidden";
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("wheel", onWheel, {
      capture: true,
      passive: false
    });
    return () => {
      document.body.style.overflow = previousOverflow;
      document.documentElement.style.overflow = previousHtmlOverflow;
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("wheel", onWheel, {
        capture: true
      });
    };
  }, [isOpen]);
  const toolbar = <div className="diagram-viewer__bar">
      <span className="diagram-viewer__title">{title}</span>
      <div className="diagram-viewer__actions">
        {isOpen && <span className="diagram-viewer__zoom" aria-live="polite">
            {Math.round(zoom * 100)}%
          </span>}
        {isOpen && <button className="diagram-viewer__button" type="button" onClick={resetView} aria-label="Reset diagram">
            <ResetIcon />
          </button>}
        <button className="diagram-viewer__button" type="button" onClick={isOpen ? close : open} aria-label={isOpen ? "Close" : "Maximize"}>
          {isOpen ? <CloseIcon /> : <MaximizeIcon />}
        </button>
      </div>
    </div>;
  const diagram = <div ref={viewportRef} className="diagram-viewer__viewport" onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={endDrag} onPointerCancel={endDrag}>
      <div className="diagram-viewer__stage" style={{
    transform: isOpen ? `translate(${pan.x}px, ${pan.y}px)` : undefined
  }}>
        <div className="diagram-viewer__centerer">
          <div ref={contentRef} className="diagram-viewer__content" style={{
    transform: isOpen ? `scale(${zoom})` : "none"
  }}>
            {children}
          </div>
        </div>
      </div>
    </div>;
  return <>
      {!isOpen && <div className="diagram-viewer not-prose">
          {toolbar}
          {diagram}
        </div>}

      <dialog ref={dialogRef} className="diagram-viewer-dialog" onWheel={event => {
    if (event.target === event.currentTarget) {
      event.preventDefault();
      event.stopPropagation();
    }
  }} onCancel={event => {
    event.preventDefault();
    close();
  }} onClose={() => setIsOpen(false)}>
        {isOpen && <div className="diagram-viewer diagram-viewer--open not-prose">
            {toolbar}
            {diagram}
          </div>}
      </dialog>
    </>;
};

HTTP Server is the local control API inside a Hiro Server process.

It runs beside the message pipeline. Devices do not use it; device traffic goes through Hiro Gate, the `devices` channel plugin, Channel Manager, Communication Manager, and Agent Manager. The HTTP Server is for same-machine clients that need to inspect or control the already-running workspace server.

The component also exposes the workspace Tool Registry over HTTP. Local clients can discover registered tools with `GET /tools` and invoke one by name with `POST /invoke`.

<DiagramViewer title="HTTP Server boundary">
  ```mermaid actions={false} theme={null}
  flowchart TB
      subgraph LocalClients["Local clients"]
          CLI["hiro CLI<br/>selected commands"]
          LocalTools["Local tool clients"]
          AdminSurfaces["Local app surfaces"]
          Browser["Browser / health checks"]
      end

      subgraph ServerProcess["Hiro Server process"]
          direction TB
          HTTP["HTTP Server<br/>FastAPI / uvicorn"]
          ServerContext["ServerContext"]
          ToolRegistry["Tool Registry"]
          ChannelManager["Channel Manager"]
          MetricsCollector["Metrics Collector"]
          CharacterStore["Character files"]
          RuntimeState["PID + runtime state"]
      end

      subgraph MessagePipeline["Message pipeline"]
          direction LR
          PipelineChannel["Channel Manager"]
          CommunicationManager["Communication Manager"]
          AgentManager["Agent Manager"]
      end

      CLI --> HTTP
      LocalTools --> HTTP
      AdminSurfaces --> HTTP
      Browser --> HTTP

      HTTP -->|"status / lifecycle"| ServerContext
      HTTP -->|"GET /tools<br/>POST /invoke"| ToolRegistry
      HTTP -->|"GET /channels"| ChannelManager
      HTTP -->|"GET /metrics"| MetricsCollector
      HTTP -->|"GET /characters/*"| CharacterStore
      HTTP -->|"GET /status"| RuntimeState

      PipelineChannel --> CommunicationManager
      CommunicationManager --> AgentManager

      style HTTP fill:#EF444455
      style MessagePipeline fill:transparent,stroke-dasharray:5 5
  ```
</DiagramViewer>

## Responsibilities

**Local status API** - `GET /status` reports whether the workspace server process is running, the PID, gateway URL, and current gateway WebSocket connection state.

**Channel inspection** - `GET /channels` returns connected channel plugin metadata from Channel Manager. This is live runtime state, not just workspace configuration.

**Tool Registry API** - `GET /tools` exposes the registered tool schema. `POST /invoke` executes one registered tool by name with a flat params object.

**Character profile API** - `GET /characters`, `GET /characters/{id}/profile`, and `GET /characters/{id}/photo` expose local character profile data and images.

**Metrics API** - Metrics endpoints return the latest metrics snapshot, history, collector status, and runtime metrics configuration.

**Lifecycle control** - `POST /_shutdown` and `POST /_restart` trigger graceful server shutdown or restart through `ServerContext`, allowing channel plugin subprocesses to be cleaned up.

***

## Main components

### **FastAPI app**

The HTTP app lives in `runtime/http_server.py`. It defines the local routes and runs under uvicorn inside the same asyncio event loop as the other server components.

### **`ServerContext`**

The HTTP app reads `ServerContext` from app state. Lifecycle routes set `stop_event` and restart flags there; status routes use it to locate workspace files and runtime state.

### **Tool Registry**

`server_process.py` builds a `ToolRegistry`, registers all Hiro tools, and attaches it to the HTTP app.

| Route          | Purpose                                                                |
| -------------- | ---------------------------------------------------------------------- |
| `GET /tools`   | Return all registered tool names, descriptions, and parameter schemas. |
| `POST /invoke` | Call `registry.invoke(tool, params)` and return the tool result.       |

The CLI usually calls tool classes directly with `.execute(...)`. The HTTP Tool Registry API is an additional local entry point, not the CLI's normal dispatch path.

### **Channel info provider**

The HTTP app receives a channel info provider from Channel Manager. `GET /channels` uses it to return connected plugin names, versions, and descriptions.

### **Metrics Collector**

The Metrics Collector is attached to the HTTP app state. Metrics routes read its latest snapshot, history, and configuration.

***

## Endpoint groups

| Endpoint group | Routes                                                                                   | Used for                                           |
| -------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------- |
| Status         | `GET /status`                                                                            | Process and gateway connection status.             |
| Channels       | `GET /channels`                                                                          | Connected channel plugin inspection.               |
| Tools          | `GET /tools`, `POST /invoke`                                                             | Tool discovery and tool execution over local HTTP. |
| Characters     | `GET /characters`, `GET /characters/{id}/profile`, `GET /characters/{id}/photo`          | Local character profile and image reads.           |
| Metrics        | `GET /metrics`, `GET /metrics/history`, `GET /metrics/status`, `POST /metrics/configure` | Runtime health snapshots and metrics settings.     |
| Lifecycle      | `POST /_shutdown`, `POST /_restart`                                                      | Graceful stop and restart.                         |

***

## Caller model

| Caller                         | Uses HTTP Server for                                                      |
| ------------------------------ | ------------------------------------------------------------------------- |
| Selected CLI commands          | Live channel status, metrics, graceful stop/restart.                      |
| Local tool clients             | Discovering and invoking registered tools through `/tools` and `/invoke`. |
| Local browser or health checks | Reading `/status` and other inspection endpoints.                         |
| Local app surfaces             | Reading local workspace runtime data and operational state.               |

Mobile devices and third-party communication apps do not call this HTTP server. They communicate through Hiro Gate and channel plugins.

***

## Processing flow

<DiagramViewer title="HTTP tool invocation">
  ```mermaid actions={false} theme={null}
  sequenceDiagram
      participant Client as Local client
      participant HTTP as HTTP Server
      participant Registry as Tool Registry
      participant Tool as Hiro Tool
      participant Workspace as Workspace state

      Client->>HTTP: GET /tools
      HTTP->>Registry: schema()
      Registry-->>HTTP: tool schemas
      HTTP-->>Client: JSON schemas

      Client->>HTTP: POST /invoke<br/>{tool, params}
      HTTP->>Registry: invoke(tool, params)
      Registry->>Tool: execute(**safe_params)
      Tool->>Workspace: read/write workspace state
      Workspace-->>Tool: result data
      Tool-->>Registry: result
      Registry-->>HTTP: InvokeResult
      HTTP-->>Client: JSON result
  ```
</DiagramViewer>

***

## Related components

### **Channel Manager**

HTTP Server reads connected channel plugin metadata from Channel Manager for `/channels`. It does not route messages through Channel Manager.

### **Metrics Collector**

HTTP Server exposes Metrics Collector snapshots and configuration over the metrics endpoints.

### **Tool Registry**

HTTP Server provides a generic local API over the same tool implementations used by CLI commands, Admin UI services, and agent tooling.

### **ServerContext**

HTTP Server uses `ServerContext` for lifecycle control and workspace-scoped runtime access.

***

## See also

<CardGroup cols={2}>
  <Card title="Hiro Server components" icon="server" href="/architecture/concepts/hiro-server-components">
    How the main server components relate to each other.
  </Card>

  <Card title="Channel Manager" icon="plug" href="/architecture/concepts/channel-manager">
    The transport manager that owns channel plugins and WebSocket JSON-RPC.
  </Card>

  <Card title="Communication Manager" icon="arrows-left-right" href="/architecture/concepts/communication-manager">
    The internal message router between channels and the agent runtime.
  </Card>

  <Card title="Agent Manager" icon="robot" href="/architecture/concepts/agent-manager">
    The assistant runtime that consumes agent-ready messages.
  </Card>
</CardGroup>
