# Mfl Networking

> MFL networking builtins: building HTTP servers and clients, WebSocket, request parsing, Basic auth, MIME types, concurrent request handling with goroutines. Patterns from machin-serve.

- Skill: `javimosch/mfl-networking` (Agent Skill)
- Install (CLI): `npx skillmds@latest add javimosch/mfl-networking`
- Raw SKILL.md: https://api.skillmd.com/api/skills/javimosch/mfl-networking/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: javimosch (https://skillmd.com/u/javimosch)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/javimosch/mfl-networking

---


# MFL Networking

Build network services using machin's `listen`/`accept`/`read`/`write`/`close` builtins, HTTP clients, and WebSocket.

## 1. Server socket model

```mfl
server := listen(8080)    // create listener on 0.0.0.0:8080
for {
    conn := accept(server)  // block until connection arrives
    go handle(conn)         // handle concurrently in a goroutine
}

func handle(conn) {
    defer close(conn)  // always close the connection
    req := read(conn)  // read HTTP request
    // ... process ...
    write(conn, response)
    close(conn)
}
```

**Key points:**
- `listen(port)` binds to `0.0.0.0` by default
- `accept(server)` returns a connection file descriptor (int)
- `read(conn)` blocks until data arrives, returns the request as string
- `write(conn, data)` sends data
- `close(conn)` closes the connection
- Use `go handle(conn)` to handle requests concurrently

## 2. HTTP request parsing

Parse the request line to extract the method and path:

```mfl
req := read(conn)

// "GET /path HTTP/1.1\r\nHeader: value\r\n\r\n"

path := "/"
m := regex_groups(req, "^GET ([^ ]+) HTTP")
if len(m) > 1 { path = m[1] }   // m[0]=full match, m[1]=capture

// URL decode
path = url_decode(path)
```

**⚠️ `regex_groups` indexing:** index 0 is the full match, index 1+ are capture groups.

## 3. HTTP response building

```mfl
func ok(body, ctype) (s) {
    s = "HTTP/1.1 200 OK\r\nContent-Type: " + ctype +
        "\r\nContent-Length: " + str(len(body)) +
        "\r\nConnection: close\r\n\r\n" + body
}

func not_found() (s) {
    body := "404 Not Found"
    s = "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain" +
        "\r\nContent-Length: " + str(len(body)) +
        "\r\nConnection: close\r\n\r\n" + body
}

func unauthorized() (s) {
    body := "401 Unauthorized"
    s = "HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"machin-serve\"" +
        "\r\nContent-Type: text/plain" +
        "\r\nContent-Length: " + str(len(body)) +
        "\r\nConnection: close\r\n\r\n" + body
}
```

## 4. Handling GET requests

```mfl
func handle(conn) {
    req := read(conn)

    // Parse path
    path := "/"
    m := regex_groups(req, "^GET ([^ ]+) HTTP")
    if len(m) > 1 { path = m[1] }

    // Prevent path traversal
    if contains(path, "..") {
        write(conn, not_found())
        close(conn)
        return
    }

    // URL decode
    path = url_decode(path)

    // Determine content type from file extension
    ctype := mime_type(path)

    // Read and serve
    body := read_file("." + path)
    if len(body) == 0 {
        write(conn, not_found())
    } else {
        write(conn, ok(body, ctype))
    }
    close(conn)
}
```

## 5. Basic auth pattern

```mfl
func handle(conn, auth) {
    req := read(conn)

    if len(auth) > 0 {
        am := regex_groups(req, "Authorization: Basic ([^\r\n]+)")
        authed := false
        if len(am) > 1 {
            decoded := base64_decode(am[1])  // decodes "user:pass"
            if decoded == auth { authed = true }
        }
        if authed == false {
            write(conn, unauthorized())
            close(conn)
            return
        }
    }

    // ... serve file ...
}
```

## 6. MIME type detection

```mfl
func mime_type(path) (t) {
    if has_suffix(path, ".html") || has_suffix(path, ".htm") { return "text/html; charset=utf-8" }
    if has_suffix(path, ".css") { return "text/css; charset=utf-8" }
    if has_suffix(path, ".js") { return "application/javascript" }
    if has_suffix(path, ".json") { return "application/json" }
    if has_suffix(path, ".png") { return "image/png" }
    if has_suffix(path, ".jpg") || has_suffix(path, ".jpeg") { return "image/jpeg" }
    if has_suffix(path, ".gif") { return "image/gif" }
    if has_suffix(path, ".webp") { return "image/webp" }
    if has_suffix(path, ".svg") { return "image/svg+xml" }
    if has_suffix(path, ".ico") { return "image/x-icon" }
    if has_suffix(path, ".txt") { return "text/plain; charset=utf-8" }
    if has_suffix(path, ".pdf") { return "application/pdf" }
    if has_suffix(path, ".zip") { return "application/zip" }
    if has_suffix(path, ".md") { return "text/markdown; charset=utf-8" }
    if has_suffix(path, ".wasm") { return "application/wasm" }
    return "application/octet-stream"
}
```

## 7. Directory index pattern

```mfl
// Check if path is a directory using list_dir
entries := list_dir(fpath)
if len(entries) > 0 {
    // It's a directory — serve index.html
    if charat(fpath, len(fpath)-1) != "/" {
        fpath = fpath + "/"
    }
    fpath = fpath + "index.html"
    body := read_file(fpath)
    if len(body) > 0 {
        write(conn, ok(body, mime_type(fpath)))
    } else {
        write(conn, not_found())
    }
    close(conn)
    return
}

// Otherwise serve the file
body := read_file(fpath)
```

**⚠️ Always check directories with `list_dir` BEFORE `read_file`** — `read_file` on a directory causes SEGFAULT.

## 8. HTTP client

```mfl
// Simple GET
body := https_get("https://example.com")

// GET with error handling
status, body, err := http_get("https://api.github.com/repos/javimosch/machin")
if len(err) > 0 {
    println("unreachable: " + err)
    exit(1)
}
println("status: " + str(status))
println("body length: " + str(len(body)))

// POST JSON
resp := https_post("https://httpbin.org/post", json(some_data))

// Auth'd request (http_request is multi-assign)
headers := []string{"Authorization: Bearer " + token}
status, body, err := http_request("GET", "https://api.example.com/data", headers, "")
```

**⚠️** `http_get` and `http_request` return multiple values — must use multi-assign:

```mfl
// WRONG
body := http_get("https://example.com")

// RIGHT
status, body, err := http_get("https://example.com")
```

## 9. WebSocket client

```mfl
ws := wss_open("wss://echo.websocket.org")
if ws > 0 {
    wss_send(ws, "hello")
    reply := wss_recv(ws)  // blocks
    println("got: " + reply)
    wss_send(ws, "close")
    close(ws)  // or use wss_close?
}
```

## 9b. WebSocket server (framework/ws.src + hub broadcast)

machweb's `ws(req, fn)` upgrades an HTTP request to a WebSocket. `fn` receives a `WSConn` and reads/writes frames via `ws_next_text`/`ws_send_text`. For a dashboard/chat server, use a **hub goroutine** that owns all state via channels — race-free, no mutexes:

```mfl
// Compose: machin encode framework/machweb.src framework/ws.src app.src > app.mfl

type Hub struct {
	register chan DashReg    // dashboard connects
	clients  chan string      // client reports JSON
	getState chan chan string  // synchronous state query (for REST fallback)
}

func hub_loop() {
	dashboards := make(map[int]chan string)
	clients := make(map[string]ClientEntry)
	for {
		select {
			case r := <-hub.register:
				dashboards[r.fd] = r.ch
			case raw := <-hub.clients:
				name, _ := json_get(raw, ".name")
				clients[replace(name, "\"", "")] = ClientEntry{data: raw, ts: now_ms()}
				state := build_state(clients)
				for _, ch := range dashboards { ch <- state }  // broadcast
			case reply := <-hub.getState:
				reply <- build_state(clients)  // REST endpoint gets current state
		}
	}
}

func handle_request(req) {
	if is_ws_request(req) == 1 {
		return ws(req, func(c) {
			myCh := make(chan string)
			hub.register <- DashReg{fd: c.fd, ch: myCh}
			// send current state immediately (not just on next update)
			reply := make(chan string)
			hub.getState <- reply
			initial, _ := <-reply
			ws_send_text(c.fd, initial)
			for { msg, ok := <-myCh; if !ok { return }; ws_send_text(c.fd, msg) }
		})
	}
	if req.path == "/api/report" && req.method == "POST" {
		hub.clients <- req.body  // client POSTs metrics JSON
		return ok_text("ok")
	}
	if req.path == "/api/clients" {
		reply := make(chan string)
		hub.getState <- reply
		state, _ := <-reply
		return ok_json(state)
	}
	return ok_html(dashboard_html())
}

func main() {
	hub = Hub{register: make(chan DashReg), clients: make(chan string), getState: make(chan chan string)}
	go hub_loop()
	serve(9094, func(req) { return handle_request(req) })
}
```

**Key patterns:**
- `go hub_loop()` — `go` takes a named function call, not a closure (see mfl-gotchas #24)
- `hub.getState <- reply; state, _ := <-reply` — synchronous request/reply over a channel (for REST endpoints that need current state without waiting for the next broadcast)
- Send current state to a dashboard **immediately** on connect (via `getState`), not just on the next client update
- Clients POST via `http_request()` (HTTP, not WebSocket) — simpler and works behind firewalls that block WS upgrades
- `ws_next_text` transparently answers pings and stops on close/EOF — use it instead of raw `ws_recv`

**Real app:** [machin-tinystats](https://github.com/javimosch/machin-tinystats) — 71 KB binary, full HTTP+WS dashboard, hub broadcast pattern.

## 10. Full server skeleton

```mfl
func ok(body, ctype) (s) {
    s = "HTTP/1.1 200 OK\r\nContent-Type: " + ctype +
        "\r\nContent-Length: " + str(len(body)) +
        "\r\nConnection: close\r\n\r\n" + body
}

func handle(conn) {
    req := read(conn)
    println("got request")  // will be buffered — call flush()!
    // parse, serve, respond
    write(conn, ok("<h1>hello</h1>", "text/html; charset=utf-8"))
    close(conn)
}

func main() {
    server := listen(8080)
    println("listening on http://0.0.0.0:8080/")
    for {
        conn := accept(server)
        go handle(conn)
    }
}
```

## 11. Reverse proxy pattern (TCP relay)

A reverse proxy accepts HTTP requests from clients, forwards them to a backend server, and relays the response back. This demonstrates raw TCP `dial`/`read`/`write` relay between client ↔ proxy ↔ backend.

### Core flow

```mfl
func handle(conn, backend_host, backend_port) {
    // 1. Read client request
    req := read(conn)

    // 2. Parse request line
    line_end := index(req, "\r\n")
    request_line := substr(req, 0, line_end)
    parts := split(request_line, " ")
    method := parts[0]
    path := parts[1]

    // 3. Rewrite path (strip route prefix)
    new_path := path
    // ... prefix stripping logic ...

    // 4. Rebuild request with rewritten headers
    forward := method + " " + new_path + " HTTP/1.1\r\n"
    // ... parse and forward headers, with Host rewriting ...
    forward = forward + "Host: " + backend_host + "\r\n"
    forward = forward + "X-Forwarded-For: 127.0.0.1\r\n"
    forward = forward + "\r\n"

    // 5. Connect to backend and forward
    backend := dial(backend_host, backend_port)
    write(backend, forward)

    // 6. Read backend response (binary-safe) and relay to client
    resp := read_bytes(backend)
    write(conn, bytes_str(resp))

    close(backend)
    close(conn)
}
```

### Key patterns

| Step | MFL builtins |
|------|-------------|
| Accept connection | `listen`/`accept` |
| Parse HTTP request | `read` + `split`/`index`/`substr` |
| Route matching | `has_prefix(path, prefix)` — longest prefix match |
| Connect to backend | `dial(host, port)` |
| Forward request | `write(fd, request_string)` |
| Read response | `read_bytes(fd)` — binary-safe, no NUL truncation |
| Forward to client | `write(conn, bytes_str(resp))` |
| Concurrency | `go handle(conn, ...)` |

### ⚠️ Binary body limitation

`read_bytes` returns `bytes` (NUL-safe), but `write()` takes a `string`. Converting with `bytes_str()` truncates at the first NUL byte. This means **binary content** (images, WASM, zip files) will be corrupted. Text content (HTML, JSON, JS, CSS, XML) works correctly.

For binary-safe proxying, MFL would need a `write_bytes()` builtin.

### Route matching with longest prefix

```mfl
type Route struct {
    prefix string
    host   string
    port   int
}

func match_route(routes, path) (host, port, prefix) {
    best := -1
    i := 0
    while i < len(routes) {
        r := routes[i]
        if has_prefix(path, r.prefix) && len(r.prefix) > best {
            best = len(r.prefix)
            host = r.host
            port = r.port
            prefix = r.prefix
        }
        i = i + 1
    }
}
```

### Real app reference

| App | Features | Link |
|-----|----------|------|
| machin-revproxy | Full reverse proxy, path routing, header rewriting, 26 KB | [Source](https://github.com/javimosch/machin-revproxy) |

## 12. JSON-RPC / MCP server pattern (stdio transport)

The Model Context Protocol uses JSON-RPC 2.0 over stdio — one JSON message per line on stdin/stdout.

### Core loop

```mfl
func main() {
    running := true
    while running {
        line := input()                 // read one line from stdin
        if len(line) == 0 { break }    // EOF

        method_raw, merr := json_get(line, ".method")
        if len(merr) > 0 { continue }  // invalid JSON
        method := strip_quotes(method_raw)

        id_raw, ierr := json_get(line, ".id")
        is_notification := len(ierr) > 0

        if method == "initialize" && is_notification == false {
            println(handle_initialize(id_raw))
        } else if method == "tools/list" && is_notification == false {
            println(handle_list_tools(id_raw))
        } else if method == "tools/call" && is_notification == false {
            println(handle_call_tool(line, id_raw))
        } else if is_notification == false {
            // MethodNotFound error
            println("{\"jsonrpc\":\"2.0\",\"id\":" + id_raw + ",\"error\":{\"code\":-32601,\"message\":\"Method not found\"}}")
        }
        flush()
    }
}
```

### Key JSON-RPC patterns

| Pattern | Code |
|---------|------|
| Extract method | `method_raw, _ := json_get(line, \".method\")` → `strip_quotes(method_raw)` |
| Detect notification | `id_raw, ierr := json_get(line, \".id\")` → `len(ierr) > 0` |
| Safe string encoding | `escaped := json(text)` — handles quotes, newlines, etc. |
| JSON unescape | `replace(s, \"\\\\\", \"\\\\\")`, then `\"\\n\" → \"\\n\"` etc. |

### ⚠️ `json_get` vs `parse` for escape handling

`json_get` returns RAW JSON tokens **preserving escape sequences** — `\\n` stays as literal backslash-n. `parse()` (with struct types) interprets escapes correctly. Use a `json_unescape` helper when extracting string values with `json_get`:

```mfl
func json_unescape(s) (out) {
    out = replace(s, "\\\\", "\\")
    out = replace(out, "\\\"", "\"")
    out = replace(out, "\\n", "\n")
    out = replace(out, "\\r", "\r")
    out = replace(out, "\\t", "\t")
}
```

### Real app reference

| App | Features | Link |
|-----|----------|------|
| machin-tinystats | HTTP+WS server, hub broadcast, dashboard, 71 KB binary | [Source](https://github.com/javimosch/machin-tinystats) |
| machin-mcp | Full MCP server, JSON-RPC 2.0, 6 tools, stdio transport | [Source](https://github.com/javimosch/machin-mcp) |
| machin-serve | Static file server, Basic auth, MIME types, directory index, traversal protection | [Source](https://github.com/javimosch/machin-serve) |
| machin-fetch | HTTPS CLI client | [Source](https://github.com/javimosch/machin-fetch) |
| machin-http | Multi-command HTTPS client with flags | [Source](https://github.com/javimosch/machin-http) |

