Add Kotlin client quickstart (#189)
This commit is contained in:
parent
3b2c2037ce
commit
78282fae4e
1 changed files with 399 additions and 0 deletions
|
|
@ -966,6 +966,405 @@ This provides similar functionality but uses a WebFlux-based SSE transport imple
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab title="Kotlin">
|
||||||
|
|
||||||
|
[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/kotlin-mcp-client)
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
Before starting, ensure your system meets these requirements:
|
||||||
|
- Java 17 or higher
|
||||||
|
- Anthropic API key (Claude)
|
||||||
|
|
||||||
|
## Setting up your environment
|
||||||
|
|
||||||
|
First, let's install `java` and `gradle` if you haven't already.
|
||||||
|
You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
|
||||||
|
Verify your `java` installation:
|
||||||
|
```bash
|
||||||
|
java --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, let's create and set up your project:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```bash MacOS/Linux
|
||||||
|
# Create a new directory for our project
|
||||||
|
mkdir kotlin-mcp-client
|
||||||
|
cd kotlin-mcp-client
|
||||||
|
|
||||||
|
# Initialize a new kotlin project
|
||||||
|
gradle init
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell Windows
|
||||||
|
# Create a new directory for our project
|
||||||
|
md kotlin-mcp-client
|
||||||
|
cd kotlin-mcp-client
|
||||||
|
# Initialize a new kotlin project
|
||||||
|
gradle init
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
After running `gradle init`, you will be presented with options for creating your project.
|
||||||
|
Select **Application** as the project type, **Kotlin** as the programming language, and **Java 17** as the Java version.
|
||||||
|
|
||||||
|
Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
|
||||||
|
|
||||||
|
After creating the project, add the following dependencies:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```kotlin build.gradle.kts
|
||||||
|
val mcpVersion = "0.3.0"
|
||||||
|
val slf4jVersion = "2.0.9"
|
||||||
|
val anthropicVersion = "0.8.0"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
|
||||||
|
implementation("org.slf4j:slf4j-nop:$slf4jVersion")
|
||||||
|
implementation("com.anthropic:anthropic-java:$anthropicVersion")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```groovy build.gradle
|
||||||
|
def mcpVersion = '0.3.0'
|
||||||
|
def slf4jVersion = '2.0.9'
|
||||||
|
def anthropicVersion = '0.8.0'
|
||||||
|
dependencies {
|
||||||
|
implementation "io.modelcontextprotocol:kotlin-sdk:$mcpVersion"
|
||||||
|
implementation "org.slf4j:slf4j-nop:$slf4jVersion"
|
||||||
|
implementation "com.anthropic:anthropic-java:$anthropicVersion"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
Also, add the following plugins to your build script:
|
||||||
|
<CodeGroup>
|
||||||
|
```kotlin build.gradle.kts
|
||||||
|
plugins {
|
||||||
|
id("com.github.johnrengelman.shadow") version "8.1.1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```groovy build.gradle
|
||||||
|
plugins {
|
||||||
|
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Setting up your API key
|
||||||
|
|
||||||
|
You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
|
||||||
|
|
||||||
|
Set up your API key:
|
||||||
|
```bash
|
||||||
|
export ANTHROPIC_API_KEY='your-anthropic-api-key-here'
|
||||||
|
```
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Make sure your keep your `ANTHROPIC_API_KEY` secure!
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## Creating the Client
|
||||||
|
|
||||||
|
### Basic Client Structure
|
||||||
|
|
||||||
|
First, let's create the basic client class:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class MCPClient : AutoCloseable {
|
||||||
|
private val anthropic = AnthropicOkHttpClient.fromEnv()
|
||||||
|
private val mcp: Client = Client(clientInfo = Implementation(name = "mcp-client-cli", version = "1.0.0"))
|
||||||
|
private lateinit var tools: List<ToolUnion>
|
||||||
|
|
||||||
|
// methods will go here
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
runBlocking {
|
||||||
|
mcp.close()
|
||||||
|
anthropic.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server connection managment
|
||||||
|
|
||||||
|
Next, we'll implement the method to connect to an MCP server:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
suspend fun connectToServer(serverScriptPath: String) {
|
||||||
|
try {
|
||||||
|
val command = buildList {
|
||||||
|
when (serverScriptPath.substringAfterLast(".")) {
|
||||||
|
"js" -> add("node")
|
||||||
|
"py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3")
|
||||||
|
"jar" -> addAll(listOf("java", "-jar"))
|
||||||
|
else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file")
|
||||||
|
}
|
||||||
|
add(serverScriptPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
val process = ProcessBuilder(command).start()
|
||||||
|
val transport = StdioClientTransport(
|
||||||
|
input = process.inputStream.asSource().buffered(),
|
||||||
|
output = process.outputStream.asSink().buffered()
|
||||||
|
)
|
||||||
|
|
||||||
|
mcp.connect(transport)
|
||||||
|
|
||||||
|
val toolsResult = mcp.listTools()
|
||||||
|
tools = toolsResult?.tools?.map { tool ->
|
||||||
|
ToolUnion.ofTool(
|
||||||
|
Tool.builder()
|
||||||
|
.name(tool.name)
|
||||||
|
.description(tool.description ?: "")
|
||||||
|
.inputSchema(
|
||||||
|
Tool.InputSchema.builder()
|
||||||
|
.type(JsonValue.from(tool.inputSchema.type))
|
||||||
|
.properties(tool.inputSchema.properties.toJsonValue())
|
||||||
|
.putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
} ?: emptyList()
|
||||||
|
println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Failed to connect to MCP server: $e")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also create a helper function to convert from `JsonObject` to `JsonValue` for Anthropic:
|
||||||
|
```kotlin
|
||||||
|
private fun JsonObject.toJsonValue(): JsonValue {
|
||||||
|
val mapper = ObjectMapper()
|
||||||
|
val node = mapper.readTree(this.toString())
|
||||||
|
return JsonValue.fromJsonNode(node)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query processing logic
|
||||||
|
|
||||||
|
Now let's add the core functionality for processing queries and handling tool calls:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private val messageParamsBuilder: MessageCreateParams.Builder = MessageCreateParams.builder()
|
||||||
|
.model(Model.CLAUDE_3_5_SONNET_20241022)
|
||||||
|
.maxTokens(1024)
|
||||||
|
|
||||||
|
suspend fun processQuery(query: String): String {
|
||||||
|
val messages = mutableListOf(
|
||||||
|
MessageParam.builder()
|
||||||
|
.role(MessageParam.Role.USER)
|
||||||
|
.content(query)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = anthropic.messages().create(
|
||||||
|
messageParamsBuilder
|
||||||
|
.messages(messages)
|
||||||
|
.tools(tools)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
val finalText = mutableListOf<String>()
|
||||||
|
response.content().forEach { content ->
|
||||||
|
when {
|
||||||
|
content.isText() -> finalText.add(content.text().getOrNull()?.text() ?: "")
|
||||||
|
|
||||||
|
content.isToolUse() -> {
|
||||||
|
val toolName = content.toolUse().get().name()
|
||||||
|
val toolArgs =
|
||||||
|
content.toolUse().get()._input().convert(object : TypeReference<Map<String, JsonValue>>() {})
|
||||||
|
|
||||||
|
val result = mcp.callTool(
|
||||||
|
name = toolName,
|
||||||
|
arguments = toolArgs ?: emptyMap()
|
||||||
|
)
|
||||||
|
finalText.add("[Calling tool $toolName with args $toolArgs]")
|
||||||
|
|
||||||
|
messages.add(
|
||||||
|
MessageParam.builder()
|
||||||
|
.role(MessageParam.Role.USER)
|
||||||
|
.content(
|
||||||
|
"""
|
||||||
|
"type": "tool_result",
|
||||||
|
"tool_name": $toolName,
|
||||||
|
"result": ${result?.content?.joinToString("\n") { (it as TextContent).text ?: "" }}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
val aiResponse = anthropic.messages().create(
|
||||||
|
messageParamsBuilder
|
||||||
|
.messages(messages)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
finalText.add(aiResponse.content().first().text().getOrNull()?.text() ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalText.joinToString("\n", prefix = "", postfix = "")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interactive chat
|
||||||
|
|
||||||
|
We'll add the chat loop:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
suspend fun chatLoop() {
|
||||||
|
println("\nMCP Client Started!")
|
||||||
|
println("Type your queries or 'quit' to exit.")
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
print("\nQuery: ")
|
||||||
|
val message = readLine() ?: break
|
||||||
|
if (message.lowercase() == "quit") break
|
||||||
|
val response = processQuery(message)
|
||||||
|
println("\n$response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Main entry point
|
||||||
|
|
||||||
|
Finally, we'll add the main execution function:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun main(args: Array<String>) = runBlocking {
|
||||||
|
if (args.isEmpty()) throw IllegalArgumentException("Usage: java -jar <your_path>/build/libs/kotlin-mcp-client-0.1.0-all.jar <path_to_server_script>")
|
||||||
|
val serverPath = args.first()
|
||||||
|
val client = MCPClient()
|
||||||
|
client.use {
|
||||||
|
client.connectToServer(serverPath)
|
||||||
|
client.chatLoop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the client
|
||||||
|
|
||||||
|
To run your client with any MCP server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew build
|
||||||
|
|
||||||
|
# Run the client
|
||||||
|
java -jar build/libs/<your-jar-name>.jar path/to/server.jar # jvm server
|
||||||
|
java -jar build/libs/<your-jar-name>.jar path/to/server.py # python server
|
||||||
|
java -jar build/libs/<your-jar-name>.jar path/to/build/index.js # node server
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you're continuing the weather tutorial from the server quickstart, your command might look something like this: `java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar .../samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar`
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
**The client will:**
|
||||||
|
1. Connect to the specified server
|
||||||
|
2. List available tools
|
||||||
|
3. Start an interactive chat session where you can:
|
||||||
|
- Enter queries
|
||||||
|
- See tool executions
|
||||||
|
- Get responses from Claude
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Here's a high-level workflow schema:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
theme: neutral
|
||||||
|
---
|
||||||
|
sequenceDiagram
|
||||||
|
actor User
|
||||||
|
participant Client
|
||||||
|
participant Claude
|
||||||
|
participant MCP_Server as MCP Server
|
||||||
|
participant Tools
|
||||||
|
|
||||||
|
User->>Client: Send query
|
||||||
|
Client<<->>MCP_Server: Get available tools
|
||||||
|
Client->>Claude: Send query with tool descriptions
|
||||||
|
Claude-->>Client: Decide tool execution
|
||||||
|
Client->>MCP_Server: Request tool execution
|
||||||
|
MCP_Server->>Tools: Execute chosen tools
|
||||||
|
Tools-->>MCP_Server: Return results
|
||||||
|
MCP_Server-->>Client: Send results
|
||||||
|
Client->>Claude: Send tool results
|
||||||
|
Claude-->>Client: Provide final response
|
||||||
|
Client-->>User: Display response
|
||||||
|
```
|
||||||
|
|
||||||
|
When you submit a query:
|
||||||
|
1. The client gets the list of available tools from the server
|
||||||
|
2. Your query is sent to Claude along with tool descriptions
|
||||||
|
3. Claude decides which tools (if any) to use
|
||||||
|
4. The client executes any requested tool calls through the server
|
||||||
|
5. Results are sent back to Claude
|
||||||
|
6. Claude provides a natural language response
|
||||||
|
7. The response is displayed to you
|
||||||
|
|
||||||
|
## Best practices
|
||||||
|
|
||||||
|
1. **Error Handling**
|
||||||
|
- Leverage Kotlin's type system to model errors explicitly
|
||||||
|
- Wrap external tool and API calls in `try-catch` blocks when exceptions are possible
|
||||||
|
- Provide clear and meaningful error messages
|
||||||
|
- Handle network timeouts and connection issues gracefully
|
||||||
|
|
||||||
|
2. **Security**
|
||||||
|
- Store API keys and secrets securely in `local.properties`, environment variables, or secret managers
|
||||||
|
- Validate all external responses to avoid unexpected or unsafe data usage
|
||||||
|
- Be cautious with permissions and trust boundaries when using tools
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Server Path Issues
|
||||||
|
- Double-check the path to your server script is correct
|
||||||
|
- Use the absolute path if the relative path isn't working
|
||||||
|
- For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
|
||||||
|
- Make sure that the required runtime is installed (java for Java, npm for Node.js, or uv for Python)
|
||||||
|
- Verify the server file has the correct extension (.jar for Java, .js for Node.js or .py for Python)
|
||||||
|
|
||||||
|
Example of correct path usage:
|
||||||
|
```bash
|
||||||
|
# Relative path
|
||||||
|
java -jar build/libs/client.jar ./server/build/libs/server.jar
|
||||||
|
|
||||||
|
# Absoulute path
|
||||||
|
java -jar build/libs/client.jar /Users/username/projects/mcp-server/build/libs/server.jar
|
||||||
|
|
||||||
|
# Windows path (either format works)
|
||||||
|
java -jar build/libs/client.jar C:/projects/mcp-server/build/libs/server.jar
|
||||||
|
java -jar build/libs/client.jar C:\\projects\\mcp-server\\build\\libs\\server.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Timing
|
||||||
|
- The first response might take up to 30 seconds to return
|
||||||
|
- This is normal and happens while:
|
||||||
|
- The server initializes
|
||||||
|
- Claude processes the query
|
||||||
|
- Tools are being executed
|
||||||
|
- Subsequent responses are typically faster
|
||||||
|
- Don't interrupt the process during this initial waiting period
|
||||||
|
|
||||||
|
### Common Error Messages
|
||||||
|
|
||||||
|
If you see:
|
||||||
|
- `Connection refused`: Ensure the server is running and the path is correct
|
||||||
|
- `Tool execution failed`: Verify the tool's required environment variables are set
|
||||||
|
- `ANTHROPIC_API_KEY is not set`: Check your environment variables
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
## Next steps
|
## Next steps
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue