├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ ├── docs.yml │ ├── release.yml │ └── test-build.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlin-js-store └── yarn.lock ├── mcp4k-build ├── build.gradle.kts ├── gradle.properties └── settings.gradle.kts ├── mcp4k-compiler ├── build.gradle.kts ├── gradle.properties └── src │ └── main │ ├── kotlin │ └── sh │ │ └── ondr │ │ └── mcp4k │ │ └── compiler │ │ ├── Mcp4kCommandLineProcessor.kt │ │ ├── Mcp4kCompilerPluginRegistrar.kt │ │ └── Mcp4kIrTransformer.kt │ └── resources │ └── META-INF │ └── services │ ├── org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor │ └── org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar ├── mcp4k-file-provider ├── README.md ├── build.gradle.kts ├── gradle.properties └── src │ ├── commonMain │ └── kotlin │ │ └── sh │ │ └── ondr │ │ └── mcp4k │ │ └── fileprovider │ │ ├── DiscreteFileProvider.kt │ │ ├── File.kt │ │ ├── MimeTypeDetector.kt │ │ └── TemplateFileProvider.kt │ └── commonTest │ └── kotlin │ └── sh │ └── ondr │ └── mcp4k │ └── fileprovider │ └── test │ ├── DiscreteFileProviderTest.kt │ └── TemplateFileProviderTest.kt ├── mcp4k-gradle ├── build.gradle.kts ├── gradle.properties └── src │ └── main │ ├── kotlin │ └── sh │ │ └── ondr │ │ └── mcp4k │ │ └── gradle │ │ └── Mcp4kGradlePlugin.kt │ └── resources │ └── META-INF │ └── services │ └── org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin ├── mcp4k-ksp ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ ├── kotlin │ │ └── sh │ │ │ └── ondr │ │ │ └── mcp4k │ │ │ └── ksp │ │ │ ├── KdocUtil.kt │ │ │ ├── Mcp4kProcessor.kt │ │ │ ├── Mcp4kProcessorProvider.kt │ │ │ ├── ParamInfo.kt │ │ │ ├── Util.kt │ │ │ ├── generateInitializer.kt │ │ │ ├── prompts │ │ │ ├── PromptMeta.kt │ │ │ ├── generatePromptHandlersFile.kt │ │ │ ├── generatePromptParamsClass.kt │ │ │ └── validatePrompt.kt │ │ │ └── tools │ │ │ ├── ToolMeta.kt │ │ │ ├── generateToolHandlersFile.kt │ │ │ ├── generateToolParamsClass.kt │ │ │ └── validateTool.kt │ └── resources │ │ └── META-INF │ │ └── services │ │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider │ └── test │ ├── kotlin │ └── sh │ │ └── ondr │ │ └── mcp4k │ │ └── ksp │ │ ├── BaseKspTest.kt │ │ ├── KDocParserTest.kt │ │ ├── KspTest.kt │ │ └── TestUtil.kt │ └── resources │ └── expected │ └── Mcp4kGeneratedToolRegistryInitializer.kt.expected ├── mcp4k-runtime ├── build.gradle.kts ├── gradle.properties └── src │ └── commonMain │ └── kotlin │ └── sh │ └── ondr │ └── mcp4k │ ├── runtime │ ├── Client.kt │ ├── McpComponent.kt │ ├── Server.kt │ ├── ServerContext.kt │ ├── annotation │ │ ├── McpPrompt.kt │ │ └── McpTool.kt │ ├── core │ │ ├── ClientApprovable.kt │ │ ├── Global.kt │ │ ├── Util.kt │ │ └── pagination │ │ │ ├── PaginatedEndpoint.kt │ │ │ ├── PaginatedRequestFactory.kt │ │ │ └── paginate.kt │ ├── error │ │ ├── ErrorUtil.kt │ │ ├── MethodNotFoundException.kt │ │ ├── MissingRequiredArgumentException.kt │ │ ├── ResourceNotFoundException.kt │ │ └── UnknownArgumentException.kt │ ├── prompts │ │ ├── McpPromptHandler.kt │ │ └── promptDsl.kt │ ├── resources │ │ ├── ResourceProvider.kt │ │ └── ResourceProviderManager.kt │ ├── sampling │ │ └── SamplingProvider.kt │ ├── serialization │ │ ├── JsonRpcMessageSerializer.kt │ │ ├── ResourceContentsSerializer.kt │ │ ├── SerializationUtil.kt │ │ └── SerializersModule.kt │ ├── tools │ │ ├── McpToolHandler.kt │ │ └── McpToolUtil.kt │ └── transport │ │ ├── ChannelTransport.kt │ │ ├── StdioTransport.kt │ │ ├── Transport.kt │ │ └── TransportUtil.kt │ ├── schema │ ├── capabilities │ │ ├── ClientCapabilities.kt │ │ ├── Implementation.kt │ │ ├── InitializeRequest.kt │ │ ├── InitializeResult.kt │ │ ├── InitializedNotification.kt │ │ ├── PromptsCapability.kt │ │ ├── ResourcesCapability.kt │ │ ├── RootsCapability.kt │ │ ├── ServerCapabilities.kt │ │ └── ToolsCapability.kt │ ├── completion │ │ ├── CompleteRef.kt │ │ ├── CompleteRequest.kt │ │ └── CompleteResult.kt │ ├── content │ │ ├── Content.kt │ │ ├── EmbeddedResourceContent.kt │ │ ├── ImageContent.kt │ │ ├── PromptContent.kt │ │ ├── SamplingContent.kt │ │ ├── TextContent.kt │ │ └── ToolContent.kt │ ├── core │ │ ├── Annotated.kt │ │ ├── Annotations.kt │ │ ├── EmptyParams.kt │ │ ├── EmptyResult.kt │ │ ├── JsonRpcError.kt │ │ ├── JsonRpcErrorCodes.kt │ │ ├── JsonRpcMessage.kt │ │ ├── JsonRpcNotification.kt │ │ ├── JsonRpcRequest.kt │ │ ├── JsonRpcResponse.kt │ │ ├── PaginatedResult.kt │ │ ├── PingRequest.kt │ │ ├── ProgressNotification.kt │ │ ├── Result.kt │ │ └── Role.kt │ ├── logging │ │ ├── LoggingLevel.kt │ │ ├── LoggingMessageNotification.kt │ │ └── SetLoggingLevelRequest.kt │ ├── prompts │ │ ├── GetPromptRequest.kt │ │ ├── GetPromptResult.kt │ │ ├── ListPromptsRequest.kt │ │ ├── ListPromptsResult.kt │ │ ├── Prompt.kt │ │ ├── PromptArgument.kt │ │ ├── PromptListChangedNotification.kt │ │ └── PromptMessage.kt │ ├── resources │ │ ├── ListResourceTemplatesRequest.kt │ │ ├── ListResourceTemplatesResult.kt │ │ ├── ListResourcesRequest.kt │ │ ├── ListResourcesResult.kt │ │ ├── ReadResourceRequest.kt │ │ ├── ReadResourceResult.kt │ │ ├── Resource.kt │ │ ├── ResourceContents.kt │ │ ├── ResourceListChangedNotification.kt │ │ ├── ResourceTemplate.kt │ │ ├── ResourceUpdatedNotification.kt │ │ ├── SubscribeRequest.kt │ │ └── UnsubscribeRequest.kt │ ├── roots │ │ ├── ListRootsRequest.kt │ │ ├── ListRootsResult.kt │ │ ├── Root.kt │ │ └── RootsListChangedNotification.kt │ ├── sampling │ │ ├── CreateMessageRequest.kt │ │ ├── CreateMessageResult.kt │ │ ├── IncludeContext.kt │ │ ├── ModelHint.kt │ │ ├── ModelPreferences.kt │ │ └── SamplingMessage.kt │ └── tools │ │ ├── CallToolRequest.kt │ │ ├── CallToolResult.kt │ │ ├── ListToolsRequest.kt │ │ ├── ListToolsResult.kt │ │ ├── Tool.kt │ │ └── ToolListChangedNotification.kt │ └── test │ └── TestUtil.kt ├── mcp4k-test ├── build.gradle.kts ├── gradle.properties └── src │ ├── commonMain │ └── kotlin │ │ └── sh │ │ └── ondr │ │ └── mcp4k │ │ └── test │ │ ├── prompts │ │ └── TestPrompts.kt │ │ └── tools │ │ ├── AsyncTools.kt │ │ ├── ContextTools.kt │ │ └── TestTools.kt │ └── commonTest │ └── kotlin │ └── sh │ └── ondr │ └── mcp4k │ └── test │ ├── integration │ ├── CancellationTest.kt │ ├── InitializationTest.kt │ ├── PromptErrorTest.kt │ ├── PromptTest.kt │ ├── RootsTest.kt │ ├── SamplingTest.kt │ ├── ServerContextTest.kt │ └── ToolTest.kt │ ├── resources │ └── ResourceProviderManagerTest.kt │ ├── schema │ ├── JsonRpcMessageTest.kt │ └── messages │ │ └── content │ │ └── SimpleDeserializationTest.kt │ └── transport │ └── ChannelTransportTest.kt ├── mcp4k.svg └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{kt,gradle.kts}] 4 | indent_style = tab 5 | indent_size = 2 6 | ktlint_standard_max-line-length=disabled 7 | ktlint_standard_filename=disabled 8 | ktlint_standard_value-argument-comment=disabled 9 | ktlint_standard_value-parameter-comment=disabled 10 | ktlint_standard_multiline-expression-wrapping=disabled 11 | ktlint_standard_string-template-indent=disabled 12 | ktlint_standard_function-expression-body=disabled 13 | ktlint_standard_class-signature=disabled 14 | ktlint_standard_chain-method-continuation=disabled 15 | 16 | [{mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/Result.kt,mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/JsonRpcNotification.kt}] 17 | ktlint_standard_backing-property-naming=disabled 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force Unix line endings for all text files 2 | * text=auto eol=lf 3 | 4 | # Explicitly set file types 5 | *.kt text eol=lf 6 | *.kts text eol=lf 7 | *.java text eol=lf 8 | *.gradle text eol=lf 9 | *.xml text eol=lf 10 | *.json text eol=lf 11 | *.yml text eol=lf 12 | *.yaml text eol=lf 13 | *.md text eol=lf 14 | *.txt text eol=lf 15 | *.properties text eol=lf 16 | *.toml text eol=lf 17 | 18 | # Binary files (don't modify) 19 | *.jar binary 20 | *.png binary 21 | *.jpg binary 22 | *.jpeg binary 23 | *.gif binary 24 | *.ico binary 25 | *.pdf binary -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest, macos-14, windows-latest] 19 | 20 | runs-on: ${{ matrix.os }} 21 | timeout-minutes: 45 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Configure Git line endings on Windows 30 | if: runner.os == 'Windows' 31 | run: | 32 | git config --global core.autocrlf false 33 | git config --global core.eol lf 34 | 35 | - name: Setup Java 36 | uses: actions/setup-java@v4 37 | with: 38 | distribution: temurin 39 | java-version: 21 40 | cache: gradle 41 | 42 | - name: Setup Gradle 43 | uses: gradle/actions/setup-gradle@v3 44 | with: 45 | add-job-summary: on-failure 46 | 47 | - name: Cache Kotlin/Native 48 | uses: actions/cache@v4 49 | with: 50 | path: | 51 | ~/.konan 52 | ~/.gradle/kotlin 53 | key: ${{ runner.os }}-konan-${{ hashFiles('gradle/libs.versions.toml') }} 54 | restore-keys: | 55 | ${{ runner.os }}-konan- 56 | 57 | - name: Check code formatting 58 | run: ./gradlew spotlessCheck --no-daemon 59 | 60 | - name: Build and Test 61 | run: ./gradlew build --no-daemon --stacktrace --parallel --build-cache 62 | 63 | - name: Upload test results 64 | if: always() 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: test-results-${{ matrix.os }} 68 | path: | 69 | **/build/test-results/ 70 | **/build/reports/tests/ 71 | retention-days: 7 -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Dokka Documentation 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Set up JDK 24 | uses: actions/setup-java@v4 25 | with: 26 | java-version: '21' 27 | distribution: 'temurin' 28 | 29 | - name: Setup Gradle 30 | uses: gradle/actions/setup-gradle@v4 31 | 32 | - name: Generate Dokka documentation 33 | run: ./gradlew dokkaGenerate 34 | 35 | - name: Setup Pages 36 | uses: actions/configure-pages@v5 37 | 38 | - name: Upload artifact 39 | uses: actions/upload-pages-artifact@v3 40 | with: 41 | path: './build/dokka/html' 42 | 43 | deploy: 44 | environment: 45 | name: github-pages 46 | url: ${{ steps.deployment.outputs.page_url }} 47 | runs-on: ubuntu-latest 48 | needs: build 49 | steps: 50 | - name: Deploy to GitHub Pages 51 | id: deployment 52 | uses: actions/deploy-pages@v4 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | inputs: 9 | dry_run: 10 | description: 'Dry run (build only, no publishing)' 11 | required: false 12 | type: boolean 13 | default: true 14 | 15 | permissions: 16 | contents: read 17 | 18 | concurrency: 19 | group: release 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | publish: 24 | runs-on: macos-14 25 | timeout-minutes: 60 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Setup Java 34 | uses: actions/setup-java@v4 35 | with: 36 | distribution: temurin 37 | java-version: 21 38 | cache: gradle 39 | 40 | - name: Install MinGW toolchain 41 | run: | 42 | brew update && brew install mingw-w64 43 | echo "$(brew --prefix)/opt/mingw-w64/bin" >> $GITHUB_PATH 44 | 45 | - name: Setup Gradle 46 | uses: gradle/actions/setup-gradle@v3 47 | with: 48 | add-job-summary: on-failure 49 | 50 | - name: Cache Kotlin/Native 51 | uses: actions/cache@v4 52 | with: 53 | path: | 54 | ~/.konan 55 | ~/.gradle/kotlin 56 | key: ${{ runner.os }}-konan-${{ hashFiles('gradle/libs.versions.toml') }} 57 | restore-keys: | 58 | ${{ runner.os }}-konan- 59 | 60 | - name: Build all targets (dry run) 61 | if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} 62 | run: | 63 | echo "🔨 DRY RUN: Building all targets without publishing..." 64 | ./gradlew build --no-daemon --stacktrace --parallel --build-cache 65 | 66 | echo "📦 Would publish these artifacts:" 67 | ./gradlew :mcp4k-runtime:publishToMavenLocal :mcp4k-ksp:publishToMavenLocal :mcp4k-compiler:publishToMavenLocal :mcp4k-gradle:publishToMavenLocal :mcp4k-file-provider:publishToMavenLocal --dry-run 68 | 69 | - name: Publish to Maven Central 70 | if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) }} 71 | env: 72 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} 73 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} 74 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }} 75 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }} 76 | run: | 77 | ./gradlew \ 78 | publishAndReleaseToMavenCentral \ 79 | --no-daemon --stacktrace --parallel --build-cache -------------------------------------------------------------------------------- /.github/workflows/test-build.yml: -------------------------------------------------------------------------------- 1 | name: Test Build (Dry Run) 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | test_publishing: 7 | description: 'Test publishing to local repository' 8 | required: false 9 | type: boolean 10 | default: false 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | test-multiplatform-build: 17 | runs-on: macos-14 18 | timeout-minutes: 60 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Setup Java 27 | uses: actions/setup-java@v4 28 | with: 29 | distribution: temurin 30 | java-version: 21 31 | cache: gradle 32 | 33 | - name: Install MinGW toolchain 34 | run: | 35 | brew update && brew install mingw-w64 36 | echo "$(brew --prefix)/opt/mingw-w64/bin" >> $GITHUB_PATH 37 | 38 | - name: Setup Gradle 39 | uses: gradle/actions/setup-gradle@v3 40 | with: 41 | add-job-summary: on-failure 42 | 43 | - name: Cache Kotlin/Native 44 | uses: actions/cache@v4 45 | with: 46 | path: | 47 | ~/.konan 48 | ~/.gradle/kotlin 49 | key: ${{ runner.os }}-konan-${{ hashFiles('gradle/libs.versions.toml') }} 50 | restore-keys: | 51 | ${{ runner.os }}-konan- 52 | 53 | - name: Build all targets 54 | run: | 55 | echo "🔨 Building all platforms including Windows (mingwX64)..." 56 | echo "Host OS: $(uname -s)" 57 | echo "Available targets on this host:" 58 | echo " - JVM" 59 | echo " - JavaScript (Browser & Node.js)" 60 | echo " - Apple: macosX64, macosArm64, iosArm64, iosX64, iosSimulatorArm64" 61 | echo " - Windows: mingwX64 (via cross-compilation)" 62 | echo "" 63 | ./gradlew build --no-daemon --stacktrace --parallel --build-cache 64 | 65 | - name: List built artifacts 66 | run: | 67 | echo "📦 Built artifacts:" 68 | find . -name "*.klib" -o -name "*.jar" | grep -E "(mcp4k-runtime|mcp4k-test|mcp4k-file-provider)" | sort 69 | 70 | - name: Test local publishing (optional) 71 | if: ${{ inputs.test_publishing }} 72 | run: | 73 | echo "📤 Testing publish to local Maven repository..." 74 | ./gradlew publishToMavenLocal --no-daemon --stacktrace --parallel --build-cache 75 | 76 | echo "✅ Published artifacts in local repository:" 77 | ls -la ~/.m2/repository/sh/ondr/mcp4k/ 78 | 79 | - name: Upload build artifacts 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: build-artifacts-all-platforms 83 | path: | 84 | **/build/libs/ 85 | **/build/classes/ 86 | **/build/compileSync/ 87 | retention-days: 7 88 | 89 | test-matrix-build: 90 | strategy: 91 | fail-fast: false 92 | matrix: 93 | os: [ubuntu-latest, macos-14, windows-latest] 94 | 95 | runs-on: ${{ matrix.os }} 96 | timeout-minutes: 45 97 | 98 | steps: 99 | - name: Checkout 100 | uses: actions/checkout@v4 101 | with: 102 | fetch-depth: 0 103 | 104 | - name: Setup Java 105 | uses: actions/setup-java@v4 106 | with: 107 | distribution: temurin 108 | java-version: 21 109 | cache: gradle 110 | 111 | - name: Setup Gradle 112 | uses: gradle/actions/setup-gradle@v3 113 | with: 114 | add-job-summary: on-failure 115 | 116 | - name: Cache Kotlin/Native 117 | uses: actions/cache@v4 118 | with: 119 | path: | 120 | ~/.konan 121 | ~/.gradle/kotlin 122 | key: ${{ runner.os }}-konan-${{ hashFiles('gradle/libs.versions.toml') }} 123 | restore-keys: | 124 | ${{ runner.os }}-konan- 125 | 126 | - name: Test build on ${{ matrix.os }} 127 | run: ./gradlew build --no-daemon --stacktrace --parallel --build-cache 128 | 129 | - name: Report supported targets 130 | run: | 131 | echo "✅ Successfully built on ${{ matrix.os }}" 132 | echo "Targets built:" 133 | ./gradlew -q :mcp4k-runtime:tasks --group=build | grep -E "(compileKotlin|assemble)" || true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gradle 3 | .idea 4 | .kotlin 5 | build 6 | *.iml 7 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask 2 | 3 | plugins { 4 | alias(libs.plugins.kotlin.jvm).apply(false) 5 | alias(libs.plugins.kotlin.multiplatform).apply(false) 6 | alias(libs.plugins.gradle.versions) 7 | alias(libs.plugins.maven.publish).apply(false) 8 | alias(libs.plugins.ondrsh.mcp4k).apply(false) 9 | alias(libs.plugins.spotless) 10 | alias(libs.plugins.dokka) 11 | } 12 | 13 | allprojects { 14 | version = property("VERSION_NAME") as String 15 | 16 | configurations.configureEach { 17 | resolutionStrategy.dependencySubstitution { 18 | substitute(module("sh.ondr.mcp4k:mcp4k-compiler")) 19 | .using(project(":mcp4k-compiler")) 20 | } 21 | } 22 | } 23 | 24 | configure { 25 | kotlin { 26 | target("**/*.kt") 27 | targetExclude("**/build/**/*.kt") 28 | ktlint() 29 | lineEndings = com.diffplug.spotless.LineEnding.UNIX 30 | } 31 | kotlinGradle { 32 | target("**/*.gradle.kts") 33 | ktlint() 34 | lineEndings = com.diffplug.spotless.LineEnding.UNIX 35 | } 36 | } 37 | 38 | // Configure Dokka v2 aggregation - only user-facing modules 39 | dependencies { 40 | dokka(project(":mcp4k-runtime")) // Main API: @McpTool, @McpPrompt 41 | dokka(project(":mcp4k-gradle")) // Gradle plugin: id("sh.ondr.mcp4k") 42 | dokka(project(":mcp4k-file-provider")) // File provider utilities 43 | // Internal modules (mcp4k-ksp, mcp4k-compiler) are not included in public docs 44 | } 45 | 46 | dokka { 47 | dokkaPublications.html { 48 | outputDirectory.set(rootDir.resolve("build/dokka/html")) 49 | } 50 | } 51 | 52 | tasks.withType { 53 | rejectVersionIf { 54 | isNonStable(candidate.version) 55 | } 56 | } 57 | 58 | fun isNonStable(version: String): Boolean { 59 | val upperVersion = version.uppercase() 60 | val unstableKeywords = listOf( 61 | "ALPHA", 62 | "BETA", 63 | "RC", 64 | "CR", 65 | "M", 66 | "PREVIEW", 67 | "SNAPSHOT", 68 | "DEV", 69 | "PRE", 70 | "BUILD", 71 | "NIGHTLY", 72 | "CANARY", 73 | "EAP", 74 | "MILESTONE", 75 | ) 76 | 77 | return unstableKeywords.any { upperVersion.contains(it) } 78 | } 79 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # mcp4k 2 | GROUP=sh.ondr.mcp4k 3 | VERSION_NAME=0.4.6 4 | 5 | # Flag to signal we are building within the repo 6 | sh.ondr.mcp4k.internal=true 7 | 8 | # Kotlin 9 | kotlin.code.style=official 10 | kapt.use.k2=true 11 | ksp.useKSP2=true 12 | 13 | # Dokka v2 configuration 14 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 15 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 16 | 17 | # Gradle JVM memory settings 18 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=768m 19 | org.gradle.parallel=true 20 | org.gradle.caching=true 21 | 22 | # Publishing config 23 | SONATYPE_HOST=CENTRAL_PORTAL 24 | RELEASE_SIGNING_ENABLED=true 25 | POM_DESCRIPTION=mcp4k - Kotlin Multiplatform MCP Framework 26 | POM_URL=https://github.com/ondrsh/mcp4k 27 | 28 | POM_SCM_URL=https://github.com/ondrsh/mcp4k 29 | POM_SCM_CONNECTION=scm:git:git://github.com/ondrsh/mcp4k.git 30 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/ondrsh/mcp4k.git 31 | 32 | POM_LICENCE_NAME=Apache License 2.0 33 | POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0 34 | POM_LICENCE_DIST=repo 35 | 36 | POM_DEVELOPER_ID=ondrsh 37 | POM_DEVELOPER_NAME=Andreas Toth 38 | POM_DEVELOPER_URL=https://github.com/ondrsh 39 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | atomic = "0.29.0" 3 | build-config = "5.6.8" 4 | coroutines = "1.10.2" 5 | dokka = "2.0.0" 6 | gradle-versions = "0.53.0" 7 | mcp4k = "0.2.1" # Will be substituted 8 | koja = "0.4.6" 9 | kotlin = "2.2.21" 10 | kotlinx-io-core = "0.7.0" 11 | kotlinx-serialization = "1.9.0" 12 | ksp-api = "2.3.0" 13 | maven-publish = "0.34.0" 14 | okio = "3.16.0" 15 | spotless = "7.2.1" 16 | 17 | [plugins] 18 | build-config = { id = "com.github.gmazzo.buildconfig", version.ref = "build-config" } 19 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } 20 | gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } 21 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 22 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 23 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 24 | maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } 25 | ondrsh-mcp4k = { id = "sh.ondr.mcp4k", version.ref = "mcp4k" } 26 | spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } 27 | 28 | [libraries] 29 | koja-runtime = { module = "sh.ondr.koja:koja-runtime", version.ref = "koja" } 30 | koja-gradle = { module = "sh.ondr.koja:koja-gradle", version.ref = "koja" } 31 | kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } 32 | kotlin-gradle-api = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin-api", version.ref = "kotlin" } 33 | kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } 34 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 35 | kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomic" } 36 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } 37 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } 38 | kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io-core"} 39 | kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } 40 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 41 | ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp-api" } 42 | ksp-gradle-plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp-api" } 43 | square-okio = { module = "com.squareup.okio:okio", version.ref = "okio" } 44 | square-okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" } 45 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ondrsh/mcp4k/5871cb73899c8673c2578816812bc2a64ab3cae4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /mcp4k-build/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.build.config) 3 | } 4 | 5 | allprojects { 6 | version = "mcp4k-internal" 7 | 8 | repositories { 9 | mavenCentral() 10 | gradlePluginPortal() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /mcp4k-build/gradle.properties: -------------------------------------------------------------------------------- 1 | # Dokka 2 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled -------------------------------------------------------------------------------- /mcp4k-build/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | versionCatalogs { 3 | create("libs") { 4 | from(files("../gradle/libs.versions.toml")) 5 | } 6 | } 7 | } 8 | 9 | rootProject.name = "mcp4k-build" 10 | 11 | // Re-expose `mcp4k-gradle` project so we can substitute the GAV coords with it when building internally 12 | include(":gradle-plugin") 13 | project(":gradle-plugin").projectDir = file("../mcp4k-gradle") 14 | -------------------------------------------------------------------------------- /mcp4k-compiler/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.kotlin.jvm) 5 | alias(libs.plugins.maven.publish) 6 | } 7 | 8 | dependencies { 9 | compileOnly(libs.kotlin.compiler.embeddable) 10 | testImplementation(libs.kotlin.compiler.embeddable) 11 | } 12 | 13 | java { 14 | toolchain.languageVersion.set(JavaLanguageVersion.of(11)) 15 | } 16 | 17 | tasks.withType().configureEach { 18 | compilerOptions.jvmTarget.set(JvmTarget.JVM_11) 19 | } 20 | -------------------------------------------------------------------------------- /mcp4k-compiler/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=mcp4k-compiler 2 | POM_NAME=mcp4k Compiler plugin 3 | -------------------------------------------------------------------------------- /mcp4k-compiler/src/main/kotlin/sh/ondr/mcp4k/compiler/Mcp4kCommandLineProcessor.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.compiler 2 | 3 | import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption 4 | import org.jetbrains.kotlin.compiler.plugin.CliOption 5 | import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor 6 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 7 | import org.jetbrains.kotlin.config.CompilerConfiguration 8 | import org.jetbrains.kotlin.config.CompilerConfigurationKey 9 | 10 | object Mcp4kConfigurationKeys { 11 | val ENABLED = CompilerConfigurationKey("enabled") 12 | val IS_TEST_SET = CompilerConfigurationKey("isTestSet") 13 | } 14 | 15 | @OptIn(ExperimentalCompilerApi::class) 16 | class Mcp4kCommandLineProcessor : CommandLineProcessor { 17 | override val pluginId = "sh.ondr.mcp4k" 18 | 19 | override val pluginOptions = listOf( 20 | CliOption( 21 | optionName = "enabled", 22 | valueDescription = "true|false", 23 | description = "Whether mcp4k plugin is enabled", 24 | required = false, 25 | allowMultipleOccurrences = false, 26 | ), 27 | CliOption( 28 | optionName = "isTestSet", 29 | valueDescription = "true|false", 30 | description = "Whether this is a test compilation", 31 | required = false, 32 | allowMultipleOccurrences = false, 33 | ), 34 | ) 35 | 36 | override fun processOption( 37 | option: AbstractCliOption, 38 | value: String, 39 | configuration: CompilerConfiguration, 40 | ) { 41 | when (option.optionName) { 42 | "enabled" -> configuration.put(Mcp4kConfigurationKeys.ENABLED, value.toBoolean()) 43 | "isTestSet" -> configuration.put(Mcp4kConfigurationKeys.IS_TEST_SET, value.toBoolean()) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /mcp4k-compiler/src/main/kotlin/sh/ondr/mcp4k/compiler/Mcp4kCompilerPluginRegistrar.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.compiler 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 4 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 5 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 6 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar 7 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 8 | import org.jetbrains.kotlin.config.CommonConfigurationKeys 9 | import org.jetbrains.kotlin.config.CompilerConfiguration 10 | import org.jetbrains.kotlin.ir.declarations.IrModuleFragment 11 | 12 | @OptIn(ExperimentalCompilerApi::class) 13 | class Mcp4kCompilerPluginRegistrar : CompilerPluginRegistrar() { 14 | override val supportsK2 = true 15 | 16 | override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { 17 | val messageCollector = configuration[CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE] 18 | val isTest = configuration.get(Mcp4kConfigurationKeys.IS_TEST_SET, false) 19 | 20 | IrGenerationExtension.registerExtension( 21 | object : IrGenerationExtension { 22 | override fun generate( 23 | moduleFragment: IrModuleFragment, 24 | pluginContext: IrPluginContext, 25 | ) { 26 | moduleFragment.transform( 27 | Mcp4kIrTransformer( 28 | messageCollector = messageCollector, 29 | pluginContext = pluginContext, 30 | isTest = isTest, 31 | ), 32 | data = null, 33 | ) 34 | } 35 | }, 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mcp4k-compiler/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor: -------------------------------------------------------------------------------- 1 | sh.ondr.mcp4k.compiler.Mcp4kCommandLineProcessor -------------------------------------------------------------------------------- /mcp4k-compiler/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar: -------------------------------------------------------------------------------- 1 | sh.ondr.mcp4k.compiler.Mcp4kCompilerPluginRegistrar -------------------------------------------------------------------------------- /mcp4k-file-provider/README.md: -------------------------------------------------------------------------------- 1 | # mcp4k-file-provider 2 | 3 | File-based resource provider implementations for mcp4k. This module provides ready-to-use implementations of the `ResourceProvider` interface for exposing files through the Model Context Protocol. 4 | 5 | ## Installation 6 | 7 | First, make sure you have the main mcp4k plugin applied: 8 | 9 | ```kotlin 10 | plugins { 11 | kotlin("multiplatform") version "2.2.21" // or kotlin("jvm") 12 | kotlin("plugin.serialization") version "2.2.21" 13 | 14 | id("sh.ondr.mcp4k") version "0.4.6" // <-- Required 15 | } 16 | ``` 17 | 18 | Then add the file-provider dependency: 19 | 20 | ```kotlin 21 | dependencies { 22 | implementation("sh.ondr.mcp4k:mcp4k-file-provider:0.4.6") 23 | } 24 | ``` 25 | 26 | **Note**: The mcp4k plugin automatically includes `mcp4k-runtime`, so you don't need to add it explicitly. 27 | 28 | ## Overview 29 | 30 | This module provides two file-based implementations of the `ResourceProvider` interface: 31 | 32 | - **`DiscreteFileProvider`** - Exposes a specific set of files with discrete URIs 33 | - **`TemplateFileProvider`** - Exposes an entire directory using URI templates 34 | 35 | **⚠️ WARNING**: These implementations are experimental and NOT production-ready. Use only in sandboxed or trusted environments. 36 | 37 | ## Usage 38 | 39 | ### DiscreteFileProvider 40 | 41 | Exposes a specific set of files with discrete URIs. Use this when you want to expose only certain files from a directory. 42 | 43 | ```kotlin 44 | val fileProvider = DiscreteFileProvider( 45 | fileSystem = FileSystem.SYSTEM, 46 | rootDir = "/app/resources".toPath(), 47 | initialFiles = listOf( 48 | File( 49 | relativePath = "config/app.yaml", 50 | mimeType = "application/yaml", 51 | ), 52 | File( 53 | relativePath = "data/users.json", 54 | mimeType = "application/json", 55 | ), 56 | ) 57 | ) 58 | 59 | val server = Server.Builder() 60 | .withResourceProvider(fileProvider) 61 | .withTransport(StdioTransport()) 62 | .build() 63 | ``` 64 | 65 | #### Dynamic File Management 66 | 67 | You can add or remove files at runtime: 68 | 69 | ```kotlin 70 | // Add a new file 71 | fileProvider.addFile( 72 | File( 73 | relativePath = "logs/app.log", 74 | mimeType = "text/plain", 75 | ) 76 | ) 77 | 78 | // Remove a file 79 | fileProvider.removeFile("config/app.yaml") 80 | ``` 81 | 82 | Both operations automatically send `notifications/resources/list_changed` to connected clients. 83 | 84 | #### File Change Notifications 85 | 86 | When a file's contents change, notify subscribed clients: 87 | 88 | ```kotlin 89 | fileProvider.onResourceChange("data/users.json") 90 | ``` 91 | 92 | This sends `notifications/resources/updated` to clients that have subscribed to the resource. 93 | 94 | ### TemplateFileProvider 95 | 96 | Exposes an entire directory using URI templates. Use this when you want to provide access to all files in a directory structure. 97 | 98 | ```kotlin 99 | val templateProvider = TemplateFileProvider( 100 | fileSystem = FileSystem.SYSTEM, 101 | rootDir = "/app/documents".toPath(), 102 | ) 103 | 104 | val server = Server.Builder() 105 | .withResourceProvider(templateProvider) 106 | .withTransport(StdioTransport()) 107 | .build() 108 | ``` 109 | 110 | Clients can read any file within the root directory by providing the relative path: 111 | 112 | ```json 113 | { 114 | "method": "resources/read", 115 | "params": { 116 | "uri": "file:///reports/2024/summary.pdf" 117 | } 118 | } 119 | ``` 120 | 121 | ## Client Usage 122 | 123 | From the client side, resources work the same regardless of the provider: 124 | 125 | ```kotlin 126 | // List available resources 127 | val resources = client.listResources() 128 | resources.forEach { resource -> 129 | println("${resource.name}: ${resource.uri}") 130 | } 131 | 132 | // Read a resource 133 | val contents = client.readResource("file://config/app.yaml") 134 | println(contents.text) 135 | 136 | // Subscribe to changes 137 | client.subscribeResource("file://data/users.json") 138 | // Handle notifications via onResourceUpdated callback 139 | ``` 140 | 141 | ## Security Considerations 142 | 143 | These file providers have several security limitations: 144 | - No access control or authentication 145 | - Basic path traversal protection only 146 | - No file size limits 147 | - No rate limiting 148 | - No sandboxing of file access 149 | 150 | For production use, implement your own `ResourceProvider` with appropriate security measures. 151 | 152 | ## Custom File Systems 153 | 154 | Both providers support Okio's `FileSystem` abstraction, allowing you to use: 155 | - `FileSystem.SYSTEM` - Real file system (only available in platform sets, not in `commonMain`) 156 | - `FakeFileSystem` - In-memory file system for testing 157 | - Custom implementations - Cloud storage, encrypted files, etc. 158 | 159 | To use FakeFileSystem for testing, add the following dependency to your test set: 160 | 161 | ```kts 162 | dependencies { 163 | implementation("com.squareup.okio:okio-fakefilesystem:3.16.0") 164 | } 165 | ``` 166 | 167 | Here is some example code: 168 | 169 | ```kotlin 170 | val fakeFs = FakeFileSystem() 171 | fakeFs.createDirectories("/test/data".toPath()) 172 | fakeFs.write("/test/data/sample.txt".toPath()) { 173 | writeUtf8("Hello, MCP!") 174 | } 175 | 176 | val provider = DiscreteFileProvider( 177 | fileSystem = fakeFs, 178 | rootDir = "/test".toPath(), 179 | initialFiles = listOf( 180 | File(relativePath = "data/sample.txt", mimeType = "text/plain") 181 | ) 182 | ) 183 | ``` 184 | 185 | ## MIME Type Detection 186 | 187 | Currently, MIME types must be specified manually when creating files. Automatic MIME type detection is planned for future releases. 188 | 189 | ## License 190 | 191 | Apache License 2.0 192 | -------------------------------------------------------------------------------- /mcp4k-file-provider/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.JavadocJar 2 | import com.vanniktech.maven.publish.KotlinMultiplatform 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | import org.jetbrains.kotlin.konan.target.HostManager 5 | 6 | plugins { 7 | alias(libs.plugins.kotlin.multiplatform) 8 | alias(libs.plugins.kotlin.serialization) 9 | alias(libs.plugins.maven.publish) 10 | alias(libs.plugins.dokka) 11 | } 12 | 13 | kotlin { 14 | js(IR) { 15 | nodejs() 16 | binaries.library() 17 | } 18 | jvm { 19 | compilerOptions { 20 | jvmTarget.set(JvmTarget.JVM_11) 21 | } 22 | } 23 | 24 | when { 25 | // macOS can build & publish all 26 | HostManager.hostIsMac -> { 27 | iosArm64() 28 | iosSimulatorArm64() 29 | iosX64() 30 | macosArm64() 31 | macosX64() 32 | linuxX64() 33 | mingwX64() 34 | } 35 | 36 | HostManager.hostIsLinux -> { 37 | linuxX64() 38 | } 39 | 40 | HostManager.hostIsMingw -> { 41 | mingwX64() 42 | } 43 | } 44 | 45 | sourceSets { 46 | commonMain { 47 | dependencies { 48 | api(project(":mcp4k-runtime")) 49 | implementation(libs.square.okio) 50 | implementation(libs.kotlinx.coroutines.core) 51 | } 52 | } 53 | commonTest { 54 | dependencies { 55 | implementation(kotlin("test")) 56 | implementation(project(":mcp4k-test")) 57 | implementation(libs.kotlinx.coroutines.test) 58 | implementation(libs.square.okio.fakefilesystem) 59 | implementation(libs.kotlinx.serialization.json) 60 | } 61 | } 62 | } 63 | } 64 | 65 | mavenPublishing { 66 | configure(KotlinMultiplatform(javadocJar = JavadocJar.Dokka("dokkaGeneratePublicationHtml"))) 67 | } 68 | -------------------------------------------------------------------------------- /mcp4k-file-provider/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=mcp4k-file-provider 2 | POM_NAME=MCP4K File Provider -------------------------------------------------------------------------------- /mcp4k-file-provider/src/commonMain/kotlin/sh/ondr/mcp4k/fileprovider/DiscreteFileProvider.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalEncodingApi::class) 2 | 3 | package sh.ondr.mcp4k.fileprovider 4 | 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import okio.FileSystem 8 | import okio.Path 9 | import okio.buffer 10 | import okio.use 11 | import sh.ondr.mcp4k.runtime.resources.ResourceProvider 12 | import sh.ondr.mcp4k.schema.resources.Resource 13 | import sh.ondr.mcp4k.schema.resources.ResourceContents 14 | import kotlin.io.encoding.Base64 15 | import kotlin.io.encoding.ExperimentalEncodingApi 16 | 17 | /** 18 | * A [ResourceProvider] that exposes a finite, discrete set of files (by relative path) 19 | * from a given [rootDir]. It does NOT return any resource templates. 20 | * 21 | * Example usage: 22 | * 23 | * ``` 24 | * val fileSystem: FileSystem = FileSystem.SYSTEM 25 | * val provider = DiscreteFileProvider( 26 | * fileSystem = fileSystem, 27 | * rootDir = "/some/local/folder".toPath(), 28 | * knownFiles = listOf("notes.txt", "report.pdf") 29 | * ) 30 | * ``` 31 | * 32 | * Once created, [listResources] will show the above files, each accessible via a `file://` 33 | * URI like `file://notes.txt`. 34 | * 35 | * If you need to add or remove files at runtime, call [addFile] or [removeFile]. 36 | */ 37 | class DiscreteFileProvider( 38 | private val fileSystem: FileSystem, 39 | private val rootDir: Path, 40 | initialFiles: List = emptyList(), 41 | private val mimeTypeDetector: MimeTypeDetector = MimeTypeDetector { "text/plain" }, 42 | ) : ResourceProvider() { 43 | override val supportsSubscriptions: Boolean = true 44 | 45 | private val files = initialFiles.toMutableList() 46 | 47 | /** 48 | * Lists the currently known, discrete resources. Each resource is associated with a 49 | * `file://relativePath` URI. 50 | */ 51 | override suspend fun listResources(): List { 52 | return files.map { discreteFile -> 53 | // If user didn't provide a custom name, fallback to the actual file name from the path 54 | val resolvedPath = rootDir.resolve(discreteFile.relativePath) 55 | val fallbackName = resolvedPath.name 56 | val fallbackDesc = "File at ${discreteFile.relativePath}" 57 | val fallbackMime = mimeTypeDetector.detect(fallbackName) 58 | 59 | Resource( 60 | uri = "file://${discreteFile.relativePath}", 61 | name = discreteFile.name ?: fallbackName, 62 | description = discreteFile.description ?: fallbackDesc, 63 | mimeType = discreteFile.mimeType ?: fallbackMime, 64 | ) 65 | } 66 | } 67 | 68 | /** 69 | * Reads file contents if [uri] starts with `file://` and matches one of the known files. 70 | * If the file doesn't exist or is a directory, returns null. 71 | */ 72 | override suspend fun readResource(uri: String): ResourceContents? = 73 | withContext(Dispatchers.Default) { 74 | // Expecting uri like "file://someRelativePath" 75 | if (!uri.startsWith("file://")) return@withContext null 76 | val relativePath = uri.removePrefix("file://") 77 | 78 | // If it’s not in our known list, treat it as not found 79 | val knownEntry = files.find { it.relativePath == relativePath } 80 | ?: return@withContext null 81 | 82 | val fullPath = rootDir.resolve(relativePath) 83 | if (!fileSystem.exists(fullPath)) return@withContext null 84 | if (fileSystem.metadata(fullPath).isDirectory) return@withContext null 85 | 86 | // Read file 87 | val source = fileSystem.source(fullPath) 88 | val data = source.buffer().use { it.readByteArray() } 89 | 90 | // Either use the knownEntry.mimeType or guess 91 | val fallbackName = fullPath.name 92 | val mimeType = knownEntry.mimeType ?: mimeTypeDetector.detect(fallbackName) 93 | 94 | // Simple text vs. blob check 95 | if (mimeType.startsWith("text")) { 96 | val text = runCatching { data.decodeToString() }.getOrNull() ?: return@withContext null 97 | ResourceContents.Text( 98 | uri = uri, 99 | mimeType = mimeType, 100 | text = text, 101 | ) 102 | } else { 103 | ResourceContents.Blob( 104 | uri = uri, 105 | mimeType = mimeType, 106 | blob = Base64.encode(data), 107 | ) 108 | } 109 | } 110 | 111 | /** 112 | * Adds a file to the known list, triggering onResourcesListChanged() if 113 | * the file wasn't in the list before. 114 | * 115 | * @return `true` if the file was added, `false` if it was already in the list. 116 | */ 117 | suspend fun addFile(file: File): Boolean { 118 | return if (files.none { it.relativePath == file.relativePath }) { 119 | files += file 120 | onResourcesListChanged() 121 | true 122 | } else { 123 | false 124 | } 125 | } 126 | 127 | /** 128 | * Removes a file from the known list, triggering onResourcesListChanged() if removed. 129 | * 130 | * @return `true` if the file was removed, `false` if it wasn't in the list. 131 | */ 132 | suspend fun removeFile(relativePath: String): Boolean { 133 | val removed = files.removeAll { it.relativePath == relativePath } 134 | return if (removed) { 135 | onResourcesListChanged() 136 | true 137 | } else { 138 | false 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /mcp4k-file-provider/src/commonMain/kotlin/sh/ondr/mcp4k/fileprovider/File.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.fileprovider 2 | 3 | data class File( 4 | val relativePath: String, 5 | val name: String? = null, 6 | val description: String? = null, 7 | val mimeType: String? = null, 8 | ) 9 | -------------------------------------------------------------------------------- /mcp4k-file-provider/src/commonMain/kotlin/sh/ondr/mcp4k/fileprovider/MimeTypeDetector.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.fileprovider 2 | 3 | fun interface MimeTypeDetector { 4 | /** 5 | * Given a filename (or path segment), return its MIME type, e.g. "text/plain" 6 | * or "image/png". 7 | */ 8 | fun detect(pathName: String): String 9 | } 10 | -------------------------------------------------------------------------------- /mcp4k-file-provider/src/commonMain/kotlin/sh/ondr/mcp4k/fileprovider/TemplateFileProvider.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalEncodingApi::class) 2 | 3 | package sh.ondr.mcp4k.fileprovider 4 | 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import okio.FileSystem 8 | import okio.Path 9 | import okio.Path.Companion.toPath 10 | import okio.buffer 11 | import okio.use 12 | import sh.ondr.mcp4k.runtime.resources.ResourceProvider 13 | import sh.ondr.mcp4k.schema.resources.Resource 14 | import sh.ondr.mcp4k.schema.resources.ResourceContents 15 | import sh.ondr.mcp4k.schema.resources.ResourceTemplate 16 | import kotlin.io.encoding.Base64 17 | import kotlin.io.encoding.ExperimentalEncodingApi 18 | 19 | // TODO Allow dynamic configuration 20 | 21 | /** 22 | * A [ResourceProvider] that exposes [rootDir] through the URI template: 23 | * ``` 24 | * file:///{path} 25 | * ``` 26 | * to the client, where `path` is a relative path under [rootDir]. 27 | * 28 | * Example usage: 29 | * ``` 30 | * val templateProvider = TemplateFileProvider( 31 | * fileSystem = FileSystem.SYSTEM, 32 | * rootDir = "/app/resources".toPath(), 33 | * ) 34 | * ``` 35 | * 36 | * The client can then read resources such as `file:///sub/folder/example.txt`, which will 37 | * be resolved to `/app/resources/sub/folder/example.txt`. 38 | */ 39 | class TemplateFileProvider( 40 | private val fileSystem: FileSystem, 41 | private val rootDir: Path, 42 | private val name: String = "Arbitrary local file access", 43 | private val description: String = "Allows reading any file by specifying {path}", 44 | private val mimeTypeDetector: MimeTypeDetector = MimeTypeDetector { 45 | "text/plain" 46 | }, 47 | ) : ResourceProvider() { 48 | override val supportsSubscriptions: Boolean = true 49 | 50 | /** 51 | * Returns the resource template defined in the constructor 52 | */ 53 | override suspend fun listResourceTemplates(): List { 54 | return listOf( 55 | ResourceTemplate( 56 | uriTemplate = "file:///{path}", 57 | name = name, 58 | description = description, 59 | ), 60 | ) 61 | } 62 | 63 | /** 64 | * We have no discrete listing of resources; everything is driven by the template, 65 | * so [listResources] returns empty by default. 66 | */ 67 | override suspend fun listResources(): List = emptyList() 68 | 69 | /** 70 | * Given a 'file:///{path}', we parse the actual path from the URI. 71 | * Then we do a minimal check (normalize & trivial sandbox check) and read the file from disk if it exists. 72 | */ 73 | override suspend fun readResource(uri: String): ResourceContents? = 74 | withContext(Dispatchers.Default) { 75 | // Expect URIs like "file:///something" 76 | if (!uri.startsWith("file:///")) return@withContext null 77 | 78 | // 1) Extract the portion after "file:///" 79 | val pathPart = uri.removePrefix("file:///") 80 | 81 | // 2) Convert that substring to a Path and then resolve under rootDir. 82 | // 'normalize` will drop any '.' segments etc. 83 | val requestedPath = pathPart.toPath(normalize = true) 84 | val resolved = rootDir.resolve(requestedPath, normalize = true) 85 | 86 | // 3) Check if resolved is physically inside rootDir: 87 | val subPath = try { 88 | resolved.relativeTo(rootDir) 89 | } catch (iae: IllegalArgumentException) { 90 | // Means resolved is on a different root or otherwise can't be relative to rootDir 91 | // TODO Maybe throw 92 | return@withContext null 93 | } 94 | 95 | // If subPath has any '..' segments, we might be breaking out of rootDir 96 | if (".." in subPath.segments) { 97 | return@withContext null 98 | } 99 | 100 | // 4) Now that we've confirmed it's inside rootDir, verify that it exists and is not a directory 101 | if (!fileSystem.exists(resolved)) return@withContext null 102 | if (fileSystem.metadata(resolved).isDirectory) return@withContext null 103 | 104 | // 5) Read file contents 105 | val data = fileSystem.source(resolved).buffer().use { it.readByteArray() } 106 | 107 | // 6) Handle depending on MIME type 108 | val mimeType = mimeTypeDetector.detect(resolved.name) 109 | if (mimeType.startsWith("text")) { 110 | val text = runCatching { data.decodeToString() }.getOrNull() ?: return@withContext null 111 | ResourceContents.Text( 112 | uri = uri, 113 | mimeType = mimeType, 114 | text = text, 115 | ) 116 | } else { 117 | ResourceContents.Blob( 118 | uri = uri, 119 | mimeType = mimeType, 120 | blob = Base64.encode(data), 121 | ) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /mcp4k-gradle/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.GradlePlugin 2 | import com.vanniktech.maven.publish.JavadocJar 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | 5 | plugins { 6 | id("java-gradle-plugin") 7 | alias(libs.plugins.build.config) 8 | alias(libs.plugins.kotlin.jvm) 9 | alias(libs.plugins.maven.publish) 10 | alias(libs.plugins.dokka) 11 | } 12 | 13 | dependencies { 14 | compileOnly(libs.kotlin.compiler.embeddable) 15 | implementation(libs.koja.gradle) 16 | implementation(libs.kotlin.stdlib) 17 | compileOnly(libs.kotlin.gradle.api) 18 | compileOnly(libs.kotlin.gradle.plugin) 19 | implementation(libs.ksp.gradle.plugin) 20 | } 21 | 22 | buildConfig { 23 | useKotlinOutput { 24 | internalVisibility = true 25 | topLevelConstants = true 26 | } 27 | packageName("sh.ondr.mcp4k.gradle") 28 | buildConfigField("String", "PLUGIN_VERSION", "\"$version\"") 29 | buildConfigField("String", "REQUIRED_KOTLIN_VERSION", "\"${libs.versions.kotlin.get()}\"") 30 | buildConfigField("String", "REQUIRED_KSP_VERSION", "\"${libs.versions.ksp.api.get()}\"") 31 | } 32 | 33 | gradlePlugin { 34 | plugins { 35 | create("main") { 36 | id = "sh.ondr.mcp4k" 37 | implementationClass = "sh.ondr.mcp4k.gradle.Mcp4kGradlePlugin" 38 | } 39 | } 40 | } 41 | 42 | // If the root project is NOT 'mcp4k', we must be in `mcp4k-build` 43 | if (rootProject.name != "mcp4k") { 44 | // Move build directory into `mcp4k-build` 45 | layout.buildDirectory = file("$rootDir/build/mcp4k-gradle-included") 46 | } 47 | 48 | // Only publish from real build 49 | if (rootProject.name == "mcp4k") { 50 | apply(plugin = "com.vanniktech.maven.publish") 51 | 52 | mavenPublishing { 53 | configure(GradlePlugin(javadocJar = JavadocJar.Dokka("dokkaGeneratePublicationHtml"))) 54 | } 55 | } 56 | 57 | java { 58 | toolchain.languageVersion.set(JavaLanguageVersion.of(11)) 59 | } 60 | 61 | tasks.withType().configureEach { 62 | compilerOptions.jvmTarget.set(JvmTarget.JVM_11) 63 | } 64 | -------------------------------------------------------------------------------- /mcp4k-gradle/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=mcp4k-gradle 2 | POM_NAME=MCP4K Gradle plugin 3 | -------------------------------------------------------------------------------- /mcp4k-gradle/src/main/resources/META-INF/services/org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin: -------------------------------------------------------------------------------- 1 | sh.ondr.mcp4k.gradle.Mcp4kGradlePlugin -------------------------------------------------------------------------------- /mcp4k-ksp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.kotlin.jvm) 5 | alias(libs.plugins.maven.publish) 6 | } 7 | 8 | dependencies { 9 | implementation(libs.ksp.api) 10 | testImplementation(gradleTestKit()) 11 | testImplementation(kotlin("test-junit5")) 12 | } 13 | 14 | tasks.test { 15 | useJUnitPlatform() 16 | } 17 | 18 | java { 19 | toolchain.languageVersion.set(JavaLanguageVersion.of(11)) 20 | } 21 | 22 | tasks.withType().configureEach { 23 | compilerOptions.jvmTarget.set(JvmTarget.JVM_11) 24 | } 25 | -------------------------------------------------------------------------------- /mcp4k-ksp/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=mcp4k-ksp 2 | POM_NAME=mcp4k KSP plugin 3 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/KdocUtil.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp 2 | 3 | data class KDocDescription( 4 | val description: String?, 5 | val parameterDescriptions: Map, 6 | ) 7 | 8 | private val allowedTags = 9 | setOf( 10 | "@param", 11 | ) 12 | 13 | /** 14 | * Parses KDoc into a main description and parameter descriptions for `@McpTool` annotated functions. 15 | * 16 | * Restrictions: 17 | * - Whitespaces and tab characters are normalized to single spaces. Newlines are removed. 18 | * - Only `@param` tags are allowed. Any other `@` usage (e.g. `@return`) causes an error. 19 | * - `@param` must be followed by a known parameter name and a description. 20 | * - The main description ends when we encounter an `@param` tag. 21 | * - Having two `@param` tags with the same parameter name is an error. 22 | * 23 | * Steps: 24 | * 1. Normalize whitespace and tokenize the entire docstring into space-separated tokens. 25 | * 2. Verify that any token containing a `@` is exactly `@param`, otherwise throw an error. 26 | * 3. The main description is all tokens before the first `@param`. 27 | * 4. For each `@param`, the next token is the parameter name (must be in [parameters]). 28 | * 5. Subsequent tokens until next `@param` or end form that parameter's description. If no description, error. 29 | * 6. If a parameter is mentioned in @param but not known, error. 30 | * 31 | * If any rule is violated, we throw, explaining the issue. 32 | */ 33 | fun String.parseDescription(parameters: List): KDocDescription { 34 | // Step 1: Normalize whitespace 35 | val normalized = 36 | this 37 | .replace('\n', ' ') 38 | .replace('\r', ' ') 39 | .replace('\t', ' ') 40 | .replace(Regex("\\s+"), " ") 41 | .trim() 42 | 43 | if (normalized.isEmpty()) { 44 | return KDocDescription(null, emptyMap()) 45 | } 46 | 47 | val tokens = normalized.split(' ') 48 | 49 | // Step 2: Verify allowed tags 50 | tokens.forEachIndexed { i, t -> 51 | if (t.contains("@") && t !in allowedTags) { 52 | // Found a tag or @-starting token that's not allowed. 53 | throw IllegalArgumentException( 54 | "Unsupported tag '$t' found in KDoc. " + 55 | "For @McpTool functions, only '@param' is allowed.", 56 | ) 57 | } 58 | } 59 | 60 | var index = 0 61 | 62 | // Extract main description: until first @param or end of tokens 63 | val mainDescTokens = mutableListOf() 64 | while (index < tokens.size && tokens[index] != "@param") { 65 | mainDescTokens.add(tokens[index]) 66 | index++ 67 | } 68 | 69 | val mainDescription = mainDescTokens.joinToString(" ").ifBlank { null } 70 | val paramDescriptions = mutableMapOf() 71 | 72 | // Process @param tags, if any 73 | while (index < tokens.size) { 74 | val marker = tokens[index] 75 | 76 | if (marker != "@param") { 77 | // Any unexpected tag here is not allowed, but we already checked tags above. 78 | // So if we get here, it's just no more @param tags -> break 79 | break 80 | } 81 | 82 | index++ 83 | if (index >= tokens.size) { 84 | throw IllegalArgumentException( 85 | "'@param' at the end with no parameter name.", 86 | ) 87 | } 88 | val paramName = tokens[index] 89 | if (paramName.startsWith("@")) { 90 | throw IllegalArgumentException( 91 | "'@param' not followed by a parameter name.", 92 | ) 93 | } 94 | if (paramName !in parameters) { 95 | throw IllegalArgumentException( 96 | "'@param $paramName' references unknown parameter.", 97 | ) 98 | } 99 | 100 | index++ 101 | // Collect param description until next @param or end 102 | val paramDescTokens = mutableListOf() 103 | while (index < tokens.size && tokens[index] != "@param") { 104 | paramDescTokens.add(tokens[index]) 105 | index++ 106 | } 107 | 108 | val paramDesc = 109 | paramDescTokens.joinToString(" ").ifBlank { 110 | throw IllegalArgumentException( 111 | "'@param $paramName' has no description.", 112 | ) 113 | } 114 | 115 | if (paramName in paramDescriptions) { 116 | throw IllegalArgumentException( 117 | "'@param $paramName' is duplicated.", 118 | ) 119 | } 120 | paramDescriptions[paramName] = paramDesc 121 | } 122 | 123 | return KDocDescription(mainDescription, paramDescriptions) 124 | } 125 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/Mcp4kProcessor.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp 2 | 3 | import com.google.devtools.ksp.KspExperimental 4 | import com.google.devtools.ksp.processing.CodeGenerator 5 | import com.google.devtools.ksp.processing.KSPLogger 6 | import com.google.devtools.ksp.processing.Resolver 7 | import com.google.devtools.ksp.processing.SymbolProcessor 8 | import com.google.devtools.ksp.symbol.KSAnnotated 9 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 10 | import sh.ondr.mcp4k.ksp.prompts.PromptMeta 11 | import sh.ondr.mcp4k.ksp.prompts.generatePromptHandlersFile 12 | import sh.ondr.mcp4k.ksp.prompts.generatePromptParamsClass 13 | import sh.ondr.mcp4k.ksp.prompts.toPromptMeta 14 | import sh.ondr.mcp4k.ksp.prompts.validatePrompt 15 | import sh.ondr.mcp4k.ksp.tools.ToolMeta 16 | import sh.ondr.mcp4k.ksp.tools.generateToolHandlersFile 17 | import sh.ondr.mcp4k.ksp.tools.generateToolParamsClass 18 | import sh.ondr.mcp4k.ksp.tools.toToolMeta 19 | import sh.ondr.mcp4k.ksp.tools.validateTool 20 | import kotlin.collections.isNotEmpty 21 | 22 | @OptIn(KspExperimental::class) 23 | class Mcp4kProcessor( 24 | val codeGenerator: CodeGenerator, 25 | val logger: KSPLogger, 26 | private val options: Map, 27 | ) : SymbolProcessor { 28 | val pkg = "sh.ondr.mcp4k" 29 | 30 | val toolAnnoFqn = "$pkg.runtime.annotation.McpTool" 31 | val promptAnnoFqn = "$pkg.runtime.annotation.McpPrompt" 32 | 33 | val mcp4kParamsPackage = "$pkg.generated.params" 34 | val mcp4kHandlersPackage = "$pkg.generated.handlers" 35 | 36 | // Collections that survive across rounds - only plain data, no KS* objects! 37 | private val processedFunctions = mutableSetOf() // FQNs only 38 | private val generatedParams = mutableSetOf() // class simple names 39 | 40 | // These will be rebuilt each round from generated params 41 | val tools = mutableListOf() 42 | val prompts = mutableListOf() 43 | 44 | val isTest = options["isTestSet"]?.toBoolean() ?: false 45 | 46 | override fun process(resolver: Resolver): List { 47 | processTools(resolver) 48 | processPrompts(resolver) 49 | 50 | return emptyList() 51 | } 52 | 53 | @OptIn(KspExperimental::class) 54 | private fun processTools(resolver: Resolver) { 55 | resolver.getSymbolsWithAnnotation(toolAnnoFqn) 56 | .filterIsInstance() 57 | .forEach { ksFunction -> 58 | val fqName = ksFunction.qualifiedName?.asString() ?: return@forEach 59 | 60 | // Skip if already processed in a previous round 61 | if (fqName in processedFunctions) { 62 | return@forEach 63 | } 64 | 65 | val toolMeta = ksFunction.toToolMeta() 66 | 67 | // Check for duplicate names 68 | val existingTool = tools.find { it.functionName == toolMeta.functionName } 69 | if (existingTool != null) { 70 | logger.error( 71 | "MCP4K error: multiple @McpTool functions share the name '${toolMeta.functionName}'. " + 72 | "Conflicts: ${existingTool.fqName}, ${toolMeta.fqName}", 73 | symbol = ksFunction, 74 | ) 75 | return@forEach 76 | } 77 | 78 | // Validate the tool 79 | if (!validateTool(ksFunction, toolMeta)) { 80 | return@forEach // Skip invalid tools 81 | } 82 | 83 | // Add to list 84 | tools.add(toolMeta) 85 | 86 | // Generate param class immediately with proper dependencies 87 | generateToolParamsClass(toolMeta, ksFunction.containingFile!!) 88 | generatedParams.add(toolMeta.paramsClassName) 89 | 90 | // Mark as processed 91 | processedFunctions.add(fqName) 92 | } 93 | } 94 | 95 | private fun processPrompts(resolver: Resolver) { 96 | resolver.getSymbolsWithAnnotation(promptAnnoFqn) 97 | .filterIsInstance() 98 | .forEach { ksFunction -> 99 | val fqName = ksFunction.qualifiedName?.asString() ?: return@forEach 100 | 101 | // Skip if already processed in a previous round 102 | if (fqName in processedFunctions) { 103 | return@forEach 104 | } 105 | 106 | val promptMeta = ksFunction.toPromptMeta() 107 | 108 | // Check for duplicate names 109 | val existingPrompt = prompts.find { it.functionName == promptMeta.functionName } 110 | if (existingPrompt != null) { 111 | logger.error( 112 | "MCP4K error: multiple @McpPrompt functions share the name '${promptMeta.functionName}'. " + 113 | "Conflicts: ${existingPrompt.fqName}, ${promptMeta.fqName}", 114 | symbol = ksFunction, 115 | ) 116 | return@forEach 117 | } 118 | 119 | // Validate the prompt 120 | if (!validatePrompt(ksFunction, promptMeta)) { 121 | return@forEach // Skip invalid prompts 122 | } 123 | 124 | // Add to list 125 | prompts.add(promptMeta) 126 | 127 | // Generate param class immediately with proper dependencies 128 | generatePromptParamsClass(promptMeta, ksFunction.containingFile!!) 129 | generatedParams.add(promptMeta.paramsClassName) 130 | 131 | // Mark as processed 132 | processedFunctions.add(fqName) 133 | } 134 | } 135 | 136 | override fun finish() { 137 | // Only generate aggregated files (handlers, initializer) if we have any tools or prompts 138 | if (tools.isNotEmpty() || prompts.isNotEmpty()) { 139 | // Generate handler files 140 | generateToolHandlersFile() 141 | generatePromptHandlersFile() 142 | generateInitializer() 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/Mcp4kProcessorProvider.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp 2 | 3 | import com.google.devtools.ksp.processing.SymbolProcessor 4 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 5 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 6 | 7 | class Mcp4kProcessorProvider : SymbolProcessorProvider { 8 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { 9 | return Mcp4kProcessor( 10 | codeGenerator = environment.codeGenerator, 11 | logger = environment.logger, 12 | options = environment.options, 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/ParamInfo.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp 2 | 3 | data class ParamInfo( 4 | val name: String, 5 | val fqnType: String, 6 | val fqnTypeNonNullable: String, 7 | val readableType: String, 8 | val isNullable: Boolean, 9 | val hasDefault: Boolean, 10 | val isRequired: Boolean, 11 | var description: String? = null, 12 | ) 13 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/Util.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp 2 | 3 | import com.google.devtools.ksp.symbol.KSType 4 | 5 | /** 6 | * Converts a KSType into a fully-qualified type string, including its type arguments. 7 | * For example, a KSType representing List would become "kotlin.collections.List". 8 | */ 9 | fun KSType.toFqnString(): String { 10 | val decl = this.declaration.qualifiedName?.asString() ?: this.toString() 11 | if (this.arguments.isEmpty()) { 12 | // No type arguments 13 | return decl + (if (this.isMarkedNullable) "?" else "") 14 | } 15 | 16 | // If there are type arguments, reconstruct them 17 | val args = 18 | this.arguments.joinToString(", ") { arg -> 19 | val t = arg.type?.resolve() 20 | t?.toFqnString() ?: "*" 21 | } 22 | 23 | val nullableMark = if (this.isMarkedNullable) "?" else "" 24 | return "$decl<$args>$nullableMark" 25 | } 26 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/generateInitializer.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp 2 | 3 | import com.google.devtools.ksp.processing.Dependencies 4 | 5 | fun Mcp4kProcessor.generateInitializer() { 6 | val name = if (isTest) "Mcp4kTestInitializer" else "Mcp4kInitializer" 7 | val mcp4kInitializerPackage = "$pkg.generated.initializer" 8 | val file = codeGenerator.createNewFile( 9 | dependencies = Dependencies(aggregating = true), 10 | packageName = mcp4kInitializerPackage, 11 | fileName = name, 12 | ) 13 | 14 | val code = buildString { 15 | appendLine("// Generated by mcp4k") 16 | appendLine("package $mcp4kInitializerPackage") 17 | appendLine() 18 | appendLine("import sh.ondr.mcp4k.runtime.core.mcpPromptParams") 19 | appendLine("import sh.ondr.mcp4k.runtime.core.mcpPromptHandlers") 20 | appendLine("import sh.ondr.mcp4k.runtime.core.mcpToolParams") 21 | appendLine("import sh.ondr.mcp4k.runtime.core.mcpToolHandlers") 22 | if (tools.isNotEmpty()) { 23 | appendLine("import $mcp4kHandlersPackage.*") 24 | appendLine("import $mcp4kParamsPackage.*") 25 | } 26 | appendLine("import kotlin.reflect.KClass") 27 | appendLine() 28 | appendLine("object $name {") 29 | appendLine(" init {") 30 | 31 | for (tool in tools) { 32 | val handlerClassName = tool.functionName.replaceFirstChar { it.uppercase() } + "McpToolHandler" 33 | val paramsClassName = tool.paramsClassName 34 | appendLine(" // Register '${tool.functionName}'") 35 | appendLine(" mcpToolParams[\"${tool.functionName}\"] = $paramsClassName::class") 36 | appendLine(" mcpToolHandlers[\"${tool.functionName}\"] = $handlerClassName()") 37 | } 38 | 39 | for (prompt in prompts) { 40 | val handlerClassName = prompt.functionName.replaceFirstChar { it.uppercase() } + "McpPromptHandler" 41 | val paramsClassName = prompt.paramsClassName 42 | appendLine(" // Register '${prompt.functionName}'") 43 | appendLine(" mcpPromptParams[\"${prompt.functionName}\"] = $paramsClassName::class") 44 | appendLine(" mcpPromptHandlers[\"${prompt.functionName}\"] = $handlerClassName()") 45 | } 46 | 47 | appendLine(" }") 48 | appendLine("}") 49 | } 50 | 51 | file.write(code.toByteArray()) 52 | file.close() 53 | } 54 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/prompts/PromptMeta.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp.prompts 2 | 3 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 4 | import sh.ondr.mcp4k.ksp.ParamInfo 5 | import sh.ondr.mcp4k.ksp.toFqnString 6 | 7 | data class PromptMeta( 8 | val functionName: String, 9 | val fqName: String, 10 | val params: List, 11 | val paramsClassName: String, 12 | val returnTypeFqn: String, 13 | val returnTypeReadable: String, 14 | val originatingFilePath: String, 15 | val kdoc: String? = null, 16 | val isServerExtension: Boolean, 17 | ) 18 | 19 | fun KSFunctionDeclaration.toPromptMeta(): PromptMeta { 20 | val functionName = simpleName.asString() 21 | val paramInfos = parameters.mapIndexed { index, p -> 22 | val parameterName = p.name?.asString() ?: "arg$index" 23 | val parameterType = p.type.resolve() 24 | val fqnParameterType = parameterType.toFqnString() 25 | val hasDefault = p.hasDefault 26 | val isNullable = parameterType.isMarkedNullable 27 | val isRequired = !(hasDefault || isNullable) 28 | 29 | ParamInfo( 30 | name = parameterName, 31 | fqnType = fqnParameterType, 32 | fqnTypeNonNullable = parameterType.makeNotNullable().toFqnString(), 33 | readableType = parameterType.toString(), 34 | isNullable = isNullable, 35 | hasDefault = hasDefault, 36 | isRequired = isRequired, 37 | ) 38 | } 39 | 40 | return PromptMeta( 41 | functionName = functionName, 42 | paramsClassName = functionName.replaceFirstChar { it.uppercase() } + "McpPromptParams", 43 | fqName = qualifiedName?.asString() ?: "", 44 | params = paramInfos, 45 | returnTypeFqn = returnType?.resolve()?.toFqnString() ?: returnType.toString(), 46 | returnTypeReadable = returnType.toString(), 47 | originatingFilePath = containingFile!!.filePath, 48 | kdoc = docString, 49 | isServerExtension = extensionReceiver != null, // We check for type in validation 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/prompts/generatePromptHandlersFile.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp.prompts 2 | 3 | import com.google.devtools.ksp.processing.Dependencies 4 | import sh.ondr.mcp4k.ksp.Mcp4kProcessor 5 | import sh.ondr.mcp4k.ksp.ParamInfo 6 | 7 | fun Mcp4kProcessor.generatePromptHandlersFile() { 8 | val fileName = "Mcp4kGeneratedPromptHandlers" 9 | val file = codeGenerator.createNewFile( 10 | dependencies = Dependencies(aggregating = true), 11 | packageName = mcp4kHandlersPackage, 12 | fileName = fileName, 13 | ) 14 | 15 | val code = buildString { 16 | appendLine("// Generated by mcp4k") 17 | appendLine("package $mcp4kHandlersPackage") 18 | appendLine() 19 | appendLine("import kotlinx.serialization.json.JsonObject") 20 | appendLine("import kotlinx.serialization.json.decodeFromJsonElement") 21 | appendLine("import sh.ondr.mcp4k.runtime.core.mcpJson") 22 | appendLine("import sh.ondr.mcp4k.runtime.prompts.McpPromptHandler") 23 | appendLine("import sh.ondr.mcp4k.runtime.Server") 24 | appendLine("import sh.ondr.mcp4k.runtime.error.MissingRequiredArgumentException") 25 | appendLine("import sh.ondr.mcp4k.runtime.error.UnknownArgumentException") 26 | appendLine("import sh.ondr.mcp4k.schema.prompts.GetPromptResult") 27 | prompts.forEach { 28 | appendLine("import ${it.fqName}") 29 | } 30 | appendLine() 31 | 32 | prompts.forEach { prompt -> 33 | val handlerClassName = prompt.functionName.replaceFirstChar { it.uppercase() } + "McpPromptHandler" 34 | val fqParamsClass = "$mcp4kParamsPackage.${prompt.paramsClassName}" 35 | 36 | // Collect known parameter names 37 | val knownParams = prompt.params.joinToString { "\"${it.name}\"" } 38 | 39 | // Collect strictly required parameters (no default, not nullable): 40 | val requiredParams = prompt.params.filter { it.isRequired } 41 | 42 | appendLine("class $handlerClassName : McpPromptHandler {") 43 | appendLine(" private val knownParams: Set = setOf($knownParams)") 44 | appendLine() 45 | appendLine(" override suspend fun call(server: Server, params: JsonObject): GetPromptResult {") 46 | appendLine(" val unknownKeys = params.keys - knownParams") 47 | appendLine(" if (unknownKeys.isNotEmpty()) {") 48 | appendLine( 49 | " throw UnknownArgumentException(\"Unknown argument '\${unknownKeys.first()}' for prompt '${prompt.functionName}'\")", 50 | ) 51 | appendLine(" }") 52 | appendLine() 53 | 54 | if (requiredParams.isNotEmpty()) { 55 | appendLine(" // Check required parameters") 56 | for (reqParam in requiredParams) { 57 | appendLine(" if (!params.containsKey(\"${reqParam.name}\")) {") 58 | appendLine(" throw MissingRequiredArgumentException(\"Missing required argument '${reqParam.name}'\")") 59 | appendLine(" }") 60 | } 61 | appendLine() 62 | } 63 | 64 | // Decode into param class (which might have nulls for optional fields) 65 | appendLine(" val obj = mcpJson.decodeFromJsonElement($fqParamsClass.serializer(), params)") 66 | appendLine() 67 | 68 | // Generate function call with optional-branching for default-having params 69 | appendLine(" return ${generateInvocationCode(prompt, 3)}") 70 | appendLine(" }") 71 | appendLine("}") 72 | appendLine() 73 | } 74 | } 75 | 76 | file.write(code.toByteArray()) 77 | file.close() 78 | } 79 | 80 | private fun Mcp4kProcessor.generateInvocationCode( 81 | promptMeta: PromptMeta, 82 | indentLevel: Int = 2, 83 | ): String { 84 | // "branchingParams" are those that have a default => we might skip them if absent 85 | val branchingParams = promptMeta.params.filter { it.hasDefault } 86 | 87 | // "alwaysParams" are all others => we always pass them in the function call 88 | val alwaysParams = promptMeta.params.filter { !it.hasDefault } 89 | 90 | return generatePromptOptionalChain( 91 | functionName = promptMeta.functionName, 92 | alwaysParams = alwaysParams, 93 | defaultParams = branchingParams, 94 | indentLevel = indentLevel, 95 | isServerExtension = promptMeta.isServerExtension, 96 | ) 97 | } 98 | 99 | private fun Mcp4kProcessor.generatePromptOptionalChain( 100 | functionName: String, 101 | alwaysParams: List, 102 | defaultParams: List, 103 | indentLevel: Int, 104 | isServerExtension: Boolean, 105 | ): String { 106 | // Base case: if no more default-having params, just call the function with [alwaysParams]. 107 | if (defaultParams.isEmpty()) { 108 | return callPromptFunction( 109 | functionName = functionName, 110 | alwaysParams = alwaysParams, // not adding firstOpt 111 | optionalParams = emptyList(), 112 | indentLevel = indentLevel + 1, 113 | isServerExtension = isServerExtension, 114 | ) 115 | } 116 | val firstOpt = defaultParams.first() 117 | val remainingOpts = defaultParams.drop(1) 118 | val indent = " ".repeat(indentLevel * 2) 119 | 120 | return buildString { 121 | appendLine("${indent}if (params.containsKey(\"${firstOpt.name}\")) {") 122 | // If present, treat it like we must pass it 123 | val ifBranch = generatePromptOptionalChain( 124 | functionName = functionName, 125 | alwaysParams = alwaysParams + firstOpt, 126 | defaultParams = remainingOpts, 127 | indentLevel = indentLevel + 1, 128 | isServerExtension = isServerExtension, 129 | ) 130 | appendLine(ifBranch) 131 | appendLine("$indent} else {") 132 | // If absent, skip it so the function call uses its default 133 | val elseBranch = generatePromptOptionalChain( 134 | functionName = functionName, 135 | alwaysParams = alwaysParams, // not adding firstOpt 136 | defaultParams = remainingOpts, 137 | indentLevel = indentLevel + 1, 138 | isServerExtension = isServerExtension, 139 | ) 140 | appendLine(elseBranch) 141 | appendLine("$indent}") 142 | } 143 | } 144 | 145 | private fun Mcp4kProcessor.callPromptFunction( 146 | functionName: String, 147 | alwaysParams: List, 148 | optionalParams: List, 149 | indentLevel: Int, 150 | isServerExtension: Boolean, 151 | ): String { 152 | val indent = " ".repeat(indentLevel * 2) 153 | val allParams = alwaysParams + optionalParams 154 | 155 | // Each parameter becomes "name = obj.name" (+ "!!" if it hasDefault && not nullable) 156 | val args = allParams.joinToString(",\n$indent ") { param -> 157 | // If param.hasDefault && !param.isNullable => we do "!!" to guarantee non-null 158 | val maybeBang = if (param.hasDefault && !param.isNullable) "!!" else "" 159 | "${param.name} = obj.${param.name}$maybeBang" 160 | } 161 | 162 | val prefix = if (isServerExtension) "server." else "" 163 | 164 | return buildString { 165 | appendLine("$indent$prefix$functionName(") 166 | if (allParams.isNotEmpty()) { 167 | appendLine("$indent $args") 168 | } 169 | append("$indent)") 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/prompts/generatePromptParamsClass.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp.prompts 2 | 3 | import com.google.devtools.ksp.processing.Dependencies 4 | import com.google.devtools.ksp.symbol.KSFile 5 | import sh.ondr.mcp4k.ksp.Mcp4kProcessor 6 | 7 | fun Mcp4kProcessor.generatePromptParamsClass( 8 | promptMeta: PromptMeta, 9 | originFile: KSFile, 10 | ) { 11 | val code = buildString { 12 | appendLine("package $mcp4kParamsPackage") 13 | appendLine() 14 | appendLine("import kotlinx.serialization.Serializable") 15 | appendLine("import sh.ondr.koja.JsonSchema") 16 | appendLine() 17 | 18 | promptMeta.kdoc?.let { kdoc -> 19 | appendLine("/**") 20 | kdoc.split("\n").forEach { line -> 21 | appendLine(" * $line") 22 | } 23 | appendLine("*/") 24 | } 25 | 26 | appendLine("@Serializable") 27 | appendLine("@JsonSchema") 28 | appendLine("class ${promptMeta.paramsClassName}(") 29 | 30 | promptMeta.params.forEachIndexed { index, p -> 31 | val comma = if (index == promptMeta.params.size - 1) "" else "," 32 | 33 | // 1) Get the base (non-nullable) FQN 34 | val baseFqn = p.fqnTypeNonNullable 35 | 36 | // 2) Append '?' if it’s either nullable or hasDefault 37 | val finalType = if (!p.hasDefault && !p.isNullable) baseFqn else "$baseFqn?" 38 | 39 | if (!p.hasDefault && !p.isNullable) { 40 | // Required param => no default 41 | append(" val ${p.name}: $finalType$comma\n") 42 | } else { 43 | // Optional param => default to null so it can be omitted at JSON deserialization 44 | append(" val ${p.name}: $finalType = null$comma\n") 45 | } 46 | } 47 | appendLine(")") 48 | appendLine() 49 | appendLine("/**") 50 | appendLine(" * ${promptMeta.fqName}") 51 | appendLine("*/") 52 | appendLine("const val ${promptMeta.paramsClassName}OriginalSource = true") 53 | } 54 | 55 | // Use isolating dependencies with the originating source file 56 | val dependencies = Dependencies(aggregating = false, sources = arrayOf(originFile)) 57 | 58 | try { 59 | codeGenerator 60 | .createNewFile( 61 | dependencies = dependencies, 62 | packageName = mcp4kParamsPackage, 63 | fileName = promptMeta.paramsClassName, 64 | extensionName = "kt", 65 | ).use { output -> 66 | output.writer().use { writer -> 67 | writer.write(code) 68 | } 69 | } 70 | } catch (e: Exception) { 71 | // File already exists from a previous round, skip 72 | if (e.message?.contains("already exists") == true) { 73 | logger.info("Skipping already generated file: ${promptMeta.paramsClassName}.kt") 74 | } else { 75 | throw e 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/prompts/validatePrompt.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp.prompts 2 | 3 | import com.google.devtools.ksp.getVisibility 4 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 5 | import com.google.devtools.ksp.symbol.Modifier 6 | import com.google.devtools.ksp.symbol.Visibility 7 | import sh.ondr.mcp4k.ksp.Mcp4kProcessor 8 | 9 | /** 10 | * Validates a prompt function and its metadata. 11 | * Returns true if valid, false if errors were found (errors are logged). 12 | */ 13 | internal fun Mcp4kProcessor.validatePrompt( 14 | ksFunction: KSFunctionDeclaration, 15 | promptMeta: PromptMeta, 16 | ): Boolean { 17 | var isValid = true 18 | 19 | // 1. Return type must be GetPromptResult 20 | if (promptMeta.returnTypeFqn != "sh.ondr.mcp4k.schema.prompts.GetPromptResult") { 21 | logger.error( 22 | "MCP4K error: @McpPrompt function '${promptMeta.functionName}' must return GetPromptResult. " + 23 | "Currently returns: ${promptMeta.returnTypeReadable}. ", 24 | symbol = ksFunction, 25 | ) 26 | isValid = false 27 | } 28 | 29 | // 2. Parameter types must be String or String? 30 | promptMeta.params 31 | .filterNot { param -> 32 | param.fqnType == "kotlin.String" || param.fqnType == "kotlin.String?" 33 | }.forEach { param -> 34 | logger.error( 35 | "MCP4K error: @McpPrompt function '${promptMeta.functionName}' has a parameter '${param.name}' " + 36 | "with unsupported type '${param.readableType}'. " + 37 | "Only String (or String?) is allowed for prompt parameters.", 38 | symbol = ksFunction, 39 | ) 40 | isValid = false 41 | } 42 | 43 | // 3. Limit non-required parameters 44 | val nonRequiredLimit = 7 45 | val nonRequiredCount = promptMeta.params.count { !it.isRequired } 46 | if (nonRequiredCount > nonRequiredLimit) { 47 | logger.error( 48 | "MCP4K error: @McpPrompt function '${promptMeta.functionName}' has more than $nonRequiredLimit non-required parameters. " + 49 | "Please reduce optional/nullable/default parameters to $nonRequiredLimit or fewer.", 50 | symbol = ksFunction, 51 | ) 52 | isValid = false 53 | } 54 | 55 | // 4. Must be top-level function 56 | if (ksFunction.parentDeclaration != null) { 57 | val parent = ksFunction.parentDeclaration?.qualifiedName?.asString() ?: "unknown parent" 58 | logger.error( 59 | "MCP4K error: @McpPrompt function '${promptMeta.functionName}' is defined inside a class or object ($parent). " + 60 | "@McpPrompt functions must be top-level. Move '${promptMeta.functionName}' to file scope.", 61 | symbol = ksFunction, 62 | ) 63 | isValid = false 64 | } 65 | 66 | // 5. Must have no disallowed modifiers and be public 67 | val disallowedModifiers = setOf( 68 | Modifier.INLINE, 69 | Modifier.PRIVATE, 70 | Modifier.PROTECTED, 71 | Modifier.INTERNAL, 72 | Modifier.ABSTRACT, 73 | Modifier.OPEN, 74 | ) 75 | 76 | val visibility = ksFunction.getVisibility() 77 | if (visibility != Visibility.PUBLIC) { 78 | logger.error( 79 | "MCP4K error: @McpPrompt function '${promptMeta.functionName}' must be public. " + 80 | "Current visibility: $visibility. " + 81 | "Please ensure it's a top-level public function with no additional modifiers.", 82 | symbol = ksFunction, 83 | ) 84 | isValid = false 85 | } 86 | 87 | val foundDisallowed = ksFunction.modifiers.filter { it in disallowedModifiers } 88 | if (foundDisallowed.isNotEmpty()) { 89 | logger.error( 90 | "MCP4K error: @McpPrompt function '${promptMeta.functionName}' has disallowed modifiers: $foundDisallowed. " + 91 | "Only public, top-level, non-inline, non-abstract, non-internal functions are allowed.", 92 | symbol = ksFunction, 93 | ) 94 | isValid = false 95 | } 96 | 97 | // 6. Check extension receivers (must be null or Server) 98 | if (promptMeta.isServerExtension) { 99 | ksFunction.extensionReceiver?.resolve()?.declaration?.qualifiedName?.asString()?.let { receiverFq -> 100 | if (receiverFq != "sh.ondr.mcp4k.runtime.Server") { 101 | logger.error( 102 | "MCP4K error: @McpPrompt function '${promptMeta.functionName}' is an extension function, but the receiver type is not 'Server'. " + 103 | "Please ensure the extension receiver is 'Server' or 'null'.", 104 | symbol = ksFunction, 105 | ) 106 | isValid = false 107 | } 108 | } 109 | } 110 | 111 | return isValid 112 | } 113 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/tools/ToolMeta.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp.tools 2 | 3 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 4 | import sh.ondr.mcp4k.ksp.ParamInfo 5 | import sh.ondr.mcp4k.ksp.toFqnString 6 | 7 | data class ToolMeta( 8 | val functionName: String, 9 | val fqName: String, 10 | val params: List, 11 | val paramsClassName: String, 12 | val returnTypeFqn: String, 13 | val returnTypeReadable: String, 14 | val originatingFilePath: String, 15 | val kdoc: String? = null, 16 | val isServerExtension: Boolean, 17 | ) 18 | 19 | fun KSFunctionDeclaration.toToolMeta(): ToolMeta { 20 | val functionName = simpleName.asString() 21 | val paramInfos = parameters.mapIndexed { index, p -> 22 | val parameterName = p.name?.asString() ?: "arg$index" 23 | val parameterType = p.type.resolve() 24 | val fqnParameterType = parameterType.toFqnString() 25 | val hasDefault = p.hasDefault 26 | val isNullable = parameterType.isMarkedNullable 27 | val isRequired = !(hasDefault || isNullable) 28 | 29 | ParamInfo( 30 | name = parameterName, 31 | fqnType = fqnParameterType, 32 | fqnTypeNonNullable = parameterType.makeNotNullable().toFqnString(), 33 | readableType = parameterType.toString(), 34 | isNullable = isNullable, 35 | hasDefault = hasDefault, 36 | isRequired = isRequired, 37 | ) 38 | } 39 | 40 | return ToolMeta( 41 | functionName = functionName, 42 | paramsClassName = functionName.replaceFirstChar { it.uppercase() } + "McpToolParams", 43 | fqName = qualifiedName?.asString() ?: "", 44 | params = paramInfos, 45 | returnTypeFqn = returnType?.resolve()?.toFqnString() ?: returnType.toString(), 46 | returnTypeReadable = returnType.toString(), 47 | originatingFilePath = containingFile!!.filePath, 48 | kdoc = docString, 49 | isServerExtension = extensionReceiver != null, // We check for type in validation 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/tools/generateToolHandlersFile.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp.tools 2 | 3 | import com.google.devtools.ksp.processing.Dependencies 4 | import sh.ondr.mcp4k.ksp.Mcp4kProcessor 5 | import sh.ondr.mcp4k.ksp.ParamInfo 6 | 7 | fun Mcp4kProcessor.generateToolHandlersFile() { 8 | val fileName = "Mcp4kGeneratedToolHandlers" 9 | val file = codeGenerator.createNewFile( 10 | dependencies = Dependencies(aggregating = true), 11 | packageName = mcp4kHandlersPackage, 12 | fileName = fileName, 13 | ) 14 | 15 | val code = buildString { 16 | appendLine("// Generated by mcp4k") 17 | appendLine("package $mcp4kHandlersPackage") 18 | appendLine() 19 | appendLine("import kotlinx.serialization.json.JsonObject") 20 | appendLine("import kotlinx.serialization.json.decodeFromJsonElement") 21 | appendLine("import sh.ondr.mcp4k.runtime.core.mcpJson") 22 | appendLine("import sh.ondr.mcp4k.runtime.tools.McpToolHandler") 23 | appendLine("import sh.ondr.mcp4k.runtime.error.MissingRequiredArgumentException") 24 | appendLine("import sh.ondr.mcp4k.runtime.error.UnknownArgumentException") 25 | appendLine("import sh.ondr.mcp4k.runtime.Server") 26 | appendLine("import sh.ondr.mcp4k.schema.tools.CallToolResult") 27 | tools.forEach { 28 | appendLine("import ${it.fqName}") 29 | } 30 | appendLine() 31 | 32 | for (tool in tools) { 33 | val handlerClassName = tool.functionName.replaceFirstChar { it.uppercase() } + "McpToolHandler" 34 | val fqParamsClass = "$mcp4kParamsPackage.${tool.paramsClassName}" 35 | 36 | // Collect known parameter names 37 | val knownParams = tool.params.joinToString { "\"${it.name}\"" } 38 | 39 | // Collect strictly required parameters (no default, not nullable): 40 | val requiredParams = tool.params.filter { it.isRequired } 41 | 42 | appendLine("class $handlerClassName : McpToolHandler {") 43 | appendLine(" private val knownParams: Set = setOf($knownParams)") 44 | appendLine() 45 | appendLine(" override suspend fun call(server: Server, params: JsonObject): CallToolResult {") 46 | appendLine(" val unknownKeys = params.keys - knownParams") 47 | appendLine(" if (unknownKeys.isNotEmpty()) {") 48 | appendLine( 49 | " throw UnknownArgumentException(\"Unknown argument '\${unknownKeys.first()}' for tool '${tool.functionName}'\")", 50 | ) 51 | appendLine(" }") 52 | appendLine() 53 | 54 | // Check for missing required parameters 55 | if (requiredParams.isNotEmpty()) { 56 | appendLine(" // Check required parameters") 57 | for (reqParam in requiredParams) { 58 | appendLine(" if (!params.containsKey(\"${reqParam.name}\")) {") 59 | appendLine(" throw MissingRequiredArgumentException(\"Missing required argument '${reqParam.name}'\")") 60 | appendLine(" }") 61 | } 62 | appendLine() 63 | } 64 | 65 | // Decode into param class (which might have nulls for optional fields) 66 | appendLine(" val obj = mcpJson.decodeFromJsonElement($fqParamsClass.serializer(), params)") 67 | appendLine() 68 | 69 | // Generate function call with optional-branching for default-having params 70 | appendLine(" val result =") 71 | appendLine(generateInvocationCode(tool, 3)) 72 | appendLine(" return CallToolResult(listOf(result))") 73 | appendLine(" }") 74 | appendLine("}") 75 | appendLine() 76 | } 77 | } 78 | 79 | file.write(code.toByteArray()) 80 | file.close() 81 | } 82 | 83 | private fun Mcp4kProcessor.generateInvocationCode( 84 | toolMeta: ToolMeta, 85 | indentLevel: Int = 2, 86 | ): String { 87 | // "branchingParams" are those that have a default => we might skip them if absent 88 | val branchingParams = toolMeta.params.filter { it.hasDefault } 89 | 90 | // "alwaysParams" are all others => we always pass them in the function call 91 | val alwaysParams = toolMeta.params.filter { !it.hasDefault } 92 | 93 | return generateToolOptionalChain( 94 | functionName = toolMeta.functionName, 95 | alwaysParams = alwaysParams, 96 | defaultParams = branchingParams, 97 | indentLevel = indentLevel, 98 | isServerExtension = toolMeta.isServerExtension, 99 | ) 100 | } 101 | 102 | private fun Mcp4kProcessor.generateToolOptionalChain( 103 | functionName: String, 104 | alwaysParams: List, 105 | defaultParams: List, 106 | indentLevel: Int, 107 | isServerExtension: Boolean, 108 | ): String { 109 | // Base case: if no more default-having params, just call the function with [alwaysParams]. 110 | if (defaultParams.isEmpty()) { 111 | return callToolFunction( 112 | functionName = functionName, 113 | alwaysParams = alwaysParams, 114 | optionalParams = emptyList(), 115 | indentLevel = indentLevel, 116 | isServerExtension = isServerExtension, 117 | ) 118 | } 119 | val firstOpt = defaultParams.first() 120 | val remainingOpts = defaultParams.drop(1) 121 | val indent = " ".repeat(indentLevel * 2) 122 | 123 | return buildString { 124 | appendLine("${indent}if (params.containsKey(\"${firstOpt.name}\")) {") 125 | // If present, treat it like we must pass it 126 | val ifBranch = generateToolOptionalChain( 127 | functionName = functionName, 128 | alwaysParams = alwaysParams + firstOpt, 129 | defaultParams = remainingOpts, 130 | indentLevel = indentLevel + 1, 131 | isServerExtension = isServerExtension, 132 | ) 133 | appendLine(ifBranch) 134 | appendLine("$indent} else {") 135 | // If absent, skip it so the function call uses its default 136 | val elseBranch = generateToolOptionalChain( 137 | functionName = functionName, 138 | alwaysParams = alwaysParams, // not adding firstOpt 139 | defaultParams = remainingOpts, 140 | indentLevel = indentLevel + 1, 141 | isServerExtension = isServerExtension, 142 | ) 143 | appendLine(elseBranch) 144 | appendLine("$indent}") 145 | } 146 | } 147 | 148 | private fun Mcp4kProcessor.callToolFunction( 149 | functionName: String, 150 | alwaysParams: List, 151 | optionalParams: List, 152 | indentLevel: Int, 153 | isServerExtension: Boolean, 154 | ): String { 155 | val indent = " ".repeat(indentLevel * 2) 156 | val allParams = alwaysParams + optionalParams 157 | 158 | // Each parameter becomes "name = obj.name" (+ "!!" if it hasDefault && not nullable) 159 | val args = allParams.joinToString(",\n$indent ") { param -> 160 | // If param.hasDefault && !param.isNullable => we do "!!" to guarantee non-null 161 | val maybeBang = if (param.hasDefault && !param.isNullable) "!!" else "" 162 | "${param.name} = obj.${param.name}$maybeBang" 163 | } 164 | 165 | val prefix = if (isServerExtension) "server." else "" 166 | 167 | return buildString { 168 | appendLine("$indent$prefix$functionName(") 169 | if (allParams.isNotEmpty()) { 170 | appendLine("$indent $args") 171 | } 172 | append("$indent)") 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/tools/generateToolParamsClass.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp.tools 2 | 3 | import com.google.devtools.ksp.processing.Dependencies 4 | import com.google.devtools.ksp.symbol.KSFile 5 | import sh.ondr.mcp4k.ksp.Mcp4kProcessor 6 | 7 | fun Mcp4kProcessor.generateToolParamsClass( 8 | toolMeta: ToolMeta, 9 | originFile: KSFile, 10 | ) { 11 | val code = buildString { 12 | appendLine("package $mcp4kParamsPackage") 13 | appendLine() 14 | appendLine("import kotlinx.serialization.json.decodeFromJsonElement") 15 | appendLine("import kotlinx.serialization.json.JsonElement") 16 | appendLine("import kotlinx.serialization.Serializable") 17 | appendLine("import sh.ondr.koja.JsonSchema") 18 | appendLine() 19 | 20 | toolMeta.kdoc?.let { kdoc -> 21 | appendLine("/**") 22 | kdoc.split("\n").forEach { line -> 23 | appendLine(" * $line") 24 | } 25 | appendLine("*/") 26 | } 27 | 28 | appendLine("@Serializable") 29 | appendLine("@JsonSchema") 30 | appendLine("class ${toolMeta.paramsClassName}(") 31 | 32 | toolMeta.params.forEachIndexed { index, p -> 33 | val comma = if (index == toolMeta.params.size - 1) "" else "," 34 | 35 | // 1) Get the base (non-nullable) FQN 36 | val baseFqn = p.fqnTypeNonNullable 37 | 38 | // 2) Append '?' if it’s either nullable or hasDefault 39 | val finalType = if (!p.hasDefault && !p.isNullable) baseFqn else "$baseFqn?" 40 | 41 | if (!p.hasDefault && !p.isNullable) { 42 | // Required param => no default 43 | append(" val ${p.name}: $finalType$comma\n") 44 | } else { 45 | // Optional param => default to null so it can be omitted at JSON deserialization 46 | append(" val ${p.name}: $finalType = null$comma\n") 47 | } 48 | } 49 | appendLine(")") 50 | appendLine() 51 | appendLine("/**") 52 | appendLine(" * ${toolMeta.fqName}") 53 | appendLine("*/") 54 | appendLine("const val ${toolMeta.paramsClassName}OriginalSource = true") 55 | } 56 | 57 | // Use isolating dependencies with the originating source file 58 | val dependencies = Dependencies(aggregating = false, sources = arrayOf(originFile)) 59 | 60 | try { 61 | codeGenerator 62 | .createNewFile( 63 | dependencies = dependencies, 64 | packageName = mcp4kParamsPackage, 65 | fileName = toolMeta.paramsClassName, 66 | extensionName = "kt", 67 | ).use { output -> 68 | output.writer().use { writer -> 69 | writer.write(code) 70 | } 71 | } 72 | } catch (e: Exception) { 73 | // File already exists from a previous round, skip 74 | if (e.message?.contains("already exists") == true) { 75 | logger.info("Skipping already generated file: ${toolMeta.paramsClassName}.kt") 76 | } else { 77 | throw e 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/kotlin/sh/ondr/mcp4k/ksp/tools/validateTool.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp.tools 2 | 3 | import com.google.devtools.ksp.getVisibility 4 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 5 | import com.google.devtools.ksp.symbol.Modifier 6 | import com.google.devtools.ksp.symbol.Visibility 7 | import sh.ondr.mcp4k.ksp.Mcp4kProcessor 8 | 9 | /** 10 | * Validates a tool function and its metadata. 11 | * Returns true if valid, false if errors were found (errors are logged). 12 | */ 13 | internal fun Mcp4kProcessor.validateTool( 14 | ksFunction: KSFunctionDeclaration, 15 | toolMeta: ToolMeta, 16 | ): Boolean { 17 | var isValid = true 18 | 19 | // 1. Return type must be ToolContent or subtype 20 | val contentPkg = "$pkg.schema.content" 21 | val allowed = setOf( 22 | "$contentPkg.ToolContent", 23 | "$contentPkg.EmbeddedResourceContent", 24 | "$contentPkg.TextContent", 25 | "$contentPkg.ImageContent", 26 | ) 27 | if (toolMeta.returnTypeFqn !in allowed) { 28 | logger.error( 29 | "MCP4K error: @McpTool function '${toolMeta.functionName}' must return ToolContent or a sub-type. " + 30 | "Currently returns: ${toolMeta.returnTypeReadable}. " + 31 | "Please change the return type to ToolContent.", 32 | symbol = ksFunction, 33 | ) 34 | isValid = false 35 | } 36 | 37 | // 2. Limit non-required parameters 38 | val nonRequiredLimit = 7 39 | val nonRequiredCount = toolMeta.params.count { !it.isRequired } 40 | if (nonRequiredCount > nonRequiredLimit) { 41 | logger.error( 42 | "MCP4K error: @McpTool function '${toolMeta.functionName}' has more than $nonRequiredLimit non-required parameters. " + 43 | "Please reduce optional/nullable/default parameters to $nonRequiredLimit or fewer.", 44 | symbol = ksFunction, 45 | ) 46 | isValid = false 47 | } 48 | 49 | // 3. Must be top-level function 50 | if (ksFunction.parentDeclaration != null) { 51 | val parent = ksFunction.parentDeclaration?.qualifiedName?.asString() ?: "unknown parent" 52 | logger.error( 53 | "MCP4K error: @McpTool function '${toolMeta.functionName}' is defined inside a class or object ($parent). " + 54 | "@McpTool functions must be top-level. Move '${toolMeta.functionName}' to file scope.", 55 | symbol = ksFunction, 56 | ) 57 | isValid = false 58 | } 59 | 60 | // 4. Must have no disallowed modifiers 61 | val disallowedModifiers = setOf( 62 | Modifier.INLINE, 63 | Modifier.PRIVATE, 64 | Modifier.PROTECTED, 65 | Modifier.INTERNAL, 66 | Modifier.ABSTRACT, 67 | Modifier.OPEN, 68 | ) 69 | 70 | val visibility = ksFunction.getVisibility() 71 | if (visibility != Visibility.PUBLIC) { 72 | logger.error( 73 | "MCP4K error: @McpTool function '${toolMeta.functionName}' must be public. " + 74 | "Current visibility: $visibility. " + 75 | "Please ensure it's a top-level public function with no modifiers.", 76 | symbol = ksFunction, 77 | ) 78 | isValid = false 79 | } 80 | 81 | val foundDisallowed = ksFunction.modifiers.filter { it in disallowedModifiers } 82 | if (foundDisallowed.isNotEmpty()) { 83 | logger.error( 84 | "MCP4K error: @McpTool function '${toolMeta.functionName}' has disallowed modifiers: $foundDisallowed. " + 85 | "Only public, top-level, non-inline, non-abstract, non-internal functions are allowed.", 86 | symbol = ksFunction, 87 | ) 88 | isValid = false 89 | } 90 | 91 | // 5. Check extension receivers (must be null or Server) 92 | if (toolMeta.isServerExtension) { 93 | ksFunction.extensionReceiver?.resolve()?.declaration?.qualifiedName?.asString()?.let { receiverFq -> 94 | if (receiverFq != "sh.ondr.mcp4k.runtime.Server") { 95 | logger.error( 96 | "MCP4K error: @McpTool function '${toolMeta.functionName}' is an extension function, but the receiver type is not 'Server'. " + 97 | "Please ensure the extension receiver is 'Server' or 'null'.", 98 | symbol = ksFunction, 99 | ) 100 | isValid = false 101 | } 102 | } 103 | } 104 | 105 | return isValid 106 | } 107 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider: -------------------------------------------------------------------------------- 1 | sh.ondr.mcp4k.ksp.Mcp4kProcessorProvider 2 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/test/kotlin/sh/ondr/mcp4k/ksp/BaseKspTest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp 2 | 3 | import org.junit.jupiter.api.AfterEach 4 | import org.junit.jupiter.api.BeforeEach 5 | import java.io.File 6 | import java.nio.file.Files 7 | 8 | // TODO fix 9 | abstract class BaseKspTest { 10 | lateinit var projectDir: File 11 | 12 | @BeforeEach 13 | fun setUp() { 14 | projectDir = createMultiplatformProject() 15 | } 16 | 17 | @AfterEach 18 | fun tearDown() { 19 | projectDir.deleteRecursively() 20 | } 21 | 22 | fun createMultiplatformProject( 23 | projectName: String = "testProject", 24 | configure: (File) -> Unit = {}, 25 | ): File { 26 | val projectDir = 27 | Files.createTempDirectory(projectName).toFile().apply { 28 | deleteOnExit() 29 | } 30 | 31 | // Write common files 32 | projectDir.resolve("settings.gradle.kts").writeText( 33 | """ 34 | pluginManagement { 35 | repositories { 36 | gradlePluginPortal() 37 | mavenCentral() 38 | mavenLocal() 39 | } 40 | } 41 | """.trimIndent(), 42 | ) 43 | 44 | projectDir.resolve("build.gradle.kts").writeText( 45 | """ 46 | repositories { 47 | mavenCentral() 48 | mavenLocal() 49 | } 50 | 51 | plugins { 52 | id("org.jetbrains.kotlin.multiplatform") version "2.1.0" 53 | id("sh.ondr.mcp4k") version "0.1.0" 54 | } 55 | 56 | kotlin { 57 | jvm() 58 | js(IR) { 59 | nodejs() 60 | } 61 | 62 | macosArm64() 63 | iosX64() 64 | iosArm64() 65 | 66 | sourceSets { 67 | commonMain { 68 | dependencies { 69 | implementation("sh.ondr:kotlin-json-schema:0.1.1") 70 | } 71 | } 72 | } 73 | 74 | 75 | } 76 | """.trimIndent(), 77 | ) 78 | 79 | configure(projectDir) 80 | return projectDir 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/test/kotlin/sh/ondr/mcp4k/ksp/KspTest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp 2 | 3 | import org.gradle.testkit.runner.GradleRunner 4 | import kotlin.test.Ignore 5 | import kotlin.test.Test 6 | import kotlin.test.assertTrue 7 | 8 | class KspTest : BaseKspTest() { 9 | // TODO broke, fix 10 | @Ignore 11 | @Test 12 | fun testToolFunctionGeneration() { 13 | projectDir.resolve("src/commonMain/kotlin/test/Test.kt").apply { 14 | parentFile.mkdirs() 15 | writeText( 16 | """ 17 | package test 18 | 19 | import sh.ondr.mcp4k.runtime.annotation.McpTool 20 | import sh.ondr.mcp4k.schema.content.ToolContent 21 | import kotlinx.serialization.Serializable 22 | 23 | @Serializable 24 | data class Location( 25 | val city: String, 26 | val country: String, 27 | ) 28 | 29 | @McpTool 30 | fun greet(name: String, age: Int, location: Location): ToolContent { 31 | return TextContent("Hello \${'$'}name, you are \${'$'}age years old and from \${'$'}{location.city} in \${'$'}{location.country}!") 32 | } 33 | """.trimIndent(), 34 | ) 35 | 36 | val initialResult = GradleRunner.create() 37 | .withProjectDir(projectDir) 38 | .withArguments("clean", "build") 39 | .forwardOutput() 40 | .build() 41 | 42 | println(projectDir) 43 | assertTrue(initialResult.output.contains("BUILD SUCCESSFUL"), "Build was not successful.") 44 | checkGeneratedFile( 45 | projectDir = projectDir, 46 | className = "Mcp4kGeneratedToolRegistryInitializer", 47 | ) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/test/kotlin/sh/ondr/mcp4k/ksp/TestUtil.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.ksp 2 | 3 | import org.gradle.internal.impldep.com.google.api.client.googleapis.testing.TestUtils 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Assertions.assertTrue 6 | import java.io.File 7 | import kotlin.jvm.java 8 | 9 | fun normalizeWhitespace(input: String) = input.lines().joinToString("\n") { it.trim() } 10 | 11 | /** 12 | * Checks if a generated file exists and matches the expected content. 13 | * 14 | * @param projectDir The project directory used for the test. 15 | * @param className The class name of the generated file without the .kt extension. 16 | * For example "Mcp4kGeneratedToolRegistryInitializer" or "Mcp4kGeneratedGreetParameters". 17 | * @param packagePath The path to the generated package directory relative to "build/generated/ksp/jvm/jvmMain/kotlin". 18 | * For example "sh/ondr/mcp4k/generated". 19 | */ 20 | fun checkGeneratedFile( 21 | projectDir: File, 22 | className: String, 23 | packagePath: String = "sh/ondr/mcp4k/generated", 24 | ) { 25 | val generatedFile = 26 | File( 27 | projectDir, 28 | "build/generated/ksp/jvm/jvmMain/kotlin/$packagePath/$className.kt", 29 | ) 30 | 31 | assertTrue(generatedFile.exists(), "Generated file not found for class: $className") 32 | 33 | val actualContent = generatedFile.readText() 34 | val expectedContent = TestUtils::class.java.getResource("/expected/$className.kt.expected").readText() 35 | 36 | val expectedNormalized = normalizeWhitespace(expectedContent) 37 | val actualNormalized = normalizeWhitespace(actualContent) 38 | assertEquals(expectedNormalized, actualNormalized, "Generated code for $className does not match expected output after formatting.") 39 | } 40 | -------------------------------------------------------------------------------- /mcp4k-ksp/src/test/resources/expected/Mcp4kGeneratedToolRegistryInitializer.kt.expected: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.generated 2 | 3 | import sh.ondr.mcp4k.runtime.MCP4K 4 | 5 | object Mcp4kGeneratedToolRegistryInitializer { 6 | init { 7 | MCP4K.toolInfos["greetUser"] = ToolInfo(...) 8 | MCP4K.toolHandlers["greetUser"] = ToolHandler(...) 9 | MCP4K.toolInfos["myTool"] = ToolInfo(...) 10 | MCP4K.toolHandlers["myTool"] = ToolHandler(...) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /mcp4k-runtime/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.JavadocJar 2 | import com.vanniktech.maven.publish.KotlinMultiplatform 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | import org.jetbrains.kotlin.konan.target.HostManager 5 | 6 | plugins { 7 | alias(libs.plugins.kotlin.multiplatform) 8 | alias(libs.plugins.kotlin.serialization) 9 | alias(libs.plugins.maven.publish) 10 | alias(libs.plugins.dokka) 11 | } 12 | 13 | kotlin { 14 | js(IR) { 15 | nodejs() 16 | binaries.library() 17 | } 18 | jvm { 19 | compilerOptions { 20 | jvmTarget.set(JvmTarget.JVM_11) 21 | } 22 | } 23 | 24 | when { 25 | HostManager.hostIsMac -> { 26 | iosArm64() 27 | iosSimulatorArm64() 28 | iosX64() 29 | macosArm64() 30 | macosX64() 31 | linuxX64() 32 | mingwX64() 33 | } 34 | 35 | HostManager.hostIsLinux -> { 36 | linuxX64() 37 | } 38 | 39 | HostManager.hostIsMingw -> { 40 | mingwX64() 41 | } 42 | } 43 | 44 | sourceSets { 45 | commonMain { 46 | dependencies { 47 | implementation(libs.kotlinx.atomicfu) 48 | implementation(libs.kotlinx.coroutines.core) 49 | implementation(libs.kotlinx.serialization.core) 50 | implementation(libs.kotlinx.serialization.json) 51 | implementation(libs.koja.runtime) 52 | } 53 | } 54 | } 55 | } 56 | 57 | mavenPublishing { 58 | configure(KotlinMultiplatform(javadocJar = JavadocJar.Dokka("dokkaGeneratePublicationHtml"))) 59 | } 60 | -------------------------------------------------------------------------------- /mcp4k-runtime/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=mcp4k-runtime 2 | POM_NAME=MCP4K Runtime 3 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/ServerContext.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime 2 | 3 | interface ServerContext 4 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/annotation/McpPrompt.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.annotation 2 | 3 | @Target(AnnotationTarget.FUNCTION) 4 | annotation class McpPrompt 5 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/annotation/McpTool.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.annotation 2 | 3 | @Target(AnnotationTarget.FUNCTION) 4 | annotation class McpTool( 5 | val description: String = "", 6 | ) 7 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/core/ClientApprovable.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.core 2 | 3 | interface ClientApprovable 4 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/core/Global.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.core 2 | 3 | import kotlinx.serialization.json.Json 4 | import sh.ondr.mcp4k.runtime.prompts.McpPromptHandler 5 | import sh.ondr.mcp4k.runtime.serialization.mcp4kSerializersModule 6 | import sh.ondr.mcp4k.runtime.tools.McpToolHandler 7 | import kotlin.reflect.KClass 8 | 9 | const val JSON_RPC_VERSION = "2.0" 10 | const val MCP_VERSION = "2024-11-05" 11 | 12 | val mcpJson = Json { 13 | encodeDefaults = true 14 | explicitNulls = false 15 | isLenient = true 16 | ignoreUnknownKeys = true 17 | classDiscriminator = "method" 18 | serializersModule = mcp4kSerializersModule 19 | } 20 | 21 | val mcpToolParams = mutableMapOf>() 22 | val mcpToolHandlers = mutableMapOf() 23 | val mcpPromptParams = mutableMapOf>() 24 | val mcpPromptHandlers = mutableMapOf() 25 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/core/Util.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.core 2 | 3 | import sh.ondr.mcp4k.schema.content.TextContent 4 | import sh.ondr.mcp4k.schema.core.Annotations 5 | 6 | fun String.toTextContent(annotations: Annotations? = null) = TextContent(this, annotations) 7 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/core/pagination/PaginatedEndpoint.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.core.pagination 2 | 3 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 4 | import sh.ondr.mcp4k.schema.core.PaginatedResult 5 | 6 | /** 7 | * A descriptor for a paginated RPC endpoint, tying together: 8 | * 9 | * 1. A paginated [REQ] request type (e.g. [sh.ondr.mcp4k.schema.prompts.ListPromptsRequest]), 10 | * 2. A [RES] result type (e.g. [sh.ondr.mcp4k.schema.prompts.ListPromptsResult]), 11 | * 3. A [requestFactory] function to build each request page by page, 12 | * 4. A [transform] function converting the returned [RES] into higher-level [ITEMS]. 13 | * 14 | * To see an example implementation of this interface, see [sh.ondr.mcp4k.schema.prompts.ListPromptsRequest.Companion]. 15 | * 16 | * @param REQ The concrete request class that implements [JsonRpcRequest]. 17 | * @param RES The paginated result class that implements [PaginatedResult]. 18 | * @param ITEMS The type of items your application wants to consume or emit for each page. 19 | */ 20 | interface PaginatedEndpoint { 21 | val requestFactory: PaginatedRequestFactory 22 | 23 | /** 24 | * Takes the [RES] object (the server's result) and 25 | * transforms it into the items to be emitted, typically a List. 26 | */ 27 | fun transform(result: RES): ITEMS 28 | } 29 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/core/pagination/PaginatedRequestFactory.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.core.pagination 2 | 3 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 4 | 5 | /** 6 | * Used to build each "list" request in a paginated flow, injecting 7 | * the current `cursor` between pages. 8 | * 9 | * @param R The concrete request class (e.g. `ListPromptsRequest`) that implements [JsonRpcRequest]. 10 | */ 11 | fun interface PaginatedRequestFactory { 12 | /** 13 | * Creates a new paginated JSON-RPC request of type [R] given a unique [id] and an optional [cursor] string. 14 | */ 15 | fun create( 16 | id: String, 17 | cursor: String?, 18 | ): R 19 | } 20 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/core/pagination/paginate.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalEncodingApi::class) 2 | 3 | package sh.ondr.mcp4k.runtime.core.pagination 4 | 5 | import kotlinx.serialization.json.Json 6 | import kotlinx.serialization.json.JsonObject 7 | import kotlinx.serialization.json.buildJsonObject 8 | import kotlinx.serialization.json.contentOrNull 9 | import kotlinx.serialization.json.int 10 | import kotlinx.serialization.json.jsonObject 11 | import kotlinx.serialization.json.jsonPrimitive 12 | import kotlinx.serialization.json.put 13 | import kotlin.io.encoding.Base64 14 | import kotlin.io.encoding.ExperimentalEncodingApi 15 | 16 | /** 17 | * Returns a sub-list of [items] plus a `nextCursor` if there's another page. 18 | */ 19 | fun paginate( 20 | items: List, 21 | cursor: String?, 22 | pageSize: Int, 23 | ): Pair, String?> { 24 | val (page, decodedPageSize) = decodeCursor(cursor) 25 | val effectivePageSize = decodedPageSize ?: pageSize 26 | 27 | val fromIndex = page * effectivePageSize 28 | if (fromIndex >= items.size) { 29 | return emptyList() to null 30 | } 31 | 32 | val toIndex = minOf(fromIndex + effectivePageSize, items.size) 33 | val slice = items.subList(fromIndex, toIndex) 34 | 35 | val newCursor = if (toIndex < items.size) { 36 | encodeCursor(page + 1, effectivePageSize) 37 | } else { 38 | null 39 | } 40 | 41 | return slice to newCursor 42 | } 43 | 44 | private fun decodeCursor(cursor: String?): Pair { 45 | if (cursor == null) return 0 to null 46 | try { 47 | val jsonStr = Base64.decode(cursor).decodeToString() 48 | val obj = Json.parseToJsonElement(jsonStr).jsonObject 49 | 50 | val page = obj["page"]?.jsonPrimitive?.int ?: 0 51 | val pageSize = obj["pageSize"]?.jsonPrimitive?.contentOrNull?.toIntOrNull() 52 | return page to pageSize 53 | } catch (e: Throwable) { 54 | // Should respond with JsonRpcErrorCodes.INVALID_PARAMS in your handler 55 | throw IllegalArgumentException("Invalid cursor: $cursor", e) 56 | } 57 | } 58 | 59 | private fun encodeCursor( 60 | page: Int, 61 | pageSize: Int, 62 | ): String { 63 | val obj = buildJsonObject { 64 | put("page", page) 65 | put("pageSize", pageSize) 66 | } 67 | val jsonStr = Json.encodeToString(JsonObject.serializer(), obj) 68 | return Base64.encode(jsonStr.encodeToByteArray()) 69 | } 70 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/error/ErrorUtil.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class) 2 | 3 | package sh.ondr.mcp4k.runtime.error 4 | 5 | import kotlinx.serialization.ExperimentalSerializationApi 6 | import sh.ondr.mcp4k.schema.core.JsonRpcErrorCodes 7 | 8 | internal fun determineErrorResponse( 9 | method: String?, 10 | e: Throwable, 11 | ): Pair { 12 | val methodSuffix = if (method != null) " in $method request" else "" 13 | return when (e) { 14 | is kotlinx.serialization.MissingFieldException -> { 15 | val msg = if (e.missingFields.size == 1) { 16 | "Missing required field '${e.missingFields.single()}'$methodSuffix" 17 | } else { 18 | "Missing required fields$methodSuffix: [${e.missingFields.joinToString()}]" 19 | } 20 | JsonRpcErrorCodes.INVALID_PARAMS to msg 21 | } 22 | 23 | // Without id handling here, just return a generic error 24 | // If parsing failed at a low level, `id` is null and we won't respond anyway. 25 | is kotlinx.serialization.SerializationException -> { 26 | JsonRpcErrorCodes.INVALID_PARAMS to "Invalid parameters$methodSuffix." 27 | } 28 | 29 | else -> { 30 | // Everything else is an internal error or parse error if no id 31 | // Without id or parse checking, just return internal error message. 32 | JsonRpcErrorCodes.INTERNAL_ERROR to "Internal error: ${e.message ?: "unknown"}" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/error/MethodNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.error 2 | 3 | class MethodNotFoundException(message: String) : RuntimeException(message) 4 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/error/MissingRequiredArgumentException.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.error 2 | 3 | class MissingRequiredArgumentException(message: String) : RuntimeException(message) 4 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/error/ResourceNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.error 2 | 3 | class ResourceNotFoundException(message: String) : RuntimeException(message) 4 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/error/UnknownArgumentException.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.error 2 | 3 | // Known invalid params scenario 4 | class UnknownArgumentException(message: String) : RuntimeException(message) 5 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/prompts/McpPromptHandler.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.prompts 2 | 3 | import kotlinx.serialization.json.JsonObject 4 | import sh.ondr.mcp4k.runtime.Server 5 | import sh.ondr.mcp4k.schema.prompts.GetPromptResult 6 | 7 | interface McpPromptHandler { 8 | suspend fun call( 9 | server: Server, 10 | params: JsonObject, 11 | ): GetPromptResult 12 | } 13 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/prompts/promptDsl.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.prompts 2 | 3 | import sh.ondr.mcp4k.schema.content.PromptContent 4 | import sh.ondr.mcp4k.schema.content.TextContent 5 | import sh.ondr.mcp4k.schema.core.Role 6 | import sh.ondr.mcp4k.schema.prompts.GetPromptResult 7 | import sh.ondr.mcp4k.schema.prompts.PromptMessage 8 | 9 | // A builder for constructing a GetPromptResult easily 10 | fun buildPrompt( 11 | description: String? = null, 12 | block: PromptBuilder.() -> Unit, 13 | ): GetPromptResult { 14 | val builder = PromptBuilder() 15 | builder.block() 16 | return GetPromptResult( 17 | description = description, 18 | messages = builder.buildMessages(), 19 | ) 20 | } 21 | 22 | class PromptBuilder { 23 | private val messages = mutableListOf() 24 | 25 | fun user(message: String) { 26 | messages += PromptMessage( 27 | role = Role.USER, 28 | content = TextContent(message), 29 | ) 30 | } 31 | 32 | fun user(messageBlock: () -> String) { 33 | user(messageBlock()) 34 | } 35 | 36 | fun user(content: PromptContent) { 37 | messages += PromptMessage( 38 | role = Role.USER, 39 | content = content, 40 | ) 41 | } 42 | 43 | fun assistant(message: String) { 44 | messages += PromptMessage( 45 | role = Role.ASSISTANT, 46 | content = TextContent(message), 47 | ) 48 | } 49 | 50 | fun assistant(messageBlock: () -> String) { 51 | assistant(messageBlock()) 52 | } 53 | 54 | fun assistant(content: PromptContent) { 55 | messages += PromptMessage( 56 | role = Role.ASSISTANT, 57 | content = content, 58 | ) 59 | } 60 | 61 | fun buildMessages(): List = messages 62 | } 63 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/resources/ResourceProvider.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.resources 2 | 3 | import sh.ondr.mcp4k.schema.resources.Resource 4 | import sh.ondr.mcp4k.schema.resources.ResourceContents 5 | import sh.ondr.mcp4k.schema.resources.ResourceTemplate 6 | 7 | /** 8 | * Provides access to a set of resources and (optionally) notifies about changes. 9 | * 10 | * Subclasses can override [supportsSubscriptions] to indicate whether they can emit 11 | * resource change notifications via [onResourceChange] or [onResourcesListChanged]. 12 | */ 13 | abstract class ResourceProvider { 14 | /** 15 | * Whether this provider can emit resource change notifications. 16 | */ 17 | abstract val supportsSubscriptions: Boolean 18 | 19 | /** 20 | * Implementations should call this when a specific resource changes. By default, no-op. 21 | */ 22 | var onResourceChange: suspend (uri: String) -> Unit = {} 23 | 24 | /** 25 | * Implementations should call this when the overall list of resources changes (new ones added, removed, etc.). 26 | * By default, no-op. 27 | */ 28 | var onResourcesListChanged: suspend () -> Unit = {} 29 | 30 | /** 31 | * Sets callbacks for resource change notifications. Called by the server/manager. 32 | */ 33 | open fun attachCallbacks( 34 | onResourceChange: suspend (String) -> Unit, 35 | onResourcesListChanged: suspend () -> Unit, 36 | ) { 37 | this.onResourceChange = onResourceChange 38 | this.onResourcesListChanged = onResourcesListChanged 39 | } 40 | 41 | /** 42 | * Returns the list of currently available resources. 43 | */ 44 | open suspend fun listResources(): List = listOf() 45 | 46 | /** 47 | * Fetches the contents of the given resource URI, or `null` if unavailable. 48 | */ 49 | abstract suspend fun readResource(uri: String): ResourceContents? 50 | 51 | /** 52 | * Returns any resource templates this provider supports, e.g. `file:///{path}`. 53 | */ 54 | open suspend fun listResourceTemplates(): List = listOf() 55 | } 56 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/resources/ResourceProviderManager.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.resources 2 | 3 | import sh.ondr.mcp4k.schema.resources.Resource 4 | import sh.ondr.mcp4k.schema.resources.ResourceContents 5 | import sh.ondr.mcp4k.schema.resources.ResourceTemplate 6 | 7 | class ResourceProviderManager( 8 | private val notifyResourceChanged: suspend (String) -> Unit, 9 | private val notifyResourcesListChanged: suspend () -> Unit, 10 | builderResourceProviders: List, 11 | ) { 12 | val providers = mutableListOf() 13 | 14 | init { 15 | builderResourceProviders.forEach { provider -> 16 | addProvider(provider) 17 | } 18 | } 19 | 20 | val supportsSubscriptions: Boolean 21 | get() = providers.any { it.supportsSubscriptions } 22 | 23 | val subscriptions: MutableSet = mutableSetOf() 24 | 25 | suspend fun onResourceChange(uri: String) { 26 | if (subscriptions.contains(uri)) { 27 | notifyResourceChanged(uri) 28 | } 29 | } 30 | 31 | suspend fun onResourcesListChanged() { 32 | notifyResourcesListChanged() 33 | } 34 | 35 | fun subscribe(uri: String) { 36 | subscriptions.add(uri) 37 | } 38 | 39 | fun removeSubscription(uri: String) { 40 | subscriptions.remove(uri) 41 | } 42 | 43 | fun addProvider(provider: ResourceProvider) { 44 | providers += provider 45 | provider.attachCallbacks( 46 | onResourceChange = { uri -> 47 | onResourceChange(uri) 48 | }, 49 | onResourcesListChanged = { 50 | onResourcesListChanged() 51 | }, 52 | ) 53 | } 54 | 55 | suspend fun listResources(): List = 56 | providers 57 | .flatMap { it.listResources() } 58 | .distinctBy { it.uri } 59 | 60 | suspend fun readResource(uri: String): ResourceContents? = 61 | providers.firstNotNullOfOrNull { provider -> 62 | provider.readResource(uri) 63 | } 64 | 65 | suspend fun listResourceTemplates(): List { 66 | return providers.flatMap { it.listResourceTemplates() } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/sampling/SamplingProvider.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.sampling 2 | 3 | import sh.ondr.mcp4k.schema.sampling.CreateMessageRequest.CreateMessageParams 4 | import sh.ondr.mcp4k.schema.sampling.CreateMessageResult 5 | 6 | /** 7 | * A pluggable interface for how the client handles a sampling/createMessage request. 8 | * Providing model selection, LLM-calling logic, and ultimately returning a [CreateMessageResult]. 9 | */ 10 | fun interface SamplingProvider { 11 | /** 12 | * Called when a `sampling/createMessage` request arrives from the server and 13 | * the client grants permission. 14 | * 15 | * The implementation should: 16 | * - Pick a model 17 | * - Call an LLM 18 | * - Return a [CreateMessageResult] with containing the final model, role, content, etc. 19 | * - Throw an error if something goes wrong. 20 | */ 21 | suspend fun createMessage(params: CreateMessageParams): CreateMessageResult 22 | } 23 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/serialization/JsonRpcMessageSerializer.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.serialization 2 | 3 | import kotlinx.serialization.DeserializationStrategy 4 | import kotlinx.serialization.SerializationException 5 | import kotlinx.serialization.json.JsonContentPolymorphicSerializer 6 | import kotlinx.serialization.json.JsonElement 7 | import kotlinx.serialization.json.jsonObject 8 | import sh.ondr.mcp4k.schema.core.JsonRpcMessage 9 | import sh.ondr.mcp4k.schema.core.JsonRpcNotification 10 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 11 | import sh.ondr.mcp4k.schema.core.JsonRpcResponse 12 | 13 | object JsonRpcMessageSerializer : JsonContentPolymorphicSerializer(JsonRpcMessage::class) { 14 | override fun selectDeserializer(element: JsonElement): DeserializationStrategy { 15 | val obj = element.jsonObject 16 | 17 | val id = obj["id"] 18 | val method = obj["method"] 19 | val result = obj["result"] 20 | val error = obj["error"] 21 | 22 | return when { 23 | // Response: has id, and either result or error, but no method 24 | id != null && (result != null || error != null) && method == null -> JsonRpcResponse.serializer() 25 | 26 | // Request: has id and method 27 | id != null && method != null -> JsonRpcRequest.serializer() 28 | 29 | // Notification: has method but no id 30 | id == null && method != null -> JsonRpcNotification.serializer() 31 | else -> throw SerializationException("Unknown message type: $element") 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/serialization/ResourceContentsSerializer.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.serialization 2 | 3 | import kotlinx.serialization.json.JsonContentPolymorphicSerializer 4 | import kotlinx.serialization.json.JsonElement 5 | import kotlinx.serialization.json.jsonObject 6 | import sh.ondr.mcp4k.schema.resources.ResourceContents 7 | 8 | object ResourceContentsSerializer : JsonContentPolymorphicSerializer(ResourceContents::class) { 9 | override fun selectDeserializer(element: JsonElement) = 10 | when { 11 | element.jsonObject.containsKey("blob") -> ResourceContents.Blob.serializer() 12 | element.jsonObject.containsKey("text") -> ResourceContents.Text.serializer() 13 | else -> error("Could not deserialize ResourceContents: neither 'blob' nor 'text' present.") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/serialization/SerializationUtil.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.serialization 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import kotlinx.serialization.json.JsonObject 6 | import kotlinx.serialization.json.decodeFromJsonElement 7 | import kotlinx.serialization.json.encodeToJsonElement 8 | import kotlinx.serialization.json.jsonObject 9 | import sh.ondr.koja.kojaJson 10 | import sh.ondr.mcp4k.runtime.core.mcpJson 11 | import sh.ondr.mcp4k.schema.core.JsonRpcMessage 12 | import sh.ondr.mcp4k.schema.core.JsonRpcNotification 13 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 14 | import sh.ondr.mcp4k.schema.core.Result 15 | import sh.ondr.mcp4k.schema.tools.ListToolsResult 16 | 17 | // TODO clean this mess up 18 | 19 | /** 20 | * Serializes a result object into a JsonElement. 21 | */ 22 | inline fun serializeResult(value: T): JsonElement { 23 | return if (value is ListToolsResult) { 24 | // Workaround to serialize @JsonSchema correctly 25 | kojaJson.encodeToJsonElement(value) 26 | } else { 27 | mcpJson.encodeToJsonElement(value) 28 | } 29 | } 30 | 31 | inline fun JsonElement?.deserializeResult(): T? { 32 | if (this == null) return null 33 | 34 | return if (T::class == ListToolsResult::class) { 35 | // Workaround to deserialize @JsonSchema correctly 36 | kojaJson.decodeFromJsonElement(this) as T 37 | } else { 38 | // Decode using MCP4K.json for all other result types 39 | mcpJson.decodeFromJsonElement(this) 40 | } 41 | } 42 | 43 | inline fun T.toJsonObject(): JsonObject { 44 | return mcpJson.encodeToJsonElement(this).jsonObject 45 | } 46 | 47 | fun String.toJsonRpcMessage(): JsonRpcMessage { 48 | return mcpJson.decodeFromString(JsonRpcMessageSerializer, this) 49 | } 50 | 51 | fun JsonRpcNotification.serializeToString(): String { 52 | return mcpJson.encodeToString(JsonRpcNotification.serializer(), this) 53 | } 54 | 55 | fun JsonRpcRequest.serializeToString(): String { 56 | return mcpJson.encodeToString(JsonRpcRequest.serializer(), this) 57 | } 58 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/serialization/SerializersModule.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.serialization 2 | 3 | import kotlinx.serialization.modules.SerializersModule 4 | import kotlinx.serialization.modules.polymorphic 5 | import kotlinx.serialization.modules.subclass 6 | import sh.ondr.mcp4k.schema.capabilities.InitializeRequest 7 | import sh.ondr.mcp4k.schema.capabilities.InitializedNotification 8 | import sh.ondr.mcp4k.schema.completion.CompleteRequest 9 | import sh.ondr.mcp4k.schema.core.CancelledNotification 10 | import sh.ondr.mcp4k.schema.core.JsonRpcNotification 11 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 12 | import sh.ondr.mcp4k.schema.core.PingRequest 13 | import sh.ondr.mcp4k.schema.core.ProgressNotification 14 | import sh.ondr.mcp4k.schema.logging.LoggingMessageNotification 15 | import sh.ondr.mcp4k.schema.logging.SetLoggingLevelRequest 16 | import sh.ondr.mcp4k.schema.prompts.GetPromptRequest 17 | import sh.ondr.mcp4k.schema.prompts.ListPromptsRequest 18 | import sh.ondr.mcp4k.schema.prompts.PromptListChangedNotification 19 | import sh.ondr.mcp4k.schema.resources.ListResourceTemplatesRequest 20 | import sh.ondr.mcp4k.schema.resources.ListResourcesRequest 21 | import sh.ondr.mcp4k.schema.resources.ReadResourceRequest 22 | import sh.ondr.mcp4k.schema.resources.ResourceListChangedNotification 23 | import sh.ondr.mcp4k.schema.resources.ResourceUpdatedNotification 24 | import sh.ondr.mcp4k.schema.resources.SubscribeRequest 25 | import sh.ondr.mcp4k.schema.resources.UnsubscribeRequest 26 | import sh.ondr.mcp4k.schema.roots.ListRootsRequest 27 | import sh.ondr.mcp4k.schema.roots.RootsListChangedNotification 28 | import sh.ondr.mcp4k.schema.sampling.CreateMessageRequest 29 | import sh.ondr.mcp4k.schema.tools.CallToolRequest 30 | import sh.ondr.mcp4k.schema.tools.ListToolsRequest 31 | import sh.ondr.mcp4k.schema.tools.ToolListChangedNotification 32 | 33 | val mcp4kSerializersModule = SerializersModule { 34 | // Register all known request subclasses 35 | polymorphic(JsonRpcRequest::class) { 36 | subclass(CallToolRequest::class) 37 | subclass(GetPromptRequest::class) 38 | subclass(InitializeRequest::class) 39 | subclass(PingRequest::class) 40 | subclass(ListPromptsRequest::class) 41 | subclass(ListResourcesRequest::class) 42 | subclass(ListResourceTemplatesRequest::class) 43 | subclass(ReadResourceRequest::class) 44 | subclass(SubscribeRequest::class) 45 | subclass(UnsubscribeRequest::class) 46 | subclass(ListToolsRequest::class) 47 | subclass(CompleteRequest::class) 48 | subclass(CreateMessageRequest::class) 49 | subclass(SetLoggingLevelRequest::class) 50 | subclass(ListRootsRequest::class) 51 | } 52 | 53 | // Register all known notification subclasses 54 | polymorphic(JsonRpcNotification::class) { 55 | subclass(CancelledNotification::class) 56 | subclass(InitializedNotification::class) 57 | subclass(ProgressNotification::class) 58 | subclass(ResourceListChangedNotification::class) 59 | subclass(ResourceUpdatedNotification::class) 60 | subclass(PromptListChangedNotification::class) 61 | subclass(ToolListChangedNotification::class) 62 | subclass(LoggingMessageNotification::class) 63 | subclass(RootsListChangedNotification::class) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/tools/McpToolHandler.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.tools 2 | 3 | import kotlinx.serialization.json.JsonObject 4 | import sh.ondr.mcp4k.runtime.Server 5 | import sh.ondr.mcp4k.schema.tools.CallToolResult 6 | 7 | interface McpToolHandler { 8 | suspend fun call( 9 | server: Server, 10 | params: JsonObject, 11 | ): CallToolResult 12 | } 13 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/tools/McpToolUtil.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalSerializationApi::class) 2 | 3 | package sh.ondr.mcp4k.runtime.tools 4 | 5 | import kotlinx.serialization.InternalSerializationApi 6 | import kotlinx.serialization.serializer 7 | import sh.ondr.koja.Schema 8 | import sh.ondr.koja.toSchema 9 | import sh.ondr.mcp4k.runtime.core.mcpToolParams 10 | import sh.ondr.mcp4k.schema.tools.Tool 11 | 12 | fun getMcpTool(name: String): Tool { 13 | val params = mcpToolParams[name] ?: throw IllegalStateException("Tool not found: $name") 14 | val paramsSchema = params.serializer().descriptor.toSchema() as Schema.ObjectSchema 15 | return Tool( 16 | name = name, 17 | description = paramsSchema.description, 18 | inputSchema = paramsSchema.copy(description = null), 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/transport/ChannelTransport.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.transport 2 | 3 | import kotlinx.coroutines.channels.Channel 4 | 5 | class ChannelTransport( 6 | private val incoming: Channel = Channel(Channel.UNLIMITED), 7 | private val outgoing: Channel = Channel(Channel.UNLIMITED), 8 | ) : Transport { 9 | override suspend fun readString(): String? { 10 | return incoming.receiveCatching().getOrNull() 11 | } 12 | 13 | override suspend fun writeString(message: String) { 14 | outgoing.send(message) 15 | } 16 | 17 | override suspend fun close() { 18 | // Closing channels signals no more messages 19 | incoming.close() 20 | outgoing.close() 21 | } 22 | 23 | fun flip() = 24 | ChannelTransport( 25 | incoming = outgoing, 26 | outgoing = incoming, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/transport/StdioTransport.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.transport 2 | 3 | // TODO create non-blocking version 4 | class StdioTransport : Transport { 5 | // Blocks until a line is read or EOF is reached. 6 | override suspend fun readString(): String? = readlnOrNull() 7 | 8 | override suspend fun writeString(message: String) { 9 | println(message) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/transport/Transport.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.transport 2 | 3 | interface Transport { 4 | suspend fun connect() {} // no-op by default 5 | 6 | suspend fun close() {} // no-op by default 7 | 8 | suspend fun readString(): String? 9 | 10 | suspend fun writeString(message: String) 11 | } 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/runtime/transport/TransportUtil.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.runtime.transport 2 | 3 | suspend fun runTransportLoop( 4 | transport: Transport, 5 | onMessageLine: suspend (String) -> Unit, 6 | onError: (suspend (Throwable) -> Unit)? = null, 7 | onClose: (suspend () -> Unit)? = null, 8 | ) { 9 | try { 10 | while (true) { 11 | val line = transport.readString() ?: break 12 | onMessageLine(line) 13 | } 14 | // If we broke out of the loop, it means readString() returned null => closed 15 | onClose?.invoke() 16 | } catch (e: Throwable) { 17 | onError?.invoke(e) 18 | } finally { 19 | transport.close() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/capabilities/ClientCapabilities.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.capabilities 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | 6 | @Serializable 7 | data class ClientCapabilities( 8 | val experimental: Map? = null, 9 | val roots: RootsCapability? = null, 10 | val sampling: Map? = null, 11 | ) 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/capabilities/Implementation.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.capabilities 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Implementation( 7 | val name: String, 8 | val version: String, 9 | ) 10 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/capabilities/InitializeRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.capabilities 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 7 | 8 | @Serializable 9 | @SerialName("initialize") 10 | data class InitializeRequest( 11 | override val id: String, 12 | val params: InitializeParams, 13 | ) : JsonRpcRequest() { 14 | @Serializable 15 | data class InitializeParams( 16 | val protocolVersion: String, 17 | val capabilities: ClientCapabilities, 18 | val clientInfo: Implementation, 19 | val _meta: Map? = null, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/capabilities/InitializeResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.capabilities 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.schema.core.Result 6 | 7 | @Serializable 8 | data class InitializeResult( 9 | val protocolVersion: String, 10 | val capabilities: ServerCapabilities, 11 | val serverInfo: Implementation, 12 | val instructions: String? = null, 13 | override val _meta: Map? = null, 14 | ) : Result 15 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/capabilities/InitializedNotification.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.capabilities 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import sh.ondr.mcp4k.schema.core.JsonRpcNotification 6 | import sh.ondr.mcp4k.schema.core.NotificationParams 7 | 8 | /** 9 | * This notification is sent from the client to the server after initialization has finished. 10 | * This indicates that the client is now ready for normal operations. 11 | */ 12 | @Serializable 13 | @SerialName("notifications/initialized") 14 | data class InitializedNotification( 15 | override val params: NotificationParams? = null, 16 | ) : JsonRpcNotification() 17 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/capabilities/PromptsCapability.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.capabilities 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class PromptsCapability( 7 | val listChanged: Boolean? = null, 8 | ) 9 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/capabilities/ResourcesCapability.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.capabilities 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ResourcesCapability( 7 | val subscribe: Boolean? = null, 8 | val listChanged: Boolean? = null, 9 | ) 10 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/capabilities/RootsCapability.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.capabilities 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class RootsCapability( 7 | val listChanged: Boolean? = null, 8 | ) 9 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/capabilities/ServerCapabilities.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.capabilities 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | 6 | @Serializable 7 | data class ServerCapabilities( 8 | val experimental: Map? = null, 9 | val logging: Map? = null, 10 | val prompts: PromptsCapability? = null, 11 | val resources: ResourcesCapability? = null, 12 | val tools: ToolsCapability? = null, 13 | ) 14 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/capabilities/ToolsCapability.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.capabilities 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ToolsCapability( 7 | val listChanged: Boolean? = null, 8 | ) 9 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/completion/CompleteRef.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.completion 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | sealed interface CompleteRef { 8 | @Serializable 9 | @SerialName("ref/prompt") 10 | data class PromptRef( 11 | val name: String, 12 | ) : CompleteRef 13 | 14 | @Serializable 15 | @SerialName("ref/resource") 16 | data class ResourceRef( 17 | val uri: String, 18 | ) : CompleteRef 19 | } 20 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/completion/CompleteRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.completion 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 7 | 8 | @Serializable 9 | @SerialName("completion/complete") 10 | data class CompleteRequest( 11 | override val id: String, 12 | val params: CompleteParams, 13 | ) : JsonRpcRequest() { 14 | @Serializable 15 | data class CompleteParams( 16 | val ref: CompleteRef, 17 | val argument: Argument, 18 | val _meta: Map? = null, 19 | ) { 20 | @Serializable 21 | data class Argument( 22 | val name: String, 23 | val value: String, 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/completion/CompleteResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.completion 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.schema.core.Result 6 | 7 | @Serializable 8 | data class CompleteResult( 9 | val completion: CompletionData, 10 | override val _meta: Map? = null, 11 | ) : Result { 12 | @Serializable 13 | data class CompletionData( 14 | val values: List, 15 | val total: Int? = null, 16 | val hasMore: Boolean? = null, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/content/Content.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class) 2 | 3 | package sh.ondr.mcp4k.schema.content 4 | 5 | import kotlinx.serialization.ExperimentalSerializationApi 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.json.JsonClassDiscriminator 8 | 9 | @Serializable 10 | @JsonClassDiscriminator("type") 11 | sealed interface Content 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/content/EmbeddedResourceContent.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.content 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import sh.ondr.mcp4k.schema.core.Annotations 6 | import sh.ondr.mcp4k.schema.resources.ResourceContents 7 | 8 | @Serializable 9 | @SerialName("resource") 10 | data class EmbeddedResourceContent( 11 | val resource: ResourceContents, 12 | val annotations: Annotations? = null, 13 | ) : PromptContent, ToolContent 14 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/content/ImageContent.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.content 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import sh.ondr.mcp4k.schema.core.Annotations 6 | 7 | @Serializable 8 | @SerialName("image") 9 | data class ImageContent( 10 | val data: String, 11 | val mimeType: String, 12 | val annotations: Annotations? = null, 13 | ) : PromptContent, ToolContent, SamplingContent 14 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/content/PromptContent.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class) 2 | 3 | package sh.ondr.mcp4k.schema.content 4 | 5 | import kotlinx.serialization.ExperimentalSerializationApi 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.json.JsonClassDiscriminator 8 | 9 | @Serializable 10 | @JsonClassDiscriminator("type") 11 | sealed interface PromptContent : Content 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/content/SamplingContent.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class) 2 | 3 | package sh.ondr.mcp4k.schema.content 4 | 5 | import kotlinx.serialization.ExperimentalSerializationApi 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.json.JsonClassDiscriminator 8 | 9 | @Serializable 10 | @JsonClassDiscriminator("type") 11 | sealed interface SamplingContent : Content 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/content/TextContent.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.content 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import sh.ondr.mcp4k.schema.core.Annotations 6 | 7 | @Serializable 8 | @SerialName("text") 9 | data class TextContent( 10 | val text: String, 11 | val annotations: Annotations? = null, 12 | ) : PromptContent, ToolContent, SamplingContent 13 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/content/ToolContent.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.content 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | sealed interface ToolContent : Content 7 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/Annotated.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | interface Annotated { 4 | val annotations: Annotations? 5 | } 6 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/Annotations.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Annotations( 7 | val audience: List? = null, 8 | val priority: Double? = null, 9 | ) 10 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/EmptyParams.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | class EmptyParams 7 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/EmptyResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | 6 | @Serializable 7 | data class EmptyResult( 8 | override val _meta: Map? = null, 9 | ) : Result 10 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/JsonRpcError.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | 6 | @Serializable 7 | data class JsonRpcError( 8 | val code: Int, 9 | val message: String, 10 | val data: JsonElement? = null, 11 | ) 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/JsonRpcErrorCodes.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | object JsonRpcErrorCodes { 4 | // Standard JSON-RPC error codes 5 | const val PARSE_ERROR = -32700 // Invalid JSON was received 6 | const val INVALID_REQUEST = -32600 // JSON is not a valid Request object 7 | const val METHOD_NOT_FOUND = -32601 // Method does not exist or is not available 8 | const val INVALID_PARAMS = -32602 // Invalid method parameter(s) 9 | const val INTERNAL_ERROR = -32603 // Internal JSON-RPC error 10 | 11 | // MCP-specific error codes 12 | const val RESOURCE_NOT_FOUND = -32002 // Returned when a requested resource doesn't exist 13 | } 14 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/JsonRpcMessage.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | interface JsonRpcMessage 4 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/JsonRpcNotification.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class) 2 | 3 | package sh.ondr.mcp4k.schema.core 4 | 5 | import kotlinx.serialization.ExperimentalSerializationApi 6 | import kotlinx.serialization.Polymorphic 7 | import kotlinx.serialization.SerialName 8 | import kotlinx.serialization.Serializable 9 | import kotlinx.serialization.json.JsonElement 10 | import sh.ondr.mcp4k.runtime.core.JSON_RPC_VERSION 11 | 12 | /** 13 | * Base class for all JSON-RPC notifications. 14 | * Notifications do not have an ID and do not expect a response. 15 | * 16 | * All notifications share a `method` field. Polymorphic deserialization 17 | * is based on `method`. 18 | */ 19 | @Serializable 20 | @Polymorphic 21 | abstract class JsonRpcNotification : JsonRpcMessage { 22 | val jsonrpc: String = JSON_RPC_VERSION 23 | abstract val params: NotificationParams? 24 | } 25 | 26 | /** 27 | * Base parameters for a notification. 28 | * Notifications can have `_meta` and other fields depending on the subtype. 29 | */ 30 | interface NotificationParams { 31 | val _meta: Map? 32 | } 33 | 34 | /** 35 | * This notification is sent by either side to indicate that it is cancelling a previously-issued request. 36 | * 37 | * The request SHOULD still be in-flight, but due to communication latency, 38 | * it is possible that this notification may arrive after the request has completed. 39 | * This indicates that the result will be unused, so any associated processing SHOULD stop. 40 | * 41 | * A client MUST NOT attempt to cancel its `initialize` request. 42 | */ 43 | @Serializable 44 | @SerialName("notifications/cancelled") 45 | data class CancelledNotification( 46 | override val params: CancelledParams, 47 | ) : JsonRpcNotification() { 48 | /** 49 | * @property requestId The ID of the request to cancel. 50 | * @property reason An optional string describing the reason for cancellation. 51 | */ 52 | @Serializable 53 | data class CancelledParams( 54 | val requestId: String, 55 | val reason: String? = null, 56 | override val _meta: Map? = null, 57 | ) : NotificationParams 58 | } 59 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/JsonRpcRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | import kotlinx.serialization.Polymorphic 4 | import kotlinx.serialization.Serializable 5 | import sh.ondr.mcp4k.runtime.core.JSON_RPC_VERSION 6 | 7 | @Serializable 8 | @Polymorphic 9 | abstract class JsonRpcRequest : JsonRpcMessage { 10 | val jsonrpc: String = JSON_RPC_VERSION 11 | abstract val id: String 12 | } 13 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/JsonRpcResponse.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.runtime.core.JSON_RPC_VERSION 6 | 7 | @Serializable 8 | data class JsonRpcResponse( 9 | val jsonrpc: String = JSON_RPC_VERSION, 10 | val id: String, 11 | val result: JsonElement? = null, 12 | val error: JsonRpcError? = null, 13 | ) : JsonRpcMessage { 14 | init { 15 | require((result == null) xor (error == null)) { 16 | "A JSON-RPC response must have either 'result' or 'error', but not both." 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/PaginatedResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | /** 4 | * For paginated results, the schema defines `PaginatedResult` extending `Result` 5 | * with an optional `nextCursor` field. 6 | */ 7 | interface PaginatedResult : Result { 8 | val nextCursor: String? 9 | } 10 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/PingRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | 7 | @Serializable 8 | @SerialName("ping") 9 | data class PingRequest( 10 | override val id: String, 11 | val params: PingParams? = null, 12 | ) : JsonRpcRequest() { 13 | @Serializable 14 | data class PingParams( 15 | val _meta: Map? = null, 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/ProgressNotification.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import kotlinx.serialization.json.JsonPrimitive 7 | 8 | @Serializable 9 | @SerialName("notifications/progress") 10 | data class ProgressNotification( 11 | override val params: ProgressParams, 12 | ) : JsonRpcNotification() { 13 | /** 14 | * An out-of-band notification used to inform the receiver of a progress update 15 | * for a long-running request. 16 | * 17 | * @property progressToken The progress token from the initial request. Token is string|number. 18 | * @property progress The current progress made (increases each time). Using Double to represent numeric progress. 19 | * @property total The total amount of progress required, if known. 20 | */ 21 | @Serializable 22 | data class ProgressParams( 23 | val progressToken: JsonPrimitive, 24 | val progress: Double, 25 | val total: Double? = null, 26 | override val _meta: Map? = null, 27 | ) : NotificationParams 28 | } 29 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/Result.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | import kotlinx.serialization.json.JsonElement 4 | 5 | /** 6 | * Base interface for all result types from MCP responses. 7 | * The schema states `Result` can have an optional `_meta` field. 8 | * We'll model it as an optional Map. 9 | */ 10 | interface Result { 11 | val _meta: Map? 12 | } 13 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/core/Role.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.core 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | enum class Role { 8 | @SerialName("assistant") 9 | ASSISTANT, 10 | 11 | @SerialName("user") 12 | USER, 13 | } 14 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/logging/LoggingLevel.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.logging 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | enum class LoggingLevel { 8 | @SerialName("debug") 9 | DEBUG, 10 | 11 | @SerialName("info") 12 | INFO, 13 | 14 | @SerialName("notice") 15 | NOTICE, 16 | 17 | @SerialName("warning") 18 | WARNING, 19 | 20 | @SerialName("error") 21 | ERROR, 22 | 23 | @SerialName("critical") 24 | CRITICAL, 25 | 26 | @SerialName("alert") 27 | ALERT, 28 | 29 | @SerialName("emergency") 30 | EMERGENCY, 31 | } 32 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/logging/LoggingMessageNotification.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.logging 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.schema.core.JsonRpcNotification 7 | import sh.ondr.mcp4k.schema.core.NotificationParams 8 | 9 | @Serializable 10 | @SerialName("notifications/message") 11 | data class LoggingMessageNotification( 12 | override val params: LoggingMessageParams, 13 | ) : JsonRpcNotification() { 14 | /** 15 | * A notification of a log message passed from server to client. 16 | * If no `logging/setLevel` request has been sent, the server MAY decide which messages to send. 17 | * 18 | * @property level The severity of the log message. 19 | * @property logger An optional name of the logger issuing this message. 20 | * @property data The data to log, any JSON-serializable type. 21 | */ 22 | @Serializable 23 | data class LoggingMessageParams( 24 | val level: LoggingLevel, 25 | val logger: String? = null, 26 | val data: JsonElement, 27 | override val _meta: Map? = null, 28 | ) : NotificationParams 29 | } 30 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/logging/SetLoggingLevelRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.logging 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 7 | 8 | @Serializable 9 | @SerialName("logging/setLevel") 10 | data class SetLoggingLevelRequest( 11 | override val id: String, 12 | val params: SetLoggingLevelParams, 13 | ) : JsonRpcRequest() { 14 | @Serializable 15 | data class SetLoggingLevelParams( 16 | val level: LoggingLevel, 17 | val _meta: Map? = null, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/prompts/GetPromptRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.prompts 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 7 | 8 | @Serializable 9 | @SerialName("prompts/get") 10 | data class GetPromptRequest( 11 | override val id: String, 12 | val params: GetPromptParams, 13 | ) : JsonRpcRequest() { 14 | @Serializable 15 | data class GetPromptParams( 16 | val name: String, 17 | val arguments: Map? = null, 18 | val _meta: Map? = null, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/prompts/GetPromptResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.prompts 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.schema.core.Result 6 | 7 | @Serializable 8 | data class GetPromptResult( 9 | val description: String? = null, 10 | val messages: List, 11 | override val _meta: Map? = null, 12 | ) : Result 13 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/prompts/ListPromptsRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.prompts 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.runtime.core.pagination.PaginatedEndpoint 7 | import sh.ondr.mcp4k.runtime.core.pagination.PaginatedRequestFactory 8 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 9 | 10 | @Serializable 11 | @SerialName("prompts/list") 12 | data class ListPromptsRequest( 13 | override val id: String, 14 | val params: ListPromptsParams? = null, 15 | ) : JsonRpcRequest() { 16 | val cursor: String? get() = params?.cursor 17 | 18 | companion object : PaginatedEndpoint> { 19 | override val requestFactory = PaginatedRequestFactory { id, cursor -> 20 | ListPromptsRequest( 21 | id = id, 22 | params = cursor?.let { ListPromptsParams(it) }, 23 | ) 24 | } 25 | 26 | override fun transform(result: ListPromptsResult): List { 27 | return result.prompts 28 | } 29 | } 30 | 31 | @Serializable 32 | data class ListPromptsParams( 33 | val cursor: String? = null, 34 | val _meta: Map? = null, 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/prompts/ListPromptsResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.prompts 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.schema.core.PaginatedResult 6 | 7 | @Serializable 8 | data class ListPromptsResult( 9 | val prompts: List, 10 | override val _meta: Map? = null, 11 | override val nextCursor: String? = null, 12 | ) : PaginatedResult 13 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/prompts/Prompt.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.prompts 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Prompt( 7 | val name: String, 8 | val description: String? = null, 9 | val arguments: List? = null, 10 | ) 11 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/prompts/PromptArgument.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.prompts 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Prompt related structures 7 | */ 8 | @Serializable 9 | data class PromptArgument( 10 | val name: String, 11 | val description: String? = null, 12 | val required: Boolean = false, 13 | ) 14 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/prompts/PromptListChangedNotification.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.prompts 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import sh.ondr.mcp4k.schema.core.JsonRpcNotification 6 | import sh.ondr.mcp4k.schema.core.NotificationParams 7 | 8 | /** 9 | * A notification from the server to the client that the list of prompts has changed. 10 | * The client may issue a `prompts/list` request to get the updated list. 11 | */ 12 | @Serializable 13 | @SerialName("notifications/prompts/list_changed") 14 | data class PromptListChangedNotification( 15 | override val params: NotificationParams? = null, 16 | ) : JsonRpcNotification() 17 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/prompts/PromptMessage.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.prompts 2 | 3 | import kotlinx.serialization.Serializable 4 | import sh.ondr.mcp4k.schema.content.PromptContent 5 | import sh.ondr.mcp4k.schema.core.Role 6 | 7 | @Serializable 8 | data class PromptMessage( 9 | val role: Role, 10 | val content: PromptContent, 11 | ) 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/ListResourceTemplatesRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.runtime.core.pagination.PaginatedEndpoint 7 | import sh.ondr.mcp4k.runtime.core.pagination.PaginatedRequestFactory 8 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 9 | 10 | @Serializable 11 | @SerialName("resources/templates/list") 12 | data class ListResourceTemplatesRequest( 13 | override val id: String, 14 | val params: ListResourceTemplatesParams? = null, 15 | ) : JsonRpcRequest() { 16 | val cursor: String? get() = params?.cursor 17 | 18 | companion object : PaginatedEndpoint> { 19 | override val requestFactory = PaginatedRequestFactory { id, cursor -> 20 | ListResourceTemplatesRequest( 21 | id = id, 22 | params = cursor?.let { ListResourceTemplatesParams(it) }, 23 | ) 24 | } 25 | 26 | override fun transform(result: ListResourceTemplatesResult): List { 27 | return result.resourceTemplates 28 | } 29 | } 30 | 31 | @Serializable 32 | data class ListResourceTemplatesParams( 33 | val cursor: String? = null, 34 | val _meta: Map? = null, 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/ListResourceTemplatesResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.schema.core.PaginatedResult 6 | 7 | @Serializable 8 | data class ListResourceTemplatesResult( 9 | val resourceTemplates: List, 10 | override val _meta: Map? = null, 11 | override val nextCursor: String? = null, 12 | ) : PaginatedResult 13 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/ListResourcesRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.runtime.core.pagination.PaginatedEndpoint 7 | import sh.ondr.mcp4k.runtime.core.pagination.PaginatedRequestFactory 8 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 9 | 10 | @Serializable 11 | @SerialName("resources/list") 12 | data class ListResourcesRequest( 13 | override val id: String, 14 | val params: ListResourcesParams? = null, 15 | ) : JsonRpcRequest() { 16 | val cursor: String? get() = params?.cursor 17 | 18 | companion object : PaginatedEndpoint> { 19 | override val requestFactory = PaginatedRequestFactory { id, cursor -> 20 | ListResourcesRequest( 21 | id = id, 22 | params = cursor?.let { ListResourcesParams(it) }, 23 | ) 24 | } 25 | 26 | override fun transform(result: ListResourcesResult): List { 27 | return result.resources 28 | } 29 | } 30 | 31 | @Serializable 32 | data class ListResourcesParams( 33 | val cursor: String? = null, 34 | val _meta: Map? = null, 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/ListResourcesResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.schema.core.PaginatedResult 6 | 7 | @Serializable 8 | data class ListResourcesResult( 9 | val resources: List, 10 | override val _meta: Map? = null, 11 | override val nextCursor: String? = null, 12 | ) : PaginatedResult 13 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/ReadResourceRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 7 | 8 | @Serializable 9 | @SerialName("resources/read") 10 | data class ReadResourceRequest( 11 | override val id: String, 12 | val params: ReadResourceParams, 13 | ) : JsonRpcRequest() { 14 | @Serializable 15 | data class ReadResourceParams( 16 | val uri: String, 17 | val _meta: Map? = null, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/ReadResourceResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.schema.core.Result 6 | 7 | @Serializable 8 | data class ReadResourceResult( 9 | val contents: List, 10 | override val _meta: Map? = null, 11 | ) : Result 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/Resource.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Resource( 7 | val uri: String, 8 | val name: String, 9 | val description: String? = null, 10 | val mimeType: String? = null, 11 | ) 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/ResourceContents.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.Serializable 4 | import sh.ondr.mcp4k.runtime.serialization.ResourceContentsSerializer 5 | 6 | @Serializable(with = ResourceContentsSerializer::class) 7 | sealed class ResourceContents { 8 | abstract val uri: String 9 | abstract val mimeType: String? 10 | 11 | @Serializable 12 | data class Text( 13 | override val uri: String, 14 | override val mimeType: String? = null, 15 | val text: String, 16 | ) : ResourceContents() 17 | 18 | @Serializable 19 | data class Blob( 20 | override val uri: String, 21 | override val mimeType: String? = null, 22 | val blob: String, 23 | ) : ResourceContents() 24 | } 25 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/ResourceListChangedNotification.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import sh.ondr.mcp4k.schema.core.JsonRpcNotification 6 | import sh.ondr.mcp4k.schema.core.NotificationParams 7 | 8 | /** 9 | * A notification from the server to the client that the list of resources has changed. 10 | * The client should issue a `resources/list` request to get the updated list. 11 | */ 12 | @Serializable 13 | @SerialName("notifications/resources/list_changed") 14 | data class ResourceListChangedNotification( 15 | override val params: NotificationParams? = null, 16 | ) : JsonRpcNotification() 17 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/ResourceTemplate.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.Serializable 4 | import sh.ondr.mcp4k.schema.core.Annotations 5 | 6 | @Serializable 7 | data class ResourceTemplate( 8 | val uriTemplate: String, 9 | val name: String, 10 | val description: String? = null, 11 | val mimeType: String? = null, 12 | val annotations: Annotations? = null, 13 | ) 14 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/ResourceUpdatedNotification.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.schema.core.JsonRpcNotification 7 | import sh.ondr.mcp4k.schema.core.NotificationParams 8 | 9 | @Serializable 10 | @SerialName("notifications/resources/updated") 11 | data class ResourceUpdatedNotification( 12 | override val params: ResourceUpdatedParams, 13 | ) : JsonRpcNotification() { 14 | /** 15 | * A notification from the server to the client that a subscribed resource has been updated. 16 | * The client may re-fetch the resource with `resources/read`. 17 | * 18 | * @property uri The URI of the resource that has been updated. 19 | */ 20 | @Serializable 21 | data class ResourceUpdatedParams( 22 | val uri: String, 23 | override val _meta: Map? = null, 24 | ) : NotificationParams 25 | } 26 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/SubscribeRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 7 | 8 | @Serializable 9 | @SerialName("resources/subscribe") 10 | data class SubscribeRequest( 11 | override val id: String, 12 | val params: SubscribeParams, 13 | ) : JsonRpcRequest() { 14 | @Serializable 15 | data class SubscribeParams( 16 | val uri: String, 17 | val _meta: Map? = null, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/resources/UnsubscribeRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.resources 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 7 | 8 | @Serializable 9 | @SerialName("resources/unsubscribe") 10 | data class UnsubscribeRequest( 11 | override val id: String, 12 | val params: UnsubscribeParams, 13 | ) : JsonRpcRequest() { 14 | @Serializable 15 | data class UnsubscribeParams( 16 | val uri: String, 17 | val _meta: Map? = null, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/roots/ListRootsRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.roots 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import sh.ondr.mcp4k.runtime.core.ClientApprovable 6 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 7 | 8 | @Serializable 9 | @SerialName("roots/list") 10 | data class ListRootsRequest( 11 | override val id: String, 12 | ) : JsonRpcRequest() 13 | 14 | // Empty dummy class, just used to differentiate in the permissions callback 15 | class ListRootsParams() : ClientApprovable 16 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/roots/ListRootsResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.roots 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.schema.core.Result 6 | 7 | @Serializable 8 | data class ListRootsResult( 9 | val roots: List, 10 | override val _meta: Map? = null, 11 | ) : Result 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/roots/Root.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.roots 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Root( 7 | val uri: String, 8 | val name: String? = null, 9 | ) 10 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/roots/RootsListChangedNotification.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.roots 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import sh.ondr.mcp4k.schema.core.JsonRpcNotification 6 | import sh.ondr.mcp4k.schema.core.NotificationParams 7 | 8 | /** 9 | * A notification from the client to the server, informing that the list of roots has changed. 10 | * The server may now request `roots/list` again to get the updated set of roots. 11 | */ 12 | @Serializable 13 | @SerialName("notifications/roots/list_changed") 14 | data class RootsListChangedNotification( 15 | override val params: NotificationParams? = null, 16 | ) : JsonRpcNotification() 17 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/sampling/CreateMessageRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.sampling 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.runtime.core.ClientApprovable 7 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 8 | 9 | @Serializable 10 | @SerialName("sampling/createMessage") 11 | data class CreateMessageRequest( 12 | override val id: String, 13 | val params: CreateMessageParams, 14 | ) : JsonRpcRequest() { 15 | @Serializable 16 | data class CreateMessageParams( 17 | val messages: List, 18 | val modelPreferences: ModelPreferences? = null, 19 | val systemPrompt: String? = null, 20 | val includeContext: IncludeContext? = null, 21 | val temperature: Double? = null, 22 | val maxTokens: Int, 23 | val stopSequences: List? = null, 24 | val metadata: Map? = null, 25 | val _meta: Map? = null, 26 | ) : ClientApprovable 27 | } 28 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/sampling/CreateMessageResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.sampling 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.schema.content.SamplingContent 6 | import sh.ondr.mcp4k.schema.core.Result 7 | import sh.ondr.mcp4k.schema.core.Role 8 | 9 | @Serializable 10 | class CreateMessageResult( 11 | val content: SamplingContent, 12 | val model: String, 13 | val role: Role, 14 | val stopReason: String? = null, 15 | override val _meta: Map? = null, 16 | ) : Result 17 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/sampling/IncludeContext.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.sampling 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * A request to include context from one or more MCP servers, as requested by the caller (the server). 8 | * The client MAY ignore or override this request. 9 | */ 10 | @Serializable 11 | enum class IncludeContext { 12 | @SerialName("allServers") 13 | ALL_SERVERS, 14 | 15 | @SerialName("none") 16 | NONE, 17 | 18 | @SerialName("thisServer") 19 | THIS_SERVER, 20 | } 21 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/sampling/ModelHint.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.sampling 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ModelHint( 7 | val name: String? = null, 8 | ) 9 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/sampling/ModelPreferences.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.sampling 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ModelPreferences( 7 | val hints: List? = null, 8 | val costPriority: Double? = null, 9 | val speedPriority: Double? = null, 10 | val intelligencePriority: Double? = null, 11 | ) 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/sampling/SamplingMessage.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.sampling 2 | 3 | import kotlinx.serialization.Serializable 4 | import sh.ondr.mcp4k.schema.content.SamplingContent 5 | import sh.ondr.mcp4k.schema.core.Role 6 | 7 | @Serializable 8 | data class SamplingMessage( 9 | val role: Role, 10 | val content: SamplingContent, 11 | ) 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/tools/CallToolRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.tools 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 7 | 8 | @Serializable 9 | @SerialName("tools/call") 10 | data class CallToolRequest( 11 | override val id: String, 12 | val params: CallToolParams, 13 | ) : JsonRpcRequest() { 14 | @Serializable 15 | data class CallToolParams( 16 | val name: String, 17 | val arguments: Map? = null, 18 | val _meta: Map? = null, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/tools/CallToolResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.tools 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.schema.content.ToolContent 6 | import sh.ondr.mcp4k.schema.core.Result 7 | 8 | @Serializable 9 | data class CallToolResult( 10 | val content: List = emptyList(), 11 | val isError: Boolean? = null, 12 | override val _meta: Map? = null, 13 | ) : Result 14 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/tools/ListToolsRequest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.tools 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | import sh.ondr.mcp4k.runtime.core.pagination.PaginatedEndpoint 7 | import sh.ondr.mcp4k.runtime.core.pagination.PaginatedRequestFactory 8 | import sh.ondr.mcp4k.schema.core.JsonRpcRequest 9 | 10 | @Serializable 11 | @SerialName("tools/list") 12 | data class ListToolsRequest( 13 | override val id: String, 14 | val params: ListToolsParams? = null, 15 | ) : JsonRpcRequest() { 16 | val cursor: String? get() = params?.cursor 17 | 18 | companion object : PaginatedEndpoint> { 19 | override val requestFactory = PaginatedRequestFactory { id, cursor -> 20 | ListToolsRequest( 21 | id = id, 22 | params = cursor?.let { ListToolsParams(it) }, 23 | ) 24 | } 25 | 26 | override fun transform(result: ListToolsResult): List { 27 | return result.tools 28 | } 29 | } 30 | 31 | @Serializable 32 | data class ListToolsParams( 33 | val cursor: String? = null, 34 | val _meta: Map? = null, 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/tools/ListToolsResult.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.tools 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.JsonElement 5 | import sh.ondr.mcp4k.schema.core.PaginatedResult 6 | 7 | @Serializable 8 | data class ListToolsResult( 9 | val tools: List, 10 | override val _meta: Map? = null, 11 | override val nextCursor: String? = null, 12 | ) : PaginatedResult 13 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/tools/Tool.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.tools 2 | 3 | import kotlinx.serialization.Serializable 4 | import sh.ondr.koja.Schema 5 | 6 | @Serializable 7 | data class Tool( 8 | val name: String, 9 | val description: String? = null, 10 | val inputSchema: Schema, 11 | ) 12 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/schema/tools/ToolListChangedNotification.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.schema.tools 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import sh.ondr.mcp4k.schema.core.JsonRpcNotification 6 | import sh.ondr.mcp4k.schema.core.NotificationParams 7 | 8 | /** 9 | * A notification from the server to the client that the list of tools has changed. 10 | * The client may issue a `tools/list` request to get the updated list. 11 | */ 12 | @Serializable 13 | @SerialName("notifications/tools/list_changed") 14 | data class ToolListChangedNotification( 15 | override val params: NotificationParams? = null, 16 | ) : JsonRpcNotification() 17 | -------------------------------------------------------------------------------- /mcp4k-runtime/src/commonMain/kotlin/sh/ondr/mcp4k/test/TestUtil.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.test 2 | 3 | /** 4 | * A small DSL entry point for building expected log sequences in tests. 5 | * 6 | * Usage: 7 | * ``` 8 | * val expected = buildLog { 9 | * addClientOutgoing("""{"method":"ping","jsonrpc":"2.0","id":"2"}""") 10 | * addServerIncoming("""{"method":"ping","jsonrpc":"2.0","id":"2"}""") 11 | * addServerOutgoing("""{"jsonrpc":"2.0","id":"2","result":{}}""") 12 | * addClientIncoming("""{"jsonrpc":"2.0","id":"2","result":{}}""") 13 | * } 14 | * assertLinesMatch(expected, log, "ping test") 15 | * ``` 16 | */ 17 | fun buildLog(buildBlock: LogAssertionBuilder.() -> Unit): List { 18 | val builder = LogAssertionBuilder() 19 | builder.buildBlock() 20 | return builder.build() 21 | } 22 | 23 | fun clientIncoming(msg: String) = "CLIENT INCOMING: $msg" 24 | 25 | fun clientOutgoing(msg: String) = "CLIENT OUTGOING: $msg" 26 | 27 | fun serverIncoming(msg: String) = "SERVER INCOMING: $msg" 28 | 29 | fun serverOutgoing(msg: String) = "SERVER OUTGOING: $msg" 30 | 31 | class LogAssertionBuilder { 32 | private val expectedLines = mutableListOf() 33 | 34 | fun addClientOutgoing(msg: String) { 35 | expectedLines += clientOutgoing(msg) 36 | } 37 | 38 | fun addClientIncoming(msg: String) { 39 | expectedLines += clientIncoming(msg) 40 | } 41 | 42 | fun addServerOutgoing(msg: String) { 43 | expectedLines += serverOutgoing(msg) 44 | } 45 | 46 | fun addServerIncoming(msg: String) { 47 | expectedLines += serverIncoming(msg) 48 | } 49 | 50 | fun build() = expectedLines.toList() 51 | } 52 | 53 | fun assertLinesMatch( 54 | expected: List, 55 | actual: List, 56 | context: String = "", 57 | ) { 58 | val prefix = if (context.isNotEmpty()) " for $context" else "" 59 | 60 | // Compare line by line up to the smaller size 61 | val minSize = minOf(expected.size, actual.size) 62 | for (i in 0 until minSize) { 63 | if (expected[i] != actual[i]) { 64 | // Found mismatch 65 | val sb = StringBuilder() 66 | sb.append("line[$i] does not match$prefix.\n") 67 | sb.append("Expected: ${expected[i]}\n") 68 | sb.append("Actual: ${actual[i]}\n\n") 69 | sb.append("Full expected lines:\n") 70 | expected.forEach { sb.append(" E: $it\n") } 71 | sb.append("\nFull actual lines:\n") 72 | actual.forEach { sb.append(" A: $it\n") } 73 | 74 | throw AssertionError(sb.toString()) 75 | } 76 | } 77 | 78 | // If we reach here, lines in the overlapping portion all match. 79 | // Check if sizes differ 80 | if (expected.size != actual.size) { 81 | val sb = StringBuilder() 82 | sb.append("Number of log lines does not match$prefix\n") 83 | sb.append("Expected count: ${expected.size}\n") 84 | sb.append("Actual count: ${actual.size}\n\n") 85 | sb.append("Full expected lines:\n") 86 | expected.forEach { sb.append(" E: $it\n") } 87 | sb.append("\nFull actual lines:\n") 88 | actual.forEach { sb.append(" A: $it\n") } 89 | 90 | throw AssertionError(sb.toString()) 91 | } 92 | 93 | // If we get here, all lines match exactly. 94 | } 95 | -------------------------------------------------------------------------------- /mcp4k-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.publish.maven.tasks.PublishToMavenLocal 2 | import org.gradle.api.publish.maven.tasks.PublishToMavenRepository 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | import org.jetbrains.kotlin.konan.target.HostManager 5 | 6 | plugins { 7 | alias(libs.plugins.kotlin.multiplatform) 8 | alias(libs.plugins.kotlin.serialization) 9 | alias(libs.plugins.ondrsh.mcp4k) // Will not use GAV coordinates, will be substituted 10 | alias(libs.plugins.maven.publish) 11 | } 12 | 13 | kotlin { 14 | jvmToolchain(11) 15 | jvm { 16 | compilerOptions { 17 | jvmTarget.set(JvmTarget.JVM_11) 18 | } 19 | } 20 | js(IR) { 21 | nodejs() 22 | binaries.library() 23 | } 24 | 25 | when { 26 | // macOS can build & publish all 27 | HostManager.hostIsMac -> { 28 | iosArm64() 29 | iosSimulatorArm64() 30 | iosX64() 31 | macosArm64() 32 | macosX64() 33 | linuxX64() 34 | mingwX64() 35 | } 36 | 37 | HostManager.hostIsLinux -> { 38 | linuxX64() 39 | } 40 | 41 | HostManager.hostIsMingw -> { 42 | mingwX64() 43 | } 44 | } 45 | 46 | sourceSets { 47 | commonMain { 48 | dependencies { 49 | implementation(libs.kotlinx.serialization.json) 50 | implementation(libs.kotlinx.coroutines.core) 51 | } 52 | } 53 | commonTest { 54 | dependencies { 55 | implementation(kotlin("test")) 56 | implementation(libs.kotlinx.coroutines.test) 57 | implementation(libs.square.okio) 58 | implementation(libs.square.okio.fakefilesystem) 59 | implementation(project(":mcp4k-file-provider")) 60 | } 61 | } 62 | } 63 | } 64 | 65 | tasks.withType().configureEach { 66 | useJUnitPlatform() 67 | 68 | testLogging { 69 | events("passed", "skipped", "failed") 70 | showExceptions = true 71 | showCauses = true 72 | showStackTraces = true 73 | } 74 | } 75 | 76 | // Disable publishing for this test module 77 | tasks.withType().configureEach { 78 | enabled = false 79 | } 80 | tasks.withType().configureEach { 81 | enabled = false 82 | } 83 | 84 | // Task to verify the plugin was applied correctly 85 | tasks.register("verifyPluginApplication") { 86 | doLast { 87 | println("Kotlin version: ${libs.versions.kotlin.get()}") 88 | println("KSP tasks: ${tasks.names.filter { it.contains("ksp", ignoreCase = true) }}") 89 | println("MCP4K plugin applied: ${project.plugins.hasPlugin("sh.ondr.mcp4k")}") 90 | } 91 | } 92 | 93 | // Task to print the effective Kotlin version 94 | tasks.register("printKotlinVersion") { 95 | doLast { 96 | println("Using Kotlin version: ${libs.versions.kotlin.get()}") 97 | } 98 | } 99 | 100 | // Task to check actual Kotlin compiler version being used 101 | tasks.register("checkActualKotlinVersion") { 102 | doLast { 103 | val kotlinPlugin = project.plugins.findPlugin("org.jetbrains.kotlin.multiplatform") 104 | println("Kotlin Multiplatform Plugin Applied: ${kotlinPlugin != null}") 105 | if (kotlinPlugin != null) { 106 | println("Plugin Class: ${kotlinPlugin::class.java.name}") 107 | // Try to access the version through the plugin 108 | try { 109 | val versionField = kotlinPlugin::class.java.declaredFields.find { it.name.contains("version", true) } 110 | if (versionField != null) { 111 | versionField.isAccessible = true 112 | println("Version field: ${versionField.get(kotlinPlugin)}") 113 | } 114 | } catch (e: Exception) { 115 | println("Could not access version field: ${e.message}") 116 | } 117 | } 118 | 119 | // Check compiler classpath 120 | configurations.findByName("kotlinCompilerClasspath")?.let { config -> 121 | println("\nKotlin Compiler Classpath:") 122 | config.resolvedConfiguration.resolvedArtifacts.forEach { artifact -> 123 | if (artifact.moduleVersion.id.group == "org.jetbrains.kotlin") { 124 | println(" ${artifact.moduleVersion.id}: ${artifact.file.name}") 125 | } 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /mcp4k-test/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=mcp4k-test 2 | POM_NAME=MCP4K Integration Test Module -------------------------------------------------------------------------------- /mcp4k-test/src/commonMain/kotlin/sh/ondr/mcp4k/test/prompts/TestPrompts.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.test.prompts 2 | 3 | import sh.ondr.mcp4k.runtime.Server 4 | import sh.ondr.mcp4k.runtime.annotation.McpPrompt 5 | import sh.ondr.mcp4k.runtime.annotation.McpTool 6 | import sh.ondr.mcp4k.runtime.core.toTextContent 7 | import sh.ondr.mcp4k.runtime.prompts.buildPrompt 8 | import sh.ondr.mcp4k.schema.content.TextContent 9 | import sh.ondr.mcp4k.schema.content.ToolContent 10 | import sh.ondr.mcp4k.schema.core.Role 11 | import sh.ondr.mcp4k.schema.prompts.GetPromptResult 12 | import sh.ondr.mcp4k.schema.prompts.PromptMessage 13 | 14 | /** 15 | * Code review prompt for Server extension function testing 16 | * @param code The code to review 17 | */ 18 | @McpPrompt 19 | fun Server.codeReviewPrompt(code: String) = 20 | buildPrompt { 21 | user("Please review the following code:") 22 | user("```\n$code\n```") 23 | } 24 | 25 | /** 26 | * Simple prompt for testing 27 | * @param code The code parameter 28 | */ 29 | @McpPrompt 30 | fun secondPrompt(code: String) = 31 | buildPrompt { 32 | user("Second prompt with code: $code") 33 | } 34 | 35 | /** 36 | * Third test prompt 37 | * @param code The code parameter 38 | */ 39 | @McpPrompt 40 | fun thirdPrompt(code: String) = 41 | buildPrompt { 42 | user("Third prompt with code: $code") 43 | } 44 | 45 | /** 46 | * Fourth test prompt 47 | * @param code The code parameter 48 | */ 49 | @McpPrompt 50 | fun fourthPrompt(code: String) = 51 | buildPrompt { 52 | user("Fourth prompt with code: $code") 53 | } 54 | 55 | /** 56 | * Fifth test prompt 57 | * @param code The code parameter 58 | */ 59 | @McpPrompt 60 | fun fifthPrompt(code: String) = 61 | buildPrompt { 62 | user("Fifth prompt with code: $code") 63 | } 64 | 65 | /** 66 | * Prompt that returns GetPromptResult directly (for error testing) 67 | * @param code The code to review 68 | */ 69 | @McpPrompt 70 | fun strictReviewPrompt(code: String): GetPromptResult = 71 | GetPromptResult( 72 | messages = listOf( 73 | PromptMessage(role = Role.USER, content = TextContent("Please review: $code")), 74 | ), 75 | ) 76 | 77 | /** 78 | * Simple code review prompt (from InitializationTest) 79 | * @param code The code to review 80 | */ 81 | @McpPrompt 82 | fun simpleCodeReviewPrompt(code: String): GetPromptResult = 83 | buildPrompt { 84 | user("You are a code review assistant.") 85 | user("Please review the following code and provide feedback:") 86 | user("```\n$code\n```") 87 | assistant("I'll analyze this code and provide constructive feedback.") 88 | } 89 | 90 | /** 91 | * Simple greeting tool (from InitializationTest) 92 | * Note: This is here temporarily as it was defined inline in a test 93 | * @param name The name to greet 94 | */ 95 | @McpTool 96 | fun simpleGreet(name: String): ToolContent = "Hello, $name!".toTextContent() 97 | -------------------------------------------------------------------------------- /mcp4k-test/src/commonMain/kotlin/sh/ondr/mcp4k/test/tools/AsyncTools.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.test.tools 2 | 3 | import kotlinx.coroutines.delay 4 | import sh.ondr.mcp4k.runtime.Server 5 | import sh.ondr.mcp4k.runtime.annotation.McpTool 6 | import sh.ondr.mcp4k.runtime.core.toTextContent 7 | import sh.ondr.mcp4k.schema.content.ToolContent 8 | 9 | /** 10 | * Greets a user with an optional age 11 | * @param name The name of the user 12 | * @param age The age of the user (defaults to 25) 13 | */ 14 | @McpTool 15 | suspend fun Server.greet( 16 | name: String, 17 | age: Int = 25, 18 | ): ToolContent { 19 | // Simulate some async work 20 | delay(10) 21 | return "Hello $name! You are $age years old.".toTextContent() 22 | } 23 | 24 | /** 25 | * Tool with no parameters 26 | */ 27 | @McpTool 28 | fun noParamTool(): ToolContent = "No params needed!".toTextContent() 29 | 30 | /** 31 | * Simulates a slow operation for testing cancellation 32 | * @param iterations Number of iterations to perform 33 | */ 34 | @McpTool 35 | suspend fun slowToolOperation(iterations: Int = 10): ToolContent { 36 | for (i in 1..iterations) { 37 | // Allow cancellation between iterations 38 | delay(100) 39 | } 40 | return "Operation completed after $iterations iterations".toTextContent() 41 | } 42 | -------------------------------------------------------------------------------- /mcp4k-test/src/commonMain/kotlin/sh/ondr/mcp4k/test/tools/ContextTools.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.test.tools 2 | 3 | import sh.ondr.mcp4k.runtime.Server 4 | import sh.ondr.mcp4k.runtime.ServerContext 5 | import sh.ondr.mcp4k.runtime.annotation.McpTool 6 | import sh.ondr.mcp4k.runtime.core.toTextContent 7 | import sh.ondr.mcp4k.schema.content.ToolContent 8 | 9 | /** 10 | * Test context interface for storing values 11 | */ 12 | interface RemoteService : ServerContext { 13 | var value: String 14 | } 15 | 16 | /** 17 | * Implementation of the test context 18 | */ 19 | class RemoteServiceImpl : RemoteService { 20 | override var value: String = "Initial value" 21 | } 22 | 23 | /** 24 | * Stores a value in the server context 25 | * @param newValue The new value to store 26 | */ 27 | @McpTool 28 | fun Server.storeValueInContext(newValue: String): ToolContent { 29 | val context = getContextAs() 30 | val oldValue = context.value 31 | context.value = newValue 32 | return "Value updated from '$oldValue' to '$newValue'".toTextContent() 33 | } 34 | -------------------------------------------------------------------------------- /mcp4k-test/src/commonMain/kotlin/sh/ondr/mcp4k/test/tools/TestTools.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.test.tools 2 | 3 | import kotlinx.serialization.Serializable 4 | import sh.ondr.koja.JsonSchema 5 | import sh.ondr.mcp4k.runtime.annotation.McpTool 6 | import sh.ondr.mcp4k.runtime.core.toTextContent 7 | import sh.ondr.mcp4k.schema.content.TextContent 8 | 9 | /** 10 | * Email data class for testing complex parameters 11 | * @property title The title of the email 12 | * @property body The body of the email 13 | */ 14 | @Serializable 15 | @JsonSchema 16 | data class Email( 17 | val title: String, 18 | val body: String?, 19 | ) 20 | 21 | /** 22 | * Sends an email to multiple recipients 23 | * @param recipients The list of recipients 24 | */ 25 | @McpTool 26 | fun sendEmail( 27 | recipients: List, 28 | email: Email, 29 | ): TextContent = "Email '${email.title}' sent to ${recipients.joinToString(", ")}".toTextContent() 30 | 31 | /** 32 | * Reverses a string 33 | * @param s The string to reverse 34 | */ 35 | @McpTool 36 | fun reverseString(s: String): TextContent = s.reversed().toTextContent() 37 | 38 | /** 39 | * Tool that was never registered - for testing error handling 40 | * @param value Some value 41 | */ 42 | @McpTool 43 | fun neverRegisteredTool(value: String): TextContent = "This should never be called: $value".toTextContent() 44 | -------------------------------------------------------------------------------- /mcp4k-test/src/commonTest/kotlin/sh/ondr/mcp4k/test/integration/CancellationTest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.test.integration 2 | 3 | import kotlinx.coroutines.CancellationException 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.cancel 6 | import kotlinx.coroutines.launch 7 | import kotlinx.coroutines.test.StandardTestDispatcher 8 | import kotlinx.coroutines.test.advanceTimeBy 9 | import kotlinx.coroutines.test.advanceUntilIdle 10 | import kotlinx.coroutines.test.runTest 11 | import kotlinx.serialization.json.JsonPrimitive 12 | import sh.ondr.mcp4k.runtime.Client 13 | import sh.ondr.mcp4k.runtime.Server 14 | import sh.ondr.mcp4k.runtime.transport.ChannelTransport 15 | import sh.ondr.mcp4k.schema.tools.CallToolRequest 16 | import sh.ondr.mcp4k.test.assertLinesMatch 17 | import sh.ondr.mcp4k.test.buildLog 18 | import sh.ondr.mcp4k.test.clientIncoming 19 | import sh.ondr.mcp4k.test.clientOutgoing 20 | import sh.ondr.mcp4k.test.serverIncoming 21 | import sh.ondr.mcp4k.test.serverOutgoing 22 | import sh.ondr.mcp4k.test.tools.slowToolOperation 23 | import kotlin.test.Test 24 | import kotlin.test.fail 25 | 26 | class CancellationTest { 27 | @OptIn(ExperimentalCoroutinesApi::class) 28 | @Test 29 | fun testCancellingSlowToolCall() = 30 | runTest { 31 | val testDispatcher = StandardTestDispatcher(testScheduler) 32 | val log = mutableListOf() 33 | 34 | val clientTransport = ChannelTransport() 35 | val serverTransport = clientTransport.flip() 36 | 37 | val server = Server.Builder() 38 | .withDispatcher(testDispatcher) 39 | .withTool(::slowToolOperation) 40 | .withTransport(serverTransport) 41 | .withTransportLogger( 42 | logIncoming = { msg -> log.add(serverIncoming(msg)) }, 43 | logOutgoing = { msg -> log.add(serverOutgoing(msg)) }, 44 | ) 45 | .build() 46 | server.start() 47 | 48 | val client = Client.Builder() 49 | .withTransport(clientTransport) 50 | .withDispatcher(testDispatcher) 51 | .withTransportLogger( 52 | logIncoming = { msg -> log.add(clientIncoming(msg)) }, 53 | logOutgoing = { msg -> log.add(clientOutgoing(msg)) }, 54 | ) 55 | .withClientInfo("TestClient", "1.0.0") 56 | .build() 57 | client.start() 58 | 59 | client.initialize() 60 | advanceUntilIdle() 61 | log.clear() 62 | 63 | val requestJob = launch { 64 | try { 65 | client.sendRequest { id -> 66 | CallToolRequest( 67 | id = id, 68 | params = CallToolRequest.CallToolParams( 69 | name = "slowToolOperation", 70 | arguments = mapOf("iterations" to JsonPrimitive(20)), 71 | ), 72 | ) 73 | } 74 | fail("We expected to see a cancellation, but the request returned without error!") 75 | } catch (e: CancellationException) { 76 | // This is the normal, expected outcome 77 | } 78 | } 79 | 80 | // Let the server do partial work 81 | advanceTimeBy(600) 82 | 83 | // Cancel from the client side 84 | requestJob.cancel("Client doesn't want to wait anymore") 85 | advanceUntilIdle() 86 | 87 | val expected = buildLog { 88 | addClientOutgoing( 89 | """{"method":"tools/call","jsonrpc":"2.0","id":"2","params":{"name":"slowToolOperation","arguments":{"iterations":20}}}""", 90 | ) 91 | addServerIncoming( 92 | """{"method":"tools/call","jsonrpc":"2.0","id":"2","params":{"name":"slowToolOperation","arguments":{"iterations":20}}}""", 93 | ) 94 | addClientOutgoing( 95 | """{"method":"notifications/cancelled","jsonrpc":"2.0","params":{"requestId":"2","reason":"Client doesn't want to wait anymore"}}""", 96 | ) 97 | addServerIncoming( 98 | """{"method":"notifications/cancelled","jsonrpc":"2.0","params":{"requestId":"2","reason":"Client doesn't want to wait anymore"}}""", 99 | ) 100 | } 101 | 102 | assertLinesMatch( 103 | expected, 104 | log, 105 | "Cancelling slow tool request test", 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /mcp4k-test/src/commonTest/kotlin/sh/ondr/mcp4k/test/integration/RootsTest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.test.integration 2 | 3 | import kotlinx.coroutines.ExperimentalCoroutinesApi 4 | import kotlinx.coroutines.test.StandardTestDispatcher 5 | import kotlinx.coroutines.test.advanceUntilIdle 6 | import kotlinx.coroutines.test.runTest 7 | import sh.ondr.mcp4k.runtime.Client 8 | import sh.ondr.mcp4k.runtime.Server 9 | import sh.ondr.mcp4k.runtime.serialization.deserializeResult 10 | import sh.ondr.mcp4k.runtime.transport.ChannelTransport 11 | import sh.ondr.mcp4k.schema.core.JsonRpcResponse 12 | import sh.ondr.mcp4k.schema.roots.ListRootsRequest 13 | import sh.ondr.mcp4k.schema.roots.ListRootsResult 14 | import sh.ondr.mcp4k.schema.roots.Root 15 | import sh.ondr.mcp4k.test.assertLinesMatch 16 | import sh.ondr.mcp4k.test.buildLog 17 | import sh.ondr.mcp4k.test.clientIncoming 18 | import sh.ondr.mcp4k.test.clientOutgoing 19 | import sh.ondr.mcp4k.test.serverIncoming 20 | import sh.ondr.mcp4k.test.serverOutgoing 21 | import kotlin.test.Test 22 | import kotlin.test.assertEquals 23 | import kotlin.test.assertNotNull 24 | 25 | class RootsTest { 26 | @OptIn(ExperimentalCoroutinesApi::class) 27 | @Test 28 | fun testRootsWorkflow() = 29 | runTest { 30 | val testDispatcher = StandardTestDispatcher(testScheduler) 31 | val log = mutableListOf() 32 | 33 | // 1) Create test transport 34 | val clientTransport = ChannelTransport() 35 | val serverTransport = clientTransport.flip() 36 | 37 | // 2) Build the server 38 | val server = Server.Builder() 39 | .withDispatcher(testDispatcher) 40 | .withTransport(serverTransport) 41 | .withTransportLogger( 42 | logIncoming = { msg -> log.add(serverIncoming(msg)) }, 43 | logOutgoing = { msg -> log.add(serverOutgoing(msg)) }, 44 | ) 45 | .build() 46 | server.start() 47 | 48 | // 3) Build the client with two initial roots 49 | val client = Client.Builder() 50 | .withTransport(clientTransport) 51 | .withDispatcher(testDispatcher) 52 | .withTransportLogger( 53 | logIncoming = { msg -> log.add(clientIncoming(msg)) }, 54 | logOutgoing = { msg -> log.add(clientOutgoing(msg)) }, 55 | ) 56 | .withClientInfo("RootsTestClient", "1.0.0") 57 | .withRoot(Root(uri = "file:///home/user/projectA", name = "Project A")) 58 | .withRoot(Root(uri = "file:///home/user/projectB", name = "Project B")) 59 | .build() 60 | client.start() 61 | 62 | // 4) Perform MCP initialization 63 | client.initialize() 64 | advanceUntilIdle() 65 | log.clear() 66 | 67 | // 5) Server sends a roots/list request 68 | val listRootsResponse1: JsonRpcResponse = server.sendRequest { id -> 69 | ListRootsRequest(id) 70 | } 71 | advanceUntilIdle() 72 | 73 | // Confirm there's no error 74 | assertNotNull(listRootsResponse1.result, "Expected a result from roots/list request.") 75 | assertEquals(null, listRootsResponse1.error, "Should not produce an error for roots/list.") 76 | val listRootsResult1 = listRootsResponse1.result.deserializeResult() 77 | assertNotNull(listRootsResult1, "Expected a valid ListRootsResult") 78 | 79 | // Ensure the client responded with the two initial roots 80 | assertEquals(2, listRootsResult1.roots.size) 81 | val rootUris = listRootsResult1.roots 82 | assertEquals( 83 | listOf( 84 | Root(uri = "file:///home/user/projectA", name = "Project A"), 85 | Root(uri = "file:///home/user/projectB", name = "Project B"), 86 | ), 87 | rootUris, 88 | "Should return the two initial roots.", 89 | ) 90 | log.clear() 91 | 92 | // 6) Remove one root by name from the client, ensuring we get a notification 93 | val removedA = client.removeRootByName("Project A") 94 | assertEquals(true, removedA, "Should be able to remove existing root") 95 | advanceUntilIdle() 96 | 97 | // The client should have sent a notifications/roots/list_changed to the server 98 | val expectedNotification1 = buildLog { 99 | addClientOutgoing( 100 | """{"method":"notifications/roots/list_changed","jsonrpc":"2.0"}""", 101 | ) 102 | addServerIncoming( 103 | """{"method":"notifications/roots/list_changed","jsonrpc":"2.0"}""", 104 | ) 105 | } 106 | assertLinesMatch(expectedNotification1, log, "Check removal notification logs") 107 | log.clear() 108 | 109 | // 7) Now add a new root 110 | val rootC = Root(uri = "file:///home/user/projectC", name = "Project C") 111 | client.addRoot(rootC) 112 | advanceUntilIdle() 113 | 114 | // That again triggers the notification 115 | val expectedNotification2 = buildLog { 116 | addClientOutgoing( 117 | """{"method":"notifications/roots/list_changed","jsonrpc":"2.0"}""", 118 | ) 119 | addServerIncoming( 120 | """{"method":"notifications/roots/list_changed","jsonrpc":"2.0"}""", 121 | ) 122 | } 123 | assertLinesMatch(expectedNotification2, log, "Check addition notification logs") 124 | log.clear() 125 | 126 | // 8) Another roots/list request from the server to confirm the current set of roots 127 | val listRootsResponse2: JsonRpcResponse = server.sendRequest { id -> 128 | ListRootsRequest(id) 129 | } 130 | advanceUntilIdle() 131 | 132 | assertNotNull(listRootsResponse2.result, "Expected a result from second roots/list request.") 133 | assertEquals(null, listRootsResponse2.error, "Should not produce an error on second roots/list.") 134 | val listRootsResult2 = listRootsResponse2.result.deserializeResult() 135 | assertNotNull(listRootsResult2, "Expected a valid ListRootsResult after updates") 136 | 137 | // Should now have 2 roots: projectB (since we removed projectA) and projectC 138 | assertEquals(2, listRootsResult2.roots.size) 139 | val updatedUris = listRootsResult2.roots 140 | assertEquals( 141 | listOf( 142 | Root(uri = "file:///home/user/projectB", name = "Project B"), 143 | rootC, 144 | ), 145 | updatedUris, 146 | "Should have removed projectA, added projectC", 147 | ) 148 | log.clear() 149 | 150 | // 9) Try to remove in-existing root 151 | val removeFail = client.removeRootByName("Project A") 152 | assertEquals(false, removeFail, "Should not be able to remove non-existing root") 153 | 154 | // 10) Remove root by URI 155 | val removedB = client.removeRootByUri("file:///home/user/projectB") 156 | assertEquals(true, removedB, "Should be able to remove existing root") 157 | advanceUntilIdle() 158 | assertEquals(2, log.size, "Should have sent a notification for the removal") 159 | log.clear() 160 | 161 | // 11) Remove root by providing copy instance (should work because we're using data classes) 162 | val removedC = client.removeRoot(rootC.copy()) 163 | assertEquals(true, removedC, "Should be able to remove existing root") 164 | advanceUntilIdle() 165 | assertEquals(2, log.size, "Should have sent a notification for the removal") 166 | log.clear() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /mcp4k-test/src/commonTest/kotlin/sh/ondr/mcp4k/test/integration/SamplingTest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.test.integration 2 | 3 | import kotlinx.coroutines.ExperimentalCoroutinesApi 4 | import kotlinx.coroutines.test.StandardTestDispatcher 5 | import kotlinx.coroutines.test.advanceUntilIdle 6 | import kotlinx.coroutines.test.runTest 7 | import sh.ondr.mcp4k.runtime.Client 8 | import sh.ondr.mcp4k.runtime.Server 9 | import sh.ondr.mcp4k.runtime.sampling.SamplingProvider 10 | import sh.ondr.mcp4k.runtime.serialization.deserializeResult 11 | import sh.ondr.mcp4k.runtime.transport.ChannelTransport 12 | import sh.ondr.mcp4k.schema.content.TextContent 13 | import sh.ondr.mcp4k.schema.core.Role 14 | import sh.ondr.mcp4k.schema.sampling.CreateMessageRequest 15 | import sh.ondr.mcp4k.schema.sampling.CreateMessageRequest.CreateMessageParams 16 | import sh.ondr.mcp4k.schema.sampling.CreateMessageResult 17 | import sh.ondr.mcp4k.schema.sampling.SamplingMessage 18 | import sh.ondr.mcp4k.test.assertLinesMatch 19 | import sh.ondr.mcp4k.test.buildLog 20 | import sh.ondr.mcp4k.test.clientIncoming 21 | import sh.ondr.mcp4k.test.clientOutgoing 22 | import sh.ondr.mcp4k.test.serverIncoming 23 | import sh.ondr.mcp4k.test.serverOutgoing 24 | import kotlin.test.Test 25 | import kotlin.test.assertEquals 26 | import kotlin.test.assertNotNull 27 | 28 | class SamplingTest { 29 | @OptIn(ExperimentalCoroutinesApi::class) 30 | @Test 31 | fun testSamplingRequestFlow() = 32 | runTest { 33 | val testDispatcher = StandardTestDispatcher(testScheduler) 34 | val log = mutableListOf() 35 | 36 | // 1) Build a dummy sampling provider that just returns a fixed text 37 | val dummyProvider = SamplingProvider { params -> 38 | CreateMessageResult( 39 | model = "dummy-model", 40 | role = Role.ASSISTANT, 41 | content = TextContent("Dummy completion result"), 42 | stopReason = "endTurn", 43 | ) 44 | } 45 | 46 | // 2) Create test transport 47 | val clientTransport = ChannelTransport() 48 | val serverTransport = clientTransport.flip() 49 | 50 | // 3) Build the server 51 | val server = Server.Builder() 52 | .withServerInfo("TestServer", "1.0.0") 53 | .withDispatcher(testDispatcher) 54 | .withTransport(serverTransport) 55 | .withTransportLogger( 56 | logIncoming = { msg -> log.add(serverIncoming(msg)) }, 57 | logOutgoing = { msg -> log.add(serverOutgoing(msg)) }, 58 | ) 59 | .build() 60 | server.start() 61 | 62 | // 4) Build the client, with sampling provider & sampling capabilities 63 | val client = Client.Builder() 64 | .withClientInfo("TestClient", "1.0.0") 65 | .withDispatcher(testDispatcher) 66 | .withPermissionCallback { userApprovable -> true } 67 | .withSamplingProvider(dummyProvider) 68 | .withTransport(clientTransport) 69 | .withTransportLogger( 70 | logIncoming = { msg -> log.add(clientIncoming(msg)) }, 71 | logOutgoing = { msg -> log.add(clientOutgoing(msg)) }, 72 | ) 73 | .build() 74 | client.start() 75 | 76 | // 5) Perform MCP initialization and check capabilities 77 | client.initialize() 78 | advanceUntilIdle() 79 | 80 | val expectedInitLogs = buildLog { 81 | addClientOutgoing( 82 | """{"method":"initialize","jsonrpc":"2.0","id":"1","params":{"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"TestClient","version":"1.0.0"}}}""", 83 | ) 84 | addServerIncoming( 85 | """{"method":"initialize","jsonrpc":"2.0","id":"1","params":{"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"TestClient","version":"1.0.0"}}}""", 86 | ) 87 | addServerOutgoing( 88 | """{"jsonrpc":"2.0","id":"1","result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"TestServer","version":"1.0.0"}}}""", 89 | ) 90 | addClientIncoming( 91 | """{"jsonrpc":"2.0","id":"1","result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"TestServer","version":"1.0.0"}}}""", 92 | ) 93 | addClientOutgoing( 94 | """{"method":"notifications/initialized","jsonrpc":"2.0"}""", 95 | ) 96 | addServerIncoming( 97 | """{"method":"notifications/initialized","jsonrpc":"2.0"}""", 98 | ) 99 | } 100 | assertLinesMatch(expectedInitLogs, log, "Check sampling in init handshake") 101 | log.clear() 102 | 103 | // 6) Request sampling from client 104 | val samplingRequest = server.sendRequest { id -> 105 | CreateMessageRequest( 106 | id = id, 107 | params = CreateMessageParams( 108 | messages = listOf( 109 | SamplingMessage( 110 | role = Role.USER, 111 | content = TextContent("Hello from the server test"), 112 | ), 113 | ), 114 | maxTokens = 50, 115 | ), 116 | ) 117 | } 118 | advanceUntilIdle() 119 | 120 | // 7) Check that the server received a response with no error 121 | assertNotNull(samplingRequest.result, "Expected a result from sampling request.") 122 | assertEquals(null, samplingRequest.error, "Sampling request should not produce an error.") 123 | 124 | // 8) Parse the result into CreateMessageResult 125 | val createMsgResult = samplingRequest.result.deserializeResult() 126 | assertNotNull(createMsgResult, "Expected a valid CreateMessageResult.") 127 | assertEquals("dummy-model", createMsgResult.model) 128 | val text = (createMsgResult.content as? TextContent)?.text 129 | assertEquals("Dummy completion result", text) 130 | 131 | val expected = buildLog { 132 | addServerOutgoing( 133 | """{"method":"sampling/createMessage","jsonrpc":"2.0","id":"1","params":{"messages":[{"role":"user","content":{"type":"text","text":"Hello from the server test"}}],"maxTokens":50}}""", 134 | ) 135 | addClientIncoming( 136 | """{"method":"sampling/createMessage","jsonrpc":"2.0","id":"1","params":{"messages":[{"role":"user","content":{"type":"text","text":"Hello from the server test"}}],"maxTokens":50}}""", 137 | ) 138 | addClientOutgoing( 139 | """{"jsonrpc":"2.0","id":"1","result":{"content":{"type":"text","text":"Dummy completion result"},"model":"dummy-model","role":"assistant","stopReason":"endTurn"}}""", 140 | ) 141 | addServerIncoming( 142 | """{"jsonrpc":"2.0","id":"1","result":{"content":{"type":"text","text":"Dummy completion result"},"model":"dummy-model","role":"assistant","stopReason":"endTurn"}}""", 143 | ) 144 | } 145 | 146 | assertLinesMatch(expected, log, "sampling test") 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /mcp4k-test/src/commonTest/kotlin/sh/ondr/mcp4k/test/integration/ServerContextTest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.test.integration 2 | 3 | import kotlinx.coroutines.ExperimentalCoroutinesApi 4 | import kotlinx.coroutines.test.StandardTestDispatcher 5 | import kotlinx.coroutines.test.advanceUntilIdle 6 | import kotlinx.coroutines.test.runTest 7 | import kotlinx.serialization.json.JsonPrimitive 8 | import sh.ondr.mcp4k.runtime.Client 9 | import sh.ondr.mcp4k.runtime.Server 10 | import sh.ondr.mcp4k.runtime.serialization.deserializeResult 11 | import sh.ondr.mcp4k.runtime.transport.ChannelTransport 12 | import sh.ondr.mcp4k.schema.content.TextContent 13 | import sh.ondr.mcp4k.schema.core.JsonRpcResponse 14 | import sh.ondr.mcp4k.schema.tools.CallToolRequest 15 | import sh.ondr.mcp4k.schema.tools.CallToolRequest.CallToolParams 16 | import sh.ondr.mcp4k.schema.tools.CallToolResult 17 | import sh.ondr.mcp4k.test.assertLinesMatch 18 | import sh.ondr.mcp4k.test.buildLog 19 | import sh.ondr.mcp4k.test.clientIncoming 20 | import sh.ondr.mcp4k.test.clientOutgoing 21 | import sh.ondr.mcp4k.test.serverIncoming 22 | import sh.ondr.mcp4k.test.serverOutgoing 23 | import sh.ondr.mcp4k.test.tools.RemoteServiceImpl 24 | import sh.ondr.mcp4k.test.tools.storeValueInContext 25 | import kotlin.test.Test 26 | import kotlin.test.assertEquals 27 | import kotlin.test.assertNotNull 28 | 29 | class ServerContextTest { 30 | @OptIn(ExperimentalCoroutinesApi::class) 31 | @Test 32 | fun testContextToolFunction() = 33 | runTest { 34 | val testDispatcher = StandardTestDispatcher(testScheduler) 35 | val log = mutableListOf() 36 | 37 | // 1) Create our context object 38 | val remoteService = RemoteServiceImpl() 39 | 40 | // 2) Create test transports 41 | val clientTransport = ChannelTransport() 42 | val serverTransport = clientTransport.flip() 43 | 44 | // 3) Build the server with context + our tool 45 | val server = Server.Builder() 46 | .withDispatcher(testDispatcher) 47 | .withContext(remoteService) // <--- Pass in the context 48 | .withTool(Server::storeValueInContext) // <--- Register our tool that uses the context 49 | .withTransport(serverTransport) 50 | .withTransportLogger( 51 | logIncoming = { msg -> log.add(serverIncoming(msg)) }, 52 | logOutgoing = { msg -> log.add(serverOutgoing(msg)) }, 53 | ) 54 | .build() 55 | server.start() 56 | 57 | // 4) Build the client 58 | val client = Client.Builder() 59 | .withDispatcher(testDispatcher) 60 | .withTransport(clientTransport) 61 | .withTransportLogger( 62 | logIncoming = { msg -> log.add(clientIncoming(msg)) }, 63 | logOutgoing = { msg -> log.add(clientOutgoing(msg)) }, 64 | ) 65 | .withClientInfo("TestClient", "1.0.0") 66 | .build() 67 | client.start() 68 | 69 | // 5) Perform MCP initialization 70 | client.initialize() 71 | advanceUntilIdle() 72 | log.clear() 73 | 74 | // 6) Call our tool "storeValueInContext" 75 | val response: JsonRpcResponse = client.sendRequest { requestId -> 76 | CallToolRequest( 77 | id = requestId, 78 | params = CallToolParams( 79 | name = "storeValueInContext", 80 | arguments = mapOf("newValue" to JsonPrimitive("Hello, context!")), 81 | ), 82 | ) 83 | } 84 | advanceUntilIdle() 85 | 86 | // 7) Verify the server side was updated 87 | val storedValue = remoteService.value 88 | assertNotNull(storedValue, "Expected the server context to store a value.") 89 | assertEquals("Hello, context!", storedValue) 90 | 91 | // 8) Verify the returned response content 92 | val callToolResult = response.result?.deserializeResult() 93 | assertNotNull(callToolResult, "Expected non-null tool result.") 94 | assertEquals(1, callToolResult.content.size) 95 | val text = (callToolResult.content.first() as? TextContent)?.text 96 | assertEquals("Value updated from 'Initial value' to 'Hello, context!'", text) 97 | 98 | // 9) Optionally verify logs 99 | val expectedLogs = buildLog { 100 | addClientOutgoing( 101 | """{"method":"tools/call","jsonrpc":"2.0","id":"2","params":{"name":"storeValueInContext","arguments":{"newValue":"Hello, context!"}}}""", 102 | ) 103 | addServerIncoming( 104 | """{"method":"tools/call","jsonrpc":"2.0","id":"2","params":{"name":"storeValueInContext","arguments":{"newValue":"Hello, context!"}}}""", 105 | ) 106 | addServerOutgoing( 107 | """{"jsonrpc":"2.0","id":"2","result":{"content":[{"type":"text","text":"Value updated from 'Initial value' to 'Hello, context!'"}]}}""", 108 | ) 109 | addClientIncoming( 110 | """{"jsonrpc":"2.0","id":"2","result":{"content":[{"type":"text","text":"Value updated from 'Initial value' to 'Hello, context!'"}]}}""", 111 | ) 112 | } 113 | assertLinesMatch(expectedLogs, log, "Check logs for context-based tool usage") 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /mcp4k-test/src/commonTest/kotlin/sh/ondr/mcp4k/test/schema/JsonRpcMessageTest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.test.schema 2 | 3 | import kotlinx.serialization.json.jsonPrimitive 4 | import sh.ondr.mcp4k.runtime.serialization.toJsonRpcMessage 5 | import sh.ondr.mcp4k.schema.tools.CallToolRequest 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertIs 9 | import kotlin.test.assertTrue 10 | 11 | class JsonRpcMessageTest { 12 | @Test 13 | fun testCallToolRequestDeserialization() { 14 | val input = """{ 15 | "jsonrpc":"2.0", 16 | "id":"1", 17 | "method":"tools/call", 18 | "params":{ 19 | "name":"sendEmail", 20 | "arguments":{ 21 | "recipient":"me@test.com", 22 | "title":"Test", 23 | "body":"Hello" 24 | } 25 | } 26 | }""" 27 | 28 | val message = input.toJsonRpcMessage() 29 | assertTrue(message is CallToolRequest, "Expected a CallToolRequest") 30 | val request = message 31 | assertEquals("1", request.id) 32 | assertIs(request) 33 | assertEquals("sendEmail", request.params.name) 34 | assertEquals("me@test.com", request.params.arguments!!["recipient"]?.jsonPrimitive?.content) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mcp4k-test/src/commonTest/kotlin/sh/ondr/mcp4k/test/transport/ChannelTransportTest.kt: -------------------------------------------------------------------------------- 1 | package sh.ondr.mcp4k.test.transport 2 | 3 | import kotlinx.coroutines.test.runTest 4 | import sh.ondr.mcp4k.runtime.transport.ChannelTransport 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | class ChannelTransportTest { 9 | @Test 10 | fun testCommunication() = 11 | runTest { 12 | val clientTransport = ChannelTransport() 13 | val serverTransport = clientTransport.flip() 14 | 15 | // Client writes a message 16 | clientTransport.writeString("Hello, Server!") 17 | 18 | // Server reads the message 19 | val msg = serverTransport.readString() 20 | assertEquals("Hello, Server!", msg) 21 | 22 | // Server responds 23 | serverTransport.writeString("Hello, Client!") 24 | val response = clientTransport.readString() 25 | assertEquals("Hello, Client!", response) 26 | 27 | // Close both 28 | clientTransport.close() 29 | serverTransport.close() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | dependencyResolutionManagement { 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | versionCatalogs { 14 | // Configure the existing libs catalog 15 | configureEach { 16 | if (name == "libs") { 17 | // Allow overriding Kotlin version for testing 18 | val kotlinVersionOverride = providers.gradleProperty("test.kotlin.version") 19 | .orElse(providers.environmentVariable("TEST_KOTLIN_VERSION")) 20 | 21 | if (kotlinVersionOverride.isPresent) { 22 | val versionValue = kotlinVersionOverride.get() 23 | println("Overriding Kotlin version to $versionValue for testing") 24 | 25 | // Override the kotlin version 26 | version("kotlin", versionValue) 27 | 28 | // Also need to update the plugin versions to match 29 | plugin("kotlin-multiplatform", "org.jetbrains.kotlin.multiplatform").version(versionValue) 30 | plugin("kotlin-jvm", "org.jetbrains.kotlin.jvm").version(versionValue) 31 | plugin("kotlin-serialization", "org.jetbrains.kotlin.plugin.serialization").version(versionValue) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | rootProject.name = "mcp4k" 39 | include("mcp4k-compiler") 40 | include("mcp4k-file-provider") 41 | include("mcp4k-gradle") 42 | include("mcp4k-ksp") 43 | include("mcp4k-runtime") 44 | include("mcp4k-test") 45 | 46 | includeBuild("mcp4k-build") { 47 | dependencySubstitution { 48 | substitute(module("sh.ondr.mcp4k:mcp4k-gradle")).using(project(":gradle-plugin")) 49 | } 50 | } 51 | --------------------------------------------------------------------------------