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

# Port Proxy

> Expose HTTP services running inside sandboxes to external clients via authenticated reverse-proxy URLs.

Port proxy lets external HTTP clients reach any port inside a running sandbox. Each sandbox gets a stable URL pattern based on its ID and port number, so you can start a web server, API, or MCP server inside the VM and access it from outside without SSH tunnels or port-forwarding configuration.

## Quick start

Start an HTTP server inside a sandbox, then access it from anywhere using the proxy URL.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from declaw import Sandbox

    sbx = Sandbox.create(template="python")

    # Start a web server inside the sandbox
    sbx.commands.run("nohup python3 -c \"\n"
        "from http.server import HTTPServer, BaseHTTPRequestHandler\n"
        "class H(BaseHTTPRequestHandler):\n"
        "    def do_GET(self):\n"
        "        self.send_response(200)\n"
        "        self.send_header('Content-Type', 'text/plain')\n"
        "        self.end_headers()\n"
        "        self.wfile.write(b'Hello from sandbox!')\n"
        "HTTPServer(('', 8080), H).serve_forever()\n"
        "\" &>/dev/null &")

    import time; time.sleep(1)

    # Get the public URL
    url = sbx.get_host(8080)
    print(url)
    # https://api.declaw.ai/sandboxes/sbx-.../ports/8080
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { Sandbox } from '@declaw/sdk';

    const sbx = await Sandbox.create({ template: 'node' });

    await sbx.commands.run(`nohup node -e "
    const http = require('http');
    http.createServer((req, res) => {
      res.writeHead(200, {'Content-Type': 'text/plain'});
      res.end('Hello from sandbox!');
    }).listen(8080);
    " &>/dev/null &`);

    const url = sbx.getHost(8080);
    console.log(url);
    // https://api.declaw.ai/sandboxes/sbx-.../ports/8080
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    sbx, _ := declaw.Create(ctx, declaw.WithTemplate("base"))

    sbx.Commands.Run(ctx, `nohup python3 -c "
    from http.server import HTTPServer, BaseHTTPRequestHandler
    class H(BaseHTTPRequestHandler):
        def do_GET(self):
            self.send_response(200)
            self.send_header('Content-Type', 'text/plain')
            self.end_headers()
            self.wfile.write(b'Hello from sandbox!')
    HTTPServer(('', 8080), H).serve_forever()
    " &>/dev/null &`)

    url := sbx.GetHost(8080)
    fmt.Println(url)
    // https://api.declaw.ai/sandboxes/sbx-.../ports/8080
    ```
  </Tab>
</Tabs>

The returned URL is a fully qualified HTTPS endpoint. Any HTTP client (browser, curl, SDK) can send requests to it, authenticated with your API key in the `X-API-Key` header.

## How it works

1. Your client sends an HTTP request to `https://api.declaw.ai/sandboxes/{sandbox_id}/ports/{port}/{path}`.
2. Declaw authenticates the request using the `X-API-Key` header.
3. If `allow_public_traffic` is enabled (the default), the request is proxied to the target port inside the sandbox.
4. The response from the in-sandbox server is returned to the client.

For WebSocket requests, the same auth and validation runs first. Once authenticated, the connection is upgraded and Declaw maintains a bidirectional tunnel between your client and the in-sandbox server for the lifetime of the connection.

Subpaths are preserved. A request to `.../ports/8080/api/v1/users` reaches the sandbox as `GET /api/v1/users`.

## MCP servers

