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)
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
}
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)