# 🧱 The MCP Cell — an AI That Can Only Talk to Pipe's Own MCP Server

> Source: <https://pipe-lang.com/blog/mcp-cell.html>
> Published: 2026-08-14 00:00:00+00:00

[← All posts← Alle Beiträge](../blog.html)

# 🧱 The MCP Cell — an AI That Can Only Talk to Pipe's Own MCP Server

**The smallest demo of the strongest idea: a sandboxed AI agent whose entire universe is Pipe's own MCP server. No free network, no free filesystem, no shell — just five tools behind a whitelist.**

Everyone asks the same question about sandboxes: *"But if the agent has tools, aren't the tools the attack surface?"*

Fair. So this time I flipped it. Instead of giving the agent a toolbox and hoping the sandbox holds, I built a profile where the **only** things the agent can do are the MCP tools that *I* chose to expose — and nothing else. The AI's entire world is one MCP server, and that server lives inside the sandbox too.

The example lives in [ examples/mcp_sandbox_agent.pipe](https://github.com/MachuraHarry/pipe/blob/master/examples/mcp_sandbox_agent.pipe). It has two modes: an in-process agent mode (

`agent`

) and a real MCP server on stdio (`serve`

) that external clients like Claude Desktop or Cursor can plug into. Both modes give the exact same five tools.## The Cell 🏗️

The profile is the wall:

```
sandbox_profile "mcp-cell" {
    fs:                "temp-only",       # every write lands in ./.pipe_sandbox
    network:           true,
    network_whitelist: [provider_host],   # the ONLY network target is the AI provider
    exec:              false,             # no shell, period
    ai:                true,
    budget:            0.5,
    max_tool_calls:    25,
    audit_log:         true,
    timeout:           30
}
```

Inside that wall the agent can call exactly five tools, defined with ordinary Pipe functions and registered with `ai_tool`

:

| Tool | What it does |
|---|---|
`sb_write` | Writes a file — redirected into `.pipe_sandbox` |
`sb_read` | Reads a file from the sandbox |
`sb_list` | Lists the sandbox directory |
`sb_note` | Stores an in-memory note (a plain Pipe map) |
`sb_ping` | Liveness check |

```
fn sb_note key value
    set __cell_notes key value
    "Notiz gesetzt: " ++ key ++ " = " ++ value

ai_tool "sb_note" "Legt eine In-Memory-Notiz an" {key: "Schluessel", value: "Wert"} sb_note
```

Then the sandbox is locked in with `set_sandbox "mcp-cell"`

, and the server is started. Because `mcp_server`

bridges whatever `ai_tool`

registered, the client-facing tool list is *the same five functions* — and every single `tools/call`

executes under the active profile.

## What the Agent Actually Did 🤖

In `agent`

mode the task was simple: create three notes, write a summary file, list the sandbox. DeepSeek did it in six tool calls:

```
tool_call | sb_note
tool_call | sb_note
tool_call | sb_note
tool_call | sb_write
tool_call | sb_list
```

That's the whole audit log. No `exec`

, no foreign `http_get`

, no writes outside the cell. The agent *couldn't* have done more even if it had tried — the profile blocks it at the builtin level, not at the prompt level.

## The Real Proof: Trying to Break Out 💥

A demo that only shows success proves nothing. So I tried to escape, using the same builtins an attacker would reach for inside the cell:

| Attempt | Result |
|---|---|
`exec "id"` | `E_SANDBOX: exec blocked by profile 'mcp-cell'` |
`http_get "https://www.google.com"` | `E_SANDBOX: network target not in whitelist` |
`write_file "/tmp/escape.txt"` | silently redirected to `.pipe_sandbox/escape.txt` |

The third one is the interesting case: temp-only **doesn't fail**, it *redirects*. The agent writes wherever it wants and believes it wrote to `/tmp/escape.txt`

— the file actually lands inside the cell. The attacker gets a consistent illusion, and the host stays clean.

## One Real Bug Found Along the Way 🐛

