docs: streamable server sample (#595)

- add a simple streamable http server example with auth
- update readme: replace quickstart example from sse server to
streamable server
- add readme files for samples, simple-streamable-server, notebooks

closes #170 

## How Has This Been Tested?
knit and inspector

## 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] 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
- [x] I have added or updated documentation as needed
This commit is contained in:
Pavel Gorgulov 2026-03-10 12:45:21 +01:00 • committed by GitHub
commit 0b39f57cff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1085 additions and 254 deletions

View file

@ -180,6 +180,7 @@ jobs:
sample:
- kotlin-mcp-client
- kotlin-mcp-server
- simple-streamable-server
- weather-stdio-server
name: "Build Sample: ${{ matrix.sample }}"

View file

@ -29,6 +29,7 @@ jobs:
sample:
- kotlin-mcp-client
- kotlin-mcp-server
- simple-streamable-server
- weather-stdio-server
name: Build Sample (${{ matrix.sample }})

View file

@ -21,34 +21,34 @@ standardized protocol interface.
* [Overview](#overview)
* [Installation](#installation)
* [Artifacts](#artifacts)
* [Gradle setup (JVM)](#gradle-setup-jvm)
* [Multiplatform](#multiplatform)
* [Ktor dependencies](#ktor-dependencies)
* [Artifacts](#artifacts)
* [Gradle setup (JVM)](#gradle-setup-jvm)
* [Multiplatform](#multiplatform)
* [Ktor dependencies](#ktor-dependencies)
* [Quickstart](#quickstart)
* [Creating a Client](#creating-a-client)
* [Creating a Server](#creating-a-server)
* [Creating a Client](#creating-a-client)
* [Creating a Server](#creating-a-server)
* [Core Concepts](#core-concepts)
* [MCP Primitives](#mcp-primitives)
* [Capabilities](#capabilities)
* [Server Capabilities](#server-capabilities)
* [Client Capabilities](#client-capabilities)
* [Server Features](#server-features)
* [Prompts](#prompts)
* [Resources](#resources)
* [Tools](#tools)
* [Completion](#completion)
* [Logging](#logging)
* [Pagination](#pagination)
* [Client Features](#client-features)
* [Roots](#roots)
* [Sampling](#sampling)
* [MCP Primitives](#mcp-primitives)
* [Capabilities](#capabilities)
* [Server Capabilities](#server-capabilities)
* [Client Capabilities](#client-capabilities)
* [Server Features](#server-features)
* [Prompts](#prompts)
* [Resources](#resources)
* [Tools](#tools)
* [Completion](#completion)
* [Logging](#logging)
* [Pagination](#pagination)
* [Client Features](#client-features)
* [Roots](#roots)
* [Sampling](#sampling)
* [Transports](#transports)
* [STDIO Transport](#stdio-transport)
* [Streamable HTTP Transport](#streamable-http-transport)
* [SSE Transport](#sse-transport)
* [WebSocket Transport](#websocket-transport)
* [ChannelTransport (testing)](#channeltransport-testing)
* [STDIO Transport](#stdio-transport)
* [Streamable HTTP Transport](#streamable-http-transport)
* [SSE Transport](#sse-transport)
* [WebSocket Transport](#websocket-transport)
* [ChannelTransport (testing)](#channeltransport-testing)
* [Connecting your server](#connecting-your-server)
* [Examples](#examples)
* [Documentation](#documentation)
@ -183,17 +183,24 @@ fun main(args: Array<String>) = runBlocking {
### Creating a Server
Create an MCP server that exposes a simple tool and runs on an embedded Ktor server with SSE transport:
Create an MCP server that exposes a simple tool and runs on an embedded Ktor server with Streamable HTTP transport.
For a full working project with all required dependencies, see
the [simple-streamable-server](samples/simple-streamable-server) sample.
<!--- CLEAR -->
```kotlin
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.modelcontextprotocol.kotlin.sdk.server.Server
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
import io.modelcontextprotocol.kotlin.sdk.server.mcp
import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp
import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.McpJson
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
@ -228,7 +235,10 @@ fun main(args: Array<String>) {
}
embeddedServer(CIO, host = "127.0.0.1", port = port) {
mcp {
install(ContentNegotiation) {
json(McpJson)
}
mcpStreamableHttp {
mcpServer
}
}.start(wait = true)
@ -243,7 +253,7 @@ You can run the server and then connect to it using the client or test with the
npx -y @modelcontextprotocol/inspector
```
In the inspector UI, connect to `http://localhost:3000`.
In the inspector UI, connect to `http://localhost:3000/mcp`.
## Core Concepts
@ -787,6 +797,7 @@ private class MyServer :
fun main() {
-->
```kotlin
embeddedServer(CIO, port = 3000) {
mcpStreamableHttp(path = "/api/mcp") {
@ -794,6 +805,7 @@ embeddedServer(CIO, port = 3000) {
}
}.start(wait = true)
```
<!--- SUFFIX
}
-->
@ -831,6 +843,7 @@ private class MyServer :
fun main() {
-->
```kotlin
embeddedServer(CIO, port = 3000) {
install(SSE)
@ -841,6 +854,7 @@ embeddedServer(CIO, port = 3000) {
}
}.start(wait = true)
```
<!--- SUFFIX
}
-->
@ -880,12 +894,9 @@ allowing for easy testing of MCP functionality without the need for network setu
## Examples
| Scenario | Description | Example |
|--------------------------|-----------------------------------------------------------------|--------------------------------------------------------------------------|
| Streamable HTTP server | Full MCP server with prompts, resources, tools, completions | [samples/kotlin-mcp-server](./samples/kotlin-mcp-server) |
| STDIO weather server | Minimal STDIO transport server exposing weather info and alerts | [samples/weather-stdio-server](./samples/weather-stdio-server) |
| Interactive STDIO client | MCP client that connects over STDIO and pipes requests to LLMs | [samples/kotlin-mcp-client](./samples/kotlin-mcp-client) |
| Streamable HTTP client | MCP client demo in a runnable notebook | [samples/notebooks/McpClient.ipynb](./samples/notebooks/McpClient.ipynb) |
The [samples](./samples) directory contains runnable projects demonstrating
MCP server and client implementations with various transports.
See the [samples overview](./samples/README.md) for a comparison table and detailed descriptions.
## Documentation
@ -899,4 +910,5 @@ Please see the [contribution guide](CONTRIBUTING.md) and the [Code of conduct](C
## License
This project is licensed under Apache 2.0 for new contributions, with existing code under MIT—see the [LICENSE](LICENSE) file for details.
This project is licensed under Apache 2.0 for new contributions, with existing code under MIT—see the [LICENSE](LICENSE)
file for details.

View file

@ -6,6 +6,8 @@ plugins {
dependencies {
implementation(project(":kotlin-sdk"))
implementation(libs.ktor.server.cio)
implementation(libs.ktor.serialization)
implementation(libs.ktor.server.content.negotiation)
}
tasks.matching {

55
samples/README.md Normal file
View file

@ -0,0 +1,55 @@
# Kotlin MCP SDK Samples
Runnable projects demonstrating MCP server and client implementations with the
[Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk).
For background on the protocol itself, see the [MCP documentation](https://modelcontextprotocol.io/introduction).
## Overview
| Sample | Type | Transport | MCP Features |
|--------------------------------------------------------|-------------------|-----------------|------------------------------------|
| [simple-streamable-server](./simple-streamable-server) | Server | Streamable HTTP | Tools, Resources, Prompts, Logging |
| [kotlin-mcp-server](./kotlin-mcp-server) | Server | STDIO, SSE | Tools, Resources, Prompts |
| [weather-stdio-server](./weather-stdio-server) | Server | STDIO | Tools |
| [kotlin-mcp-client](./kotlin-mcp-client) | Client | STDIO | Tool discovery & invocation |
| [notebooks](./notebooks) | Client (Notebook) | Streamable HTTP | Tool discovery & invocation |
## Getting Started
- **Building a server?** Start with [simple-streamable-server](./simple-streamable-server) — it
uses the recommended Streamable HTTP transport and covers tools, resources, prompts, and logging.
- **Building a client?** Open the [notebooks](./notebooks) sample for a step-by-step walkthrough,
or see [kotlin-mcp-client](./kotlin-mcp-client) for a full CLI client with Anthropic API
integration.
## Samples
### Simple Streamable HTTP Server
A minimal Streamable HTTP server with optional Bearer token authentication. Demonstrates tools
(`greet`, `multi-greet`), a prompt template, a resource, and server-to-client logging notifications.
[Read more →](./simple-streamable-server)
### Kotlin MCP Server
A multi-transport server supporting STDIO, SSE (plain), and SSE (Ktor plugin). Useful for exploring
different transport modes side by side.
[Read more →](./kotlin-mcp-server)
### Weather STDIO Server
A focused STDIO server that exposes weather forecast and alert tools backed by the weather.gov API.
Includes Claude Desktop integration instructions.
[Read more →](./weather-stdio-server)
### Kotlin MCP Client
An interactive CLI client that connects to any MCP server over STDIO and routes queries through
Anthropic's Claude API, bridging MCP tools with LLM conversations.
[Read more →](./kotlin-mcp-client)
### MCP Client Notebook
A Kotlin notebook that connects to a remote MCP server via Streamable HTTP and demonstrates ping,
tool listing, and tool invocation — all in an interactive cell-by-cell format.
[Read more →](./notebooks)

View file

@ -1,69 +1,50 @@
# Kotlin MCP Client
This project demonstrates how to build a Model Context Protocol (MCP) client in Kotlin that interacts with an MCP server
via a STDIO transport layer while leveraging Anthropic's API for natural language processing. The client uses the MCP
Kotlin SDK to communicate with an MCP server that exposes various tools, and it uses Anthropic's API to process user
queries and integrate tool responses into the conversation.
For more information about the MCP SDK and protocol, please refer to
the [MCP documentation](https://modelcontextprotocol.io/introduction).
## Prerequisites
- **Java 17 or later**
- **Gradle** (or the Gradle wrapper provided with the project)
- An Anthropic API key set in your environment variable `ANTHROPIC_API_KEY`
- Basic understanding of MCP concepts and Kotlin programming
An interactive CLI client that connects to any MCP server over STDIO and pipes queries through
Anthropic's Claude API.
## Overview
The client application performs the following tasks:
This sample demonstrates a complete MCP client workflow: launching an MCP server as a subprocess,
discovering its tools, converting them to Anthropic's tool format, and running an interactive chat
loop where Claude can call server tools on behalf of the user.
- **Connecting to an MCP server** —
launches an MCP server process (implemented in JavaScript, Python, or Java) using STDIO transport.
It connects to the server, retrieves available tools, and converts them to Anthropic’s tool format.
- **Processing queries** —
accepts user queries, sends them to Anthropic’s API along with the registered tools, and handles responses.
If the response indicates a tool should be called, it invokes the corresponding MCP tool and continues the
conversation based on the tool’s result.
- **Interactive chat loop** —
runs an interactive command-line loop, allowing users to continuously submit queries and receive responses.
## Prerequisites
## Building and Running
- JDK 17+
- An `ANTHROPIC_API_KEY` environment variable set with a valid Anthropic API key
- An MCP server script to connect to (`.js`, `.py`, or `.jar`)
Use the Gradle wrapper to build the application. In a terminal, run:
## Build & Run
Run the client, passing the path to an MCP server:
```shell
./gradlew clean build
```
# Connect to a JVM server
./gradlew run --args="path/to/server.jar"
To run the client, execute the jar file and provide the path to your MCP server script.
# Connect to a Python server
./gradlew run --args="path/to/server.py"
To run the client with any MCP server:
```shell
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
# Connect to a Node.js server
./gradlew run --args="path/to/build/index.js"
```
> [!NOTE]
> The client uses STDIO transport, so it launches the MCP server as a separate process.
> The client uses STDIO transport, so it launches the MCP server as a subprocess.
> Ensure the server script is executable and is a valid `.js`, `.py`, or `.jar` file.
## Configuration for Anthropic
## MCP Capabilities
Ensure your Anthropic API key is available in your environment:
From the **client** perspective, this sample demonstrates:
```shell
export ANTHROPIC_API_KEY=your_anthropic_api_key_here
```
The client uses `AnthropicOkHttpClient.fromEnv()` to automatically load the API key from `ANTHROPIC_API_KEY` and
`ANTHROPIC_AUTH_TOKEN` environment variables.
- **Tool discovery** — lists tools from the connected server and converts them to Anthropic's tool
format.
- **Tool invocation** — when Claude's response requests a tool call, the client invokes the
corresponding MCP tool and feeds the result back into the conversation.
## Additional Resources
- [MCP Specification](https://spec.modelcontextprotocol.io/)
- [MCP Specification](https://modelcontextprotocol.io/specification/latest)
- [Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk)
- [Anthropic Java SDK](https://github.com/anthropics/anthropic-sdk-java/tree/main)
- [Anthropic Java SDK](https://github.com/anthropics/anthropic-sdk-java)

View file

@ -1,93 +1,85 @@
# MCP Kotlin Server Sample
# Kotlin MCP Server
A sample implementation of an MCP (Model Context Protocol) server in Kotlin that demonstrates different server
configurations and transport methods.
A sample MCP server demonstrating multiple transport modes: STDIO, SSE (plain), and SSE (Ktor
plugin).
## Features
> **Note:** The SSE transport modes are provided for backward compatibility. For new projects,
> consider using the [simple-streamable-server](../simple-streamable-server) sample, which uses the
> recommended Streamable HTTP transport.
- Multiple server operation modes:
- Standard I/O server
- SSE (Server-Sent Events) server with plain configuration
- SSE server using Ktor plugin
- Built-in capabilities for:
- Prompts management
- Resources handling
- Tools integration
## Overview
## Getting Started
This sample registers a prompt, a tool, and a resource, then lets you choose how to expose them.
STDIO mode is the default and is best for process-based clients. The two SSE modes show how to
serve MCP over HTTP using either a manual Ktor routing setup or the built-in `mcp { }` Ktor plugin.
### Running the Server
## Prerequisites
The server defaults [STDIO transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio).
- JDK 17+
- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) (optional, for testing)
You can customize the behavior using command-line arguments.
Logs are printed to [./build/stdout.log](./build/stdout.log)
## Build & Run
#### Standard I/O mode (STDIO):
```bash
./gradlew clean build
```
Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector)
to connect to MCP via STDIO (Click the "▶️ Connect" button):
### STDIO mode (default)
```shell
npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server stdio-server
./gradlew run
```
#### SSE with plain configuration:
Or explicitly:
**NB!: 🐞 This configuration may not work ATM**
```bash
./gradlew run --args="--sse-server 3001"
```
or
```shell
./gradlew clean build
java -jar ./build/libs/kotlin-mcp-server-0.1.0-all.jar --sse-server 3001
./gradlew run --args="--stdio"
```
Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector)
to connect to `http://localhost:3002/` via SSE Transport (Click the "▶️ Connect" button):
Connect with the MCP Inspector:
```shell
npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server sse-server
npx @modelcontextprotocol/inspector --config samples/kotlin-mcp-server/mcp-inspector-config.json --server stdio-server
```
#### SSE with Ktor plugin:
### SSE with Ktor plugin
```bash
```shell
./gradlew run --args="--sse-server-ktor 3002"
```
or
Connect with the MCP Inspector:
```shell
./gradlew clean build
java -jar ./build/libs/kotlin-mcp-server-0.1.0-all.jar --sse-server-ktor 3002
npx @modelcontextprotocol/inspector --config samples/kotlin-mcp-server/mcp-inspector-config.json --server sse-ktor-server
```
Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector)
to connect to `http://localhost:3002/` via SSE transport (Click the "▶️ Connect" button):
### SSE with plain configuration
> **Known issue:** This mode may not work correctly at this time.
```shell
npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server sse-ktor-server
./gradlew run --args="--sse-server 3001"
```
## Server Capabilities
Connect with the MCP Inspector:
- **Prompts**: Supports prompt management with list change notifications
- **Resources**: Includes subscription support and list change notifications
- **Tools**: Supports tool management with list change notifications
```shell
npx @modelcontextprotocol/inspector --config samples/kotlin-mcp-server/mcp-inspector-config.json --server sse-server
```
## Implementation Details
## MCP Capabilities
The server is implemented using:
- Ktor for HTTP server functionality (SSE modes)
- Kotlin coroutines for asynchronous operations
- SSE for real-time communication in web contexts
- Standard I/O for command-line interface and process-based communication
### Tools
## Example Capabilities
| Name | Description |
|-------------------|------------------------------------------------------|
| `kotlin-sdk-tool` | A test tool that returns a "Hello, world!" greeting. |
The sample server demonstrates:
- **Prompt**: "Kotlin Developer" - helps develop small Kotlin applications with a configurable project name
- **Tool**: "kotlin-sdk-tool" - a simple test tool that returns a greeting
- **Resource**: "Web Search" - a placeholder resource demonstrating resource handling
### Prompts
| Name | Description |
|--------------------|------------------------------------------------------------------------------------|
| `Kotlin Developer` | Generates a prompt to develop a small Kotlin application for a given project name. |
### Resources
| Name | URI | Description |
|--------------|-----------------------|---------------------------------------------------------|
| `Web Search` | `https://search.com/` | A placeholder resource demonstrating resource handling. |

View file

@ -0,0 +1,31 @@
# MCP Client Notebook
An interactive Kotlin notebook that demonstrates connecting to a remote MCP server and calling tools.
## Overview
This notebook walks through building an MCP client step by step: creating a Ktor HTTP client,
initializing an MCP `Client`, connecting via `StreamableHttpClientTransport`, and interacting with
the server (ping, list tools, call tools). It connects to the public
[Microsoft Learn MCP Server](https://learn.microsoft.com/api/mcp) as an example.
## Prerequisites
- [Kotlin Jupyter kernel](https://github.com/Kotlin/kotlin-jupyter) **or** IntelliJ IDEA with the
[Kotlin Notebook plugin](https://plugins.jetbrains.com/plugin/16340-kotlin-notebook)
- Internet access (the notebook connects to an external MCP server)
## How to Run
1. Open `McpClient.ipynb` in IntelliJ IDEA (with Kotlin Notebook plugin) or in Jupyter with the
Kotlin kernel installed.
2. Run cells sequentially from top to bottom.
## What It Demonstrates
- Adding MCP SDK dependencies in a notebook environment
- Creating a Ktor `HttpClient` with SSE and logging plugins
- Creating an MCP `Client` and connecting via `StreamableHttpClientTransport`
- Sending a `ping` request
- Listing available tools from the remote server
- Calling a tool (`microsoft_docs_search`) and displaying results

View file

@ -0,0 +1,75 @@
# Simple Streamable HTTP Server
A minimal MCP server using the recommended Streamable HTTP transport with optional Bearer token authentication.
## Overview
This sample demonstrates a Streamable HTTP MCP server built with Ktor. It exposes tools, a prompt
template, and a resource over HTTP, and optionally supports Bearer token authentication. The
`multi-greet` tool showcases server-to-client logging notifications with streaming delays.
## Prerequisites
- JDK 17+
## Build & Run
### Without authentication
```shell
./gradlew run
```
The server starts on `http://localhost:3001/mcp` by default.
Connect with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector):
```shell
npx @modelcontextprotocol/inspector
```
In the Inspector UI, select **Streamable HTTP** transport and enter `http://localhost:3001/mcp`.
Pass a port number as an argument to change it:
```shell
./gradlew run --args="8080"
```
### With authentication
```shell
MCP_AUTH_TOKEN=my-secret ./gradlew run --args="--auth"
```
When `--auth` is passed, clients must include an `Authorization: Bearer <token>` header.
The token is read from the `MCP_AUTH_TOKEN` environment variable (required when `--auth` is used).
### Authentication caveats
This sample is intended **for demonstration only**. In production you should:
- Use a proper identity provider instead of a static token.
- Restrict CORS origins to your deployment domain — the sample allows all origins.
- Serve the endpoint over HTTPS.
## MCP Capabilities
### Tools
| Name | Description |
|------|-------------|
| `greet` | Returns a greeting for a given `name`. |
| `multi-greet` | Sends logging notifications between delayed greetings, demonstrating streaming. |
### Prompts
| Name | Description |
|------|-------------|
| `greeting-template` | Generates a friendly greeting message for a given `name`. |
### Resources
| Name | URI | Description |
|------|-----|-------------|
| `Default Greeting` | `https://example.com/greetings/default` | Returns a static "Hello, world!" text. |

View file

@ -0,0 +1,29 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.shadow)
application
}
group = "org.example"
version = "0.1.0"
application {
mainClass.set("io.modelcontextprotocol.sample.server.MainKt")
}
dependencies {
implementation(dependencies.platform(libs.ktor.bom))
implementation(libs.mcp.kotlin.server)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.auth)
implementation(libs.ktor.server.cors)
implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.server.sse)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.slf4j.simple)
}
kotlin {
jvmToolchain(17)
}

View file

@ -0,0 +1,5 @@
org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.caching=true
#mcp.kotlin.overrideVersion=0.0.1-SNAPSHOT

View file

@ -0,0 +1,22 @@
[versions]
kotlin = "2.2.21"
ktor = "3.2.3"
mcp-kotlin = "0.9.0"
slf4j = "2.0.17"
shadow = "9.2.2"
[libraries]
ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" }
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty" }
ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors" }
ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation" }
ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json" }
ktor-server-auth = { group = "io.ktor", name = "ktor-server-auth" }
ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse" }
mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" }
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }

Binary file not shown.

View file

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

248
samples/simple-streamable-server/gradlew vendored Executable file
View file

@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View file

@ -0,0 +1,93 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1,25 @@
rootProject.name = "simple-streamable-server"
plugins {
// Apply the foojay-resolver plugin to allow automatic download of JDKs
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositories {
mavenLocal()
mavenCentral()
}
versionCatalogs {
create("libs") {
val mcpKotlinVersion = providers.gradleProperty(
"mcp.kotlin.overrideVersion",
).orNull
if (mcpKotlinVersion != null) {
logger.lifecycle("Using the override version $mcpKotlinVersion of MCP Kotlin SDK")
version("mcp-kotlin", mcpKotlinVersion)
}
}
}
}

View file

@ -0,0 +1,28 @@
package io.modelcontextprotocol.sample.server
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
fun main(vararg args: String) {
val authEnabled = args.any { it == "--auth" }
val port = args.firstOrNull { it != "--auth" }?.toIntOrNull() ?: 3001
val authToken = if (authEnabled) {
val envToken = System.getenv("MCP_AUTH_TOKEN")
requireNotNull(envToken) {
"MCP_AUTH_TOKEN environment variable must be set when using --auth"
}
envToken
} else {
null
}
println("Starting MCP Streamable HTTP server on port $port")
println("Use MCP inspector to connect to http://localhost:$port/mcp")
if (authToken != null) {
println("Bearer auth enabled (token sourced from MCP_AUTH_TOKEN)")
}
embeddedServer(Netty, host = "127.0.0.1", port = port) {
configureServer(authToken)
}.start(wait = true)
}

View file

@ -0,0 +1,297 @@
package io.modelcontextprotocol.sample.server
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.install
import io.ktor.server.auth.Authentication
import io.ktor.server.auth.authenticate
import io.ktor.server.auth.bearer
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.request.header
import io.ktor.server.response.respond
import io.ktor.server.routing.delete
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import io.ktor.server.sse.SSE
import io.ktor.server.sse.sse
import io.ktor.util.collections.ConcurrentMap
import io.modelcontextprotocol.kotlin.sdk.server.Server
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
import io.modelcontextprotocol.kotlin.sdk.server.StreamableHttpServerTransport
import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp
import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel
import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification
import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams
import io.modelcontextprotocol.kotlin.sdk.types.McpJson
import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument
import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult
import io.modelcontextprotocol.kotlin.sdk.types.Role
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents
import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations
import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
import kotlinx.coroutines.delay
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import kotlin.time.Duration.Companion.milliseconds
private const val MCP_SESSION_ID_HEADER = "mcp-session-id"
fun Application.configureServer(authToken: String? = null) {
installCors(authEnabled = authToken != null)
install(ContentNegotiation) {
json(McpJson)
}
if (authToken == null) {
mcpStreamableHttp {
createMcpServer()
}
} else {
configureAuthenticatedMcp(authToken)
}
}
private fun Application.configureAuthenticatedMcp(authToken: String) {
install(SSE)
install(Authentication) {
bearer("mcp-bearer") {
authenticate { credential ->
if (credential.token == authToken) {
io.ktor.server.auth.UserIdPrincipal("mcp-client")
} else {
null
}
}
}
}
val transports = ConcurrentMap<String, StreamableHttpServerTransport>()
routing {
authenticate("mcp-bearer") {
route("/mcp") {
sse {
val transport = findTransport(call, transports) ?: return@sse
transport.handleRequest(this, call)
}
post {
val transport = getOrCreateTransport(call, transports) ?: return@post
transport.handleRequest(null, call)
}
delete {
val transport = findTransport(call, transports) ?: return@delete
transport.handleRequest(null, call)
}
}
}
}
}
private suspend fun findTransport(
call: ApplicationCall,
transports: ConcurrentMap<String, StreamableHttpServerTransport>,
): StreamableHttpServerTransport? {
val sessionId = call.request.header(MCP_SESSION_ID_HEADER)
if (sessionId.isNullOrEmpty()) {
call.respond(HttpStatusCode.BadRequest, "Bad Request: No valid session ID provided")
return null
}
val transport = transports[sessionId]
if (transport == null) {
call.respond(HttpStatusCode.NotFound, "Session not found")
return null
}
return transport
}
private suspend fun getOrCreateTransport(
call: ApplicationCall,
transports: ConcurrentMap<String, StreamableHttpServerTransport>,
): StreamableHttpServerTransport? {
val sessionId = call.request.header(MCP_SESSION_ID_HEADER)
if (sessionId != null) {
val transport = transports[sessionId]
if (transport == null) {
call.respond(HttpStatusCode.NotFound, "Session not found")
}
return transport
}
val configuration = StreamableHttpServerTransport.Configuration(
enableJsonResponse = true,
)
val transport = StreamableHttpServerTransport(configuration)
transport.setOnSessionInitialized { initializedSessionId ->
transports[initializedSessionId] = transport
}
transport.setOnSessionClosed { closedSessionId ->
transports.remove(closedSessionId)
}
val server = createMcpServer()
server.onClose {
transport.sessionId?.let { transports.remove(it) }
}
server.createSession(transport)
return transport
}
private fun Application.installCors(authEnabled: Boolean = false) {
install(CORS) {
anyHost() // Don't do this in production if possible. Try to limit it.
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Delete)
allowNonSimpleContentTypes = true
allowHeader("Mcp-Session-Id")
allowHeader("Mcp-Protocol-Version")
exposeHeader("Mcp-Session-Id")
exposeHeader("Mcp-Protocol-Version")
if (authEnabled) {
allowHeader(HttpHeaders.Authorization)
}
}
}
private fun createMcpServer(): Server {
val server = Server(
Implementation(
name = "simple-streamable-http-server",
version = "1.0.0",
),
ServerOptions(
capabilities = ServerCapabilities(
prompts = ServerCapabilities.Prompts(listChanged = true),
resources = ServerCapabilities.Resources(subscribe = true, listChanged = true),
tools = ServerCapabilities.Tools(listChanged = true),
logging = ServerCapabilities.Logging,
),
),
)
// Tool: greet
server.addTool(
name = "greet",
description = "A simple greeting tool",
inputSchema = ToolSchema(
properties = buildJsonObject {
putJsonObject("name") {
put("type", "string")
put("description", "Name to greet")
}
},
required = listOf("name"),
),
) { request ->
val name = request.arguments?.get("name")?.jsonPrimitive?.content ?: "World"
CallToolResult(content = listOf(TextContent("Hello, $name!")))
}
// Tool: multi-greet (demonstrates logging notifications)
server.addTool(
name = "multi-greet",
description = "A tool that sends different greetings with delays between them",
inputSchema = ToolSchema(
properties = buildJsonObject {
putJsonObject("name") {
put("type", "string")
put("description", "Name to greet")
}
},
required = listOf("name"),
),
toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = false),
) { request ->
val name = request.arguments?.get("name")?.jsonPrimitive?.content ?: "World"
sendLoggingMessage(
LoggingMessageNotification(
LoggingMessageNotificationParams(
level = LoggingLevel.Debug,
data = JsonPrimitive("Starting multi-greet for $name")
)
)
)
delay(1000.milliseconds)
sendLoggingMessage(
LoggingMessageNotification(
LoggingMessageNotificationParams(
level = LoggingLevel.Info,
data = JsonPrimitive("Sending first greeting to $name")
)
)
)
delay(1000.milliseconds)
sendLoggingMessage(
LoggingMessageNotification(
LoggingMessageNotificationParams(
level = LoggingLevel.Info,
data = JsonPrimitive("Sending second greeting to $name")
)
)
)
CallToolResult(content = listOf(TextContent("Good morning, $name!")))
}
// Prompt: greeting-template
server.addPrompt(
name = "greeting-template",
description = "A simple greeting prompt template",
arguments = listOf(
PromptArgument(
name = "name",
description = "Name to include in greeting",
required = true,
),
),
) { request ->
val name = request.arguments?.get("name") ?: "World"
GetPromptResult(
messages = listOf(
PromptMessage(
role = Role.User,
content = TextContent("Please greet $name in a friendly manner."),
),
),
)
}
// Resource: greeting-resource
server.addResource(
uri = "https://example.com/greetings/default",
name = "Default Greeting",
description = "A simple greeting resource",
mimeType = "text/plain",
) { request ->
ReadResourceResult(
contents = listOf(
TextResourceContents("Hello, world!", request.uri, "text/plain"),
),
)
}
return server
}

View file

@ -1,140 +1,67 @@
# Kotlin MCP Weather STDIO Server
# Weather STDIO Server
This project demonstrates how to build a Model Context Protocol (MCP) server in Kotlin that provides weather-related
tools by consuming the National Weather Service (weather.gov) API. The server uses STDIO as the transport layer and
leverages the Kotlin MCP SDK to expose weather forecast and alert tools.
A minimal MCP server that exposes weather forecast and alert tools using the National Weather
Service API over STDIO transport.
For more information about the MCP SDK and protocol, please refer to
the [MCP documentation](https://modelcontextprotocol.io/introduction).
## Overview
This sample shows how to build a STDIO-based MCP server in Kotlin. It registers two tools that
query the [weather.gov](https://www.weather.gov/) API — one for weather forecasts by
latitude/longitude and one for active alerts by US state. Because it uses STDIO, the server is
launched as a subprocess by an MCP client or a desktop application like Claude Desktop.
## Prerequisites
- Java 17 or later
- Gradle (or the Gradle wrapper provided with the project)
- Basic understanding of MCP concepts
- Basic understanding of Kotlin and Kotlin ecosystems (sush as kotlinx-serialization, coroutines, ktor)
- JDK 17+
- Internet access (the server calls the weather.gov API at runtime)
## MCP Weather Server
## Build & Run
The project provides:
- A lightweight MCP server built with Kotlin.
- STDIO transport layer implementation for server-client communication.
- Two weather tools:
- **Weather Forecast Tool** — returns details such as temperature, wind information, and a detailed forecast for a
given latitude/longitude.
- **Weather Alerts Tool** — returns active weather alerts for a given US state.
## Building and running
Use the Gradle wrapper to build the application. In a terminal run:
Run the server (it communicates via stdin/stdout):
```shell
./gradlew clean build
./gradlew run
```
To run the server:
### MCP Inspector
Build the fat JAR first, then connect with the
[MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector):
```shell
java -jar build/libs/weather-stdio-server-0.1.0-all.jar
./gradlew build
npx @modelcontextprotocol/inspector -- java -jar samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar
```
> [!NOTE]
> The server uses STDIO transport, so it is typically launched in an environment where the client connects via standard
> input/output.
### Claude Desktop integration
## Tool Implementation
The project registers two MCP tools using the Kotlin MCP SDK. Below is an overview of the core tool implementations:
### 1. Weather Forecast Tool
This tool fetches the weather forecast for a specific latitude and longitude using the `weather.gov` API.
Example tool registration in Kotlin:
```kotlin
server.addTool(
name = "get_forecast",
description = """
Get weather forecast for a specific latitude/longitude
""".trimIndent(),
inputSchema = Tool.Input(
properties = JsonObject(
mapOf(
"latitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))),
"longitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))),
)
),
required = listOf("latitude", "longitude")
)
) { request ->
// Implementation tool
}
```
### 2. Weather Alerts Tool
This tool retrieves active weather alerts for a US state.
Example tool registration in Kotlin:
```kotlin
server.addTool(
name = "get_alerts",
description = """
Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)
""".trimIndent(),
inputSchema = Tool.Input(
properties = JsonObject(
mapOf(
"state" to JsonObject(
mapOf(
"type" to JsonPrimitive("string"),
"description" to JsonPrimitive("Two-letter US state code (e.g. CA, NY)")
)
),
)
),
required = listOf("state")
)
) { request ->
// Implementation tool
}
```
## Client Integration
### Kotlin Client Example
Since the server uses STDIO for transport, the client typically connects via standard input/output streams. A sample
client implementation can be found in the tests, demonstrating how to send tool requests and process responses.
### Claude for Desktop
To integrate with Claude Desktop, add the following configuration to your Claude Desktop settings:
Add the following to your Claude Desktop configuration:
```json
{
"mcpServers": {
"weather": {
"command": "java",
"args": [
"-jar",
"/absolute/path/to/<your-jar-name>.jar"
]
"mcpServers": {
"weather": {
"command": "java",
"args": [
"-jar",
"/absolute/path/to/samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar"
]
}
}
}
}
```
> [!NOTE]
> Replace `/absolute/path/to/<your-jar-name>.jar` with the actual absolute path to your built jar file.
## MCP Capabilities
### Tools
| Name | Description |
|----------------|------------------------------------------------------------------------------------------|
| `get_forecast` | Returns weather forecast for a given `latitude` / `longitude` using the weather.gov API. |
| `get_alerts` | Returns active weather alerts for a two-letter US `state` code (e.g. `CA`, `NY`). |
## Additional Resources
- [MCP Specification](https://spec.modelcontextprotocol.io/)
- [MCP Specification](https://modelcontextprotocol.io/specification/latest)
- [Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk)
- [Ktor Client Documentation](https://ktor.io/docs/welcome.html)
- [Kotlinx Serialization](https://kotlinlang.org/docs/serialization.html)
- [National Weather Service API](https://www.weather.gov/documentation/services-web-api)