diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index fa8b4e976..ab34b7e65 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -anthropic = "2.9.0" +anthropic = "2.15.0" kotlin = "2.2.21" ktor = "3.2.3" -mcp-kotlin = "0.8.1" +mcp-kotlin = "0.9.0" shadow = "9.2.2" slf4j = "2.0.17" diff --git a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt index 8360316e2..23fdfcc0d 100644 --- a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt +++ b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt @@ -2,10 +2,11 @@ package io.modelcontextprotocol.sample.client import com.anthropic.client.okhttp.AnthropicOkHttpClient import com.anthropic.core.JsonValue +import com.anthropic.models.messages.ContentBlockParam import com.anthropic.models.messages.MessageCreateParams import com.anthropic.models.messages.MessageParam -import com.anthropic.models.messages.Model import com.anthropic.models.messages.Tool +import com.anthropic.models.messages.ToolResultBlockParam import com.anthropic.models.messages.ToolUnion import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper @@ -14,24 +15,28 @@ import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.yield +import kotlinx.coroutines.withContext import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered import kotlinx.serialization.json.JsonObject -import kotlin.jvm.optionals.getOrNull +import java.util.concurrent.TimeUnit -class MCPClient : AutoCloseable { - // Configures using the `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` environment variables - private val anthropic = AnthropicOkHttpClient.fromEnv() +private const val MODEL = "claude-sonnet-4-20250514" +private const val MAX_TOKENS = 1024L + +class MCPClient(apiKey: String) : AutoCloseable { + private val anthropic = AnthropicOkHttpClient.builder() + .apiKey(apiKey) + .build() // Initialize MCP client private val mcp: Client = Client(clientInfo = Implementation(name = "mcp-client-cli", version = "1.0.0")) - private val messageParamsBuilder: MessageCreateParams.Builder = MessageCreateParams.builder() - .model(Model.CLAUDE_4_SONNET_20250514) - .maxTokens(1024) + // Server process reference for cleanup + private var serverProcess: Process? = null // List of tools offered by the server private lateinit var tools: List @@ -44,52 +49,52 @@ class MCPClient : AutoCloseable { // Connect to the server using the path to the server suspend fun connectToServer(serverScriptPath: String) { - try { - // Build the command based on the file extension of the server script - val command = buildList { - when (serverScriptPath.substringAfterLast(".")) { - "js" -> add("node") - "py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3") - "jar" -> addAll(listOf("java", "-jar")) - else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file") - } - add(serverScriptPath) + // Build the command based on the file extension of the server script + val command = buildList { + when (serverScriptPath.substringAfterLast(".")) { + "js" -> add("node") + "py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3") + "jar" -> addAll(listOf("java", "-jar")) + else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file") } + add(serverScriptPath) + } - // Start the server process - val process = ProcessBuilder(command).start() + // Start the server process + val process = withContext(Dispatchers.IO) { + ProcessBuilder(command) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + } + serverProcess = process - // Setup I/O transport using the process streams - val transport = StdioClientTransport( - input = process.inputStream.asSource().buffered(), - output = process.outputStream.asSink().buffered(), - ) + // Setup I/O transport using the process streams + val transport = StdioClientTransport( + input = process.inputStream.asSource().buffered(), + output = process.outputStream.asSink().buffered(), + ) - // Connect the MCP client to the server using the transport - mcp.connect(transport) - - // Request the list of available tools from the server - val toolsResult = mcp.listTools() - tools = toolsResult.tools.map { tool -> - ToolUnion.ofTool( - Tool.builder() - .name(tool.name) - .description(tool.description ?: "") - .inputSchema( - Tool.InputSchema.builder() - .type(JsonValue.from(tool.inputSchema.type)) - .properties(tool.inputSchema.properties?.toJsonValue() ?: EmptyJsonObject.toJsonValue()) - .putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required)) - .build(), - ) - .build(), - ) - } - println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}") - } catch (e: Exception) { - println("Failed to connect to MCP server: $e") - throw e + // Connect the MCP client to the server using the transport + mcp.connect(transport) + + // Request the list of available tools from the server + val toolsResult = mcp.listTools() + tools = toolsResult.tools.map { tool -> + ToolUnion.ofTool( + Tool.builder() + .name(tool.name) + .description(tool.description ?: "") + .inputSchema( + Tool.InputSchema.builder() + .type(JsonValue.from(tool.inputSchema.type)) + .properties(tool.inputSchema.properties?.toJsonValue() ?: EmptyJsonObject.toJsonValue()) + .putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required)) + .build(), + ) + .build(), + ) } + println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}") } // Process a user query and return a string response @@ -104,23 +109,29 @@ class MCPClient : AutoCloseable { // Send the query to the Anthropic model and get the response val response = anthropic.messages().create( - messageParamsBuilder + MessageCreateParams.builder() + .model(MODEL) + .maxTokens(MAX_TOKENS) .messages(messages) .tools(tools) .build(), ) val finalText = mutableListOf() + val toolResults = mutableListOf() + response.content().forEach { content -> when { // Append text outputs from the response - content.isText() -> finalText.add(content.text().getOrNull()?.text() ?: "") + content.isText() -> finalText.add(content.text().get().text()) // If the response indicates a tool use, process it further content.isToolUse() -> { - val toolName = content.toolUse().get().name() + val toolUse = content.toolUse().get() + val toolName = toolUse.name() + val toolUseId = toolUse.id() val toolArgs = - content.toolUse().get()._input().convert(object : TypeReference>() {}) + toolUse._input().convert(object : TypeReference>() {}) // Call the tool with provided arguments val result = mcp.callTool( @@ -129,37 +140,55 @@ class MCPClient : AutoCloseable { ) finalText.add("[Calling tool $toolName with args $toolArgs]") - // Add the tool result message to the conversation - messages.add( - MessageParam.builder() - .role(MessageParam.Role.USER) - .content( - """ - "type": "tool_result", - "tool_name": $toolName, - "result": ${ - result.content.joinToString("\n") { - (it as TextContent).text - } - } - """.trimIndent(), - ) - .build(), - ) + // Build tool_result content block with tool_use_id + val toolResultContent = result.content + .filterIsInstance() + .joinToString("\n") { it.text } - // Retrieve an updated response after tool execution - val aiResponse = anthropic.messages().create( - messageParamsBuilder - .messages(messages) - .build(), - ) + val toolResultBlock = ToolResultBlockParam.builder() + .toolUseId(toolUseId) + .content(toolResultContent) + .apply { if (result.isError == true) isError(true) } + .build() - // Append the updated response to final text - finalText.add(aiResponse.content().first().text().getOrNull()?.text() ?: "") + toolResults.add(ContentBlockParam.ofToolResult(toolResultBlock)) } } } + // If there were tool calls, send tool results back and get final response + if (toolResults.isNotEmpty()) { + // Add the full assistant response (includes tool_use blocks) + messages.add( + MessageParam.builder() + .role(MessageParam.Role.ASSISTANT) + .contentOfBlockParams(response.content().map { it.toParam() }) + .build(), + ) + + // Add user message with tool results + messages.add( + MessageParam.builder() + .role(MessageParam.Role.USER) + .contentOfBlockParams(toolResults) + .build(), + ) + + // Retrieve an updated response after tool execution (without tools) + val aiResponse = anthropic.messages().create( + MessageCreateParams.builder() + .model(MODEL) + .maxTokens(MAX_TOKENS) + .messages(messages) + .build(), + ) + + // Append the updated response to final text + aiResponse.content() + .filter { it.isText() } + .forEach { finalText.add(it.text().get().text()) } + } + return finalText.joinToString("\n", prefix = "", postfix = "") } @@ -171,17 +200,27 @@ class MCPClient : AutoCloseable { while (true) { print("\nQuery: ") val message = readlnOrNull() ?: break - if (message.lowercase() == "quit") break - val response = processQuery(message) - println("\n$response") - yield() + if (message.trim().lowercase() == "quit") break + + try { + val response = processQuery(message) + println("\n$response") + } catch (e: Exception) { + println("\nError: ${e.message}") + } } } override fun close() { runBlocking { mcp.close() - anthropic.close() } + serverProcess?.let { process -> + process.destroy() + if (!process.waitFor(5, TimeUnit.SECONDS)) { + process.destroyForcibly() + } + } + anthropic.close() } } diff --git a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt index 19a3102f5..001a25575 100644 --- a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt +++ b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt @@ -3,13 +3,14 @@ package io.modelcontextprotocol.sample.client import kotlinx.coroutines.runBlocking fun main(args: Array) = runBlocking { - require(args.isNotEmpty()) { - "Usage: java -jar /build/libs/kotlin-mcp-client-0.1.0-all.jar " - } - val serverPath = args.first() - val client = MCPClient() + require(args.isNotEmpty()) { "Usage: java -jar " } + + val apiKey = System.getenv("ANTHROPIC_API_KEY") + require(!apiKey.isNullOrBlank()) { "ANTHROPIC_API_KEY environment variable is not set" } + + val client = MCPClient(apiKey) client.use { - client.connectToServer(serverPath) + client.connectToServer(args.first()) client.chatLoop() } } diff --git a/samples/weather-stdio-server/README.md b/samples/weather-stdio-server/README.md index 4c13040c8..f377d5567 100644 --- a/samples/weather-stdio-server/README.md +++ b/samples/weather-stdio-server/README.md @@ -57,8 +57,8 @@ Add the following to your Claude Desktop configuration: | Name | Description | |----------------|------------------------------------------------------------------------------------------| -| `get_forecast` | Returns weather forecast for a given `latitude` / `longitude` using the weather.gov API. | | `get_alerts` | Returns active weather alerts for a two-letter US `state` code (e.g. `CA`, `NY`). | +| `get_forecast` | Returns weather forecast for a given `latitude` / `longitude` using the weather.gov API. | ## Additional Resources diff --git a/samples/weather-stdio-server/build.gradle.kts b/samples/weather-stdio-server/build.gradle.kts index 0420cc0d8..d6f5fc200 100644 --- a/samples/weather-stdio-server/build.gradle.kts +++ b/samples/weather-stdio-server/build.gradle.kts @@ -14,14 +14,12 @@ version = "0.1.0" dependencies { implementation(dependencies.platform(libs.ktor.bom)) + implementation(libs.mcp.kotlin.server) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) - implementation(libs.mcp.kotlin.server) - implementation(libs.ktor.server.cio) implementation(libs.ktor.client.cio) implementation(libs.slf4j.simple) runtimeOnly(libs.kotlin.logging) - runtimeOnly(libs.kotlinx.collections.immutable) testImplementation(kotlin("test")) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index 6056b9442..dab7f7d29 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -1,22 +1,19 @@ [versions] -collections-immutable = "0.4.0" coroutines = "1.10.2" kotlin = "2.2.21" ktor = "3.2.3" logging = "7.0.13" -mcp-kotlin = "0.8.1" +mcp-kotlin = "0.9.0" shadow = "9.2.2" slf4j = "2.0.17" [libraries] kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "logging" } -kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable-jvm", version.ref = "collections-immutable" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation" } ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json-jvm" } -ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" } mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } diff --git a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt index f08c436d1..1dee706f7 100644 --- a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt +++ b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.sample.server import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest import io.ktor.http.ContentType @@ -15,6 +16,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import kotlinx.coroutines.Job import kotlinx.coroutines.runBlocking @@ -32,47 +34,59 @@ import kotlinx.serialization.json.putJsonObject * weather alerts by state and weather forecasts by latitude/longitude. */ fun runMcpServer() { - // Base URL for the Weather API - val baseUrl = "https://api.weather.gov" + createHttpClient().use { httpClient -> + val server = Server( + Implementation( + name = "weather", + version = "1.0.0", + ), + ServerOptions( + capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)), + ), + ) + + server.registerTools(httpClient) - // 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") + val transport = StdioServerTransport( + System.`in`.asInput(), + System.out.asSink().buffered(), + ) + + runBlocking { + val session = server.createSession(transport) + val done = Job() + session.onClose { + done.complete() } - contentType(ContentType.Application.Json) - } - // Install content negotiation plugin for JSON serialization/deserialization - install(ContentNegotiation) { - json( - Json { - ignoreUnknownKeys = true - prettyPrint = true - }, - ) + done.join() } } +} - // 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)), - ), - ) +private fun createHttpClient(): HttpClient = HttpClient(CIO) { + defaultRequest { + url("https://api.weather.gov") + headers { + append("Accept", "application/geo+json") + append("User-Agent", "WeatherApiClient/1.0") + } + contentType(ContentType.Application.Json) + } + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + prettyPrint = true + }, + ) + } +} +private fun Server.registerTools(httpClient: HttpClient) { // Register a tool to fetch weather alerts by state - server.addTool( + addTool( name = "get_alerts", - description = """ - Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY) - """.trimIndent(), + description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)", inputSchema = ToolSchema( properties = buildJsonObject { putJsonObject("state") { @@ -82,33 +96,62 @@ fun runMcpServer() { }, required = listOf("state"), ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = true), ) { request -> - val state = request.arguments?.get("state")?.jsonPrimitive?.content ?: return@addTool CallToolResult( - content = listOf(TextContent("The 'state' parameter is required.")), - ) + val state = request.arguments?.get("state")?.jsonPrimitive?.content + ?: return@addTool CallToolResult( + content = listOf(TextContent("The 'state' parameter is required.")), + ) - val alerts = httpClient.getAlerts(state) + if (state.length != 2) { + return@addTool CallToolResult( + content = listOf(TextContent("Invalid state code: '$state'. Must be a two-letter US state code.")), + isError = true, + ) + } - CallToolResult(content = alerts.map { TextContent(it) }) + val stateCode = state.uppercase() + + httpClient.getAlerts(stateCode).fold( + onSuccess = { alerts -> + if (alerts.isEmpty()) { + CallToolResult(content = listOf(TextContent("No active alerts for $stateCode"))) + } else { + val alertsText = "Active alerts for $stateCode:\n\n${alerts.joinToString("\n---\n")}" + CallToolResult(content = listOf(TextContent(alertsText))) + } + }, + onFailure = { e -> + CallToolResult( + content = listOf(TextContent("Failed to retrieve alerts data: ${e.message}")), + isError = true, + ) + }, + ) } // Register a tool to fetch weather forecast by latitude and longitude - server.addTool( + addTool( name = "get_forecast", - description = """ - Get weather forecast for a specific latitude/longitude - """.trimIndent(), + description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.", inputSchema = ToolSchema( properties = buildJsonObject { putJsonObject("latitude") { put("type", "number") + put("description", "Latitude of the location") + put("minimum", -90) + put("maximum", 90) } putJsonObject("longitude") { put("type", "number") + put("description", "Longitude of the location") + put("minimum", -180) + put("maximum", 180) } }, required = listOf("latitude", "longitude"), ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = true), ) { request -> val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull @@ -118,23 +161,26 @@ fun runMcpServer() { ) } - 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 { - val session = server.createSession(transport) - val done = Job() - session.onClose { - done.complete() - } - done.join() + httpClient.getForecast(latitude, longitude).fold( + onSuccess = { periods -> + if (periods.isEmpty()) { + CallToolResult(content = listOf(TextContent("No forecast periods available"))) + } else { + val forecastText = periods.joinToString("\n---\n") + CallToolResult(content = listOf(TextContent(forecastText))) + } + }, + onFailure = { _ -> + CallToolResult( + content = listOf( + TextContent( + "Failed to retrieve grid point data for coordinates: $latitude, $longitude. " + + "This location may not be supported by the NWS API (only US locations are supported).", + ), + ), + isError = true, + ) + }, + ) } } diff --git a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt index 671186c3e..000ac12ff 100644 --- a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt +++ b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt @@ -4,88 +4,90 @@ 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 +import java.util.Locale -// Extension function to fetch forecast information for given latitude and longitude -suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List { - // 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() - - // Request the forecast using the URL provided in the points response - val forecast = this.get(points.properties.forecast).body() +// Extension function to fetch weather alerts for a given state +suspend fun HttpClient.getAlerts(state: String): Result> = runCatching { + val uri = "/alerts/active/area/$state" + val alerts = this.get(uri).body() - // Map each forecast period to a formatted string - return forecast.properties.periods.map { period -> + alerts.features.map { feature -> """ - ${period.name}: - Temperature: ${period.temperature} ${period.temperatureUnit} - Wind: ${period.windSpeed} ${period.windDirection} - Forecast: ${period.detailedForecast} + Event: ${feature.properties.event} + Area: ${feature.properties.areaDesc} + Severity: ${feature.properties.severity} + Status: ${feature.properties.status} + Headline: ${feature.properties.headline} """.trimIndent() } } -// Extension function to fetch weather alerts for a given state -suspend fun HttpClient.getAlerts(state: String): List { - // 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() +// Extension function to fetch forecast information for given latitude and longitude +suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): Result> = runCatching { + val lat = String.format(Locale.US, "%.4f", latitude) + val lon = String.format(Locale.US, "%.4f", longitude) + val uri = "/points/$lat,$lon" + val points = this.get(uri).body() - // Map each alert feature to a formatted string - return alerts.features.map { feature -> + val forecastUrl = points.properties.forecast + ?: error("No forecast URL available for coordinates: $latitude, $longitude") + val forecast = this.get(forecastUrl).body() + + forecast.properties.periods.map { period -> """ - Event: ${feature.properties.event} - Area: ${feature.properties.areaDesc} - Severity: ${feature.properties.severity} - Description: ${feature.properties.description} - Instruction: ${feature.properties.instruction} + ${period.name}: + Temperature: ${period.temperature}°${period.temperatureUnit} + Wind: ${period.windSpeed} ${period.windDirection} + ${period.shortForecast} """.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 PointsResponse( + val properties: PointsProperties, +) -// Data class representing the forecast response from the API @Serializable -data class Forecast( - val properties: Properties -) { - @Serializable - data class Properties(val periods: List) +data class PointsProperties( + val forecast: String? = null, +) - @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, - ) -} +@Serializable +data class ForecastResponse( + val properties: ForecastProperties, +) -// Data class representing the alerts response from the API @Serializable -data class Alert( - val features: List -) { - @Serializable - data class Feature( - val properties: Properties - ) +data class ForecastProperties( + val periods: List = emptyList(), +) - @Serializable - data class Properties( - val event: String, val areaDesc: String, val severity: String, - val description: String, val instruction: String?, - ) -} +@Serializable +data class ForecastPeriod( + val name: String? = null, + val temperature: Int? = null, + val temperatureUnit: String? = null, + val windSpeed: String? = null, + val windDirection: String? = null, + val shortForecast: String? = null, +) + +@Serializable +data class AlertsResponse( + val features: List = emptyList(), +) + +@Serializable +data class AlertFeature( + val properties: AlertProperties, +) + +@Serializable +data class AlertProperties( + val event: String? = null, + val areaDesc: String? = null, + val severity: String? = null, + val status: String? = null, + val headline: String? = null, +) diff --git a/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt b/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt index 5934ee262..32fd550e7 100644 --- a/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt +++ b/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt @@ -13,8 +13,8 @@ fun main(): Unit = runBlocking { val process = ProcessBuilder( "java", "-jar", - "${System.getProperty("user.dir")}/build/libs/weather-stdio-server-0.1.0-all.jar", - ).redirectErrorStream(true) + "${System.getProperty("user.dir")}/samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar", + ).redirectError(ProcessBuilder.Redirect.INHERIT) .start() val transport = StdioClientTransport( @@ -44,7 +44,7 @@ fun main(): Unit = runBlocking { val alertResult = client.callTool( - name = "get_alert", + name = "get_alerts", arguments = mapOf("state" to "TX"), ).content.map { if (it is TextContent) it.text else it.toString() }