Skip to content

Commit 921b1fd

Browse files
authored
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
1 parent 4ecd952 commit 921b1fd

9 files changed

Lines changed: 311 additions & 228 deletions

File tree

samples/kotlin-mcp-client/gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[versions]
2-
anthropic = "2.9.0"
2+
anthropic = "2.15.0"
33
kotlin = "2.2.21"
44
ktor = "3.2.3"
5-
mcp-kotlin = "0.8.1"
5+
mcp-kotlin = "0.9.0"
66
shadow = "9.2.2"
77
slf4j = "2.0.17"
88

samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt

Lines changed: 123 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package io.modelcontextprotocol.sample.client
22

33
import com.anthropic.client.okhttp.AnthropicOkHttpClient
44
import com.anthropic.core.JsonValue
5+
import com.anthropic.models.messages.ContentBlockParam
56
import com.anthropic.models.messages.MessageCreateParams
67
import com.anthropic.models.messages.MessageParam
7-
import com.anthropic.models.messages.Model
88
import com.anthropic.models.messages.Tool
9+
import com.anthropic.models.messages.ToolResultBlockParam
910
import com.anthropic.models.messages.ToolUnion
1011
import com.fasterxml.jackson.core.type.TypeReference
1112
import com.fasterxml.jackson.databind.ObjectMapper
@@ -14,24 +15,28 @@ import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport
1415
import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject
1516
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
1617
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
18+
import kotlinx.coroutines.Dispatchers
1719
import kotlinx.coroutines.runBlocking
18-
import kotlinx.coroutines.yield
20+
import kotlinx.coroutines.withContext
1921
import kotlinx.io.asSink
2022
import kotlinx.io.asSource
2123
import kotlinx.io.buffered
2224
import kotlinx.serialization.json.JsonObject
23-
import kotlin.jvm.optionals.getOrNull
25+
import java.util.concurrent.TimeUnit
2426

25-
class MCPClient : AutoCloseable {
26-
// Configures using the `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` environment variables
27-
private val anthropic = AnthropicOkHttpClient.fromEnv()
27+
private const val MODEL = "claude-sonnet-4-20250514"
28+
private const val MAX_TOKENS = 1024L
29+
30+
class MCPClient(apiKey: String) : AutoCloseable {
31+
private val anthropic = AnthropicOkHttpClient.builder()
32+
.apiKey(apiKey)
33+
.build()
2834

2935
// Initialize MCP client
3036
private val mcp: Client = Client(clientInfo = Implementation(name = "mcp-client-cli", version = "1.0.0"))
3137

32-
private val messageParamsBuilder: MessageCreateParams.Builder = MessageCreateParams.builder()
33-
.model(Model.CLAUDE_4_SONNET_20250514)
34-
.maxTokens(1024)
38+
// Server process reference for cleanup
39+
private var serverProcess: Process? = null
3540

3641
// List of tools offered by the server
3742
private lateinit var tools: List<ToolUnion>
@@ -44,52 +49,52 @@ class MCPClient : AutoCloseable {
4449

4550
// Connect to the server using the path to the server
4651
suspend fun connectToServer(serverScriptPath: String) {
47-
try {
48-
// Build the command based on the file extension of the server script
49-
val command = buildList {
50-
when (serverScriptPath.substringAfterLast(".")) {
51-
"js" -> add("node")
52-
"py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3")
53-
"jar" -> addAll(listOf("java", "-jar"))
54-
else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file")
55-
}
56-
add(serverScriptPath)
52+
// Build the command based on the file extension of the server script
53+
val command = buildList {
54+
when (serverScriptPath.substringAfterLast(".")) {
55+
"js" -> add("node")
56+
"py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3")
57+
"jar" -> addAll(listOf("java", "-jar"))
58+
else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file")
5759
}
60+
add(serverScriptPath)
61+
}
5862

59-
// Start the server process
60-
val process = ProcessBuilder(command).start()
63+
// Start the server process
64+
val process = withContext(Dispatchers.IO) {
65+
ProcessBuilder(command)
66+
.redirectError(ProcessBuilder.Redirect.INHERIT)
67+
.start()
68+
}
69+
serverProcess = process
6170

62-
// Setup I/O transport using the process streams
63-
val transport = StdioClientTransport(
64-
input = process.inputStream.asSource().buffered(),
65-
output = process.outputStream.asSink().buffered(),
66-
)
71+
// Setup I/O transport using the process streams
72+
val transport = StdioClientTransport(
73+
input = process.inputStream.asSource().buffered(),
74+
output = process.outputStream.asSink().buffered(),
75+
)
6776

68-
// Connect the MCP client to the server using the transport
69-
mcp.connect(transport)
70-
71-
// Request the list of available tools from the server
72-
val toolsResult = mcp.listTools()
73-
tools = toolsResult.tools.map { tool ->
74-
ToolUnion.ofTool(
75-
Tool.builder()
76-
.name(tool.name)
77-
.description(tool.description ?: "")
78-
.inputSchema(
79-
Tool.InputSchema.builder()
80-
.type(JsonValue.from(tool.inputSchema.type))
81-
.properties(tool.inputSchema.properties?.toJsonValue() ?: EmptyJsonObject.toJsonValue())
82-
.putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required))
83-
.build(),
84-
)
85-
.build(),
86-
)
87-
}
88-
println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}")
89-
} catch (e: Exception) {
90-
println("Failed to connect to MCP server: $e")
91-
throw e
77+
// Connect the MCP client to the server using the transport
78+
mcp.connect(transport)
79+
80+
// Request the list of available tools from the server
81+
val toolsResult = mcp.listTools()
82+
tools = toolsResult.tools.map { tool ->
83+
ToolUnion.ofTool(
84+
Tool.builder()
85+
.name(tool.name)
86+
.description(tool.description ?: "")
87+
.inputSchema(
88+
Tool.InputSchema.builder()
89+
.type(JsonValue.from(tool.inputSchema.type))
90+
.properties(tool.inputSchema.properties?.toJsonValue() ?: EmptyJsonObject.toJsonValue())
91+
.putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required))
92+
.build(),
93+
)
94+
.build(),
95+
)
9296
}
97+
println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}")
9398
}
9499

