DEV Community

Cover image for A Domain MCP Server in Kotlin: Exposing a Scoring Engine to AI Agents
The AX code
The AX code

Posted on

A Domain MCP Server in Kotlin: Exposing a Scoring Engine to AI Agents

Previously, I gave an AI agent hands — a Model Context Protocol server in Kotlin/Native that drives real Bluetooth hardware. This one is the other half of the pattern: a domain MCP server. Instead of touching devices, it lets an agent reason over a model — turning weather forecasts into an activity "playability score" — and, crucially, it runs on the exact same Kotlin Multiplatform core that powers the app. Write the domain once; expose it to both humans and agents.

The tools

The WeatherConditions MCP server (JVM, built on the Kotlin MCP SDK + Ktor) exposes five tools:

Tool What it does
score_location Score one location's playability for an activity profile, with a factor-by-factor breakdown
rank_locations Score and rank several locations at once
list_profiles List the available activity profiles (golf, dog-walking, …)
get_profile Fetch one profile's definition
upsert_profile Create or update a profile

So an agent can answer "where's the best place to walk the dog this afternoon?" by calling rank_locations with a dog_walking profile — and get back not just a number, but why.

Write the domain once, expose it twice

Here's the part I care about most. The scoring logic doesn't live in the server. It lives in WeatherConditions-CoreLib — a framework-free Kotlin Multiplatform module with zero platform dependencies:

core-lib/
├── domain/model/     PlayabilityProfile, WeatherPeriod, ActivityScore, ScoreFactor…
├── domain/usecase/   PlayabilityCalculator, ActivityScorer, ScorerRegistry, DogWalkingScorer
└── ports/outbound/   ForecastService, LocationService, ReverseGeocoding, Clock…  (interfaces only)
Enter fullscreen mode Exit fullscreen mode

The same core-lib artifact is consumed by the Android/iOS/watchOS/WearOS apps and by this JVM MCP server. The app provides a CameraX/CoreLocation adapter for the LocationService port; the server provides a Ktor + Google Weather API adapter. The scoring — the thing with actual business value — is written exactly once.

// In the MCP server — the domain does the work, the server just adapts I/O.
server.addTool(
    name = "score_location",
    description = "Score a location's playability for an activity profile",
    inputSchema = Tool.Input(
        properties = buildJsonObject {
            putJsonObject("location") { put("type", "string") }
            putJsonObject("profile")  { put("type", "string") }
        },
        required = listOf("location")
    )
) { request ->
    val ctx   = service.buildScoringContext(request.location())     // Ktor → Google Weather
    val score = PlayabilityCalculator().calculateScore(ctx, profile) // ← CoreLib, shared
    text(score.render())                                            // value + factor breakdown
}
Enter fullscreen mode Exit fullscreen mode

PlayabilityCalculator and DogWalkingScorer are the same classes the mobile app calls. The MCP server is a thin shell around a portable core.

Explainable by construction

A score the agent can't justify is worse than no score. The domain returns an ActivityScore made of ScoreFactors — wind, precipitation, temperature, time-of-day — each with its own contribution. So score_location doesn't just say "62/100"; it says "62 — strong wind (−18), light rain expected after 4pm (−12), comfortable temperature (+8)." That structure is what makes the tool genuinely useful to an LLM: it can explain, compare, and reason about the result instead of parroting a number.

Profiles: the configurable surface

Notice upsert_profile. A profile is the tunable definition of what "good weather" means for an activity — wind tolerance, ideal temperature band, rain sensitivity. They're first-class, stored, and syncable (the core even has a pairing-code-derived PSK sync protocol so profiles travel between devices).

That matters beyond this server: profiles are configuration. An agent can edit them via upsert_profile, but a human will want a real settings screen — sliders and toggles — on whatever device they're holding. Which is exactly where the next post in this series goes: using Multiplat to render cross-platform settings UIs for MCP servers like this one, straight from their tool schemas.

The pattern, generalized

Two MCP servers, two flavors:

  • Bluetooth MCP — a side-effect server: scan, connect, write, sync. Kotlin/Native, talks to hardware.
  • WeatherConditions MCP — a domain server: score, rank, configure. JVM, wraps a shared KMP core.

Both prove the same thesis: Kotlin Multiplatform lets you expose native and domain capabilities to agents while reusing the code that already runs your apps. The agent ecosystem is busy wrapping web APIs; there's a lot of room below that, in the native and cross-platform layer.

Top comments (0)