From df80a0e2c7b6adfd1e673225837f3a79735c56eb Mon Sep 17 00:00:00 2001 From: devcrocod Date: Wed, 12 Mar 2025 13:33:26 +0100 Subject: [PATCH 1/2] Add Kotlin server implementation guide for weather server to quickstart --- quickstart/server.mdx | 372 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) diff --git a/quickstart/server.mdx b/quickstart/server.mdx index b391fea..aa93eaa 100644 --- a/quickstart/server.mdx +++ b/quickstart/server.mdx @@ -1032,6 +1032,378 @@ For more information, see the [MCP Client Boot Starters](https://docs.spring.io/ The [starter-webflux-server](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server) demonstrates how to create a MCP server using SSE transport. It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities. + + + +Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/weather-stdio-server) + +### Prerequisite knowledge + +This quickstart assumes you have familiarity with: +- Kotlin +- LLMs like Claude + +### System requirements + +- Java 17 or higher installed. + +### Set up your environment + +First, let's install `java` and `gradle` if you haven't already. +You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/). +Verify your `java` installation: +```bash +java --version +``` + +Now, let's create and set up your project: + + +```bash MacOS/Linux +# Create a new directory for our project +mkdir weather +cd weather + +# Initialize a new kotlin project +gradle init +``` + +```powershell Windows +# Create a new directory for our project +md weather +cd weather + +# Initialize a new kotlin project +gradle init +``` + + +After running `gradle init`, you will be presented with options for creating your project. +Select **Application** as the project type, **Kotlin** as the programming language, and **Java 17** as the Java version. + +Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html). + +After creating the project, add the following dependencies: + +```kotlin build.gradle.kts +val mcpVersion = "0.3.0" +val slf4jVersion = "2.0.9" +val ktorVersion = "3.1.1" + +dependencies { + implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion") + implementation("org.slf4j:slf4j-nop:$slf4jVersion") + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") +} +``` + +```groovy build.gradle +def mcpVersion = '0.3.0' +def slf4jVersion = '2.0.9' +def ktorVersion = '3.1.1' + +dependencies { + implementation "io.modelcontextprotocol:kotlin-sdk:$mcpVersion" + implementation "org.slf4j:slf4j-nop:$slf4jVersion" + implementation "io.ktor:ktor-client-content-negotiation:$ktorVersion" + implementation "io.ktor:ktor-serialization-kotlinx-json:$ktorVersion" +} +``` + + +Also, add the following plugins to your build script: + +```kotlin build.gradle.kts +plugins { + kotlin("plugin.serialization") version "your_version_of_kotlin" + id("com.github.johnrengelman.shadow") version "8.1.1" +} +``` + +```groovy build.gradle +plugins { + id 'org.jetbrains.kotlin.plugin.serialization' version 'your_version_of_kotlin' + id 'com.github.johnrengelman.shadow' version '8.1.1' +} +``` + + +Now let’s dive into building your server. + +## Building your server + +### Setting up the instance + +Add a server initialization function: + +```kotlin +// Main function to run the MCP server +fun `run mcp server`() { + val def = CompletableDeferred() + + // 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) } + + // 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() + } +} +``` + +### Weather API helper functions + +Next, let's add functions and data classes for querying and converting responses from the National Weather Service API: + +```kotlin +// Extension function to fetch forecast information for given latitude and longitude +suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List { + val points = this.get("/points/$latitude,$longitude").body() + val forecast = this.get(points.properties.forecast).body() + 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 { + val alerts = this.get("/alerts/active/area/$state").body() + 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() + } +} + +@Serializable +data class Points( + val properties: Properties +) { + @Serializable + data class Properties(val forecast: String) +} + +@Serializable +data class Forecast( + val properties: Properties +) { + @Serializable + data class Properties(val periods: List) + + @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 Alert( + val features: List +) { + @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?, + ) +} +``` + +### Implementing tool execution + +The tool execution handler is responsible for actually executing the logic of each tool. Let's add it: + +```kotlin +// Create an HTTP client with a default request configuration and JSON content negotiation +val httpClient = HttpClient { + defaultRequest { + url("https://api.weather.gov") + 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 }) } +} + +// 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) }) +} +``` + +### Running the server + +Finally, implement the main function to run the server: + +```kotlin +fun main() = `run mcp server`() +``` + +Make sure to run `./gradlew build` to build your server. This is a very important step in getting your server to connect. + +Let's now test your server from an existing MCP host, Claude for Desktop. + +## Testing your server with Claude for Desktop + + +Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](/quickstart/client) tutorial to build an MCP client that connects to the server we just built. + + +First, make sure you have Claude for Desktop installed. [You can install the latest version +here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.** + +We'll need to configure Claude for Desktop for whichever MCP servers you want to use. +To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. +Make sure to create the file if it doesn't exist. + +For example, if you have [VS Code](https://code.visualstudio.com/) installed: + + +```bash MacOS/Linux +code ~/Library/Application\ Support/Claude/claude_desktop_config.json +``` + +```powershell Windows +code $env:AppData\Claude\claude_desktop_config.json +``` + + +You'll then add your servers in the `mcpServers` key. +The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured. + +In this case, we'll add our single weather server like so: + + +```json MacOS/Linux +{ + "mcpServers": { + "weather": { + "command": "java", + "args": [ + "-jar", + "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar" + ] + } + } +} +``` + +```json Windows +{ + "mcpServers": { + "weather": { + "command": "java", + "args": [ + "-jar", + "C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar" + ] + } + } +} +``` + + +This tells Claude for Desktop: +1. There's an MCP server named "weather" +2. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar` + +Save the file, and restart **Claude for Desktop**. + From a5dc69f6996ca4025324bedac5e55bcedafad73f Mon Sep 17 00:00:00 2001 From: devcrocod Date: Wed, 19 Mar 2025 00:14:12 +0100 Subject: [PATCH 2/2] remove CompletableDeferred --- quickstart/server.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/quickstart/server.mdx b/quickstart/server.mdx index aa93eaa..1a31fe7 100644 --- a/quickstart/server.mdx +++ b/quickstart/server.mdx @@ -1140,8 +1140,6 @@ Add a server initialization function: ```kotlin // Main function to run the MCP server fun `run mcp server`() { - val def = CompletableDeferred() - // Create the MCP Server instance with a basic implementation val server = Server( Implementation( @@ -1151,7 +1149,7 @@ fun `run mcp server`() { ServerOptions( capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)) ) - ) { def.complete(Unit) } + ) // Create a transport using standard IO for server communication val transport = StdioServerTransport(