Skip to content

Commit b81c31f

Browse files
authored
Merge pull request modelcontextprotocol#178 from modelcontextprotocol/devcrocod/kotlin-quickstart-server
Add Kotlin server implementation guide for weather server to quickstart
2 parents 92fdc62 + a5dc69f commit b81c31f

File tree

1 file changed

+370
-0
lines changed

1 file changed

+370
-0
lines changed

quickstart/server.mdx

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,376 @@ 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+
// Create the MCP Server instance with a basic implementation
1144+
val server = Server(
1145+
Implementation(
1146+
name = "weather", // Tool name is "weather"
1147+
version = "1.0.0" // Version of the implementation
1148+
),
1149+
ServerOptions(
1150+
capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true))
1151+
)
1152+
)
1153+
1154+
// Create a transport using standard IO for server communication
1155+
val transport = StdioServerTransport(
1156+
System.`in`.asInput(),
1157+
System.out.asSink().buffered()
1158+
)
1159+
1160+
runBlocking {
1161+
server.connect(transport)
1162+
val done = Job()
1163+
server.onCloseCallback = {
1164+
done.complete()
1165+
}
1166+
done.join()
1167+
}
1168+
}
1169+
```
1170+
1171+
### Weather API helper functions
1172+
1173+
Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1174+
1175+
```kotlin
1176+
// Extension function to fetch forecast information for given latitude and longitude
1177+
suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
1178+
val points = this.get("/points/$latitude,$longitude").body<Points>()
1179+
val forecast = this.get(points.properties.forecast).body<Forecast>()
1180+
return forecast.properties.periods.map { period ->
1181+
"""
1182+
${period.name}:
1183+
Temperature: ${period.temperature} ${period.temperatureUnit}
1184+
Wind: ${period.windSpeed} ${period.windDirection}
1185+
Forecast: ${period.detailedForecast}
1186+
""".trimIndent()
1187+
}
1188+
}
1189+
1190+
// Extension function to fetch weather alerts for a given state
1191+
suspend fun HttpClient.getAlerts(state: String): List<String> {
1192+
val alerts = this.get("/alerts/active/area/$state").body<Alert>()
1193+
return alerts.features.map { feature ->
1194+
"""
1195+
Event: ${feature.properties.event}
1196+
Area: ${feature.properties.areaDesc}
1197+
Severity: ${feature.properties.severity}
1198+
Description: ${feature.properties.description}
1199+
Instruction: ${feature.properties.instruction}
1200+
""".trimIndent()
1201+
}
1202+
}
1203+
1204+
@Serializable
1205+
data class Points(
1206+
val properties: Properties
1207+
) {
1208+
@Serializable
1209+
data class Properties(val forecast: String)
1210+
}
1211+
1212+
@Serializable
1213+
data class Forecast(
1214+
val properties: Properties
1215+
) {
1216+
@Serializable
1217+
data class Properties(val periods: List<Period>)
1218+
1219+
@Serializable
1220+
data class Period(
1221+
val number: Int, val name: String, val startTime: String, val endTime: String,
1222+
val isDaytime: Boolean, val temperature: Int, val temperatureUnit: String,
1223+
val temperatureTrend: String, val probabilityOfPrecipitation: JsonObject,
1224+
val windSpeed: String, val windDirection: String,
1225+
val shortForecast: String, val detailedForecast: String,
1226+
)
1227+
}
1228+
1229+
@Serializable
1230+
data class Alert(
1231+
val features: List<Feature>
1232+
) {
1233+
@Serializable
1234+
data class Feature(
1235+
val properties: Properties
1236+
)
1237+
1238+
@Serializable
1239+
data class Properties(
1240+
val event: String, val areaDesc: String, val severity: String,
1241+
val description: String, val instruction: String?,
1242+
)
1243+
}
1244+
```
1245+
1246+
### Implementing tool execution
1247+
1248+
The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
1249+
1250+
```kotlin
1251+
// Create an HTTP client with a default request configuration and JSON content negotiation
1252+
val httpClient = HttpClient {
1253+
defaultRequest {
1254+
url("https://api.weather.gov")
1255+
headers {
1256+
append("Accept", "application/geo+json")
1257+
append("User-Agent", "WeatherApiClient/1.0")
1258+
}
1259+
contentType(ContentType.Application.Json)
1260+
}
1261+
// Install content negotiation plugin for JSON serialization/deserialization
1262+
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
1263+
}
1264+
1265+
// Register a tool to fetch weather alerts by state
1266+
server.addTool(
1267+
name = "get_alerts",
1268+
description = """
1269+
Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)
1270+
""".trimIndent(),
1271+
inputSchema = Tool.Input(
1272+
properties = JsonObject(
1273+
mapOf(
1274+
"state" to JsonObject(
1275+
mapOf(
1276+
"type" to JsonPrimitive("string"),
1277+
"description" to JsonPrimitive("Two-letter US state code (e.g. CA, NY)")
1278+
)
1279+
),
1280+
)
1281+
),
1282+
required = listOf("state")
1283+
)
1284+
) { request ->
1285+
val state = request.arguments["state"]?.jsonPrimitive?.content
1286+
if (state == null) {
1287+
return@addTool CallToolResult(
1288+
content = listOf(TextContent("The 'state' parameter is required."))
1289+
)
1290+
}
1291+
1292+
val alerts = httpClient.getAlerts(state)
1293+
1294+
CallToolResult(content = alerts.map { TextContent(it) })
1295+
}
1296+
1297+
// Register a tool to fetch weather forecast by latitude and longitude
1298+
server.addTool(
1299+
name = "get_forecast",
1300+
description = """
1301+
Get weather forecast for a specific latitude/longitude
1302+
""".trimIndent(),
1303+
inputSchema = Tool.Input(
1304+
properties = JsonObject(
1305+
mapOf(
1306+
"latitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))),
1307+
"longitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))),
1308+
)
1309+
),
1310+
required = listOf("latitude", "longitude")
1311+
)
1312+
) { request ->
1313+
val latitude = request.arguments["latitude"]?.jsonPrimitive?.doubleOrNull
1314+
val longitude = request.arguments["longitude"]?.jsonPrimitive?.doubleOrNull
1315+
if (latitude == null || longitude == null) {
1316+
return@addTool CallToolResult(
1317+
content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required."))
1318+
)
1319+
}
1320+
1321+
val forecast = httpClient.getForecast(latitude, longitude)
1322+
1323+
CallToolResult(content = forecast.map { TextContent(it) })
1324+
}
1325+
```
1326+
1327+
### Running the server
1328+
1329+
Finally, implement the main function to run the server:
1330+
1331+
```kotlin
1332+
fun main() = `run mcp server`()
1333+
```
1334+
1335+
Make sure to run `./gradlew build` to build your server. This is a very important step in getting your server to connect.
1336+
1337+
Let's now test your server from an existing MCP host, Claude for Desktop.
1338+
1339+
## Testing your server with Claude for Desktop
1340+
1341+
<Note>
1342+
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.
1343+
</Note>
1344+
1345+
First, make sure you have Claude for Desktop installed. [You can install the latest version
1346+
here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1347+
1348+
We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1349+
To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1350+
Make sure to create the file if it doesn't exist.
1351+
1352+
For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1353+
1354+
<CodeGroup>
1355+
```bash MacOS/Linux
1356+
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1357+
```
1358+
1359+
```powershell Windows
1360+
code $env:AppData\Claude\claude_desktop_config.json
1361+
```
1362+
</CodeGroup>
1363+
1364+
You'll then add your servers in the `mcpServers` key.
1365+
The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1366+
1367+
In this case, we'll add our single weather server like so:
1368+
1369+
<CodeGroup>
1370+
```json MacOS/Linux
1371+
{
1372+
"mcpServers": {
1373+
"weather": {
1374+
"command": "java",
1375+
"args": [
1376+
"-jar",
1377+
"/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
1378+
]
1379+
}
1380+
}
1381+
}
1382+
```
1383+
1384+
```json Windows
1385+
{
1386+
"mcpServers": {
1387+
"weather": {
1388+
"command": "java",
1389+
"args": [
1390+
"-jar",
1391+
"C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
1392+
]
1393+
}
1394+
}
1395+
}
1396+
```
1397+
</CodeGroup>
1398+
1399+
This tells Claude for Desktop:
1400+
1. There's an MCP server named "weather"
1401+
2. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1402+
1403+
Save the file, and restart **Claude for Desktop**.
1404+
10351405
</Tab>
10361406
</Tabs>
10371407

0 commit comments

Comments
 (0)