Skip to content

Commit df80a0e

Browse files
committed
Add Kotlin server implementation guide for weather server to quickstart
1 parent b40820b commit df80a0e

File tree

1 file changed

+372
-0
lines changed

1 file changed

+372
-0
lines changed

quickstart/server.mdx

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,378 @@ For more information, see the [MCP Client Boot Starters](https://docs.spring.io/
10321032
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.
10331033
It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities.
10341034

1035+
</Tab>
1036+
1037+
<Tab title='Kotlin'>
1038+
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)
1039+
1040+
### Prerequisite knowledge
1041+
1042+
This quickstart assumes you have familiarity with:
1043+
- Kotlin
1044+
- LLMs like Claude
1045+
1046+
### System requirements
1047+
1048+
- Java 17 or higher installed.
1049+
1050+
### Set up your environment
1051+
1052+
First, let's install `java` and `gradle` if you haven't already.
1053+
You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1054+
Verify your `java` installation:
1055+
```bash
1056+
java --version
1057+
```
1058+
1059+
Now, let's create and set up your project:
1060+
1061+
<CodeGroup>
1062+
```bash MacOS/Linux
1063+
# Create a new directory for our project
1064+
mkdir weather
1065+
cd weather
1066+
1067+
# Initialize a new kotlin project
1068+
gradle init
1069+
```
1070+
1071+
```powershell Windows
1072+
# Create a new directory for our project
1073+
md weather
1074+
cd weather
1075+
1076+
# Initialize a new kotlin project
1077+
gradle init
1078+
```
1079+
</CodeGroup>
1080+
1081+
After running `gradle init`, you will be presented with options for creating your project.
1082+
Select **Application** as the project type, **Kotlin** as the programming language, and **Java 17** as the Java version.
1083+
1084+
Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1085+
1086+
After creating the project, add the following dependencies:
1087+
<CodeGroup>
1088+
```kotlin build.gradle.kts
1089+
val mcpVersion = "0.3.0"
1090+
val slf4jVersion = "2.0.9"
1091+
val ktorVersion = "3.1.1"
1092+
1093+
dependencies {
1094+
implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1095+
implementation("org.slf4j:slf4j-nop:$slf4jVersion")
1096+
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
1097+
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
1098+
}
1099+
```
1100+
1101+
```groovy build.gradle
1102+
def mcpVersion = '0.3.0'
1103+
def slf4jVersion = '2.0.9'
1104+
def ktorVersion = '3.1.1'
1105+
1106+
dependencies {
1107+
implementation "io.modelcontextprotocol:kotlin-sdk:$mcpVersion"
1108+
implementation "org.slf4j:slf4j-nop:$slf4jVersion"
1109+
implementation "io.ktor:ktor-client-content-negotiation:$ktorVersion"
1110+
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktorVersion"
1111+
}
1112+
```
1113+
</CodeGroup>
1114+
1115+
Also, add the following plugins to your build script:
1116+
<CodeGroup>
1117+
```kotlin build.gradle.kts
1118+
plugins {
1119+
kotlin("plugin.serialization") version "your_version_of_kotlin"
1120+
id("com.github.johnrengelman.shadow") version "8.1.1"
1121+
}
1122+
```
1123+
1124+
```groovy build.gradle
1125+
plugins {
1126+
id 'org.jetbrains.kotlin.plugin.serialization' version 'your_version_of_kotlin'
1127+
id 'com.github.johnrengelman.shadow' version '8.1.1'
1128+
}
1129+
```
1130+
</CodeGroup>
1131+
1132+
Now let’s dive into building your server.
1133+
1134+
## Building your server
1135+
1136+
### Setting up the instance
1137+
1138+
Add a server initialization function:
1139+
1140+
```kotlin
1141+
// Main function to run the MCP server
1142+
fun `run mcp server`() {
1143+
val def = CompletableDeferred<Unit>()
1144+
1145+
// Create the MCP Server instance with a basic implementation
1146+
val server = Server(
1147+
Implementation(
1148+
name = "weather", // Tool name is "weather"
1149+
version = "1.0.0" // Version of the implementation
1150+
),
1151+
ServerOptions(
1152+
capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
1153+
)
1154+
) { def.complete(Unit) }
1155+
1156+
// Create a transport using standard IO for server communication
1157+
val transport = StdioServerTransport(
1158+
System.`in`.asInput(),
1159+
System.out.asSink().buffered()
1160+
)
1161+
1162+
runBlocking {
1163+
server.connect(transport)
1164+
val done = Job()
1165+
server.onCloseCallback = {
1166+
done.complete()
1167+
}
1168+
done.join()
1169+
}
1170+
}
1171+
```
1172+
1173+
### Weather API helper functions
1174+
1175+
Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1176+
1177+
```kotlin
1178+
// Extension function to fetch forecast information for given latitude and longitude
1179+
suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
1180+
val points = this.get("/points/$latitude,$longitude").body<Points>()
1181+
val forecast = this.get(points.properties.forecast).body<Forecast>()
1182+
return forecast.properties.periods.map { period ->
1183+
"""
1184+
${period.name}:
1185+
Temperature: ${period.temperature} ${period.temperatureUnit}
1186+
Wind: ${period.windSpeed} ${period.windDirection}
1187+
Forecast: ${period.detailedForecast}
1188+
""".trimIndent()
1189+
}
1190+
}
1191+
1192+
// Extension function to fetch weather alerts for a given state
1193+
suspend fun HttpClient.getAlerts(state: String): List<String> {
1194+
val alerts = this.get("/alerts/active/area/$state").body<Alert>()
1195+
return alerts.features.map { feature ->
1196+
"""
1197+
Event: ${feature.properties.event}
1198+
Area: ${feature.properties.areaDesc}
1199+
Severity: ${feature.properties.severity}
1200+
Description: ${feature.properties.description}
1201+
Instruction: ${feature.properties.instruction}
1202+
""".trimIndent()
1203+
}
1204+
}
1205+
1206+
@Serializable
1207+
data class Points(
1208+
val properties: Properties
1209+
) {
1210+
@Serializable
1211+
data class Properties(val forecast: String)
1212+
}
1213+
1214+
@Serializable
1215+
data class Forecast(
1216+
val properties: Properties
1217+
) {
1218+
@Serializable
1219+
data class Properties(val periods: List<Period>)
1220+
1221+
@Serializable
1222+
data class Period(
1223+
val number: Int, val name: String, val startTime: String, val endTime: String,
1224+
val isDaytime: Boolean, val temperature: Int, val temperatureUnit: String,
1225+
val temperatureTrend: String, val probabilityOfPrecipitation: JsonObject,
1226+
val windSpeed: String, val windDirection: String,
1227+
val shortForecast: String, val detailedForecast: String,
1228+
)
1229+
}
1230+
1231+
@Serializable
1232+
data class Alert(
1233+
val features: List<Feature>
1234+
) {
1235+
@Serializable
1236+
data class Feature(
1237+
val properties: Properties
1238+
)
1239+
1240+
@Serializable
1241+
data class Properties(
1242+
val event: String, val areaDesc: String, val severity: String,
1243+
val description: String, val instruction: String?,
1244+
)
1245+
}
1246+
```
1247+
1248+
### Implementing tool execution
1249+
1250+
The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
1251+
1252+
```kotlin
1253+
// Create an HTTP client with a default request configuration and JSON content negotiation
1254+
val httpClient = HttpClient {
1255+
defaultRequest {
1256+
url("https://api.weather.gov")
1257+
headers {
1258+
append("Accept", "application/geo+json")
1259+
append("User-Agent", "WeatherApiClient/1.0")
1260+
}
1261+
contentType(ContentType.Application.Json)
1262+
}
1263+
// Install content negotiation plugin for JSON serialization/deserialization
1264+
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
1265+
}
1266+
1267+
// Register a tool to fetch weather alerts by state
1268+
server.addTool(
1269+
name = "get_alerts",
1270+
description = """
1271+
Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)
1272+
""".trimIndent(),
1273+
inputSchema = Tool.Input(
1274+
properties = JsonObject(
1275+
mapOf(
1276+
"state" to JsonObject(
1277+
mapOf(
1278+
"type" to JsonPrimitive("string"),
1279+
"description" to JsonPrimitive("Two-letter US state code (e.g. CA, NY)")
1280+
)
1281+
),
1282+
)
1283+
),
1284+
required = listOf("state")
1285+
)
1286+
) { request ->
1287+
val state = request.arguments["state"]?.jsonPrimitive?.content
1288+
if (state == null) {
1289+
return@addTool CallToolResult(
1290+
content = listOf(TextContent("The 'state' parameter is required."))
1291+
)
1292+
}
1293+
1294+
val alerts = httpClient.getAlerts(state)
1295+
1296+
CallToolResult(content = alerts.map { TextContent(it) })
1297+
}
1298+
1299+
// Register a tool to fetch weather forecast by latitude and longitude
1300+
server.addTool(
1301+
name = "get_forecast",
1302+
description = """
1303+
Get weather forecast for a specific latitude/longitude
1304+
""".trimIndent(),
1305+
inputSchema = Tool.Input(
1306+
properties = JsonObject(
1307+
mapOf(
1308+
"latitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))),
1309+
"longitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))),
1310+
)
1311+
),
1312+
required = listOf("latitude", "longitude")
1313+
)
1314+
) { request ->
1315+
val latitude = request.arguments["latitude"]?.jsonPrimitive?.doubleOrNull
1316+
val longitude = request.arguments["longitude"]?.jsonPrimitive?.doubleOrNull
1317+
if (latitude == null || longitude == null) {
1318+
return@addTool CallToolResult(
1319+
content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required."))
1320+
)
1321+
}
1322+
1323+
val forecast = httpClient.getForecast(latitude, longitude)
1324+
1325+
CallToolResult(content = forecast.map { TextContent(it) })
1326+
}
1327+
```
1328+
1329+
### Running the server
1330+
1331+
Finally, implement the main function to run the server:
1332+
1333+
```kotlin
1334+
fun main() = `run mcp server`()
1335+
```
1336+
1337+
Make sure to run `./gradlew build` to build your server. This is a very important step in getting your server to connect.
1338+
1339+
Let's now test your server from an existing MCP host, Claude for Desktop.
1340+
1341+
## Testing your server with Claude for Desktop
1342+
1343+
<Note>
1344+
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.
1345+
</Note>
1346+
1347+
First, make sure you have Claude for Desktop installed. [You can install the latest version
1348+
here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1349+
1350+
We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1351+
To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1352+
Make sure to create the file if it doesn't exist.
1353+
1354+
For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1355+
1356+
<CodeGroup>
1357+
```bash MacOS/Linux
1358+
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1359+
```
1360+
1361+
```powershell Windows
1362+
code $env:AppData\Claude\claude_desktop_config.json
1363+
```
1364+
</CodeGroup>
1365+
1366+
You'll then add your servers in the `mcpServers` key.
1367+
The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1368+
1369+
In this case, we'll add our single weather server like so:
1370+
1371+
<CodeGroup>
1372+
```json MacOS/Linux
1373+
{
1374+
"mcpServers": {
1375+
"weather": {
1376+
"command": "java",
1377+
"args": [
1378+
"-jar",
1379+
"/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
1380+
]
1381+
}
1382+
}
1383+
}
1384+
```
1385+
1386+
```json Windows
1387+
{
1388+
"mcpServers": {
1389+
"weather": {
1390+
"command": "java",
1391+
"args": [
1392+
"-jar",
1393+
"C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
1394+
]
1395+
}
1396+
}
1397+
}
1398+
```
1399+
</CodeGroup>
1400+
1401+
This tells Claude for Desktop:
1402+
1. There's an MCP server named "weather"
1403+
2. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1404+
1405+
Save the file, and restart **Claude for Desktop**.
1406+
10351407
</Tab>
10361408
</Tabs>
10371409

0 commit comments

Comments
 (0)