docs: update client and server samples (#598)

Updated the client and server examples for further use in the following
guides: https://modelcontextprotocol.io/docs/develop/build-server and
https://modelcontextprotocol.io/docs/develop/build-client

## How Has This Been Tested?
claude desktop, cli

## Breaking Changes
NaN

## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Example, Documentation update

## Checklist
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed
This commit is contained in:
Pavel Gorgulov 2026-03-13 10:23:51 +01:00 • committed by GitHub
commit 921b1fd30e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 311 additions and 228 deletions

View file

@ -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)),
),
)
// 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")
server.registerTools(httpClient)
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.")),
)
if (state.length != 2) {
return@addTool CallToolResult(
content = listOf(TextContent("Invalid state code: '$state'. Must be a two-letter US state code.")),
isError = true,
)
}
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,
)
},
)
val alerts = httpClient.getAlerts(state)
CallToolResult(content = alerts.map { TextContent(it) })
}
// 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,
)
},
)
}
}

View file

@ -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
// 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()
}
}
import java.util.Locale
// 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
suspend fun HttpClient.getAlerts(state: String): Result<List<String>> = runCatching {
val uri = "/alerts/active/area/$state"
// Request the alerts data from the API
val alerts = this.get(uri).body<Alert>()
val alerts = this.get(uri).body<AlertsResponse>()
// Map each alert feature to a formatted string
return alerts.features.map { feature ->
alerts.features.map { feature ->
"""
Event: ${feature.properties.event}
Area: ${feature.properties.areaDesc}
Severity: ${feature.properties.severity}
Description: ${feature.properties.description}
Instruction: ${feature.properties.instruction}
Status: ${feature.properties.status}
Headline: ${feature.properties.headline}
""".trimIndent()
}
}
// Data class representing the points response from the API
@Serializable
data class Points(
val properties: Properties
) {
@Serializable
data class Properties(val forecast: String)
// Extension function to fetch forecast information for given latitude and longitude
suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): Result<List<String>> = 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<PointsResponse>()
val forecastUrl = points.properties.forecast
?: error("No forecast URL available for coordinates: $latitude, $longitude")
val forecast = this.get(forecastUrl).body<ForecastResponse>()
forecast.properties.periods.map { period ->
"""
${period.name}:
Temperature: ${period.temperature}°${period.temperatureUnit}
Wind: ${period.windSpeed} ${period.windDirection}
${period.shortForecast}
""".trimIndent()
}
}
// Data class representing the forecast response from the API
@Serializable
data class Forecast(
val properties: Properties
) {
@Serializable
data class Properties(val periods: List<Period>)
data class PointsResponse(
val properties: PointsProperties,
)
@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
)
data class PointsProperties(
val forecast: String? = null,
)
@Serializable
data class Properties(
val event: String, val areaDesc: String, val severity: String,
val description: String, val instruction: String?,
)
}
@Serializable
data class ForecastResponse(
val properties: ForecastProperties,
)
@Serializable
data class ForecastProperties(
val periods: List<ForecastPeriod> = emptyList(),
)
@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<AlertFeature> = 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,
)

View file

@ -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() }