Building this demo surfaced an actual bug in Pipe. With `fs: "temp-only"`

and the default `workingDir: "."`

, listing the sandbox directory broke:

```
list_dir: open /tmp/.pipe_sandbox/tmp: no such file or directory
```

The `filepath.Rel`

call in the redirect logic can't relate an absolute path to a *relative* base — so `list_dir "."`

resolved to the wrong place. The fix was to make the profile's working directory absolute everywhere ([ sandbox.go](https://github.com/MachuraHarry/pipe/blob/master/pkg/object/sandbox.go)):

```
func currentDir() string {
    d, err := os.Getwd()
    if err != nil || d == "" {
        return "."
    }
    return d
}
```

Now every profile starts with an absolute working directory, and temp-only redirects behave correctly — including `list_dir "."`

.

## Try It Yourself 🚀

```
# DeepSeek
DEEPSEEK_API_KEY=sk-... pipe examples/mcp_sandbox_agent.pipe agent

# local, no key needed
pipe examples/mcp_sandbox_agent.pipe serve ollama
```

Then point Claude Desktop or Cursor at the stdio server:

```
{
  "mcpServers": {
    "pipe-cell": {
      "command": "pipe",
      "args": ["examples/mcp_sandbox_agent.pipe", "serve"]
    }
  }
}
```

The same `mcp-cell`

profile wraps both the in-process agent and every external client call. One sandbox, two entry points, same guarantee.

# 🧱 Die MCP-Zelle — eine KI, die nur mit Pipes eigenem MCP-Server sprechen kann

**Die kleinste Demo der stärksten Idee: eine KI in der Sandbox, deren ganzes Universum Pipes eigener MCP-Server ist. Kein freies Netz, kein freies Dateisystem, keine Shell — nur fünf Tools hinter einer Whitelist.**

Jeder fragt bei Sandboxen dasselbe: *"Aber wenn der Agent Werkzeuge hat, ist der Werkzeugsatz nicht die Angriffsfläche?"*

Fair. Also habe ich es diesmal umgedreht. Statt dem Agenten einen Werkzeugkasten zu geben und zu hoffen, dass die Sandbox hält, habe ich ein Profil gebaut, in dem der Agent **ausschließlich** die MCP-Tools nutzen kann, die *ich* freigebe — und sonst nichts. Die ganze Welt der KI ist ein einziger MCP-Server, und dieser Server lebt ebenfalls in der Sandbox.

Das Beispiel liegt in [ examples/mcp_sandbox_agent.pipe](https://github.com/MachuraHarry/pipe/blob/master/examples/mcp_sandbox_agent.pipe). Es hat zwei Modi: einen In-Process-Agenten (

`agent`

) und einen echten MCP-Server auf stdio (`serve`

), in den sich externe Clients wie Claude Desktop oder Cursor einklinken. Beide Modi geben exakt dieselben fünf Tools.## Die Zelle 🏗️

Das Profil ist die Wand:

```
sandbox_profile "mcp-cell" {
    fs:                "temp-only",       # every write lands in ./.pipe_sandbox
    network:           true,
    network_whitelist: [provider_host],   # the ONLY network target is the AI provider
    exec:              false,             # no shell, period
    ai:                true,
    budget:            0.5,
    max_tool_calls:    25,
    audit_log:         true,
    timeout:           30
}
```

Innerhalb dieser Wand kann der Agent genau fünf Tools aufrufen, definiert als ganz normale Pipe-Funktionen und registriert mit `ai_tool`

:

| Tool | Was es tut |
|---|---|
`sb_write` | Schreibt eine Datei — umgeleitet nach `.pipe_sandbox` |
`sb_read` | Liest eine Datei aus der Sandbox |
`sb_list` | Listet das Sandbox-Verzeichnis |
`sb_note` | Legt eine In-Memory-Notiz an (eine Pipe-Map) |
`sb_ping` | Lebenszeichen-Check |

```
fn sb_note key value
    set __cell_notes key value
    "Notiz gesetzt: " ++ key ++ " = " ++ value

ai_tool "sb_note" "Legt eine In-Memory-Notiz an" {key: "Schluessel", value: "Wert"} sb_note
```