95100
// Process a user query and return a string response
@@ -104,23 +109,29 @@ class MCPClient : AutoCloseable {
104109

105110
// Send the query to the Anthropic model and get the response
106111
val response = anthropic.messages().create(
107-
messageParamsBuilder
112+
MessageCreateParams.builder()
113+
.model(MODEL)
114+
.maxTokens(MAX_TOKENS)
108115
.messages(messages)
109116
.tools(tools)
110117
.build(),
111118
)
112119

113120
val finalText = mutableListOf<String>()
121+
val toolResults = mutableListOf<ContentBlockParam>()
122+
114123
response.content().forEach { content ->
115124
when {
116125
// Append text outputs from the response
117-
content.isText() -> finalText.add(content.text().getOrNull()?.text() ?: "")
126+
content.isText() -> finalText.add(content.text().get().text())
118127

119128
// If the response indicates a tool use, process it further
120129
content.isToolUse() -> {
121-
val toolName = content.toolUse().get().name()
130+
val toolUse = content.toolUse().get()
131+
val toolName = toolUse.name()
132+
val toolUseId = toolUse.id()
122133
val toolArgs =
123-
content.toolUse().get()._input().convert(object : TypeReference<Map<String, JsonValue>>() {})
134+
toolUse._input().convert(object : TypeReference<Map<String, JsonValue>>() {})
124135

125136
// Call the tool with provided arguments
126137
val result = mcp.callTool(
@@ -129,37 +140,55 @@ class MCPClient : AutoCloseable {
129140
)
130141
finalText.add("[Calling tool $toolName with args $toolArgs]")
131142

132-
// Add the tool result message to the conversation
133-
messages.add(
134-
MessageParam.builder()
135-
.role(MessageParam.Role.USER)
136-
.content(
137-
"""
138-
"type": "tool_result",
139-
"tool_name": $toolName,
140-
"result": ${
141-
result.content.joinToString("\n") {
142-
(it as TextContent).text
143-
}
144-
}
145-
""".trimIndent(),
146-
)
147-
.build(),
148-
)
143+
// Build tool_result content block with tool_use_id
144+
val toolResultContent = result.content
145+
.filterIsInstance<TextContent>()
146+
.joinToString("\n") { it.text }
149147

150-
// Retrieve an updated response after tool execution
151-
val aiResponse = anthropic.messages().create(
152-
messageParamsBuilder
153-
.messages(messages)
154-
.build(),
155-
)
148+
val toolResultBlock = ToolResultBlockParam.builder()
149+
.toolUseId(toolUseId)
150+
.content(toolResultContent)
151+
.apply { if (result.isError == true) isError(true) }
152+
.build()
156153

157-
// Append the updated response to final text
158-
finalText.add(aiResponse.content().first().text().getOrNull()?.text() ?: "")
154+
toolResults.add(ContentBlockParam.ofToolResult(toolResultBlock))
159155
}
160156
}
161157
}
162158

159+
// If there were tool calls, send tool results back and get final response
160+
if (toolResults.isNotEmpty()) {
161+
// Add the full assistant response (includes tool_use blocks)
162+
messages.add(
163+
MessageParam.builder()
164+
.role(MessageParam.Role.ASSISTANT)
165+
.contentOfBlockParams(response.content().map { it.toParam() })
166+
.build(),
167+
)
168+
169+
// Add user message with tool results
170+
messages.add(
171+
MessageParam.builder()
172+
.role(MessageParam.Role.USER)
173+
.contentOfBlockParams(toolResults)
174+
.build(),
175+
)
176+
177+
// Retrieve an updated response after tool execution (without tools)
178+
val aiResponse = anthropic.messages().create(
179+
MessageCreateParams.builder()
180+
.model(MODEL)
181+
.maxTokens(MAX_TOKENS)
182+
.messages(messages)
183+
.build(),
184+
)
185+
186+
// Append the updated response to final text
187+
aiResponse.content()
188+
.filter { it.isText() }
189+
.forEach { finalText.add(it.text().get().text()) }
190+
}
191+
163192
return finalText.joinToString("\n", prefix = "", postfix = "")
164193
}
165194

@@ -171,17 +200,27 @@ class MCPClient : AutoCloseable {
171200
while (true) {
172201
print("\nQuery: ")
173202
val message = readlnOrNull() ?: break
174-
if (message.lowercase() == "quit") break
175-
val response = processQuery(message)
176-
println("\n$response")
177-
yield()
203+
if (message.trim().lowercase() == "quit") break
204+
205+
try {
206+
val response = processQuery(message)
207+
println("\n$response")
208+
} catch (e: Exception) {
209+
println("\nError: ${e.message}")
210+
}
178211
}
179212
}
180213

181214
override fun close() {
182215
runBlocking {
183216
mcp.close()
184-
anthropic.close()
185217
}
218+
serverProcess?.let { process ->
219+
process.destroy()
220+
if (!process.waitFor(5, TimeUnit.SECONDS)) {
221+
process.destroyForcibly()
222+
}
223+
}
224+
anthropic.close()
186225
}
187226
}

samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ package io.modelcontextprotocol.sample.client
33
import kotlinx.coroutines.runBlocking
44

55
fun main(args: Array<String>) = runBlocking {
6-
require(args.isNotEmpty()) {
7-
"Usage: java -jar <your_path>/build/libs/kotlin-mcp-client-0.1.0-all.jar <path_to_server_script>"
8-
}
9-
val serverPath = args.first()
10-
val client = MCPClient()
6+
require(args.isNotEmpty()) { "Usage: java -jar <path> <path_to_server_script>" }
7+
8+
val apiKey = System.getenv("ANTHROPIC_API_KEY")
9+
require(!apiKey.isNullOrBlank()) { "ANTHROPIC_API_KEY environment variable is not set" }
10+
11+
val client = MCPClient(apiKey)
1112
client.use {
12-
client.connectToServer(serverPath)
13+
client.connectToServer(args.first())
1314
client.chatLoop()
1415
}
1516
}

samples/weather-stdio-server/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ Add the following to your Claude Desktop configuration:
5757

5858
| Name | Description |
5959
|----------------|------------------------------------------------------------------------------------------|
60-
| `get_forecast` | Returns weather forecast for a given `latitude` / `longitude` using the weather.gov API. |
6160
| `get_alerts` | Returns active weather alerts for a two-letter US `state` code (e.g. `CA`, `NY`). |
61+
| `get_forecast` | Returns weather forecast for a given `latitude` / `longitude` using the weather.gov API. |
6262

6363
## Additional Resources
6464

samples/weather-stdio-server/build.gradle.kts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ version = "0.1.0"
1414

1515
dependencies {
1616
implementation(dependencies.platform(libs.ktor.bom))
17+
implementation(libs.mcp.kotlin.server)
1718
implementation(libs.ktor.client.content.negotiation)
1819
implementation(libs.ktor.serialization.kotlinx.json)
19-
implementation(libs.mcp.kotlin.server)
20-
implementation(libs.ktor.server.cio)
2120
implementation(libs.ktor.client.cio)
2221
implementation(libs.slf4j.simple)
2322
runtimeOnly(libs.kotlin.logging)
24-
runtimeOnly(libs.kotlinx.collections.immutable)
2523

2624
testImplementation(kotlin("test"))
2725

samples/weather-stdio-server/gradle/libs.versions.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
[versions]
2-
collections-immutable = "0.4.0"
32
coroutines = "1.10.2"
43
kotlin = "2.2.21"
54
ktor = "3.2.3"
65
logging = "7.0.13"
7-
mcp-kotlin = "0.8.1"
6+
mcp-kotlin = "0.9.0"
87
shadow = "9.2.2"
98
slf4j = "2.0.17"
109

1110
[libraries]
1211
kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "logging" }
13-
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable-jvm", version.ref = "collections-immutable" }
1412
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
1513
ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" }
1614
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" }
1715
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation" }
1816
ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json-jvm" }
19-
ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" }
2017
mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" }
2118
mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" }
2219
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }

0 commit comments

Comments
 (0)