Add Kotlin Weather STDIO server sample (#50)
* Add Kotlin Weather STDIO server sample * Update gradle build command in Readme to exclude tests
This commit is contained in:
parent
c57c56e18f
commit
0a6490c7f3
4 changed files with 863 additions and 0 deletions
|
|
@ -0,0 +1,132 @@
|
|||
package io.modelcontextprotocol.sample.server
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.utils.io.streams.*
|
||||
import io.modelcontextprotocol.kotlin.sdk.*
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.Server
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.io.asSink
|
||||
import kotlinx.io.buffered
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
// Main function to run the MCP server
|
||||
fun `run mcp server`() {
|
||||
val def = CompletableDeferred<Unit>()
|
||||
|
||||
// Base URL for the Weather API
|
||||
val baseUrl = "https://api.weather.gov"
|
||||
|
||||
// Create an HTTP client with a default request configuration and JSON content negotiation
|
||||
val httpClient = HttpClient {
|
||||
defaultRequest {
|
||||
url(baseUrl)
|
||||
headers {
|
||||
append("Accept", "application/geo+json")
|
||||
append("User-Agent", "WeatherApiClient/1.0")
|
||||
}
|
||||
contentType(ContentType.Application.Json)
|
||||
}
|
||||
// Install content negotiation plugin for JSON serialization/deserialization
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
prettyPrint = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create the MCP Server instance with a basic implementation
|
||||
val server = Server(
|
||||
Implementation(
|
||||
name = "weather", // Tool name is "weather"
|
||||
version = "1.0.0" // Version of the implementation
|
||||
),
|
||||
ServerOptions(
|
||||
capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
|
||||
)
|
||||
) { def.complete(Unit) }
|
||||
|
||||
// Register a tool to fetch weather alerts by state
|
||||
server.addTool(
|
||||
name = "get_alerts",
|
||||
description = """
|
||||
Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)
|
||||
""".trimIndent(),
|
||||
inputSchema = Tool.Input(
|
||||
properties = JsonObject(
|
||||
mapOf(
|
||||
"state" to JsonObject(
|
||||
mapOf(
|
||||
"type" to JsonPrimitive("string"),
|
||||
"description" to JsonPrimitive("Two-letter US state code (e.g. CA, NY)")
|
||||
)
|
||||
),
|
||||
)
|
||||
),
|
||||
required = listOf("state")
|
||||
)
|
||||
) { request ->
|
||||
val state = request.arguments["state"]?.jsonPrimitive?.content
|
||||
if (state == null) {
|
||||
return@addTool CallToolResult(
|
||||
content = listOf(TextContent("The 'state' parameter is required."))
|
||||
)
|
||||
}
|
||||
|
||||
val alerts = httpClient.getAlerts(state)
|
||||
|
||||
CallToolResult(content = alerts.map { TextContent(it) })
|
||||
}
|
||||
|
||||
// Register a tool to fetch weather forecast by latitude and longitude
|
||||
server.addTool(
|
||||
name = "get_forecast",
|
||||
description = """
|
||||
Get weather forecast for a specific latitude/longitude
|
||||
""".trimIndent(),
|
||||
inputSchema = Tool.Input(
|
||||
properties = JsonObject(
|
||||
mapOf(
|
||||
"latitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))),
|
||||
"longitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))),
|
||||
)
|
||||
),
|
||||
required = listOf("latitude", "longitude")
|
||||
)
|
||||
) { request ->
|
||||
val latitude = request.arguments["latitude"]?.jsonPrimitive?.doubleOrNull
|
||||
val longitude = request.arguments["longitude"]?.jsonPrimitive?.doubleOrNull
|
||||
if (latitude == null || longitude == null) {
|
||||
return@addTool CallToolResult(
|
||||
content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required."))
|
||||
)
|
||||
}
|
||||
|
||||
val forecast = httpClient.getForecast(latitude, longitude)
|
||||
|
||||
CallToolResult(content = forecast.map { TextContent(it) })
|
||||
}
|
||||
|
||||
// Create a transport using standard IO for server communication
|
||||
val transport = StdioServerTransport(
|
||||
System.`in`.asInput(),
|
||||
System.out.asSink().buffered()
|
||||
)
|
||||
|
||||
runBlocking {
|
||||
server.connect(transport)
|
||||
val done = Job()
|
||||
server.onCloseCallback = {
|
||||
done.complete()
|
||||
}
|
||||
done.join()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package io.modelcontextprotocol.sample.server
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
// Extension function to fetch forecast information for given latitude and longitude
|
||||
suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
|
||||
// Build the URI using provided latitude and longitude
|
||||
val uri = "/points/$latitude,$longitude"
|
||||
// Request the points data from the API
|
||||
val points = this.get(uri).body<Points>()
|
||||
|
||||
// Request the forecast using the URL provided in the points response
|
||||
val forecast = this.get(points.properties.forecast).body<Forecast>()
|
||||
|
||||
// Map each forecast period to a formatted string
|
||||
return forecast.properties.periods.map { period ->
|
||||
"""
|
||||
${period.name}:
|
||||
Temperature: ${period.temperature} ${period.temperatureUnit}
|
||||
Wind: ${period.windSpeed} ${period.windDirection}
|
||||
Forecast: ${period.detailedForecast}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
// Extension function to fetch weather alerts for a given state
|
||||
suspend fun HttpClient.getAlerts(state: String): List<String> {
|
||||
// Build the URI using the given state code
|
||||
val uri = "/alerts/active/area/$state"
|
||||
// Request the alerts data from the API
|
||||
val alerts = this.get(uri).body<Alert>()
|
||||
|
||||
// Map each alert feature to a formatted string
|
||||
return alerts.features.map { feature ->
|
||||
"""
|
||||
Event: ${feature.properties.event}
|
||||
Area: ${feature.properties.areaDesc}
|
||||
Severity: ${feature.properties.severity}
|
||||
Description: ${feature.properties.description}
|
||||
Instruction: ${feature.properties.instruction}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
// Data class representing the points response from the API
|
||||
@Serializable
|
||||
data class Points(
|
||||
val properties: Properties
|
||||
) {
|
||||
@Serializable
|
||||
data class Properties(val forecast: String)
|
||||
}
|
||||
|
||||
// Data class representing the forecast response from the API
|
||||
@Serializable
|
||||
data class Forecast(
|
||||
val properties: Properties
|
||||
) {
|
||||
@Serializable
|
||||
data class Properties(val periods: List<Period>)
|
||||
|
||||
@Serializable
|
||||
data class Period(
|
||||
val number: Int, val name: String, val startTime: String, val endTime: String,
|
||||
val isDaytime: Boolean, val temperature: Int, val temperatureUnit: String,
|
||||
val temperatureTrend: String, val probabilityOfPrecipitation: JsonObject,
|
||||
val windSpeed: String, val windDirection: String,
|
||||
val shortForecast: String, val detailedForecast: String,
|
||||
)
|
||||
}
|
||||
|
||||
// Data class representing the alerts response from the API
|
||||
@Serializable
|
||||
data class Alert(
|
||||
val features: List<Feature>
|
||||
) {
|
||||
@Serializable
|
||||
data class Feature(
|
||||
val properties: Properties
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Properties(
|
||||
val event: String, val areaDesc: String, val severity: String,
|
||||
val description: String, val instruction: String?,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package io.modelcontextprotocol.sample.server
|
||||
|
||||
fun main() = `run mcp server`()
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package io.modelcontextprotocol.sample.client
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.CallToolRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.Client
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.io.asSink
|
||||
import kotlinx.io.asSource
|
||||
import kotlinx.io.buffered
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
|
||||
fun main(): Unit = runBlocking {
|
||||
val process = ProcessBuilder("java", "-jar", "build/libs/weather-stdio-server-0.1.0-all.jar")
|
||||
.start()
|
||||
|
||||
val transport = StdioClientTransport(
|
||||
input = process.inputStream.asSource().buffered(),
|
||||
output = process.outputStream.asSink().buffered()
|
||||
)
|
||||
|
||||
// Initialize the MCP client with client information
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "weather", version = "1.0.0"),
|
||||
)
|
||||
|
||||
client.connect(transport)
|
||||
|
||||
|
||||
val toolsList = client.listTools()?.tools?.map { it.name }
|
||||
println("Available Tools = $toolsList")
|
||||
|
||||
val weatherForecastResult = client.callTool(
|
||||
CallToolRequest(
|
||||
name = "get_forecast",
|
||||
arguments = JsonObject(mapOf("latitude" to JsonPrimitive(38.5816), "longitude" to JsonPrimitive(-121.4944)))
|
||||
)
|
||||
)?.content?.map { if (it is TextContent) it.text else it.toString() }
|
||||
|
||||
println("Weather Forcast: ${weatherForecastResult?.joinToString(separator = "\n", prefix = "\n", postfix = "\n")}")
|
||||
|
||||
val alertResult =
|
||||
client.callTool(
|
||||
CallToolRequest(
|
||||
name = "get_alerts",
|
||||
arguments = JsonObject(mapOf("state" to JsonPrimitive("TX")))
|
||||
)
|
||||
)?.content?.map { if (it is TextContent) it.text else it.toString() }
|
||||
|
||||
println("Alert Response = $alertResult")
|
||||
|
||||
client.close()
|
||||
}
|
||||
Loading…
Reference in a new issue