Просмотр исходного кода

Reapply "feat(core): optional mdns service (#6192)"

This reverts commit 505068d5a6cccc732aed76580f9bec6a5dbca507.
Adam 1 месяц назад
Родитель
Сommit
b2f45d574f

+ 11 - 0
bun.lock

@@ -292,6 +292,7 @@
         "@standard-schema/spec": "1.0.0",
         "@zip.js/zip.js": "2.7.62",
         "ai": "catalog:",
+        "bonjour-service": "1.3.0",
         "bun-pty": "0.4.2",
         "chokidar": "4.0.3",
         "clipboardy": "4.0.0",
@@ -1081,6 +1082,8 @@
 
     "@kurkle/color": ["@kurkle/[email protected]", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
 
+    "@leichtgewicht/ip-codec": ["@leichtgewicht/[email protected]", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="],
+
     "@mdx-js/mdx": ["@mdx-js/[email protected]", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
 
     "@mixmark-io/domino": ["@mixmark-io/[email protected]", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
@@ -2003,6 +2006,8 @@
 
     "body-parser": ["[email protected]", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
 
+    "bonjour-service": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="],
+
     "boolbase": ["[email protected]", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
 
     "bottleneck": ["[email protected]", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
@@ -2247,6 +2252,8 @@
 
     "dlv": ["[email protected]", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
 
+    "dns-packet": ["[email protected]", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="],
+
     "dom-serializer": ["[email protected]", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
 
     "domelementtype": ["[email protected]", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
@@ -3023,6 +3030,8 @@
 
     "ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
 
+    "multicast-dns": ["[email protected]", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="],
+
     "mustache": ["[email protected]", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
 
     "mysql2": ["[email protected]", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="],
@@ -3595,6 +3604,8 @@
 
     "three": ["[email protected]", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
 
+    "thunky": ["[email protected]", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="],
+
     "tiny-inflate": ["[email protected]", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
 
     "tiny-invariant": ["[email protected]", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],

+ 1 - 0
packages/opencode/package.json

@@ -88,6 +88,7 @@
     "@standard-schema/spec": "1.0.0",
     "@zip.js/zip.js": "2.7.62",
     "ai": "catalog:",
+    "bonjour-service": "1.3.0",
     "bun-pty": "0.4.2",
     "chokidar": "4.0.3",
     "clipboardy": "4.0.0",

+ 10 - 20
packages/opencode/src/cli/cmd/acp.ts

@@ -3,8 +3,10 @@ import { bootstrap } from "../bootstrap"
 import { cmd } from "./cmd"
 import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
 import { ACP } from "@/acp/agent"
+import { Config } from "@/config/config"
 import { Server } from "@/server/server"
 import { createOpencodeClient } from "@opencode-ai/sdk/v2"
+import { withNetworkOptions, resolveNetworkOptions } from "../network"
 
 const log = Log.create({ service: "acp-command" })
 
@@ -19,29 +21,17 @@ export const AcpCommand = cmd({
   command: "acp",
   describe: "start ACP (Agent Client Protocol) server",
   builder: (yargs) => {
-    return yargs
-      .option("cwd", {
-        describe: "working directory",
-        type: "string",
-        default: process.cwd(),
-      })
-      .option("port", {
-        type: "number",
-        describe: "port to listen on",
-        default: 0,
-      })
-      .option("hostname", {
-        type: "string",
-        describe: "hostname to listen on",
-        default: "127.0.0.1",
-      })
+    return withNetworkOptions(yargs).option("cwd", {
+      describe: "working directory",
+      type: "string",
+      default: process.cwd(),
+    })
   },
   handler: async (args) => {
     await bootstrap(process.cwd(), async () => {
-      const server = Server.listen({
-        port: args.port,
-        hostname: args.hostname,
-      })
+      const config = await Config.get()
+      const opts = resolveNetworkOptions(args, config)
+      const server = Server.listen(opts)
 
       const sdk = createOpencodeClient({
         baseUrl: `http://${server.hostname}:${server.port}`,

+ 6 - 19
packages/opencode/src/cli/cmd/serve.ts

@@ -1,29 +1,16 @@
+import { Config } from "../../config/config"
 import { Server } from "../../server/server"
 import { cmd } from "./cmd"
+import { withNetworkOptions, resolveNetworkOptions } from "../network"
 
 export const ServeCommand = cmd({
   command: "serve",
-  builder: (yargs) =>
-    yargs
-      .option("port", {
-        alias: ["p"],
-        type: "number",
-        describe: "port to listen on",
-        default: 0,
-      })
-      .option("hostname", {
-        type: "string",
-        describe: "hostname to listen on",
-        default: "127.0.0.1",
-      }),
+  builder: (yargs) => withNetworkOptions(yargs),
   describe: "starts a headless opencode server",
   handler: async (args) => {
-    const hostname = args.hostname
-    const port = args.port
-    const server = Server.listen({
-      port,
-      hostname,
-    })
+    const config = await Config.get()
+    const opts = resolveNetworkOptions(args, config)
+    const server = Server.listen(opts)
     console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
     await new Promise(() => {})
     await server.stop()

+ 9 - 19
packages/opencode/src/cli/cmd/tui/spawn.ts

@@ -1,33 +1,23 @@
 import { cmd } from "@/cli/cmd/cmd"
+import { Config } from "@/config/config"
 import { Instance } from "@/project/instance"
 import path from "path"
 import { Server } from "@/server/server"
 import { upgrade } from "@/cli/upgrade"
+import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
 
 export const TuiSpawnCommand = cmd({
   command: "spawn [project]",
   builder: (yargs) =>
-    yargs
-      .positional("project", {
-        type: "string",
-        describe: "path to start opencode in",
-      })
-      .option("port", {
-        type: "number",
-        describe: "port to listen on",
-        default: 0,
-      })
-      .option("hostname", {
-        type: "string",
-        describe: "hostname to listen on",
-        default: "127.0.0.1",
-      }),
+    withNetworkOptions(yargs).positional("project", {
+      type: "string",
+      describe: "path to start opencode in",
+    }),
   handler: async (args) => {
     upgrade()
-    const server = Server.listen({
-      port: args.port,
-      hostname: "127.0.0.1",
-    })
+    const config = await Config.get()
+    const opts = resolveNetworkOptions(args, config)
+    const server = Server.listen(opts)
     const bin = process.execPath
     const cmd = []
     let cwd = process.cwd()

+ 6 - 16
packages/opencode/src/cli/cmd/tui/thread.ts

@@ -6,6 +6,8 @@ import path from "path"
 import { UI } from "@/cli/ui"
 import { iife } from "@/util/iife"
 import { Log } from "@/util/log"
+import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
+import { Config } from "@/config/config"
 
 declare global {
   const OPENCODE_WORKER_PATH: string
@@ -15,7 +17,7 @@ export const TuiThreadCommand = cmd({
   command: "$0 [project]",
   describe: "start opencode tui",
   builder: (yargs) =>
-    yargs
+    withNetworkOptions(yargs)
       .positional("project", {
         type: "string",
         describe: "path to start opencode in",
@@ -36,23 +38,12 @@ export const TuiThreadCommand = cmd({
         describe: "session id to continue",
       })
       .option("prompt", {
-        alias: ["p"],
         type: "string",
         describe: "prompt to use",
       })
       .option("agent", {
         type: "string",
         describe: "agent to use",
-      })
-      .option("port", {
-        type: "number",
-        describe: "port to listen on",
-        default: 0,
-      })
-      .option("hostname", {
-        type: "string",
-        describe: "hostname to listen on",
-        default: "127.0.0.1",
       }),
   handler: async (args) => {
     // Resolve relative paths against PWD to preserve behavior when using --cwd flag
@@ -87,10 +78,9 @@ export const TuiThreadCommand = cmd({
     process.on("unhandledRejection", (e) => {
       Log.Default.error(e)
     })
-    const server = await client.call("server", {
-      port: args.port,
-      hostname: args.hostname,
-    })
+    const config = await Config.get()
+    const networkOpts = resolveNetworkOptions(args, config)
+    const server = await client.call("server", networkOpts)
     const prompt = await iife(async () => {
       const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
       if (!args.prompt) return piped

+ 1 - 1
packages/opencode/src/cli/cmd/tui/worker.ts

@@ -30,7 +30,7 @@ process.on("uncaughtException", (e) => {
 
 let server: Bun.Server<BunWebSocketData>
 export const rpc = {
-  async server(input: { port: number; hostname: string }) {
+  async server(input: { port: number; hostname: string; mdns?: boolean }) {
     if (server) await server.stop(true)
     try {
       server = Server.listen(input)

+ 11 - 20
packages/opencode/src/cli/cmd/web.ts

@@ -1,6 +1,8 @@
+import { Config } from "../../config/config"
 import { Server } from "../../server/server"
 import { UI } from "../ui"
 import { cmd } from "./cmd"
+import { withNetworkOptions, resolveNetworkOptions } from "../network"
 import open from "open"
 import { networkInterfaces } from "os"
 
@@ -28,32 +30,17 @@ function getNetworkIPs() {
 
 export const WebCommand = cmd({
   command: "web",
-  builder: (yargs) =>
-    yargs
-      .option("port", {
-        alias: ["p"],
-        type: "number",
-        describe: "port to listen on",
-        default: 0,
-      })
-      .option("hostname", {
-        type: "string",
-        describe: "hostname to listen on",
-        default: "127.0.0.1",
-      }),
+  builder: (yargs) => withNetworkOptions(yargs),
   describe: "starts a headless opencode server",
   handler: async (args) => {
-    const hostname = args.hostname
-    const port = args.port
-    const server = Server.listen({
-      port,
-      hostname,
-    })
+    const config = await Config.get()
+    const opts = resolveNetworkOptions(args, config)
+    const server = Server.listen(opts)
     UI.empty()
     UI.println(UI.logo("  "))
     UI.empty()
 
-    if (hostname === "0.0.0.0") {
+    if (opts.hostname === "0.0.0.0") {
       // Show localhost for local access
       const localhostUrl = `http://localhost:${server.port}`
       UI.println(UI.Style.TEXT_INFO_BOLD + "  Local access:      ", UI.Style.TEXT_NORMAL, localhostUrl)
@@ -70,6 +57,10 @@ export const WebCommand = cmd({
         }
       }
 
+      if (opts.mdns) {
+        UI.println(UI.Style.TEXT_INFO_BOLD + "  mDNS:              ", UI.Style.TEXT_NORMAL, "opencode.local")
+      }
+
       // Open localhost in browser
       open(localhostUrl.toString()).catch(() => {})
     } else {

+ 42 - 0
packages/opencode/src/cli/network.ts

@@ -0,0 +1,42 @@
+import type { Argv, InferredOptionTypes } from "yargs"
+import type { Config } from "../config/config"
+
+const options = {
+  port: {
+    type: "number" as const,
+    describe: "port to listen on",
+    default: 0,
+  },
+  hostname: {
+    type: "string" as const,
+    describe: "hostname to listen on",
+    default: "127.0.0.1",
+  },
+  mdns: {
+    type: "boolean" as const,
+    describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)",
+    default: false,
+  },
+}
+
+export type NetworkOptions = InferredOptionTypes<typeof options>
+
+export function withNetworkOptions<T>(yargs: Argv<T>) {
+  return yargs.options(options)
+}
+
+export function resolveNetworkOptions(args: NetworkOptions, config?: Config.Info) {
+  const portExplicitlySet = process.argv.includes("--port")
+  const hostnameExplicitlySet = process.argv.includes("--hostname")
+  const mdnsExplicitlySet = process.argv.includes("--mdns")
+
+  const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
+  const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
+  const hostname = hostnameExplicitlySet
+    ? args.hostname
+    : mdns && !config?.server?.hostname
+      ? "0.0.0.0"
+      : (config?.server?.hostname ?? args.hostname)
+
+  return { hostname, port, mdns }
+}

+ 12 - 0
packages/opencode/src/config/config.ts

@@ -587,6 +587,17 @@ export namespace Config {
       .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
   })
 
+  export const Server = z
+    .object({
+      port: z.number().int().positive().optional().describe("Port to listen on"),
+      hostname: z.string().optional().describe("Hostname to listen on"),
+      mdns: z.boolean().optional().describe("Enable mDNS service discovery"),
+    })
+    .strict()
+    .meta({
+      ref: "ServerConfig",
+    })
+
   export const Layout = z.enum(["auto", "stretch"]).meta({
     ref: "LayoutConfig",
   })
@@ -635,6 +646,7 @@ export namespace Config {
       keybinds: Keybinds.optional().describe("Custom keybind configurations"),
       logLevel: Log.Level.optional().describe("Log level"),
       tui: TUI.optional().describe("TUI specific settings"),
+      server: Server.optional().describe("Server configuration for opencode serve and web commands"),
       command: z
         .record(z.string(), Command)
         .optional()

+ 57 - 0
packages/opencode/src/server/mdns.ts

@@ -0,0 +1,57 @@
+import { Log } from "@/util/log"
+import Bonjour from "bonjour-service"
+
+const log = Log.create({ service: "mdns" })
+
+export namespace MDNS {
+  let bonjour: Bonjour | undefined
+  let currentPort: number | undefined
+
+  export function publish(port: number, name = "opencode") {
+    if (currentPort === port) return
+    if (bonjour) unpublish()
+
+    try {
+      bonjour = new Bonjour()
+      const service = bonjour.publish({
+        name,
+        type: "http",
+        port,
+        txt: { path: "/" },
+      })
+
+      service.on("up", () => {
+        log.info("mDNS service published", { name, port })
+      })
+
+      service.on("error", (err) => {
+        log.error("mDNS service error", { error: err })
+      })
+
+      currentPort = port
+    } catch (err) {
+      log.error("mDNS publish failed", { error: err })
+      if (bonjour) {
+        try {
+          bonjour.destroy()
+        } catch {}
+      }
+      bonjour = undefined
+      currentPort = undefined
+    }
+  }
+
+  export function unpublish() {
+    if (bonjour) {
+      try {
+        bonjour.unpublishAll()
+        bonjour.destroy()
+      } catch (err) {
+        log.error("mDNS unpublish failed", { error: err })
+      }
+      bonjour = undefined
+      currentPort = undefined
+      log.info("mDNS service unpublished")
+    }
+  }
+}

+ 28 - 5
packages/opencode/src/server/server.ts

@@ -45,9 +45,11 @@ import { Snapshot } from "@/snapshot"
 import { SessionSummary } from "@/session/summary"
 import { SessionStatus } from "@/session/status"
 import { upgradeWebSocket, websocket } from "hono/bun"
+import type { BunWebSocketData } from "hono/bun"
 import { errors } from "./error"
 import { Pty } from "@/pty"
 import { Installation } from "@/installation"
+import { MDNS } from "./mdns"
 
 // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
 globalThis.AI_SDK_LOG_WARNINGS = false
@@ -2623,20 +2625,41 @@ export namespace Server {
     return result
   }
 
-  export function listen(opts: { port: number; hostname: string }) {
+  export function listen(opts: { port: number; hostname: string; mdns?: boolean }) {
     const args = {
       hostname: opts.hostname,
       idleTimeout: 0,
       fetch: App().fetch,
       websocket: websocket,
     } as const
-    if (opts.port === 0) {
+    const tryServe = (port: number) => {
       try {
-        return Bun.serve({ ...args, port: 4096 })
+        return Bun.serve({ ...args, port })
       } catch {
-        // port 4096 not available, fall through to use port 0
+        return undefined
       }
     }
-    return Bun.serve({ ...args, port: opts.port })
+    const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port)
+    if (!server) throw new Error(`Failed to start server on port ${opts.port}`)
+
+    const shouldPublishMDNS =
+      opts.mdns &&
+      server.port &&
+      opts.hostname !== "127.0.0.1" &&
+      opts.hostname !== "localhost" &&
+      opts.hostname !== "::1"
+    if (shouldPublishMDNS) {
+      MDNS.publish(server.port!)
+    } else if (opts.mdns) {
+      log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
+    }
+
+    const originalStop = server.stop.bind(server)
+    server.stop = async (closeActiveConnections?: boolean) => {
+      if (shouldPublishMDNS) MDNS.unpublish()
+      return originalStop(closeActiveConnections)
+    }
+
+    return server
   }
 }

+ 10 - 8
packages/web/src/content/docs/cli.mdx

@@ -335,10 +335,11 @@ This starts an HTTP server that provides API access to opencode functionality wi
 
 #### Flags
 
-| Flag         | Short | Description           |
-| ------------ | ----- | --------------------- |
-| `--port`     | `-p`  | Port to listen on     |
-| `--hostname` |       | Hostname to listen on |
+| Flag         | Description           |
+| ------------ | --------------------- |
+| `--port`     | Port to listen on     |
+| `--hostname` | Hostname to listen on |
+| `--mdns`     | Enable mDNS discovery |
 
 ---
 
@@ -428,10 +429,11 @@ This starts an HTTP server and opens a web browser to access OpenCode through a
 
 #### Flags
 
-| Flag         | Short | Description           |
-| ------------ | ----- | --------------------- |
-| `--port`     | `-p`  | Port to listen on     |
-| `--hostname` |       | Hostname to listen on |
+| Flag         | Description           |
+| ------------ | --------------------- |
+| `--port`     | Port to listen on     |
+| `--hostname` | Hostname to listen on |
+| `--mdns`     | Enable mDNS discovery |
 
 ---
 

+ 25 - 0
packages/web/src/content/docs/config.mdx

@@ -120,6 +120,31 @@ Available options:
 
 ---
 
+### Server
+
+You can configure server settings for the `opencode serve` and `opencode web` commands through the `server` option.
+
+```json title="opencode.json"
+{
+  "$schema": "https://opencode.ai/config.json",
+  "server": {
+    "port": 4096,
+    "hostname": "0.0.0.0",
+    "mdns": true
+  }
+}
+```
+
+Available options:
+
+- `port` - Port to listen on.
+- `hostname` - Hostname to listen on. When `mdns` is enabled and no hostname is set, defaults to `0.0.0.0`.
+- `mdns` - Enable mDNS service discovery. This allows other devices on the network to discover your OpenCode server.
+
+[Learn more about the server here](/docs/server).
+
+---
+
 ### Tools
 
 You can manage the tools an LLM can use through the `tools` option.

+ 5 - 4
packages/web/src/content/docs/server.mdx

@@ -18,10 +18,11 @@ opencode serve [--port <number>] [--hostname <string>]
 
 #### Options
 
-| Flag         | Short | Description           | Default     |
-| ------------ | ----- | --------------------- | ----------- |
-| `--port`     | `-p`  | Port to listen on     | `4096`      |
-| `--hostname` | `-h`  | Hostname to listen on | `127.0.0.1` |
+| Flag         | Description           | Default     |
+| ------------ | --------------------- | ----------- |
+| `--port`     | Port to listen on     | `4096`      |
+| `--hostname` | Hostname to listen on | `127.0.0.1` |
+| `--mdns`     | Enable mDNS discovery | `false`     |
 
 ---