Declaw provides a convenience method for the common pattern of running an MCP server on port 50005 inside a sandbox.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    url = sbx.get_mcp_url()
    print(url)
    # https://api.declaw.ai/sandboxes/sbx-.../ports/50005/mcp
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const url = sbx.getMcpUrl();
    console.log(url);
    // https://api.declaw.ai/sandboxes/sbx-.../ports/50005/mcp
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    url := sbx.GetMcpURL()
    fmt.Println(url)
    // https://api.declaw.ai/sandboxes/sbx-.../ports/50005/mcp
    ```
  </Tab>
</Tabs>

This is equivalent to calling `get_host(50005) + "/mcp"`. Use the `mcp-server` template to get a sandbox pre-configured with FastMCP and the standard MCP dependencies.

## WebSocket

Port proxy supports WebSocket connections. Use the same URL from `get_host()`, replacing `https://` with `wss://`.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import asyncio
    import websockets
    from declaw import Sandbox

    sbx = Sandbox.create(template="python")

    # Start a WebSocket echo server inside the sandbox
    sbx.commands.run("pip install websockets -q", timeout=30)
    sbx.files.write("/tmp/ws.py", """
    import asyncio, websockets

    async def echo(ws):
        async for msg in ws:
            await ws.send("echo: " + msg)

    async def main():
        async with websockets.serve(echo, "0.0.0.0", 8765):
            await asyncio.Future()

    asyncio.run(main())
    """)
    sbx.commands.run("nohup python3 /tmp/ws.py &>/dev/null &")

    import time; time.sleep(2)

    # Connect via WebSocket
    url = sbx.get_host(8765).replace("https://", "wss://")

    async def main():
        async with websockets.connect(url, additional_headers={"X-API-Key": "YOUR_API_KEY"}) as ws:
            await ws.send("hello")
            print(await ws.recv())  # "echo: hello"

    asyncio.run(main())
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { Sandbox } from '@declaw/sdk';

    const sbx = await Sandbox.create({ template: 'node' });

    // Start a WebSocket server inside the sandbox
    await sbx.commands.run(`npm install ws -q && nohup node -e "
    const { WebSocketServer } = require('ws');
    const wss = new WebSocketServer({ port: 8765 });
    wss.on('connection', ws => {
      ws.on('message', msg => ws.send('echo: ' + msg));
    });
    " &>/dev/null &`);

    // Connect via WebSocket (requires the 'ws' package)
    import WebSocket from 'ws';

    const url = sbx.getHost(8765).replace('https://', 'wss://');
    const ws = new WebSocket(url, { headers: { 'X-API-Key': 'YOUR_API_KEY' } });
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    sbx, _ := declaw.Create(ctx, declaw.WithTemplate("python"))

    url := strings.Replace(sbx.GetHost(8765), "https://", "wss://", 1)
    // Connect using gorilla/websocket or any WS client library
    ```
  </Tab>
</Tabs>

WebSocket connections require the same `X-API-Key` header as HTTP requests, passed during the upgrade handshake via `additional_headers` (Python) or connection options (TypeScript/Go).

## Security

### Authentication

All port proxy requests require a valid `X-API-Key` header, the same key used for every other sandbox operation. Unauthenticated requests receive `HTTP 401`.

### Disabling port proxy

Port proxy access is controlled by the `allow_public_traffic` field in the sandbox's network configuration. It defaults to `true`. Set it to `false` to block all inbound port proxy requests.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from declaw import Sandbox

    sbx = Sandbox.create(
        network={"allow_public_traffic": False}
    )
    # Requests to sbx.get_host(8080) will receive HTTP 403
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { Sandbox } from '@declaw/sdk';

    const sbx = await Sandbox.create({
      network: { allowOut: [], denyOut: [], allowPublicTraffic: false },
    });
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    f := false
    sbx, _ := declaw.Create(ctx, declaw.WithNetwork(declaw.SandboxNetworkOpts{
        AllowPublicTraffic: &f,
    }))
    ```
  </Tab>
</Tabs>

### Blocked ports

Port 49983 is reserved for internal use and cannot be proxied. Requests targeting this port return `HTTP 403`.

### Cookie stripping

`Set-Cookie` headers are stripped from all proxied responses to prevent cookie injection attacks against the API domain.

## Limits

| Limit                  | Value                               |
| ---------------------- | ----------------------------------- |
| Request body size      | 100 MiB (HTTP only)                 |
| Supported HTTP methods | GET, POST, PUT, PATCH, DELETE, HEAD |
| WebSocket              | Supported (upgrade via GET)         |
| Blocked ports          | 49983 (reserved)                    |

## CORS

CORS preflight (`OPTIONS`) requests are handled automatically by the proxy and do not require an `X-API-Key` header. This allows browser-based applications to call sandbox services directly without a backend relay.

<Note>
  All other HTTP methods (GET, POST, etc.) still require the `X-API-Key` header, even when called from a browser. Use a backend proxy or serverless function if you cannot expose your API key to the client.
</Note>
