Generate MCP classes with OpenAPI Generator (#294)
- Add OpenAPI Generator to build system to automatically generate MCP protocol data classes from the official schema. - Configure code quality tools to exclude generated files - Add deserialization test ## Motivation and Context Writing classes by hand is error-prone, when json schema exists. See #185, Also, autogenerating model classes ensures the SDK is in sync with the protocol and can quickly catch up with its changes (see #218) ## How Has This Been Tested? Regression tests ## Breaking Changes No ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [ ] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions -->
This commit is contained in:
parent
8958f4c731
commit
d962e28ff7
6 changed files with 2606 additions and 0 deletions
|
|
@ -33,10 +33,18 @@ dokka {
|
|||
}
|
||||
}
|
||||
|
||||
ktlint {
|
||||
filter {
|
||||
exclude("**/generated*/**")
|
||||
}
|
||||
}
|
||||
|
||||
kover {
|
||||
reports {
|
||||
filters {
|
||||
includes.classes("io.modelcontextprotocol.kotlin.sdk.*")
|
||||
excludes.classes("io.modelcontextprotocol.kotlin.sdk.models.*") // temporary
|
||||
excludes.classes("io.modelcontextprotocol.kotlin.sdk.models.infrastructure.*") // generated
|
||||
}
|
||||
total {
|
||||
log {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref =
|
|||
|
||||
# Testing
|
||||
awaitility = { group = "org.awaitility", name = "awaitility-kotlin", version.ref = "awaitility" }
|
||||
kotest-assertions-core = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotest" }
|
||||
kotest-assertions-json = { group = "io.kotest", name = "kotest-assertions-json", version.ref = "kotest" }
|
||||
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||
ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" }
|
||||
|
|
|
|||
29
kotlin-sdk-core/.openapi-generator-ignore
Normal file
29
kotlin-sdk-core/.openapi-generator-ignore
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# OpenAPI Generator Ignore
|
||||
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||
|
||||
# Use this file to prevent files from being overwritten by the generator.
|
||||
# The patterns follow closely to .gitignore or .dockerignore.
|
||||
|
||||
# As an example, the C# client generator defines ApiClient.cs.
|
||||
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||
**/auth/**
|
||||
**/infrastructure/**
|
||||
!**/Base64ByteArray.kt
|
||||
!**/Bytes.kt
|
||||
|
||||
|
||||
## Exclude Models which are causing problems
|
||||
**/JSONRPCResponse.kt
|
||||
**/Result.kt
|
||||
**/ListToolsResult.kt
|
||||
**/InitializeRequest.kt
|
||||
**/InitializeResult.kt
|
||||
**/ServerCapabilities.kt
|
||||
**/InitializeRequestParams.kt
|
||||
**/CreateMessageRequest.kt
|
||||
**/CreateMessageRequestParams.kt
|
||||
**/ClientCapabilities.kt
|
||||
**/Tool.kt
|
||||
**/ToolInputSchema.kt
|
||||
**/ToolOutputSchema.kt
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,67 @@
|
|||
@file:OptIn(ExperimentalWasmDsl::class)
|
||||
|
||||
import org.jetbrains.dokka.gradle.tasks.DokkaGenerateTask
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
|
||||
|
||||
plugins {
|
||||
id("mcp.multiplatform")
|
||||
id("mcp.publishing")
|
||||
id("mcp.dokka")
|
||||
alias(libs.plugins.kotlinx.binary.compatibility.validator)
|
||||
id("org.openapi.generator") version "7.15.0"
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompilationTask<*>>().configureEach {
|
||||
dependsOn(tasks.openApiGenerate)
|
||||
}
|
||||
|
||||
tasks.named("runKtlintCheckOverCommonMainSourceSet") {
|
||||
dependsOn(tasks.openApiGenerate)
|
||||
}
|
||||
|
||||
// Also ensure it runs before other relevant tasks
|
||||
tasks.withType<DokkaGenerateTask>().configureEach {
|
||||
dependsOn(tasks.openApiGenerate)
|
||||
}
|
||||
|
||||
tasks.withType<Task>().configureEach {
|
||||
if (name.lowercase().contains("sourcesjar")) {
|
||||
dependsOn(tasks.openApiGenerate)
|
||||
}
|
||||
}
|
||||
|
||||
openApiGenerate {
|
||||
val schemaVersion = "2025-03-26" // or "2025-06-18" or "draft"
|
||||
val schemaUrl =
|
||||
// "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema/$schemaVersion/schema.json"
|
||||
"https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/tags/$schemaVersion/schema/$schemaVersion/schema.json"
|
||||
generatorName = "kotlin"
|
||||
remoteInputSpec = schemaUrl
|
||||
packageName = "io.modelcontextprotocol.kotlin.sdk.models"
|
||||
modelPackage = "io.modelcontextprotocol.kotlin.sdk.models"
|
||||
apiPackage = "io.modelcontextprotocol.kotlin.sdk.api"
|
||||
generateModelTests = false
|
||||
generateModelDocumentation = false
|
||||
cleanupOutput = false
|
||||
skipValidateSpec = true // do not validate spec
|
||||
library = "multiplatform"
|
||||
ignoreFileOverride = "${layout.projectDirectory}/.openapi-generator-ignore"
|
||||
globalProperties.set(
|
||||
mapOf(
|
||||
"supportingFiles" to "",
|
||||
"models" to "", // or generate all models
|
||||
),
|
||||
)
|
||||
configOptions.set(
|
||||
mapOf(
|
||||
"omitGradleWrapper" to "true",
|
||||
"enumPropertyNaming" to "UPPERCASE",
|
||||
"dateLibrary" to "kotlinx-datetime",
|
||||
"explicitApi" to "true",
|
||||
"modelMutable" to "false",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Generation library versions
|
||||
|
|
@ -53,6 +108,7 @@ kotlin {
|
|||
sourceSets {
|
||||
commonMain {
|
||||
kotlin.srcDir(generateLibVersion)
|
||||
kotlin.srcDir("build/generate-resources/main/src/commonMain/kotlin")
|
||||
dependencies {
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
|
|
@ -66,6 +122,7 @@ kotlin {
|
|||
commonTest {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation(libs.kotest.assertions.core)
|
||||
implementation(libs.kotest.assertions.json)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.models
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.modelcontextprotocol.kotlin.sdk.ProgressNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.RequestId
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.McpJson
|
||||
import kotlin.test.Test
|
||||
|
||||
class ProgressNotificationsTest {
|
||||
|
||||
/**
|
||||
* https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress-flow
|
||||
*/
|
||||
@Test
|
||||
fun `Read ProgressNotifications with string token`() {
|
||||
//language=json
|
||||
val json = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/progress",
|
||||
"params": {
|
||||
"progressToken": "abc123",
|
||||
"progress": 50,
|
||||
"total": 100,
|
||||
"message": "Reticulating splines..."
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = McpJson.decodeFromString<ProgressNotification>(json)
|
||||
|
||||
result shouldBe ProgressNotification(
|
||||
params = ProgressNotification.Params(
|
||||
progressToken = RequestId.StringId("abc123"),
|
||||
progress = 50.0,
|
||||
message = "Reticulating splines...",
|
||||
total = 100.0,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress-flow
|
||||
*/
|
||||
@Test
|
||||
fun `Read ProgressNotifications with integer token`() {
|
||||
//language=json
|
||||
val json = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/progress",
|
||||
"params": {
|
||||
"progressToken": 100500,
|
||||
"progress": 50,
|
||||
"total": 100,
|
||||
"message": "Reticulating splines..."
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = McpJson.decodeFromString<ProgressNotification>(json)
|
||||
result shouldBe ProgressNotification(
|
||||
params = ProgressNotification.Params(
|
||||
progressToken = RequestId.NumberId(100500),
|
||||
progress = 50.0,
|
||||
message = "Reticulating splines...",
|
||||
total = 100.0,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue