// hosts-tools (novox/hq ADR 0199): the hosts file's tools. One binary, launched by the machine's tool // runtime and speaking MCP to it over stdio through the Go SDK (ADR 0193, ADR 0198): the // node-hosts-file seat's three verbs — the file's lines with whose each is, add an operator's line, // remove one. They change the machine's file and nothing else; the controller holds none of it. // // stdout is the MCP channel; everything this module says, it says on stderr. package main import ( "context" "fmt" "os" "regexp" "strings" stdio "git.novox.be/novox/mesh-sdk/go" ) // Seat is the role this module holds. const Seat = "node-hosts-file" func main() { if err := stdio.Serve("", tools(HostsFile{Path: HostsPath, Run: execRunner})); err != nil { fmt.Fprintf(os.Stderr, "[hosts] %v\n", err) os.Exit(1) } } func str(description string) map[string]any { return map[string]any{"type": "string", "description": description} } func arg(a map[string]any, k string) string { v, _ := a[k].(string) return strings.TrimSpace(v) } var separators = regexp.MustCompile(`[\s,]+`) // namesArg is the names given, separated by spaces or commas — or, from a caller that sends a list, // the list. func namesArg(a map[string]any) []string { var raw []string switch v := a["names"].(type) { case string: raw = separators.Split(v, -1) case []any: for _, n := range v { if s, ok := n.(string); ok { raw = append(raw, separators.Split(s, -1)...) } } } names := []string{} for _, n := range raw { if n != "" { names = append(names, n) } } return names } // verb is one of the seat's verbs: listed as `.`, so the runtime serves it on the seat's // subject, as /node-hosts-file.. func verb(name, description string, input map[string]any, run func(a map[string]any) (any, error)) stdio.Tool { return stdio.Tool{Name: Seat + "." + name, Description: description, Input: input, Run: run} } func tools(h HostsFile) []stdio.Tool { ctx := context.Background() return []stdio.Tool{ verb("entries", "Every line of this machine's /etc/hosts, each marked whose it is: the operator's, or the block of the module or tool that writes it.", nil, func(map[string]any) (any, error) { return h.Entries() }), verb("add", "Add one address and its names to the operator's lines of this machine's /etc/hosts — a name for this machine's own programs, not the mesh's. Nothing changes when they are already there.", map[string]any{"address": str("the IPv4 or IPv6 address"), "names": str("the names for it, separated by spaces")}, func(a map[string]any) (any, error) { return h.Add(ctx, arg(a, "address"), namesArg(a)) }), verb("remove", "Remove one name, or every line of one address, from the operator's lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.", map[string]any{"name": str("a host name, or an address to remove every line of")}, func(a map[string]any) (any, error) { return h.Remove(ctx, arg(a, "name")) }), } }