diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..ffd98374 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,67 @@ +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '0 4 * * 0' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + matrix: + language: [ java-kotlin ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: manual + + - uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + + - name: Build Kotlin sources + run: | + ./gradlew \ + :kotlin-sdk-core:compileKotlinJvm \ + :kotlin-sdk-client:compileKotlinJvm \ + :kotlin-sdk-server:compileKotlinJvm \ + :kotlin-sdk:compileKotlinJvm \ + :kotlin-sdk-test:compileKotlinJvm \ + -Pkotlin.incremental=false \ + --no-daemon --stacktrace + + - name: Analyze + uses: github/codeql-action/analyze@v3 + with: + category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index b33803c7..0ab75a73 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -3,7 +3,11 @@ name: Validate PR on: workflow_dispatch: pull_request: - types: [auto_merge_enabled] + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: validate-pr: @@ -17,7 +21,7 @@ jobs: java-version: '21' distribution: 'temurin' - name: Setup Gradle - uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 + uses: gradle/actions/setup-gradle@v4 - name: Clean Build with Gradle run: ./gradlew clean build diff --git a/.gitignore b/.gitignore index 389cec6f..4db43814 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ bin/ ### Node.js ### node_modules dist + +### SWE agents ### +.claude/ +.junie/ diff --git a/build.gradle.kts b/build.gradle.kts index ec12f22f..8672d45f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,278 +1,4 @@ -@file:OptIn(ExperimentalWasmDsl::class) - -import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jreleaser.model.Active - -plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.kotlin.atomicfu) - alias(libs.plugins.dokka) - alias(libs.plugins.jreleaser) - `maven-publish` - signing - alias(libs.plugins.kotlinx.binary.compatibility.validator) -} - -group = "io.modelcontextprotocol" -version = "0.6.0" - -val javadocJar by tasks.registering(Jar::class) { - archiveClassifier.set("javadoc") -} - -publishing { - publications.withType(MavenPublication::class).all { - if (name.contains("jvm", ignoreCase = true)) { - artifact(javadocJar) - } - pom.configureMavenCentralMetadata() - signPublicationIfKeyPresent() - } - - repositories { - maven(url = layout.buildDirectory.dir("staging-deploy")) - } -} - -jreleaser { - gitRootSearch = true - strict = true - - signing { - active = Active.ALWAYS - armored = true - artifacts = true - files = true - } - - deploy { - active.set(Active.ALWAYS) - maven { - active.set(Active.ALWAYS) - mavenCentral { - val ossrh by creating { - active.set(Active.ALWAYS) - url.set("https://central.sonatype.com/api/v1/publisher") - applyMavenCentralRules = false - maxRetries = 240 - stagingRepository(layout.buildDirectory.dir("staging-deploy").get().asFile.path) - // workaround: https://github.com/jreleaser/jreleaser/issues/1784 - afterEvaluate { - publishing.publications.forEach { publication -> - if (publication is MavenPublication) { - val pubName = publication.name - - if (!pubName.contains("jvm", ignoreCase = true) - && !pubName.contains("metadata", ignoreCase = true) - && !pubName.contains("kotlinMultiplatform", ignoreCase = true) - ) { - - artifactOverride { - artifactId = when { - pubName.contains("wasm", ignoreCase = true) -> - "${project.name}-wasm-${pubName.lowercase().substringAfter("wasm")}" - - else -> "${project.name}-${pubName.lowercase()}" - } - jar = false - verifyPom = false - sourceJar = false - javadocJar = false - } - } - } - } - } - } - } - } - } - - release { - github { - changelog.enabled = false - skipRelease = true - skipTag = true - overwrite = false - token = "none" - } - } - - checksum { - individual = false - artifacts = false - files = false - } -} - -fun MavenPom.configureMavenCentralMetadata() { - name by project.name - description by "Kotlin implementation of the Model Context Protocol (MCP)" - url by "https://github.com/modelcontextprotocol/kotlin-sdk" - - licenses { - license { - name by "MIT License" - url by "https://github.com/modelcontextprotocol/kotlin-sdk/blob/main/LICENSE" - distribution by "repo" - } - } - - developers { - developer { - id by "Anthropic" - name by "Anthropic Team" - organization by "Anthropic" - organizationUrl by "https://www.anthropic.com" - } - } - - scm { - url by "https://github.com/modelcontextprotocol/kotlin-sdk" - connection by "scm:git:git://github.com/modelcontextprotocol/kotlin-sdk.git" - developerConnection by "scm:git:git@github.com:modelcontextprotocol/kotlin-sdk.git" - } -} - -fun MavenPublication.signPublicationIfKeyPresent() { - val signingKey = project.getSensitiveProperty("GPG_SECRET_KEY") - val signingKeyPassphrase = project.getSensitiveProperty("SIGNING_PASSPHRASE") - - if (!signingKey.isNullOrBlank()) { - the().apply { - useInMemoryPgpKeys(signingKey, signingKeyPassphrase) - - sign(this@signPublicationIfKeyPresent) - } - } -} - -fun Project.getSensitiveProperty(name: String?): String? { - if (name == null) { - error("Expected not null property '$name' for publication repository config") - } - - return project.findProperty(name) as? String - ?: System.getenv(name) - ?: System.getProperty(name) -} - -infix fun Property.by(value: T) { - set(value) -} - -tasks.withType().configureEach { - useJUnitPlatform() -} - -abstract class GenerateLibVersionTask @Inject constructor( - @get:Input val libVersion: String, - @get:OutputDirectory val sourcesDir: File, -) : DefaultTask() { - @TaskAction - fun generate() { - val sourceFile = File(sourcesDir.resolve("io/modelcontextprotocol/kotlin/sdk"), "LibVersion.kt") - - if (!sourceFile.exists()) { - sourceFile.parentFile.mkdirs() - sourceFile.createNewFile() - } - - sourceFile.writeText( - """ - package io.modelcontextprotocol.kotlin.sdk - - public const val LIB_VERSION: String = "$libVersion" - - """.trimIndent() - ) - } -} - -dokka { - moduleName.set("MCP Kotlin SDK") - - dokkaSourceSets.configureEach { - sourceLink { - localDirectory.set(file("src/main/kotlin")) - remoteUrl("https://github.com/modelcontextprotocol/kotlin-sdk") - remoteLineSuffix.set("#L") - documentedVisibilities(VisibilityModifier.Public) - } - } - dokkaPublications.html { - outputDirectory.set(project.layout.projectDirectory.dir("docs")) - } -} - -val sourcesDir = File(project.layout.buildDirectory.asFile.get(), "generated-sources/libVersion") - -val generateLibVersionTask = - tasks.register("generateLibVersion", version.toString(), sourcesDir) - -kotlin { - jvm { - compilerOptions { - jvmTarget = JvmTarget.JVM_1_8 - } - } - - iosArm64() - iosX64() - iosSimulatorArm64() - - js(IR) { - nodejs { - testTask { - useMocha { - timeout = "30s" - } - } - } - } - - wasmJs { - nodejs() - } - - explicitApi = ExplicitApiMode.Strict - - jvmToolchain(21) - - sourceSets { - commonMain { - kotlin.srcDir(generateLibVersionTask.map { it.sourcesDir }) - dependencies { - api(libs.kotlinx.serialization.json) - api(libs.kotlinx.collections.immutable) - api(libs.ktor.client.cio) - api(libs.ktor.server.cio) - api(libs.ktor.server.sse) - api(libs.ktor.server.websockets) - - implementation(libs.kotlin.logging) - } - } - - commonTest { - dependencies { - implementation(libs.kotlin.test) - implementation(libs.ktor.server.test.host) - implementation(libs.kotlinx.coroutines.test) - implementation(libs.kotest.assertions.json) - } - } - - jvmTest { - dependencies { - implementation(libs.ktor.client.mock) - implementation(libs.mockk) - implementation(libs.slf4j.simple) - } - } - } -} +allprojects { + group = "io.modelcontextprotocol" + version = "0.6.0" +} \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..16c0eb9b --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +dependencies { + implementation(libs.kotlin.gradle) + implementation(libs.kotlin.serialization) + implementation(libs.kotlinx.atomicfu.gradle) + implementation(libs.dokka.gradle) + implementation(libs.jreleaser.gradle) +} \ No newline at end of file diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..fa8bc749 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/mcp.dokka.gradle.kts b/buildSrc/src/main/kotlin/mcp.dokka.gradle.kts new file mode 100644 index 00000000..8f912e9a --- /dev/null +++ b/buildSrc/src/main/kotlin/mcp.dokka.gradle.kts @@ -0,0 +1,23 @@ +import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier + +plugins { + id("org.jetbrains.dokka") +} + +dokka { + moduleName.set("MCP Kotlin SDK - ${project.name}") + + dokkaSourceSets.configureEach { + sourceLink { + localDirectory = projectDir.resolve("src") + remoteUrl("https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/${project.name}/src") + remoteLineSuffix = "#L" + } + + documentedVisibilities(VisibilityModifier.Public) + } + + dokkaPublications.html { + outputDirectory = rootProject.layout.projectDirectory.dir("docs/${project.name}") + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts b/buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts new file mode 100644 index 00000000..95c641ec --- /dev/null +++ b/buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts @@ -0,0 +1,66 @@ +import org.jreleaser.model.Active + +plugins { + id("org.jreleaser") + id("mcp.publishing") +} + +jreleaser { + gitRootSearch = true + strict = true + + signing { + active = Active.ALWAYS + armored = true + artifacts = true + files = true + } + + deploy { + active = Active.ALWAYS + maven { + active = Active.ALWAYS + mavenCentral.create("ossrh") { + active = Active.ALWAYS + sign = true + url = "https://central.sonatype.com/api/v1/publisher" + applyMavenCentralRules = false + maxRetries = 240 + stagingRepository(layout.buildDirectory.dir("staging-deploy").get().asFile.path) + + // workaround: https://github.com/jreleaser/jreleaser/issues/1784 + afterEvaluate { + publishing.publications.forEach { publication -> + if (publication is MavenPublication) { + val pubName = publication.name + + if (!pubName.contains("jvm", ignoreCase = true) + && !pubName.contains("metadata", ignoreCase = true) + && !pubName.contains("kotlinMultiplatform", ignoreCase = true) + ) { + artifactOverride { + artifactId = when { + pubName.contains("wasm", ignoreCase = true) -> + "${project.name}-wasm-${pubName.lowercase().substringAfter("wasm")}" + + else -> "${project.name}-${pubName.lowercase()}" + } + jar = false + verifyPom = false + sourceJar = false + javadocJar = false + } + } + } + } + } + } + } + + checksum { + individual = false + artifacts = false + files = false + } + } +} diff --git a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts new file mode 100644 index 00000000..86569842 --- /dev/null +++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts @@ -0,0 +1,50 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + id("org.jetbrains.kotlinx.atomicfu") +} + +// Generation library versions +val generateLibVersion by tasks.registering { + val outputDir = layout.buildDirectory.dir("generated-sources/libVersion") + outputs.dir(outputDir) + + doLast { + val sourceFile = outputDir.get().file("io/modelcontextprotocol/kotlin/sdk/LibVersion.kt").asFile + sourceFile.parentFile.mkdirs() + sourceFile.writeText( + """ + package io.modelcontextprotocol.kotlin.sdk + + public const val LIB_VERSION: String = "${project.version}" + + """.trimIndent() + ) + } +} + +kotlin { + jvm { + compilerOptions.jvmTarget = JvmTarget.JVM_1_8 + } + macosX64(); macosArm64() + linuxX64(); linuxArm64() + mingwX64() + js { nodejs() } + wasmJs { nodejs() } + + explicitApi = ExplicitApiMode.Strict + jvmToolchain(21) + + sourceSets { + commonMain { + kotlin.srcDir(generateLibVersion) + } + } +} diff --git a/buildSrc/src/main/kotlin/mcp.publishing.gradle.kts b/buildSrc/src/main/kotlin/mcp.publishing.gradle.kts new file mode 100644 index 00000000..bef396de --- /dev/null +++ b/buildSrc/src/main/kotlin/mcp.publishing.gradle.kts @@ -0,0 +1,71 @@ +plugins { + `maven-publish` + signing +} + +val javadocJar by tasks.registering(Jar::class) { + archiveClassifier.set("javadoc") +} + +publishing { + publications.withType().configureEach { + if (name.contains("jvm", ignoreCase = true)) { + artifact(javadocJar) + } + + pom { + name = project.name + description = "Kotlin implementation of the Model Context Protocol (MCP)" + url = "https://github.com/modelcontextprotocol/kotlin-sdk" + + licenses { + license { + name = "MIT License" + url = "https://github.com/modelcontextprotocol/kotlin-sdk/blob/main/LICENSE" + distribution = "repo" + } + } + + organization { + name = "Anthropic" + url = "https://www.anthropic.com" + } + + developers { + developer { + id = "JetBrains" + name = "JetBrains Team" + organization = "JetBrains" + organizationUrl = "https://www.jetbrains.com" + } + } + + scm { + url = "https://github.com/modelcontextprotocol/kotlin-sdk" + connection = "scm:git:git://github.com/modelcontextprotocol/kotlin-sdk.git" + developerConnection = "scm:git:git@github.com:modelcontextprotocol/kotlin-sdk.git" + } + } + } + + repositories { + maven { + name = "staging" + url = uri(layout.buildDirectory.dir("staging-deploy")) + } + } +} + +signing { + val gpgKeyName = "GPG_SIGNING_KEY" + val gpgPassphraseName = "SIGNING_PASSPHRASE" + val signingKey = providers.environmentVariable(gpgKeyName) + .orElse(providers.gradleProperty(gpgKeyName)) + val signingPassphrase = providers.environmentVariable(gpgPassphraseName) + .orElse(providers.gradleProperty(gpgPassphraseName)) + + if (signingKey.isPresent) { + useInMemoryPgpKeys(signingKey.get(), signingPassphrase.get()) + sign(publishing.publications) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 7a184e54..a868081a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,11 @@ -kotlin.code.style=official org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 org.gradle.parallel=true org.gradle.caching=true - +# Dokka org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true - +# Kotlin +kotlin.code.style=official kotlin.daemon.jvmargs=-Xmx4G +# MPP +kotlin.mpp.enableCInteropCommonization=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1227aab6..5afb3d7a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,17 +8,31 @@ atomicfu = "0.29.0" serialization = "1.9.0" collections-immutable = "0.4.0" coroutines = "1.10.2" -ktor = "3.2.1" -mockk = "1.14.4" +kotlinx-io = "0.8.0" +ktor = "3.2.2" logging = "7.0.7" jreleaser = "1.19.0" -binaryCompatibilityValidatorPlugin = "0.18.0" +binaryCompatibilityValidatorPlugin = "0.18.1" slf4j = "2.0.17" kotest = "5.9.1" +# Samples +mcp-kotlin = "0.6.0" +anthropic = "0.8.0" +shadow = "8.1.1" + [libraries] +# Plugins +kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } +kotlinx-atomicfu-gradle = { module = "org.jetbrains.kotlinx:atomicfu-gradle-plugin", version.ref = "atomicfu" } +dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } +jreleaser-gradle = { module = "org.jreleaser:jreleaser-gradle-plugin", version.ref = "jreleaser" } + # Kotlinx libraries kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-io-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-core", version.ref = "kotlinx-io" } kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collections-immutable" } kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version.ref = "logging" } @@ -29,18 +43,23 @@ ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", v ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" } # Testing -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" } ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" } -mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } kotest-assertions-json = { group = "io.kotest", name = "kotest-assertions-json", version.ref = "kotest" } +# Samples +mcp-kotlin = { group = "io.modelcontextprotocol", name = "kotlin-sdk", version.ref = "mcp-kotlin" } +anthropic-java = { group = "com.anthropic", name = "anthropic-java", version.ref = "anthropic" } +ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } + [plugins] +kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidatorPlugin" } + +# Samples +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -kotlin-atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" } -dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } -jreleaser = { id = "org.jreleaser", version.ref = "jreleaser"} -kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidatorPlugin" } +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api new file mode 100644 index 00000000..00d80eb4 --- /dev/null +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -0,0 +1,117 @@ +public final class io/modelcontextprotocol/kotlin/sdk/LibVersionKt { + public static final field LIB_VERSION Ljava/lang/String; +} + +public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { + public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun addRoot (Ljava/lang/String;Ljava/lang/String;)V + public final fun addRoots (Ljava/util/List;)V + protected final fun assertCapability (Ljava/lang/String;Ljava/lang/String;)V + protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + public final fun callTool (Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun callTool (Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun complete (Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun complete$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getPrompt (Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun getPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun getServerCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; + public final fun getServerVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; + public final fun listPrompts (Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listPrompts$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun listResourceTemplates (Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listResourceTemplates$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun listResources (Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listResources$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun listTools (Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listTools$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun ping (Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun ping$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun readResource (Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun readResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun removeRoot (Ljava/lang/String;)Z + public final fun removeRoots (Ljava/util/List;)I + public final fun sendRootsListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun setElicitationHandler (Lkotlin/jvm/functions/Function1;)V + public final fun setLoggingLevel (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun setLoggingLevel$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun subscribeResource (Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun subscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun unsubscribeResource (Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun unsubscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/ClientOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Z)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/KtorClientKt { + public static final fun mcpSse-BZiP2OM (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun mcpSse-BZiP2OM$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun mcpSseTransport-5_5nbZA (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/SseClientTransport; + public static synthetic fun mcpSseTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/SseClientTransport; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getProtocolVersion ()Ljava/lang/String; + public final fun getSessionId ()Ljava/lang/String; + public final fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun send$default (Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun setProtocolVersion (Ljava/lang/String;)V + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun terminateSession (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpError : java/lang/Exception { + public fun ()V + public fun (Ljava/lang/Integer;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/Integer;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCode ()Ljava/lang/Integer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensionsKt { + public static final fun mcpStreamableHttp-BZiP2OM (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun mcpStreamableHttp-BZiP2OM$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun mcpStreamableHttpTransport-5_5nbZA (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; + public static synthetic fun mcpStreamableHttpTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport { + public fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensionsKt { + public static final fun mcpWebSocket (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun mcpWebSocket$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun mcpWebSocketTransport (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport; + public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport; +} + diff --git a/kotlin-sdk-client/build.gradle.kts b/kotlin-sdk-client/build.gradle.kts new file mode 100644 index 00000000..ffc83cee --- /dev/null +++ b/kotlin-sdk-client/build.gradle.kts @@ -0,0 +1,43 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + id("mcp.multiplatform") + id("mcp.publishing") + id("mcp.dokka") + id("mcp.jreleaser") + alias(libs.plugins.kotlinx.binary.compatibility.validator) +} + +kotlin { + iosArm64(); iosX64(); iosSimulatorArm64() + watchosX64(); watchosArm64(); watchosSimulatorArm64() + tvosX64(); tvosArm64(); tvosSimulatorArm64() + js { + browser() + nodejs() + } + wasmJs { + browser() + nodejs() + } + + sourceSets { + commonMain { + dependencies { + api(project(":kotlin-sdk-core")) + api(libs.ktor.client.cio) + implementation(libs.kotlin.logging) + } + } + + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.ktor.client.mock) + implementation(libs.kotlinx.coroutines.test) + } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt similarity index 96% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index 59abee5b..75d0b221 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -299,8 +299,8 @@ public open class Client( * @return The completion result returned by the server, or `null` if none. * @throws IllegalStateException If the server does not support prompts or completion. */ - public suspend fun complete(params: CompleteRequest, options: RequestOptions? = null): CompleteResult? { - return request(params, options) + public suspend fun complete(params: CompleteRequest, options: RequestOptions? = null): CompleteResult { + return request(params, options) } /** @@ -322,8 +322,8 @@ public open class Client( * @return The requested prompt details, or `null` if not found. * @throws IllegalStateException If the server does not support prompts. */ - public suspend fun getPrompt(request: GetPromptRequest, options: RequestOptions? = null): GetPromptResult? { - return request(request, options) + public suspend fun getPrompt(request: GetPromptRequest, options: RequestOptions? = null): GetPromptResult { + return request(request, options) } /** @@ -337,8 +337,8 @@ public open class Client( public suspend fun listPrompts( request: ListPromptsRequest = ListPromptsRequest(), options: RequestOptions? = null, - ): ListPromptsResult? { - return request(request, options) + ): ListPromptsResult { + return request(request, options) } /** @@ -352,8 +352,8 @@ public open class Client( public suspend fun listResources( request: ListResourcesRequest = ListResourcesRequest(), options: RequestOptions? = null, - ): ListResourcesResult? { - return request(request, options) + ): ListResourcesResult { + return request(request, options) } /** @@ -367,8 +367,8 @@ public open class Client( public suspend fun listResourceTemplates( request: ListResourceTemplatesRequest, options: RequestOptions? = null, - ): ListResourceTemplatesResult? { - return request(request, options) + ): ListResourceTemplatesResult { + return request(request, options) } /** @@ -382,8 +382,8 @@ public open class Client( public suspend fun readResource( request: ReadResourceRequest, options: RequestOptions? = null, - ): ReadResourceResult? { - return request(request, options) + ): ReadResourceResult { + return request(request, options) } /** @@ -397,7 +397,7 @@ public open class Client( request: SubscribeRequest, options: RequestOptions? = null, ): EmptyRequestResult { - return request(request, options) + return request(request, options) } /** @@ -411,7 +411,7 @@ public open class Client( request: UnsubscribeRequest, options: RequestOptions? = null, ): EmptyRequestResult { - return request(request, options) + return request(request, options) } /** @@ -480,8 +480,8 @@ public open class Client( public suspend fun listTools( request: ListToolsRequest = ListToolsRequest(), options: RequestOptions? = null, - ): ListToolsResult? { - return request(request, options) + ): ListToolsResult { + return request(request, options) } /** diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt similarity index 99% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt index 6584bc12..7b365638 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt @@ -150,6 +150,7 @@ public class StreamableHttpClientTransport( response, onResumptionToken = onResumptionToken, replayMessageId = if (message is JSONRPCRequest) message.id else null ) + else -> { val body = response.bodyAsText() if (response.contentType() == null && body.isBlank()) return diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt diff --git a/src/jvmTest/kotlin/client/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt similarity index 96% rename from src/jvmTest/kotlin/client/StreamableHttpClientTransportTest.kt rename to kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt index ca255f4a..c286eaab 100644 --- a/src/jvmTest/kotlin/client/StreamableHttpClientTransportTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt @@ -1,4 +1,4 @@ -package client +package io.modelcontextprotocol.kotlin.sdk.client import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine @@ -16,14 +16,12 @@ import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.RequestId -import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport import io.modelcontextprotocol.kotlin.sdk.shared.McpJson import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject -import org.junit.jupiter.api.assertDoesNotThrow import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals @@ -149,9 +147,7 @@ class StreamableHttpClientTransportTest { transport.start() // Should not throw for 405 - assertDoesNotThrow { - transport.terminateSession() - } + transport.terminateSession() // Session ID should still be cleared assertNull(transport.sessionId) @@ -174,7 +170,7 @@ class StreamableHttpClientTransportTest { transport.close() } - @Ignore("Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support") + @Ignore //Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support @Test fun testNotificationSchemaE2E() = runTest { val receivedMessages = mutableListOf() @@ -309,7 +305,7 @@ class StreamableHttpClientTransportTest { transport.close() } - @Ignore("Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support") + @Ignore // Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support @Test fun testNotificationWithResumptionToken() = runTest { var resumptionTokenReceived: String? = null diff --git a/api/kotlin-sdk.api b/kotlin-sdk-core/api/kotlin-sdk-core.api similarity index 87% rename from api/kotlin-sdk.api rename to kotlin-sdk-core/api/kotlin-sdk-core.api index 8d356d5c..569dfb3e 100644 --- a/api/kotlin-sdk.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -133,17 +133,45 @@ public final class io/modelcontextprotocol/kotlin/sdk/CallToolResultBase$Default public static fun isError (Lio/modelcontextprotocol/kotlin/sdk/CallToolResultBase;)Ljava/lang/Boolean; } -public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ServerNotification, io/modelcontextprotocol/kotlin/sdk/WithMeta { +public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; public final fun getReason ()Ljava/lang/String; public final fun getRequestId ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; public fun get_meta ()Lkotlinx/serialization/json/JsonObject; @@ -151,18 +179,18 @@ public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification : io public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } @@ -942,7 +970,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/InitializeResult$Companion public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Companion; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification;Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification; + public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final synthetic class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { @@ -960,6 +998,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Co public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/JSONRPCError : io/modelcontextprotocol/kotlin/sdk/JSONRPCMessage { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/ErrorCode;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V @@ -1386,22 +1453,16 @@ public final class io/modelcontextprotocol/kotlin/sdk/LoggingLevel$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification, io/modelcontextprotocol/kotlin/sdk/WithMeta { +public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun component4 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; + public fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; public fun equals (Ljava/lang/Object;)Z - public final fun getData ()Lkotlinx/serialization/json/JsonObject; - public final fun getLevel ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public final fun getLogger ()Ljava/lang/String; public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1421,6 +1482,40 @@ public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonElement; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public final fun getData ()Lkotlinx/serialization/json/JsonElement; + public final fun getLevel ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; + public final fun getLogger ()Ljava/lang/String; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lkotlinx/serialization/json/JsonObject;)V @@ -1587,12 +1682,21 @@ public final class io/modelcontextprotocol/kotlin/sdk/ModelPreferences$Companion public abstract interface class io/modelcontextprotocol/kotlin/sdk/Notification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Notification$Companion; public abstract fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public abstract fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; } public final class io/modelcontextprotocol/kotlin/sdk/Notification$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface class io/modelcontextprotocol/kotlin/sdk/NotificationParams : io/modelcontextprotocol/kotlin/sdk/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/NotificationParams$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/NotificationParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public abstract interface class io/modelcontextprotocol/kotlin/sdk/PaginatedRequest : io/modelcontextprotocol/kotlin/sdk/Request, io/modelcontextprotocol/kotlin/sdk/WithMeta { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PaginatedRequest$Companion; public abstract fun getCursor ()Ljava/lang/String; @@ -1635,10 +1739,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/PingRequest$Companion { public class io/modelcontextprotocol/kotlin/sdk/Progress : io/modelcontextprotocol/kotlin/sdk/ProgressBase { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Progress$Companion; - public synthetic fun (IILjava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V - public fun (ILjava/lang/Double;Ljava/lang/String;)V + public fun (DLjava/lang/Double;Ljava/lang/String;)V + public synthetic fun (IDLjava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V public fun getMessage ()Ljava/lang/String; - public fun getProgress ()I + public fun getProgress ()D public fun getTotal ()Ljava/lang/Double; public static final synthetic fun write$Self (Lio/modelcontextprotocol/kotlin/sdk/Progress;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V } @@ -1661,7 +1765,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/Progress$Companion { public abstract interface class io/modelcontextprotocol/kotlin/sdk/ProgressBase { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ProgressBase$Companion; public abstract fun getMessage ()Ljava/lang/String; - public abstract fun getProgress ()I + public abstract fun getProgress ()D public abstract fun getTotal ()Ljava/lang/Double; } @@ -1669,24 +1773,16 @@ public final class io/modelcontextprotocol/kotlin/sdk/ProgressBase$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ProgressBase, io/modelcontextprotocol/kotlin/sdk/ServerNotification { +public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Companion; - public fun (ILio/modelcontextprotocol/kotlin/sdk/RequestId;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Double;Ljava/lang/String;)V - public synthetic fun (ILio/modelcontextprotocol/kotlin/sdk/RequestId;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Double;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()I - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun component4 ()Ljava/lang/Double; - public final fun component5 ()Ljava/lang/String; - public final fun copy (ILio/modelcontextprotocol/kotlin/sdk/RequestId;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Double;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification;ILio/modelcontextprotocol/kotlin/sdk/RequestId;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Double;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; + public fun (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification;Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; public fun equals (Ljava/lang/Object;)Z - public fun getMessage ()Ljava/lang/String; public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun getProgress ()I - public final fun getProgressToken ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public fun getTotal ()Ljava/lang/Double; - public final fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1706,6 +1802,42 @@ public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Compa public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams, io/modelcontextprotocol/kotlin/sdk/ProgressBase { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$Companion; + public fun (DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()D + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; + public final fun component3 ()Ljava/lang/Double; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun getMessage ()Ljava/lang/String; + public fun getProgress ()D + public final fun getProgressToken ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; + public fun getTotal ()Ljava/lang/Double; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/Prompt { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Prompt$Companion; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V @@ -1763,7 +1895,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/PromptArgument$Companion { public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Companion; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification; + public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final synthetic class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { @@ -1781,6 +1923,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotificat public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/PromptMessage { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptMessage$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContent;)V @@ -2053,7 +2224,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/ResourceContents$Companion public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Companion; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification; + public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { @@ -2071,6 +2252,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotific public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/ResourceReference : io/modelcontextprotocol/kotlin/sdk/Reference { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceReference$Companion; public static final field TYPE Ljava/lang/String; @@ -2133,18 +2343,16 @@ public final class io/modelcontextprotocol/kotlin/sdk/ResourceTemplate$Companion public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification, io/modelcontextprotocol/kotlin/sdk/WithMeta { +public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Companion; - public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; + public fun (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun getUri ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2164,6 +2372,36 @@ public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotificatio public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$Companion; + public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public final fun getUri ()Ljava/lang/String; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/Role : java/lang/Enum { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Role$Companion; public static final field assistant Lio/modelcontextprotocol/kotlin/sdk/Role; @@ -2209,7 +2447,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/Root$Companion { public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Companion; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification; + public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final synthetic class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { @@ -2227,6 +2475,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotificati public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/SamplingMessage { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/SamplingMessage$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal;)V @@ -2558,20 +2835,22 @@ public final class io/modelcontextprotocol/kotlin/sdk/TextResourceContents$Compa public final class io/modelcontextprotocol/kotlin/sdk/Tool { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Tool$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; - public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; - public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)Lio/modelcontextprotocol/kotlin/sdk/Tool; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Tool;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Tool; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; + public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; + public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)Lio/modelcontextprotocol/kotlin/sdk/Tool; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Tool;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Tool; public fun equals (Ljava/lang/Object;)Z public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; public final fun getDescription ()Ljava/lang/String; public final fun getInputSchema ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; public final fun getName ()Ljava/lang/String; public final fun getOutputSchema ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; + public final fun getTitle ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2694,7 +2973,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/ToolAnnotations$Companion public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Companion; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification; + public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final synthetic class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { @@ -2712,15 +3001,47 @@ public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotificatio public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/TypesKt { public static final field JSONRPC_VERSION Ljava/lang/String; public static final field LATEST_PROTOCOL_VERSION Ljava/lang/String; public static final fun getSUPPORTED_PROTOCOL_VERSIONS ()[Ljava/lang/String; + public static final fun toJSON (Lio/modelcontextprotocol/kotlin/sdk/Notification;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCNotification; + public static final fun toJSON (Lio/modelcontextprotocol/kotlin/sdk/Request;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCRequest; } public final class io/modelcontextprotocol/kotlin/sdk/Types_utilKt { public static final fun error (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; public static synthetic fun error$default (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; + public static final fun getEmptyJsonObject ()Lkotlinx/serialization/json/JsonObject; public static final fun ok (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; public static synthetic fun ok$default (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; } @@ -2754,12 +3075,15 @@ public final class io/modelcontextprotocol/kotlin/sdk/UnknownContent$Companion { public final class io/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/ServerNotification, io/modelcontextprotocol/kotlin/sdk/ServerRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Method;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification;Lio/modelcontextprotocol/kotlin/sdk/Method;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification;Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2876,246 +3200,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/WithMeta$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun addRoot (Ljava/lang/String;Ljava/lang/String;)V - public final fun addRoots (Ljava/util/List;)V - protected final fun assertCapability (Ljava/lang/String;Ljava/lang/String;)V - protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public final fun callTool (Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun callTool (Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun complete (Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun complete$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun getPrompt (Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun getPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun getServerCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; - public final fun getServerVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public final fun listPrompts (Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listPrompts$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun listResourceTemplates (Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listResourceTemplates$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun listResources (Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listResources$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun listTools (Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listTools$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun ping (Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun ping$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun readResource (Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun readResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun removeRoot (Ljava/lang/String;)Z - public final fun removeRoots (Ljava/util/List;)I - public final fun sendRootsListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun setElicitationHandler (Lkotlin/jvm/functions/Function1;)V - public final fun setLoggingLevel (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun setLoggingLevel$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun subscribeResource (Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun subscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun unsubscribeResource (Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun unsubscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/ClientOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { - public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Z)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/KtorClientKt { - public static final fun mcpSse-BZiP2OM (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun mcpSse-BZiP2OM$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun mcpSseTransport-5_5nbZA (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/SseClientTransport; - public static synthetic fun mcpSseTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/SseClientTransport; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun getProtocolVersion ()Ljava/lang/String; - public final fun getSessionId ()Ljava/lang/String; - public final fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun send$default (Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun setProtocolVersion (Ljava/lang/String;)V - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun terminateSession (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpError : java/lang/Exception { - public fun ()V - public fun (Ljava/lang/Integer;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/Integer;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getCode ()Ljava/lang/Integer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensionsKt { - public static final fun mcpStreamableHttp-BZiP2OM (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun mcpStreamableHttp-BZiP2OM$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun mcpStreamableHttpTransport-5_5nbZA (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; - public static synthetic fun mcpStreamableHttpTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport { - public fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensionsKt { - public static final fun mcpWebSocket (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun mcpWebSocket$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun mcpWebSocketTransport (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport; - public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt { - public static final fun MCP (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V - public static final fun mcp (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V - public static final fun mcp (Lio/ktor/server/routing/Routing;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V - public static final fun mcp (Lio/ktor/server/routing/Routing;Lkotlin/jvm/functions/Function0;)V -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Prompt; - public final fun component2 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; - public fun equals (Ljava/lang/Object;)Z - public final fun getMessageProvider ()Lkotlin/jvm/functions/Function2; - public final fun getPrompt ()Lio/modelcontextprotocol/kotlin/sdk/Prompt; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredResource { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Resource; - public final fun component2 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; - public fun equals (Ljava/lang/Object;)Z - public final fun getReadHandler ()Lkotlin/jvm/functions/Function2; - public final fun getResource ()Lio/modelcontextprotocol/kotlin/sdk/Resource; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Tool; - public final fun component2 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; - public fun equals (Ljava/lang/Object;)Z - public final fun getHandler ()Lkotlin/jvm/functions/Function2; - public final fun getTool ()Lio/modelcontextprotocol/kotlin/sdk/Tool; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public class io/modelcontextprotocol/kotlin/sdk/server/Server : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;)V - public final fun addPrompt (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V - public final fun addPrompt (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun addPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public final fun addPrompts (Ljava/util/List;)V - public final fun addResource (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun addResource$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public final fun addResources (Ljava/util/List;)V - public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V - public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public final fun addTools (Ljava/util/List;)V - protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public final fun createElicitation (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun getClientCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; - public final fun getClientVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public final fun getPrompts ()Ljava/util/Map; - public final fun getResources ()Ljava/util/Map; - public final fun getTools ()Ljava/util/Map; - public final fun listRoots (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public fun onClose ()V - public final fun onClose (Lkotlin/jvm/functions/Function0;)V - public final fun onInitialized (Lkotlin/jvm/functions/Function0;)V - public final fun ping (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun removePrompt (Ljava/lang/String;)Z - public final fun removePrompts (Ljava/util/List;)I - public final fun removeResource (Ljava/lang/String;)Z - public final fun removeResources (Ljava/util/List;)I - public final fun removeTool (Ljava/lang/String;)Z - public final fun removeTools (Ljava/util/List;)I - public final fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendPromptListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendResourceListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendResourceUpdated (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendToolListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { - public fun (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Z)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/SseServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public fun (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun getSessionId ()Ljava/lang/String; - public final fun handleMessage (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun handlePostMessage (Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensionsKt { - public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V - public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun mcpWebSocket$default (Lio/ktor/server/routing/Route;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public static synthetic fun mcpWebSocket$default (Lio/ktor/server/routing/Route;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public static final fun mcpWebSocketTransport (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V - public static final fun mcpWebSocketTransport (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport { - public fun (Lio/ktor/server/websocket/WebSocketServerSession;)V - public synthetic fun getSession ()Lio/ktor/websocket/WebSocketSession; +public final class io/modelcontextprotocol/kotlin/sdk/internal/Utils_jvmKt { + public static final fun getIODispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; } public abstract class io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport : io/modelcontextprotocol/kotlin/sdk/shared/Transport { @@ -3157,6 +3243,7 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { } public final class io/modelcontextprotocol/kotlin/sdk/shared/ProtocolKt { + public static final field IMPLEMENTATION_NAME Ljava/lang/String; public static final fun getDEFAULT_REQUEST_TIMEOUT ()J public static final fun getMcpJson ()Lkotlinx/serialization/json/Json; } @@ -3177,6 +3264,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer { public final fun readMessage ()Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage; } +public final class io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferKt { + public static final fun serializeMessage (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;)Ljava/lang/String; +} + public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra { public fun ()V } @@ -3213,3 +3304,7 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTran public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransportKt { + public static final field MCP_SUBPROTOCOL Ljava/lang/String; +} + diff --git a/kotlin-sdk-core/build.gradle.kts b/kotlin-sdk-core/build.gradle.kts new file mode 100644 index 00000000..ee8c3477 --- /dev/null +++ b/kotlin-sdk-core/build.gradle.kts @@ -0,0 +1,45 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + id("mcp.multiplatform") + id("mcp.publishing") + id("mcp.dokka") + id("mcp.jreleaser") + alias(libs.plugins.kotlinx.binary.compatibility.validator) +} + +kotlin { + iosArm64(); iosX64(); iosSimulatorArm64() + watchosX64(); watchosArm64(); watchosSimulatorArm64() + tvosX64(); tvosArm64(); tvosSimulatorArm64() + js { + browser() + nodejs() + } + wasmJs { + browser() + nodejs() + } + + sourceSets { + commonMain { + dependencies { + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.io.core) + api(libs.ktor.server.websockets) + api(libs.kotlinx.collections.immutable) + implementation(libs.kotlin.logging) + } + } + + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.kotest.assertions.json) + } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt similarity index 65% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt index 49436f93..d1cbe781 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt @@ -2,4 +2,4 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher -internal expect val IODispatcher: CoroutineDispatcher \ No newline at end of file +public expect val IODispatcher: CoroutineDispatcher diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt similarity index 96% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 06ce6baa..b5f15751 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -36,6 +36,7 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.serializer +import kotlin.collections.get import kotlin.reflect.KType import kotlin.reflect.typeOf import kotlin.time.Duration @@ -43,7 +44,7 @@ import kotlin.time.Duration.Companion.milliseconds private val LOGGER = KotlinLogging.logger { } -internal const val IMPLEMENTATION_NAME = "mcp-ktor" +public const val IMPLEMENTATION_NAME: String = "mcp-ktor" /** * Callback for progress notifications. @@ -292,11 +293,11 @@ public abstract class Protocol( } private fun onProgress(notification: ProgressNotification) { - LOGGER.trace { "Received progress notification: token=${notification.progressToken}, progress=${notification.progress}/${notification.total}" } - val progress = notification.progress - val total = notification.total - val message = notification.message - val progressToken = notification.progressToken + LOGGER.trace { "Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}" } + val progress = notification.params.progress + val total = notification.params.total + val message = notification.params.message + val progressToken = notification.params.progressToken val handler = _progressHandlers.value[progressToken] if (handler == null) { @@ -424,7 +425,12 @@ public abstract class Protocol( _responseHandlers.update { current -> current.remove(messageId) } _progressHandlers.update { current -> current.remove(messageId) } - val notification = CancelledNotification(requestId = messageId, reason = reason.message ?: "Unknown") + val notification = CancelledNotification( + params = CancelledNotification.Params( + requestId = messageId, + reason = reason.message ?: "Unknown" + ) + ) val serialized = JSONRPCNotification( notification.method.value, @@ -433,7 +439,6 @@ public abstract class Protocol( transport.send(serialized) result.completeExceptionally(reason) - Unit } val timeout = options?.timeout ?: DEFAULT_REQUEST_TIMEOUT diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt similarity index 88% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt index ddffaa99..c235e65b 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt @@ -1,11 +1,9 @@ package io.modelcontextprotocol.kotlin.sdk.shared -import io.ktor.utils.io.core.writeFully import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import kotlinx.io.Buffer import kotlinx.io.indexOf import kotlinx.io.readString -import kotlinx.serialization.encodeToString /** * Buffers a continuous stdio stream into discrete JSON-RPC messages. @@ -14,7 +12,7 @@ public class ReadBuffer { private val buffer: Buffer = Buffer() public fun append(chunk: ByteArray) { - buffer.writeFully(chunk) + buffer.write(chunk) } public fun readMessage(): JSONRPCMessage? { @@ -50,7 +48,7 @@ internal fun deserializeMessage(line: String): JSONRPCMessage { return McpJson.decodeFromString(line) } -internal fun serializeMessage(message: JSONRPCMessage): String { +public fun serializeMessage(message: JSONRPCMessage): String { return McpJson.encodeToString(message) + "\n" } diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt similarity index 97% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt index ff601ed3..29e7b866 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt @@ -12,11 +12,10 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.job import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi -internal const val MCP_SUBPROTOCOL = "mcp" +public const val MCP_SUBPROTOCOL: String = "mcp" /** * Abstract class representing a WebSocket transport for the Model Context Protocol (MCP). diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt similarity index 90% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt index 130585bf..8918f5c3 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt @@ -9,6 +9,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.jsonObject @@ -17,11 +18,11 @@ import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.concurrent.atomics.incrementAndFetch import kotlin.jvm.JvmInline -public const val LATEST_PROTOCOL_VERSION: String = "2024-11-05" +public const val LATEST_PROTOCOL_VERSION: String = "2025-03-26" public val SUPPORTED_PROTOCOL_VERSIONS: Array = arrayOf( LATEST_PROTOCOL_VERSION, - "2024-10-07", + "2024-11-05", ) public const val JSONRPC_VERSION: String = "2.0" @@ -124,11 +125,12 @@ public sealed interface Request { * * @return The JSON-RPC request representation. */ -internal fun Request.toJSON(): JSONRPCRequest { - val encoded = JsonObject(McpJson.encodeToJsonElement(this).jsonObject.minus("method")) +public fun Request.toJSON(): JSONRPCRequest { + val fullJson = McpJson.encodeToJsonElement(this).jsonObject + val params = JsonObject(fullJson.filterKeys { it != "method" }) return JSONRPCRequest( method = method.value, - params = encoded, + params = params, jsonrpc = JSONRPC_VERSION, ) } @@ -139,8 +141,7 @@ internal fun Request.toJSON(): JSONRPCRequest { * @return The decoded [Request] or null */ internal fun JSONRPCRequest.fromJSON(): Request { - val requestData = JsonObject(params.jsonObject.plus("method" to JsonPrimitive(method))) - + val requestData = JsonObject(params.jsonObject + ("method" to JsonPrimitive(method))) val deserializer = selectRequestDeserializer(method) return McpJson.decodeFromJsonElement(deserializer, requestData) } @@ -159,6 +160,7 @@ public open class CustomRequest(override val method: Method) : Request @Serializable(with = NotificationPolymorphicSerializer::class) public sealed interface Notification { public val method: Method + public val params: NotificationParams? } /** @@ -166,11 +168,10 @@ public sealed interface Notification { * * @return The JSON-RPC notification representation. */ -internal fun Notification.toJSON(): JSONRPCNotification { - val encoded = JsonObject(McpJson.encodeToJsonElement(this).jsonObject.minus("method")) +public fun Notification.toJSON(): JSONRPCNotification { return JSONRPCNotification( - method.value, - params = encoded + method = method.value, + params = McpJson.encodeToJsonElement(params), ) } @@ -180,7 +181,10 @@ internal fun Notification.toJSON(): JSONRPCNotification { * @return The decoded [Notification]. */ internal fun JSONRPCNotification.fromJSON(): Notification { - val data = JsonObject(params.jsonObject.plus("method" to JsonPrimitive(method))) + val data = buildJsonObject { + put("method", JsonPrimitive(method)) + put("params", params) + } return McpJson.decodeFromJsonElement(data) } @@ -295,6 +299,12 @@ public data class JSONRPCError( val data: JsonObject = EmptyJsonObject, ) : JSONRPCMessage +/** + * Base interface for notification parameters with optional metadata. + */ +@Serializable +public sealed interface NotificationParams : WithMeta + /* Cancellation */ /** * This notification can be sent by either side to indicate that it is cancelling a previously issued request. @@ -307,19 +317,24 @@ public data class JSONRPCError( */ @Serializable public data class CancelledNotification( - /** - * The ID of the request to cancel. - * - * It MUST correspond to the ID of a request previously issued in the same direction. - */ - val requestId: RequestId, - /** - * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. - */ - val reason: String?, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientNotification, ServerNotification, WithMeta { + override val params: Params, +) : ClientNotification, ServerNotification { override val method: Method = Method.Defined.NotificationsCancelled + + @Serializable + public data class Params( + /** + * The ID of the request to cancel. + * + * It MUST correspond to the ID of a request previously issued in the same direction. + */ + val requestId: RequestId, + /** + * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. + */ + val reason: String? = null, + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /* Initialization */ @@ -408,7 +423,7 @@ public sealed interface ServerResult : RequestResult */ @Serializable public data class UnknownMethodRequestOrNotification( - override val method: Method, + override val method: Method, override val params: NotificationParams? = null, ) : ClientNotification, ClientRequest, ServerNotification, ServerRequest /** @@ -506,8 +521,15 @@ public data class InitializeResult( * This notification is sent from the client to the server after initialization has finished. */ @Serializable -public class InitializedNotification : ClientNotification { +public data class InitializedNotification( + override val params: Params = Params(), +) : ClientNotification { override val method: Method = Method.Defined.NotificationsInitialized + + @Serializable + public data class Params( + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /* Ping */ @@ -528,7 +550,7 @@ public sealed interface ProgressBase { /** * The progress thus far. This should increase every time progress is made, even if the total is unknown. */ - public val progress: Int + public val progress: Double /** * Total number of items to a process (or total progress required), if known. @@ -553,7 +575,7 @@ public open class Progress( /** * The progress thus far. This should increase every time progress is made, even if the total is unknown. */ - override val progress: Int, + override val progress: Double, /** * Total number of items to a process (or total progress required), if known. @@ -571,18 +593,32 @@ public open class Progress( */ @Serializable public data class ProgressNotification( - override val progress: Int, - /** - * The progress token, - * which was given in the initial request, - * used to associate this notification with the request that is proceeding. - */ - public val progressToken: ProgressToken, - @Suppress("PropertyName") val _meta: JsonObject = EmptyJsonObject, - override val total: Double?, - override val message: String?, -) : ClientNotification, ServerNotification, ProgressBase { + override val params: Params, +) : ClientNotification, ServerNotification { override val method: Method = Method.Defined.NotificationsProgress + + @Serializable + public data class Params( + /** + * The progress thus far. This should increase every time progress is made, even if the total is unknown. + */ + override val progress: Double, + /** + * The progress token, + * which was given in the initial request, + * used to associate this notification with the request that is proceeding. + */ + val progressToken: ProgressToken, + /** + * Total number of items to process (or total progress required), if known. + */ + override val total: Double? = null, + /** + * An optional message describing the current progress. + */ + override val message: String? = null, + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams, ProgressBase } /* Pagination */ @@ -784,8 +820,15 @@ public class ReadResourceResult( * Servers may issue this without any previous subscription from the client. */ @Serializable -public class ResourceListChangedNotification : ServerNotification { +public data class ResourceListChangedNotification( + override val params: Params = Params(), +) : ServerNotification { override val method: Method = Method.Defined.NotificationsResourcesListChanged + + @Serializable + public data class Params( + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /** @@ -821,13 +864,18 @@ public data class UnsubscribeRequest( */ @Serializable public data class ResourceUpdatedNotification( - /** - * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. - */ - val uri: String, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerNotification, WithMeta { + override val params: Params, +) : ServerNotification { override val method: Method = Method.Defined.NotificationsResourcesUpdated + + @Serializable + public data class Params( + /** + * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. + */ + val uri: String, + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /* Prompts */ @@ -1044,8 +1092,15 @@ public class GetPromptResult( * Servers may issue this without any previous subscription from the client. */ @Serializable -public class PromptListChangedNotification : ServerNotification { +public data class PromptListChangedNotification( + override val params: Params = Params(), +) : ServerNotification { override val method: Method = Method.Defined.NotificationsPromptsListChanged + + @Serializable + public data class Params( + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /* Tools */ @@ -1110,6 +1165,10 @@ public data class Tool( * The name of the tool. */ val name: String, + /** + * The title of the tool. + */ + val title: String?, /** * A human-readable description of the tool. */ @@ -1191,7 +1250,7 @@ public class CallToolResult( ) : CallToolResultBase /** - * [CallToolResult] extended with backwards compatibility to protocol version 2024-10-07. + * [CallToolResult] extended with backwards compatibility to protocol version 2024-11-05. */ @Serializable public class CompatibilityCallToolResult( @@ -1219,8 +1278,15 @@ public data class CallToolRequest( * Servers may issue this without any previous subscription from the client. */ @Serializable -public class ToolListChangedNotification : ServerNotification { +public data class ToolListChangedNotification( + override val params: Params = Params(), +) : ServerNotification { override val method: Method = Method.Defined.NotificationsToolsListChanged + + @Serializable + public data class Params( + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /* Logging */ @@ -1248,22 +1314,27 @@ public enum class LoggingLevel { */ @Serializable public data class LoggingMessageNotification( - /** - * The severity of this log message. - */ - val level: LoggingLevel, + override val params: Params, +) : ServerNotification { + override val method: Method = Method.Defined.NotificationsMessage - /** - * An optional name of the logger issuing this message. - */ - val logger: String? = null, + @Serializable + public data class Params( + /** + * The severity of this log message. + */ + val level: LoggingLevel, + /** + * An optional name of the logger issuing this message. + */ + val logger: String? = null, + /** + * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + */ + val data: JsonElement, + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams - /** - * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. - */ - val data: JsonObject = EmptyJsonObject, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerNotification, WithMeta { /** * A request from the client to the server to enable or adjust logging. */ @@ -1277,8 +1348,6 @@ public data class LoggingMessageNotification( ) : ClientRequest, WithMeta { override val method: Method = Method.Defined.LoggingSetLevel } - - override val method: Method = Method.Defined.NotificationsMessage } /* Sampling */ @@ -1574,8 +1643,15 @@ public class ListRootsResult( * A notification from the client to the server, informing it that the list of roots has changed. */ @Serializable -public class RootsListChangedNotification : ClientNotification { +public data class RootsListChangedNotification( + override val params: Params = Params(), +) : ClientNotification { override val method: Method = Method.Defined.NotificationsRootsListChanged + + @Serializable + public data class Params( + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /** diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt similarity index 98% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt index 67b6b9f3..570b376a 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt @@ -178,8 +178,8 @@ internal fun selectServerNotificationDeserializer(element: JsonElement): Deseria Method.Defined.NotificationsMessage.value -> LoggingMessageNotification.serializer() Method.Defined.NotificationsResourcesUpdated.value -> ResourceUpdatedNotification.serializer() Method.Defined.NotificationsResourcesListChanged.value -> ResourceListChangedNotification.serializer() - Method.Defined.ToolsList.value -> ToolListChangedNotification.serializer() - Method.Defined.PromptsList.value -> PromptListChangedNotification.serializer() + Method.Defined.NotificationsToolsListChanged.value -> ToolListChangedNotification.serializer() + Method.Defined.NotificationsPromptsListChanged.value -> PromptListChangedNotification.serializer() else -> null } } @@ -279,7 +279,7 @@ internal object JSONRPCMessagePolymorphicSerializer : } } -internal val EmptyJsonObject = JsonObject(emptyMap()) +public val EmptyJsonObject: JsonObject = JsonObject(emptyMap()) public class RequestIdSerializer : KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("RequestId") diff --git a/src/commonTest/kotlin/AudioContentSerializationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt similarity index 95% rename from src/commonTest/kotlin/AudioContentSerializationTest.kt rename to kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt index 6745972c..5b2d8e2b 100644 --- a/src/commonTest/kotlin/AudioContentSerializationTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt @@ -2,7 +2,6 @@ package io.modelcontextprotocol.kotlin.sdk import io.kotest.assertions.json.shouldEqualJson import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlinx.serialization.encodeToString import kotlin.test.Test import kotlin.test.assertEquals diff --git a/src/commonTest/kotlin/CallToolResultUtilsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt similarity index 100% rename from src/commonTest/kotlin/CallToolResultUtilsTest.kt rename to kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt new file mode 100644 index 00000000..0e1f704a --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt @@ -0,0 +1,427 @@ +package io.modelcontextprotocol.kotlin.sdk + +import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlin.test.Test +import kotlin.test.assertEquals + +class ToolSerializationTest { + + // see https://docs.anthropic.com/en/docs/build-with-claude/tool-use + /* language=json */ + private val getWeatherToolJson = """ + { + "name": "get_weather", + "title": "Get weather", + "description": "Get the current weather in a given location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + } + }, + "required": ["location"] + }, + "outputSchema": { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "description": "Temperature in celsius" + }, + "conditions": { + "type": "string", + "description": "Weather conditions description" + }, + "humidity": { + "type": "number", + "description": "Humidity percentage" + } + }, + "required": ["temperature", "conditions", "humidity"] + } + } + """.trimIndent() + + val getWeatherTool = Tool( + name = "get_weather", + title = "Get weather", + description = "Get the current weather in a given location", + annotations = null, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("location", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("The city and state, e.g. San Francisco, CA")) + }) + }, + required = listOf("location") + ), + outputSchema = Tool.Output( + properties = buildJsonObject { + put("temperature", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Temperature in celsius")) + }) + put("conditions", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Weather conditions description")) + }) + put("humidity", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Humidity percentage")) + }) + }, + required = listOf("temperature", "conditions", "humidity") + ) + ) + + //region Serialize + + @Test + fun `should serialize get_weather tool`() { + McpJson.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson + } + + @Test + fun `should always serialize default value`() { + val json = Json(from = McpJson) { + encodeDefaults = false + } + json.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson + } + + @Test + fun `should serialize get_weather tool without optional properties`() { + val weatherTool = createWeatherTool(name = "get_weather") + val expectedJson = createWeatherToolJson(name = "get_weather") + val actualJson = McpJson.encodeToString(weatherTool) + + actualJson shouldEqualJson expectedJson + } + + @Test + fun `should serialize get_weather tool with title optional property specified`() { + val weatherTool = createWeatherTool(name = "get_weather", title = "Get weather") + val expectedJson = createWeatherToolJson(name = "get_weather", title = "Get weather") + val actualJson = McpJson.encodeToString(weatherTool) + + actualJson shouldEqualJson expectedJson + } + + @Test + fun `should serialize get_weather tool with outputSchema optional property specified`() { + val weatherTool = createWeatherTool( + name = "get_weather", + outputSchema = Tool.Output( + properties = buildJsonObject { + put("temperature", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Temperature in celsius")) + }) + put("conditions", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Weather conditions description")) + }) + put("humidity", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Humidity percentage")) + }) + }, + required = listOf("temperature", "conditions", "humidity") + ) + ) + val expectedJson = createWeatherToolJson(name = "get_weather", outputSchema = """ + { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "description": "Temperature in celsius" + }, + "conditions": { + "type": "string", + "description": "Weather conditions description" + }, + "humidity": { + "type": "number", + "description": "Humidity percentage" + } + }, + "required": ["temperature", "conditions", "humidity"] + } + """.trimIndent()) + + val actualJson = McpJson.encodeToString(weatherTool) + + actualJson shouldEqualJson expectedJson + } + + @Test + fun `should serialize get_weather tool with all properties specified`() { + val weatherTool = createWeatherTool( + name = "get_weather", + title = "Get weather", + outputSchema = Tool.Output( + properties = buildJsonObject { + put("temperature", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Temperature in celsius")) + }) + put("conditions", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Weather conditions description")) + }) + put("humidity", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Humidity percentage")) + }) + }, + required = listOf("temperature", "conditions", "humidity") + ) + ) + val expectedJson = createWeatherToolJson( + name = "get_weather", + title = "Get weather", + outputSchema = """ + { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "description": "Temperature in celsius" + }, + "conditions": { + "type": "string", + "description": "Weather conditions description" + }, + "humidity": { + "type": "number", + "description": "Humidity percentage" + } + }, + "required": ["temperature", "conditions", "humidity"] + } + """.trimIndent()) + + val actualJson = McpJson.encodeToString(weatherTool) + + actualJson shouldEqualJson expectedJson + } + + //endregion Serialize + + //region Deserialize + + @Test + fun `should deserialize get_weather tool`() { + val actualTool = McpJson.decodeFromString(getWeatherToolJson) + assertEquals(expected = getWeatherTool, actual = actualTool) + } + + @Test + fun `should deserialize get_weather tool without optional properties`() { + val toolJson = createWeatherToolJson(name = "get_weather") + val expectedTool = createWeatherTool(name = "get_weather") + val actualTool = McpJson.decodeFromString(toolJson) + + assertEquals(expected = expectedTool, actual = actualTool) + } + + @Test + fun `should deserialize get_weather tool with title properties specified`() { + val toolJson = createWeatherToolJson(name = "get_weather", title = "Get weather") + val expectedTool = createWeatherTool(name = "get_weather", title = "Get weather") + + val actualTool = McpJson.decodeFromString(toolJson) + + assertEquals(expected = expectedTool, actual = actualTool) + } + + @Test + fun `should deserialize get_weather tool with outputSchema optional property specified`() { + val toolJson = createWeatherToolJson(name = "get_weather", outputSchema = """ + { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "description": "Temperature in celsius" + }, + "conditions": { + "type": "string", + "description": "Weather conditions description" + }, + "humidity": { + "type": "number", + "description": "Humidity percentage" + } + }, + "required": ["temperature", "conditions", "humidity"] + } + """.trimIndent()) + + val expectedTool = createWeatherTool( + name = "get_weather", + outputSchema = Tool.Output( + properties = buildJsonObject { + put("temperature", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Temperature in celsius")) + }) + put("conditions", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Weather conditions description")) + }) + put("humidity", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Humidity percentage")) + }) + }, + required = listOf("temperature", "conditions", "humidity") + ) + ) + + val actualTool = McpJson.decodeFromString(toolJson) + + assertEquals(expected = expectedTool, actual = actualTool) + } + + @Test + fun `should deserialize get_weather tool with all properties specified`() { + val toolJson = createWeatherToolJson( + name = "get_weather", + title = "Get weather", + outputSchema = """ + { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "description": "Temperature in celsius" + }, + "conditions": { + "type": "string", + "description": "Weather conditions description" + }, + "humidity": { + "type": "number", + "description": "Humidity percentage" + } + }, + "required": ["temperature", "conditions", "humidity"] + } + """.trimIndent()) + + val expectedTool = createWeatherTool( + name = "get_weather", + title = "Get weather", + outputSchema = Tool.Output( + properties = buildJsonObject { + put("temperature", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Temperature in celsius")) + }) + put("conditions", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Weather conditions description")) + }) + put("humidity", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Humidity percentage")) + }) + }, + required = listOf("temperature", "conditions", "humidity") + ) + ) + + val actualTool = McpJson.decodeFromString(toolJson) + + assertEquals(expected = expectedTool, actual = actualTool) + } + + //endregion Deserialize + + //region Private Methods + + private fun createWeatherToolJson( + name: String = "get_weather", + title: String? = null, + outputSchema: String? = null + ): String { + + val stringBuilder = StringBuilder() + + stringBuilder + .appendLine("{") + .append(" \"name\": \"$name\"") + + if (title != null) { + stringBuilder + .appendLine(",") + .append(" \"title\": \"$title\"") + } + + stringBuilder + .appendLine(",") + .append(" \"description\": \"Get the current weather in a given location\"") + .appendLine(",") + .append(""" + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + } + }, + "required": ["location"] + } + """.trimIndent()) + + if (outputSchema != null) { + stringBuilder + .appendLine(",") + .append(""" + "outputSchema": $outputSchema + """.trimIndent()) + } + + stringBuilder + .appendLine() + .appendLine("}") + + + return stringBuilder.toString().trimIndent() + } + + private fun createWeatherTool( + name: String = "get_weather", + title: String? = null, + outputSchema: Tool.Output? = null + ): Tool { + return Tool( + name = name, + title = title, + description = "Get the current weather in a given location", + annotations = null, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("location", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("The city and state, e.g. San Francisco, CA")) + }) + }, + required = listOf("location") + ), + outputSchema = outputSchema + ) + } + + //endregion Private Methods +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt new file mode 100644 index 00000000..819db328 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt @@ -0,0 +1,378 @@ +package io.modelcontextprotocol.kotlin.sdk + +import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +class TypesTest { + + @Test + fun `should have correct latest protocol version`() { + assertNotEquals("", LATEST_PROTOCOL_VERSION) + assertEquals("2025-03-26", LATEST_PROTOCOL_VERSION) + } + + @Test + fun `should have correct supported protocol versions`() { + assertIs>(SUPPORTED_PROTOCOL_VERSIONS) + assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains(LATEST_PROTOCOL_VERSION)) + assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains("2024-11-05")) + assertEquals(2, SUPPORTED_PROTOCOL_VERSIONS.size) + } + + @Test + fun `should validate JSONRPC version constant`() { + assertEquals("2.0", JSONRPC_VERSION) + } + + // Reference Tests + @Test + fun `should validate ResourceReference`() { + val resourceRef = ResourceReference(uri = "file:///path/to/file.txt") + + assertEquals("ref/resource", resourceRef.type) + assertEquals("file:///path/to/file.txt", resourceRef.uri) + } + + @Test + fun `should serialize and deserialize ResourceReference correctly`() { + val resourceRef = ResourceReference(uri = "https://example.com/resource") + + val json = McpJson.encodeToString(resourceRef) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("ref/resource", decoded.type) + assertEquals("https://example.com/resource", decoded.uri) + } + + @Test + fun `should validate PromptReference`() { + val promptRef = PromptReference(name = "greeting") + + assertEquals("ref/prompt", promptRef.type) + assertEquals("greeting", promptRef.name) + } + + @Test + fun `should serialize and deserialize PromptReference correctly`() { + val promptRef = PromptReference(name = "test-prompt") + + val json = McpJson.encodeToString(promptRef) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("ref/prompt", decoded.type) + assertEquals("test-prompt", decoded.name) + } + + @Test + fun `should handle UnknownReference for invalid type`() { + val invalidJson = """{"type": "invalid_type"}""" + + val decoded = McpJson.decodeFromString(invalidJson) + + assertIs(decoded) + assertEquals("invalid_type", decoded.type) + } + + // PromptMessageContent Tests + @Test + fun `should validate text content`() { + val textContent = TextContent(text = "Hello, world!") + + assertEquals("text", textContent.type) + assertEquals("Hello, world!", textContent.text) + } + + @Test + fun `should serialize and deserialize text content correctly`() { + val textContent = TextContent(text = "Test message") + + val json = McpJson.encodeToString(textContent) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("text", decoded.type) + assertEquals("Test message", decoded.text) + } + + @Test + fun `should validate image content`() { + val imageContent = ImageContent( + data = "aGVsbG8=", // base64 encoded "hello" + mimeType = "image/png" + ) + + assertEquals("image", imageContent.type) + assertEquals("aGVsbG8=", imageContent.data) + assertEquals("image/png", imageContent.mimeType) + } + + @Test + fun `should serialize and deserialize image content correctly`() { + val imageContent = ImageContent( + data = "dGVzdA==", // base64 encoded "test" + mimeType = "image/jpeg" + ) + + val json = McpJson.encodeToString(imageContent) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("image", decoded.type) + assertEquals("dGVzdA==", decoded.data) + assertEquals("image/jpeg", decoded.mimeType) + } + + @Test + fun `should validate audio content`() { + val audioContent = AudioContent( + data = "aGVsbG8=", // base64 encoded "hello" + mimeType = "audio/mp3" + ) + + assertEquals("audio", audioContent.type) + assertEquals("aGVsbG8=", audioContent.data) + assertEquals("audio/mp3", audioContent.mimeType) + } + + @Test + fun `should serialize and deserialize audio content correctly`() { + val audioContent = AudioContent( + data = "YXVkaW8=", // base64 encoded "audio" + mimeType = "audio/wav" + ) + + val json = McpJson.encodeToString(audioContent) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("audio", decoded.type) + assertEquals("YXVkaW8=", decoded.data) + assertEquals("audio/wav", decoded.mimeType) + } + + @Test + fun `should validate embedded resource content`() { + val resource = TextResourceContents( + text = "File contents", + uri = "file:///path/to/file.txt", + mimeType = "text/plain" + ) + val embeddedResource = EmbeddedResource(resource = resource) + + assertEquals("resource", embeddedResource.type) + assertEquals(resource, embeddedResource.resource) + } + + @Test + fun `should serialize and deserialize embedded resource content correctly`() { + val resource = BlobResourceContents( + blob = "YmluYXJ5ZGF0YQ==", + uri = "file:///path/to/binary.dat", + mimeType = "application/octet-stream" + ) + val embeddedResource = EmbeddedResource(resource = resource) + + val json = McpJson.encodeToString(embeddedResource) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("resource", decoded.type) + assertIs(decoded.resource) + val decodedBlob = decoded.resource + assertEquals("YmluYXJ5ZGF0YQ==", decodedBlob.blob) + assertEquals("file:///path/to/binary.dat", decodedBlob.uri) + assertEquals("application/octet-stream", decodedBlob.mimeType) + } + + @Test + fun `should handle unknown content type`() { + val unknownJson = """{"type": "unknown_type"}""" + + val decoded = McpJson.decodeFromString(unknownJson) + + assertIs(decoded) + assertEquals("unknown_type", decoded.type) + } + + // PromptMessage Tests + @Test + fun `should validate prompt message with text content`() { + val textContent = TextContent(text = "Hello, assistant!") + val promptMessage = PromptMessage( + role = Role.user, + content = textContent + ) + + assertEquals(Role.user, promptMessage.role) + assertEquals(textContent, promptMessage.content) + assertEquals("text", promptMessage.content.type) + } + + @Test + fun `should validate prompt message with embedded resource`() { + val resource = TextResourceContents( + text = "Primary application entry point", + uri = "file:///project/src/main.rs", + mimeType = "text/x-rust" + ) + val embeddedResource = EmbeddedResource(resource = resource) + val promptMessage = PromptMessage( + role = Role.assistant, + content = embeddedResource + ) + + assertEquals(Role.assistant, promptMessage.role) + assertEquals("resource", promptMessage.content.type) + val content = promptMessage.content as EmbeddedResource + val textResource = content.resource as TextResourceContents + assertEquals("Primary application entry point", textResource.text) + assertEquals("file:///project/src/main.rs", textResource.uri) + assertEquals("text/x-rust", textResource.mimeType) + } + + @Test + fun `should serialize and deserialize prompt message correctly`() { + val imageContent = ImageContent( + data = "aW1hZ2VkYXRh", // base64 encoded "imagedata" + mimeType = "image/png" + ) + val promptMessage = PromptMessage( + role = Role.assistant, + content = imageContent + ) + + val json = McpJson.encodeToString(promptMessage) + val decoded = McpJson.decodeFromString(json) + + assertEquals(Role.assistant, decoded.role) + assertIs(decoded.content) + val decodedContent = decoded.content + assertEquals("aW1hZ2VkYXRh", decodedContent.data) + assertEquals("image/png", decodedContent.mimeType) + } + + // CallToolResult Tests + @Test + fun `should validate tool result with multiple content types`() { + val toolResult = CallToolResult( + content = listOf( + TextContent(text = "Found the following files:"), + EmbeddedResource( + resource = TextResourceContents( + text = "fn main() {}", + uri = "file:///project/src/main.rs", + mimeType = "text/x-rust" + ) + ), + EmbeddedResource( + resource = TextResourceContents( + text = "pub mod lib;", + uri = "file:///project/src/lib.rs", + mimeType = "text/x-rust" + ) + ) + ) + ) + + assertEquals(3, toolResult.content.size) + assertEquals("text", toolResult.content[0].type) + assertEquals("resource", toolResult.content[1].type) + assertEquals("resource", toolResult.content[2].type) + assertEquals(false, toolResult.isError) + } + + @Test + fun `should validate empty content array with default`() { + val toolResult = CallToolResult(content = emptyList()) + + assertEquals(0, toolResult.content.size) + assertEquals(false, toolResult.isError) + } + + @Test + fun `should serialize and deserialize CallToolResult correctly`() { + val toolResult = CallToolResult( + content = listOf( + TextContent(text = "Operation completed"), + ImageContent(data = "aW1hZ2U=", mimeType = "image/png") + ), + isError = false + ) + + val json = McpJson.encodeToString(toolResult) + val decoded = McpJson.decodeFromString(json) + + assertEquals(2, decoded.content.size) + assertIs(decoded.content[0]) + assertIs(decoded.content[1]) + assertEquals(false, decoded.isError) + } + + // CompleteRequest Tests + @Test + fun `should validate CompleteRequest with prompt reference`() { + val request = CompleteRequest( + ref = PromptReference(name = "greeting"), + argument = CompleteRequest.Argument(name = "name", value = "A") + ) + + assertEquals("completion/complete", request.method.value) + assertIs(request.ref) + val promptRef = request.ref + assertEquals("greeting", promptRef.name) + assertEquals("name", request.argument.name) + assertEquals("A", request.argument.value) + } + + @Test + fun `should validate CompleteRequest with resource reference`() { + val request = CompleteRequest( + ref = ResourceReference(uri = "github://repos/{owner}/{repo}"), + argument = CompleteRequest.Argument(name = "repo", value = "t") + ) + + assertEquals("completion/complete", request.method.value) + assertIs(request.ref) + val resourceRef = request.ref + assertEquals("github://repos/{owner}/{repo}", resourceRef.uri) + assertEquals("repo", request.argument.name) + assertEquals("t", request.argument.value) + } + + @Test + fun `should serialize and deserialize CompleteRequest correctly`() { + val request = CompleteRequest( + ref = PromptReference(name = "test"), + argument = CompleteRequest.Argument(name = "arg", value = "") + ) + + val json = McpJson.encodeToString(request) + val decoded = McpJson.decodeFromString(json) + + assertEquals("completion/complete", decoded.method.value) + assertIs(decoded.ref) + val promptRef = decoded.ref + assertEquals("test", promptRef.name) + assertEquals("arg", decoded.argument.name) + assertEquals("", decoded.argument.value) + } + + @Test + fun `should validate CompleteRequest with complex URIs`() { + val request = CompleteRequest( + ref = ResourceReference(uri = "api://v1/{tenant}/{resource}/{id}"), + argument = CompleteRequest.Argument(name = "id", value = "123") + ) + + val resourceRef = request.ref as ResourceReference + assertEquals("api://v1/{tenant}/{resource}/{id}", resourceRef.uri) + assertEquals("id", request.argument.name) + assertEquals("123", request.argument.value) + } +} \ No newline at end of file diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt new file mode 100644 index 00000000..444e4180 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt @@ -0,0 +1,252 @@ +package io.modelcontextprotocol.kotlin.sdk + +import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class TypesUtilTest { + + // ErrorCode Serializer Tests + @Test + fun `should serialize and deserialize ErrorCode Defined correctly`() { + val errorCode: ErrorCode = ErrorCode.Defined.InvalidRequest + + val json = McpJson.encodeToString(errorCode) + val decoded = McpJson.decodeFromString(json) + + assertEquals(ErrorCode.Defined.InvalidRequest, decoded) + assertEquals(-32600, decoded.code) + } + + @Test + fun `should serialize and deserialize ErrorCode Unknown correctly`() { + val errorCode: ErrorCode = ErrorCode.Unknown(1001) + + val json = McpJson.encodeToString(errorCode) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(1001, decoded.code) + } + + // Method Serializer Tests + @Test + fun `should serialize and deserialize Method Defined correctly`() { + val method: Method = Method.Defined.Initialize + + val json = McpJson.encodeToString(method) + val decoded = McpJson.decodeFromString(json) + + assertEquals(Method.Defined.Initialize, decoded) + assertEquals("initialize", decoded.value) + } + + @Test + fun `should serialize and deserialize Method Custom correctly`() { + val method: Method = Method.Custom("custom/method") + + val json = McpJson.encodeToString(method) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("custom/method", decoded.value) + } + + // StopReason Serializer Tests + @Test + fun `should serialize and deserialize StopReason EndTurn correctly`() { + val stopReason: StopReason = StopReason.EndTurn + + val json = McpJson.encodeToString(stopReason) + val decoded = McpJson.decodeFromString(json) + + assertEquals(StopReason.EndTurn, decoded) + assertEquals("endTurn", decoded.value) + } + + @Test + fun `should serialize and deserialize StopReason Other correctly`() { + val stopReason: StopReason = StopReason.Other("custom_reason") + + val json = McpJson.encodeToString(stopReason) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("custom_reason", decoded.value) + } + + // Reference Polymorphic Serializer Tests + @Test + fun `should deserialize ResourceReference polymorphically`() { + val json = """{"type": "ref/resource", "uri": "file:///test.txt"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("ref/resource", decoded.type) + assertEquals("file:///test.txt", decoded.uri) + } + + @Test + fun `should deserialize PromptReference polymorphically`() { + val json = """{"type": "ref/prompt", "name": "test-prompt"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("ref/prompt", decoded.type) + assertEquals("test-prompt", decoded.name) + } + + @Test + fun `should deserialize UnknownReference for invalid type`() { + val json = """{"type": "unknown_ref", "data": "test"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("unknown_ref", decoded.type) + } + + // PromptMessageContent Polymorphic Serializer Tests + @Test + fun `should deserialize TextContent polymorphically`() { + val json = """{"type": "text", "text": "Hello world"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("text", decoded.type) + assertEquals("Hello world", decoded.text) + } + + @Test + fun `should deserialize ImageContent polymorphically`() { + val json = """{"type": "image", "data": "aW1hZ2U=", "mimeType": "image/png"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("image", decoded.type) + assertEquals("aW1hZ2U=", decoded.data) + assertEquals("image/png", decoded.mimeType) + } + + @Test + fun `should deserialize AudioContent polymorphically`() { + val json = """{"type": "audio", "data": "YXVkaW8=", "mimeType": "audio/mp3"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("audio", decoded.type) + assertEquals("YXVkaW8=", decoded.data) + assertEquals("audio/mp3", decoded.mimeType) + } + + @Test + fun `should deserialize EmbeddedResource polymorphically`() { + val json = + """{"type": "resource", "resource": {"uri": "file:///test.txt", "mimeType": "text/plain", "text": "content"}}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("resource", decoded.type) + assertIs(decoded.resource) + val textResource = decoded.resource + assertEquals("file:///test.txt", textResource.uri) + assertEquals("content", textResource.text) + } + + // ResourceContents Polymorphic Serializer Tests + @Test + fun `should deserialize TextResourceContents polymorphically`() { + val json = """{"uri": "file:///test.txt", "mimeType": "text/plain", "text": "file content"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("file:///test.txt", decoded.uri) + assertEquals("file content", decoded.text) + assertEquals("text/plain", decoded.mimeType) + } + + @Test + fun `should deserialize BlobResourceContents polymorphically`() { + val json = """{"uri": "file:///binary.dat", "mimeType": "application/octet-stream", "blob": "YmluYXJ5"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("file:///binary.dat", decoded.uri) + assertEquals("YmluYXJ5", decoded.blob) + assertEquals("application/octet-stream", decoded.mimeType) + } + + @Test + fun `should deserialize UnknownResourceContents for missing fields`() { + val json = """{"uri": "file:///unknown.dat", "mimeType": "unknown/type"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("file:///unknown.dat", decoded.uri) + assertEquals("unknown/type", decoded.mimeType) + } + + // RequestId Serializer Tests + @Test + fun `should serialize and deserialize RequestId StringId correctly`() { + val requestId: RequestId = RequestId.StringId("test-id") + + val json = McpJson.encodeToString(requestId) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("test-id", decoded.value) + } + + @Test + fun `should serialize and deserialize RequestId NumberId correctly`() { + val requestId: RequestId = RequestId.NumberId(42L) + + val json = McpJson.encodeToString(requestId) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(42L, decoded.value) + } + + // Utility Functions Tests + @Test + fun `should create CallToolResult ok correctly`() { + val result = CallToolResult.ok("Success message") + + assertEquals(listOf(TextContent("Success message")), result.content) + assertEquals(false, result.isError) + assertEquals(EmptyJsonObject, result._meta) + } + + @Test + fun `should create CallToolResult error correctly`() { + val result = CallToolResult.error("Error message") + + assertEquals(listOf(TextContent("Error message")), result.content) + assertEquals(true, result.isError) + assertEquals(EmptyJsonObject, result._meta) + } + + @Test + fun `should create CallToolResult with custom meta`() { + val meta = buildJsonObject { put("custom", "value") } + val result = CallToolResult.ok("Success", meta) + + assertEquals(listOf(TextContent("Success")), result.content) + assertEquals(false, result.isError) + assertEquals(meta, result._meta) + } +} \ No newline at end of file diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt similarity index 97% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt rename to kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt index 6890aef6..8e6f4f65 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt @@ -4,7 +4,6 @@ import io.ktor.utils.io.charsets.Charsets import io.ktor.utils.io.core.toByteArray import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.test.Test import kotlin.test.assertEquals diff --git a/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt b/kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt similarity index 61% rename from src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt rename to kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt index 1ecad771..27fee2f7 100644 --- a/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt +++ b/kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt @@ -3,5 +3,5 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -internal actual val IODispatcher: CoroutineDispatcher - get() = Dispatchers.Default \ No newline at end of file +public actual val IODispatcher: CoroutineDispatcher + get() = Dispatchers.Default diff --git a/src/jvmMain/java/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt b/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt similarity index 63% rename from src/jvmMain/java/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt rename to kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt index 2c44eec8..d00c1ab5 100644 --- a/src/jvmMain/java/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt +++ b/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt @@ -3,5 +3,5 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -internal actual val IODispatcher: CoroutineDispatcher - get() = Dispatchers.IO \ No newline at end of file +public actual val IODispatcher: CoroutineDispatcher + get() = Dispatchers.IO diff --git a/src/iosMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.ios.kt b/kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.native.kt similarity index 67% rename from src/iosMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.ios.kt rename to kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.native.kt index 17f6555d..470ae23d 100644 --- a/src/iosMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.ios.kt +++ b/kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.native.kt @@ -4,5 +4,5 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO -internal actual val IODispatcher: CoroutineDispatcher - get() = Dispatchers.IO \ No newline at end of file +public actual val IODispatcher: CoroutineDispatcher + get() = Dispatchers.IO diff --git a/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt b/kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt similarity index 61% rename from src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt rename to kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt index 1ecad771..27fee2f7 100644 --- a/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt +++ b/kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt @@ -3,5 +3,5 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -internal actual val IODispatcher: CoroutineDispatcher - get() = Dispatchers.Default \ No newline at end of file +public actual val IODispatcher: CoroutineDispatcher + get() = Dispatchers.Default diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api new file mode 100644 index 00000000..7e2ed4e1 --- /dev/null +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -0,0 +1,133 @@ +public final class io/modelcontextprotocol/kotlin/sdk/LibVersionKt { + public static final field LIB_VERSION Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt { + public static final fun MCP (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/routing/Routing;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/routing/Routing;Lkotlin/jvm/functions/Function1;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt { + public fun (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Prompt; + public final fun component2 ()Lkotlin/jvm/functions/Function2; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; + public fun equals (Ljava/lang/Object;)Z + public final fun getMessageProvider ()Lkotlin/jvm/functions/Function2; + public final fun getPrompt ()Lio/modelcontextprotocol/kotlin/sdk/Prompt; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredResource { + public fun (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Resource; + public final fun component2 ()Lkotlin/jvm/functions/Function2; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; + public fun equals (Ljava/lang/Object;)Z + public final fun getReadHandler ()Lkotlin/jvm/functions/Function2; + public final fun getResource ()Lio/modelcontextprotocol/kotlin/sdk/Resource; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool { + public fun (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Tool; + public final fun component2 ()Lkotlin/jvm/functions/Function2; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; + public fun equals (Ljava/lang/Object;)Z + public final fun getHandler ()Lkotlin/jvm/functions/Function2; + public final fun getTool ()Lio/modelcontextprotocol/kotlin/sdk/Tool; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class io/modelcontextprotocol/kotlin/sdk/server/Server : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { + public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;)V + public final fun addPrompt (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V + public final fun addPrompt (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun addPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public final fun addPrompts (Ljava/util/List;)V + public final fun addResource (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun addResource$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public final fun addResources (Ljava/util/List;)V + public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V + public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public final fun addTools (Ljava/util/List;)V + protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + public final fun createElicitation (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun getClientCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; + public final fun getClientVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; + public final fun getPrompts ()Ljava/util/Map; + public final fun getResources ()Ljava/util/Map; + public final fun getTools ()Ljava/util/Map; + public final fun listRoots (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun onClose ()V + public final fun onClose (Lkotlin/jvm/functions/Function0;)V + public final fun onInitialized (Lkotlin/jvm/functions/Function0;)V + public final fun ping (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun removePrompt (Ljava/lang/String;)Z + public final fun removePrompts (Ljava/util/List;)I + public final fun removeResource (Ljava/lang/String;)Z + public final fun removeResources (Ljava/util/List;)I + public final fun removeTool (Ljava/lang/String;)Z + public final fun removeTools (Ljava/util/List;)I + public final fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendPromptListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendResourceListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendResourceUpdated (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendToolListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { + public fun (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Z)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/SseServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public fun (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getSessionId ()Ljava/lang/String; + public final fun handleMessage (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun handlePostMessage (Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensionsKt { + public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V + public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun mcpWebSocket$default (Lio/ktor/server/routing/Route;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun mcpWebSocket$default (Lio/ktor/server/routing/Route;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static final fun mcpWebSocketTransport (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V + public static final fun mcpWebSocketTransport (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport { + public fun (Lio/ktor/server/websocket/WebSocketServerSession;)V + public synthetic fun getSession ()Lio/ktor/websocket/WebSocketSession; +} + diff --git a/kotlin-sdk-server/build.gradle.kts b/kotlin-sdk-server/build.gradle.kts new file mode 100644 index 00000000..d107debf --- /dev/null +++ b/kotlin-sdk-server/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("mcp.multiplatform") + id("mcp.publishing") + id("mcp.dokka") + id("mcp.jreleaser") + alias(libs.plugins.kotlinx.binary.compatibility.validator) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(project(":kotlin-sdk-core")) + api(libs.ktor.server.cio) + api(libs.ktor.server.sse) + implementation(libs.kotlin.logging) + } + } + + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.slf4j.simple) + } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt similarity index 88% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt index f3683497..260ef967 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt @@ -19,7 +19,7 @@ import io.ktor.utils.io.KtorDsl private val logger = KotlinLogging.logger {} @KtorDsl -public fun Routing.mcp(path: String, block: () -> Server) { +public fun Routing.mcp(path: String, block: ServerSSESession.() -> Server) { route(path) { mcp(block) } @@ -29,7 +29,7 @@ public fun Routing.mcp(path: String, block: () -> Server) { * Configures the Ktor Application to handle Model Context Protocol (MCP) over Server-Sent Events (SSE). */ @KtorDsl -public fun Routing.mcp(block: () -> Server) { +public fun Routing.mcp(block: ServerSSESession.() -> Server) { val transports = ConcurrentMap() sse { @@ -43,12 +43,12 @@ public fun Routing.mcp(block: () -> Server) { @Suppress("FunctionName") @Deprecated("Use mcp() instead", ReplaceWith("mcp(block)"), DeprecationLevel.WARNING) -public fun Application.MCP(block: () -> Server) { +public fun Application.MCP(block: ServerSSESession.() -> Server) { mcp(block) } @KtorDsl -public fun Application.mcp(block: () -> Server) { +public fun Application.mcp(block: ServerSSESession.() -> Server) { val transports = ConcurrentMap() install(SSE) @@ -67,9 +67,9 @@ public fun Application.mcp(block: () -> Server) { private suspend fun ServerSSESession.mcpSseEndpoint( postEndpoint: String, transports: ConcurrentMap, - block: () -> Server, + block: ServerSSESession.() -> Server, ) { - val transport = mcpSseTransport(postEndpoint, transports) + val transport = mcpSseTransport(postEndpoint, transports) val server = block() diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt similarity index 98% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt index c5b59702..83defa2e 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt @@ -12,7 +12,6 @@ import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.McpJson import kotlinx.coroutines.job -import kotlinx.serialization.encodeToString import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.uuid.ExperimentalUuidApi diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt similarity index 99% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index ecfaeba1..f0655fd9 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -206,6 +206,7 @@ public open class Server( * Registers a single tool. The client can then call this tool. * * @param name The name of the tool. + * @param title An optional human-readable name of the tool for display purposes. * @param description A human-readable description of what the tool does. * @param inputSchema The expected input schema for the tool. * @param outputSchema The optional expected output schema for the tool. @@ -217,11 +218,12 @@ public open class Server( name: String, description: String, inputSchema: Tool.Input = Tool.Input(), + title: String? = null, outputSchema: Tool.Output? = null, toolAnnotations: ToolAnnotations? = null, handler: suspend (CallToolRequest) -> CallToolResult ) { - val tool = Tool(name, description, inputSchema, outputSchema, toolAnnotations) + val tool = Tool(name, title, description, inputSchema, outputSchema, toolAnnotations) addTool(tool, handler) } @@ -556,7 +558,7 @@ public open class Server( * @param params The logging message notification parameters. */ public suspend fun sendLoggingMessage(params: LoggingMessageNotification) { - logger.trace { "Sending logging message: ${params.data}" } + logger.trace { "Sending logging message: ${params.params.data}" } notification(params) } @@ -566,7 +568,7 @@ public open class Server( * @param params Details of the updated resource. */ public suspend fun sendResourceUpdated(params: ResourceUpdatedNotification) { - logger.debug { "Sending resource updated notification for: ${params.uri}" } + logger.debug { "Sending resource updated notification for: ${params.params.uri}" } notification(params) } diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt diff --git a/src/jvmTest/kotlin/server/StdioServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt similarity index 53% rename from src/jvmTest/kotlin/server/StdioServerTransportTest.kt rename to kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt index 8c865aa2..be1e64e8 100644 --- a/src/jvmTest/kotlin/server/StdioServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt @@ -1,23 +1,27 @@ -package server +package io.modelcontextprotocol.kotlin.sdk.server import io.modelcontextprotocol.kotlin.sdk.InitializedNotification import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.PingRequest -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.runBlocking -import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage import io.modelcontextprotocol.kotlin.sdk.toJSON +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import kotlinx.io.Sink import kotlinx.io.Source import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered -import java.io.* +import java.io.ByteArrayOutputStream +import java.io.PipedInputStream +import java.io.PipedOutputStream +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class StdioServerTransportTest { private lateinit var input: PipedInputStream @@ -29,7 +33,7 @@ class StdioServerTransportTest { private lateinit var bufferedInput: Source private lateinit var printOutput: Sink - @BeforeEach + @BeforeTest fun setUp() { // Simulate an input stream that we can push data into using inputWriter. input = PipedInputStream() @@ -72,70 +76,66 @@ class StdioServerTransportTest { } @Test - fun `should not read until started`() { - runBlocking { - val server = StdioServerTransport(bufferedInput, printOutput) - server.onError { error -> - throw error - } + fun `should not read until started`() = runTest { + val server = StdioServerTransport(bufferedInput, printOutput) + server.onError { error -> + throw error + } - var didRead = false - val readMessage = CompletableDeferred() + var didRead = false + val readMessage = CompletableDeferred() - server.onMessage { message -> - didRead = true - readMessage.complete(message) - } + server.onMessage { message -> + didRead = true + readMessage.complete(message) + } - val message = PingRequest().toJSON() + val message = PingRequest().toJSON() - // Push a message before the server started - val serialized = serializeMessage(message) - inputWriter.write(serialized) - inputWriter.flush() + // Push a message before the server started + val serialized = serializeMessage(message) + inputWriter.write(serialized) + inputWriter.flush() - assertFalse(didRead, "Should not have read message before start") + assertFalse(didRead, "Should not have read message before start") - server.start() - val received = readMessage.await() - assertEquals(message, received) - } + server.start() + val received = readMessage.await() + assertEquals(message, received) } @Test - fun `should read multiple messages`() { - runBlocking { - val server = StdioServerTransport(bufferedInput, printOutput) - server.onError { error -> - throw error - } + fun `should read multiple messages`() = runTest { + val server = StdioServerTransport(bufferedInput, printOutput) + server.onError { error -> + throw error + } - val messages = listOf( - PingRequest().toJSON(), - InitializedNotification().toJSON(), - ) + val messages = listOf( + PingRequest().toJSON(), + InitializedNotification().toJSON(), + ) - val readMessages = mutableListOf() - val finished = CompletableDeferred() + val readMessages = mutableListOf() + val finished = CompletableDeferred() - server.onMessage { message -> - readMessages.add(message) - if (message == messages[1]) { - finished.complete(Unit) - } + server.onMessage { message -> + readMessages.add(message) + if (message == messages[1]) { + finished.complete(Unit) } + } - // Push both messages before starting the server - for (m in messages) { - inputWriter.write(serializeMessage(m)) - } - inputWriter.flush() + // Push both messages before starting the server + for (m in messages) { + inputWriter.write(serializeMessage(m)) + } + inputWriter.flush() - server.start() - finished.await() + server.start() + finished.await() - assertEquals(messages, readMessages) - } + assertEquals(messages, readMessages) } } diff --git a/kotlin-sdk-test/build.gradle.kts b/kotlin-sdk-test/build.gradle.kts new file mode 100644 index 00000000..3300e53b --- /dev/null +++ b/kotlin-sdk-test/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("mcp.multiplatform") +} + +kotlin { + jvm { + testRuns["test"].executionTask.configure { + useJUnitPlatform() + } + } + sourceSets { + commonTest { + dependencies { + implementation(project(":kotlin-sdk")) + implementation(kotlin("test")) + implementation(libs.ktor.server.test.host) + implementation(libs.kotlinx.coroutines.test) + } + } + jvmTest { + dependencies { + implementation(kotlin("test-junit5")) + implementation(libs.kotlin.logging) + implementation(libs.ktor.server.cio) + implementation(libs.ktor.client.cio) + } + } + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/client/ClientTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt similarity index 92% rename from src/jvmTest/kotlin/client/ClientTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index 9e2ea055..26330ce1 100644 --- a/src/jvmTest/kotlin/client/ClientTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -1,7 +1,5 @@ -package client +package io.modelcontextprotocol.kotlin.sdk.client -import io.mockk.coEvery -import io.mockk.spyk import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest import io.modelcontextprotocol.kotlin.sdk.CreateElicitationResult @@ -9,7 +7,6 @@ import io.modelcontextprotocol.kotlin.sdk.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.CreateMessageResult import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.InMemoryTransport import io.modelcontextprotocol.kotlin.sdk.InitializeRequest import io.modelcontextprotocol.kotlin.sdk.InitializeResult import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage @@ -31,26 +28,26 @@ import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.Tool -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertInstanceOf -import org.junit.jupiter.api.assertThrows import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertIs import kotlin.test.assertTrue import kotlin.test.fail @@ -200,28 +197,13 @@ class ClientTest { @Test fun `should reject due to non cancellation exception`() = runTest { var closed = false - val clientTransport = object : AbstractTransport() { + val failingTransport = object : AbstractTransport() { override suspend fun start() {} override suspend fun send(message: JSONRPCMessage) { if (message !is JSONRPCRequest) return check(message.method == Method.Defined.Initialize.value) - - val result = InitializeResult( - protocolVersion = LATEST_PROTOCOL_VERSION, - capabilities = ServerCapabilities(), - serverInfo = Implementation( - name = "test", - version = "1.0" - ) - ) - - val response = JSONRPCResponse( - id = message.id, - result = result - ) - - _onMessage.invoke(response) + throw IllegalStateException("Test error") } override suspend fun close() { @@ -229,22 +211,16 @@ class ClientTest { } } - val mockClient = spyk( - Client( - clientInfo = Implementation( - name = "test client", - version = "1.0" - ), - options = ClientOptions() - ) + val client = Client( + clientInfo = Implementation( + name = "test client", + version = "1.0" + ), + options = ClientOptions() ) - coEvery{ - mockClient.request(any()) - } throws IllegalStateException("Test error") - val exception = assertFailsWith { - mockClient.connect(clientTransport) + client.connect(failingTransport) } assertEquals("Error connecting to transport: Test error", exception.message) @@ -265,7 +241,7 @@ class ClientTest { serverOptions ) - server.setRequestHandler(Method.Defined.Initialize) { request, _ -> + server.setRequestHandler(Method.Defined.Initialize) { _, _ -> InitializeResult( protocolVersion = LATEST_PROTOCOL_VERSION, capabilities = ServerCapabilities( @@ -276,11 +252,11 @@ class ClientTest { ) } - server.setRequestHandler(Method.Defined.ResourcesList) { request, _ -> + server.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> ListResourcesResult(resources = emptyList(), nextCursor = null) } - server.setRequestHandler(Method.Defined.ToolsList) { request, _ -> + server.setRequestHandler(Method.Defined.ToolsList) { _, _ -> ListToolsResult(tools = emptyList(), nextCursor = null) } @@ -411,8 +387,10 @@ class ClientTest { } server.sendLoggingMessage( LoggingMessageNotification( - level = LoggingLevel.info, - data = jsonObject + params = LoggingMessageNotification.Params( + level = LoggingLevel.info, + data = jsonObject + ) ) ) server.sendResourceListChanged() @@ -429,23 +407,28 @@ class ClientTest { val server = Server( Implementation(name = "test server", version = "1.0"), ServerOptions( - capabilities = ServerCapabilities(resources = ServerCapabilities.Resources(listChanged = null, subscribe = null)) + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources( + listChanged = null, + subscribe = null + ) + ) ) ) val def = CompletableDeferred() val defTimeOut = CompletableDeferred() - server.setRequestHandler(Method.Defined.ResourcesList) { _, extra -> + server.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> // Simulate delay def.complete(Unit) try { - kotlinx.coroutines.delay(1000) + delay(1000) } catch (e: CancellationException) { defTimeOut.complete(Unit) throw e } - fail("Shouldn't have been called") ListResourcesResult(resources = emptyList()) + fail("Shouldn't have been called") } val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() @@ -486,17 +469,22 @@ class ClientTest { val server = Server( Implementation(name = "test server", version = "1.0"), ServerOptions( - capabilities = ServerCapabilities(resources = ServerCapabilities.Resources(listChanged = null, subscribe = null)) + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources( + listChanged = null, + subscribe = null + ) + ) ) ) - server.setRequestHandler(Method.Defined.ResourcesList) { _, extra -> + server.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> // Simulate a delayed response // Wait ~100ms unless canceled try { - kotlinx.coroutines.withTimeout(100L) { + withTimeout(100L) { // Just delay here, if timeout is 0 on the client side, this won't return in time - kotlinx.coroutines.delay(100) + delay(100) } } catch (_: Exception) { // If aborted, just rethrow or return early @@ -523,7 +511,7 @@ class ClientTest { // Request with 1 msec timeout should fail immediately val ex = assertFailsWith { - kotlinx.coroutines.withTimeout(1) { + withTimeout(1) { client.listResources() } } @@ -544,7 +532,7 @@ class ClientTest { ) ) - client.setRequestHandler(Method.Defined.SamplingCreateMessage) { request, _ -> + client.setRequestHandler(Method.Defined.SamplingCreateMessage) { _, _ -> CreateMessageResult( model = "test-model", role = Role.assistant, @@ -571,7 +559,7 @@ class ClientTest { serverOptions ) - server.setRequestHandler(Method.Defined.Initialize) { request, _ -> + server.setRequestHandler(Method.Defined.Initialize) { _, _ -> InitializeResult( protocolVersion = LATEST_PROTOCOL_VERSION, capabilities = ServerCapabilities( @@ -585,6 +573,7 @@ class ClientTest { tools = listOf( Tool( name = "testTool", + title = "testTool title", description = "testTool description", annotations = null, inputSchema = Tool.Input(), @@ -593,7 +582,7 @@ class ClientTest { ), nextCursor = null ) - server.setRequestHandler(Method.Defined.ToolsList) { request, _ -> + server.setRequestHandler(Method.Defined.ToolsList) { _, _ -> serverListToolsResult } @@ -628,7 +617,7 @@ class ClientTest { ) clientTransport.send(request) - assertInstanceOf(receivedMessage) + assertIs(receivedMessage) val receivedAsResponse = receivedMessage as JSONRPCResponse assertEquals(request.id, receivedAsResponse.id) assertEquals(request.jsonrpc, receivedAsResponse.jsonrpc) @@ -685,7 +674,7 @@ class ClientTest { ) // Verify that adding a root throws an exception - val exception = assertThrows { + val exception = assertFailsWith { client.addRoot(uri = "file:///test-root1", name = "testRoot1") } assertEquals("Client does not support roots capability.", exception.message) @@ -701,7 +690,7 @@ class ClientTest { ) // Verify that removing a root throws an exception - val exception = assertThrows { + val exception = assertFailsWith { client.removeRoot(uri = "file:///test-root1") } assertEquals("Client does not support roots capability.", exception.message) @@ -825,7 +814,7 @@ class ClientTest { ).joinAll() // Verify that creating an elicitation throws an exception - val exception = assertThrows { + val exception = assertFailsWith { server.createElicitation( message = "Please provide your GitHub username", requestedSchema = CreateElicitationRequest.RequestedSchema( diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt similarity index 52% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt index 1c63ff65..23ddadf1 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt @@ -1,46 +1,54 @@ package io.modelcontextprotocol.kotlin.sdk.client import io.ktor.client.HttpClient -import io.ktor.client.plugins.sse.SSE import io.ktor.server.application.install import io.ktor.server.cio.CIO import io.ktor.server.engine.EmbeddedServer import io.ktor.server.engine.embeddedServer -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.util.collections.ConcurrentMap -import io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport -import io.modelcontextprotocol.kotlin.sdk.server.mcpPostEndpoint -import io.modelcontextprotocol.kotlin.sdk.server.mcpSseTransport +import io.modelcontextprotocol.kotlin.sdk.Implementation +import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities +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.shared.BaseTransportTest import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest +import kotlin.test.Ignore import kotlin.test.Test +import io.ktor.client.plugins.sse.SSE as ClientSSE +import io.ktor.server.sse.SSE as ServerSSE class SseTransportTest : BaseTransportTest() { private suspend fun EmbeddedServer<*, *>.actualPort() = engine.resolvedConnectors().first().port + private lateinit var mcpServer: Server + + @BeforeTest + fun setUp() { + mcpServer = Server( + serverInfo = Implementation( + name = "test-server", + version = "1.0" + ), + options = ServerOptions(ServerCapabilities()) + ) + } + @Test fun `should start then close cleanly`() = runTest { val server = embeddedServer(CIO, port = 0) { - install(io.ktor.server.sse.SSE) - val transports = ConcurrentMap() + install(ServerSSE) routing { - sse { - mcpSseTransport("", transports).start() - } - - post { - mcpPostEndpoint(transports) - } + mcp { mcpServer } } }.startSuspend(wait = false) val actualPort = server.actualPort() val client = HttpClient { - install(SSE) + install(ClientSSE) }.mcpSseTransport { url { host = "localhost" @@ -55,32 +63,33 @@ class SseTransportTest : BaseTransportTest() { } } + @Ignore @Test fun `should read messages`() = runTest { val server = embeddedServer(CIO, port = 0) { - install(io.ktor.server.sse.SSE) - val transports = ConcurrentMap() + install(ServerSSE) routing { - sse { - mcpSseTransport("", transports).apply { - onMessage { - send(it) - } - - start() - } - } - - post { - mcpPostEndpoint(transports) - } + mcp { mcpServer } +// sse { +// mcpSseTransport("", transports).apply { +// onMessage { +// send(it) +// } +// +// start() +// } +// } +// +// post { +// mcpPostEndpoint(transports) +// } } }.startSuspend(wait = false) val actualPort = server.actualPort() val client = HttpClient { - install(SSE) + install(ClientSSE) }.mcpSseTransport { url { host = "localhost" @@ -95,34 +104,35 @@ class SseTransportTest : BaseTransportTest() { } } + @Ignore @Test fun `test sse path not root path`() = runTest { val server = embeddedServer(CIO, port = 0) { - install(io.ktor.server.sse.SSE) - val transports = ConcurrentMap() + install(ServerSSE) routing { - route("/sse") { - sse { - mcpSseTransport("", transports).apply { - onMessage { - send(it) - } - - start() - } - } - - post { - mcpPostEndpoint(transports) - } - } + mcp("/sse") { mcpServer } +// route("/sse") { +// sse { +// mcpSseTransport("", transports).apply { +// onMessage { +// send(it) +// } +// +// start() +// } +// } +// +// post { +// mcpPostEndpoint(transports) +// } +// } } }.startSuspend(wait = false) val actualPort = server.actualPort() val client = HttpClient { - install(SSE) + install(ClientSSE) }.mcpSseTransport { url { host = "localhost" diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt similarity index 95% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt index 57423b50..a4bf7a3f 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt @@ -4,6 +4,7 @@ import io.ktor.server.testing.testApplication import io.ktor.server.websocket.WebSockets import io.modelcontextprotocol.kotlin.sdk.server.mcpWebSocket import io.modelcontextprotocol.kotlin.sdk.server.mcpWebSocketTransport +import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest import kotlinx.coroutines.CompletableDeferred import kotlin.test.Ignore import kotlin.test.Test diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/InMemoryTransportTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt similarity index 95% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/InMemoryTransportTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt index 6ab3feaf..24e5261e 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/InMemoryTransportTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt @@ -1,8 +1,8 @@ -package io.modelcontextprotocol.kotlin.sdk.client +package io.modelcontextprotocol.kotlin.sdk.integration -import io.modelcontextprotocol.kotlin.sdk.InMemoryTransport import io.modelcontextprotocol.kotlin.sdk.InitializedNotification import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport import io.modelcontextprotocol.kotlin.sdk.toJSON import kotlinx.coroutines.test.runTest import kotlin.test.BeforeTest diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt similarity index 71% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt index 19d84589..72052518 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt @@ -1,7 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.integration import io.ktor.client.HttpClient -import io.ktor.client.plugins.sse.SSE import io.ktor.server.application.install import io.ktor.server.cio.CIOApplicationEngine import io.ktor.server.engine.EmbeddedServer @@ -20,36 +19,32 @@ import kotlinx.coroutines.withContext import kotlin.test.Test import kotlin.test.fail import io.ktor.client.engine.cio.CIO as ClientCIO +import io.ktor.client.plugins.sse.SSE as ClientSSE import io.ktor.server.cio.CIO as ServerCIO +import io.ktor.server.sse.SSE as ServerSSE + +private const val URL = "127.0.0.1" class SseIntegrationTest { @Test fun `client should be able to connect to sse server`() = runTest { val serverEngine = initServer() - var client: Client? = null try { withContext(Dispatchers.Default) { - assertDoesNotThrow { client = initClient() } + val port = serverEngine.engine.resolvedConnectors().first().port + val client = initClient(port) + client.close() } } catch (e: Exception) { fail("Failed to connect client: $e") } finally { - client?.close() // Make sure to stop the server serverEngine.stopSuspend(1000, 2000) } } - private inline fun assertDoesNotThrow(block: () -> T): T { - return try { - block() - } catch (e: Throwable) { - fail("Expected no exception, but got: $e") - } - } - - private suspend fun initClient(): Client { - return HttpClient(ClientCIO) { install(SSE) }.mcpSse("http://$URL:$PORT") + private suspend fun initClient(port: Int): Client { + return HttpClient(ClientCIO) { install(ClientSSE) }.mcpSse("http://$URL:$port") } private suspend fun initServer(): EmbeddedServer { @@ -58,16 +53,11 @@ class SseIntegrationTest { ServerOptions(capabilities = ServerCapabilities()), ) - return embeddedServer(ServerCIO, host = URL, port = PORT) { - install(io.ktor.server.sse.SSE) + return embeddedServer(ServerCIO, host = URL, port = 0) { + install(ServerSSE) routing { mcp { server } } }.startSuspend(wait = false) } - - companion object { - private const val PORT = 3001 - private const val URL = "localhost" - } } \ No newline at end of file diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/BaseTransportTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt similarity index 94% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/BaseTransportTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt index 2c82ff72..6c69a135 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/BaseTransportTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt @@ -1,9 +1,8 @@ -package io.modelcontextprotocol.kotlin.sdk.client +package io.modelcontextprotocol.kotlin.sdk.shared import io.modelcontextprotocol.kotlin.sdk.InitializedNotification import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.PingRequest -import io.modelcontextprotocol.kotlin.sdk.shared.Transport import io.modelcontextprotocol.kotlin.sdk.toJSON import kotlinx.coroutines.CompletableDeferred import kotlin.test.assertEquals diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/InMemoryTransport.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt similarity index 93% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/InMemoryTransport.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt index cd17bfc7..86dc706a 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/InMemoryTransport.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt @@ -1,6 +1,6 @@ -package io.modelcontextprotocol.kotlin.sdk +package io.modelcontextprotocol.kotlin.sdk.shared -import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage /** * In-memory transport for creating clients and servers that talk to each other within the same process. @@ -44,4 +44,4 @@ class InMemoryTransport : AbstractTransport() { other._onMessage.invoke(message) } -} +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/client/StdioClientTransportTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt similarity index 89% rename from src/jvmTest/kotlin/client/StdioClientTransportTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt index 15defaed..a8007a28 100644 --- a/src/jvmTest/kotlin/client/StdioClientTransportTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt @@ -1,8 +1,7 @@ -package client +package io.modelcontextprotocol.kotlin.sdk.client -import io.modelcontextprotocol.kotlin.sdk.client.BaseTransportTest +import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest import kotlinx.coroutines.test.runTest -import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt new file mode 100644 index 00000000..06621a92 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt @@ -0,0 +1,98 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.sse.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.routing.* +import io.modelcontextprotocol.kotlin.sdk.Implementation +import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport +import io.modelcontextprotocol.kotlin.sdk.integration.utils.Retry +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.mcp +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import kotlin.time.Duration.Companion.seconds +import io.ktor.server.cio.CIO as ServerCIO +import io.ktor.server.sse.SSE as ServerSSE + +@Retry(times = 3) +abstract class KotlinTestBase { + + protected val host = "localhost" + protected abstract val port: Int + + protected lateinit var server: Server + protected lateinit var client: Client + protected lateinit var serverEngine: EmbeddedServer<*, *> + + protected abstract fun configureServerCapabilities(): ServerCapabilities + protected abstract fun configureServer() + + @BeforeEach + fun setUp() { + setupServer() + runBlocking { + setupClient() + } + } + + protected suspend fun setupClient() { + val transport = SseClientTransport(HttpClient(CIO) { + install(SSE) + }, "http://$host:$port") + client = Client( + Implementation("test", "1.0"), + ) + client.connect(transport) + } + + protected fun setupServer() { + val capabilities = configureServerCapabilities() + + server = Server( + Implementation(name = "test-server", version = "1.0"), + ServerOptions(capabilities = capabilities) + ) + + configureServer() + + serverEngine = embeddedServer(ServerCIO, host = host, port = port) { + install(ServerSSE) + routing { + mcp { server } + } + }.start(wait = false) + } + + @AfterEach + fun tearDown() { + // close client + if (::client.isInitialized) { + try { + runBlocking { + withTimeout(3.seconds) { + client.close() + } + } + } catch (e: Exception) { + println("Warning: Error during client close: ${e.message}") + } + } + + // stop server + if (::serverEngine.isInitialized) { + try { + serverEngine.stop(500, 1000) + } catch (e: Exception) { + println("Warning: Error during server stop: ${e.message}") + } + } + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptEdgeCasesTest.kt new file mode 100644 index 00000000..f23ffda7 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptEdgeCasesTest.kt @@ -0,0 +1,402 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin + +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class PromptEdgeCasesTest : KotlinTestBase() { + + override val port = 3008 + + private val basicPromptName = "basic-prompt" + private val basicPromptDescription = "A basic prompt for testing" + + private val complexPromptName = "complex-prompt" + private val complexPromptDescription = "A complex prompt with many arguments" + + private val largePromptName = "large-prompt" + private val largePromptDescription = "A very large prompt for testing" + private val largePromptContent = "X".repeat(100_000) // 100KB of data + + private val specialCharsPromptName = "special-chars-prompt" + private val specialCharsPromptDescription = "A prompt with special characters" + private val specialCharsContent = "!@#$%^&*()_+{}|:\"<>?~`-=[]\\;',./\n\t" + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + prompts = ServerCapabilities.Prompts( + listChanged = true + ) + ) + } + + override fun configureServer() { + server.addPrompt( + name = basicPromptName, + description = basicPromptDescription, + arguments = listOf( + PromptArgument( + name = "name", + description = "The name to greet", + required = true + ) + ) + ) { request -> + val name = request.arguments?.get("name") ?: "World" + + GetPromptResult( + description = basicPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Hello, $name!") + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "Greetings, $name! How can I assist you today?") + ) + ) + ) + } + + server.addPrompt( + name = complexPromptName, + description = complexPromptDescription, + arguments = listOf( + PromptArgument(name = "arg1", description = "Argument 1", required = true), + PromptArgument(name = "arg2", description = "Argument 2", required = true), + PromptArgument(name = "arg3", description = "Argument 3", required = true), + PromptArgument(name = "arg4", description = "Argument 4", required = false), + PromptArgument(name = "arg5", description = "Argument 5", required = false), + PromptArgument(name = "arg6", description = "Argument 6", required = false), + PromptArgument(name = "arg7", description = "Argument 7", required = false), + PromptArgument(name = "arg8", description = "Argument 8", required = false), + PromptArgument(name = "arg9", description = "Argument 9", required = false), + PromptArgument(name = "arg10", description = "Argument 10", required = false) + ) + ) { request -> + // validate required arguments + val requiredArgs = listOf("arg1", "arg2", "arg3") + for (argName in requiredArgs) { + if (request.arguments?.get(argName) == null) { + throw IllegalArgumentException("Missing required argument: $argName") + } + } + + val args = mutableMapOf() + for (i in 1..10) { + val argName = "arg$i" + val argValue = request.arguments?.get(argName) + if (argValue != null) { + args[argName] = argValue + } + } + + GetPromptResult( + description = complexPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Arguments: ${args.entries.joinToString { "${it.key}=${it.value}" }}") + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "Received ${args.size} arguments") + ) + ) + ) + } + + // Very large prompt + server.addPrompt( + name = largePromptName, + description = largePromptDescription, + arguments = listOf( + PromptArgument( + name = "size", + description = "Size multiplier", + required = false + ) + ) + ) { request -> + val size = request.arguments?.get("size")?.toIntOrNull() ?: 1 + val content = largePromptContent.repeat(size) + + GetPromptResult( + description = largePromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Generate a large response") + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = content) + ) + ) + ) + } + + server.addPrompt( + name = specialCharsPromptName, + description = specialCharsPromptDescription, + arguments = listOf( + PromptArgument( + name = "special", + description = "Special characters to include", + required = false + ) + ) + ) { request -> + val special = request.arguments?.get("special") ?: specialCharsContent + + GetPromptResult( + description = specialCharsPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Special characters: $special") + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "Received special characters: $special") + ) + ) + ) + } + } + + @Test + fun testBasicPrompt() { + runTest { + val testName = "Alice" + val result = client.getPrompt( + GetPromptRequest( + name = basicPromptName, + arguments = mapOf("name" to testName) + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(basicPromptDescription, result.description, "Prompt description should match") + + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + assertEquals("Hello, $testName!", userContent.text, "User message content should match") + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + assertEquals( + "Greetings, $testName! How can I assist you today?", + assistantContent.text, + "Assistant message content should match" + ) + } + } + + @Test + fun testComplexPromptWithManyArguments() { + runTest { + val arguments = (1..10).associate { i -> "arg$i" to "value$i" } + + val result = client.getPrompt( + GetPromptRequest( + name = complexPromptName, + arguments = arguments + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(complexPromptDescription, result.description, "Prompt description should match") + + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + + // verify all arguments + val text = userContent.text ?: "" + for (i in 1..10) { + assertTrue(text.contains("arg$i=value$i"), "Message should contain arg$i=value$i") + } + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + assertEquals( + "Received 10 arguments", + assistantContent.text, + "Assistant message should indicate 10 arguments" + ) + } + } + + @Test + fun testLargePrompt() { + runTest { + val result = client.getPrompt( + GetPromptRequest( + name = largePromptName, + arguments = mapOf("size" to "1") + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(largePromptDescription, result.description, "Prompt description should match") + + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + val text = assistantContent.text ?: "" + assertEquals(100_000, text.length, "Assistant message should be 100KB in size") + } + } + + @Test + fun testSpecialCharacters() { + runTest { + val result = client.getPrompt( + GetPromptRequest( + name = specialCharsPromptName, + arguments = mapOf("special" to specialCharsContent) + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(specialCharsPromptDescription, result.description, "Prompt description should match") + + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + val userText = userContent.text ?: "" + assertTrue(userText.contains(specialCharsContent), "User message should contain special characters") + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + val assistantText = assistantContent.text ?: "" + assertTrue( + assistantText.contains(specialCharsContent), + "Assistant message should contain special characters" + ) + } + } + + @Test + fun testMissingRequiredArguments() { + runTest { + val exception = assertThrows { + runBlocking { + client.getPrompt( + GetPromptRequest( + name = complexPromptName, + arguments = mapOf("arg4" to "value4", "arg5" to "value5") + ) + ) + } + } + + assertTrue( + exception.message?.contains("arg1") == true || + exception.message?.contains("arg2") == true || + exception.message?.contains("arg3") == true || + exception.message?.contains("required") == true, + "Exception should mention missing required arguments" + ) + } + } + + @Test + fun testConcurrentPromptRequests() { + runTest { + val concurrentCount = 10 + val results = mutableListOf() + + runBlocking { + repeat(concurrentCount) { index -> + launch { + val promptName = when (index % 4) { + 0 -> basicPromptName + 1 -> complexPromptName + 2 -> largePromptName + else -> specialCharsPromptName + } + + val arguments = when (promptName) { + basicPromptName -> mapOf("name" to "User$index") + complexPromptName -> mapOf("arg1" to "v1", "arg2" to "v2", "arg3" to "v3") + largePromptName -> mapOf("size" to "1") + else -> mapOf("special" to "!@#$%^&*()") + } + + val result = client.getPrompt( + GetPromptRequest( + name = promptName, + arguments = arguments + ) + ) + + synchronized(results) { + results.add(result) + } + } + } + } + + assertEquals(concurrentCount, results.size, "All concurrent operations should complete") + + results.forEach { result -> + assertNotNull(result, "Result should not be null") + assertTrue(result.messages.isNotEmpty(), "Result messages should not be empty") + } + } + } + + @Test + fun testNonExistentPrompt() { + runTest { + val nonExistentPromptName = "non-existent-prompt" + + val exception = assertThrows { + runBlocking { + client.getPrompt( + GetPromptRequest( + name = nonExistentPromptName, + arguments = mapOf("name" to "Test") + ) + ) + } + } + + assertTrue( + exception.message?.contains("not found") == true || + exception.message?.contains("does not exist") == true || + exception.message?.contains("unknown") == true || + exception.message?.contains("error") == true, + "Exception should indicate prompt not found" + ) + } + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptIntegrationTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptIntegrationTest.kt new file mode 100644 index 00000000..78616979 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptIntegrationTest.kt @@ -0,0 +1,414 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin + +import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class PromptIntegrationTest : KotlinTestBase() { + + override val port = 3004 + private val testPromptName = "greeting" + private val testPromptDescription = "A simple greeting prompt" + private val complexPromptName = "multimodal-prompt" + private val complexPromptDescription = "A prompt with multiple content types" + private val conversationPromptName = "conversation" + private val conversationPromptDescription = "A prompt with multiple messages and roles" + private val strictPromptName = "strict-prompt" + private val strictPromptDescription = "A prompt with required arguments" + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + prompts = ServerCapabilities.Prompts( + listChanged = true + ) + ) + } + + override fun configureServer() { + // simple prompt with a name parameter + server.addPrompt( + name = testPromptName, + description = testPromptDescription, + arguments = listOf( + PromptArgument( + name = "name", + description = "The name to greet", + required = true, + ) + ) + ) { request -> + val name = request.arguments?.get("name") ?: "World" + + GetPromptResult( + description = testPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Hello, $name!"), + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "Greetings, $name! How can I assist you today?"), + ) + ) + ) + } + + // prompt with multiple content types + server.addPrompt( + name = complexPromptName, + description = complexPromptDescription, + arguments = listOf( + PromptArgument( + name = "topic", + description = "The topic to discuss", + required = false, + ), + PromptArgument( + name = "includeImage", + description = "Whether to include an image", + required = false, + ) + ) + ) { request -> + val topic = request.arguments?.get("topic") ?: "general knowledge" + val includeImage = request.arguments?.get("includeImage")?.toBoolean() ?: true + + val messages = mutableListOf() + + messages.add( + PromptMessage( + role = Role.user, + content = TextContent(text = "I'd like to discuss $topic."), + ) + ) + + val assistantContents = mutableListOf() + assistantContents.add(TextContent(text = "I'd be happy to discuss $topic with you.")) + + if (includeImage) { + assistantContents.add( + ImageContent( + data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", + mimeType = "image/png", + ) + ) + } + + messages.add( + PromptMessage( + role = Role.assistant, + content = assistantContents[0], + ) + ) + + GetPromptResult( + description = complexPromptDescription, + messages = messages, + ) + } + + // prompt with multiple messages and roles + server.addPrompt( + name = conversationPromptName, + description = conversationPromptDescription, + arguments = listOf( + PromptArgument( + name = "topic", + description = "The topic of the conversation", + required = false, + ) + ) + ) { request -> + val topic = request.arguments?.get("topic") ?: "weather" + + GetPromptResult( + description = conversationPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Let's talk about the $topic."), + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "Sure, I'd love to discuss the $topic. What would you like to know?"), + ), + PromptMessage( + role = Role.user, + content = TextContent(text = "What's your opinion on the $topic?"), + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "As an AI, I don't have personal opinions, but I can provide information about $topic."), + ), + PromptMessage( + role = Role.user, + content = TextContent(text = "That's helpful, thank you!"), + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "You're welcome! Let me know if you have more questions about $topic."), + ), + ) + ) + } + + // prompt with strict required arguments + server.addPrompt( + name = strictPromptName, + description = strictPromptDescription, + arguments = listOf( + PromptArgument( + name = "requiredArg1", + description = "First required argument", + required = true, + ), + PromptArgument( + name = "requiredArg2", + description = "Second required argument", + required = true, + ), + PromptArgument( + name = "optionalArg", + description = "Optional argument", + required = false, + ), + ) + ) { request -> + val args = request.arguments ?: emptyMap() + val arg1 = args["requiredArg1"] ?: throw IllegalArgumentException("Missing required argument: requiredArg1") + val arg2 = args["requiredArg2"] ?: throw IllegalArgumentException("Missing required argument: requiredArg2") + val optArg = args["optionalArg"] ?: "default" + + GetPromptResult( + description = strictPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Required arguments: $arg1, $arg2. Optional: $optArg"), + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "I received your arguments: $arg1, $arg2, and $optArg"), + ), + ) + ) + } + } + + @Test + fun testListPrompts() = runTest { + val result = client.listPrompts() + + assertNotNull(result, "List prompts result should not be null") + assertTrue(result.prompts.isNotEmpty(), "Prompts list should not be empty") + + val testPrompt = result.prompts.find { it.name == testPromptName } + assertNotNull(testPrompt, "Test prompt should be in the list") + assertEquals(testPromptDescription, testPrompt.description, "Prompt description should match") + + val arguments = testPrompt.arguments ?: error("Prompt arguments should not be null") + assertTrue(arguments.isNotEmpty(), "Prompt arguments should not be empty") + + val nameArg = arguments.find { it.name == "name" } + assertNotNull(nameArg, "Name argument should be in the list") + assertEquals("The name to greet", nameArg.description, "Argument description should match") + assertEquals(true, nameArg.required, "Argument required flag should match") + } + + @Test + fun testGetPrompt() = runTest { + val testName = "Alice" + val result = client.getPrompt( + GetPromptRequest( + name = testPromptName, + arguments = mapOf("name" to testName) + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(testPromptDescription, result.description, "Prompt description should match") + + assertTrue(result.messages.isNotEmpty(), "Prompt messages should not be empty") + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + assertNotNull(userContent.text, "User message text should not be null") + assertEquals("Hello, $testName!", userContent.text, "User message content should match") + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + assertNotNull(assistantContent.text, "Assistant message text should not be null") + assertEquals( + "Greetings, $testName! How can I assist you today?", + assistantContent.text, + "Assistant message content should match" + ) + } + + @Test + fun testMissingRequiredArguments() = runTest { + val promptsList = client.listPrompts() + assertNotNull(promptsList, "Prompts list should not be null") + val strictPrompt = promptsList.prompts.find { it.name == strictPromptName } + assertNotNull(strictPrompt, "Strict prompt should be in the list") + + val argsDef = strictPrompt.arguments ?: error("Prompt arguments should not be null") + val requiredArgs = argsDef.filter { it.required == true } + assertEquals(2, requiredArgs.size, "Strict prompt should have 2 required arguments") + + // test missing required arg + val exception = assertThrows { + runBlocking { + client.getPrompt( + GetPromptRequest( + name = strictPromptName, + arguments = mapOf("requiredArg1" to "value1"), + ) + ) + } + } + + assertEquals(exception.message?.contains("requiredArg2"), true, "Exception should mention the missing argument") + + // test with no args + val exception2 = assertThrows { + runBlocking { + client.getPrompt( + GetPromptRequest( + name = strictPromptName, + arguments = emptyMap(), + ) + ) + } + } + + assertEquals( + exception2.message?.contains("requiredArg"), + true, + "Exception should mention a missing required argument" + ) + + // test with all required args + val result = client.getPrompt( + GetPromptRequest( + name = strictPromptName, + arguments = mapOf( + "requiredArg1" to "value1", + "requiredArg2" to "value2", + ) + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + val userText = requireNotNull(userContent.text) + assertTrue(userText.contains("value1"), "Message should contain first argument") + assertTrue(userText.contains("value2"), "Message should contain second argument") + } + + @Test + fun testComplexContentTypes() = runTest { + val topic = "artificial intelligence" + val result = client.getPrompt( + GetPromptRequest( + name = complexPromptName, + arguments = mapOf( + "topic" to topic, + "includeImage" to "true", + ) + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(complexPromptDescription, result.description, "Prompt description should match") + + assertTrue(result.messages.isNotEmpty(), "Prompt messages should not be empty") + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + val userText2 = requireNotNull(userContent.text) + assertTrue(userText2.contains(topic), "User message should contain the topic") + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + val assistantText = requireNotNull(assistantContent.text) + assertTrue(assistantText.contains(topic), "Assistant message should contain the topic") + + val resultNoImage = client.getPrompt( + GetPromptRequest( + name = complexPromptName, + arguments = mapOf( + "topic" to topic, + "includeImage" to "false", + ) + ) + ) + + assertNotNull(resultNoImage, "Get prompt result (no image) should not be null") + assertEquals(2, resultNoImage.messages.size, "Prompt should have 2 messages") + } + + @Test + fun testMultipleMessagesAndRoles() = runTest { + val topic = "climate change" + val result = client.getPrompt( + GetPromptRequest( + name = conversationPromptName, + arguments = mapOf("topic" to topic), + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(conversationPromptDescription, result.description, "Prompt description should match") + + assertTrue(result.messages.isNotEmpty(), "Prompt messages should not be empty") + assertEquals(6, result.messages.size, "Prompt should have 6 messages") + + val userMessages = result.messages.filter { it.role == Role.user } + val assistantMessages = result.messages.filter { it.role == Role.assistant } + + assertEquals(3, userMessages.size, "Should have 3 user messages") + assertEquals(3, assistantMessages.size, "Should have 3 assistant messages") + + for (i in 0 until result.messages.size) { + val expectedRole = if (i % 2 == 0) Role.user else Role.assistant + assertEquals(expectedRole, result.messages[i].role, "Message $i should have role $expectedRole") + } + + for (message in result.messages) { + val content = message.content as? TextContent + assertNotNull(content, "Message content should be TextContent") + val text = requireNotNull(content.text) + + // Either the message contains the topic or it's a generic conversation message + val containsTopic = text.contains(topic) + val isGenericMessage = text.contains("thank you") || text.contains("welcome") + + assertTrue( + containsTopic || isGenericMessage, + "Message should either contain the topic or be a generic conversation message" + ) + } + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceEdgeCasesTest.kt new file mode 100644 index 00000000..b3748736 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceEdgeCasesTest.kt @@ -0,0 +1,279 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin + +import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class ResourceEdgeCasesTest : KotlinTestBase() { + + override val port = 3007 + + private val testResourceUri = "test://example.txt" + private val testResourceName = "Test Resource" + private val testResourceDescription = "A test resource for integration testing" + private val testResourceContent = "This is the content of the test resource." + + private val binaryResourceUri = "test://image.png" + private val binaryResourceName = "Binary Resource" + private val binaryResourceDescription = "A binary resource for testing" + private val binaryResourceContent = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" + + private val largeResourceUri = "test://large.txt" + private val largeResourceName = "Large Resource" + private val largeResourceDescription = "A large text resource for testing" + private val largeResourceContent = "X".repeat(100_000) // 100KB of data + + private val dynamicResourceUri = "test://dynamic.txt" + private val dynamicResourceName = "Dynamic Resource" + private val dynamicResourceContent = AtomicBoolean(false) + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + resources = ServerCapabilities.Resources( + subscribe = true, + listChanged = true, + ) + ) + } + + override fun configureServer() { + server.addResource( + uri = testResourceUri, + name = testResourceName, + description = testResourceDescription, + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = testResourceContent, + uri = request.uri, + mimeType = "text/plain", + ) + ) + ) + } + + server.addResource( + uri = binaryResourceUri, + name = binaryResourceName, + description = binaryResourceDescription, + mimeType = "image/png", + ) { request -> + ReadResourceResult( + contents = listOf( + BlobResourceContents( + blob = binaryResourceContent, + uri = request.uri, + mimeType = "image/png", + ) + ) + ) + } + + server.addResource( + uri = largeResourceUri, + name = largeResourceName, + description = largeResourceDescription, + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = largeResourceContent, + uri = request.uri, + mimeType = "text/plain", + ) + ) + ) + } + + server.addResource( + uri = dynamicResourceUri, + name = dynamicResourceName, + description = "A resource that can be updated", + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = if (dynamicResourceContent.get()) "Updated content" else "Original content", + uri = request.uri, + mimeType = "text/plain", + ) + ) + ) + } + + server.setRequestHandler(Method.Defined.ResourcesSubscribe) { _, _ -> + EmptyRequestResult() + } + + server.setRequestHandler(Method.Defined.ResourcesUnsubscribe) { _, _ -> + EmptyRequestResult() + } + } + + @Test + fun testBinaryResource() { + runTest { + val result = client.readResource(ReadResourceRequest(uri = binaryResourceUri)) + + assertNotNull(result, "Read resource result should not be null") + assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty") + + val content = result.contents.firstOrNull() as? BlobResourceContents + assertNotNull(content, "Resource content should be BlobResourceContents") + assertEquals(binaryResourceContent, content.blob, "Binary resource content should match") + assertEquals("image/png", content.mimeType, "MIME type should match") + } + } + + @Test + fun testLargeResource() { + runTest { + val result = client.readResource(ReadResourceRequest(uri = largeResourceUri)) + + assertNotNull(result, "Read resource result should not be null") + assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty") + + val content = result.contents.firstOrNull() as? TextResourceContents + assertNotNull(content, "Resource content should be TextResourceContents") + assertEquals(100_000, content.text.length, "Large resource content length should match") + assertEquals("X".repeat(100_000), content.text, "Large resource content should match") + } + } + + @Test + fun testInvalidResourceUri() { + runTest { + val invalidUri = "test://nonexistent.txt" + + val exception = assertThrows { + runBlocking { + client.readResource(ReadResourceRequest(uri = invalidUri)) + } + } + + assertTrue( + exception.message?.contains("not found") == true || + exception.message?.contains("invalid") == true || + exception.message?.contains("error") == true, + "Exception should indicate resource not found or invalid URI" + ) + } + } + + @Test + fun testDynamicResource() { + runTest { + val initialResult = client.readResource(ReadResourceRequest(uri = dynamicResourceUri)) + assertNotNull(initialResult, "Initial read result should not be null") + val initialContent = (initialResult.contents.firstOrNull() as? TextResourceContents)?.text + assertEquals("Original content", initialContent, "Initial content should match") + + // update resource + dynamicResourceContent.set(true) + + val updatedResult = client.readResource(ReadResourceRequest(uri = dynamicResourceUri)) + assertNotNull(updatedResult, "Updated read result should not be null") + val updatedContent = (updatedResult.contents.firstOrNull() as? TextResourceContents)?.text + assertEquals("Updated content", updatedContent, "Updated content should match") + } + } + + @Test + fun testResourceAddAndRemove() { + runTest { + val initialList = client.listResources() + assertNotNull(initialList, "Initial list result should not be null") + val initialCount = initialList.resources.size + + val newResourceUri = "test://new-resource.txt" + server.addResource( + uri = newResourceUri, + name = "New Resource", + description = "A newly added resource", + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "New resource content", + uri = request.uri, + mimeType = "text/plain", + ) + ) + ) + } + + val updatedList = client.listResources() + assertNotNull(updatedList, "Updated list result should not be null") + val updatedCount = updatedList.resources.size + + assertEquals(initialCount + 1, updatedCount, "Resource count should increase by 1") + val newResource = updatedList.resources.find { it.uri == newResourceUri } + assertNotNull(newResource, "New resource should be in the list") + + server.removeResource(newResourceUri) + + val finalList = client.listResources() + assertNotNull(finalList, "Final list result should not be null") + val finalCount = finalList.resources.size + + assertEquals(initialCount, finalCount, "Resource count should return to initial value") + val removedResource = finalList.resources.find { it.uri == newResourceUri } + assertEquals(null, removedResource, "Resource should be removed from the list") + } + } + + @Test + fun testConcurrentResourceOperations() { + runTest { + val concurrentCount = 10 + val results = mutableListOf() + + runBlocking { + repeat(concurrentCount) { index -> + launch { + val uri = when (index % 3) { + 0 -> testResourceUri + 1 -> binaryResourceUri + else -> largeResourceUri + } + + val result = client.readResource(ReadResourceRequest(uri = uri)) + synchronized(results) { + results.add(result) + } + } + } + } + + assertEquals(concurrentCount, results.size, "All concurrent operations should complete") + results.forEach { result -> + assertNotNull(result, "Result should not be null") + assertTrue(result.contents.isNotEmpty(), "Result contents should not be empty") + } + } + } + + @Test + fun testSubscribeAndUnsubscribe() { + runTest { + val subscribeResult = client.subscribeResource(SubscribeRequest(uri = testResourceUri)) + assertNotNull(subscribeResult, "Subscribe result should not be null") + + val unsubscribeResult = client.unsubscribeResource(UnsubscribeRequest(uri = testResourceUri)) + assertNotNull(unsubscribeResult, "Unsubscribe result should not be null") + } + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceIntegrationTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceIntegrationTest.kt new file mode 100644 index 00000000..5c5c0569 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceIntegrationTest.kt @@ -0,0 +1,89 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin + +import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class ResourceIntegrationTest : KotlinTestBase() { + + override val port = 3005 + private val testResourceUri = "test://example.txt" + private val testResourceName = "Test Resource" + private val testResourceDescription = "A test resource for integration testing" + private val testResourceContent = "This is the content of the test resource." + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + resources = ServerCapabilities.Resources( + subscribe = true, + listChanged = true, + ) + ) + } + + override fun configureServer() { + server.addResource( + uri = testResourceUri, + name = testResourceName, + description = testResourceDescription, + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = testResourceContent, + uri = request.uri, + mimeType = "text/plain", + ) + ) + ) + } + + server.setRequestHandler(Method.Defined.ResourcesSubscribe) { _, _ -> + EmptyRequestResult() + } + + server.setRequestHandler(Method.Defined.ResourcesUnsubscribe) { _, _ -> + EmptyRequestResult() + } + } + + @Test + fun testListResources() = runTest { + val result = client.listResources() + + assertNotNull(result, "List resources result should not be null") + assertTrue(result.resources.isNotEmpty(), "Resources list should not be empty") + + val testResource = result.resources.find { it.uri == testResourceUri } + assertNotNull(testResource, "Test resource should be in the list") + assertEquals(testResourceName, testResource.name, "Resource name should match") + assertEquals(testResourceDescription, testResource.description, "Resource description should match") + } + + @Test + fun testReadResource() = runTest { + val result = client.readResource(ReadResourceRequest(uri = testResourceUri)) + + assertNotNull(result, "Read resource result should not be null") + assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty") + + val content = result.contents.firstOrNull() as? TextResourceContents + assertNotNull(content, "Resource content should be TextResourceContents") + assertEquals(testResourceContent, content.text, "Resource content should match") + } + + @Test + fun testSubscribeAndUnsubscribe() { + runTest { + val subscribeResult = client.subscribeResource(SubscribeRequest(uri = testResourceUri)) + assertNotNull(subscribeResult, "Subscribe result should not be null") + + val unsubscribeResult = client.unsubscribeResource(UnsubscribeRequest(uri = testResourceUri)) + assertNotNull(unsubscribeResult, "Unsubscribe result should not be null") + } + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolEdgeCasesTest.kt new file mode 100644 index 00000000..eb8f90f5 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolEdgeCasesTest.kt @@ -0,0 +1,439 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin + +import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertCallToolResult +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertJsonProperty +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertTextContent +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class ToolEdgeCasesTest : KotlinTestBase() { + + override val port = 3009 + + private val basicToolName = "basic-tool" + private val basicToolDescription = "A basic tool for testing" + + private val complexToolName = "complex-tool" + private val complexToolDescription = "A complex tool with nested schema" + + private val largeToolName = "large-tool" + private val largeToolDescription = "A tool that returns a large response" + private val largeToolContent = "X".repeat(100_000) // 100KB of data + + private val slowToolName = "slow-tool" + private val slowToolDescription = "A tool that takes time to respond" + + private val specialCharsToolName = "special-chars-tool" + private val specialCharsToolDescription = "A tool that handles special characters" + private val specialCharsContent = "!@#$%^&*()_+{}|:\"<>?~`-=[]\\;',./\n\t" + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + tools = ServerCapabilities.Tools( + listChanged = true + ) + ) + } + + override fun configureServer() { + server.addTool( + name = basicToolName, + description = basicToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("text", buildJsonObject { + put("type", "string") + put("description", "The text to echo back") + }) + }, + required = listOf("text") + ) + ) { request -> + val text = (request.arguments["text"] as? JsonPrimitive)?.content ?: "No text provided" + + CallToolResult( + content = listOf(TextContent(text = "Echo: $text")), + structuredContent = buildJsonObject { + put("result", text) + } + ) + } + + server.addTool( + name = complexToolName, + description = complexToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("user", buildJsonObject { + put("type", "object") + put("description", "User information") + put("properties", buildJsonObject { + put("name", buildJsonObject { + put("type", "string") + put("description", "User's name") + }) + put("age", buildJsonObject { + put("type", "integer") + put("description", "User's age") + }) + put("address", buildJsonObject { + put("type", "object") + put("description", "User's address") + put("properties", buildJsonObject { + put("street", buildJsonObject { + put("type", "string") + }) + put("city", buildJsonObject { + put("type", "string") + }) + put("country", buildJsonObject { + put("type", "string") + }) + }) + }) + }) + }) + put("options", buildJsonObject { + put("type", "array") + put("description", "Additional options") + put("items", buildJsonObject { + put("type", "string") + }) + }) + }, + required = listOf("user") + ) + ) { request -> + val user = request.arguments["user"] as? JsonObject + val name = (user?.get("name") as? JsonPrimitive)?.content ?: "Unknown" + val age = (user?.get("age") as? JsonPrimitive)?.content?.toIntOrNull() ?: 0 + + val address = user?.get("address") as? JsonObject + val street = (address?.get("street") as? JsonPrimitive)?.content ?: "Unknown" + val city = (address?.get("city") as? JsonPrimitive)?.content ?: "Unknown" + val country = (address?.get("country") as? JsonPrimitive)?.content ?: "Unknown" + + val options = (request.arguments["options"] as? JsonArray)?.mapNotNull { + (it as? JsonPrimitive)?.content + } ?: emptyList() + + val summary = + "User: $name, Age: $age, Address: $street, $city, $country, Options: ${options.joinToString(", ")}" + + CallToolResult( + content = listOf(TextContent(text = summary)), + structuredContent = buildJsonObject { + put("name", name) + put("age", age) + put("address", buildJsonObject { + put("street", street) + put("city", city) + put("country", country) + }) + put("options", buildJsonArray { + options.forEach { add(it) } + }) + } + ) + } + + server.addTool( + name = largeToolName, + description = largeToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("size", buildJsonObject { + put("type", "integer") + put("description", "Size multiplier") + }) + } + ) + ) { request -> + val size = (request.arguments["size"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 1 + val content = largeToolContent.take(largeToolContent.length.coerceAtMost(size * 1000)) + + CallToolResult( + content = listOf(TextContent(text = content)), + structuredContent = buildJsonObject { + put("size", content.length) + } + ) + } + + server.addTool( + name = slowToolName, + description = slowToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("delay", buildJsonObject { + put("type", "integer") + put("description", "Delay in milliseconds") + }) + } + ) + ) { request -> + val delay = (request.arguments["delay"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 1000 + + // simulate slow operation + runBlocking { + delay(delay.toLong()) + } + + CallToolResult( + content = listOf(TextContent(text = "Completed after ${delay}ms delay")), + structuredContent = buildJsonObject { + put("delay", delay) + } + ) + } + + server.addTool( + name = specialCharsToolName, + description = specialCharsToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("special", buildJsonObject { + put("type", "string") + put("description", "Special characters to process") + }) + } + ) + ) { request -> + val special = (request.arguments["special"] as? JsonPrimitive)?.content ?: specialCharsContent + + CallToolResult( + content = listOf(TextContent(text = "Received special characters: $special")), + structuredContent = buildJsonObject { + put("special", special) + put("length", special.length) + } + ) + } + } + + @Test + fun testBasicTool() { + runTest { + val testText = "Hello, world!" + val arguments = mapOf("text" to testText) + + val result = client.callTool(basicToolName, arguments) + + val toolResult = assertCallToolResult(result) + assertTextContent(toolResult.content.firstOrNull(), "Echo: $testText") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "result", testText) + } + } + + @Test + fun testComplexNestedSchema() { + runTest { + val userJson = buildJsonObject { + put("name", JsonPrimitive("John Doe")) + put("age", JsonPrimitive(30)) + put("address", buildJsonObject { + put("street", JsonPrimitive("123 Main St")) + put("city", JsonPrimitive("New York")) + put("country", JsonPrimitive("USA")) + }) + } + + val optionsJson = buildJsonArray { + add(JsonPrimitive("option1")) + add(JsonPrimitive("option2")) + add(JsonPrimitive("option3")) + } + + val arguments = buildJsonObject { + put("user", userJson) + put("options", optionsJson) + } + + val result = client.callTool( + CallToolRequest( + name = complexToolName, + arguments = arguments + ) + ) + + val toolResult = assertCallToolResult(result) + val content = toolResult.content.firstOrNull() as? TextContent + assertNotNull(content, "Tool result content should be TextContent") + val text = content.text ?: "" + + assertTrue(text.contains("John Doe"), "Result should contain the name") + assertTrue(text.contains("30"), "Result should contain the age") + assertTrue(text.contains("123 Main St"), "Result should contain the street") + assertTrue(text.contains("New York"), "Result should contain the city") + assertTrue(text.contains("USA"), "Result should contain the country") + assertTrue(text.contains("option1"), "Result should contain option1") + assertTrue(text.contains("option2"), "Result should contain option2") + assertTrue(text.contains("option3"), "Result should contain option3") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "name", "John Doe") + assertJsonProperty(structuredContent, "age", 30) + + val address = structuredContent["address"] as? JsonObject + assertNotNull(address, "Address should be present in structured content") + assertJsonProperty(address, "street", "123 Main St") + assertJsonProperty(address, "city", "New York") + assertJsonProperty(address, "country", "USA") + + val options = structuredContent["options"] as? JsonArray + assertNotNull(options, "Options should be present in structured content") + assertEquals(3, options.size, "Options should have 3 items") + } + } + + @Test + fun testLargeResponse() { + runTest { + val size = 10 + val arguments = mapOf("size" to size) + + val result = client.callTool(largeToolName, arguments) + + val toolResult = assertCallToolResult(result) + val content = toolResult.content.firstOrNull() as? TextContent + assertNotNull(content, "Tool result content should be TextContent") + val text = content.text ?: "" + + assertEquals(10000, text.length, "Response should be 10KB in size") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "size", 10000) + } + } + + @Test + fun testSlowTool() { + runTest { + val delay = 500 + val arguments = mapOf("delay" to delay) + + val startTime = System.currentTimeMillis() + val result = client.callTool(slowToolName, arguments) + val endTime = System.currentTimeMillis() + + val toolResult = assertCallToolResult(result) + val content = toolResult.content.firstOrNull() as? TextContent + assertNotNull(content, "Tool result content should be TextContent") + val text = content.text ?: "" + + assertTrue(text.contains("${delay}ms"), "Result should mention the delay") + assertTrue(endTime - startTime >= delay, "Tool should take at least the specified delay") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "delay", delay) + } + } + + @Test + fun testSpecialCharacters() { + runTest { + val arguments = mapOf("special" to specialCharsContent) + + val result = client.callTool(specialCharsToolName, arguments) + + val toolResult = assertCallToolResult(result) + val content = toolResult.content.firstOrNull() as? TextContent + assertNotNull(content, "Tool result content should be TextContent") + val text = content.text ?: "" + + assertTrue(text.contains(specialCharsContent), "Result should contain the special characters") + + val structuredContent = toolResult.structuredContent as JsonObject + val special = structuredContent["special"]?.toString()?.trim('"') + + assertNotNull(special, "Special characters should be in structured content") + assertTrue(text.contains(specialCharsContent), "Special characters should be in the content") + } + } + + @Test + fun testConcurrentToolCalls() { + runTest { + val concurrentCount = 10 + val results = mutableListOf() + + runBlocking { + repeat(concurrentCount) { index -> + launch { + val toolName = when (index % 5) { + 0 -> basicToolName + 1 -> complexToolName + 2 -> largeToolName + 3 -> slowToolName + else -> specialCharsToolName + } + + val arguments = when (toolName) { + basicToolName -> mapOf("text" to "Concurrent call $index") + complexToolName -> mapOf( + "user" to mapOf( + "name" to "User $index", + "age" to 20 + index, + "address" to mapOf( + "street" to "Street $index", + "city" to "City $index", + "country" to "Country $index" + ) + ) + ) + + largeToolName -> mapOf("size" to 1) + slowToolName -> mapOf("delay" to 100) + else -> mapOf("special" to "!@#$%^&*()") + } + + val result = client.callTool(toolName, arguments) + + synchronized(results) { + results.add(result) + } + } + } + } + + assertEquals(concurrentCount, results.size, "All concurrent operations should complete") + results.forEach { result -> + assertNotNull(result, "Result should not be null") + assertTrue(result.content.isNotEmpty(), "Result content should not be empty") + } + } + } + + @Test + fun testNonExistentTool() { + runTest { + val nonExistentToolName = "non-existent-tool" + val arguments = mapOf("text" to "Test") + + val exception = assertThrows { + runBlocking { + client.callTool(nonExistentToolName, arguments) + } + } + + assertTrue( + exception.message?.contains("not found") == true || + exception.message?.contains("does not exist") == true || + exception.message?.contains("unknown") == true || + exception.message?.contains("error") == true, + "Exception should indicate tool not found" + ) + } + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolIntegrationTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolIntegrationTest.kt new file mode 100644 index 00000000..a8ba56ce --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolIntegrationTest.kt @@ -0,0 +1,401 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin + +import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertCallToolResult +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertJsonProperty +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertTextContent +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class ToolIntegrationTest : KotlinTestBase() { + + override val port = 3006 + private val testToolName = "echo" + private val testToolDescription = "A simple echo tool that returns the input text" + private val complexToolName = "calculator" + private val complexToolDescription = "A calculator tool that performs operations on numbers" + private val errorToolName = "error-tool" + private val errorToolDescription = "A tool that demonstrates error handling" + private val multiContentToolName = "multi-content" + private val multiContentToolDescription = "A tool that returns multiple content types" + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + tools = ServerCapabilities.Tools( + listChanged = true + ) + ) + } + + override fun configureServer() { + setupEchoTool() + setupCalculatorTool() + setupErrorHandlingTool() + setupMultiContentTool() + } + + private fun setupEchoTool() { + server.addTool( + name = testToolName, + description = testToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("text", buildJsonObject { + put("type", "string") + put("description", "The text to echo back") + }) + }, + required = listOf("text") + ) + ) { request -> + val text = (request.arguments["text"] as? JsonPrimitive)?.content ?: "No text provided" + + CallToolResult( + content = listOf(TextContent(text = "Echo: $text")), + structuredContent = buildJsonObject { + put("result", text) + } + ) + } + } + + private fun setupCalculatorTool() { + server.addTool( + name = complexToolName, + description = complexToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("operation", buildJsonObject { + put("type", "string") + put("description", "The operation to perform (add, subtract, multiply, divide)") + put("enum", buildJsonArray { + add("add") + add("subtract") + add("multiply") + add("divide") + }) + }) + put("a", buildJsonObject { + put("type", "number") + put("description", "First operand") + }) + put("b", buildJsonObject { + put("type", "number") + put("description", "Second operand") + }) + put("precision", buildJsonObject { + put("type", "integer") + put("description", "Number of decimal places (optional)") + put("default", 2) + }) + put("showSteps", buildJsonObject { + put("type", "boolean") + put("description", "Whether to show calculation steps") + put("default", false) + }) + put("tags", buildJsonObject { + put("type", "array") + put("description", "Optional tags for the calculation") + put("items", buildJsonObject { + put("type", "string") + }) + }) + }, + required = listOf("operation", "a", "b") + ) + ) { request -> + val operation = (request.arguments["operation"] as? JsonPrimitive)?.content ?: "add" + val a = (request.arguments["a"] as? JsonPrimitive)?.content?.toDoubleOrNull() ?: 0.0 + val b = (request.arguments["b"] as? JsonPrimitive)?.content?.toDoubleOrNull() ?: 0.0 + val precision = (request.arguments["precision"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 2 + val showSteps = (request.arguments["showSteps"] as? JsonPrimitive)?.content?.toBoolean() ?: false + val tags = (request.arguments["tags"] as? JsonArray)?.mapNotNull { + (it as? JsonPrimitive)?.content + } ?: emptyList() + + val result = when (operation) { + "add" -> a + b + "subtract" -> a - b + "multiply" -> a * b + "divide" -> if (b != 0.0) a / b else Double.POSITIVE_INFINITY + else -> 0.0 + } + + val formattedResult = "%.${precision}f".format(result) + + val textContent = if (showSteps) { + "Operation: $operation\nA: $a\nB: $b\nResult: $formattedResult\nTags: ${tags.joinToString(", ")}" + } else { + "Result: $formattedResult" + } + + CallToolResult( + content = listOf(TextContent(text = textContent)), + structuredContent = buildJsonObject { + put("operation", operation) + put("a", a) + put("b", b) + put("result", result) + put("formattedResult", formattedResult) + put("precision", precision) + put("tags", buildJsonArray { tags.forEach { add(it) } }) + } + ) + } + } + + private fun setupErrorHandlingTool() { + server.addTool( + name = errorToolName, + description = errorToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("errorType", buildJsonObject { + put("type", "string") + put("description", "Type of error to simulate (none, exception, error)") + put("enum", buildJsonArray { + add("none") + add("exception") + add("error") + }) + }) + put("message", buildJsonObject { + put("type", "string") + put("description", "Custom error message") + put("default", "An error occurred") + }) + }, + required = listOf("errorType") + ) + ) { request -> + val errorType = (request.arguments["errorType"] as? JsonPrimitive)?.content ?: "none" + val message = (request.arguments["message"] as? JsonPrimitive)?.content ?: "An error occurred" + + when (errorType) { + "exception" -> throw IllegalArgumentException(message) + "error" -> CallToolResult( + content = listOf(TextContent(text = "Error: $message")), + structuredContent = buildJsonObject { + put("error", true) + put("message", message) + } + ) + + else -> CallToolResult( + content = listOf(TextContent(text = "No error occurred")), + structuredContent = buildJsonObject { + put("error", false) + put("message", "Success") + } + ) + } + } + } + + private fun setupMultiContentTool() { + server.addTool( + name = multiContentToolName, + description = multiContentToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("text", buildJsonObject { + put("type", "string") + put("description", "Text to include in the response") + }) + put("includeImage", buildJsonObject { + put("type", "boolean") + put("description", "Whether to include an image in the response") + put("default", true) + }) + }, + required = listOf("text") + ) + ) { request -> + val text = (request.arguments["text"] as? JsonPrimitive)?.content ?: "Default text" + val includeImage = (request.arguments["includeImage"] as? JsonPrimitive)?.content?.toBoolean() ?: true + + val content = mutableListOf( + TextContent(text = "Text content: $text") + ) + + if (includeImage) { + content.add( + ImageContent( + data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", + mimeType = "image/png" + ) + ) + } + + CallToolResult( + content = content, + structuredContent = buildJsonObject { + put("text", text) + put("includeImage", includeImage) + } + ) + } + } + + + @Test + fun testListTools() = runTest { + val result = client.listTools() + + assertNotNull(result, "List utils result should not be null") + assertTrue(result.tools.isNotEmpty(), "Tools list should not be empty") + + val testTool = result.tools.find { it.name == testToolName } + assertNotNull(testTool, "Test tool should be in the list") + assertEquals(testToolDescription, testTool.description, "Tool description should match") + } + + @Test + fun testCallTool() = runTest { + val testText = "Hello, world!" + val arguments = mapOf("text" to testText) + + val result = client.callTool(testToolName, arguments) + + val toolResult = assertCallToolResult(result) + assertTextContent(toolResult.content.firstOrNull(), "Echo: $testText") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "result", testText) + } + + @Test + fun testComplexInputSchemaTool() { + runTest { + val toolsList = client.listTools() + assertNotNull(toolsList, "Tools list should not be null") + val calculatorTool = toolsList.tools.find { it.name == complexToolName } + assertNotNull(calculatorTool, "Calculator tool should be in the list") + + val arguments = mapOf( + "operation" to "multiply", + "a" to 5.5, + "b" to 2.0, + "precision" to 3, + "showSteps" to true, + "tags" to listOf("test", "calculator", "integration"), + ) + + val result = client.callTool(complexToolName, arguments) + + val toolResult = assertCallToolResult(result) + + val content = toolResult.content.firstOrNull() as? TextContent + assertNotNull(content, "Tool result content should be TextContent") + val contentText = requireNotNull(content.text) + + assertTrue(contentText.contains("Operation"), "Result should contain operation") + assertTrue(contentText.contains("multiply"), "Result should contain multiply operation") + assertTrue(contentText.contains("5.5"), "Result should contain first operand") + assertTrue(contentText.contains("2.0"), "Result should contain second operand") + assertTrue(contentText.contains("11"), "Result should contain result value") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "operation", "multiply") + assertJsonProperty(structuredContent, "result", 11.0) + + val formattedResult = structuredContent["formattedResult"]?.toString()?.trim('"') ?: "" + assertTrue( + formattedResult == "11.000" || formattedResult == "11,000", + "Formatted result should be either '11.000' or '11,000', but was '$formattedResult'" + ) + assertJsonProperty(structuredContent, "precision", 3) + + val tags = structuredContent["tags"] as? JsonArray + assertNotNull(tags, "Tags should be present") + } + } + + @Test + fun testToolErrorHandling() = runTest { + val successArgs = mapOf("errorType" to "none") + val successResult = client.callTool(errorToolName, successArgs) + + val successToolResult = assertCallToolResult(successResult, "No error: ") + assertTextContent(successToolResult.content.firstOrNull(), "No error occurred") + + val noErrorStructured = successToolResult.structuredContent as JsonObject + assertJsonProperty(noErrorStructured, "error", false) + + val errorArgs = mapOf( + "errorType" to "error", + "message" to "Custom error message", + ) + val errorResult = client.callTool(errorToolName, errorArgs) + + val errorToolResult = assertCallToolResult(errorResult, "Error: ") + assertTextContent(errorToolResult.content.firstOrNull(), "Error: Custom error message") + + val errorStructured = errorToolResult.structuredContent as JsonObject + assertJsonProperty(errorStructured, "error", true) + assertJsonProperty(errorStructured, "message", "Custom error message") + + val exceptionArgs = mapOf( + "errorType" to "exception", + "message" to "Exception message", + ) + + val exception = assertThrows { + runBlocking { + client.callTool(errorToolName, exceptionArgs) + } + } + + assertEquals( + exception.message?.contains("Exception message"), + true, + "Exception message should contain 'Exception message'" + ) + } + + @Test + fun testMultiContentTool() = runTest { + val testText = "Test multi-content" + val arguments = mapOf( + "text" to testText, + "includeImage" to true, + ) + + val result = client.callTool(multiContentToolName, arguments) + + val toolResult = assertCallToolResult(result) + assertEquals(2, toolResult.content.size, "Tool result should have 2 content items") + + val textContent = toolResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Result should contain TextContent") + assertNotNull(textContent.text, "Text content should not be null") + assertEquals("Text content: $testText", textContent.text, "Text content should match") + + val imageContent = toolResult.content.firstOrNull { it is ImageContent } as? ImageContent + assertNotNull(imageContent, "Result should contain ImageContent") + assertEquals("image/png", imageContent.mimeType, "Image MIME type should match") + assertTrue(imageContent.data.isNotEmpty(), "Image data should not be empty") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "text", testText) + assertJsonProperty(structuredContent, "includeImage", true) + + val textOnlyArgs = mapOf( + "text" to testText, + "includeImage" to false, + ) + + val textOnlyResult = client.callTool(multiContentToolName, textOnlyArgs) + + val textOnlyToolResult = assertCallToolResult(textOnlyResult, "Text-only: ") + assertEquals(1, textOnlyToolResult.content.size, "Text-only result should have 1 content item") + + assertTextContent(textOnlyToolResult.content.firstOrNull(), "Text content: $testText") + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt new file mode 100644 index 00000000..5b72b7da --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt @@ -0,0 +1,248 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.sse.* +import io.modelcontextprotocol.kotlin.sdk.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.TextContent +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp +import kotlinx.coroutines.* +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.junit.jupiter.api.* +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class KotlinClientTypeScriptServerEdgeCasesTest : TypeScriptTestBase() { + + private var port: Int = 0 + private val host = "localhost" + private lateinit var serverUrl: String + + private lateinit var client: Client + private lateinit var tsServerProcess: Process + + @BeforeEach + fun setUp() { + port = findFreePort() + serverUrl = "http://$host:$port/mcp" + tsServerProcess = startTypeScriptServer(port) + println("TypeScript server started on port $port") + } + + @AfterEach + fun tearDown() { + if (::client.isInitialized) { + try { + runBlocking { + withTimeout(3.seconds) { + client.close() + } + } + } catch (e: Exception) { + println("Warning: Error during client close: ${e.message}") + } + } + + if (::tsServerProcess.isInitialized) { + try { + println("Stopping TypeScript server") + stopProcess(tsServerProcess) + } catch (e: Exception) { + println("Warning: Error during TypeScript server stop: ${e.message}") + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testNonExistentTool() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val nonExistentToolName = "non-existent-tool" + val arguments = mapOf("name" to "TestUser") + + val exception = assertThrows { + client.callTool(nonExistentToolName, arguments) + } + + val errorMessage = exception.message ?: "" + assertTrue( + errorMessage.contains("not found") || + errorMessage.contains("unknown") || + errorMessage.contains("error"), + "Exception should indicate tool not found: $errorMessage" + ) + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testSpecialCharactersInArguments() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val specialChars = "!@#$%^&*()_+{}[]|\\:;\"'<>,.?/" + val arguments = mapOf("name" to specialChars) + + val result = client.callTool("greet", arguments) + assertNotNull(result, "Tool call result should not be null") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present in the result") + + val text = textContent.text ?: "" + assertTrue( + text.contains(specialChars), + "Tool response should contain the special characters" + ) + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testLargePayload() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val largeName = "A".repeat(10 * 1024) + val arguments = mapOf("name" to largeName) + + val result = client.callTool("greet", arguments) + assertNotNull(result, "Tool call result should not be null") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present in the result") + + val text = textContent.text ?: "" + assertTrue( + text.contains("Hello,") && text.contains("A"), + "Tool response should contain the greeting with the large name" + ) + } + } + } + + @Test + @Timeout(60, unit = TimeUnit.SECONDS) + fun testConcurrentRequests() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val concurrentCount = 5 + val results = mutableListOf>() + + for (i in 1..concurrentCount) { + val deferred = async { + val name = "ConcurrentClient$i" + val arguments = mapOf("name" to name) + + val result = client.callTool("greet", arguments) + assertNotNull(result, "Tool call result should not be null for client $i") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present for client $i") + + textContent.text ?: "" + } + results.add(deferred) + } + + val responses = results.awaitAll() + + for (i in 1..concurrentCount) { + val expectedName = "ConcurrentClient$i" + val matchingResponses = responses.filter { it.contains("Hello, $expectedName!") } + assertEquals( + 1, + matchingResponses.size, + "Should have exactly one response for $expectedName" + ) + } + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testInvalidArguments() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val invalidArguments = mapOf( + "name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))) + ) + + try { + val result = client.callTool("greet", invalidArguments) + assertNotNull(result, "Tool call result should not be null") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present in the result") + } catch (e: Exception) { + assertTrue( + e.message?.contains("invalid") == true || + e.message?.contains("error") == true, + "Exception should indicate invalid arguments: ${e.message}" + ) + } + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testMultipleToolCalls() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + repeat(10) { i -> + val name = "SequentialClient$i" + val arguments = mapOf("name" to name) + + val result = client.callTool("greet", arguments) + assertNotNull(result, "Tool call result should not be null for call $i") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present for call $i") + + assertEquals( + "Hello, $name!", + textContent.text, + "Tool response should contain the greeting with the provided name" + ) + } + } + } + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerTest.kt new file mode 100644 index 00000000..b021a86e --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerTest.kt @@ -0,0 +1,173 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.sse.* +import io.modelcontextprotocol.kotlin.sdk.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.TextContent +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class KotlinClientTypeScriptServerTest : TypeScriptTestBase() { + + private var port: Int = 0 + private val host = "localhost" + private lateinit var serverUrl: String + + private lateinit var client: Client + private lateinit var tsServerProcess: Process + + + @BeforeEach + fun setUp() { + port = findFreePort() + serverUrl = "http://$host:$port/mcp" + tsServerProcess = startTypeScriptServer(port) + println("TypeScript server started on port $port") + } + + @AfterEach + fun tearDown() { + if (::client.isInitialized) { + try { + runBlocking { + withTimeout(3.seconds) { + client.close() + } + } + } catch (e: Exception) { + println("Warning: Error during client close: ${e.message}") + } + } + + if (::tsServerProcess.isInitialized) { + try { + println("Stopping TypeScript server") + stopProcess(tsServerProcess) + } catch (e: Exception) { + println("Warning: Error during TypeScript server stop: ${e.message}") + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testKotlinClientConnectsToTypeScriptServer() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + assertNotNull(client, "Client should be initialized") + + val pingResult = client.ping() + assertNotNull(pingResult, "Ping result should not be null") + + val serverImpl = client.serverVersion + assertNotNull(serverImpl, "Server implementation should not be null") + println("Connected to TypeScript server: ${serverImpl.name} v${serverImpl.version}") + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testListTools() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val result = client.listTools() + assertNotNull(result, "Tools list should not be null") + assertTrue(result.tools.isNotEmpty(), "Tools list should not be empty") + + // Verify specific utils are available + val toolNames = result.tools.map { it.name } + assertTrue(toolNames.contains("greet"), "Greet tool should be available") + assertTrue(toolNames.contains("multi-greet"), "Multi-greet tool should be available") + assertTrue(toolNames.contains("collect-user-info"), "Collect-user-info tool should be available") + + println("Available utils: ${toolNames.joinToString()}") + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testToolCall() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val testName = "TestUser" + val arguments = mapOf("name" to testName) + + val result = client.callTool("greet", arguments) + assertNotNull(result, "Tool call result should not be null") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present in the result") + assertEquals( + "Hello, $testName!", + textContent.text, + "Tool response should contain the greeting with the provided name" + ) + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testMultipleClients() { + runBlocking { + withContext(Dispatchers.IO) { + // First client connection + val client1 = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val tools1 = client1.listTools() + assertNotNull(tools1, "Tools list for first client should not be null") + assertTrue(tools1.tools.isNotEmpty(), "Tools list for first client should not be empty") + + val client2 = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val tools2 = client2.listTools() + assertNotNull(tools2, "Tools list for second client should not be null") + assertTrue(tools2.tools.isNotEmpty(), "Tools list for second client should not be empty") + + val toolNames1 = tools1.tools.map { it.name } + val toolNames2 = tools2.tools.map { it.name } + + assertTrue(toolNames1.contains("greet"), "Greet tool should be available to first client") + assertTrue(toolNames1.contains("multi-greet"), "Multi-greet tool should be available to first client") + assertTrue(toolNames2.contains("greet"), "Greet tool should be available to second client") + assertTrue(toolNames2.contains("multi-greet"), "Multi-greet tool should be available to second client") + + client1.close() + client2.close() + } + } + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt new file mode 100644 index 00000000..638e288c --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt @@ -0,0 +1,190 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript + +import io.modelcontextprotocol.kotlin.sdk.integration.utils.KotlinServerForTypeScriptClient +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import java.util.concurrent.TimeUnit +import kotlin.test.assertTrue + +class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { + + private var port: Int = 0 + private lateinit var serverUrl: String + private var httpServer: KotlinServerForTypeScriptClient? = null + + @BeforeEach + fun setUp() { + port = findFreePort() + serverUrl = "http://localhost:$port/mcp" + killProcessOnPort(port) + httpServer = KotlinServerForTypeScriptClient() + httpServer?.start(port) + if (!waitForPort(port = port)) { + throw IllegalStateException("Kotlin test server did not become ready on localhost:$port within timeout") + } + println("Kotlin server started on port $port") + } + + @AfterEach + fun tearDown() { + try { + httpServer?.stop() + println("HTTP server stopped") + } catch (e: Exception) { + println("Error during server shutdown: ${e.message}") + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testToolCall() { + val testName = "TestUser" + val command = "npx tsx myClient.ts $serverUrl greet $testName" + val output = executeCommand(command, tsClientDir) + + assertTrue( + output.contains("Hello, $testName!"), + "Tool response should contain the greeting with the provided name" + ) + assertTrue(output.contains("Tool result:"), "Output should indicate a successful tool call") + assertTrue(output.contains("Text content:"), "Output should contain the text content section") + assertTrue(output.contains("Structured content:"), "Output should contain the structured content section") + assertTrue( + output.contains("\"greeting\": \"Hello, $testName!\""), + "Structured content should contain the greeting" + ) + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testToolCallWithSessionManagement() { + val testName = "SessionTest" + val command = "npx tsx myClient.ts $serverUrl greet $testName" + val output = executeCommand(command, tsClientDir) + + assertTrue(output.contains("Connected to server"), "Client should connect to server") + assertTrue( + output.contains("Hello, $testName!"), + "Tool response should contain the greeting with the provided name" + ) + assertTrue(output.contains("Tool result:"), "Output should indicate a successful tool call") + assertTrue(output.contains("Disconnected from server"), "Client should disconnect cleanly") + + val multiGreetName = "NotificationTest" + val multiGreetCommand = "npx tsx myClient.ts $serverUrl multi-greet $multiGreetName" + val multiGreetOutput = executeCommand(multiGreetCommand, tsClientDir) + + assertTrue(multiGreetOutput.contains("Connected to server"), "Client should connect to server") + assertTrue( + multiGreetOutput.contains("Multiple greetings") || multiGreetOutput.contains("greeting"), + "Tool response should contain greeting message" + ) + assertTrue(multiGreetOutput.contains("Disconnected from server"), "Client should disconnect cleanly") + } + + @Test + @Timeout(120, unit = TimeUnit.SECONDS) + fun testMultipleClientSequence() { + val testName1 = "FirstClient" + val command1 = "npx tsx myClient.ts $serverUrl greet $testName1" + val output1 = executeCommand(command1, tsClientDir) + + assertTrue(output1.contains("Connected to server"), "First client should connect to server") + assertTrue(output1.contains("Hello, $testName1!"), "Tool response should contain the greeting for first client") + assertTrue(output1.contains("Disconnected from server"), "First client should disconnect cleanly") + + val testName2 = "SecondClient" + val command2 = "npx tsx myClient.ts $serverUrl multi-greet $testName2" + val output2 = executeCommand(command2, tsClientDir) + + assertTrue(output2.contains("Connected to server"), "Second client should connect to server") + assertTrue( + output2.contains("Multiple greetings") || output2.contains("greeting"), + "Tool response should contain greeting message" + ) + assertTrue(output2.contains("Disconnected from server"), "Second client should disconnect cleanly") + + val command3 = "npx tsx myClient.ts $serverUrl" + val output3 = executeCommand(command3, tsClientDir) + + assertTrue(output3.contains("Connected to server"), "Third client should connect to server") + assertTrue(output3.contains("Available utils:"), "Third client should list available utils") + assertTrue(output3.contains("greet"), "Greet tool should be available to third client") + assertTrue(output3.contains("multi-greet"), "Multi-greet tool should be available to third client") + assertTrue(output3.contains("Disconnected from server"), "Third client should disconnect cleanly") + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testMultipleClientParallel() { + val clientCount = 3 + val clients = listOf( + "FirstClient" to "greet", + "SecondClient" to "multi-greet", + "ThirdClient" to "" + ) + + val threads = mutableListOf() + val outputs = mutableListOf>() + val exceptions = mutableListOf() + + for (i in 0 until clientCount) { + val (clientName, toolName) = clients[i] + val thread = Thread { + try { + val command = if (toolName.isEmpty()) { + "npx tsx myClient.ts $serverUrl" + } else { + "npx tsx myClient.ts $serverUrl $toolName $clientName" + } + + val output = executeCommand(command, tsClientDir) + synchronized(outputs) { + outputs.add(i to output) + } + } catch (e: Exception) { + synchronized(exceptions) { + exceptions.add(e) + } + } + } + threads.add(thread) + thread.start() + Thread.sleep(100) + } + + threads.forEach { it.join() } + + assertTrue(exceptions.isEmpty(), "No exceptions should occur: ${exceptions.joinToString { it.message ?: "" }}") + + val sortedOutputs = outputs.sortedBy { it.first }.map { it.second } + + sortedOutputs.forEachIndexed { index, output -> + val clientName = clients[index].first + val toolName = clients[index].second + + when (toolName) { + "greet" -> { + val containsGreeting = output.contains("Hello, $clientName!") || + output.contains("\"greeting\": \"Hello, $clientName!\"") + assertTrue( + containsGreeting, + "Tool response should contain the greeting for $clientName" + ) + } + + "multi-greet" -> { + val containsGreeting = output.contains("Multiple greetings") || + output.contains("greeting") || + output.contains("greet") + assertTrue( + containsGreeting, + "Tool response should contain greeting message for $clientName" + ) + } + } + } + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt new file mode 100644 index 00000000..a418021b --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt @@ -0,0 +1,182 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript + +import io.modelcontextprotocol.kotlin.sdk.integration.utils.KotlinServerForTypeScriptClient +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import java.io.File +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class TypeScriptEdgeCasesTest : TypeScriptTestBase() { + + private var port: Int = 0 + private lateinit var serverUrl: String + private var httpServer: KotlinServerForTypeScriptClient? = null + + @BeforeEach + fun setUp() { + port = findFreePort() + serverUrl = "http://localhost:$port/mcp" + killProcessOnPort(port) + httpServer = KotlinServerForTypeScriptClient() + httpServer?.start(port) + if (!waitForPort(port = port)) { + throw IllegalStateException("Kotlin test server did not become ready on localhost:$port within timeout") + } + println("Kotlin server started on port $port") + } + + @AfterEach + fun tearDown() { + try { + httpServer?.stop() + println("HTTP server stopped") + } catch (e: Exception) { + println("Error during server shutdown: ${e.message}") + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testErrorHandling() { + val nonExistentToolCommand = "npx tsx myClient.ts $serverUrl non-existent-tool" + val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, tsClientDir) + + assertTrue( + nonExistentToolOutput.contains("Tool \"non-existent-tool\" not found"), + "Client should handle non-existent tool gracefully" + ) + + val invalidUrlCommand = "npx tsx myClient.ts http://localhost:${port + 1000}/mcp greet TestUser" + val invalidUrlOutput = executeCommandAllowingFailure(invalidUrlCommand, tsClientDir) + + assertTrue( + invalidUrlOutput.contains("Error:") || invalidUrlOutput.contains("ECONNREFUSED"), + "Client should handle connection errors gracefully" + ) + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testSpecialCharacters() { + val specialChars = "!@#$+-[].,?" + + val tempFile = File.createTempFile("special_chars", ".txt") + tempFile.writeText(specialChars) + tempFile.deleteOnExit() + + val specialCharsContent = tempFile.readText() + val specialCharsCommand = "npx tsx myClient.ts $serverUrl greet \"$specialCharsContent\"" + val specialCharsOutput = executeCommand(specialCharsCommand, tsClientDir) + + assertTrue( + specialCharsOutput.contains("Hello, $specialChars!"), + "Tool should handle special characters in arguments" + ) + assertTrue( + specialCharsOutput.contains("Disconnected from server"), + "Client should disconnect cleanly after handling special characters" + ) + } + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testLargePayload() { + val largeName = "A".repeat(10 * 1024) + + val tempFile = File.createTempFile("large_name", ".txt") + tempFile.writeText(largeName) + tempFile.deleteOnExit() + + val largeNameContent = tempFile.readText() + val largePayloadCommand = "npx tsx myClient.ts $serverUrl greet \"$largeNameContent\"" + val largePayloadOutput = executeCommand(largePayloadCommand, tsClientDir) + + tempFile.delete() + + assertTrue( + largePayloadOutput.contains("Hello,") && largePayloadOutput.contains("A".repeat(20)), + "Tool should handle large payloads" + ) + assertTrue( + largePayloadOutput.contains("Disconnected from server"), + "Client should disconnect cleanly after handling large payload" + ) + } + + @Test + @Timeout(60, unit = TimeUnit.SECONDS) + fun testComplexConcurrentRequests() { + val commands = listOf( + "npx tsx myClient.ts $serverUrl greet \"Client1\"", + "npx tsx myClient.ts $serverUrl multi-greet \"Client2\"", + "npx tsx myClient.ts $serverUrl greet \"Client3\"", + "npx tsx myClient.ts $serverUrl", + "npx tsx myClient.ts $serverUrl multi-greet \"Client5\"" + ) + + val threads = commands.mapIndexed { index, command -> + Thread { + println("Starting client $index") + val output = executeCommand(command, tsClientDir) + println("Client $index completed") + + assertTrue( + output.contains("Connected to server"), + "Client $index should connect to server" + ) + assertTrue( + output.contains("Disconnected from server"), + "Client $index should disconnect cleanly" + ) + + when { + command.contains("greet \"Client1\"") -> + assertTrue(output.contains("Hello, Client1!"), "Client 1 should receive correct greeting") + + command.contains("multi-greet \"Client2\"") -> + assertTrue(output.contains("Multiple greetings"), "Client 2 should receive multiple greetings") + + command.contains("greet \"Client3\"") -> + assertTrue(output.contains("Hello, Client3!"), "Client 3 should receive correct greeting") + + !command.contains("greet") && !command.contains("multi-greet") -> + assertTrue(output.contains("Available utils:"), "Client 4 should list available tools") + + command.contains("multi-greet \"Client5\"") -> + assertTrue(output.contains("Multiple greetings"), "Client 5 should receive multiple greetings") + } + }.apply { start() } + } + + threads.forEach { it.join() } + } + + @Test + @Timeout(120, unit = TimeUnit.SECONDS) + fun testRapidSequentialRequests() { + val outputs = (1..10).map { i -> + val command = "npx tsx myClient.ts $serverUrl greet \"RapidClient$i\"" + val output = executeCommand(command, tsClientDir) + + assertTrue( + output.contains("Connected to server"), + "Client $i should connect to server" + ) + assertTrue( + output.contains("Hello, RapidClient$i!"), + "Client $i should receive correct greeting" + ) + assertTrue( + output.contains("Disconnected from server"), + "Client $i should disconnect cleanly" + ) + + output + } + + assertEquals(10, outputs.size, "All 10 rapid requests should complete successfully") + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt new file mode 100644 index 00000000..37a162ce --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt @@ -0,0 +1,163 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript + +import io.modelcontextprotocol.kotlin.sdk.integration.utils.Retry +import org.junit.jupiter.api.BeforeAll +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.net.ServerSocket +import java.net.Socket +import java.nio.file.Files +import java.util.concurrent.TimeUnit + +@Retry(times = 3) +abstract class TypeScriptTestBase { + + protected val projectRoot: File get() = File(System.getProperty("user.dir")) + protected val tsClientDir: File + get() = File( + projectRoot, + "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils" + ) + + companion object { + @JvmStatic + private val tempRootDir: File = Files.createTempDirectory("typescript-sdk-").toFile().apply { deleteOnExit() } + + @JvmStatic + protected val sdkDir: File = File(tempRootDir, "typescript-sdk") + + @JvmStatic + @BeforeAll + fun setupTypeScriptSdk() { + println("Cloning TypeScript SDK repository") + if (!sdkDir.exists()) { + val cloneCommand = + "git clone --depth 1 https://github.com/modelcontextprotocol/typescript-sdk.git ${sdkDir.absolutePath}" + val process = ProcessBuilder() + .command("bash", "-c", cloneCommand) + .redirectErrorStream(true) + .start() + val exitCode = process.waitFor() + if (exitCode != 0) { + throw RuntimeException("Failed to clone TypeScript SDK repository: exit code $exitCode") + } + } + + println("Installing TypeScript SDK dependencies") + executeCommand("npm install", sdkDir) + } + + @JvmStatic + protected fun executeCommand(command: String, workingDir: File): String = + runCommand(command, workingDir, allowFailure = false, timeoutSeconds = null) + + @JvmStatic + protected fun killProcessOnPort(port: Int) { + executeCommand("lsof -ti:$port | xargs kill -9 2>/dev/null || true", File(".")) + } + + @JvmStatic + protected fun findFreePort(): Int { + ServerSocket(0).use { socket -> + return socket.localPort + } + } + + private fun runCommand( + command: String, + workingDir: File, + allowFailure: Boolean, + timeoutSeconds: Long? + ): String { + val process = ProcessBuilder() + .command("bash", "-c", "TYPESCRIPT_SDK_DIR='${sdkDir.absolutePath}' $command") + .directory(workingDir) + .redirectErrorStream(true) + .start() + + val output = StringBuilder() + BufferedReader(InputStreamReader(process.inputStream)).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + println(line) + output.append(line).append("\n") + } + } + + if (timeoutSeconds == null) { + val exitCode = process.waitFor() + if (!allowFailure && exitCode != 0) { + throw RuntimeException("Command execution failed with exit code $exitCode: $command\nOutput:\n$output") + } + } else { + process.waitFor(timeoutSeconds, TimeUnit.SECONDS) + } + + return output.toString() + } + } + + protected fun waitForProcessTermination(process: Process, timeoutSeconds: Long): Boolean { + if (process.isAlive && !process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) { + process.destroyForcibly() + process.waitFor(2, TimeUnit.SECONDS) + return false + } + return true + } + + protected fun createProcessOutputReader(process: Process, prefix: String = "TS-SERVER"): Thread { + val outputReader = Thread { + try { + process.inputStream.bufferedReader().useLines { lines -> + for (line in lines) { + println("[$prefix] $line") + } + } + } catch (e: Exception) { + println("Warning: Error reading process output: ${e.message}") + } + } + outputReader.isDaemon = true + return outputReader + } + + protected fun waitForPort(host: String = "localhost", port: Int, timeoutSeconds: Long = 10): Boolean { + val deadline = System.currentTimeMillis() + timeoutSeconds * 1000 + while (System.currentTimeMillis() < deadline) { + try { + Socket(host, port).use { return true } + } catch (_: Exception) { + Thread.sleep(100) + } + } + return false + } + + protected fun executeCommandAllowingFailure(command: String, workingDir: File, timeoutSeconds: Long = 20): String = + runCommand(command, workingDir, allowFailure = true, timeoutSeconds = timeoutSeconds) + + protected fun startTypeScriptServer(port: Int): Process { + killProcessOnPort(port) + val processBuilder = ProcessBuilder() + .command("bash", "-c", "MCP_PORT=$port npx tsx src/examples/server/simpleStreamableHttp.ts") + .directory(sdkDir) + .redirectErrorStream(true) + val process = processBuilder.start() + if (!waitForPort(port = port)) { + throw IllegalStateException("TypeScript server did not become ready on localhost:$port within timeout") + } + createProcessOutputReader(process).start() + return process + } + + protected fun stopProcess(process: Process, waitSeconds: Long = 3, name: String = "TypeScript server") { + process.destroy() + if (waitForProcessTermination(process, waitSeconds)) { + println("$name stopped gracefully") + } else { + println("$name did not stop gracefully, forced termination") + } + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/KotlinServerForTypeScriptClient.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/KotlinServerForTypeScriptClient.kt new file mode 100644 index 00000000..9029d233 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/KotlinServerForTypeScriptClient.kt @@ -0,0 +1,385 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.utils + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.cio.* +import io.ktor.server.engine.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.json.* +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +private val logger = KotlinLogging.logger {} + +class KotlinServerForTypeScriptClient { + private val serverTransports = ConcurrentHashMap() + private val jsonFormat = Json { ignoreUnknownKeys = true } + private var server: EmbeddedServer<*, *>? = null + + fun start(port: Int = 3000) { + logger.info { "Starting HTTP server on port $port" } + + server = embeddedServer(CIO, port = port) { + routing { + post("/mcp") { + val sessionId = call.request.header("mcp-session-id") + val requestBody = call.receiveText() + + logger.debug { "Received request with sessionId: $sessionId" } + logger.trace { "Request body: $requestBody" } + + val jsonElement = try { + jsonFormat.parseToJsonElement(requestBody) + } catch (e: Exception) { + logger.error(e) { "Failed to parse request body as JSON" } + call.respond( + HttpStatusCode.BadRequest, + jsonFormat.encodeToString( + JsonObject.serializer(), + JsonObject( + mapOf( + "jsonrpc" to JsonPrimitive("2.0"), + "error" to JsonObject( + mapOf( + "code" to JsonPrimitive(-32700), + "message" to JsonPrimitive("Parse error: ${e.message}") + ) + ), + "id" to JsonNull + ) + ) + ) + ) + return@post + } + + if (sessionId != null && serverTransports.containsKey(sessionId)) { + logger.debug { "Using existing transport for session: $sessionId" } + val transport = serverTransports[sessionId]!! + transport.handleRequest(call, jsonElement) + } else { + if (isInitializeRequest(jsonElement)) { + val newSessionId = UUID.randomUUID().toString() + logger.info { "Creating new session with ID: $newSessionId" } + + val transport = HttpServerTransport(newSessionId) + + serverTransports[newSessionId] = transport + + val mcpServer = createMcpServer() + + call.response.header("mcp-session-id", newSessionId) + + val serverThread = Thread { + runBlocking { + mcpServer.connect(transport) + } + } + serverThread.start() + + Thread.sleep(500) + + transport.handleRequest(call, jsonElement) + } else { + logger.warn { "Invalid request: no session ID or not an initialization request" } + call.respond( + HttpStatusCode.BadRequest, + jsonFormat.encodeToString( + JsonObject.serializer(), + JsonObject( + mapOf( + "jsonrpc" to JsonPrimitive("2.0"), + "error" to JsonObject( + mapOf( + "code" to JsonPrimitive(-32000), + "message" to JsonPrimitive("Bad Request: No valid session ID provided") + ) + ), + "id" to JsonNull + ) + ) + ) + ) + } + } + } + + delete("/mcp") { + val sessionId = call.request.header("mcp-session-id") + if (sessionId != null && serverTransports.containsKey(sessionId)) { + logger.info { "Terminating session: $sessionId" } + val transport = serverTransports[sessionId]!! + serverTransports.remove(sessionId) + runBlocking { + transport.close() + } + call.respond(HttpStatusCode.OK) + } else { + logger.warn { "Invalid session termination request: $sessionId" } + call.respond(HttpStatusCode.BadRequest, "Invalid or missing session ID") + } + } + } + } + + server?.start(wait = false) + } + + fun stop() { + logger.info { "Stopping HTTP server" } + server?.stop(500, 1000) + server = null + } + + private fun createMcpServer(): Server { + val server = Server( + Implementation( + name = "kotlin-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), + ) + ) + ) + + server.addTool( + name = "greet", + description = "A simple greeting tool", + inputSchema = Tool.Input( + properties = buildJsonObject { + put("name", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Name to greet")) + }) + }, + required = listOf("name") + ) + ) { request -> + val name = (request.arguments["name"] as? JsonPrimitive)?.content ?: "World" + CallToolResult( + content = listOf(TextContent("Hello, $name!")), + structuredContent = buildJsonObject { + put("greeting", JsonPrimitive("Hello, $name!")) + } + ) + } + + server.addTool( + name = "multi-greet", + description = "A greeting tool that sends multiple notifications", + inputSchema = Tool.Input( + properties = buildJsonObject { + put("name", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Name to greet")) + }) + }, + required = listOf("name") + ) + ) { request -> + val name = (request.arguments["name"] as? JsonPrimitive)?.content ?: "World" + + // Notifications disabled for test stability + + CallToolResult( + content = listOf(TextContent("Multiple greetings sent to $name!")), + structuredContent = buildJsonObject { + put("greeting", JsonPrimitive("Multiple greetings sent to $name!")) + put("notificationCount", JsonPrimitive(3)) + } + ) + } + + 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 -> + GetPromptResult( + "Greeting for ${request.name}", + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent("Please greet ${request.arguments?.get("name") ?: "someone"} in a friendly manner.") + ) + ) + ) + } + + 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 + } + + private fun isInitializeRequest(json: JsonElement): Boolean { + if (json !is JsonObject) return false + + val method = json["method"]?.jsonPrimitive?.contentOrNull + return method == "initialize" + } +} + +class HttpServerTransport(private val sessionId: String) : AbstractTransport() { + private val logger = KotlinLogging.logger {} + private val pendingResponses = ConcurrentHashMap>() + private val messageQueue = Channel(Channel.UNLIMITED) + + suspend fun handleRequest(call: ApplicationCall, requestBody: JsonElement) { + try { + logger.info { "Handling request body: $requestBody" } + val message = McpJson.decodeFromJsonElement(requestBody) + logger.info { "Decoded message: $message" } + + if (message is JSONRPCRequest) { + val id = message.id.toString() + logger.info { "Received request with ID: $id, method: ${message.method}" } + val responseDeferred = CompletableDeferred() + pendingResponses[id] = responseDeferred + logger.info { "Created deferred response for ID: $id" } + + logger.info { "Invoking onMessage handler" } + _onMessage.invoke(message) + logger.info { "onMessage handler completed" } + + try { + val response = withTimeoutOrNull(10000) { + responseDeferred.await() + } + + if (response != null) { + val jsonResponse = McpJson.encodeToString(response) + call.respondText(jsonResponse, ContentType.Application.Json) + } else { + logger.warn { "Timeout waiting for response to request ID: $id" } + call.respondText( + McpJson.encodeToString( + JSONRPCResponse( + id = message.id, + error = JSONRPCError( + code = ErrorCode.Defined.RequestTimeout, + message = "Request timed out" + ) + ) + ), + ContentType.Application.Json + ) + } + } catch (_: CancellationException) { + logger.warn { "Request cancelled for ID: $id" } + pendingResponses.remove(id) + if (!call.response.isCommitted) { + call.respondText( + McpJson.encodeToString( + JSONRPCResponse( + id = message.id, + error = JSONRPCError( + code = ErrorCode.Defined.ConnectionClosed, + message = "Request cancelled" + ) + ) + ), + ContentType.Application.Json, + HttpStatusCode.ServiceUnavailable + ) + } + } + } else { + call.respondText("", ContentType.Application.Json, HttpStatusCode.Accepted) + } + } catch (e: Exception) { + logger.error(e) { "Error handling request" } + if (!call.response.isCommitted) { + try { + val errorResponse = JSONRPCResponse( + id = RequestId.NumberId(0), + error = JSONRPCError( + code = ErrorCode.Defined.InternalError, + message = "Internal server error: ${e.message}" + ) + ) + + call.respondText( + McpJson.encodeToString(errorResponse), + ContentType.Application.Json, + HttpStatusCode.InternalServerError + ) + } catch (responseEx: Exception) { + logger.error(responseEx) { "Failed to send error response" } + } + } + } + } + + override suspend fun start() { + logger.debug { "Starting HTTP server transport for session: $sessionId" } + } + + override suspend fun send(message: JSONRPCMessage) { + logger.info { "Sending message: $message" } + + if (message is JSONRPCResponse) { + val id = message.id.toString() + logger.info { "Sending response for request ID: $id" } + val deferred = pendingResponses.remove(id) + if (deferred != null) { + logger.info { "Found pending response for ID: $id, completing deferred" } + deferred.complete(message) + return + } else { + logger.warn { "No pending response found for ID: $id" } + } + } else if (message is JSONRPCRequest) { + logger.info { "Sending request with ID: ${message.id}" } + } else if (message is JSONRPCNotification) { + logger.info { "Sending notification: ${message.method}" } + } + + logger.info { "Queueing message for next client request" } + messageQueue.send(message) + } + + override suspend fun close() { + logger.debug { "Closing HTTP server transport for session: $sessionId" } + messageQueue.close() + _onClose.invoke() + } +} + +fun main() { + val server = KotlinServerForTypeScriptClient() + server.start() +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Retry.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Retry.kt new file mode 100644 index 00000000..c39f321a --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Retry.kt @@ -0,0 +1,89 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.utils + +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.InvocationInterceptor +import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation +import org.junit.jupiter.api.extension.ReflectiveInvocationContext +import org.opentest4j.TestAbortedException +import java.lang.reflect.AnnotatedElement +import java.lang.reflect.Method +import java.util.* + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(RetryExtension::class) +annotation class Retry( + val times: Int = 3, + val delayMs: Long = 1000, +) + +class RetryExtension : InvocationInterceptor { + override fun interceptTestMethod( + invocation: Invocation, + invocationContext: ReflectiveInvocationContext, + extensionContext: ExtensionContext + ) { + executeWithRetry(invocation, extensionContext) + } + + private fun resolveRetryAnnotation(extensionContext: ExtensionContext): Retry? { + val classAnn = extensionContext.testClass.flatMap { findRetry(it) } + return classAnn.orElse(null) + } + + private fun findRetry(element: AnnotatedElement): Optional { + return Optional.ofNullable(element.getAnnotation(Retry::class.java)) + } + + private fun executeWithRetry(invocation: Invocation, extensionContext: ExtensionContext) { + val retry = resolveRetryAnnotation(extensionContext) + if (retry == null || retry.times <= 1) { + invocation.proceed() + return + } + + val maxAttempts = retry.times + val delay = retry.delayMs + var lastError: Throwable? = null + + for (attempt in 1..maxAttempts) { + if (attempt > 1 && delay > 0) { + try { + Thread.sleep(delay) + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + break + } + } + + try { + if (attempt == 1) { + invocation.proceed() + } else { + val instance = extensionContext.requiredTestInstance + val testMethod = extensionContext.requiredTestMethod + testMethod.isAccessible = true + testMethod.invoke(instance) + } + return + } catch (t: Throwable) { + if (t is TestAbortedException) throw t + lastError = if (t is java.lang.reflect.InvocationTargetException) t.targetException ?: t else t + if (attempt == maxAttempts) { + println("[Retry] Giving up after $attempt attempts for ${describeTest(extensionContext)}: ${lastError.message}") + throw lastError + } + println("[Retry] Failure on attempt $attempt/${maxAttempts} for ${describeTest(extensionContext)}: ${lastError.message}") + } + } + + throw lastError ?: IllegalStateException("Unexpected state in retry logic") + } + + private fun describeTest(ctx: ExtensionContext): String { + val methodName = ctx.testMethod.map(Method::getName).orElse("") + val className = ctx.testClass.map { it.name }.orElse("") + return "$className#$methodName" + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/TestUtils.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/TestUtils.kt new file mode 100644 index 00000000..0c8b4b2f --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/TestUtils.kt @@ -0,0 +1,78 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.utils + +import io.modelcontextprotocol.kotlin.sdk.CallToolResultBase +import io.modelcontextprotocol.kotlin.sdk.PromptMessageContent +import io.modelcontextprotocol.kotlin.sdk.TextContent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +object TestUtils { + fun runTest(block: suspend () -> T): T { + return runBlocking { + withContext(Dispatchers.IO) { + block() + } + } + } + + fun assertTextContent(content: PromptMessageContent?, expectedText: String) { + assertNotNull(content, "Content should not be null") + assertTrue(content is TextContent, "Content should be TextContent") + assertNotNull(content.text, "Text content should not be null") + assertEquals(expectedText, content.text, "Text content should match") + } + + fun assertCallToolResult(result: Any?, message: String = ""): CallToolResultBase { + assertNotNull(result, "${message}Call tool result should not be null") + assertTrue(result is CallToolResultBase, "${message}Result should be CallToolResultBase") + assertTrue(result.content.isNotEmpty(), "${message}Tool result content should not be empty") + assertNotNull(result.structuredContent, "${message}Tool result structured content should not be null") + + return result + } + + /** + * Asserts that a JSON property has the expected string value. + */ + fun assertJsonProperty(json: JsonObject, property: String, expectedValue: String, message: String = "") { + assertEquals(expectedValue, json[property]?.toString()?.trim('"'), "${message}${property} should match") + } + + /** + * Asserts that a JSON property has the expected numeric value. + */ + fun assertJsonProperty(json: JsonObject, property: String, expectedValue: Number, message: String = "") { + when (expectedValue) { + is Int -> assertEquals( + expectedValue, + (json[property] as? JsonPrimitive)?.content?.toIntOrNull(), + "${message}${property} should match" + ) + + is Double -> assertEquals( + expectedValue, + (json[property] as? JsonPrimitive)?.content?.toDoubleOrNull(), + "${message}${property} should match" + ) + + else -> assertEquals( + expectedValue.toString(), + json[property]?.toString()?.trim('"'), + "${message}${property} should match" + ) + } + } + + /** + * Asserts that a JSON property has the expected boolean value. + */ + fun assertJsonProperty(json: JsonObject, property: String, expectedValue: Boolean, message: String = "") { + assertEquals(expectedValue.toString(), json[property].toString(), "${message}${property} should match") + } +} \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/myClient.ts b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/myClient.ts new file mode 100644 index 00000000..3b5ea75c --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/myClient.ts @@ -0,0 +1,108 @@ +// @ts-ignore +const args = process.argv.slice(2); +const serverUrl = args[0] || 'http://localhost:3001/mcp'; +const toolName = args[1]; +const toolArgs = args.slice(2); +const PROTOCOL_VERSION = "2024-11-05"; + +// @ts-ignore +async function main() { + // @ts-ignore + const sdkDir = process.env.TYPESCRIPT_SDK_DIR; + let Client: any; + let StreamableHTTPClientTransport: any; + if (sdkDir) { + // @ts-ignore + ({Client} = await import(`${sdkDir}/src/client`)); + // @ts-ignore + ({StreamableHTTPClientTransport} = await import(`${sdkDir}/src/client/streamableHttp.js`)); + } else { + // @ts-ignore + ({Client} = await import("../../../../../../../resources/typescript-sdk/src/client")); + // @ts-ignore + ({StreamableHTTPClientTransport} = await import("../../../../../../../resources/typescript-sdk/src/client/streamableHttp.js")); + } + if (!toolName) { + console.log('Usage: npx tsx myClient.ts [server-url] [tool-args...]'); + console.log('Using default server URL:', serverUrl); + console.log('Available utils will be listed after connection'); + } + + console.log(`Connecting to server at ${serverUrl}`); + if (toolName) { + console.log(`Will call tool: ${toolName} with args: ${toolArgs.join(', ')}`); + } + + const client = new Client({ + name: 'test-client', + version: '1.0.0' + }); + + const transport = new StreamableHTTPClientTransport(new URL(serverUrl)); + + try { + await client.connect(transport, {protocolVersion: PROTOCOL_VERSION}); + console.log('Connected to server'); + + const toolsResult = await client.listTools(); + const tools = toolsResult.tools; + console.log('Available utils:', tools.map((t: { name: any; }) => t.name).join(', ')); + + if (!toolName) { + await client.close(); + return; + } + + const tool = tools.find((t: { name: string; }) => t.name === toolName); + if (!tool) { + console.error(`Tool "${toolName}" not found`); + // @ts-ignore + process.exit(1); + } + + const toolArguments = {}; + + if (toolName === "greet" && toolArgs.length > 0) { + toolArguments["name"] = toolArgs[0]; + } else if (tool.input && tool.input.properties) { + const propNames = Object.keys(tool.input.properties); + if (propNames.length > 0 && toolArgs.length > 0) { + toolArguments[propNames[0]] = toolArgs[0]; + } + } + + console.log(`Calling tool ${toolName} with arguments:`, toolArguments); + + const result = await client.callTool({ + name: toolName, + arguments: toolArguments + }); + console.log('Tool result:', result); + + if (result.content) { + for (const content of result.content) { + if (content.type === 'text') { + console.log('Text content:', content.text); + } + } + } + + if (result.structuredContent) { + console.log('Structured content:', JSON.stringify(result.structuredContent, null, 2)); + } + + } catch (error) { + console.error('Error:', error); + // @ts-ignore + process.exit(1); + } finally { + await client.close(); + console.log('Disconnected from server'); + } +} + +main().catch(error => { + console.error('Unhandled error:', error); + // @ts-ignore + process.exit(1); +}); \ No newline at end of file diff --git a/src/jvmTest/kotlin/server/ServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt similarity index 96% rename from src/jvmTest/kotlin/server/ServerTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt index 35e07741..bc6ae014 100644 --- a/src/jvmTest/kotlin/server/ServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt @@ -1,9 +1,8 @@ -package server +package io.modelcontextprotocol.kotlin.sdk.server import io.modelcontextprotocol.kotlin.sdk.CallToolResult import io.modelcontextprotocol.kotlin.sdk.GetPromptResult import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.InMemoryTransport import io.modelcontextprotocol.kotlin.sdk.Method import io.modelcontextprotocol.kotlin.sdk.Prompt import io.modelcontextprotocol.kotlin.sdk.PromptListChangedNotification @@ -15,8 +14,7 @@ import io.modelcontextprotocol.kotlin.sdk.TextResourceContents import io.modelcontextprotocol.kotlin.sdk.Tool import io.modelcontextprotocol.kotlin.sdk.ToolListChangedNotification import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest @@ -42,7 +40,7 @@ class ServerTest { ) // Add a tool - server.addTool("test-tool", "Test Tool", Tool.Input()) { request -> + server.addTool("test-tool", "Test Tool", Tool.Input()) { CallToolResult(listOf(TextContent("Test result"))) } @@ -133,10 +131,10 @@ class ServerTest { ) // Add tools - server.addTool("test-tool-1", "Test Tool 1") { request -> + server.addTool("test-tool-1", "Test Tool 1") { CallToolResult(listOf(TextContent("Test result 1"))) } - server.addTool("test-tool-2", "Test Tool 2") { request -> + server.addTool("test-tool-2", "Test Tool 2") { CallToolResult(listOf(TextContent("Test result 2"))) } @@ -172,7 +170,7 @@ class ServerTest { // Add a prompt val testPrompt = Prompt("test-prompt", "Test Prompt", null) - server.addPrompt(testPrompt) { request -> + server.addPrompt(testPrompt) { GetPromptResult( description = "Test prompt description", messages = listOf() @@ -212,13 +210,13 @@ class ServerTest { // Add prompts val testPrompt1 = Prompt("test-prompt-1", "Test Prompt 1", null) val testPrompt2 = Prompt("test-prompt-2", "Test Prompt 2", null) - server.addPrompt(testPrompt1) { request -> + server.addPrompt(testPrompt1) { GetPromptResult( description = "Test prompt description 1", messages = listOf() ) } - server.addPrompt(testPrompt2) { request -> + server.addPrompt(testPrompt2) { GetPromptResult( description = "Test prompt description 2", messages = listOf() @@ -262,7 +260,7 @@ class ServerTest { name = "Test Resource", description = "A test resource", mimeType = "text/plain" - ) { request -> + ) { ReadResourceResult( contents = listOf( TextResourceContents( @@ -312,7 +310,7 @@ class ServerTest { name = "Test Resource 1", description = "A test resource 1", mimeType = "text/plain" - ) { request -> + ) { ReadResourceResult( contents = listOf( TextResourceContents( @@ -328,7 +326,7 @@ class ServerTest { name = "Test Resource 2", description = "A test resource 2", mimeType = "text/plain" - ) { request -> + ) { ReadResourceResult( contents = listOf( TextResourceContents( @@ -448,7 +446,10 @@ class ServerTest { // Verify the result assertFalse(result, "Removing non-existent resource should return false") - assertFalse(resourceListChangedNotificationReceived, "No notification should be sent when resource doesn't exist") + assertFalse( + resourceListChangedNotificationReceived, + "No notification should be sent when resource doesn't exist" + ) } @Test diff --git a/kotlin-sdk/build.gradle.kts b/kotlin-sdk/build.gradle.kts new file mode 100644 index 00000000..37680171 --- /dev/null +++ b/kotlin-sdk/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("mcp.multiplatform") + id("mcp.publishing") + id("mcp.dokka") + id("mcp.jreleaser") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(project(":kotlin-sdk-core")) + api(project(":kotlin-sdk-client")) + api(project(":kotlin-sdk-server")) + } + } + } +} diff --git a/samples/kotlin-mcp-client/.gitignore b/samples/kotlin-mcp-client/.gitignore deleted file mode 100644 index 108d5673..00000000 --- a/samples/kotlin-mcp-client/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -### IntelliJ IDEA ### -.idea/ -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### Kotlin ### -.kotlin - -### Eclipse ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### -.vscode/ - -### Mac OS ### -.DS_Store \ No newline at end of file diff --git a/samples/kotlin-mcp-client/build.gradle.kts b/samples/kotlin-mcp-client/build.gradle.kts index 6c7f1bbe..58254002 100644 --- a/samples/kotlin-mcp-client/build.gradle.kts +++ b/samples/kotlin-mcp-client/build.gradle.kts @@ -1,25 +1,20 @@ plugins { - kotlin("jvm") version "2.1.10" + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.shadow) application - id("com.github.johnrengelman.shadow") version "8.1.1" } application { mainClass.set("io.modelcontextprotocol.sample.client.MainKt") } - group = "org.example" version = "0.1.0" -val mcpVersion = "0.6.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") + implementation(libs.mcp.kotlin) + implementation(libs.slf4j.simple) + implementation(libs.anthropic.java) } tasks.test { diff --git a/samples/kotlin-mcp-client/gradle.properties b/samples/kotlin-mcp-client/gradle.properties deleted file mode 100644 index 7fc6f1ff..00000000 --- a/samples/kotlin-mcp-client/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -kotlin.code.style=official diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 9bbc975c..00000000 Binary files a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 37f853b1..00000000 --- a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/samples/kotlin-mcp-client/gradlew b/samples/kotlin-mcp-client/gradlew deleted file mode 100755 index faf93008..00000000 --- a/samples/kotlin-mcp-client/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 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 - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# 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" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - 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" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# 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" "$@" diff --git a/samples/kotlin-mcp-client/gradlew.bat b/samples/kotlin-mcp-client/gradlew.bat deleted file mode 100644 index 9d21a218..00000000 --- a/samples/kotlin-mcp-client/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@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 - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -: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 diff --git a/samples/kotlin-mcp-client/settings.gradle.kts b/samples/kotlin-mcp-client/settings.gradle.kts index 39d0cc29..6e2f160d 100644 --- a/samples/kotlin-mcp-client/settings.gradle.kts +++ b/samples/kotlin-mcp-client/settings.gradle.kts @@ -1,11 +1,19 @@ rootProject.name = "kotlin-mcp-client" -plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } } dependencyResolutionManagement { repositories { mavenCentral() } -} + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/samples/kotlin-mcp-server/.gitignore b/samples/kotlin-mcp-server/.gitignore deleted file mode 100644 index b1dff0dd..00000000 --- a/samples/kotlin-mcp-server/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### Kotlin ### -.kotlin - -### Eclipse ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### -.vscode/ - -### Mac OS ### -.DS_Store \ No newline at end of file diff --git a/samples/kotlin-mcp-server/.idea/.gitignore b/samples/kotlin-mcp-server/.idea/.gitignore deleted file mode 100644 index 13566b81..00000000 --- a/samples/kotlin-mcp-server/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/samples/kotlin-mcp-server/.idea/gradle.xml b/samples/kotlin-mcp-server/.idea/gradle.xml deleted file mode 100644 index 2a65317e..00000000 --- a/samples/kotlin-mcp-server/.idea/gradle.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/samples/kotlin-mcp-server/.idea/kotlinc.xml b/samples/kotlin-mcp-server/.idea/kotlinc.xml deleted file mode 100644 index bb449370..00000000 --- a/samples/kotlin-mcp-server/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/samples/kotlin-mcp-server/.idea/misc.xml b/samples/kotlin-mcp-server/.idea/misc.xml deleted file mode 100644 index f16dea79..00000000 --- a/samples/kotlin-mcp-server/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/samples/kotlin-mcp-server/.idea/vcs.xml b/samples/kotlin-mcp-server/.idea/vcs.xml deleted file mode 100644 index fdf1fc87..00000000 --- a/samples/kotlin-mcp-server/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/samples/kotlin-mcp-server/build.gradle.kts b/samples/kotlin-mcp-server/build.gradle.kts index 10520c20..5ecbc43c 100644 --- a/samples/kotlin-mcp-server/build.gradle.kts +++ b/samples/kotlin-mcp-server/build.gradle.kts @@ -4,8 +4,8 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { - kotlin("multiplatform") version "2.1.20" - kotlin("plugin.serialization") version "2.1.20" + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.serialization) } group = "org.example" @@ -43,10 +43,10 @@ kotlin { sourceSets { commonMain.dependencies { - implementation("io.modelcontextprotocol:kotlin-sdk:0.6.0") + implementation(libs.mcp.kotlin) } jvmMain.dependencies { - implementation("org.slf4j:slf4j-nop:2.0.9") + implementation(libs.slf4j.simple) } wasmJsMain.dependencies {} } diff --git a/samples/kotlin-mcp-server/gradle.properties b/samples/kotlin-mcp-server/gradle.properties deleted file mode 100644 index a39b7000..00000000 --- a/samples/kotlin-mcp-server/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -kotlin.code.style=official - -kotlin.daemon.jvmargs=-Xmx2G diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 249e5832..00000000 Binary files a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index d4490fbf..00000000 --- a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Dec 16 12:27:09 CET 2024 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/samples/kotlin-mcp-server/gradlew b/samples/kotlin-mcp-server/gradlew deleted file mode 100755 index 1b6c7873..00000000 --- a/samples/kotlin-mcp-server/gradlew +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 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. -# - -############################################################################## -# -# 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/master/subprojects/plugins/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 - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# 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"' - -# 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 - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# 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 - which java >/dev/null 2>&1 || 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 - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - 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" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - 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 - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# 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" "$@" diff --git a/samples/kotlin-mcp-server/gradlew.bat b/samples/kotlin-mcp-server/gradlew.bat deleted file mode 100644 index ac1b06f9..00000000 --- a/samples/kotlin-mcp-server/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@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 - -@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=. -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%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="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! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/kotlin-mcp-server/settings.gradle.kts b/samples/kotlin-mcp-server/settings.gradle.kts index 6efbc10a..08039844 100644 --- a/samples/kotlin-mcp-server/settings.gradle.kts +++ b/samples/kotlin-mcp-server/settings.gradle.kts @@ -1,4 +1,19 @@ -plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" -} rootProject.name = "kotlin-mcp-server" + +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/samples/weather-stdio-server/.gitignore b/samples/weather-stdio-server/.gitignore deleted file mode 100644 index 108d5673..00000000 --- a/samples/weather-stdio-server/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -### IntelliJ IDEA ### -.idea/ -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### Kotlin ### -.kotlin - -### Eclipse ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### -.vscode/ - -### Mac OS ### -.DS_Store \ No newline at end of file diff --git a/samples/weather-stdio-server/build.gradle.kts b/samples/weather-stdio-server/build.gradle.kts index bd599728..1b019d27 100644 --- a/samples/weather-stdio-server/build.gradle.kts +++ b/samples/weather-stdio-server/build.gradle.kts @@ -1,7 +1,7 @@ plugins { - kotlin("jvm") version "2.1.10" - kotlin("plugin.serialization") version "2.1.10" - id("com.github.johnrengelman.shadow") version "8.1.1" + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.shadow) application } @@ -9,21 +9,21 @@ application { mainClass.set("io.modelcontextprotocol.sample.server.MainKt") } +repositories { + mavenCentral() +} + group = "org.example" version = "0.1.0" -val mcpVersion = "0.6.0" -val slf4jVersion = "2.0.9" -val ktorVersion = "3.1.1" - dependencies { - implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion") - implementation("org.slf4j:slf4j-nop:$slf4jVersion") - implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + implementation(libs.mcp.kotlin) + implementation(libs.slf4j.simple) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) testImplementation(kotlin("test")) - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1") + testImplementation(libs.kotlinx.coroutines.test) } tasks.test { diff --git a/samples/weather-stdio-server/gradle.properties b/samples/weather-stdio-server/gradle.properties deleted file mode 100644 index 7fc6f1ff..00000000 --- a/samples/weather-stdio-server/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -kotlin.code.style=official diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 9bbc975c..00000000 Binary files a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 37f853b1..00000000 --- a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/samples/weather-stdio-server/gradlew b/samples/weather-stdio-server/gradlew deleted file mode 100755 index faf93008..00000000 --- a/samples/weather-stdio-server/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 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 - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# 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" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - 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" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# 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" "$@" diff --git a/samples/weather-stdio-server/gradlew.bat b/samples/weather-stdio-server/gradlew.bat deleted file mode 100644 index 9d21a218..00000000 --- a/samples/weather-stdio-server/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@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 - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -: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 diff --git a/samples/weather-stdio-server/settings.gradle.kts b/samples/weather-stdio-server/settings.gradle.kts index 015fa3f2..1121bf5f 100644 --- a/samples/weather-stdio-server/settings.gradle.kts +++ b/samples/weather-stdio-server/settings.gradle.kts @@ -1,11 +1,19 @@ rootProject.name = "weather-stdio-server" -plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } } dependencyResolutionManagement { repositories { mavenCentral() } -} + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c0ad7518..aa90c769 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,5 @@ +rootProject.name = "kotlin-sdk" + pluginManagement { repositories { mavenCentral() @@ -15,5 +17,13 @@ dependencyResolutionManagement { } } -rootProject.name = "kotlin-sdk" +include(":kotlin-sdk-core") +include(":kotlin-sdk-client") +include(":kotlin-sdk-server") +include(":kotlin-sdk") +include(":kotlin-sdk-test") +// Include sample projects as composite builds +includeBuild("samples/kotlin-mcp-client") +includeBuild("samples/kotlin-mcp-server") +includeBuild("samples/weather-stdio-server") diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt deleted file mode 100644 index 6e2ac447..00000000 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt +++ /dev/null @@ -1,100 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk - -import io.kotest.assertions.json.shouldEqualJson -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject -import kotlin.test.Test -import kotlin.test.assertEquals - -class ToolSerializationTest { - - // see https://docs.anthropic.com/en/docs/build-with-claude/tool-use - /* language=json */ - private val getWeatherToolJson = """ - { - "name": "get_weather", - "description": "Get the current weather in a given location", - "inputSchema": { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state, e.g. San Francisco, CA" - } - }, - "required": ["location"] - }, - "outputSchema": { - "type": "object", - "properties": { - "temperature": { - "type": "number", - "description": "Temperature in celsius" - }, - "conditions": { - "type": "string", - "description": "Weather conditions description" - }, - "humidity": { - "type": "number", - "description": "Humidity percentage" - } - }, - "required": ["temperature", "conditions", "humidity"] - } - } - """.trimIndent() - - val getWeatherTool = Tool( - name = "get_weather", - description = "Get the current weather in a given location", - annotations = null, - inputSchema = Tool.Input( - properties = buildJsonObject { - put("location", buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("The city and state, e.g. San Francisco, CA")) - }) - }, - required = listOf("location") - ), - outputSchema = Tool.Output( - properties = buildJsonObject { - put("temperature", buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Temperature in celsius")) - }) - put("conditions", buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("Weather conditions description")) - }) - put("humidity", buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("Humidity percentage")) - }) - }, - required = listOf("temperature", "conditions", "humidity") - ) - ) - - @Test - fun `should serialize get_weather tool`() { - McpJson.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson - } - - @Test - fun `should deserialize get_weather tool`() { - val tool = McpJson.decodeFromString(getWeatherToolJson) - assertEquals(expected = getWeatherTool, actual = tool) - } - - @Test - fun `should always serialize default value`() { - val json = Json(from = McpJson) { - encodeDefaults = false - } - json.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson - } -} diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/TypesTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/TypesTest.kt deleted file mode 100644 index 1714ded6..00000000 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/TypesTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.client - -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.Request -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlin.test.Test - -class TypesTest { - - @Test - fun testRequestResult() { - val message = - "{\"result\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{\"listChanged\":true},\"resources\":{}},\"serverInfo\":{\"name\":\"jetbrains/proxy\",\"version\":\"0.1.0\"}},\"jsonrpc\":\"2.0\",\"id\":1}" - McpJson.decodeFromString(message) - } - - @Test - fun testRequestError() { - val message = - "{\"method\":\"initialize\", \"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"experimental\":{},\"sampling\":{}},\"clientInfo\":{\"name\":\"test client\",\"version\":\"1.0\"},\"_meta\":{}}" - McpJson.decodeFromString(message) - } - - @Test - fun testJSONRPCMessage() { - val line = "{\"result\":{\"content\":[{\"type\":\"text\"}],\"isError\":false},\"jsonrpc\":\"2.0\",\"id\":4}" - McpJson.decodeFromString(line) - } - - @Test - fun testJSONRPCMessageWithStringId() { - val line = """ - { - "jsonrpc": "2.0", - "method": "initialize", - "id": "ebf9f64a-0", - "params": { - "protocolVersion": "2024-11-05", - "capabilities": {}, - "clientInfo": { - "name": "mcp-java-client", - "version": "0.2.0" - } - } - } - """.trimIndent() - McpJson.decodeFromString(line) - } -} \ No newline at end of file diff --git a/src/jvmTest/kotlin/client/ClientIntegrationTest.kt b/src/jvmTest/kotlin/client/ClientIntegrationTest.kt deleted file mode 100644 index 5eee8179..00000000 --- a/src/jvmTest/kotlin/client/ClientIntegrationTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -package client - -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.ListToolsResult -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport -import kotlinx.coroutines.test.runTest -import kotlinx.io.asSink -import kotlinx.io.asSource -import kotlinx.io.buffered -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test -import java.net.Socket - -class ClientIntegrationTest { - - fun createTransport(): StdioClientTransport { - val socket = Socket("localhost", 3000) - - return StdioClientTransport( - socket.inputStream.asSource().buffered(), - socket.outputStream.asSink().buffered() - ) - } - - @Disabled("This test requires a running server") - @Test - fun testRequestTools() = runTest { - val client = Client( - Implementation("test", "1.0"), - ) - - val transport = createTransport() - try { - client.connect(transport) - - val response: ListToolsResult? = client.listTools() - println(response?.tools) - - } finally { - transport.close() - } - } - -}