@@ -2,10 +2,11 @@ package io.modelcontextprotocol.sample.client
22
33import com.anthropic.client.okhttp.AnthropicOkHttpClient
44import com.anthropic.core.JsonValue
5+ import com.anthropic.models.messages.ContentBlockParam
56import com.anthropic.models.messages.MessageCreateParams
67import com.anthropic.models.messages.MessageParam
7- import com.anthropic.models.messages.Model
88import com.anthropic.models.messages.Tool
9+ import com.anthropic.models.messages.ToolResultBlockParam
910import com.anthropic.models.messages.ToolUnion
1011import com.fasterxml.jackson.core.type.TypeReference
1112import com.fasterxml.jackson.databind.ObjectMapper
@@ -14,24 +15,28 @@ import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport
1415import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject
1516import io.modelcontextprotocol.kotlin.sdk.types.Implementation
1617import io.modelcontextprotocol.kotlin.sdk.types.TextContent
18+ import kotlinx.coroutines.Dispatchers
1719import kotlinx.coroutines.runBlocking
18- import kotlinx.coroutines.yield
20+ import kotlinx.coroutines.withContext
1921import kotlinx.io.asSink
2022import kotlinx.io.asSource
2123import kotlinx.io.buffered
2224import 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 (" \n Query: " )
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 (" \n Error: ${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}
0 commit comments