---
title: "Tatolab: turn anything into a live stream"
description: "Tatolab turns anything into a realtime stream and lets you decide exactly who can reach it."
type: "page"
url: "https://tatolab.com/"
site: "Tatolab"
---

# Turn anything into a live stream

Wire cameras, models and your own code into a stream. Expose an output, and apps, agents and your other machines read it by URL.

- **Install on macOS:** `brew install --cask tatolab/tap/tatolab`, then `open -a Tatolab`
- **Install on Linux:** `curl -fsSL https://get.tatolab.com/install.sh | sh` (the script: https://get.tatolab.com/install.sh)
- **Needs:** Apple silicon, macOS 15 or later, or x86_64 Linux with a Vulkan GPU. Install guide: https://tatolab.com/docs/install
- **Then connect your agent:** `claude mcp add tatolab -- tatolab mcp`
- **Next:** the quickstart at https://tatolab.com/docs/quickstart

## Every exposed output has a URL

Open it in a browser, pull it into another stream, or hand it to an agent. The overlay node's output is at `studio/pitch/overlay/video_to_downstream`, and the page shows it open in a browser: a soccer match with every person and the ball boxed.

An agent connected over MCP samples the output with `tap("pitch/overlay/video_to_downstream")`, gets 3 bags, turns a frame into an image with `exchange(surface_id)`, and says what it sees: five players are in view, and the ball is near the center circle.

Change `stream.py` while `tatolab dev` runs, and the feed follows. Box just the ball:

```python
overlay = stream_builder.add(
    Overlay, config={"only": "sports ball"}
)
```

```txt
tatolab dev: a saved change — loading pitch again
tatolab dev: pitch loaded (2 nodes, project /home/you/pitch); Ctrl-C stops it
```

## A stream is a function, a node is a class

Wrap a model, an SDK or a driver in a node. Each one runs in its own process.

`stream.py` pulls the camera from pitch-cam, runs a Hugging Face model on it, and draws what it finds:

```python
@stream
def pitch(stream_builder: StreamBuilder) -> None:
    camera = stream_builder.remote_output("pitch-cam/sideline/camera/video")
    detector = stream_builder.add(
        HuggingFaceDetector, config={"model": "facebook/detr-resnet-50"}
    )
    overlay = stream_builder.add(Overlay)
    stream_builder.connect(camera, detector.input("video_from_upstream"))
    stream_builder.connect(camera, overlay.input("video_from_upstream"))
    stream_builder.connect(
        detector.output("detections_to_downstream"),
        overlay.input("detections_from_upstream"),
    )
    stream_builder.expose(
        overlay.output("video_to_downstream"), Exposure.PUBLIC
    )
```

`nodes/detector.py`:

```python
@node
class HuggingFaceDetector:
    @node.input(delivery_profile="newest")
    def video_from_upstream(self) -> VideoFrame: ...

    @node.output()
    def detections_to_downstream(self) -> dict: ...

    def __init__(self, config: HuggingFaceDetectorConfig) -> None:
        self.model = config.model

    def setup(self, ctx: RuntimeContextFullAccess) -> None:
        self.detect = pipeline("object-detection", model=self.model)

    def process(self, ctx: RuntimeContextLimitedAccess) -> None:
        frame = ctx.inputs.read("video_from_upstream", into=VideoFrame)
        if frame is not None:
            with frame.cpu() as pixels:
                objects = self.detect(Image.fromarray(pixels[:, :, :3]))
            ctx.outputs.write("detections_to_downstream", {"objects": objects})
```

The page draws `stream.py` as a graph: the camera on pitch-cam (`pitch-cam/sideline`) feeds the detector (`detr-resnet-50`) and the overlay, the detector feeds the overlay, and the overlay's output has a URL, `studio/pitch/overlay/video_to_downstream`.

Run one in five minutes: https://tatolab.com/docs/quickstart. Write a node: https://tatolab.com/docs/build/nodes

## Works with your agents

Over MCP, an agent sees the live graph, samples outputs, looks at frames, reads logs, and rewires nodes.

- **Over MCP:** Claude Code, Claude Desktop, Cline, Codex, Cursor, Devin Desktop, Gemini CLI, GitHub Copilot, Grok Build and Zed.
- **Computer use:** Grok Bot and Muse. They open a stream by its URL.
- **Set up:** `claude mcp add tatolab -- tatolab mcp`. One command or a few lines of config for each agent: https://tatolab.com/docs/share/agents

## Nothing leaves until you expose it

Set each output's level in code, and change it while the stream runs.

- **Internal.** Only links inside the stream read it. Every output starts here.
- **Private.** Streams and agents on this machine read it: `stream_builder.expose(output)`.
- **Public.** Your other machines read it by URL, over your tailnet or a relay: `stream_builder.expose(output, Exposure.PUBLIC)`.
- **Live.** Raise or remove a level without a restart: `tatolab expose pitch overlay video_to_downstream --remove`.

Outputs travel over MoQ on QUIC, which is always encrypted: across your tailnet, or through a relay you run or join.

## Install it

The quickstart has a stream running in about five minutes: https://tatolab.com/docs/quickstart. The docs: https://tatolab.com/docs

- **macOS:** `brew install --cask tatolab/tap/tatolab`, then `open -a Tatolab`
- **Linux:** `curl -fsSL https://get.tatolab.com/install.sh | sh`
- **Needs:** Apple silicon, macOS 15 or later, or x86_64 Linux with a Vulkan GPU.

Questions? Email hello@tatolab.com. Agents can read this page as Markdown at https://tatolab.com/index.md, and https://tatolab.com/llms.txt lists the rest of the site.
