DEV Community

Cover image for Multiplatform Settings for MCP Servers: It's Schemas All the Way Down
The AX code
The AX code

Posted on

Multiplatform Settings for MCP Servers: It's Schemas All the Way Down

This series built up a stack: C interop at the bottom, Multiplat (schema-driven Compose Multiplatform forms) for UI, and two Model Context Protocol servers — one that drives Bluetooth hardware, one that scores weather. This final post connects them, around one observation:

An MCP tool already publishes a JSON schema so an LLM can call it. That same schema can render a settings screen for a human.

You don't need to hand-build a configuration UI for every MCP server. The server already told you its shape. The job is to turn that shape into native controls on whatever device the user is holding — and Compose Multiplatform makes "whatever device" mean Android, iOS, and desktop from one codebase.

The two schemas are the same schema

Here's the Bluetooth MCP's ble_update_gatt_config and the WeatherConditions MCP's upsert_profile. Each tool ships an inputSchema — plain JSON Schema:

// what the LLM sees in tools/list — and what the human settings UI can render
{
  "name": "upsert_profile",
  "inputSchema": {
    "type": "object",
    "properties": {
      "name":          { "type": "string" },
      "maxWindMph":    { "type": "number", "minimum": 0, "maximum": 60 },
      "idealTempF":    { "type": "number" },
      "allowRain":     { "type": "boolean" }
    },
    "required": ["name"]
  }
}
Enter fullscreen mode Exit fullscreen mode

A person configuring a "dog walking" profile wants a text field, two sliders, and a switch. An LLM calling upsert_profile wants the same field names and types. It's one description, two consumers.

Multiplat: from JSON Schema to native form

Multiplat is a Compose Multiplatform library that renders forms from a typed FormSchema. Its DSL looks like this:

form {
    section("Dog Walking") {
        text("name")      { label = "Profile name"; required("Required") }
        number("maxWindMph") { label = "Max wind (mph)"; range = 0..60 }
        number("idealTempF") { label = "Ideal temp (°F)" }
        switch("allowRain")  { label = "OK in light rain" }
    }
    submitButton("Save")
}
Enter fullscreen mode Exit fullscreen mode

Because that's a schema, the bridge from an MCP tool's inputSchema is mechanical — walk the JSON Schema, emit a field per property by type:

fun mcpToolToForm(tool: McpTool): FormSchema = form {
    section(tool.name) {
        for ((key, prop) in tool.inputSchema.properties) when (prop.type) {
            "string"  -> text(key)   { label = prop.title ?: key; if (key in tool.required) required("Required") }
            "number"  -> number(key) { label = prop.title ?: key; prop.range?.let { range = it } }
            "boolean" -> switch(key) { label = prop.title ?: key }
            "string?enum" -> dropdown(key, prop.enum)
        }
    }
    submitButton("Save")
}
Enter fullscreen mode Exit fullscreen mode

RenderForm(form, context) then draws it natively on every platform. On submit, the form's value map is the tool-call arguments — so saving settings and calling the tool are the same payload:

client.callTool("upsert_profile", context.values.value)
Enter fullscreen mode Exit fullscreen mode

One screen configures any MCP server you point it at. Add a tool, and its settings panel appears for free.

Why Multiplat's "extra" features matter here

A settings UI for a fleet of MCP servers has two annoying realities, and Multiplat was built for exactly them:

1. Settings must survive offline and restarts. Multiplat persists form objects to a local SQLite store (SQLDelight) as a JSON envelope, reconciled against the current schema on read. Add, remove, or reorder a field and stored settings still load — no migration code. Your saved Bluetooth GATT config and weather profiles are just there, next launch, offline.

2. Servers evolve; settings shouldn't break. When a server renames maxWindMph to windCeilingMph, a naive UI drops the old value. Multiplat handles the ambiguous changes with an on-device LLM migration: the model proposes a declarative, sandbox-validated transform (rename/merge/backfill) that trusted code applies — no app update, no cloud round-trip, and the model can never run arbitrary code. So an MCP server can change its schema and existing users' settings migrate themselves.

The whole picture

   ┌──────────────┐         inputSchema (JSON Schema)        ┌────────────────────┐
   │  MCP servers │  ───────────────────────────────────▶   │   Multiplat (KMP)  │
   │  • Bluetooth │                                          │  schema → native   │
   │  • Weather   │  ◀───────────  tool call  ───────────    │  form (Android/iOS │
   └──────────────┘         (form values = args)             │  /desktop)         │
        Kotlin/Native + JVM                                   │  + local persist   │
                                                              │  + LLM migration   │
                                                              └────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Every box is Kotlin. The Bluetooth server is Kotlin/Native against CoreBluetooth; the weather server is JVM on a shared KMP domain core; the settings UI is Compose Multiplatform. One language, from C bindings at the bottom to a cross-platform configuration surface at the top — which is the through-line of this whole series.

Why this is a direction worth taking

The agent world is standardizing on MCP for what an agent can do. Far less settled is how a human supervises and configures those capabilities — especially on phones and across platforms. Treating the tool schema as the single source of truth for both the agent interface and the human UI collapses that into one artifact you already maintain. And doing it in Kotlin Multiplatform means the servers, the domain logic, and the settings app are the same stack.

That's the foundation this series was building toward: small, composable KMP pieces — C interop, libraries, MCP servers, and a schema-driven UI — that add up to agent-ready, cross-platform software.

Thanks for following the series. If you're building at the agent-plus-devices or KMP-plus-MCP intersection, I'd love to compare notes.

Top comments (0)