Danach wird die Sandbox mit `set_sandbox "mcp-cell"`

festgeschaltet und der Server gestartet. Da `mcp_server`

alles bridged, was `ai_tool`

registriert hat, ist die client-seitige Toolliste *dieselben fünf Funktionen* — und jeder `tools/call`

läuft unter dem aktiven Profil.

## Was der Agent wirklich tat 🤖

Im `agent`

-Modus war die Aufgabe simpel: drei Notizen anlegen, eine Zusammenfassung schreiben, die Sandbox auflisten. DeepSeek erledigte das in sechs Tool-Calls:

```
tool_call | sb_note
tool_call | sb_note
tool_call | sb_note
tool_call | sb_write
tool_call | sb_list
```

Das ist das gesamte Audit-Log. Kein `exec`

, kein fremder `http_get`

, keine Schreibzugriffe außerhalb der Zelle. Der Agent *hätte* nicht mehr tun können, selbst wenn er es versucht hätte — das Profil blockt auf Builtin-Ebene, nicht auf Prompt-Ebene.

## Der echte Beweis: Der Ausbruchsversuch 💥

Eine Demo, die nur Erfolg zeigt, beweist nichts. Also habe ich versucht auszubrechen — mit genau den Builtins, nach denen ein Angreifer in der Zelle greifen würde:

| Versuch | Ergebnis |
|---|---|
`exec "id"` | `E_SANDBOX: exec blocked by profile 'mcp-cell'` |
`http_get "https://www.google.com"` | `E_SANDBOX: network target not in whitelist` |
`write_file "/tmp/escape.txt"` | stillschweigend umgeleitet nach `.pipe_sandbox/escape.txt` |

Der dritte Fall ist der interessante: temp-only **scheitert nicht**, es *leitet um*. Der Agent schreibt, wohin er will, und glaubt, er habe nach `/tmp/escape.txt`

geschrieben — die Datei landet tatsächlich in der Zelle. Der Angreifer bekommt eine konsistente Illusion, und der Host bleibt sauber.

## Ein echter Bug, der dabei auffiel 🐛

Beim Bauen dieser Demo fiel ein echter Bug in Pipe auf. Mit `fs: "temp-only"`

und dem Standard-`workingDir: "."`

brach das Auflisten des Sandbox-Verzeichnisses:

```
list_dir: open /tmp/.pipe_sandbox/tmp: no such file or directory
```

Der `filepath.Rel`

-Aufruf in der Redirect-Logik kann einen absoluten Pfad nicht auf eine *relative* Basis beziehen — deshalb landete `list_dir "."`

an der falschen Stelle. Der Fix: Das Arbeitsverzeichnis des Profils wird überall absolut gemacht ([ sandbox.go](https://github.com/MachuraHarry/pipe/blob/master/pkg/object/sandbox.go)):

```
func currentDir() string {
    d, err := os.Getwd()
    if err != nil || d == "" {
        return "."
    }
    return d
}
```

Jetzt startet jedes Profil mit absolutem Arbeitsverzeichnis, und temp-only- Redirects verhalten sich korrekt — inklusive `list_dir "."`

.

## Probier es selbst 🚀

```
# DeepSeek
DEEPSEEK_API_KEY=sk-... pipe examples/mcp_sandbox_agent.pipe agent

# local, no key needed
pipe examples/mcp_sandbox_agent.pipe serve ollama
```

Danach Claude Desktop oder Cursor auf den stdio-Server zeigen lassen:

```
{
  "mcpServers": {
    "pipe-cell": {
      "command": "pipe",
      "args": ["examples/mcp_sandbox_agent.pipe", "serve"]
    }
  }
}
```

Dasselbe `mcp-cell`

-Profil umschließt sowohl den In-Process-Agenten als auch jeden externen Client-Aufruf. Eine Sandbox, zwei Einstiegspunkte, dieselbe Garantie.
